diff --git a/core/libs/prx/libSceAgc/Acb/src/Control.cpp b/core/libs/prx/libSceAgc/Acb/src/Control.cpp index 567bfd5e18..1dd0314800 100644 --- a/core/libs/prx/libSceAgc/Acb/src/Control.cpp +++ b/core/libs/prx/libSceAgc/Acb/src/Control.cpp @@ -102,6 +102,18 @@ uint32_t* APS5_VABI sceAgcAcbPushMarker(CommandBuffer* buf, const char* str, uin return Agc::Marker::Push(buf, str, __func__); } +uint32_t* APS5_VABI sceAgcAcbPushMarkerSpan(CommandBuffer* buf, const char* str, uint32_t length, uint32_t color) { + (void)color; + return Agc::Marker::PushSpan(buf, str, length, __func__); +} + +uint32_t* APS5_VABI sceAgcAcbSetMarkerSpan(CommandBuffer* buf, const char* str, uint32_t length, uint32_t color) { + (void)color; + auto* packet = Agc::Marker::PushSpan(buf, str, length, __func__); + Agc::Marker::Pop(buf, __func__); + return packet; +} + uint32_t* APS5_VABI sceAgcAcbPopMarker(CommandBuffer* buf) { return Agc::Marker::Pop(buf, __func__); } diff --git a/core/libs/prx/libSceAgc/DcbState/include/Marker.hpp b/core/libs/prx/libSceAgc/DcbState/include/Marker.hpp index c0ccfdea02..688c0a80b2 100644 --- a/core/libs/prx/libSceAgc/DcbState/include/Marker.hpp +++ b/core/libs/prx/libSceAgc/DcbState/include/Marker.hpp @@ -7,6 +7,7 @@ namespace Agc::Marker { std::uint32_t* Push(CommandBuffer* buf, const char* str, const char* function); +std::uint32_t* PushSpan(CommandBuffer* buf, const char* str, std::uint32_t length, const char* function); std::uint32_t* Pop(CommandBuffer* buf, const char* function); } diff --git a/core/libs/prx/libSceAgc/DcbState/src/Marker.cpp b/core/libs/prx/libSceAgc/DcbState/src/Marker.cpp index 7e88856ccc..12304f16a3 100644 --- a/core/libs/prx/libSceAgc/DcbState/src/Marker.cpp +++ b/core/libs/prx/libSceAgc/DcbState/src/Marker.cpp @@ -22,13 +22,21 @@ namespace Agc::Marker { std::uint32_t* Push(CommandBuffer* buf, const char* str, const char* function) { Agc::Command::Require(buf != nullptr, function, "null command buffer"); const char* text = str ? str : ""; - const std::size_t bytes = std::strlen(text) + 1; + const std::size_t length = std::strlen(text); + Agc::Command::Require(length < 0x10000u, function, "marker text too long"); + return PushSpan(buf, text, static_cast(length), function); +} + +std::uint32_t* PushSpan(CommandBuffer* buf, const char* str, std::uint32_t length, const char* function) { + Agc::Command::Require(buf != nullptr, function, "null command buffer"); + Agc::Command::Require(str != nullptr || length == 0, function, "null marker text"); + const std::size_t bytes = std::size_t{length} + 1; const auto payload = static_cast((bytes + 3) / 4); Agc::Command::Require(payload <= 0x4000u, function, "marker text too long"); auto* packet = Agc::Command::Allocate(buf, payload + 1, function); packet[0] = Agc::Command::Header(OpcodeNop, payload + 1, CustomPushMarker << 2); std::memset(packet + 1, 0, payload * sizeof(std::uint32_t)); - std::memcpy(packet + 1, text, bytes); + if (length != 0) std::memcpy(packet + 1, str, length); return packet; } @@ -60,4 +68,16 @@ uint32_t* APS5_VABI sceAgcDcbPushMarker(CommandBuffer* buf, const char* str, uin return Agc::Marker::Push(buf, str, __func__); } +uint32_t* APS5_VABI sceAgcDcbPushMarkerSpan(CommandBuffer* buf, const char* str, uint32_t length, uint32_t color) { + (void)color; + return Agc::Marker::PushSpan(buf, str, length, __func__); +} + +uint32_t* APS5_VABI sceAgcDcbSetMarkerSpan(CommandBuffer* buf, const char* str, uint32_t length, uint32_t color) { + (void)color; + auto* packet = Agc::Marker::PushSpan(buf, str, length, __func__); + Agc::Marker::Pop(buf, __func__); + return packet; +} + } diff --git a/core/libs/prx/libSceAgc/Unimplemented.cpp b/core/libs/prx/libSceAgc/Unimplemented.cpp index e01ddfe0d1..487d737193 100644 --- a/core/libs/prx/libSceAgc/Unimplemented.cpp +++ b/core/libs/prx/libSceAgc/Unimplemented.cpp @@ -5,31 +5,11 @@ extern "C" { -std::uint32_t* APS5_VABI sceAgcAcbPushMarkerSpan(CommandBuffer* buffer, const char* text, std::uint32_t length, std::uint32_t color) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -std::uint32_t* APS5_VABI sceAgcAcbSetMarkerSpan(CommandBuffer* buffer, const char* text, std::uint32_t length, std::uint32_t color) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAgcAcquireMemSetEngine(std::uint32_t* packet, std::uint8_t engine) { NotImplemented_nid_no_patch(__func__); return 0; } -std::uint32_t* APS5_VABI sceAgcDcbPushMarkerSpan(CommandBuffer* buffer, const char* text, std::uint32_t length, std::uint32_t color) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -std::uint32_t* APS5_VABI sceAgcDcbSetMarkerSpan(CommandBuffer* buffer, const char* text, std::uint32_t length, std::uint32_t color) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - std::uint32_t* APS5_VABI sceAgcCbMemsetExclusive(CommandBuffer* buffer, void* destination, const void* pattern, std::uint64_t size) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index e497ed1fa5..bcda442f6b 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -48,6 +48,10 @@ extern "C" std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadStreamInactive(CommandBu extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetMarker(CommandBuffer* buf, const char* str, std::uint32_t color); extern "C" std::uint32_t* APS5_VABI sceAgcAcbPushMarker(CommandBuffer* buf, const char* str, std::uint32_t color); extern "C" std::uint32_t* APS5_VABI sceAgcAcbPopMarker(CommandBuffer* buf); +extern "C" std::uint32_t* APS5_VABI sceAgcAcbPushMarkerSpan(CommandBuffer* buf, const char* str, std::uint32_t length, std::uint32_t color); +extern "C" std::uint32_t* APS5_VABI sceAgcAcbSetMarkerSpan(CommandBuffer* buf, const char* str, std::uint32_t length, std::uint32_t color); +extern "C" std::uint32_t* APS5_VABI sceAgcDcbPushMarkerSpan(CommandBuffer* buf, const char* str, std::uint32_t length, std::uint32_t color); +extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetMarkerSpan(CommandBuffer* buf, const char* str, std::uint32_t length, std::uint32_t color); extern "C" std::uint32_t* APS5_VABI sceAgcAcbSetMarker(CommandBuffer* buf, const char* str, std::uint32_t color); extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetIndexBuffer(CommandBuffer* buf, std::uint64_t indexAddress); extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetIndexIndirectArgs(CommandBuffer* buf, std::uint64_t attributeAddress, std::uint32_t attributeIndex); @@ -225,6 +229,29 @@ void testMarkers() { expectFailure([] { sceAgcAcbSetMarker(nullptr, "frame", 0); }); } +void testMarkerSpans() { + Storage dcb; + Storage acb; + const char text[] = "framebuffer"; + const auto* acbPush = sceAgcAcbPushMarkerSpan(&acb.buffer, text, 5, 0x00ff00u); + const auto* dcbPush = sceAgcDcbPushMarkerSpan(&dcb.buffer, text, 5, 0xff0000u); + check(acbPush == acb.words.data() && acbPush[0] == Agc::Command::Header(0x10, 3, 0x0bu << 2u), "ACB push marker span header mismatch"); + check(std::strcmp(reinterpret_cast(acbPush + 1), "frame") == 0, "ACB push marker span does not stop at its length"); + check(dcbPush == dcb.words.data(), "DCB push marker span did not return its packet"); + const auto* acbSet = sceAgcAcbSetMarkerSpan(&acb.buffer, nullptr, 0, 0); + const auto* dcbSet = sceAgcDcbSetMarkerSpan(&dcb.buffer, nullptr, 0, 0); + check(acbSet == acb.words.data() + 3 && dcbSet == dcb.words.data() + 3, "set marker span did not return its push packet"); + check(acbSet[0] == Agc::Command::Header(0x10, 2, 0x0bu << 2u) && acbSet[1] == 0 && acbSet[2] == Agc::Command::Header(0x10, 2, 0x0cu << 2u), "ACB set marker span is not a push and pop pair"); + check(dcb.words == acb.words && acb.buffer.cursor_up == acb.words.data() + 7, "ACB and DCB marker spans differ"); + Storage plain; + sceAgcAcbPushMarker(&plain.buffer, "frame", 0); + check(std::equal(plain.words.begin(), plain.words.begin() + 3, acb.words.begin()), "marker span differs from the same text pushed whole"); + expectFailure([] { sceAgcAcbPushMarkerSpan(nullptr, "frame", 5, 0); }); + expectFailure([] { sceAgcAcbPushMarker(nullptr, reinterpret_cast(std::uintptr_t{0x10}), 0); }); + expectFailure([] { sceAgcDcbSetMarkerSpan(nullptr, "frame", 5, 0); }); + expectFailure([&] { sceAgcDcbPushMarkerSpan(&dcb.buffer, nullptr, 4, 0); }); +} + void testWorkloads() { Storage dcb; const std::uint32_t ids[2] = {3, 4}; @@ -822,6 +849,7 @@ int main(int argc, char** argv) { testClearState(); testIndexedIndirectDraws(); testMarkers(); + testMarkerSpans(); testWorkloads(); testWorkloadStreams(); testIndexBuffer(); diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index aba0b27f09..584d279f06 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2164,6 +2164,9 @@ if(NOT ANYPS5_AGC_EXECUTION_TESTS OR "PooledBufferRange" IN_LIST ANYPS5_AGC_EXEC endif() target_link_libraries(agc_driver_gpu_map_on_fiber_tests PRIVATE libSceFiber) +if(ANYPS5_ENABLE_SPIRV_TOOLS) + target_link_libraries(agc_driver_optimize_on_fiber_tests PRIVATE libSceFiber shader_recompiler_agc_driver) +endif() target_link_libraries(agc_driver_rect_list_corner_tests PRIVATE shader_recompiler_agc_driver) target_link_libraries(agc_driver_queue_zero_late_interrupt_tests PRIVATE libkernel) @@ -2304,6 +2307,8 @@ target_include_directories(agc_driver_packed_unorm4444_multisample_partial_write target_include_directories(agc_driver_color1555_target_tests PRIVATE $) +target_include_directories(agc_driver_color_logic_op_tests PRIVATE $) + target_include_directories(agc_driver_color565_target_tests PRIVATE $) target_include_directories(agc_driver_depth_range_remap_draw_tests PRIVATE $) @@ -2500,6 +2505,14 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_sparse_indirect_indices_unreadable PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_driver_draw_cache_relocation_evicted COMMAND agc_driver_draw_cache_relocation_tests --evicted) + add_test(NAME agc_driver_draw_cache_relocation_kept COMMAND agc_driver_draw_cache_relocation_tests --kept) + set_tests_properties(agc_driver_draw_cache_relocation_evicted agc_driver_draw_cache_relocation_kept PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_DRAW_CACHE_ENTRIES=2" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_draw_cache_relocation_evicted agc_driver_draw_cache_relocation_kept PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() set_tests_properties(agc_driver_pixel_implicit_derivatives PROPERTIES TIMEOUT 30) set_tests_properties(agc_driver_storage_file_write PROPERTIES TIMEOUT 30 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_SUSPEND_DRAIN=1") set_tests_properties(agc_driver_clamped_global_fill PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_LABEL_READBACK_STORE=1") diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index 4ed2b8471c..01d318202e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -416,7 +416,7 @@ private: std::unordered_set traceShapes, traceKeysSeen, traceKeysEvicted; std::list drawOrder; std::unordered_map> drawRelocations; - std::unordered_map> drawAliases; + std::unordered_map drawAliases; DrawRelocationPending drawRelocationPending; std::unordered_map drawRelocationTrials; DrawRelocationCounters drawRelocationCounters; 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 175c487d29..47cac201b9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -9,6 +9,7 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" +#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "Optimization/ResourceProgram.hpp" #include "prx/libc/include/HostThreadLocal.hpp" #include @@ -46,7 +47,8 @@ struct DrawTail { std::shared_ptr dataEntry; std::uint64_t shapeKey = 0; ShaderRecompiler::ShaderPixelStageInfo pixel{}; - std::vector programs; + std::vector writablePrograms; + const std::vector* programs = nullptr; std::vector memory; std::vector linked; std::vector> vertexInfos; @@ -94,7 +96,7 @@ void noteFinisherHandOff(bool handed) { if (!stats) return; const auto tenSeconds = std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count() / 10; if (reportedAt.exchange(tenSeconds, std::memory_order_relaxed) == tenSeconds) return; - std::fprintf(stderr, "[finisher] draws (10 s): %llu handed to the draw finisher, %llu finished on the worker%s\n", static_cast(handedOff.exchange(0)), static_cast(kept.exchange(0)), DrawFinisher::DrainText().c_str()); + AgcDriver::ProfilePrint_nid_no_patch("[finisher] draws (10 s): %llu handed to the draw finisher, %llu finished on the worker%s\n", static_cast(handedOff.exchange(0)), static_cast(kept.exchange(0)), DrawFinisher::DrainText().c_str()); } } @@ -147,7 +149,7 @@ DrawFinisher::Pending heldWrites(const DrawTail& tail) { noteHeld(HeldReason::NoDecode); return held; } - if (tail.matched.size() != tail.programs.size()) { + if (tail.matched.size() != tail.programs->size()) { noteHeld(HeldReason::Size); return held; } @@ -157,10 +159,10 @@ DrawFinisher::Pending heldWrites(const DrawTail& tail) { const auto& variant = tail.matched[i]; if (variant == nullptr || variant->compiled == nullptr) { const bool candidates = !tail.drawHit && i < tail.dataCandidates.size() && !tail.dataCandidates[i].empty(); - const auto& program = tail.programs[i]; + const auto& program = (*tail.programs)[i]; if (!candidates && program.snapshot != nullptr && !ProgramWritesMemory(*program.snapshot, program.codeOffset) && (program.tailContinuation == nullptr || !ProgramWritesMemory(*program.tailContinuation, 0))) continue; if (!candidates) { - const auto stage = tail.programs[i].binary.stage; + const auto stage = (*tail.programs)[i].binary.stage; noteHeld(stage == ShaderRecompiler::ShaderStage::Fragment ? HeldReason::NullFragment : stage == ShaderRecompiler::ShaderStage::Vertex ? HeldReason::NullVertex : HeldReason::NullOther); return held; } @@ -395,9 +397,12 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe const auto& graphics = decode->state; auto& pixel = tail.pixel; pixel = decode->pixel; - auto& programs = tail.programs; - programs = decode->programs; - for (const auto& patch : userPatches) programs[patch.program].userData[patch.index] = patch.value; + const bool writesPrograms = graphics.stages.mesh || drawParameters.indirect || !userPatches.empty(); + auto& writablePrograms = tail.writablePrograms; + if (writesPrograms) writablePrograms = decode->programs; + for (const auto& patch : userPatches) writablePrograms[patch.program].userData[patch.index] = patch.value; + tail.programs = writesPrograms ? &writablePrograms : &decode->programs; + const auto& programs = *tail.programs; if (const auto skipped = SkippedShader(); skipped != 0 && std::any_of(programs.begin(), programs.end(), [skipped](const DrawProgram& program) { return program.binary.codeAddress == skipped; })) return drawn(); const bool emptyIndirectSkip = EmptyIndirectSkip(); auto& cpuIndirectPrograms = CpuIndirectPrograms(); @@ -420,7 +425,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe } const auto setMeshIndexBuffer = [&](const Pm4::DrawParameters& parameters) { if (!graphics.stages.mesh) return; - auto& words = programs.front().userData; + auto& words = writablePrograms.front().userData; require(programs.front().firstUserSgpr == 0 && words.size() >= ShaderRecompiler::MeshIndexBufferUserWord + 4, "mesh program lacks the hidden user words"); const auto descriptor = Graphics::MeshIndexBufferDescriptor(parameters); std::copy(descriptor.begin(), descriptor.end(), words.begin() + ShaderRecompiler::MeshIndexBufferUserWord); @@ -497,7 +502,6 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe auto& compiledStages = tail.compiledStages; compiledStages.reserve(programs.size()); auto& stages = tail.stages; - results.reserve(programs.size() + 2u); stages.reserve(programs.size()); auto& pushCursorBytes = tail.pushCursorBytes; @@ -538,6 +542,14 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe if (drawHit || verifyHit) { ++drawRelocationCounters.hits; ++drawRelocationTrials[drawIdentity].hits; + if (const auto alias = drawAliases.find(drawAlias); alias != drawAliases.end()) { + const auto donor = drawCache.find(alias->second); + if (donor != drawCache.end() && drawCacheHits - donor->second->touched > drawCacheEntries() / 8) { + drawOrder.splice(drawOrder.begin(), drawOrder, donor->second->order); + donor->second->touched = drawCacheHits; + ++drawEntryCounters.touches; + } + } } if (profile && drawRelocationCounters.lookups >= 100000) reportDrawRelocation(); } @@ -640,7 +652,8 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) { auto& shapeKey = tail.shapeKey; const auto& graphics = decode->state; auto& pixel = tail.pixel; - auto& programs = tail.programs; + auto& writablePrograms = tail.writablePrograms; + const auto& programs = *tail.programs; const std::vector& roles = decode->roles; auto& memory = tail.memory; auto& linked = tail.linked; @@ -682,7 +695,7 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) { }; const auto setMeshIndexBuffer = [&](const Pm4::DrawParameters& parameters) { if (!graphics.stages.mesh) return; - auto& words = programs.front().userData; + auto& words = writablePrograms.front().userData; require(programs.front().firstUserSgpr == 0 && words.size() >= ShaderRecompiler::MeshIndexBufferUserWord + 4, "mesh program lacks the hidden user words"); const auto descriptor = Graphics::MeshIndexBufferDescriptor(parameters); std::copy(descriptor.begin(), descriptor.end(), words.begin() + ShaderRecompiler::MeshIndexBufferUserWord); @@ -853,6 +866,7 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) { return; } require(stages.size() == 2, "rect-list requires vertex and fragment programs"); + results.reserve(programs.size() + 2u); rectIndex = results.size(); results.push_back(std::move(control)); results.push_back(std::move(rectangle.evaluation)); @@ -878,6 +892,7 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) { } else if (!graphics.rectList && !programs.empty() && programResults.back() != nullptr && programResults.back()->barycentricEmulation.active) { require(graphics.stages.path == Graphics::ShaderPath::Vertex && programs.size() == 2 && programResults[0] != nullptr && stages.size() == 2, "a pixel shader that reads barycentrics without VK_KHR_fragment_shader_barycentric needs a vertex shader before it"); const auto geometryLimits = localDevice->GeometryLimits(); + results.reserve(programs.size() + 2u); if (geometryLimits || !localDevice->Target().tessellation) { results.push_back(ShaderRecompiler::BuildBarycentricGeometryShader(*programResults[0], *programResults[1], localDevice->Target(), geometryLimits)); stages.insert(stages.begin() + 1, Graphics::CompiledShader{Stage::Geometry, &results.back(), 0}); @@ -919,6 +934,7 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) { if (drawParameters.indirect && indirectCpu) { const auto indirect = *drawParameters.indirect; + results.reserve(programs.size() + 2u); for (std::size_t i = 0; i < programs.size(); ++i) { if (programResults[i] == nullptr) continue; resultIndex[i] = results.size(); @@ -949,7 +965,7 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) { std::set patched; const auto patch = [&](const std::vector>& words, std::uint32_t value) { for (const auto& [program, word] : words) { - programs[program].userData[word] = value; + writablePrograms[program].userData[word] = value; patched.insert(program); } }; @@ -996,7 +1012,7 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) { const auto commitQueue = submission.queue; const auto commitPacket = tail.packetOpcode; const auto epoch = DrawPipeline::EpochToken().load(std::memory_order_relaxed); - auto commit = [this, localDevice = std::move(localDevice), decode = std::move(decode), direct, stages = std::move(stages), snapshots = std::move(snapshots), commitQueue, commitPacket, epoch, programs = std::move(programs), results = std::move(results), memory = std::move(memory), linked = std::move(linked), stageCaptures = std::move(stageCaptures), matched = std::move(matched), matchedRegions = std::move(matchedRegions), fresh = std::move(fresh), decodeReads = std::move(decodeReads), shaderMemory = std::move(shaderMemory), entry = std::move(entry), dataEntry = std::move(dataEntry), compiledStages = std::move(compiledStages), stageData = std::move(stageData)] { + auto commit = [this, localDevice = std::move(localDevice), decode = std::move(decode), direct, stages = std::move(stages), snapshots = std::move(snapshots), commitQueue, commitPacket, epoch, writablePrograms = std::move(writablePrograms), results = std::move(results), memory = std::move(memory), linked = std::move(linked), stageCaptures = std::move(stageCaptures), matched = std::move(matched), matchedRegions = std::move(matchedRegions), fresh = std::move(fresh), decodeReads = std::move(decodeReads), shaderMemory = std::move(shaderMemory), entry = std::move(entry), dataEntry = std::move(dataEntry), compiledStages = std::move(compiledStages), stageData = std::move(stageData)] { DrawPipeline::FollowEpoch(epoch); GuestMemory::SetCurrentPacket(commitPacket, commitQueue); commitDraw(localDevice, commitQueue, decode->state, direct, stages, snapshots, nullptr, {}, 0); @@ -1042,7 +1058,7 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) { const auto commitPacket = tail.packetOpcode; const auto epoch = DrawFinisher::OnFinisherThread() ? tail.epoch : DrawPipeline::EpochToken().load(std::memory_order_relaxed); auto reads = QueuedDrawReadRanges(drawParameters, stages); - auto commit = [this, localDevice = std::move(localDevice), decode = std::move(decode), drawParameters, stages = std::move(stages), snapshots = std::move(snapshots), recipe = std::move(recipe), recipeStages = std::move(recipeStages), drawKey, commitQueue, commitPacket, epoch, programs = std::move(programs), results = std::move(results), memory = std::move(memory), linked = std::move(linked), stageCaptures = std::move(stageCaptures), matched = std::move(matched), matchedRegions = std::move(matchedRegions), fresh = std::move(fresh), decodeReads = std::move(decodeReads), shaderMemory = std::move(shaderMemory), entry = std::move(entry), dataEntry = std::move(dataEntry), compiledStages = std::move(compiledStages), stageData = std::move(stageData)]() mutable { + auto commit = [this, localDevice = std::move(localDevice), decode = std::move(decode), drawParameters, stages = std::move(stages), snapshots = std::move(snapshots), recipe = std::move(recipe), recipeStages = std::move(recipeStages), drawKey, commitQueue, commitPacket, epoch, writablePrograms = std::move(writablePrograms), results = std::move(results), memory = std::move(memory), linked = std::move(linked), stageCaptures = std::move(stageCaptures), matched = std::move(matched), matchedRegions = std::move(matchedRegions), fresh = std::move(fresh), decodeReads = std::move(decodeReads), shaderMemory = std::move(shaderMemory), entry = std::move(entry), dataEntry = std::move(dataEntry), compiledStages = std::move(compiledStages), stageData = std::move(stageData)]() mutable { DrawPipeline::FollowEpoch(epoch); GuestMemory::SetCurrentPacket(commitPacket, commitQueue); commitDraw(localDevice, commitQueue, decode->state, drawParameters, stages, snapshots, recipe, recipeStages, drawKey); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp index c207db4cc2..a1a5ebaab1 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/HandoffSpin.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp" +#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp" #include "prx/libc/include/HostThreadLocal.hpp" #include @@ -364,7 +365,7 @@ void DrawPipeline::report(std::chrono::steady_clock::time_point now) { std::snprintf(reads, sizeof(reads), " %llu read ranges, %llu stores held, %llu decode drains", static_cast(heldReadRanges.load(std::memory_order_relaxed)), static_cast(heldReadHits.exchange(0, std::memory_order_relaxed)), static_cast(decodeDrainCount - reportedDecodeDrains)); reportedDecodeDrains = decodeDrainCount; drainText += reads; - std::fprintf(stderr, "[draw] pipelined (10 s, depth %zu): %llu commits at %.1f us each on the committer, %llu failed; %llu enqueued behind %.2f on average, waiting %.1f us per draw for room, %.2f us for the lock; %llu committer wakes;%s; drains:%s\n", Depth(), static_cast(commits), perCommit, static_cast(commitErrors), static_cast(enqueued), queued, perDraw, enqueued == 0 ? 0.0 : static_cast(lockWaitNs) / 1e3 / static_cast(enqueued), static_cast(wakes), reads, drainText.c_str()); + AgcDriver::ProfilePrint_nid_no_patch("[draw] pipelined (10 s, depth %zu): %llu commits at %.1f us each on the committer, %llu failed; %llu enqueued behind %.2f on average, waiting %.1f us per draw for room, %.2f us for the lock; %llu committer wakes;%s; drains:%s\n", Depth(), static_cast(commits), perCommit, static_cast(commitErrors), static_cast(enqueued), queued, perDraw, enqueued == 0 ? 0.0 : static_cast(lockWaitNs) / 1e3 / static_cast(enqueued), static_cast(wakes), reads, drainText.c_str()); commits = commitNs = commitErrors = enqueued = fullWaitNs = depthSum = wakes = lockWaitNs = 0; drains = {}; drainWaitNs = {}; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp index 62fea0e6ae..7acaf98715 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -251,7 +251,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) { for (std::size_t i = 0; i < s.blends.size(); ++i) { if (!sameBlend(s.blends[i], t.blends[i])) return false; } - if (s.hasColorTarget != t.hasColorTarget || s.rectList != t.rectList || s.renderExtent.width != t.renderExtent.width || s.renderExtent.height != t.renderExtent.height || s.topology != t.topology || s.negativeOneToOne != t.negativeOneToOne || s.depthClamp != t.depthClamp || s.conservativeRasterization != t.conservativeRasterization || s.perSampleShading != t.perSampleShading || s.cullMode != t.cullMode || s.polygonMode != t.polygonMode || s.frontFace != t.frontFace || !sameBlend(s.blend, t.blend) || s.blendConstants != t.blendConstants) return false; + if (s.colorRop != t.colorRop || s.hasColorTarget != t.hasColorTarget || s.rectList != t.rectList || s.renderExtent.width != t.renderExtent.width || s.renderExtent.height != t.renderExtent.height || s.topology != t.topology || s.negativeOneToOne != t.negativeOneToOne || s.depthClamp != t.depthClamp || s.conservativeRasterization != t.conservativeRasterization || s.perSampleShading != t.perSampleShading || s.cullMode != t.cullMode || s.polygonMode != t.polygonMode || s.frontFace != t.frontFace || !sameBlend(s.blend, t.blend) || s.blendConstants != t.blendConstants) return false; if (std::memcmp(&s.viewport, &t.viewport, sizeof(VkViewport)) != 0 || std::memcmp(&s.scissor, &t.scissor, sizeof(VkRect2D)) != 0) return false; const auto& p = a.pixel; const auto& q = b.pixel; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRelocate.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRelocate.cpp index 33665a380c..c03e05baea 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRelocate.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRelocate.cpp @@ -173,15 +173,21 @@ void Driver::learnPendingRelocation() { } if (pending.alias == 0) return; if (drawAliases.size() >= MaxAliases) drawRelocationCounters.evicted += dropEveryOther(drawAliases); - drawAliases[pending.alias] = inserted->second; + drawAliases[pending.alias] = pending.key; } void Driver::relocateDrawEntry(const QueueState& queue, std::uint64_t alias, std::shared_ptr& entry, std::shared_ptr& decode, std::vector& patches) { auto& counters = drawRelocationCounters; ++counters.lookups; const auto found = drawAliases.find(alias); - if (found == drawAliases.end() || found->second->decode == nullptr) return; - const auto donor = found->second; + if (found == drawAliases.end()) return; + const auto cached = drawCache.find(found->second); + if (cached == drawCache.end()) { + drawAliases.erase(found); + return; + } + if (cached->second->decode == nullptr) return; + const auto donor = cached->second; const auto& programs = donor->decode->programs; if (donor->stages.size() != programs.size()) return; struct Move { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 876c1b2181..fae7bc0c5d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -422,6 +422,7 @@ struct VulkanDevice::State { bool depthBounds = false; bool depthBiasClamp = false; bool dualSrcBlend = false; + bool logicOp = false; bool occlusionQueryPrecise = false; VkDeviceSize hostImportAlignment = 0; bool drawInputsInPlace = true; @@ -1775,6 +1776,8 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : enabled.depthBiasClamp = available.depthBiasClamp; state->depthBiasClamp = enabled.depthBiasClamp == VK_TRUE; enabled.dualSrcBlend = available.dualSrcBlend; + enabled.logicOp = available.logicOp; + state->logicOp = enabled.logicOp == VK_TRUE; state->dualSrcBlend = enabled.dualSrcBlend == VK_TRUE; enabled.occlusionQueryPrecise = available.occlusionQueryPrecise; state->occlusionQueryPrecise = enabled.occlusionQueryPrecise == VK_TRUE; @@ -3971,6 +3974,7 @@ Graphics::Context VulkanDevice::buildContext() const { context.drawIndirectFirstInstance = state->drawIndirectFirstInstance; context.multiDrawIndirect = state->multiDrawIndirect; context.dualSrcBlend = state->dualSrcBlend; + context.logicOp = state->logicOp; context.depthBounds = state->depthBounds; context.depthBiasClamp = state->depthBiasClamp; context.samplerFilterMinmax = state->samplerFilterMinmax; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index 286666da03..7e0e07d07b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -213,6 +213,7 @@ struct Context { bool depthBounds = false; bool depthBiasClamp = false; bool dualSrcBlend = false; + bool logicOp = false; bool samplerFilterMinmax = false; bool nonSeamlessCubeMap = false; VkSampleCountFlags mergedCoverageSampleCounts = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp index 4cf143ebb3..99f3716fdb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp @@ -107,7 +107,8 @@ public: static constexpr std::size_t DrawInputEntries = 16384; std::shared_ptr UnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array& stamp, VkDeviceSize* offset = nullptr); void NoteUnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array& stamp, std::shared_ptr buffer, VkDeviceSize offset = 0); - std::shared_ptr ReusableDrawSnapshot(std::uint64_t address, std::size_t bytes, SnapshotUse use = SnapshotUse::Storage, std::uint32_t* derived = nullptr, std::uint64_t generation = 0); + std::shared_ptr ReusableDrawSnapshot(std::uint64_t address, std::size_t bytes, SnapshotUse use = SnapshotUse::Storage, std::uint32_t* derived = nullptr, std::uint64_t generation = 0, VkDeviceSize* offset = nullptr); + static constexpr std::size_t ContainingSnapshotProbes = 16; void KeepDrawSnapshot(std::uint64_t address, std::size_t bytes, std::uint64_t generation, std::uint64_t registryGeneration, std::shared_ptr buffer, SnapshotUse use = SnapshotUse::Storage, std::uint32_t derived = 0); static constexpr std::size_t DrawUploadChunk = std::size_t{1} << 20u; struct UploadSlice { @@ -888,6 +889,7 @@ private: std::map indexRanges; std::list indexRangeRecency; bool refreshDrawSnapshot(DrawSnapshot& entry, std::uint64_t address, std::size_t bytes); + std::shared_ptr containingDrawSnapshot(std::uint64_t address, std::size_t bytes, VkDeviceSize* offset); mutable std::weak_ptr publishedWriteRanges; mutable bool publishedWriteRangesValid = false; }; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index 0d796dad29..361295e739 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -153,6 +153,7 @@ struct State { VkProvokingVertexModeEXT provokingVertexMode = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT; VkPipelineColorBlendAttachmentState blend; std::array blendConstants; + std::uint8_t colorRop = 0xccu; std::uint32_t samples = 1; std::array sampleLocations{}; bool customSampleLocations = false; @@ -171,6 +172,9 @@ struct State { // distance vectors); a vertex program that exports pos1..pos3 depends on them. inline constexpr std::uint32_t PositionExportLayoutBits = 0xffffu | (1u << 21u) | (1u << 22u) | (1u << 23u); +inline constexpr std::uint8_t CopyRop = 0xccu; +VkLogicOp LogicOpFromRop(std::uint8_t rop); + ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup = false); void ApplyPointSize(const State& state, ShaderRecompiler::ShaderVertexStageInfo& vertex); State DecodeState(const QueueState& queue, bool vertexWorkgroup = false); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index f20bac7cc3..7273cb82e3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -2105,6 +2105,12 @@ std::vector> FlushDepthPlanes(std::uint6 const bool overlapped = !surface->syncing && plane < end && address < plane + planeBytes; if (overlapped && (surface->pendingClear & VK_IMAGE_ASPECT_DEPTH_BIT) != 0) surface->MarkAllStale(planeBytes); if (overlapped && (scope == DepthPlaneScope::Whole || anyUnit(surface->StaleUnitsIn(planeBytes, address, end)))) { + if (GuestMemory::CommittedRanges(plane, static_cast(planeBytes)).empty()) { + static std::atomic reported{false}; + if (!reported.exchange(true)) std::fprintf(stderr, "[gpu] depth plane 0x%llx+0x%llx was unmapped with GPU results pending; they are dropped\n", static_cast(plane), static_cast(planeBytes)); + surface->ClearStale(); + continue; + } surface->SyncFromMemory(resource); auto units = scope == DepthPlaneScope::Whole ? surface->staleUnits : surface->StaleUnitsIn(planeBytes, address, end); if (anyUnit(units)) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index d930aab702..2aa8b06e62 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -16,6 +16,7 @@ #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/Mutex.hpp" +#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "ThreadOwned.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestAllocations.hpp" @@ -763,8 +764,8 @@ void reportDraw(const std::array& us, const ShaderResources: for (std::size_t i = 1; i < profile.recipeMisses.size() && room(); ++i) { n += std::snprintf(line + n, sizeof(line) - static_cast(n), " %s %llu", DrawRecipeMissName(static_cast(i)), static_cast(profile.recipeMisses[i])); } - std::fprintf(stderr, "%s\n", line); - std::fprintf(stderr, "[rescache] draws: %llu hits, %llu misses, %llu invalidated, %llu uncacheable; validation memo %llu hits / %llu misses (which key words the misses differ in: the miss churn line)\n", static_cast(profile.cacheHits), static_cast(profile.cacheMisses), static_cast(profile.cacheInvalidated), static_cast(profile.uncacheable), static_cast(profile.validateHits), static_cast(profile.validateMisses)); + AgcDriver::ProfilePrint_nid_no_patch("%s\n", line); + AgcDriver::ProfilePrint_nid_no_patch("[rescache] draws: %llu hits, %llu misses, %llu invalidated, %llu uncacheable; validation memo %llu hits / %llu misses (which key words the misses differ in: the miss churn line)\n", static_cast(profile.cacheHits), static_cast(profile.cacheMisses), static_cast(profile.cacheInvalidated), static_cast(profile.uncacheable), static_cast(profile.validateHits), static_cast(profile.validateMisses)); profile.totalsUs.fill(0); profile.maxUs.fill(0); profile.maxDrawUs = profile.hookWaitUs = profile.maxHookWaitUs = profile.ownSyncUs = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index db0d8b7619..12939ea058 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -1365,7 +1365,7 @@ ImportRun joinedRun(const GuestAllocations::Lease& lease, std::uint64_t begin, s auto run = crossedRun(lease, begin, end); if (run.ranges.empty()) return run; if (std::all_of(run.ranges.begin(), run.ranges.end(), [window](const auto& range) { return range->bytes > window; })) return run; - if (begin == run.base && end == run.base + run.bytes) return run; + if (begin == run.base) return run; return {}; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index efbf47caf5..fcad4ff7bf 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -28,6 +28,34 @@ namespace AgcDriver::Graphics { +namespace { + +bool LogicOpFormat(VkFormat format) { + switch (format) { + case VK_FORMAT_R8_UNORM: + case VK_FORMAT_R8G8_UNORM: + case VK_FORMAT_R8G8B8A8_UNORM: + case VK_FORMAT_B8G8R8A8_UNORM: + case VK_FORMAT_A2B10G10R10_UNORM_PACK32: + case VK_FORMAT_A2R10G10B10_UNORM_PACK32: + case VK_FORMAT_R16_UNORM: + case VK_FORMAT_R16G16_UNORM: + case VK_FORMAT_R16G16B16A16_UNORM: + case VK_FORMAT_R8_UINT: + case VK_FORMAT_R8G8_UINT: + case VK_FORMAT_R8G8B8A8_UINT: + case VK_FORMAT_R16_UINT: + case VK_FORMAT_R16G16_UINT: + case VK_FORMAT_R16G16B16A16_UINT: + case VK_FORMAT_R32_UINT: + case VK_FORMAT_R32G32_UINT: + case VK_FORMAT_R32G32B32A32_UINT: return true; + default: return false; + } +} + +} + void LogPipelineStatistics_nid_no_patch(const Context& context, VkPipeline pipeline) { if (!context.pipelineExecutableInfo) return; const auto getProperties = context.Function("vkGetPipelineExecutablePropertiesKHR"); @@ -363,6 +391,7 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& } } for (const auto& blend : state.blends) appendKey(keys.fragmentOutput, blend); + appendKey(keys.fragmentOutput, state.colorRop); for (const auto value : state.blendConstants) appendKey(keys.fragmentOutput, value); appendKey(keys.renderPass, state.blends.size()); for (const auto& color : state.colors) { @@ -429,6 +458,8 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput 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"); Require(state.reversedBlend == 0 || (context.dualSrcBlend && state.blends.size() <= context.limits.maxFragmentDualSrcAttachments), "blending into a color target with a reversed component order through a second source color needs dual-source blending, which the device does not support"); + Require(LogicOpFromRop(state.colorRop) == VK_LOGIC_OP_COPY || context.logicOp, "device does not support color logic operations"); + Require(LogicOpFromRop(state.colorRop) == VK_LOGIC_OP_COPY || std::ranges::all_of(state.colors, [](const ColorTarget& color) { return LogicOpFormat(color.format); }), "color logic operations on a color target that is not a normalized or integer format are not implemented"); Require(!state.negativeOneToOne || context.depthClipControl, "negative-one-to-one depth clipping requires VK_EXT_depth_clip_control with depthClipControl enabled"); Require(!state.perSampleShading || context.sampleRateShading, "per-sample pixel shading requires the sampleRateShading device feature"); Require(!state.mergedCoverage || (context.mergedCoverageSampleCounts & state.samples) != 0, "an overrasterized draw (a multisampled rasterizer into one-sample color targets) requires VK_NV_framebuffer_mixed_samples with merged coverage reduction"); @@ -652,6 +683,10 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput secondChannels[index].colorBlendOp = state.blends[index].alphaBlendOp; } } + if (const auto logicOp = LogicOpFromRop(state.colorRop); logicOp != VK_LOGIC_OP_COPY) { + blend.logicOpEnable = VK_TRUE; + blend.logicOp = logicOp; + } blend.attachmentCount = static_cast(firstChannels.size()); blend.pAttachments = firstChannels.empty() ? nullptr : firstChannels.data(); std::copy(state.blendConstants.begin(), state.blendConstants.end(), blend.blendConstants); @@ -995,6 +1030,7 @@ void pipelineKey(std::vector& key, const Context& context, const Stat append(key, state.conservativeRasterization); append(key, state.blends.size()); for (const auto& blend : state.blends) append(key, blend); + append(key, state.colorRop); for (const auto value : state.blendConstants) append(key, value); append(key, state.samples); append(key, state.perSampleShading); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp index c92e277908..120d921571 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp @@ -47,7 +47,7 @@ static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 40, "Tessel constexpr std::uint32_t FileMagic = 0x4c4a5041u; // Bumped when the recipe codec, CanonicalPipelineState or the store key change in a way the // layout guards above do not catch (a field renumbered or reinterpreted at the same size). -constexpr std::uint32_t FileFormat = 9u; +constexpr std::uint32_t FileFormat = 10u; constexpr std::uint32_t ModuleRecord = 1u; constexpr std::uint32_t GraphicsRecord = 2u; constexpr std::uint32_t ComputeRecord = 3u; @@ -206,6 +206,7 @@ void encodeState(Writer& writer, const State& state) { writer.Bool(state.rectList); writer.Bool(state.dualSourceBlend); writer.U32(state.reversedBlend); + writer.U32(state.colorRop); writer.U32(static_cast(state.topology)); writer.Bool(state.primitiveRestart); writer.Bool(state.negativeOneToOne); @@ -273,6 +274,7 @@ void decodeState(Reader& reader, State& state) { state.rectList = reader.Bool(); state.dualSourceBlend = reader.Bool(); state.reversedBlend = reader.U32(); + state.colorRop = static_cast(reader.U32()); state.topology = static_cast(reader.U32()); state.primitiveRestart = reader.Bool(); state.negativeOneToOne = reader.Bool(); @@ -1511,6 +1513,7 @@ State CanonicalPipelineState(const State& state) { canonical.rectList = state.rectList; canonical.dualSourceBlend = state.dualSourceBlend; canonical.reversedBlend = state.reversedBlend; + canonical.colorRop = state.colorRop; canonical.topology = state.topology; canonical.primitiveRestart = state.primitiveRestart; canonical.negativeOneToOne = state.negativeOneToOne; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index dab5468c75..4248154b0e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -2794,9 +2794,13 @@ void Recorder::NoteUnwrittenDrawSnapshot(std::uint64_t address, std::size_t byte unwrittenBytes += bytes; } -std::shared_ptr Recorder::ReusableDrawSnapshot(std::uint64_t address, std::size_t bytes, SnapshotUse use, std::uint32_t* derived, std::uint64_t generation) { +std::shared_ptr Recorder::ReusableDrawSnapshot(std::uint64_t address, std::size_t bytes, SnapshotUse use, std::uint32_t* derived, std::uint64_t generation, VkDeviceSize* offset) { auto found = use == SnapshotUse::Vertex ? drawSnapshots.lower_bound({address, use, bytes}) : drawSnapshots.find({address, use, bytes}); - if (found == drawSnapshots.end() || std::get<0>(found->first) != address || std::get<1>(found->first) != use) return {}; + if (found == drawSnapshots.end() || std::get<0>(found->first) != address || std::get<1>(found->first) != use) { + if (use != SnapshotUse::Storage || offset == nullptr) return {}; + return containingDrawSnapshot(address, bytes, offset); + } + if (offset != nullptr) *offset = 0; const bool sameRegistry = found->second.registryGeneration == GuestAllocations::GuestAllocationsGeneration_nid_postfix(); if (!sameRegistry || !GuestMemory::UnchangedSince(address, bytes, found->second.generation)) { if (!sameRegistry || generation == 0 || use != SnapshotUse::Storage || !refreshDrawSnapshot(found->second, address, bytes)) { @@ -2811,6 +2815,23 @@ std::shared_ptr Recorder::ReusableDrawSnapshot(std::uint64_t address, st return found->second.buffer; } +std::shared_ptr Recorder::containingDrawSnapshot(std::uint64_t address, std::size_t bytes, VkDeviceSize* offset) { + const VkDeviceSize alignment = std::max(context.limits.minStorageBufferOffsetAlignment, 4); + const auto registry = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + auto entry = drawSnapshots.upper_bound({address, SnapshotUse::Storage, std::numeric_limits::max()}); + for (std::size_t probe = 0; probe < ContainingSnapshotProbes && entry != drawSnapshots.begin(); ++probe) { + --entry; + const auto& [start, use, held] = entry->first; + if (use != SnapshotUse::Storage || start + held < address + bytes || (address - start) % alignment != 0) continue; + if (entry->second.registryGeneration != registry || !GuestMemory::UnchangedSince(address, bytes, entry->second.generation)) continue; + auto& recency = drawSnapshotPools[SnapshotPool(use)].recency; + recency.splice(recency.end(), recency, entry->second.recent); + *offset = address - start; + return entry->second.buffer; + } + return {}; +} + bool Recorder::refreshDrawSnapshot(DrawSnapshot& entry, std::uint64_t address, std::size_t bytes) { const auto held = entry.buffer->Bytes(); if (held.size() != bytes || bytes == 0) return false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 0c51d34b74..84bc841971 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -5671,8 +5671,9 @@ std::shared_ptr ShaderResources::PrepareDrawBindi if (verifySnapshots && buffer != nullptr) { static std::atomic checked{0}, differing{0}; const auto fast = buffer->Bytes().subspan(static_cast(offset), bytes); - const auto slow = recorder.ReusableDrawSnapshot(begin, bytes, Recorder::SnapshotUse::Storage, nullptr, GuestMemory::CollectWrites(begin, bytes)); - const bool same = slow == buffer || (slow != nullptr && slow->Bytes().size() == fast.size() && std::memcmp(slow->Bytes().data(), fast.data(), fast.size()) == 0) || (slow == nullptr && std::memcmp(fast.data(), reinterpret_cast(begin), bytes) == 0); + VkDeviceSize slowOffset = 0; + const auto slow = recorder.ReusableDrawSnapshot(begin, bytes, Recorder::SnapshotUse::Storage, nullptr, GuestMemory::CollectWrites(begin, bytes), &slowOffset); + const bool same = (slow == buffer && slowOffset == offset) || (slow != nullptr && slow->Bytes().size() >= slowOffset + fast.size() && std::memcmp(slow->Bytes().data() + slowOffset, fast.data(), fast.size()) == 0) || (slow == nullptr && std::memcmp(fast.data(), reinterpret_cast(begin), bytes) == 0); if (!same) differing.fetch_add(1); if (checked.fetch_add(1) % 200000 == 199999) std::fprintf(stderr, "[draw-snapshots] verify: %llu fast answers checked, %llu differing%c", static_cast(checked.load()), static_cast(differing.load()), 10); } @@ -5685,7 +5686,7 @@ std::shared_ptr ShaderResources::PrepareDrawBindi offset = slice.offset; } else { const auto generation = GuestMemory::CollectWrites(begin, bytes); - buffer = recorder.ReusableDrawSnapshot(begin, bytes, Recorder::SnapshotUse::Storage, nullptr, generation); + buffer = recorder.ReusableDrawSnapshot(begin, bytes, Recorder::SnapshotUse::Storage, nullptr, generation, &offset); if (buffer == nullptr) { buffer = std::make_shared(context, bytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT); std::memcpy(buffer->Bytes().data(), reinterpret_cast(begin), bytes); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 899b1a2f05..90893f4494 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -730,15 +730,19 @@ void rasterizeSampleZero(State& state) { state.sampleLocationGrid = sampleZero[8] != sampleZero[0] || sampleZero[16] != sampleZero[0] || sampleZero[24] != sampleZero[0]; } +bool documentedRop(std::uint32_t rop) { + return (rop >> 4u) == (rop & 0xfu) || ((rop >> 2u) & 0x33u) == (rop & 0x33u); +} + bool colorControlSupported(std::uint32_t colorControl, bool hasColorTarget) { colorControl &= ~1u; - return colorControl == 0xcc0010u || ((colorControl >> 4u) & 7u) == 0u || (!hasColorTarget && (colorControl & ~0x70u) == 0xcc0000u); + return ((colorControl & ~0xff0000u) == 0x10u && documentedRop((colorControl >> 16u) & 0xffu)) || ((colorControl >> 4u) & 7u) == 0u || (!hasColorTarget && (colorControl & ~0x70u) == 0xcc0000u); } std::string colorControlMessage(std::uint32_t colorControl) { static constexpr const char* modes[8] = {"disable", "normal", "eliminate fast clear", "resolve", "decompress", "FMASK decompress", "DCC decompress", "reserved"}; char text[160]; - std::snprintf(text, sizeof(text), "AGC graphics: only normal color rendering with copy ROP is supported (CB_COLOR_CONTROL 0x%08x, mode %s)", colorControl, modes[(colorControl >> 4u) & 7u]); + std::snprintf(text, sizeof(text), "AGC graphics: only normal color rendering with a ROP3 code of the source or brush form is supported (CB_COLOR_CONTROL 0x%08x, mode %s)", colorControl, modes[(colorControl >> 4u) & 7u]); return text; } @@ -1112,6 +1116,21 @@ std::array QuadSampleMasks(const State& state, const SampleOrd return {mask, mask, mask, mask}; } +VkLogicOp LogicOpFromRop(std::uint8_t rop) { + if (!documentedRop(rop)) { + char text[96]; + std::snprintf(text, sizeof(text), "AGC graphics: ROP3 code 0x%02x combines source and brush, which is not implemented", rop); + throw std::runtime_error(text); + } + static constexpr std::array operations{ + VK_LOGIC_OP_CLEAR, VK_LOGIC_OP_NOR, VK_LOGIC_OP_AND_INVERTED, VK_LOGIC_OP_COPY_INVERTED, + VK_LOGIC_OP_AND_REVERSE, VK_LOGIC_OP_INVERT, VK_LOGIC_OP_XOR, VK_LOGIC_OP_NAND, + VK_LOGIC_OP_AND, VK_LOGIC_OP_EQUIVALENT, VK_LOGIC_OP_NO_OP, VK_LOGIC_OP_OR_INVERTED, + VK_LOGIC_OP_COPY, VK_LOGIC_OP_OR_REVERSE, VK_LOGIC_OP_OR, VK_LOGIC_OP_SET}; + const std::uint32_t index = (rop & 3u) | ((rop >> 4u) & 4u) | ((rop >> 4u) & 8u); + return operations[index]; +} + std::array SampleLocations(const State& state) { Require(state.samples == 1 || state.samples == 2 || state.samples == 4 || state.samples == 8, "invalid sample count"); std::array locations{}; @@ -1492,6 +1511,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { // CB_COLOR_CONTROL mode 0 disables color writes, which only matters when a target is written. if (const auto colorControl = read(cx, 0x202); !colorControlSupported(colorControl, result.hasColorTarget)) throw std::runtime_error(colorControlMessage(colorControl)); + if (const auto colorControl = read(cx, 0x202); ((colorControl >> 4u) & 7u) == 1u) result.colorRop = static_cast(colorControl >> 16u); zero(cx, 0x1c4, zFormatSupported(read(cx, 0x1c4)) ? 0u : ~0u, "depth or sample-mask export"); const auto exportFormat = result.hasColorTarget ? read(cx, 0x1c5) : 0u; APS5_LOG_OUT_DEBUG("Export format=%u", exportFormat); @@ -1708,6 +1728,9 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { result.color = result.colors.front(); result.blend = result.blends[result.colors.front().exportIndex]; } + if (LogicOpFromRop(result.colorRop) != VK_LOGIC_OP_COPY) { + for (const auto& blend : result.blends) Require(blend.blendEnable == VK_FALSE, "a color logic operation together with blending is not implemented"); + } Require(result.reversedBlend == 0 || (exportSlots.size() == 1 && exportCount == 1), "blending into a color target with a reversed component order through a second source color needs that target to be the shader's only color export"); if (result.dualSourceBlend) { Require(exportSlots.size() == 2 && exportSlots[0] == 0 && exportSlots[1] == 1 && exportCount == 1, "dual-source blending needs pixel exports to MRT slots 0 and 1 and color writes to slot 0 alone"); diff --git a/core/libs/prx/libSceAgcDriver/tests/AsyncLog.cpp b/core/libs/prx/libSceAgcDriver/tests/AsyncLog.cpp index f334f4b036..a370fe2802 100644 --- a/core/libs/prx/libSceAgcDriver/tests/AsyncLog.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/AsyncLog.cpp @@ -243,7 +243,12 @@ void FullQueueBlocksCaller() { constexpr int ChildLines = 50; [[noreturn]] void EndChild(const std::string& mode, const char* output) { - std::FILE* file = std::fopen(output, "wb"); + const auto path = std::filesystem::temp_directory_path() / output; +#ifdef _WIN32 + std::FILE* file = _wfopen(path.c_str(), L"wb"); +#else + std::FILE* file = std::fopen(path.c_str(), "wb"); +#endif Check(file != nullptr, "child output file"); #ifdef _WIN32 SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); @@ -288,12 +293,18 @@ std::string Quote(const std::string& text) { void LinesSurviveEnd(const std::string& self, const std::string& mode) { const auto output = std::filesystem::temp_directory_path() / ("aps5-async-log-" + mode + "-" + std::to_string(Clock::now().time_since_epoch().count()) + ".txt"); - std::string command = Quote(self) + " child " + mode + " " + Quote(output.string()); #ifdef _WIN32 - command = Quote(command); -#endif + (void)self; + std::wstring executable(32768, L'\0'); + executable.resize(GetModuleFileNameW(nullptr, executable.data(), static_cast(executable.size()))); + const std::wstring command = L"\"\"" + executable + L"\" child " + std::filesystem::path(mode).wstring() + L" \"" + output.filename().wstring() + L"\"\""; + const auto start = Clock::now(); + const int status = _wsystem(command.c_str()); +#else + const std::string command = Quote(self) + " child " + mode + " " + Quote(output.filename().string()); const auto start = Clock::now(); const int status = std::system(command.c_str()); +#endif const auto lived = Clock::now() - start; std::error_code error; const bool started = std::filesystem::exists(output, error); diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 6d7f230ea1..c2c44c6aed 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -602,6 +602,9 @@ void importJoinTests(const Context& context) { Require(pieces != nullptr && pieces->base == heapBase && pieces->bytes == 2 * piece && piecesBuffer == pieces->buffer && piecesCopied == 0, "a buffer bound exactly over two pieces no larger than the import window was not imported as one"); change(heap + 2 * piece, piece, true); static_cast(bind(heapBase + 2 * piece - 16)); + const auto [prefixBuffer, prefixCopied] = bind(heapBase, 2 * piece + 16); + const auto* prefix = HostImportFor(importing, heapBase, 2 * piece + 16); + Require(prefix != nullptr && prefix->base == heapBase && prefix->bytes == 3 * piece && prefixBuffer == prefix->buffer && prefixCopied == 0, "a buffer bound from the first of three pieces into the last was not imported over the pieces it crosses"); const auto* kept = HostImportFor(importing, first, 16); Require(kept != nullptr && kept->buffer == joinedBuffer && kept->bytes == 2 * large, "a registry change elsewhere re-imported the joined ranges"); change(guest + 2 * large, large, true); diff --git a/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp index 79878c2766..cd876ad472 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp @@ -1,4 +1,5 @@ #include "GraphicsTests.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" @@ -13,6 +14,14 @@ #include #include #include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif namespace { @@ -417,6 +426,62 @@ void TestRetiredSurfaceRelease(const Context& context) { ClearDepthSurfaces(context.device); } +void TestUnmappedStalePlaneFlush(const Context& context) { + ClearDepthSurfaces(context.device); + constexpr VkExtent2D extent{256, 256}; + constexpr std::uint64_t unmapped = 0xa0000000; + DepthTarget target{unmapped, 0, extent, VK_FORMAT_D32_SFLOAT, 1.0f, 0}; + target.depthSwizzle = XorSwizzleMode(TextureTileMode::kZ64KBX); + Require(DepthSurfaceView(context, target, true) != VK_NULL_HANDLE, "a mirrorable depth surface must render"); + Require(DepthPlanesStale(), "a written mirrorable depth surface must leave its plane stale"); + std::vector> stored; + try { + stored = FlushDepthPlanes(unmapped, DepthSliceBytes(extent, 4u)); + } catch (const std::exception& error) { + Require(false, std::string("flushing a depth plane whose guest memory is not mapped must drop its results: ") + error.what()); + } + Require(stored.empty(), "a depth plane without mapped guest memory must not be stored"); + Require(!DepthPlanesStale(), "a dropped depth plane must not stay stale"); + ClearDepthSurfaces(context.device); +} + +void TestReadOnlyStalePlaneFlush(const Context& context) { + ClearDepthSurfaces(context.device); + constexpr std::size_t bytes = 1024 * 1024; +#ifdef _WIN32 + const auto release = [](void* pointer) { VirtualFree(pointer, 0, MEM_RELEASE); }; + std::unique_ptr mapping(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READONLY), release); + Require(mapping != nullptr, "cannot map a read-only depth plane"); +#else + const auto release = [](void* pointer) { munmap(pointer, bytes); }; + std::unique_ptr mapping(mmap(nullptr, bytes, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0), release); + Require(mapping.get() != MAP_FAILED, "cannot map a read-only depth plane"); +#endif + const auto address = (reinterpret_cast(mapping.get()) + 65535u) & ~std::uintptr_t{65535u}; + constexpr VkExtent2D extent{256, 256}; + const auto planeBytes = DepthSliceBytes(extent, 4u); + Require(!AgcDriver::GuestMemory::CommittedRanges(address, planeBytes).empty(), "a read-only depth plane must remain readable"); + Require(AgcDriver::GuestMemory::CommittedRanges(address, planeBytes, true).empty(), "a read-only depth plane must not be writable"); + auto storageContext = context; + storageContext.formatProperties = [](VkPhysicalDevice, VkFormat, VkFormatProperties* properties) { + *properties = {}; + properties->optimalTilingFeatures = VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT; + }; + DepthTarget target{address, 0, extent, VK_FORMAT_D32_SFLOAT, 1.0f, 0}; + target.depthSwizzle = XorSwizzleMode(TextureTileMode::kZ64KBX); + Require(DepthSurfaceView(storageContext, target, true) != VK_NULL_HANDLE, "a read-only depth plane must render"); + Require(DepthPlanesStale(), "the read-only depth plane must have pending results"); + bool rejected = false; + try { + static_cast(FlushDepthPlanes(address, planeBytes)); + } catch (const std::runtime_error& error) { + rejected = std::string_view(error.what()).find("storage texture has no committed guest pages") != std::string_view::npos; + } + Require(rejected, "a mapped read-only depth plane must retain the unsupported-store rejection"); + Require(DepthPlanesStale(), "a mapped read-only depth plane must retain its pending results"); + ClearDepthSurfaces(context.device); +} + void RunDepthSurfaceReuseTests() { static int deviceTag = 0; Context context{}; @@ -432,6 +497,8 @@ void RunDepthSurfaceReuseTests() { TestMultisampleDepthOwnership(context); TestMultisampleStencilAliases(context); TestStencilUnormView(context); + TestUnmappedStalePlaneFlush(context); + TestReadOnlyStalePlaneFlush(context); constexpr std::uint64_t depth32 = 0x40000000; constexpr std::uint64_t depth16 = 0x50000000; constexpr std::uint64_t depthStencil = 0x60000000; diff --git a/core/libs/prx/libSceAgcDriver/tests/Flip.cpp b/core/libs/prx/libSceAgcDriver/tests/Flip.cpp index ec73dea01a..9e89fdd61c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Flip.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Flip.cpp @@ -355,7 +355,11 @@ void testFailureLog() { std::fflush(stderr); const int saved = APS5_DUP(APS5_FILENO(stderr)); check(saved >= 0, "stderr could not be saved"); - check(std::freopen(path.string().c_str(), "wb", stderr) != nullptr, "stderr could not be redirected"); +#ifdef _WIN32 + check(_wfreopen(path.wstring().c_str(), L"wb", stderr) != nullptr, "stderr could not be redirected"); +#else + check(std::freopen(path.c_str(), "wb", stderr) != nullptr, "stderr could not be redirected"); +#endif std::string message; std::string error; try { diff --git a/core/libs/prx/libSceAgcDriver/tests/FrameDumpIo.cpp b/core/libs/prx/libSceAgcDriver/tests/FrameDumpIo.cpp index ec52e5f1d9..f6b12a62e6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/FrameDumpIo.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/FrameDumpIo.cpp @@ -120,13 +120,15 @@ void DisableAfterFailure() { AgcDriver::FrameDumpOutput output; const std::array pixels{}; Check(output.Enabled_nid_no_patch(), "BMP output was disabled before any failure"); - const auto blocked = directory.string(); + const auto previous = std::filesystem::current_path(); + std::filesystem::current_path(directory.parent_path()); + const auto blocked = directory.filename().string(); Check(!output.Write_nid_no_patch(blocked.c_str(), 2, 2, pixels), "BMP output accepted a directory as a file"); Check(!output.Enabled_nid_no_patch(), "failed BMP output remained enabled"); - const auto skipped = directory / "must-not-exist.bmp"; - const auto path = skipped.string(); + const auto path = (directory.filename() / "must-not-exist.bmp").string(); for (unsigned i = 0; i < 64; ++i) Check(!output.Write_nid_no_patch(path.c_str(), 2, 2, pixels), "disabled BMP output wrote another file"); - Check(!std::filesystem::exists(skipped), "disabled BMP output created a file"); + std::filesystem::current_path(previous); + Check(!std::filesystem::exists(directory / "must-not-exist.bmp"), "disabled BMP output created a file"); Check(std::filesystem::remove(directory), "cannot remove empty fixture directory"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 3deb925a41..75f1c4c22e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1925,8 +1925,35 @@ void TuningFieldTests() { Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "DISABLE_DUAL_QUAD was rejected"); Require(state.hasColorTarget && state.blend.colorWriteMask == baseline.blend.colorWriteMask, "DISABLE_DUAL_QUAD changed color output"); queue.context[0x202] = 0xcc0013; - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "copy ROP"); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "source or brush form"); + queue.context[0x202] = 0x5b0010; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "source or brush form"); + queue.context[0x202] = 0x5a0010; + state = AgcDriver::Graphics::DecodeState(queue); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "an XOR ROP was rejected"); + Require(state.hasColorTarget && state.colorRop == 0x5a && AgcDriver::Graphics::LogicOpFromRop(state.colorRop) == VK_LOGIC_OP_XOR, "an XOR ROP did not decode"); queue.context[0x202] = 0xcc0010; + Require(AgcDriver::Graphics::DecodeState(queue).colorRop == AgcDriver::Graphics::CopyRop, "the copy ROP did not decode as the copy operation"); + const std::array, 14> ropOperations{{ + {0x0f, VK_LOGIC_OP_COPY_INVERTED}, {0xa0, VK_LOGIC_OP_AND}, {0xf0, VK_LOGIC_OP_COPY}, {0xfa, VK_LOGIC_OP_OR}, + {0x00, VK_LOGIC_OP_CLEAR}, {0x11, VK_LOGIC_OP_NOR}, {0x33, VK_LOGIC_OP_COPY_INVERTED}, {0x55, VK_LOGIC_OP_INVERT}, {0x66, VK_LOGIC_OP_XOR}, + {0x88, VK_LOGIC_OP_AND}, {0xaa, VK_LOGIC_OP_NO_OP}, {0xcc, VK_LOGIC_OP_COPY}, {0xee, VK_LOGIC_OP_OR}, {0xff, VK_LOGIC_OP_SET}}}; + for (const auto& [rop, operation] : ropOperations) Require(AgcDriver::Graphics::LogicOpFromRop(rop) == operation, "a ROP3 code mapped to the wrong logic operation"); + std::uint32_t documentedRops = 0; + for (std::uint32_t rop = 0; rop < 256u; ++rop) { + try { + static_cast(AgcDriver::Graphics::LogicOpFromRop(static_cast(rop))); + ++documentedRops; + } catch (const std::runtime_error&) { + } + } + Require(documentedRops == 28u, "a ROP3 code outside the source and brush forms was accepted, or one inside them was refused"); + auto blendedLogic = queue; + blendedLogic.context[0x202] = 0x5a0010; + blendedLogic.context[0x1e0] = 0x40000101u; + for (std::uint32_t index = 0; index < 4u; ++index) blendedLogic.context[0x105 + index] = 0u; + blendedLogic.context[0x31c] |= 0x8000u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(blendedLogic); }, "logic operation together with blending"); for (const auto groups : {1u, 2u, 15u}) { queue.context[0x2d5] = 0x2000u | (groups << 15u); state = AgcDriver::Graphics::DecodeState(queue); diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp index b5db144ac8..2c138fecf3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp @@ -105,6 +105,8 @@ public: Device() { #ifdef _WIN32 library = SDL_LoadObject("vulkan-1.dll"); +#elif defined(__APPLE__) + library = SDL_LoadObject("libvulkan.1.dylib"); #else library = SDL_LoadObject("libvulkan.so.1"); #endif @@ -116,6 +118,12 @@ public: application.apiVersion = VK_API_VERSION_1_1; VkInstanceCreateInfo info{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; info.pApplicationInfo = &application; +#ifdef __APPLE__ + const char* portability = VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME; + info.flags = VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; + info.enabledExtensionCount = 1; + info.ppEnabledExtensionNames = &portability; +#endif Check(function("vkCreateInstance")(&info, nullptr, &instance), "vkCreateInstance"); std::uint32_t count = 0; const auto enumerate = function("vkEnumeratePhysicalDevices"); diff --git a/core/libs/prx/libSceAgcDriver/tests/PresentationPass.cpp b/core/libs/prx/libSceAgcDriver/tests/PresentationPass.cpp index b5887de16a..11915c8575 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PresentationPass.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PresentationPass.cpp @@ -39,6 +39,8 @@ public: Device() { #ifdef _WIN32 library = SDL_LoadObject("vulkan-1.dll"); +#elif defined(__APPLE__) + library = SDL_LoadObject("libvulkan.1.dylib"); #else library = SDL_LoadObject("libvulkan.so.1"); #endif @@ -50,6 +52,12 @@ public: application.apiVersion = VK_API_VERSION_1_1; VkInstanceCreateInfo info{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; info.pApplicationInfo = &application; +#ifdef __APPLE__ + const char* portability = VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME; + info.flags = VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; + info.enabledExtensionCount = 1; + info.ppEnabledExtensionNames = &portability; +#endif Check(function("vkCreateInstance")(&info, nullptr, &instance), "vkCreateInstance"); std::uint32_t count = 0; const auto enumerate = function("vkEnumeratePhysicalDevices"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 0c14df96b6..9b820c6314 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -3955,10 +3955,11 @@ void drawSnapshotTableSetTests(const Device& device, Recorder& recorder) { } constexpr std::uint32_t width = 4; constexpr std::uint32_t height = 4; - alignas(256) static std::array texels{}; + alignas(256) static std::array texels{}; texels.fill(1.0f); const auto address = static_cast(reinterpret_cast(texels.data())); const std::array texture{static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (22u << 20u) | (((width - 1u) & 3u) << 30u), ((width - 1u) >> 2u) | ((height - 1u) << 14u), 0xfacu | (9u << 28u), 0u, 0u, 0u, 0u}; + Require(DescribeSurface(DecodeTextureResource(texture)).guestBytes <= sizeof(texels), "the draw snapshot table set test T# reads past its texels"); constexpr auto bindingCount = static_cast(ShaderRecompiler::RuntimeAbi::Binding::Count); const auto addTable = [&](ShaderRecompiler::RecompileResult& program, ShaderRecompiler::RuntimeAbi::Stage stage, std::uint32_t set, std::uint32_t elements) { ShaderRecompiler::DescriptorBinding map{}; @@ -4552,6 +4553,46 @@ void drawSnapshotEvictionTests(const Device& device) { Require(cache.ReusableDrawSnapshot(address, 16) == nullptr && cache.ReusableDrawSnapshot(address, 16) == nullptr, "a snapshot outlived a CPU store"); } +void containingSnapshotTests(const Device& device) { + using namespace AgcDriver::GuestMemory; + constexpr std::size_t bytes = 131072; + void* block = WriteWatched() ? AllocateWatched(bytes, 65536) : nullptr; + if (block == nullptr) { + std::cout << "write watching unavailable: containing draw snapshots not tested\n"; + return; + } + struct Release { + void* block; + ~Release() { ReleaseWatched(block, bytes); } + } release{block}; + const auto address = reinterpret_cast(block); + const VkDeviceSize alignment = std::max(device.GetContext().limits.minStorageBufferOffsetAlignment, 4); + Recorder cache(device.GetContext()); + const auto whole = std::make_shared(device.GetContext(), bytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + const auto exact = std::make_shared(device.GetContext(), 4096, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + const auto registry = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + const auto generation = CollectWrites(address, bytes); + Require(generation != 0, "the watched block has no generation"); + cache.KeepDrawSnapshot(address, bytes, generation, registry, whole); + const auto inner = address + 65536 + 16 * alignment; + VkDeviceSize offset = 1; + Require(cache.ReusableDrawSnapshot(inner, 4096) == nullptr, "a lookup without an offset took a containing snapshot"); + Require(cache.ReusableDrawSnapshot(inner, 4096, Recorder::SnapshotUse::Storage, nullptr, 0, &offset) == whole && offset == inner - address, "an unchanged range inside a kept snapshot was not taken from it at its offset"); + Require(cache.ReusableDrawSnapshot(inner, address + bytes - inner + 4, Recorder::SnapshotUse::Storage, nullptr, 0, &offset) == nullptr, "a range past the end of a kept snapshot was taken from it"); + Require(cache.ReusableDrawSnapshot(inner + alignment / 2, 4096, Recorder::SnapshotUse::Storage, nullptr, 0, &offset) == nullptr, "a misaligned range was taken from a kept snapshot"); + Require(cache.ReusableDrawSnapshot(inner, 4096, Recorder::SnapshotUse::Vertex, nullptr, 0, &offset) == nullptr, "a vertex lookup took a storage snapshot"); + cache.KeepDrawSnapshot(inner, 4096, generation, registry, exact); + offset = 1; + Require(cache.ReusableDrawSnapshot(inner, 4096, Recorder::SnapshotUse::Storage, nullptr, 0, &offset) == exact && offset == 0, "an exact snapshot lost to a containing one"); + std::memset(block, 0x5a, 16); + CollectWrites(address, 16); + Require(cache.ReusableDrawSnapshot(inner + 4096, 4096, Recorder::SnapshotUse::Storage, nullptr, 0, &offset) == whole && offset == inner + 4096 - address, "a store outside the range stopped it from being taken from a containing snapshot"); + Require(cache.ReusableDrawSnapshot(address, 4096, Recorder::SnapshotUse::Storage, nullptr, 0, &offset) == nullptr, "a range over a CPU store was taken from a containing snapshot"); + std::memset(reinterpret_cast(inner + 4096), 0x5a, 16); + CollectWrites(inner + 4096, 16); + Require(cache.ReusableDrawSnapshot(inner + 4096, 4096, Recorder::SnapshotUse::Storage, nullptr, 0, &offset) == nullptr, "a containing snapshot outlived a CPU store into the range"); +} + void unwrittenSnapshotTests(const Device& device) { Recorder cache(device.GetContext()); const auto buffer = std::make_shared(device.GetContext(), 16, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); @@ -9867,7 +9908,7 @@ int pipelineCacheFileTests(const Device& device) { } removeDirectory{directory}; #ifdef _WIN32 _putenv_s("ANYPS5_NO_SHADER_CACHE", "0"); - _putenv_s("ANYPS5_SHADER_CACHE_DIR", directory.string().c_str()); + _wputenv_s(L"ANYPS5_SHADER_CACHE_DIR", directory.wstring().c_str()); #else setenv("ANYPS5_NO_SHADER_CACHE", "0", 1); setenv("ANYPS5_SHADER_CACHE_DIR", directory.string().c_str(), 1); @@ -11183,6 +11224,7 @@ int main(int argc, char** argv) { } if (argc == 2 && std::string_view(argv[1]) == "--draw-snapshot-cap-only") { drawSnapshotCapTests(device, recorder); + containingSnapshotTests(device); unwrittenSnapshotTests(device); drawSnapshotUploadTests(device, recorder); std::cout << "Draw snapshot cap tests passed\n"; @@ -11235,6 +11277,7 @@ int main(int argc, char** argv) { imageTableFirstLayerRevalidateTests(device, recorder); drawSnapshotCapTests(device, recorder); drawSnapshotEvictionTests(device); + containingSnapshotTests(device); unwrittenSnapshotTests(device); drawSnapshotUploadTests(device, recorder); drawInputReuseTests(device, recorder); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 247e7a92dc..cc86441030 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -62,11 +62,19 @@ void setEnvironment(const char* name, const std::string& value) { #endif } +void setPathEnvironment(const char* name, const std::filesystem::path& value) { +#ifdef _WIN32 + _wputenv_s(std::filesystem::path(name).wstring().c_str(), value.wstring().c_str()); +#else + setenv(name, value.c_str(), 1); +#endif +} + #ifdef _WIN32 void verifyHostStandardError() { const auto directory = std::filesystem::temp_directory_path() / ("aps5-shader-host-stderr-" + std::to_string(std::random_device{}())); setEnvironment("ANYPS5_NO_SHADER_CACHE", "0"); - setEnvironment("ANYPS5_SHADER_CACHE_DIR", directory.string()); + setPathEnvironment("ANYPS5_SHADER_CACHE_DIR", directory); int capture[2], guest[2]; require(::_pipe(capture, 65536, _O_BINARY) == 0, "cannot create host capture"); require(::_pipe(guest, 65536, _O_BINARY) == 0, "cannot create guest capture"); @@ -1074,7 +1082,7 @@ void verifyLongHostPaths() { const auto extended = [](const auto& path) { return std::filesystem::path(L"\\\\?\\" + path.native()); }; std::filesystem::create_directories(extended(directory)); setEnvironment("ANYPS5_NO_SHADER_CACHE", "0"); - setEnvironment("ANYPS5_SHADER_CACHE_DIR", directory.string()); + setPathEnvironment("ANYPS5_SHADER_CACHE_DIR", directory); std::vector files; std::filesystem::path entries; struct Cleanup { @@ -2530,7 +2538,7 @@ int main(int argc, char** argv) { std::filesystem::remove_all(directory); verifyDefaultDirectory(argv[0]); setEnvironment("ANYPS5_NO_SHADER_CACHE", "0"); - setEnvironment("ANYPS5_SHADER_CACHE_DIR", directory.string()); + setPathEnvironment("ANYPS5_SHADER_CACHE_DIR", directory); verifyAtomicReplacement(directory); verifyResultRoundTrip(); verifyEntryRoundTrip(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ColorLogicOp.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ColorLogicOp.cpp new file mode 100644 index 0000000000..89963aafb1 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ColorLogicOp.cpp @@ -0,0 +1,155 @@ +#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 Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::uint32_t WaveSize = 64; +alignas(256) std::array Pixels{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0x7e0802ff, 0x3e4ccccd, 0x7e0a02ff, 0x3ecccccd, 0x7e0c02ff, 0x3f19999a, 0x7e0e02ff, 0x3f4ccccd, + 0xf800180f, 0x07060504, 0xbf810000, +}; + +constexpr std::array Source{0x33, 0x66, 0x99, 0xcc}; + +constexpr std::array, 3> Vertices{{ + {-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, 3> VertexStorage = Vertices; +std::array Before{}; + + +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 Draw(AgcDriver::VulkanDevice& device, std::uint8_t rop) { + for (std::size_t index = 0; index < Pixels.size(); ++index) Before[index] = Pixels[index] = static_cast((index * 37u + 11u) & 0xffu); + const auto target = device.Target(); + + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(VertexStorage.data(), 16u, static_cast(VertexStorage.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {WaveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixel.posX = true; + pixel.posY = true; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + const std::vector pixelUserData(8, 0u); + const std::array pixelMemory{{{reinterpret_cast(PixelCode.data()), std::as_bytes(std::span(PixelCode))}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, + {WaveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, WaveSize, WaveSize, std::nullopt, std::nullopt}; + state.color = {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.colorRop = rop; + 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(VertexStorage.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +std::uint8_t Combine(std::uint8_t rop, std::uint8_t destination, std::uint8_t source) { + switch (rop) { + case 0xcc: return source; + case 0x5a: return static_cast(destination ^ source); + case 0x88: return static_cast(destination & source); + case 0xee: return static_cast(destination | source); + default: throw std::runtime_error("the test has no expectation for this ROP"); + } +} + +void Check(std::uint8_t rop, std::string_view what) { + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + for (std::uint32_t channel = 0; channel < 4; ++channel) { + const auto index = (y * Width + x) * 4u + channel; + const auto expected = Combine(rop, static_cast(Before[index]), Source[channel]); + const auto actual = static_cast(Pixels[index]); + Require(actual == expected, std::string(what) + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel) + " is " + std::to_string(actual) + ", expected " + std::to_string(expected)); + } + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const std::uint8_t rop : {0xcc, 0x5a, 0x88, 0xee, 0xcc}) { + try { + Draw(*device, rop); + } catch (const std::exception& error) { + Require(std::string_view(error.what()).find("device does not support color logic operations") != std::string_view::npos, std::string("unexpected rejection: ") + error.what()); + std::puts("skipped, the device has no logicOp feature"); + return VulkanTestSkipped; + } + char what[48]; + std::snprintf(what, sizeof(what), "ROP3 0x%02x into an 8_8_8_8 unorm target", rop); + Check(rop, what); + } + std::puts("color logic operation tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp index db4b9a1841..4145b544b5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp @@ -76,6 +76,8 @@ public: Device() { #ifdef _WIN32 library = SDL_LoadObject("vulkan-1.dll"); +#elif defined(__APPLE__) + library = SDL_LoadObject("libvulkan.1.dylib"); #else library = SDL_LoadObject("libvulkan.so.1"); #endif @@ -87,6 +89,12 @@ public: application.apiVersion = VK_API_VERSION_1_1; VkInstanceCreateInfo info{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; info.pApplicationInfo = &application; +#ifdef __APPLE__ + const char* portability = VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME; + info.flags = VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; + info.enabledExtensionCount = 1; + info.ppEnabledExtensionNames = &portability; +#endif Check(function("vkCreateInstance")(&info, nullptr, &instance), "vkCreateInstance"); std::uint32_t count = 0; const auto enumerate = function("vkEnumeratePhysicalDevices"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp index 06da54f66b..0c706f407d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp @@ -38,6 +38,8 @@ public: Device() { #ifdef _WIN32 library = SDL_LoadObject("vulkan-1.dll"); +#elif defined(__APPLE__) + library = SDL_LoadObject("libvulkan.1.dylib"); #else library = SDL_LoadObject("libvulkan.so.1"); #endif @@ -49,6 +51,12 @@ public: application.apiVersion = VK_API_VERSION_1_1; VkInstanceCreateInfo info{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; info.pApplicationInfo = &application; +#ifdef __APPLE__ + const char* portability = VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME; + info.flags = VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; + info.enabledExtensionCount = 1; + info.ppEnabledExtensionNames = &portability; +#endif Check(function("vkCreateInstance")(&info, nullptr, &instance), "vkCreateInstance"); std::uint32_t count = 0; const auto enumerate = function("vkEnumeratePhysicalDevices"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthSurfaceLastWriter.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthSurfaceLastWriter.cpp index 8afad65394..cdb726b3bd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthSurfaceLastWriter.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthSurfaceLastWriter.cpp @@ -37,6 +37,8 @@ public: Device() { #ifdef _WIN32 library = SDL_LoadObject("vulkan-1.dll"); +#elif defined(__APPLE__) + library = SDL_LoadObject("libvulkan.1.dylib"); #else library = SDL_LoadObject("libvulkan.so.1"); #endif @@ -48,6 +50,12 @@ public: application.apiVersion = VK_API_VERSION_1_1; VkInstanceCreateInfo info{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; info.pApplicationInfo = &application; +#ifdef __APPLE__ + const char* portability = VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME; + info.flags = VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; + info.enabledExtensionCount = 1; + info.ppEnabledExtensionNames = &portability; +#endif Check(function("vkCreateInstance")(&info, nullptr, &instance), "vkCreateInstance"); std::uint32_t count = 0; const auto enumerate = function("vkEnumeratePhysicalDevices"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheRelocation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheRelocation.cpp index a82e34a36a..45f1e60352 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheRelocation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheRelocation.cpp @@ -135,6 +135,69 @@ void MovedDataHitsTheCache(bool libraries) { Require(again.lookups == 10 && again.hits == 5, mode + ": a draw at an address seen before did not reuse the cached draw (" + std::to_string(again.hits) + " hits)"); } +void EvictedEntriesServeNoRelocation(bool libraries) { + Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); + Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + Registered(SecondVertexHeader, SecondVertexCode.data(), sizeof(SecondVertexCode), 2); + const auto mode = std::string(libraries ? "with" : "without") + " graphics pipeline libraries"; + const auto draw = [&](const float* source, const std::uint32_t* vertexCode) { + auto commands = DrawFrom({source}, vertexCode); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); + return AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + }; + static std::array, 6> same{}; + for (auto& entry : same) entry = {1.0f, 0.0f, 0.0f, 0.0f}; + alignas(16) static std::array, 3> others{}; + for (std::size_t i = 0; i < others.size(); ++i) others[i] = {0.25f + 0.03125f * static_cast(i), 0.0f, 0.0f, 0.0f}; + AgcDriver::DriverDetail::DrawEntryCounters counters{}; + for (std::size_t i = 0; i < 4; ++i) counters = draw(same[i].data(), VertexCode.data()); + Require(counters.lookups == 4 && counters.hits == 0 && counters.inserts == 2, mode + ": the draws that teach the moved pointer and its contents made " + std::to_string(counters.hits) + " hits and " + std::to_string(counters.inserts) + " inserts, expected 0 and 2"); + const auto relocated = draw(same[4].data(), VertexCode.data()); + Require(relocated.hits == 1, mode + ": a draw whose data moved after the teaching draws did not reuse the cached draw (" + std::to_string(relocated.hits) + " hits)"); + auto evicted = relocated; + for (const auto& entry : others) { + draw(entry.data(), SecondVertexCode.data()); + evicted = draw(entry.data(), SecondVertexCode.data()); + } + Require(evicted.evictions - relocated.evictions >= 2 && evicted.inserts - relocated.inserts >= 2, mode + ": draws of another vertex shader did not evict the two taught entries from the draw cache (" + std::to_string(evicted.inserts - relocated.inserts) + " inserts, " + std::to_string(evicted.evictions - relocated.evictions) + " evictions; run with APS5_DRAW_CACHE_ENTRIES=2)"); + const auto after = draw(same[5].data(), VertexCode.data()); + Require(after.lookups == evicted.lookups + 1 && after.hits == evicted.hits, mode + ": a draw whose data moved reused a draw the cache had evicted, through its relocation alias (" + std::to_string(after.hits - evicted.hits) + " hits)"); +} + +void RelocatingDrawsKeepTheirDonor(bool libraries) { + Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); + Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + Registered(SecondVertexHeader, SecondVertexCode.data(), sizeof(SecondVertexCode), 2); + const auto mode = std::string(libraries ? "with" : "without") + " graphics pipeline libraries"; + const auto draw = [&](const float* source, const std::uint32_t* vertexCode) { + auto commands = DrawFrom({source}, vertexCode); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); + return AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + }; + constexpr std::size_t Rounds = 3; + alignas(16) static std::array, Rounds> others{}; + for (std::size_t i = 0; i < others.size(); ++i) others[i] = {0.25f + 0.03125f * static_cast(i), 0.0f, 0.0f, 0.0f}; + static std::array, 5 + Rounds> same{}; + for (auto& entry : same) entry = {1.0f, 0.0f, 0.0f, 0.0f}; + AgcDriver::DriverDetail::DrawEntryCounters counters{}; + for (std::size_t i = 0; i < 4; ++i) counters = draw(same[i].data(), VertexCode.data()); + Require(counters.lookups == 4 && counters.hits == 0 && counters.inserts == 2, mode + ": the draws that teach the moved pointer and its contents made " + std::to_string(counters.hits) + " hits and " + std::to_string(counters.inserts) + " inserts, expected 0 and 2"); + const auto start = draw(same[4].data(), VertexCode.data()); + Require(start.hits == 1, mode + ": a draw whose data moved after the teaching draws did not reuse the cached draw (" + std::to_string(start.hits) + " hits)"); + auto relocated = start; + for (std::size_t i = 0; i < others.size(); ++i) { + draw(others[i].data(), SecondVertexCode.data()); + const auto inserted = draw(others[i].data(), SecondVertexCode.data()); + relocated = draw(same[5 + i].data(), VertexCode.data()); + Require(relocated.hits == inserted.hits + 1, mode + ": a draw whose data moved again lost the cached draw it reused before each of " + std::to_string(i + 1) + " rounds of another vertex shader's draws (" + std::to_string(inserted.inserts - start.inserts) + " inserts, " + std::to_string(inserted.evictions - start.evictions) + " evictions)"); + } + Require(relocated.evictions - start.evictions >= 2, mode + ": draws of another vertex shader evicted " + std::to_string(relocated.evictions - start.evictions) + " entries, expected at least 2 (run with APS5_DRAW_CACHE_ENTRIES=2)"); +} + void SplitRunsNameTheirSharedWords() { alignas(16) static std::array data{1u, 2u, 3u, 4u}; auto table = std::make_shared(); @@ -228,9 +291,11 @@ void MovedForwardedWordReachesStageDataHit(bool libraries) { } -int main() { +int main(int argc, char** argv) { try { - SplitRunsNameTheirSharedWords(); + const bool evicted = argc == 2 && std::string(argv[1]) == "--evicted"; + const bool kept = argc == 2 && std::string(argv[1]) == "--kept"; + if (!evicted && !kept) SplitRunsNameTheirSharedWords(); bool libraries = false; { const auto device = OpenVulkanTestDevice(); @@ -238,6 +303,18 @@ int main() { libraries = device->GraphicsPipelineLibraries(); } if (!libraries && std::getenv("APS5_NO_GPL") == nullptr) return VulkanTestSkipped; + if (evicted) { + EvictedEntriesServeNoRelocation(libraries); + AgcDriverShutdown_nid_postfix(); + std::cout << "An evicted draw served no relocation " << (libraries ? "with" : "without") << " graphics pipeline libraries\n"; + return 0; + } + if (kept) { + RelocatingDrawsKeepTheirDonor(libraries); + AgcDriverShutdown_nid_postfix(); + std::cout << "A draw relocated from a cached draw kept it cached " << (libraries ? "with" : "without") << " graphics pipeline libraries\n"; + return 0; + } MovedDataHitsTheCache(libraries); UnrelocatedAliasesStayOut(libraries); MovedForwardedWordReachesStageDataHit(libraries); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F32AddMulDenormalModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F32AddMulDenormalModes.cpp new file mode 100644 index 0000000000..7b5780067d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F32AddMulDenormalModes.cpp @@ -0,0 +1,244 @@ +#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 = 96; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 16; +constexpr std::uint32_t Instructions = 7; +constexpr std::uint32_t FirstMultiply = 3; +constexpr std::uint32_t FirstConstantFactor = 5; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020084u, 0x34040086u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x06140b04u, 0x08160b04u, 0x0a180b04u, + 0x101a0b04u, 0x0e1c0b04u, 0x101e08ffu, 0x3fc00000u, 0xd5070010u, 0x0001ff04u, 0x3fc00000u, 0xe0781000u, + 0x80010a02u, 0xe07c1010u, 0x80010e02u, 0xbf810000u, +}; + +constexpr const char* Names[Instructions] = {"v_add_f32", "v_sub_f32", "v_subrev_f32", "v_mul_f32", "v_mul_legacy_f32", "v_mul_f32 1.5", "v_mul_legacy_f32 1.5"}; + +template +struct Vector { + std::uint32_t operands[2]; + std::uint32_t outputsFlushed[Count]; + std::uint32_t inputsFlushed[Count]; +}; + +constexpr std::array, 40> ArithmeticVectors{{ + {{0x00000000u, 0x00000001u}, {0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {{0x00000001u, 0x00800002u}, {0x00800003u, 0x80800001u, 0x00800001u, 0x00000000u, 0x00000000u}, {0x00800002u, 0x80800002u, 0x00800002u, 0x00000000u, 0x00000000u}}, + {{0x00000001u, 0x81000001u}, {0x81000000u, 0x01000002u, 0x81000002u, 0x80000000u, 0x80000000u}, {0x81000001u, 0x01000001u, 0x81000001u, 0x80000000u, 0x00000000u}}, + {{0x00000002u, 0x3f800000u}, {0x3f800000u, 0xbf800000u, 0x3f800000u, 0x00000000u, 0x00000000u}, {0x3f800000u, 0xbf800000u, 0x3f800000u, 0x00000000u, 0x00000000u}}, + {{0x00000002u, 0xff7fffffu}, {0xff7fffffu, 0x7f7fffffu, 0xff7fffffu, 0xb57fffffu, 0xb57fffffu}, {0xff7fffffu, 0x7f7fffffu, 0xff7fffffu, 0x80000000u, 0x00000000u}}, + {{0x00000003u, 0x80000001u}, {0x00000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x80000000u}, {0x00000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x00000000u}}, + {{0x00400000u, 0x00000002u}, {0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {{0x00400000u, 0x807fffffu}, {0x80000000u, 0x00bfffffu, 0x80bfffffu, 0x80000000u, 0x80000000u}, {0x00000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x00000000u}}, + {{0x007fffffu, 0x00800000u}, {0x00ffffffu, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00800000u, 0x80800000u, 0x00800000u, 0x00000000u, 0x00000000u}}, + {{0x007fffffu, 0x80800002u}, {0x80000000u, 0x01000000u, 0x81000000u, 0x80000000u, 0x80000000u}, {0x80800002u, 0x00800002u, 0x80800002u, 0x80000000u, 0x00000000u}}, + {{0x00800000u, 0x80000001u}, {0x00000000u, 0x00800001u, 0x80800001u, 0x80000000u, 0x80000000u}, {0x00800000u, 0x00800000u, 0x80800000u, 0x80000000u, 0x00000000u}}, + {{0x00800001u, 0x33800000u}, {0x33800000u, 0xb3800000u, 0x33800000u, 0x00000000u, 0x00000000u}, {0x33800000u, 0xb3800000u, 0x33800000u, 0x00000001u, 0x00000001u}}, + {{0x00800002u, 0x00800000u}, {0x01000001u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u}, {0x01000001u, 0x00000002u, 0x80000002u, 0x00000000u, 0x00000000u}}, + {{0x00ffffffu, 0x00000002u}, {0x01000000u, 0x00fffffdu, 0x80fffffdu, 0x00000000u, 0x00000000u}, {0x00ffffffu, 0x00ffffffu, 0x80ffffffu, 0x00000000u, 0x00000000u}}, + {{0x00ffffffu, 0x80800002u}, {0x00000000u, 0x01400000u, 0x81400000u, 0x80000000u, 0x80000000u}, {0x007ffffdu, 0x01400000u, 0x81400000u, 0x80000000u, 0x80000000u}}, + {{0x01000000u, 0x80000003u}, {0x00fffffdu, 0x01000002u, 0x81000002u, 0x80000000u, 0x80000000u}, {0x01000000u, 0x01000000u, 0x81000000u, 0x80000000u, 0x00000000u}}, + {{0x01000001u, 0x80000001u}, {0x01000000u, 0x01000002u, 0x81000002u, 0x80000000u, 0x80000000u}, {0x01000001u, 0x01000001u, 0x81000001u, 0x80000000u, 0x00000000u}}, + {{0x3f800000u, 0x807fffffu}, {0x3f800000u, 0x3f800000u, 0xbf800000u, 0x80000000u, 0x80000000u}, {0x3f800000u, 0x3f800000u, 0xbf800000u, 0x80000000u, 0x00000000u}}, + {{0x34000000u, 0x00800001u}, {0x34000000u, 0x34000000u, 0xb4000000u, 0x00000000u, 0x00000000u}, {0x34000000u, 0x34000000u, 0xb4000000u, 0x00000001u, 0x00000001u}}, + {{0x7f7fffffu, 0x00000003u}, {0x7f7fffffu, 0x7f7fffffu, 0xff7fffffu, 0x35bfffffu, 0x35bfffffu}, {0x7f7fffffu, 0x7f7fffffu, 0xff7fffffu, 0x00000000u, 0x00000000u}}, + {{0x80000000u, 0x00000001u}, {0x00000000u, 0x80000000u, 0x00000000u, 0x80000000u, 0x00000000u}, {0x00000000u, 0x80000000u, 0x00000000u, 0x80000000u, 0x00000000u}}, + {{0x80000001u, 0x00800002u}, {0x00800001u, 0x80800003u, 0x00800003u, 0x80000000u, 0x80000000u}, {0x00800002u, 0x80800002u, 0x00800002u, 0x80000000u, 0x00000000u}}, + {{0x80000001u, 0x80ffffffu}, {0x81000000u, 0x00fffffeu, 0x80fffffeu, 0x00000000u, 0x00000000u}, {0x80ffffffu, 0x00ffffffu, 0x80ffffffu, 0x00000000u, 0x00000000u}}, + {{0x80000002u, 0x3f800000u}, {0x3f800000u, 0xbf800000u, 0x3f800000u, 0x80000000u, 0x80000000u}, {0x3f800000u, 0xbf800000u, 0x3f800000u, 0x80000000u, 0x00000000u}}, + {{0x80000002u, 0xff7fffffu}, {0xff7fffffu, 0x7f7fffffu, 0xff7fffffu, 0x357fffffu, 0x357fffffu}, {0xff7fffffu, 0x7f7fffffu, 0xff7fffffu, 0x00000000u, 0x00000000u}}, + {{0x80000003u, 0x7f800000u}, {0x7f800000u, 0xff800000u, 0x7f800000u, 0xff800000u, 0xff800000u}, {0x7f800000u, 0xff800000u, 0x7f800000u, 0xffc00000u, 0x00000000u}}, + {{0x80400000u, 0x00000002u}, {0x80000000u, 0x80000000u, 0x00000000u, 0x80000000u, 0x80000000u}, {0x00000000u, 0x80000000u, 0x00000000u, 0x80000000u, 0x00000000u}}, + {{0x80400000u, 0x80000003u}, {0x80000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {{0x807fffffu, 0x00800000u}, {0x00000000u, 0x80ffffffu, 0x00ffffffu, 0x80000000u, 0x80000000u}, {0x00800000u, 0x80800000u, 0x00800000u, 0x80000000u, 0x00000000u}}, + {{0x807fffffu, 0x80800001u}, {0x81000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u}, {0x80800001u, 0x00800001u, 0x80800001u, 0x00000000u, 0x00000000u}}, + {{0x80800000u, 0x80000001u}, {0x80800001u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x80800000u, 0x80800000u, 0x00800000u, 0x00000000u, 0x00000000u}}, + {{0x80800001u, 0x33800000u}, {0x33800000u, 0xb3800000u, 0x33800000u, 0x80000000u, 0x80000000u}, {0x33800000u, 0xb3800000u, 0x33800000u, 0x80000001u, 0x80000001u}}, + {{0x80800002u, 0x00800000u}, {0x80000000u, 0x81000001u, 0x01000001u, 0x80000000u, 0x80000000u}, {0x80000002u, 0x81000001u, 0x01000001u, 0x80000000u, 0x80000000u}}, + {{0x80ffffffu, 0x00000002u}, {0x80fffffdu, 0x81000000u, 0x01000000u, 0x80000000u, 0x80000000u}, {0x80ffffffu, 0x80ffffffu, 0x00ffffffu, 0x80000000u, 0x00000000u}}, + {{0x80ffffffu, 0x80800002u}, {0x81400000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x81400000u, 0x807ffffdu, 0x007ffffdu, 0x00000000u, 0x00000000u}}, + {{0x81000000u, 0x80000003u}, {0x81000002u, 0x80fffffdu, 0x00fffffdu, 0x00000000u, 0x00000000u}, {0x81000000u, 0x81000000u, 0x01000000u, 0x00000000u, 0x00000000u}}, + {{0x81000001u, 0x80000001u}, {0x81000002u, 0x81000000u, 0x01000000u, 0x00000000u, 0x00000000u}, {0x81000001u, 0x81000001u, 0x01000001u, 0x00000000u, 0x00000000u}}, + {{0xbf800000u, 0x807fffffu}, {0xbf800000u, 0xbf800000u, 0x3f800000u, 0x00000000u, 0x00000000u}, {0xbf800000u, 0xbf800000u, 0x3f800000u, 0x00000000u, 0x00000000u}}, + {{0xb4000000u, 0x00000003u}, {0xb4000000u, 0xb4000000u, 0x34000000u, 0x80000000u, 0x80000000u}, {0xb4000000u, 0xb4000000u, 0x34000000u, 0x80000000u, 0x00000000u}}, + {{0xff7fffffu, 0x00000003u}, {0xff7fffffu, 0xff7fffffu, 0x7f7fffffu, 0xb5bfffffu, 0xb5bfffffu}, {0xff7fffffu, 0xff7fffffu, 0x7f7fffffu, 0x80000000u, 0x00000000u}}, +}}; + +constexpr std::array, 24> TinyProductVectors{{ + {{0x3f7ffffeu, 0x00800001u}, {0x00800000u, 0x00800000u}, {0x00800000u, 0x00800000u}}, + {{0x3f7ffffeu, 0x80800001u}, {0x80800000u, 0x80800000u}, {0x80800000u, 0x80800000u}}, + {{0xbf7ffffeu, 0x00800001u}, {0x80800000u, 0x80800000u}, {0x80800000u, 0x80800000u}}, + {{0xbf7ffffeu, 0x80800001u}, {0x00800000u, 0x00800000u}, {0x00800000u, 0x00800000u}}, + {{0x3f800001u, 0x007fffffu}, {0x00800000u, 0x00800000u}, {0x00000000u, 0x00000000u}}, + {{0x3f800001u, 0x807fffffu}, {0x80800000u, 0x80800000u}, {0x80000000u, 0x00000000u}}, + {{0xbf800001u, 0x007fffffu}, {0x80800000u, 0x80800000u}, {0x80000000u, 0x00000000u}}, + {{0xbf800001u, 0x807fffffu}, {0x00800000u, 0x00800000u}, {0x00000000u, 0x00000000u}}, + {{0x3f800002u, 0x007ffffeu}, {0x00800000u, 0x00800000u}, {0x00000000u, 0x00000000u}}, + {{0x3f800002u, 0x807ffffeu}, {0x80800000u, 0x80800000u}, {0x80000000u, 0x00000000u}}, + {{0xbf800002u, 0x007ffffeu}, {0x80800000u, 0x80800000u}, {0x80000000u, 0x00000000u}}, + {{0xbf800002u, 0x807ffffeu}, {0x00800000u, 0x00800000u}, {0x00000000u, 0x00000000u}}, + {{0x3f7ffffeu, 0x00800000u}, {0x00000000u, 0x00000000u}, {0x007fffffu, 0x007fffffu}}, + {{0xbf7fffffu, 0x00400000u}, {0x80000000u, 0x80000000u}, {0x80000000u, 0x00000000u}}, + {{0x3f800001u, 0x80000001u}, {0x80000000u, 0x80000000u}, {0x80000000u, 0x00000000u}}, + {{0xbfffffffu, 0x00400000u}, {0x80000000u, 0x80000000u}, {0x80000000u, 0x00000000u}}, + {{0x3f400000u, 0x00800000u}, {0x00000000u, 0x00000000u}, {0x00600000u, 0x00600000u}}, + {{0x40000000u, 0x807fffffu}, {0x80fffffeu, 0x80fffffeu}, {0x80000000u, 0x00000000u}}, + {{0xcb000000u, 0x007ffffeu}, {0x8bfffffcu, 0x8bfffffcu}, {0x80000000u, 0x00000000u}}, + {{0xcb7fffffu, 0x807ffffeu}, {0x0c7ffffbu, 0x0c7ffffbu}, {0x00000000u, 0x00000000u}}, + {{0x5f800000u, 0x00400000u}, {0x20000000u, 0x20000000u}, {0x00000000u, 0x00000000u}}, + {{0x7f000000u, 0x80400000u}, {0xbf800000u, 0xbf800000u}, {0x80000000u, 0x00000000u}}, + {{0xb3800000u, 0x00ffffffu}, {0x80000000u, 0x80000000u}, {0x80000001u, 0x80000001u}}, + {{0xb4000000u, 0x80800000u}, {0x00000000u, 0x00000000u}, {0x00000001u, 0x00000001u}}, +}}; + +constexpr std::array, 32> ConstantFactorVectors{{ + {{0x00000001u, 0x00000001u}, {0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u}}, + {{0x00000002u, 0x00000001u}, {0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u}}, + {{0x00000003u, 0x00000001u}, {0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u}}, + {{0x00155555u, 0x00000001u}, {0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u}}, + {{0x003fffffu, 0x00000001u}, {0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u}}, + {{0x00400000u, 0x00000001u}, {0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u}}, + {{0x00400001u, 0x00000001u}, {0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u}}, + {{0x00555555u, 0x00000001u}, {0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u}}, + {{0x007ffffeu, 0x00000001u}, {0x00bffffdu, 0x00bffffdu}, {0x00000000u, 0x00000000u}}, + {{0x007fffffu, 0x00000001u}, {0x00bffffeu, 0x00bffffeu}, {0x00000000u, 0x00000000u}}, + {{0x00800000u, 0x00000001u}, {0x00c00000u, 0x00c00000u}, {0x00c00000u, 0x00c00000u}}, + {{0x00800001u, 0x00000001u}, {0x00c00002u, 0x00c00002u}, {0x00c00002u, 0x00c00002u}}, + {{0x00aaaaabu, 0x00000001u}, {0x01000000u, 0x01000000u}, {0x01000000u, 0x01000000u}}, + {{0x00ffffffu, 0x00000001u}, {0x013fffffu, 0x013fffffu}, {0x013fffffu, 0x013fffffu}}, + {{0x01000000u, 0x00000001u}, {0x01400000u, 0x01400000u}, {0x01400000u, 0x01400000u}}, + {{0x3f800000u, 0x00000001u}, {0x3fc00000u, 0x3fc00000u}, {0x3fc00000u, 0x3fc00000u}}, + {{0x00000000u, 0x00000001u}, {0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u}}, + {{0x80000001u, 0x00000001u}, {0x80000000u, 0x80000000u}, {0x80000000u, 0x00000000u}}, + {{0x80000002u, 0x00000001u}, {0x80000000u, 0x80000000u}, {0x80000000u, 0x00000000u}}, + {{0x80000003u, 0x00000001u}, {0x80000000u, 0x80000000u}, {0x80000000u, 0x00000000u}}, + {{0x80155555u, 0x00000001u}, {0x80000000u, 0x80000000u}, {0x80000000u, 0x00000000u}}, + {{0x803fffffu, 0x00000001u}, {0x80000000u, 0x80000000u}, {0x80000000u, 0x00000000u}}, + {{0x80400000u, 0x00000001u}, {0x80000000u, 0x80000000u}, {0x80000000u, 0x00000000u}}, + {{0x80400001u, 0x00000001u}, {0x80000000u, 0x80000000u}, {0x80000000u, 0x00000000u}}, + {{0x80555555u, 0x00000001u}, {0x80000000u, 0x80000000u}, {0x80000000u, 0x00000000u}}, + {{0x807ffffeu, 0x00000001u}, {0x80bffffdu, 0x80bffffdu}, {0x80000000u, 0x00000000u}}, + {{0x807fffffu, 0x00000001u}, {0x80bffffeu, 0x80bffffeu}, {0x80000000u, 0x00000000u}}, + {{0x80800000u, 0x00000001u}, {0x80c00000u, 0x80c00000u}, {0x80c00000u, 0x80c00000u}}, + {{0x80800001u, 0x00000001u}, {0x80c00002u, 0x80c00002u}, {0x80c00002u, 0x80c00002u}}, + {{0x80aaaaabu, 0x00000001u}, {0x81000000u, 0x81000000u}, {0x81000000u, 0x81000000u}}, + {{0x80ffffffu, 0x00000001u}, {0x813fffffu, 0x813fffffu}, {0x813fffffu, 0x813fffffu}}, + {{0x81000000u, 0x00000001u}, {0x81400000u, 0x81400000u}, {0x81400000u, 0x81400000u}}, +}}; + +constexpr std::uint32_t FirstTinyProductLane = static_cast(ArithmeticVectors.size()); +constexpr std::uint32_t FirstConstantFactorLane = FirstTinyProductLane + static_cast(TinyProductVectors.size()); +static_assert(FirstConstantFactorLane + ConstantFactorVectors.size() <= Threads); + +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; +} + +template +void Fill(const std::array, Size>& vectors, std::uint32_t firstLane) { + for (std::uint32_t row = 0; row < Size; ++row) { + Input[(firstLane + row) * Inputs] = vectors[row].operands[0]; + Input[(firstLane + row) * Inputs + 1u] = vectors[row].operands[1]; + } +} + +bool Run(AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloatMode floatMode) { + Input.fill(0u); + Fill(ArithmeticVectors, 0u); + Fill(TinyProductVectors, FirstTinyProductLane); + Fill(ConstantFactorVectors, FirstConstantFactorLane); + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = floatMode; + std::optional result; + try { + result.emplace(ShaderRecompiler::Recompile(request)); + } catch (const std::exception& error) { + const std::string message = error.what(); + const bool keepsDenormals = request.target.float32DenormPreserve && !request.target.float32FlushToZero; + Require(!keepsDenormals && message.find("needs DenormPreserve 32") != std::string::npos, "f32 add and multiply FLOAT_MODE " + Hex(floatMode.floatMode) + ": " + message); + return false; + } + device.Dispatch(*result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + return true; +} + +template +void Check(const std::array, Size>& vectors, std::uint32_t firstLane, std::uint32_t firstInstruction, std::uint32_t denormals, bool ieee) { + for (std::uint32_t row = 0; row < Size; ++row) { + const auto& vector = vectors[row]; + for (std::uint32_t index = 0; index < Count; ++index) { + const std::uint32_t instruction = firstInstruction + index; + const auto actual = Output[(firstLane + row) * Results + instruction]; + const auto expected = denormals == 1u ? vector.outputsFlushed[index] : vector.inputsFlushed[index]; + Require(actual == expected, std::string(Names[instruction]) + " f32 denormal mode " + std::to_string(denormals) + (ieee ? ", IEEE=1" : ", IEEE=0") + " of " + + Hex(vector.operands[0]) + ", " + Hex(vector.operands[1]) + " is " + Hex(actual) + ", expected " + Hex(expected)); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + std::uint32_t checked = 0u; + for (const std::uint32_t denormals : {1u, 2u}) { + for (const bool ieee : {false, true}) { + if (!Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u | denormals << 4u, true, ieee, false})) continue; + ++checked; + Check(ArithmeticVectors, 0u, 0u, denormals, ieee); + Check(TinyProductVectors, FirstTinyProductLane, FirstMultiply, denormals, ieee); + Check(ConstantFactorVectors, FirstConstantFactorLane, FirstConstantFactor, denormals, ieee); + } + } + std::printf("f32 add and multiply denormal mode 1 and 2 tests passed (%u of 4 modes run; the others need DenormPreserve 32)\n", checked); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MullitF32DenormalModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MullitF32DenormalModes.cpp new file mode 100644 index 0000000000..2dd9886984 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MullitF32DenormalModes.cpp @@ -0,0 +1,226 @@ +#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 ConstantForms = 4; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array MullitCode{ + 0x34020084u, 0x34040082u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e140280u, 0xd550000au, 0x041a0b04u, + 0xe0701000u, 0x80010a02u, 0xbf810000u, +}; + +alignas(256) constexpr std::array ConstantMullitCode{ + 0x34020084u, 0x34040084u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0xd550000au, 0x041a08f2u, 0xd550000bu, + 0x0419e904u, 0xd550000cu, 0x041a0a81u, 0xd550000du, 0x041a08ffu, 0x3fc00000u, 0xe0781000u, 0x80010a02u, + 0xbf810000u, +}; + +constexpr const char* ConstantNames[ConstantForms] = {"v_mullit_f32 1.0, v4, v6", "v_mullit_f32 v4, 2.0, v6", "v_mullit_f32 0x1, v5, v6", "v_mullit_f32 0x3fc00000, v4, v6"}; + +struct Vector { + std::uint32_t lhs; + std::uint32_t rhs; + std::uint32_t limit; + std::uint32_t expected[8]; +}; + +constexpr Vector Vectors[] = { + {0x00000000u, 0x00000000u, 0x00000001u, {0xff7fffffu, 0xff7fffffu, 0x00000000u, 0x00000000u, 0xff7fffffu, 0xff7fffffu, 0x00000000u, 0x00000000u}}, + {0x00000000u, 0x00800000u, 0x00000001u, {0xff7fffffu, 0xff7fffffu, 0x00000000u, 0x00000000u, 0xff7fffffu, 0xff7fffffu, 0x00000000u, 0x00000000u}}, + {0x00000001u, 0x80000001u, 0x00800000u, {0x00000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x80000000u}}, + {0x00000001u, 0x3f800000u, 0x00000001u, {0xff7fffffu, 0xff7fffffu, 0x00000000u, 0x00000000u, 0xff7fffffu, 0xff7fffffu, 0x00000001u, 0x00000001u}}, + {0x00000001u, 0x3f800000u, 0x00800000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u}}, + {0x00000001u, 0xbf800000u, 0x00800000u, {0x00000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x80000001u, 0x80000001u}}, + {0x00000001u, 0x7f7fffffu, 0x00800000u, {0x00000000u, 0x00000000u, 0x34ffffffu, 0x34ffffffu, 0x00000000u, 0x00000000u, 0x34ffffffu, 0x34ffffffu}}, + {0x00800000u, 0x00000000u, 0x00000001u, {0xff7fffffu, 0xff7fffffu, 0x00000000u, 0x00000000u, 0xff7fffffu, 0xff7fffffu, 0x00000000u, 0x00000000u}}, + {0x00800000u, 0x80000001u, 0x00800000u, {0x00000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x80000000u}}, + {0x00800000u, 0x00800000u, 0x00000001u, {0xff7fffffu, 0xff7fffffu, 0x00000000u, 0x00000000u, 0xff7fffffu, 0xff7fffffu, 0x00000000u, 0x00000000u}}, + {0x00800000u, 0x3f000000u, 0x00000001u, {0xff7fffffu, 0xff7fffffu, 0x00000000u, 0x00000000u, 0xff7fffffu, 0xff7fffffu, 0x00400000u, 0x00400000u}}, + {0x00800000u, 0x3f000000u, 0x00800000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00400000u, 0x00400000u, 0x00400000u, 0x00400000u}}, + {0x3f000000u, 0x007fffffu, 0x00000001u, {0xff7fffffu, 0xff7fffffu, 0x00000000u, 0x00000000u, 0xff7fffffu, 0xff7fffffu, 0x00400000u, 0x00400000u}}, + {0x3f000000u, 0x007fffffu, 0x00800000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00400000u, 0x00400000u}}, + {0x3f000000u, 0x807fffffu, 0x00800000u, {0x00000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x80400000u, 0x80400000u}}, + {0x7f800001u, 0x00000001u, 0x00000001u, {0xff7fffffu, 0xff7fffffu, 0x7f800001u, 0x7fc00001u, 0xff7fffffu, 0xff7fffffu, 0x7f800001u, 0x7fc00001u}}, + {0x7f800001u, 0x00000001u, 0x00800000u, {0x00000000u, 0x00000000u, 0x7f800001u, 0x7fc00001u, 0x00000000u, 0x00000000u, 0x7f800001u, 0x7fc00001u}}, + {0x7f800001u, 0x00800000u, 0x00000001u, {0xff7fffffu, 0xff7fffffu, 0x7f800001u, 0x7fc00001u, 0xff7fffffu, 0xff7fffffu, 0x7f800001u, 0x7fc00001u}}, + {0x7f800001u, 0x00800000u, 0x00800000u, {0x7f800001u, 0x7fc00001u, 0x7f800001u, 0x7fc00001u, 0x7f800001u, 0x7fc00001u, 0x7f800001u, 0x7fc00001u}}, + {0x3f800000u, 0x40000000u, 0x3f800000u, {0x40000000u, 0x40000000u, 0x40000000u, 0x40000000u, 0x40000000u, 0x40000000u, 0x40000000u, 0x40000000u}}, + {0x3f800000u, 0xff7fffffu, 0x3f800000u, {0xff7fffffu, 0xff7fffffu, 0xff7fffffu, 0xff7fffffu, 0xff7fffffu, 0xff7fffffu, 0xff7fffffu, 0xff7fffffu}}, + {0x3f800000u, 0x3f800000u, 0x80000000u, {0xff7fffffu, 0xff7fffffu, 0xff7fffffu, 0xff7fffffu, 0xff7fffffu, 0xff7fffffu, 0xff7fffffu, 0xff7fffffu}}, + {0x7f7fffffu, 0x7f7fffffu, 0x3f800000u, {0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u}}, +}; + +struct ConstantVector { + std::uint32_t inputs[3]; + std::uint32_t expected[4][ConstantForms]; +}; + +constexpr ConstantVector ConstantVectors[] = { + {{0x00000001u, 0x00000001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000001u, 0x00000002u, 0x00000000u, 0x00000002u}}}, + {{0x00000001u, 0x00800001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000001u, 0x00000002u, 0x00000000u, 0x00000002u}}}, + {{0x00000002u, 0x00000001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u}}}, + {{0x00000002u, 0x00800001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u}}}, + {{0x00000003u, 0x00000001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000003u, 0x00000006u, 0x00000000u, 0x00000004u}}}, + {{0x00000003u, 0x00800001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000003u, 0x00000006u, 0x00000000u, 0x00000004u}}}, + {{0x00155555u, 0x00000001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00155555u, 0x002aaaaau, 0x00000000u, 0x00200000u}}}, + {{0x00155555u, 0x00800001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00155555u, 0x002aaaaau, 0x00000000u, 0x00200000u}}}, + {{0x003fffffu, 0x00000001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x003fffffu, 0x007ffffeu, 0x00000000u, 0x005ffffeu}}}, + {{0x003fffffu, 0x00800001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x003fffffu, 0x007ffffeu, 0x00000000u, 0x005ffffeu}}}, + {{0x00400000u, 0x00000001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00800000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00400000u, 0x00800000u, 0x00000000u, 0x00600000u}}}, + {{0x00400000u, 0x00800001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00800000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00400000u, 0x00800000u, 0x00000000u, 0x00600000u}}}, + {{0x00400001u, 0x00000001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00800002u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00400001u, 0x00800002u, 0x00000000u, 0x00600002u}}}, + {{0x00400001u, 0x00800001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00800002u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00400001u, 0x00800002u, 0x00000000u, 0x00600002u}}}, + {{0x00555555u, 0x00000001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00aaaaaau, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00555555u, 0x00aaaaaau, 0x00000000u, 0x00800000u}}}, + {{0x00555555u, 0x00800001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00aaaaaau, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00555555u, 0x00aaaaaau, 0x00000000u, 0x00800000u}}}, + {{0x007ffffeu, 0x00000001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00fffffcu, 0x00000000u, 0x00bffffdu}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x007ffffeu, 0x00fffffcu, 0x00000000u, 0x00bffffdu}}}, + {{0x007ffffeu, 0x00800001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00fffffcu, 0x00000000u, 0x00bffffdu}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x007ffffeu, 0x00fffffcu, 0x00000000u, 0x00bffffdu}}}, + {{0x007fffffu, 0x00000001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00fffffeu, 0x00000000u, 0x00bffffeu}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x007fffffu, 0x00fffffeu, 0x00000000u, 0x00bffffeu}}}, + {{0x007fffffu, 0x00800001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00fffffeu, 0x00000000u, 0x00bffffeu}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x007fffffu, 0x00fffffeu, 0x00000000u, 0x00bffffeu}}}, + {{0x80000001u, 0x00000001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x80000000u, 0x80000000u, 0x00000000u, 0x80000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x80000001u, 0x80000002u, 0x00000000u, 0x80000002u}}}, + {{0x80000001u, 0x00800001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x80000000u, 0x80000000u, 0x00000000u, 0x80000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x80000001u, 0x80000002u, 0x00000000u, 0x80000002u}}}, + {{0x80000002u, 0x00000001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x80000000u, 0x80000000u, 0x00000000u, 0x80000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x80000002u, 0x80000004u, 0x00000000u, 0x80000003u}}}, + {{0x80000002u, 0x00800001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x80000000u, 0x80000000u, 0x00000000u, 0x80000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x80000002u, 0x80000004u, 0x00000000u, 0x80000003u}}}, + {{0x80000003u, 0x00000001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x80000000u, 0x80000000u, 0x00000000u, 0x80000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x80000003u, 0x80000006u, 0x00000000u, 0x80000004u}}}, + {{0x80000003u, 0x00800001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x80000000u, 0x80000000u, 0x00000000u, 0x80000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x80000003u, 0x80000006u, 0x00000000u, 0x80000004u}}}, + {{0x80155555u, 0x00000001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x80000000u, 0x80000000u, 0x00000000u, 0x80000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x80155555u, 0x802aaaaau, 0x00000000u, 0x80200000u}}}, + {{0x80155555u, 0x00800001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x80000000u, 0x80000000u, 0x00000000u, 0x80000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x80155555u, 0x802aaaaau, 0x00000000u, 0x80200000u}}}, + {{0x803fffffu, 0x00000001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x80000000u, 0x80000000u, 0x00000000u, 0x80000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x803fffffu, 0x807ffffeu, 0x00000000u, 0x805ffffeu}}}, + {{0x803fffffu, 0x00800001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x80000000u, 0x80000000u, 0x00000000u, 0x80000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x803fffffu, 0x807ffffeu, 0x00000000u, 0x805ffffeu}}}, + {{0x80400000u, 0x00000001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x80000000u, 0x80800000u, 0x00000000u, 0x80000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x80400000u, 0x80800000u, 0x00000000u, 0x80600000u}}}, + {{0x80400000u, 0x00800001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x80000000u, 0x80800000u, 0x00000000u, 0x80000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x80400000u, 0x80800000u, 0x00000000u, 0x80600000u}}}, + {{0x80400001u, 0x00000001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x80000000u, 0x80800002u, 0x00000000u, 0x80000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x80400001u, 0x80800002u, 0x00000000u, 0x80600002u}}}, + {{0x80400001u, 0x00800001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x80000000u, 0x80800002u, 0x00000000u, 0x80000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x80400001u, 0x80800002u, 0x00000000u, 0x80600002u}}}, + {{0x80555555u, 0x00000001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x80000000u, 0x80aaaaaau, 0x00000000u, 0x80000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x80555555u, 0x80aaaaaau, 0x00000000u, 0x80800000u}}}, + {{0x80555555u, 0x00800001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x80000000u, 0x80aaaaaau, 0x00000000u, 0x80000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x80555555u, 0x80aaaaaau, 0x00000000u, 0x80800000u}}}, + {{0x807ffffeu, 0x00000001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x80000000u, 0x80fffffcu, 0x00000000u, 0x80bffffdu}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x807ffffeu, 0x80fffffcu, 0x00000000u, 0x80bffffdu}}}, + {{0x807ffffeu, 0x00800001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x80000000u, 0x80fffffcu, 0x00000000u, 0x80bffffdu}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x807ffffeu, 0x80fffffcu, 0x00000000u, 0x80bffffdu}}}, + {{0x807fffffu, 0x00000001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x80000000u, 0x80fffffeu, 0x00000000u, 0x80bffffeu}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x807fffffu, 0x80fffffeu, 0x00000000u, 0x80bffffeu}}}, + {{0x807fffffu, 0x00800001u, 0x3f800000u}, {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x80000000u, 0x80fffffeu, 0x00000000u, 0x80bffffeu}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x807fffffu, 0x80fffffeu, 0x00000000u, 0x80bffffeu}}}, +}; + +static_assert(std::size(Vectors) <= Threads && std::size(ConstantVectors) <= Threads); + +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; +} + +bool KeepsDenormals(const AgcDriver::VulkanDevice& device) { + const auto target = device.Target(); + return target.float32DenormPreserve && !target.float32FlushToZero; +} + +bool Run(AgcDriver::VulkanDevice& device, std::span code, std::uint32_t denorm32, bool ieee) { + 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}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = ShaderRecompiler::ShaderFloatMode{(denorm32 << 4u) | (3u << 6u), true, ieee, false}; + std::optional result; + try { + result.emplace(ShaderRecompiler::Recompile(request)); + } catch (const std::exception& error) { + const std::string message = error.what(); + Require(!KeepsDenormals(device) && (denorm32 == 1u || denorm32 == 2u) && message.find("needs DenormPreserve 32") != std::string::npos, + "v_mullit_f32 denorm32 " + std::to_string(denorm32) + ": " + message); + return false; + } + device.Dispatch(*result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + return true; +} + +void CheckRegisters(std::uint32_t denorm32, bool ieee) { + for (std::uint32_t row = 0; row < std::size(Vectors); ++row) { + const auto& vector = Vectors[row]; + const auto actual = Output[row]; + const auto expected = vector.expected[denorm32 * 2u + (ieee ? 1u : 0u)]; + Require(actual == expected, "v_mullit_f32 denorm32 " + std::to_string(denorm32) + (ieee ? " ieee" : " no ieee") + " inputs " + Hex(vector.lhs) + " " + Hex(vector.rhs) + " " + + Hex(vector.limit) + " is " + Hex(actual) + ", expected " + Hex(expected)); + } +} + +void CheckConstants(std::uint32_t denorm32, bool ieee) { + for (std::uint32_t row = 0; row < std::size(ConstantVectors); ++row) { + const auto& vector = ConstantVectors[row]; + for (std::uint32_t form = 0; form < ConstantForms; ++form) { + const auto actual = Output[row * ConstantForms + form]; + const auto expected = vector.expected[denorm32][form]; + Require(actual == expected, std::string(ConstantNames[form]) + " denorm32 " + std::to_string(denorm32) + (ieee ? " ieee" : " no ieee") + " inputs " + Hex(vector.inputs[0]) + " " + + Hex(vector.inputs[1]) + " " + Hex(vector.inputs[2]) + " is " + Hex(actual) + ", expected " + Hex(expected)); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const bool keeps = KeepsDenormals(*device); + std::uint32_t checked = 0u; + for (std::uint32_t denorm32 = 0u; denorm32 < 4u; ++denorm32) { + if (denorm32 == 3u && !keeps) continue; + for (const bool ieee : {false, true}) { + Input.fill(0u); + for (std::uint32_t row = 0; row < std::size(Vectors); ++row) { + Input[row * Inputs] = Vectors[row].lhs; + Input[row * Inputs + 1] = Vectors[row].rhs; + Input[row * Inputs + 2] = Vectors[row].limit; + } + if (Run(*device, MullitCode, denorm32, ieee)) { + CheckRegisters(denorm32, ieee); + ++checked; + } + Input.fill(0u); + for (std::uint32_t row = 0; row < std::size(ConstantVectors); ++row) std::copy(std::begin(ConstantVectors[row].inputs), std::end(ConstantVectors[row].inputs), Input.begin() + row * Inputs); + if (Run(*device, ConstantMullitCode, denorm32, ieee)) { + CheckConstants(denorm32, ieee); + ++checked; + } + } + } + std::printf("v_mullit_f32 denormal mode tests passed (%u of 16 runs checked; denormal modes 1 and 2 need DenormPreserve 32, mode 3 is checked only where the device keeps f32 denormals)\n", checked); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp index 3f9886f149..5548d5d7fa 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -55,6 +55,8 @@ public: Device() { #ifdef _WIN32 library = SDL_LoadObject("vulkan-1.dll"); +#elif defined(__APPLE__) + library = SDL_LoadObject("libvulkan.1.dylib"); #else library = SDL_LoadObject("libvulkan.so.1"); #endif @@ -66,6 +68,12 @@ public: app.apiVersion = VK_API_VERSION_1_1; VkInstanceCreateInfo info{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; info.pApplicationInfo = &app; +#ifdef __APPLE__ + const char* portability = VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME; + info.flags = VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; + info.enabledExtensionCount = 1; + info.ppEnabledExtensionNames = &portability; +#endif Check(function("vkCreateInstance")(&info, nullptr, &instance), "vkCreateInstance"); std::uint32_t count = 0u; const auto enumerate = function("vkEnumeratePhysicalDevices"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/OptimizeOnFiber.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/OptimizeOnFiber.cpp new file mode 100644 index 0000000000..36fc0c7c91 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/OptimizeOnFiber.cpp @@ -0,0 +1,148 @@ +#if defined(ANYPS5_ENABLE_SPIRV_TOOLS) && ANYPS5_ENABLE_SPIRV_TOOLS +#include "SpirvBackend/SpirvOptimizer.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include + +using GuestFiberEntry = void (APS5_VABI*)(std::uint64_t, std::uint64_t); + +extern "C" { +std::int32_t APS5_VABI _sceFiberInitializeImpl_nid_postfix(FiberObject*, const char*, GuestFiberEntry, std::uint64_t, void*, std::uint64_t, const void*, std::uint32_t); +std::int32_t APS5_VABI sceFiberFinalize(FiberObject*); +std::int32_t APS5_VABI sceFiberRun_nid_postfix(FiberObject*, std::uint64_t, std::uint64_t*); +std::int32_t APS5_VABI sceFiberReturnToThread(std::uint64_t, std::uint64_t*); +} + +namespace { + +constexpr std::size_t ContextBytes = 64 * 1024; +constexpr std::size_t FiberStackLimit = 8 * 1024; +constexpr std::uint32_t VulkanVersion = 0x00401000u; +constexpr std::uint32_t SpirvVersion = 0x00010300u; +constexpr std::uint64_t Fill = 0x5aa5c33c5aa5c33cull; + +alignas(16) std::array FiberContext; +struct alignas(8) FiberStorage { + unsigned char bytes[0x100]; +}; +FiberStorage FiberObjectStorage; + +constexpr std::array Module{ + 0x07230203u, 0x00010300u, 0x0008000bu, 0x00000044u, 0x00000000u, 0x00020011u, 0x00000001u, 0x0006000bu, + 0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u, + 0x0006000fu, 0x00000005u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x0000000bu, 0x00060010u, 0x00000004u, + 0x00000011u, 0x00000040u, 0x00000001u, 0x00000001u, 0x00030003u, 0x00000002u, 0x000001c2u, 0x00040005u, + 0x00000004u, 0x6e69616du, 0x00000000u, 0x00040005u, 0x00000008u, 0x65646e69u, 0x00000078u, 0x00080005u, + 0x0000000bu, 0x475f6c67u, 0x61626f6cu, 0x766e496cu, 0x7461636fu, 0x496e6f69u, 0x00000044u, 0x00030005u, + 0x00000010u, 0x006d7573u, 0x00040005u, 0x00000011u, 0x74617473u, 0x00000065u, 0x00040005u, 0x00000013u, + 0x61746144u, 0x00000000u, 0x00050006u, 0x00000013u, 0x00000000u, 0x756c6176u, 0x00007365u, 0x00030005u, + 0x00000015u, 0x00000000u, 0x00030005u, 0x0000001cu, 0x00000069u, 0x00040047u, 0x0000000bu, 0x0000000bu, + 0x0000001cu, 0x00040047u, 0x00000012u, 0x00000006u, 0x00000004u, 0x00030047u, 0x00000013u, 0x00000002u, + 0x00050048u, 0x00000013u, 0x00000000u, 0x00000023u, 0x00000000u, 0x00040047u, 0x00000015u, 0x00000021u, + 0x00000000u, 0x00040047u, 0x00000015u, 0x00000022u, 0x00000000u, 0x00040047u, 0x00000043u, 0x0000000bu, + 0x00000019u, 0x00020013u, 0x00000002u, 0x00030021u, 0x00000003u, 0x00000002u, 0x00040015u, 0x00000006u, + 0x00000020u, 0x00000000u, 0x00040020u, 0x00000007u, 0x00000007u, 0x00000006u, 0x00040017u, 0x00000009u, + 0x00000006u, 0x00000003u, 0x00040020u, 0x0000000au, 0x00000001u, 0x00000009u, 0x0004003bu, 0x0000000au, + 0x0000000bu, 0x00000001u, 0x0004002bu, 0x00000006u, 0x0000000cu, 0x00000000u, 0x00040020u, 0x0000000du, + 0x00000001u, 0x00000006u, 0x0003001du, 0x00000012u, 0x00000006u, 0x0003001eu, 0x00000013u, 0x00000012u, + 0x00040020u, 0x00000014u, 0x0000000cu, 0x00000013u, 0x0004003bu, 0x00000014u, 0x00000015u, 0x0000000cu, + 0x00040015u, 0x00000016u, 0x00000020u, 0x00000001u, 0x0004002bu, 0x00000016u, 0x00000017u, 0x00000000u, + 0x00040020u, 0x00000019u, 0x0000000cu, 0x00000006u, 0x0004002bu, 0x00000006u, 0x00000023u, 0x00000010u, + 0x00020014u, 0x00000024u, 0x0004002bu, 0x00000006u, 0x00000027u, 0x00000001u, 0x0004002bu, 0x00000006u, + 0x00000038u, 0x0019660du, 0x0004002bu, 0x00000006u, 0x0000003au, 0x3c6ef35fu, 0x0004002bu, 0x00000016u, + 0x0000003du, 0x00000001u, 0x0004002bu, 0x00000006u, 0x00000042u, 0x00000040u, 0x0006002cu, 0x00000009u, + 0x00000043u, 0x00000042u, 0x00000027u, 0x00000027u, 0x00050036u, 0x00000002u, 0x00000004u, 0x00000000u, + 0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003bu, 0x00000007u, 0x00000008u, 0x00000007u, 0x0004003bu, + 0x00000007u, 0x00000010u, 0x00000007u, 0x0004003bu, 0x00000007u, 0x00000011u, 0x00000007u, 0x0004003bu, + 0x00000007u, 0x0000001cu, 0x00000007u, 0x00050041u, 0x0000000du, 0x0000000eu, 0x0000000bu, 0x0000000cu, + 0x0004003du, 0x00000006u, 0x0000000fu, 0x0000000eu, 0x0003003eu, 0x00000008u, 0x0000000fu, 0x0003003eu, + 0x00000010u, 0x0000000cu, 0x0004003du, 0x00000006u, 0x00000018u, 0x00000008u, 0x00060041u, 0x00000019u, + 0x0000001au, 0x00000015u, 0x00000017u, 0x00000018u, 0x0004003du, 0x00000006u, 0x0000001bu, 0x0000001au, + 0x0003003eu, 0x00000011u, 0x0000001bu, 0x0003003eu, 0x0000001cu, 0x0000000cu, 0x000200f9u, 0x0000001du, + 0x000200f8u, 0x0000001du, 0x000400f6u, 0x0000001fu, 0x00000020u, 0x00000000u, 0x000200f9u, 0x00000021u, + 0x000200f8u, 0x00000021u, 0x0004003du, 0x00000006u, 0x00000022u, 0x0000001cu, 0x000500b0u, 0x00000024u, + 0x00000025u, 0x00000022u, 0x00000023u, 0x000400fau, 0x00000025u, 0x0000001eu, 0x0000001fu, 0x000200f8u, + 0x0000001eu, 0x0004003du, 0x00000006u, 0x00000026u, 0x00000011u, 0x000500c7u, 0x00000006u, 0x00000028u, + 0x00000026u, 0x00000027u, 0x000500abu, 0x00000024u, 0x00000029u, 0x00000028u, 0x0000000cu, 0x000300f7u, + 0x0000002bu, 0x00000000u, 0x000400fau, 0x00000029u, 0x0000002au, 0x00000031u, 0x000200f8u, 0x0000002au, + 0x0004003du, 0x00000006u, 0x0000002cu, 0x00000011u, 0x0004003du, 0x00000006u, 0x0000002du, 0x0000001cu, + 0x00050084u, 0x00000006u, 0x0000002eu, 0x0000002cu, 0x0000002du, 0x0004003du, 0x00000006u, 0x0000002fu, + 0x00000010u, 0x00050080u, 0x00000006u, 0x00000030u, 0x0000002fu, 0x0000002eu, 0x0003003eu, 0x00000010u, + 0x00000030u, 0x000200f9u, 0x0000002bu, 0x000200f8u, 0x00000031u, 0x0004003du, 0x00000006u, 0x00000032u, + 0x00000011u, 0x0004003du, 0x00000006u, 0x00000033u, 0x0000001cu, 0x00050080u, 0x00000006u, 0x00000034u, + 0x00000032u, 0x00000033u, 0x0004003du, 0x00000006u, 0x00000035u, 0x00000010u, 0x000500c6u, 0x00000006u, + 0x00000036u, 0x00000035u, 0x00000034u, 0x0003003eu, 0x00000010u, 0x00000036u, 0x000200f9u, 0x0000002bu, + 0x000200f8u, 0x0000002bu, 0x0004003du, 0x00000006u, 0x00000037u, 0x00000011u, 0x00050084u, 0x00000006u, + 0x00000039u, 0x00000037u, 0x00000038u, 0x00050080u, 0x00000006u, 0x0000003bu, 0x00000039u, 0x0000003au, + 0x0003003eu, 0x00000011u, 0x0000003bu, 0x000200f9u, 0x00000020u, 0x000200f8u, 0x00000020u, 0x0004003du, + 0x00000006u, 0x0000003cu, 0x0000001cu, 0x00050080u, 0x00000006u, 0x0000003eu, 0x0000003cu, 0x0000003du, + 0x0003003eu, 0x0000001cu, 0x0000003eu, 0x000200f9u, 0x0000001du, 0x000200f8u, 0x0000001fu, 0x0004003du, + 0x00000006u, 0x0000003fu, 0x00000008u, 0x0004003du, 0x00000006u, 0x00000040u, 0x00000010u, 0x00060041u, + 0x00000019u, 0x00000041u, 0x00000015u, 0x00000017u, 0x0000003fu, 0x0003003eu, 0x00000041u, 0x00000040u, + 0x000100fdu, 0x00010038u +}; + +std::vector Optimized; +std::exception_ptr Failure; + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +std::vector Optimize() { + return ShaderRecompiler::ValidateAndOptimizeSpirv(Module, VulkanVersion, SpirvVersion); +} + +void APS5_VABI OptimizeOnFiber(std::uint64_t, std::uint64_t) { + try { + Optimized = Optimize(); + } catch (...) { + Failure = std::current_exception(); + } + sceFiberReturnToThread(0, nullptr); +} + +std::size_t FiberStackUsed() { + std::size_t untouched = 0; + while (untouched < FiberContext.size() && FiberContext[untouched] == Fill) ++untouched; + return (FiberContext.size() - untouched) * sizeof(std::uint64_t); +} + +} + +int main() { + try { + FiberContext.fill(Fill); + auto* fiber = reinterpret_cast(&FiberObjectStorage); + Require(_sceFiberInitializeImpl_nid_postfix(fiber, "optimize", OptimizeOnFiber, 0, FiberContext.data(), ContextBytes, nullptr, 0) == 0, "SPIR-V optimization on a fiber: the fiber was not initialized"); + const auto before = ShaderRecompiler::OptimizerHostThreads(); + Require(sceFiberRun_nid_postfix(fiber, 0, nullptr) == 0, "SPIR-V optimization on a fiber: the fiber did not run"); + const auto onFiber = ShaderRecompiler::OptimizerHostThreads() - before; + Require(sceFiberFinalize(fiber) == 0, "SPIR-V optimization on a fiber: the fiber was not finalized"); + if (Failure) std::rethrow_exception(Failure); + const auto used = FiberStackUsed(); + const auto beforeThread = ShaderRecompiler::OptimizerHostThreads(); + const auto onThreadResult = Optimize(); + const auto onThread = ShaderRecompiler::OptimizerHostThreads() - beforeThread; + Require(Optimized == onThreadResult, "SPIR-V optimization on a fiber returned another module than on the thread"); + Require(used <= FiberStackLimit, "SPIR-V optimization on a fiber running on its context used " + std::to_string(used) + " bytes of it, more than " + std::to_string(FiberStackLimit) + ": the optimizer ran on it"); + Require(onFiber == 1, "SPIR-V optimization on a fiber running on its context started " + std::to_string(onFiber) + " host threads instead of one"); + Require(onThread == 0, "SPIR-V optimization on an ordinary thread started " + std::to_string(onThread) + " host threads instead of optimizing on its own stack"); + std::cout << "SPIR-V optimization on a fiber used " << used << " bytes of its context\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} +#else +int main() { + return 77; +} +#endif diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp index 9b5c7bbba5..44bbbf5a34 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp @@ -55,6 +55,8 @@ public: Device() { #ifdef _WIN32 library = SDL_LoadObject("vulkan-1.dll"); +#elif defined(__APPLE__) + library = SDL_LoadObject("libvulkan.1.dylib"); #else library = SDL_LoadObject("libvulkan.so.1"); #endif @@ -66,6 +68,12 @@ public: application.apiVersion = VK_API_VERSION_1_1; VkInstanceCreateInfo info{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; info.pApplicationInfo = &application; +#ifdef __APPLE__ + const char* portability = VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME; + info.flags = VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; + info.enabledExtensionCount = 1; + info.ppEnabledExtensionNames = &portability; +#endif Check(function("vkCreateInstance")(&info, nullptr, &instance), "vkCreateInstance"); std::uint32_t count = 0; const auto enumerate = function("vkEnumeratePhysicalDevices"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp index 8e295cf318..988207259f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp @@ -144,6 +144,8 @@ public: Device() { #ifdef _WIN32 library = SDL_LoadObject("vulkan-1.dll"); +#elif defined(__APPLE__) + library = SDL_LoadObject("libvulkan.1.dylib"); #else library = SDL_LoadObject("libvulkan.so.1"); #endif @@ -155,6 +157,12 @@ public: application.apiVersion = VK_API_VERSION_1_1; VkInstanceCreateInfo info{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; info.pApplicationInfo = &application; +#ifdef __APPLE__ + const char* portability = VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME; + info.flags = VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; + info.enabledExtensionCount = 1; + info.ppEnabledExtensionNames = &portability; +#endif Check(function("vkCreateInstance")(&info, nullptr, &instance), "vkCreateInstance"); std::uint32_t count = 0; const auto enumerate = function("vkEnumeratePhysicalDevices"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SparseBufferHoles.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SparseBufferHoles.cpp index ba59e3af42..55cae19285 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SparseBufferHoles.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SparseBufferHoles.cpp @@ -41,6 +41,8 @@ public: Device() { #ifdef _WIN32 library = SDL_LoadObject("vulkan-1.dll"); +#elif defined(__APPLE__) + library = SDL_LoadObject("libvulkan.1.dylib"); #else library = SDL_LoadObject("libvulkan.so.1"); #endif @@ -52,6 +54,12 @@ public: application.apiVersion = VK_API_VERSION_1_1; VkInstanceCreateInfo info{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; info.pApplicationInfo = &application; +#ifdef __APPLE__ + const char* portability = VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME; + info.flags = VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; + info.enabledExtensionCount = 1; + info.ppEnabledExtensionNames = &portability; +#endif Check(function("vkCreateInstance")(&info, nullptr, &instance), "vkCreateInstance"); std::uint32_t count = 0; const auto enumerate = function("vkEnumeratePhysicalDevices"); diff --git a/core/libs/prx/libSceLibcInternal/CMakeLists.txt b/core/libs/prx/libSceLibcInternal/CMakeLists.txt index dfd2af21a2..8087b230e5 100644 --- a/core/libs/prx/libSceLibcInternal/CMakeLists.txt +++ b/core/libs/prx/libSceLibcInternal/CMakeLists.txt @@ -3,6 +3,19 @@ target_include_directories(libSceLibcInternal PRIVATE ${LIBS_INCLUDE_DIR}) set_source_files_properties(UserContext.cpp PROPERTIES COMPILE_OPTIONS -fno-omit-frame-pointer) target_link_libraries(libSceLibcInternal PRIVATE ${CMAKE_DL_LIBS}) target_sources(libSceLibcInternal PRIVATE Directory.cpp) +target_sources(libSceLibcInternal PRIVATE InitOnce.cpp) +if(BUILD_TESTING) + add_test_executable(guest_init_once_tests ${LIBS_INCLUDE_DIR}/tests/GuestInitOnce.cpp) + target_include_directories(guest_init_once_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_init_once_tests PRIVATE libSceLibcInternal libkernel libc) + configure_windows_unwind(guest_init_once_tests) + add_test(NAME guest_init_once COMMAND guest_init_once_tests) + set_tests_properties(guest_init_once PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_init_once PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() target_sources(libSceLibcInternal PRIVATE Err.cpp) configure_windows_unwind(libSceLibcInternal) target_sources(libSceLibcInternal PRIVATE FilesystemStatistics.cpp PathStatistics.cpp) diff --git a/core/libs/prx/libSceLibcInternal/InitOnce.cpp b/core/libs/prx/libSceLibcInternal/InitOnce.cpp new file mode 100644 index 0000000000..56f54923a0 --- /dev/null +++ b/core/libs/prx/libSceLibcInternal/InitOnce.cpp @@ -0,0 +1,58 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +extern "C" int APS5_VABI sceKernelSyncOnAddressWait(std::uint32_t* address, std::uint32_t expected, const KernelUseconds* timeout, const char* name); +extern "C" int APS5_VABI sceKernelSyncOnAddressWake(void* address, std::int32_t count); + +namespace { + +constexpr std::uintptr_t OnceIdle = 0; +constexpr std::uintptr_t OnceRunning = 1; +constexpr std::uintptr_t OnceDone = 2; +constexpr unsigned long InitFailed = 4; + +std::atomic& OnceState(void** once) { + return *reinterpret_cast*>(once); +} + +} + +extern "C" { + +int APS5_VABI __std_init_once_begin_initialize_clr_nid_postfix(void** once, unsigned long flags, int* pending, void** context) { + (void)flags; + (void)context; + auto& state = OnceState(once); + for (;;) { + std::uintptr_t current = state.load(std::memory_order_acquire); + if (current == OnceIdle) { + if (state.compare_exchange_weak(current, OnceRunning, std::memory_order_acq_rel)) { + *pending = 1; + return 1; + } + continue; + } + if (current == OnceDone) { + *pending = 0; + return 1; + } + sceKernelSyncOnAddressWait(reinterpret_cast(once), static_cast(OnceRunning), nullptr, nullptr); + } +} + +int APS5_VABI __std_init_once_complete_clr_nid_postfix(void** once, unsigned long flags, void* context) { + (void)context; + OnceState(once).store(flags == InitFailed ? OnceIdle : OnceDone, std::memory_order_release); + sceKernelSyncOnAddressWake(once, INT_MAX); + return 1; +} + +[[noreturn]] void APS5_VABI __std_init_once_link_alternate_names_and_abort_nid_postfix() { + std::abort(); +} + +} diff --git a/core/libs/prx/libSceNet/CMakeLists.txt b/core/libs/prx/libSceNet/CMakeLists.txt index 9ecb525065..d314868198 100644 --- a/core/libs/prx/libSceNet/CMakeLists.txt +++ b/core/libs/prx/libSceNet/CMakeLists.txt @@ -82,10 +82,16 @@ if(BUILD_TESTING) target_link_libraries(guest_sce_net_tests PRIVATE libSceNet libSceNetCtl libkernel libc) configure_windows_unwind(guest_sce_net_tests) add_test(NAME guest_sce_net COMMAND guest_sce_net_tests) + add_test_executable(guest_net_thread_tests ${LIBS_INCLUDE_DIR}/tests/GuestNetThread.cpp) + target_include_directories(guest_net_thread_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_net_thread_tests PRIVATE libSceNet libkernel libc) + configure_windows_unwind(guest_net_thread_tests) + add_test(NAME guest_net_thread COMMAND guest_net_thread_tests) + set_tests_properties(guest_net_thread PROPERTIES TIMEOUT 20) set_tests_properties(guest_sce_net PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77) if(WIN32) target_link_libraries(guest_sce_net_tests PRIVATE ws2_32) - set_tests_properties(guest_sce_net PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_sce_net guest_net_thread PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test_executable(net_epoll_lifetime_tests ${LIBS_INCLUDE_DIR}/tests/NetEpollLifetime.cpp) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index c6a8b4ed0a..b800dd81ef 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -44,6 +44,7 @@ #include "prx/libkernel/Socket/include/SocketPoll.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include "prx/libkernel/Time/include/Time.hpp" +#include "prx/libkernel/Pthread/include/ThreadLifecycle.hpp" #include "src/ResolverTestHooks.hpp" // Guest socket descriptors retain PS5 semantics while their transport is backed by host sockets. @@ -97,7 +98,12 @@ struct NetDataTraffic; struct NetEmulationParameter; struct NetResolverConnection; -struct NetThreadParameter; +struct NetThreadParameter { + void (APS5_VABI *entry)(void*); + void* arg; + std::uint64_t reserved; + std::uint32_t flags; +}; struct NetIpv4InterfaceAddress; struct NetStatisticsInfoInternal; struct NetInitializeParameter; @@ -501,6 +507,15 @@ int fail(int err) { return NET_ERROR_BASE | err; } +extern "C" int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +extern "C" int APS5_VABI scePthreadJoin(Pthread thread, void** retval); + +void* APS5_VABI RunNetThread(void* param) { + const auto* thread = static_cast(param); + thread->entry(thread->arg); + return nullptr; +} + int set_native_timeout(NativeSocket native, int optname, std::int64_t microseconds) { const int native_option = optname == NET_SO_RCVTIMEO ? SO_RCVTIMEO : SO_SNDTIMEO; #ifdef _WIN32 @@ -1656,7 +1671,9 @@ int APS5_VABI sceNetGetpeername(int s, void* addr, uint32_t* addrlen) { constexpr int NET_SOCKINFO_F_SELF = 0x1; constexpr int NET_SOCKINFO_F_NONBLOCK = 0x200000; +constexpr int NET_SOCKINFO_F_IPV6 = 0x400000; constexpr int NET_SOCKINFO_TIME_WAIT = 0x40; +constexpr int NET_SOCKINFO6_IPV46 = 0x1000; struct NetSockInfo { char name[32]; @@ -1693,6 +1710,44 @@ struct NetSockInfo { static_assert(sizeof(NetSockInfo) == 0xA0 && offsetof(NetSockInfo, s) == 0x24 && offsetof(NetSockInfo, local_adr) == 0x34 && offsetof(NetSockInfo, local_port) == 0x3C && offsetof(NetSockInfo, recv_buffer_size) == 0x64); +struct NetSockInfo6 { + char name[32]; + std::int32_t pid; + std::int32_t s; + std::int8_t socket_type; + std::int8_t policy; + std::int8_t priority; + std::int8_t reserved8; + std::int32_t recv_queue_length; + std::int32_t send_queue_length; + std::uint32_t local_adr; + std::uint32_t remote_adr; + std::uint16_t local_port; + std::uint16_t remote_port; + std::uint16_t local_vport; + std::uint16_t remote_vport; + std::int32_t state; + std::int32_t flags; + std::int32_t tx_bps; + std::int32_t rx_bps; + std::int32_t max_tx_bps; + std::int32_t max_rx_bps; + std::int32_t tx_vbps; + std::int32_t rx_vbps; + std::int32_t recv_buffer_size; + std::int32_t send_buffer_size; + std::uint8_t local_adr6[16]; + std::uint8_t remote_adr6[16]; + std::int32_t tx_drops; + std::int32_t rx_drops; + std::int32_t tx_wait; + std::int32_t reserved[2]; +}; +static_assert(sizeof(NetSockInfo6) == sizeof(NetSockInfo) && offsetof(NetSockInfo6, s) == offsetof(NetSockInfo, s) && + offsetof(NetSockInfo6, local_port) == offsetof(NetSockInfo, local_port) && offsetof(NetSockInfo6, flags) == offsetof(NetSockInfo, flags) && + offsetof(NetSockInfo6, recv_buffer_size) == offsetof(NetSockInfo, recv_buffer_size) && offsetof(NetSockInfo6, local_adr6) == 0x6C && + offsetof(NetSockInfo6, remote_adr6) == 0x7C && offsetof(NetSockInfo6, tx_drops) == offsetof(NetSockInfo, tx_drops)); + static int sock_buffer_size(const Sock& socket, bool send, std::int32_t& value) { value = send ? socket.snd_buffer : socket.rcv_buffer; if (value != 0) return 0; @@ -1766,6 +1821,86 @@ int APS5_VABI sceNetGetSockInfo(int s, void* info, int n, int flags) { return written; } +static int fill_sock_info6(int s, NetSockInfo6& record, bool includeIpv4) { + Sock socket; + { + std::lock_guard lk(g_mutex); + const auto it = g_socks.find(s); + if (it == g_socks.end()) return NET_EBADF; + socket = it->second; + } + const bool ipv6 = socket.family == NET_AF_INET6; + if (!ipv6 && !(includeIpv4 && socket.family == NET_AF_INET)) return NET_EAFNOSUPPORT; + sockaddr_storage local{}; + NativeLength length = sizeof(local); + if (::getsockname(socket.native->value, reinterpret_cast(&local), &length) != 0) { +#ifdef _WIN32 + if (WSAGetLastError() != WSAEINVAL) return native_error(); +#else + return native_error(); +#endif + } + sockaddr_storage remote{}; + length = sizeof(remote); + if (::getpeername(socket.native->value, reinterpret_cast(&remote), &length) != 0) { + const int error = native_error(); + if (error != NET_ENOTCONN) return error; + } + record = {}; + if (const int error = sock_buffer_size(socket, false, record.recv_buffer_size)) return error; + if (const int error = sock_buffer_size(socket, true, record.send_buffer_size)) return error; + record.s = s; + record.socket_type = static_cast(socket.type); + record.flags = NET_SOCKINFO_F_SELF | (socket.native->nonblock ? NET_SOCKINFO_F_NONBLOCK : 0); + if (ipv6) { + const auto& local6 = reinterpret_cast(local); + const auto& remote6 = reinterpret_cast(remote); + std::memcpy(record.local_adr6, &local6.sin6_addr, sizeof(record.local_adr6)); + std::memcpy(record.remote_adr6, &remote6.sin6_addr, sizeof(record.remote_adr6)); + record.local_port = local6.sin6_port; + record.remote_port = remote6.sin6_port; + record.flags |= NET_SOCKINFO_F_IPV6; + } else { + const auto& local4 = reinterpret_cast(local); + const auto& remote4 = reinterpret_cast(remote); + record.local_adr = local4.sin_addr.s_addr; + record.remote_adr = remote4.sin_addr.s_addr; + record.local_port = local4.sin_port; + record.remote_port = remote4.sin_port; + } + return 0; +} + +int APS5_VABI sceNetGetSockInfo6(std::int32_t s, NetSockInfo6* info, std::uint32_t capacity, std::uint32_t flags) { + if ((flags & ~(NET_SOCKINFO_TIME_WAIT | NET_SOCKINFO6_IPV46)) != 0) return fail(NET_EINVAL); + const bool includeIpv4 = (flags & NET_SOCKINFO6_IPV46) != 0; + std::vector listed; + if (s < 0) { + std::lock_guard lk(g_mutex); + for (const auto& [descriptor, socket] : g_socks) { + if (socket.family == NET_AF_INET6 || (includeIpv4 && socket.family == NET_AF_INET)) listed.push_back(descriptor); + } + } + if (!info) return s < 0 ? static_cast(listed.size()) : fail(NET_EINVAL); + if (static_cast(capacity) < 1) return fail(NET_EINVAL); + NetSockInfo6 record; + if (s >= 0) { + if (const int error = fill_sock_info6(s, record, includeIpv4)) return fail(error); + *info = record; + return 1; + } + int written = 0; + for (const int descriptor : listed) { + if (written == static_cast(capacity)) break; + const int error = fill_sock_info6(descriptor, record, includeIpv4); + if (error == NET_EBADF || error == NET_EAFNOSUPPORT) continue; + if (error != 0) return fail(error); + info[written] = record; + ++written; + } + return written; +} + int APS5_VABI sceNetEpollCreate(const char* name, int flags) { (void)name; (void)flags; @@ -2330,11 +2465,6 @@ int APS5_VABI sceNetResolverConnectDestroy(NetResolverConnection* connection) { return 0; } -int APS5_VABI sceNetGetSockInfo6(std::int32_t socket, NetSockInfo6* info, std::uint32_t capacity, std::uint32_t flags) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNetSetDns6Info(const NetDns6Info* info, std::uint32_t flags) { NotImplemented_nid_no_patch(__func__); return 0; @@ -2600,18 +2730,20 @@ int APS5_VABI sceNetSysctl(const int* name, std::uint32_t nameLength, void* oldV } int APS5_VABI sceNetThreadCreate(Pthread* thread, const NetThreadParameter* parameter, const char* name) { - NotImplemented_nid_no_patch(__func__); + if (!parameter) return fail(NET_EINVAL); + Pthread created = nullptr; + const int result = scePthreadCreate(&created, nullptr, RunNetThread, const_cast(parameter), name != nullptr ? name : ""); + if (result != 0) return result; + if (thread) *thread = created; return 0; } -int APS5_VABI sceNetThreadExit(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +void APS5_VABI sceNetThreadExit() { + scePthreadExit(nullptr); } int APS5_VABI sceNetThreadJoin(void* thread) { - NotImplemented_nid_no_patch(__func__); - return 0; + return scePthreadJoin(static_cast(thread), nullptr); } int APS5_VABI sceNetUsleep(int microseconds) { @@ -3167,14 +3299,12 @@ int APS5_VABI sceNetEmulationDebugSettingsGet(NetEmulationParameter* parameter, return 0; } -int APS5_VABI sceNetEmulationSet(const NetEmulationParameter* parameter, std::uint32_t flags) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNetEmulationSet(const NetEmulationParameter*, std::uint32_t) { + return fail(NET_EOPNOTSUPP); } -int APS5_VABI sceNetEmulationGet(NetEmulationParameter* parameter, std::uint32_t flags) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNetEmulationGet(NetEmulationParameter*, std::uint32_t) { + return fail(NET_EOPNOTSUPP); } } diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index 3a31755e82..c8b0e482f3 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -99,6 +99,7 @@ static std::atomic_bool g_initialized{true}; static std::mutex g_callbackLock; static CallbackSlot g_callbacks[MAX_CALLBACKS] = {}; static CallbackSlot g_callbacksV6[MAX_CALLBACKS] = {}; +static CallbackSlot g_callbacksNpToolkit[MAX_CALLBACKS] = {}; static int RegisterCallback(CallbackSlot (&callbacks)[MAX_CALLBACKS], NetCtlCallback func, void* arg, int* cid) { if (!g_initialized.load()) return SCE_NET_CTL_ERROR_NOT_INITIALIZED; @@ -123,9 +124,9 @@ static int UnregisterCallback(CallbackSlot (&callbacks)[MAX_CALLBACKS], int cid) return 0; } -static bool HasIpv4Callback() { +static bool HasCallback(const CallbackSlot (&callbacks)[MAX_CALLBACKS]) { std::lock_guard lock(g_callbackLock); - for (const auto& slot : g_callbacks) { + for (const auto& slot : callbacks) { if (slot.func) return true; } return false; @@ -512,17 +513,15 @@ static void HostTraffic(std::uint64_t& sent, std::uint64_t& received) { #endif } -extern "C" { - -int APS5_VABI sceNetCtlCheckCallback(void) { +static int CheckCallbacks(CallbackSlot (&callbacks)[MAX_CALLBACKS]) { if (!g_initialized.load()) return SCE_NET_CTL_ERROR_NOT_INITIALIZED; - if (!HasIpv4Callback()) return 0; + if (!HasCallback(callbacks)) return 0; const int event = HostHasAddress(AF_UNSPEC) ? NET_CTL_EVENT_IPOBTAINED : NET_CTL_EVENT_DISCONNECTED; CallbackSlot due[MAX_CALLBACKS]; int count = 0; { std::lock_guard lock(g_callbackLock); - for (auto& slot : g_callbacks) { + for (auto& slot : callbacks) { if (!slot.func || slot.lastEvent == event) continue; slot.lastEvent = event; due[count++] = slot; @@ -532,6 +531,12 @@ int APS5_VABI sceNetCtlCheckCallback(void) { return 0; } +extern "C" { + +int APS5_VABI sceNetCtlCheckCallback(void) { + return CheckCallbacks(g_callbacks); +} + int APS5_VABI sceNetCtlGetIfStat(NetCtlIfStat* stat) { if (!g_initialized.load()) return SCE_NET_CTL_ERROR_NOT_INITIALIZED; if (!stat) return SCE_NET_CTL_ERROR_INVALID_ADDR; @@ -637,8 +642,7 @@ int APS5_VABI sceNetCtlUnregisterCallbackV6(int cid) { } int APS5_VABI sceNetCtlRegisterCallbackForNpToolkit(NetCtlCallback callback, void* argument, int* callbackId) { - NotImplemented_nid_no_patch(__func__); - return 0; + return RegisterCallback(g_callbacksNpToolkit, callback, argument, callbackId); } int APS5_VABI sceNetCtlRegisterCallbackIpcInt(NetCtlCallback callback, void* argument, int* callbackId) { @@ -652,8 +656,7 @@ int APS5_VABI sceNetCtlRegisterCallbackForLibIpcInt(NetCtlCallback callback, voi } int APS5_VABI sceNetCtlUnregisterCallbackForNpToolkit(int callbackId) { - NotImplemented_nid_no_patch(__func__); - return 0; + return UnregisterCallback(g_callbacksNpToolkit, callbackId); } int APS5_VABI sceNetCtlUnregisterCallbackIpcInt(int callbackId) { @@ -667,8 +670,7 @@ int APS5_VABI sceNetCtlUnregisterCallbackForLibIpcInt(int callbackId) { } int APS5_VABI sceNetCtlCheckCallbackForNpToolkit(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return CheckCallbacks(g_callbacksNpToolkit); } int APS5_VABI sceNetCtlCheckCallbackForLibIpcInt(void) { diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 23d05ddcaa..d847237e5c 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -113,7 +113,8 @@ constexpr int PAD_ERROR_INVALID_USER_ID = static_cast(0x809b0001); constexpr int PAD_ERROR_INVALID_PORT_INDEX = static_cast(0x809b0003); constexpr int PAD_USER_ID_INVALID = -1; -bool g_opened = false; +std::array g_opened{false}; +std::array g_everOpened{false}; bool g_remoteOpened = false; std::array g_specialUsers{}; std::atomic_bool g_initialized{false}; @@ -130,8 +131,14 @@ int SpecialHandleOf(int userId) { return PAD_ERROR_DEVICE_NO_HANDLE; } +int PortOf(int handle) { + const int port = handle - PAD_HANDLE_BASE; + if (port < 0 || port >= PAD_MAX_CONTROLLERS || !g_everOpened[port]) return -1; + return port; +} + bool KnownHandle(int handle) { - return handle == PAD_HANDLE || SpecialHandle(handle); + return PortOf(handle) >= 0 || SpecialHandle(handle); } PadData SpecialData() { @@ -150,9 +157,19 @@ bool RemoteHandle(int handle) { return g_remoteOpened && handle == PAD_REMOTE_HANDLE; } +int PortForUser(int userId, int type, int index) { + if (index != 0) return -1; + if (type == PAD_PORT_TYPE_STANDARD) { + const int port = userId - 0x10000000; + if (port >= 0 && port < PAD_MAX_CONTROLLERS) return port; + } + return -1; +} + bool ValidPort(int userId, int type, int index) { - const bool personalPort = type == PAD_PORT_TYPE_STANDARD || type == PAD_PORT_TYPE_SPECIAL; - return index == 0 && (personalPort || SystemRemote(userId, type)); + if (index != 0) return false; + if (type == PAD_PORT_TYPE_SPECIAL || SystemRemote(userId, type)) return true; + return PortForUser(userId, type, index) >= 0; } } @@ -184,10 +201,9 @@ int APS5_VABI scePadClose_nid_postfix(int handle) { g_specialUsers[handle - PAD_SPECIAL_HANDLE] = 0; return PAD_OK; } - if (handle != PAD_HANDLE) { - return PAD_ERROR_INVALID_HANDLE; - } - g_opened = false; + const int port = PortOf(handle); + if (port < 0) return PAD_ERROR_INVALID_HANDLE; + g_opened[port] = false; return PAD_OK; } @@ -249,22 +265,41 @@ int APS5_VABI scePadGetHandle(int user_id, int type, int index) { if (!ValidPort(user_id, type, index)) return PAD_ERROR_DEVICE_NO_HANDLE; if (type == PAD_PORT_TYPE_SPECIAL) return SpecialHandleOf(user_id); if (SystemRemote(user_id, type)) return g_remoteOpened ? PAD_REMOTE_HANDLE : PAD_ERROR_DEVICE_NO_HANDLE; - if (!g_opened) return PAD_ERROR_DEVICE_NO_HANDLE; - return PAD_HANDLE; + const int port = PortForUser(user_id, type, index); + if (port < 0 || !g_opened[port]) return PAD_ERROR_DEVICE_NO_HANDLE; + return PAD_HANDLE_BASE + port; } int APS5_VABI scePadGetInfo_nid_postfix(PadInfo* info) { if (info == nullptr) return PAD_ERROR_INVALID_ARG; std::memset(info, 0, sizeof(*info)); - info->maxConnectCount = 4; - info->connectedCount[0] = 1; + info->maxConnectCount = PAD_MAX_CONTROLLERS; + std::uint8_t count = 0; + for (int port = 0; port < PAD_MAX_CONTROLLERS; ++port) { + if (Pad::IsConnected(port)) { + PadInfo::PadTypeInfo& slot = info->padTypeInfo[count]; + slot.connectPort = static_cast(port); + slot.status = 1; + slot.deviceType = PAD_DEVICE_TYPE_DUAL_SENSE; + slot.connectType = PAD_CONNECT_TYPE_BLUETOOTH; + std::uint8_t mac[6]; + std::memcpy(mac, PAD_BD_ADDRESS, sizeof(mac)); + mac[5] = static_cast(mac[5] + port); + std::memcpy(slot.bluetoothMacAddress, mac, sizeof(slot.bluetoothMacAddress)); + ++count; + } + } + if (count == 0) { + PadInfo::PadTypeInfo& slot = info->padTypeInfo[0]; + slot.connectPort = 0; + slot.status = 1; + slot.deviceType = PAD_DEVICE_TYPE_DUAL_SENSE; + slot.connectType = PAD_CONNECT_TYPE_BLUETOOTH; + std::memcpy(slot.bluetoothMacAddress, PAD_BD_ADDRESS, sizeof(slot.bluetoothMacAddress)); + count = 1; + } + info->connectedCount[0] = count; info->connectedCount[1] = 0; - PadInfo::PadTypeInfo& slot = info->padTypeInfo[0]; - slot.connectPort = 0; - slot.status = 1; - slot.deviceType = PAD_DEVICE_TYPE_DUAL_SENSE; - slot.connectType = PAD_CONNECT_TYPE_BLUETOOTH; - std::memcpy(slot.bluetoothMacAddress, PAD_BD_ADDRESS, sizeof(slot.bluetoothMacAddress)); return PAD_OK; } @@ -312,7 +347,8 @@ int APS5_VABI scePadInit_nid_postfix(void) { int APS5_VABI scePadOpen_nid_postfix(int userId, int type, int index, const void* param) { (void)param; const bool unknownType = type == 3 || type == 4; - if (!unknownType && !ValidPort(userId, type, 0)) return PAD_ERROR_INVALID_ARG; + const bool knownType = type == PAD_PORT_TYPE_STANDARD || type == PAD_PORT_TYPE_SPECIAL || SystemRemote(userId, type); + if (!unknownType && !knownType) return PAD_ERROR_INVALID_ARG; if (userId == PAD_USER_ID_INVALID) return PAD_ERROR_INVALID_USER_ID; if (unknownType) NotImplemented_nid_no_patch(__func__); if (index != 0) return PAD_ERROR_INVALID_PORT_INDEX; @@ -330,8 +366,13 @@ int APS5_VABI scePadOpen_nid_postfix(int userId, int type, int index, const void g_remoteOpened = true; return PAD_REMOTE_HANDLE; } - g_opened = true; - return PAD_HANDLE; + const int port = PortForUser(userId, type, index); + if (port < 0) { + return PAD_ERROR_INVALID_ARG; + } + g_opened[port] = true; + g_everOpened[port] = true; + return PAD_HANDLE_BASE + port; } int APS5_VABI scePadOpenExt(int userId, int type, int index, const void* param) { @@ -361,14 +402,14 @@ int APS5_VABI scePadRead_nid_postfix(int handle, PadData* data, int num) { data[0] = SpecialData(); return 1; } - return Pad::Read(data, num); + return Pad::Read(data, num, PortOf(handle)); } int APS5_VABI scePadReadState(int handle, PadData* data) { if (!KnownHandle(handle) && !RemoteHandle(handle)) return PAD_ERROR_INVALID_HANDLE; if (data == nullptr) return PAD_ERROR_INVALID_ARG; - *data = RemoteHandle(handle) ? PadData{} : SpecialHandle(handle) ? SpecialData() : Pad::ReadState(); + *data = RemoteHandle(handle) ? PadData{} : SpecialHandle(handle) ? SpecialData() : Pad::ReadState(PortOf(handle)); return 0; } @@ -376,21 +417,21 @@ int APS5_VABI scePadReadState(int handle, PadData* data) { int APS5_VABI scePadResetLightBar(int handle) { if (!KnownHandle(handle)) return PAD_ERROR_INVALID_HANDLE; if (SpecialHandle(handle)) return PAD_OK; - Pad::SetLightBar(false, 0, 0, 0); + Pad::SetLightBar(PortOf(handle), false, 0, 0, 0); return PAD_OK; } int APS5_VABI scePadResetOrientation(int handle) { if (!KnownHandle(handle)) return PAD_ERROR_INVALID_HANDLE; if (SpecialHandle(handle)) return PAD_OK; - Pad::ResetOrientation(); + Pad::ResetOrientation(PortOf(handle)); return PAD_OK; } int APS5_VABI scePadSetAngularVelocityDeadbandState(int handle, bool enable) { if (!KnownHandle(handle)) return PAD_ERROR_INVALID_HANDLE; if (SpecialHandle(handle)) return PAD_OK; - Pad::SetAngularVelocityDeadband(enable); + Pad::SetAngularVelocityDeadband(PortOf(handle), enable); return PAD_OK; } @@ -398,21 +439,21 @@ int APS5_VABI scePadSetLightBar(int handle, const PadLightBarParam* param) { if (!KnownHandle(handle)) return PAD_ERROR_INVALID_HANDLE; if (param == nullptr) return PAD_ERROR_INVALID_ARG; if (SpecialHandle(handle)) return PAD_OK; - Pad::SetLightBar(true, param->r, param->g, param->b); + Pad::SetLightBar(PortOf(handle), true, param->r, param->g, param->b); return PAD_OK; } int APS5_VABI scePadSetMotionSensorState(int handle, bool enable) { if (!KnownHandle(handle)) return PAD_ERROR_INVALID_HANDLE; if (SpecialHandle(handle)) return PAD_OK; - Pad::SetMotionEnabled(enable); + Pad::SetMotionEnabled(PortOf(handle), enable); return PAD_OK; } int APS5_VABI scePadSetTiltCorrectionState(int handle, bool enabled) { if (!KnownHandle(handle)) return PAD_ERROR_INVALID_HANDLE; if (SpecialHandle(handle)) return PAD_OK; - Pad::SetTiltCorrection(enabled); + Pad::SetTiltCorrection(PortOf(handle), enabled); return PAD_OK; } @@ -430,8 +471,9 @@ int APS5_VABI scePadSetTriggerEffect(int handle, const void* param) { if (mode[0] > 6 || mode[1] > 6) { return PAD_ERROR_INVALID_ARG; } + const int port = PortOf(handle); for (int trigger = 0; trigger < 2; ++trigger) { - if ((mask & (1u << trigger)) != 0) Pad::SetTriggerCommand(trigger, bytes + 8 + 56 * trigger); + if ((mask & (1u << trigger)) != 0) Pad::SetTriggerCommand(port, trigger, bytes + 8 + 56 * trigger); } return PAD_OK; } @@ -440,7 +482,7 @@ int APS5_VABI scePadSetVibration(int handle, const PadVibrationParam* param) { if (!KnownHandle(handle)) return PAD_ERROR_INVALID_HANDLE; if (param == nullptr) return PAD_ERROR_INVALID_ARG; if (SpecialHandle(handle)) return PAD_OK; - Pad::SetVibration(param->large_motor, param->small_motor); + Pad::SetVibration(PortOf(handle), param->large_motor, param->small_motor); return PAD_OK; } @@ -450,7 +492,7 @@ int APS5_VABI scePadSetVibrationMode(int handle, int mode) { if (!KnownHandle(handle)) return PAD_ERROR_INVALID_HANDLE; if (mode > 3 && mode != 5) return PAD_ERROR_INVALID_ARG; if (SpecialHandle(handle)) return PAD_OK; - Pad::SetVibrationMode(mode); + Pad::SetVibrationMode(PortOf(handle), mode); return PAD_OK; } @@ -670,7 +712,7 @@ int APS5_VABI scePadGetVersionInfo(std::int32_t handle, PadVersionInfo* info) { int APS5_VABI scePadGetControllerType(std::int32_t handle, std::int32_t* type) { if (!Pad::IsInitialized()) return PAD_ERROR_NOT_INITIALIZED; if (type == nullptr) return PAD_ERROR_INVALID_ARG; - if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; + if (PortOf(handle) < 0) return PAD_ERROR_INVALID_HANDLE; *type = PAD_CONTROLLER_TYPE_DUAL_SENSE; return PAD_OK; } diff --git a/core/libs/prx/libScePad/include/Pad.hpp b/core/libs/prx/libScePad/include/Pad.hpp index 56a12100d7..03958f4eed 100644 --- a/core/libs/prx/libScePad/include/Pad.hpp +++ b/core/libs/prx/libScePad/include/Pad.hpp @@ -12,6 +12,8 @@ constexpr int PAD_PORT_TYPE_STANDARD = 0; constexpr int PAD_PORT_TYPE_SPECIAL = 2; constexpr int PAD_PORT_TYPE_REMOTE = 16; constexpr int PAD_USER_ID_SYSTEM = 0xff; +constexpr int PAD_MAX_CONTROLLERS = 4; +constexpr int PAD_HANDLE_BASE = 1; constexpr int PAD_HANDLE = 1; constexpr int PAD_REMOTE_HANDLE = 0x10; constexpr int PAD_SPECIAL_HANDLE = 0x20; diff --git a/core/libs/prx/libScePad/include/PadState.hpp b/core/libs/prx/libScePad/include/PadState.hpp index fb621171ee..aee2c05569 100644 --- a/core/libs/prx/libScePad/include/PadState.hpp +++ b/core/libs/prx/libScePad/include/PadState.hpp @@ -56,23 +56,36 @@ struct PadOutputState { namespace Pad { void Initialize(); bool IsInitialized(); -PadData ReadState(); -int Read(PadData* data, int num); +PadData ReadState(int port = 0); +int Read(PadData* data, int num, int port = 0); +void SetVibration(int port, std::uint8_t large, std::uint8_t small); void SetVibration(std::uint8_t large, std::uint8_t small); +void SetVibrationMode(int port, int mode); void SetVibrationMode(int mode); +void SetLightBar(int port, bool valid, std::uint8_t r, std::uint8_t g, std::uint8_t b); void SetLightBar(bool valid, std::uint8_t r, std::uint8_t g, std::uint8_t b); +void SetTriggerCommand(int port, int trigger, const std::uint8_t* command); void SetTriggerCommand(int trigger, const std::uint8_t* command); -void ResetOrientation(); +void ResetOrientation(int port = 0); +void SetMotionEnabled(int port, bool enabled); void SetMotionEnabled(bool enabled); +void SetTiltCorrection(int port, bool enabled); void SetTiltCorrection(bool enabled); +void SetAngularVelocityDeadband(int port, bool enabled); void SetAngularVelocityDeadband(bool enabled); +void SetConnected(int port, bool connected); +bool IsConnected(int port); } extern "C" void PadPublishInput_nid_postfix(const PadInputState& input); +extern "C" void PadPublishInputForPort_nid_postfix(int port, const PadInputState& input); extern "C" void PadReportInputFailure_nid_postfix(std::exception_ptr error); extern "C" bool PadFetchOutput_nid_postfix(std::uint32_t* seenSequence, PadOutputState* out); +extern "C" bool PadFetchOutputForPort_nid_postfix(int port, std::uint32_t* seenSequence, PadOutputState* out); extern "C" void PadSubmitAudioHaptics_nid_postfix(std::uint8_t large, std::uint8_t small); extern "C" void PadQueueLegacyAudioHaptics_nid_postfix(int source, std::uint64_t begin, std::uint64_t end, std::uint8_t large, std::uint8_t small); extern "C" void PadClearLegacyAudioHaptics_nid_postfix(int source); +extern "C" void PadSubmitAudioHapticsForPort_nid_postfix(int port, std::uint8_t large, std::uint8_t small); +extern "C" void PadSetPortConnected_nid_postfix(int port, bool connected); #endif diff --git a/core/libs/prx/libScePad/src/PadState.cpp b/core/libs/prx/libScePad/src/PadState.cpp index e4bcf6eb90..4e1453dbfe 100644 --- a/core/libs/prx/libScePad/src/PadState.cpp +++ b/core/libs/prx/libScePad/src/PadState.cpp @@ -13,18 +13,8 @@ #include namespace { - std::mutex stateMutex; - PadInputState state; - std::uint32_t pressedSinceRead = 0; - std::uint8_t pressedAnalogL2 = 0; - std::uint8_t pressedAnalogR2 = 0; - std::uint64_t pressedAt = 0; + struct Quat { float w = 1.0f, x = 0.0f, y = 0.0f, z = 0.0f; }; constexpr std::uint64_t kLatchExpiry = 100000; - PadOutputState output; - std::chrono::steady_clock::time_point audioHapticEnd; - std::array audioHapticRumble{}; - PadAudioHapticQueue legacyHaptics; - std::uint64_t timestamp = 0; std::uint64_t injectedTimestamp = 0; struct QueuedInput { @@ -32,29 +22,48 @@ namespace { std::uint64_t timestamp = 0; }; constexpr std::size_t kQueueCapacity = 64; - std::deque queue; + + struct PadSlot { + PadInputState state; + PadOutputState output; + std::deque queue; + Quat orientation; + std::uint64_t lastFuseTime = 0; + float biasIntegral[3] = {0.0f, 0.0f, 0.0f}; + bool tiltCorrection = true; + bool angularVelocityDeadband = false; + std::uint8_t nextTouchId = 0; + bool prevTouchActive[2] = {false, false}; + std::uint8_t touchIds[2] = {0, 0}; + std::chrono::steady_clock::time_point audioHapticEnd{}; + std::array audioHapticRumble{}; + PadAudioHapticQueue legacyHaptics; + std::uint64_t timestamp = 0; + std::uint32_t pressedSinceRead = 0; + std::uint8_t pressedAnalogL2 = 0; + std::uint8_t pressedAnalogR2 = 0; + std::uint64_t pressedAt = 0; + bool connected = false; + }; + + std::mutex stateMutex; + std::array slots = [] { + std::array initialSlots{}; + initialSlots[0].connected = true; + return initialSlots; + }(); std::exception_ptr failure; bool initialized = false; - - struct Quat { float w = 1.0f, x = 0.0f, y = 0.0f, z = 0.0f; }; - Quat orientation; - std::uint64_t lastFuseTime = 0; - float biasIntegral[3] = {0.0f, 0.0f, 0.0f}; - bool tiltCorrection = true; - bool angularVelocityDeadband = false; constexpr float kAngularVelocityDeadband = 0.017453292f; - std::uint8_t nextTouchId = 0; - bool prevTouchActive[2] = {false, false}; - std::uint8_t touchIds[2] = {0, 0}; - bool RefreshAudioHaptics() { - if (std::chrono::steady_clock::now() >= audioHapticEnd) audioHapticRumble = {}; - const auto legacy = legacyHaptics.Read(sceKernelGetProcessTime()); - const auto large = std::max(audioHapticRumble[0], legacy[0]); - const auto small = std::max(audioHapticRumble[1], legacy[1]); - if (output.audioVibrationLarge == large && output.audioVibrationSmall == small) return false; - output.audioVibrationLarge = large; - output.audioVibrationSmall = small; + bool RefreshAudioHaptics(PadSlot& slot) { + if (std::chrono::steady_clock::now() >= slot.audioHapticEnd) slot.audioHapticRumble = {}; + const auto legacy = slot.legacyHaptics.Read(sceKernelGetProcessTime()); + const auto large = std::max(slot.audioHapticRumble[0], legacy[0]); + const auto small = std::max(slot.audioHapticRumble[1], legacy[1]); + if (slot.output.audioVibrationLarge == large && slot.output.audioVibrationSmall == small) return false; + slot.output.audioVibrationLarge = large; + slot.output.audioVibrationSmall = small; return true; } @@ -64,33 +73,33 @@ namespace { q.w /= n; q.x /= n; q.y /= n; q.z /= n; } - void fuse(float dt, const std::array& accel, const std::array& gyro) { + void fuse(PadSlot& slot, float dt, const std::array& accel, const std::array& gyro) { float gx = gyro[0], gy = gyro[1], gz = gyro[2]; const float an = std::sqrt(accel[0] * accel[0] + accel[1] * accel[1] + accel[2] * accel[2]); - if (tiltCorrection && an > 1.0f) { + if (slot.tiltCorrection && an > 1.0f) { const float ax = accel[0] / an, ay = accel[1] / an, az = accel[2] / an; - const float vx = 2.0f * (orientation.x * orientation.y + orientation.w * orientation.z); - const float vy = orientation.w * orientation.w - orientation.x * orientation.x + orientation.y * orientation.y - orientation.z * orientation.z; - const float vz = 2.0f * (orientation.y * orientation.z - orientation.w * orientation.x); + const float vx = 2.0f * (slot.orientation.x * slot.orientation.y + slot.orientation.w * slot.orientation.z); + const float vy = slot.orientation.w * slot.orientation.w - slot.orientation.x * slot.orientation.x + slot.orientation.y * slot.orientation.y - slot.orientation.z * slot.orientation.z; + const float vz = 2.0f * (slot.orientation.y * slot.orientation.z - slot.orientation.w * slot.orientation.x); const float ex = ay * vz - az * vy; const float ey = az * vx - ax * vz; const float ez = ax * vy - ay * vx; constexpr float kp = 1.5f; constexpr float ki = 0.02f; - biasIntegral[0] += ki * ex * dt; - biasIntegral[1] += ki * ey * dt; - biasIntegral[2] += ki * ez * dt; - gx += kp * ex + biasIntegral[0]; - gy += kp * ey + biasIntegral[1]; - gz += kp * ez + biasIntegral[2]; + slot.biasIntegral[0] += ki * ex * dt; + slot.biasIntegral[1] += ki * ey * dt; + slot.biasIntegral[2] += ki * ez * dt; + gx += kp * ex + slot.biasIntegral[0]; + gy += kp * ey + slot.biasIntegral[1]; + gz += kp * ez + slot.biasIntegral[2]; } const float hx = 0.5f * gx * dt, hy = 0.5f * gy * dt, hz = 0.5f * gz * dt; - const Quat q = orientation; - orientation.w = q.w - q.x * hx - q.y * hy - q.z * hz; - orientation.x = q.x + q.w * hx + q.y * hz - q.z * hy; - orientation.y = q.y + q.w * hy - q.x * hz + q.z * hx; - orientation.z = q.z + q.w * hz + q.x * hy - q.y * hx; - normalize(orientation); + const Quat q = slot.orientation; + slot.orientation.w = q.w - q.x * hx - q.y * hy - q.z * hz; + slot.orientation.x = q.x + q.w * hx + q.y * hz - q.z * hy; + slot.orientation.y = q.y + q.w * hy - q.x * hz + q.z * hx; + slot.orientation.z = q.z + q.w * hz + q.x * hy - q.y * hx; + normalize(slot.orientation); } void encodeZones(std::uint8_t* dst, std::uint8_t mode, const std::uint8_t* strengths, int count, std::uint8_t frequency) { @@ -125,10 +134,13 @@ void Pad::Initialize() { std::lock_guard lock(stateMutex); if (failure) std::rethrow_exception(failure); if (!initialized) { - timestamp = sceKernelGetProcessTime(); - lastFuseTime = timestamp; - queue.clear(); - pressedSinceRead = 0; + const std::uint64_t now = sceKernelGetProcessTime(); + for (std::size_t i = 0; i < slots.size(); ++i) { + slots[i].timestamp = now; + slots[i].lastFuseTime = now; + slots[i].queue.clear(); + slots[i].pressedSinceRead = 0; + } } initialized = true; } @@ -139,7 +151,7 @@ bool Pad::IsInitialized() { } namespace { -PadData BuildData(const PadInputState& state, std::uint64_t changeTime, std::uint64_t now, const Pad::InputCommandFile::Input& injected) { +PadData BuildData(PadSlot& slot, const PadInputState& state, std::uint64_t changeTime, std::uint64_t now, const Pad::InputCommandFile::Input& injected, int port) { PadData data{}; data.buttons = state.buttons | injected.buttons; const auto& sticks = injected.hasSticks ? injected.sticks : state.sticks; @@ -153,28 +165,28 @@ PadData BuildData(const PadInputState& state, std::uint64_t changeTime, std::uin data.analog_buttons_l2 = trigger(state.analogButtonsL2, 0x100); data.analog_buttons_r2 = trigger(state.analogButtonsR2, 0x200); - const bool live = state.hasMotion && output.motionEnabled; + const bool live = state.hasMotion && slot.output.motionEnabled; const std::array rest{0.0f, 9.80665f, 0.0f}; const std::array& accel = live ? state.accel : rest; std::array gyro = live ? state.gyro : std::array{0.0f, 0.0f, 0.0f}; - if (angularVelocityDeadband) { + if (slot.angularVelocityDeadband) { for (float& axis : gyro) { if (axis < kAngularVelocityDeadband && axis > -kAngularVelocityDeadband) axis = 0.0f; } } if (live) { - float dt = lastFuseTime != 0 && now > lastFuseTime ? static_cast(now - lastFuseTime) * 1e-6f : 0.0f; + float dt = slot.lastFuseTime != 0 && now > slot.lastFuseTime ? static_cast(now - slot.lastFuseTime) * 1e-6f : 0.0f; dt = std::min(dt, 0.1f); - fuse(dt, accel, gyro); + fuse(slot, dt, accel, gyro); } else { - orientation = Quat{}; - biasIntegral[0] = biasIntegral[1] = biasIntegral[2] = 0.0f; + slot.orientation = Quat{}; + slot.biasIntegral[0] = slot.biasIntegral[1] = slot.biasIntegral[2] = 0.0f; } - lastFuseTime = now; - data.orientation_x = orientation.x; - data.orientation_y = orientation.y; - data.orientation_z = orientation.z; - data.orientation_w = orientation.w; + slot.lastFuseTime = now; + data.orientation_x = slot.orientation.x; + data.orientation_y = slot.orientation.y; + data.orientation_z = slot.orientation.z; + data.orientation_w = slot.orientation.w; constexpr float kGravity = 9.80665f; data.acceleration_x = accel[0] / kGravity; data.acceleration_y = accel[1] / kGravity; @@ -183,20 +195,24 @@ PadData BuildData(const PadInputState& state, std::uint64_t changeTime, std::uin data.angular_velocity_y = gyro[1]; data.angular_velocity_z = gyro[2]; - data.connected = true; - data.connected_count = 1; + std::uint8_t connectedCount = 0; + for (const auto& s : slots) { + if (s.connected) ++connectedCount; + } + data.connected = slot.connected; + data.connected_count = connectedCount > 0 ? connectedCount : 1; data.timestamp = changeTime; std::uint8_t touchNum = 0; for (int i = 0; i < 2; ++i) { const PadTouchPoint& tp = state.touch[i]; - if (!tp.active) { prevTouchActive[i] = false; continue; } - if (!prevTouchActive[i]) touchIds[i] = static_cast(nextTouchId++ & 0x7F); - prevTouchActive[i] = true; + if (!tp.active) { slot.prevTouchActive[i] = false; continue; } + if (!slot.prevTouchActive[i]) slot.touchIds[i] = static_cast(slot.nextTouchId++ & 0x7F); + slot.prevTouchActive[i] = true; if (touchNum == 0) { - data.touch_data_touch0_x = tp.x; data.touch_data_touch0_y = tp.y; data.touch_data_touch0_id = touchIds[i]; + data.touch_data_touch0_x = tp.x; data.touch_data_touch0_y = tp.y; data.touch_data_touch0_id = slot.touchIds[i]; } else { - data.touch_data_touch1_x = tp.x; data.touch_data_touch1_y = tp.y; data.touch_data_touch1_id = touchIds[i]; + data.touch_data_touch1_x = tp.x; data.touch_data_touch1_y = tp.y; data.touch_data_touch1_id = slot.touchIds[i]; } ++touchNum; } @@ -224,67 +240,85 @@ Pad::InputCommandFile::Input InjectedInput(std::uint64_t now) { const auto injected = commands.Poll(now); if (injected != lastInjected) { lastInjected = injected; - timestamp = now; + slots[0].timestamp = now; injectedTimestamp = now; } return injected; } } -PadData Pad::ReadState() { +PadData Pad::ReadState(int port) { + if (port < 0 || port >= PAD_MAX_CONTROLLERS) throw std::invalid_argument("Pad: invalid port"); std::lock_guard lock(stateMutex); if (failure) std::rethrow_exception(failure); if (!initialized) throw std::runtime_error("Pad: read before initialization"); const std::uint64_t now = sceKernelGetProcessTime(); - const auto injected = InjectedInput(now); - PadData data = BuildData(state, timestamp, now, injected); - if (pressedSinceRead != 0 && now - pressedAt <= kLatchExpiry) { - data.buttons |= pressedSinceRead; - if (pressedSinceRead & 0x100u) data.analog_buttons_l2 = std::max(data.analog_buttons_l2, pressedAnalogL2); - if (pressedSinceRead & 0x200u) data.analog_buttons_r2 = std::max(data.analog_buttons_r2, pressedAnalogR2); + const auto injected = port == 0 ? InjectedInput(now) : Pad::InputCommandFile::Input{}; + auto& slot = slots[port]; + PadData data = BuildData(slot, slot.state, slot.timestamp, now, injected, port); + if (slot.pressedSinceRead != 0 && now - slot.pressedAt <= kLatchExpiry) { + data.buttons |= slot.pressedSinceRead; + if (slot.pressedSinceRead & 0x100u) data.analog_buttons_l2 = std::max(data.analog_buttons_l2, slot.pressedAnalogL2); + if (slot.pressedSinceRead & 0x200u) data.analog_buttons_r2 = std::max(data.analog_buttons_r2, slot.pressedAnalogR2); } - pressedSinceRead = 0; + slot.pressedSinceRead = 0; return data; } -int Pad::Read(PadData* data, int num) { +int Pad::Read(PadData* data, int num, int port) { if (data == nullptr || num <= 0) throw std::invalid_argument("Pad: invalid read buffer"); + if (port < 0 || port >= PAD_MAX_CONTROLLERS) throw std::invalid_argument("Pad: invalid port"); std::lock_guard lock(stateMutex); if (failure) std::rethrow_exception(failure); if (!initialized) throw std::runtime_error("Pad: read before initialization"); - pressedSinceRead = 0; + auto& slot = slots[port]; + slot.pressedSinceRead = 0; const std::uint64_t now = sceKernelGetProcessTime(); - const auto injected = InjectedInput(now); - if (queue.empty()) { - data[0] = BuildData(state, timestamp, now, injected); + const auto injected = port == 0 ? InjectedInput(now) : Pad::InputCommandFile::Input{}; + if (slot.queue.empty()) { + data[0] = BuildData(slot, slot.state, slot.timestamp, now, injected, port); return 1; } int count = 0; - while (count < num && !queue.empty()) { - const QueuedInput& entry = queue.front(); - data[count++] = BuildData(entry.input, std::max(entry.timestamp, injectedTimestamp), now, injected); - queue.pop_front(); + while (count < num && !slot.queue.empty()) { + const QueuedInput& entry = slot.queue.front(); + data[count++] = BuildData(slot, entry.input, port == 0 ? std::max(entry.timestamp, injectedTimestamp) : entry.timestamp, now, injected, port); + slot.queue.pop_front(); } return count; } -void Pad::SetVibration(std::uint8_t large, std::uint8_t small) { +void Pad::SetVibration(int port, std::uint8_t large, std::uint8_t small) { + if (port < 0 || port >= PAD_MAX_CONTROLLERS) return; std::lock_guard lock(stateMutex); + auto& output = slots[port].output; if (output.vibrationLarge == large && output.vibrationSmall == small) return; output.vibrationLarge = large; output.vibrationSmall = small; ++output.sequence; } -void Pad::SetVibrationMode(int mode) { +void Pad::SetVibration(std::uint8_t large, std::uint8_t small) { + SetVibration(0, large, small); +} + +void Pad::SetVibrationMode(int port, int mode) { + if (port < 0 || port >= PAD_MAX_CONTROLLERS) return; std::lock_guard lock(stateMutex); + auto& output = slots[port].output; if (output.vibrationMode == mode) return; output.vibrationMode = mode; ++output.sequence; } -void Pad::SetLightBar(bool valid, std::uint8_t r, std::uint8_t g, std::uint8_t b) { +void Pad::SetVibrationMode(int mode) { + SetVibrationMode(0, mode); +} + +void Pad::SetLightBar(int port, bool valid, std::uint8_t r, std::uint8_t g, std::uint8_t b) { + if (port < 0 || port >= PAD_MAX_CONTROLLERS) return; std::lock_guard lock(stateMutex); + auto& output = slots[port].output; output.lightBarValid = valid; output.lightBar[0] = r; output.lightBar[1] = g; @@ -292,8 +326,12 @@ void Pad::SetLightBar(bool valid, std::uint8_t r, std::uint8_t g, std::uint8_t b ++output.sequence; } -void Pad::SetTriggerCommand(int trigger, const std::uint8_t* command) { - if (trigger < 0 || trigger > 1 || command == nullptr) return; +void Pad::SetLightBar(bool valid, std::uint8_t r, std::uint8_t g, std::uint8_t b) { + SetLightBar(0, valid, r, g, b); +} + +void Pad::SetTriggerCommand(int port, int trigger, const std::uint8_t* command) { + if (port < 0 || port >= PAD_MAX_CONTROLLERS || trigger < 0 || trigger > 1 || command == nullptr) return; std::uint32_t mode = 0; std::memcpy(&mode, command, 4); const std::uint8_t* p = command + 8; @@ -350,55 +388,95 @@ void Pad::SetTriggerCommand(int trigger, const std::uint8_t* command) { req.valid = req.effect[0] != 0x05; if (!req.valid) req.fallback = 0; std::lock_guard lock(stateMutex); + auto& output = slots[port].output; output.trigger[trigger] = req; output.triggerTouched = true; ++output.sequence; } -void Pad::ResetOrientation() { +void Pad::SetTriggerCommand(int trigger, const std::uint8_t* command) { + SetTriggerCommand(0, trigger, command); +} + +void Pad::ResetOrientation(int port) { + if (port < 0 || port >= PAD_MAX_CONTROLLERS) return; std::lock_guard lock(stateMutex); - orientation = Quat{}; - biasIntegral[0] = biasIntegral[1] = biasIntegral[2] = 0.0f; + slots[port].orientation = Quat{}; + slots[port].biasIntegral[0] = slots[port].biasIntegral[1] = slots[port].biasIntegral[2] = 0.0f; +} + +void Pad::SetTiltCorrection(int port, bool enabled) { + if (port < 0 || port >= PAD_MAX_CONTROLLERS) return; + std::lock_guard lock(stateMutex); + slots[port].tiltCorrection = enabled; + slots[port].biasIntegral[0] = slots[port].biasIntegral[1] = slots[port].biasIntegral[2] = 0.0f; } void Pad::SetTiltCorrection(bool enabled) { + SetTiltCorrection(0, enabled); +} + +void Pad::SetAngularVelocityDeadband(int port, bool enabled) { + if (port < 0 || port >= PAD_MAX_CONTROLLERS) return; std::lock_guard lock(stateMutex); - tiltCorrection = enabled; - biasIntegral[0] = biasIntegral[1] = biasIntegral[2] = 0.0f; + slots[port].angularVelocityDeadband = enabled; } void Pad::SetAngularVelocityDeadband(bool enabled) { - std::lock_guard lock(stateMutex); - angularVelocityDeadband = enabled; + SetAngularVelocityDeadband(0, enabled); } -void Pad::SetMotionEnabled(bool enabled) { +void Pad::SetMotionEnabled(int port, bool enabled) { + if (port < 0 || port >= PAD_MAX_CONTROLLERS) return; std::lock_guard lock(stateMutex); + auto& output = slots[port].output; if (output.motionEnabled == enabled) return; output.motionEnabled = enabled; ++output.sequence; } -extern "C" void PadPublishInput_nid_postfix(const PadInputState& input) { +void Pad::SetMotionEnabled(bool enabled) { + SetMotionEnabled(0, enabled); +} + +void Pad::SetConnected(int port, bool connected) { + if (port < 0 || port >= PAD_MAX_CONTROLLERS) return; + std::lock_guard lock(stateMutex); + slots[port].connected = connected; +} + +bool Pad::IsConnected(int port) { + if (port < 0 || port >= PAD_MAX_CONTROLLERS) return false; + std::lock_guard lock(stateMutex); + return slots[port].connected; +} + +extern "C" void PadPublishInputForPort_nid_postfix(int port, const PadInputState& input) { + if (port < 0 || port >= PAD_MAX_CONTROLLERS) return; std::lock_guard lock(stateMutex); if (failure) std::rethrow_exception(failure); - if (state.buttons == input.buttons && state.sticks == input.sticks && - state.analogButtonsL2 == input.analogButtonsL2 && state.analogButtonsR2 == input.analogButtonsR2 && - state.touchLeft == input.touchLeft && state.touchRight == input.touchRight && - state.hasMotion == input.hasMotion && state.accel == input.accel && state.gyro == input.gyro && - state.touch == input.touch && state.deviceKind == input.deviceKind) return; - const std::uint32_t pressed = input.buttons & ~state.buttons; - timestamp = sceKernelGetProcessTime(); + auto& slot = slots[port]; + if (slot.state.buttons == input.buttons && slot.state.sticks == input.sticks && + slot.state.analogButtonsL2 == input.analogButtonsL2 && slot.state.analogButtonsR2 == input.analogButtonsR2 && + slot.state.touchLeft == input.touchLeft && slot.state.touchRight == input.touchRight && + slot.state.hasMotion == input.hasMotion && slot.state.accel == input.accel && slot.state.gyro == input.gyro && + slot.state.touch == input.touch && slot.state.deviceKind == input.deviceKind) return; + const std::uint32_t pressed = input.buttons & ~slot.state.buttons; + slot.timestamp = sceKernelGetProcessTime(); if (pressed != 0) { - if (pressedSinceRead != 0 && timestamp - pressedAt > kLatchExpiry) pressedSinceRead = 0; - pressedSinceRead |= pressed; - pressedAt = timestamp; - if (pressed & 0x100u) pressedAnalogL2 = input.analogButtonsL2; - if (pressed & 0x200u) pressedAnalogR2 = input.analogButtonsR2; + if (slot.pressedSinceRead != 0 && slot.timestamp - slot.pressedAt > kLatchExpiry) slot.pressedSinceRead = 0; + slot.pressedSinceRead |= pressed; + slot.pressedAt = slot.timestamp; + if (pressed & 0x100u) slot.pressedAnalogL2 = input.analogButtonsL2; + if (pressed & 0x200u) slot.pressedAnalogR2 = input.analogButtonsR2; } - state = input; - if (queue.size() == kQueueCapacity) queue.pop_front(); - queue.push_back({input, timestamp}); + slot.state = input; + if (slot.queue.size() == kQueueCapacity) slot.queue.pop_front(); + slot.queue.push_back({input, slot.timestamp}); +} + +extern "C" void PadPublishInput_nid_postfix(const PadInputState& input) { + PadPublishInputForPort_nid_postfix(0, input); } extern "C" void PadReportInputFailure_nid_postfix(std::exception_ptr error) { @@ -407,35 +485,51 @@ extern "C" void PadReportInputFailure_nid_postfix(std::exception_ptr error) { if (!failure) failure = error; } -extern "C" bool PadFetchOutput_nid_postfix(std::uint32_t* seenSequence, PadOutputState* out) { +extern "C" bool PadFetchOutputForPort_nid_postfix(int port, std::uint32_t* seenSequence, PadOutputState* out) { + if (port < 0 || port >= PAD_MAX_CONTROLLERS) return false; std::lock_guard lock(stateMutex); - if (RefreshAudioHaptics()) ++output.sequence; - if (seenSequence == nullptr || out == nullptr || *seenSequence == output.sequence) return false; - *seenSequence = output.sequence; - *out = output; + auto& slot = slots[port]; + if (RefreshAudioHaptics(slot)) ++slot.output.sequence; + if (seenSequence == nullptr || out == nullptr || *seenSequence == slot.output.sequence) return false; + *seenSequence = slot.output.sequence; + *out = slot.output; return true; } -extern "C" void PadSubmitAudioHaptics_nid_postfix(std::uint8_t large, std::uint8_t small) { +extern "C" bool PadFetchOutput_nid_postfix(std::uint32_t* seenSequence, PadOutputState* out) { + return PadFetchOutputForPort_nid_postfix(0, seenSequence, out); +} + +extern "C" void PadSubmitAudioHapticsForPort_nid_postfix(int port, std::uint8_t large, std::uint8_t small) { + if (port < 0 || port >= PAD_MAX_CONTROLLERS) return; std::lock_guard lock(stateMutex); if (large == 0 && small == 0) return; const auto now = std::chrono::steady_clock::now(); - if (now >= audioHapticEnd) audioHapticRumble = {}; - audioHapticRumble[0] = std::max(audioHapticRumble[0], large); - audioHapticRumble[1] = std::max(audioHapticRumble[1], small); - audioHapticEnd = now + std::chrono::milliseconds(80); - static_cast(RefreshAudioHaptics()); - ++output.sequence; + auto& slot = slots[port]; + if (now >= slot.audioHapticEnd) slot.audioHapticRumble = {}; + slot.audioHapticRumble[0] = std::max(slot.audioHapticRumble[0], large); + slot.audioHapticRumble[1] = std::max(slot.audioHapticRumble[1], small); + slot.audioHapticEnd = now + std::chrono::milliseconds(80); + static_cast(RefreshAudioHaptics(slot)); + ++slot.output.sequence; } extern "C" void PadQueueLegacyAudioHaptics_nid_postfix(int source, std::uint64_t begin, std::uint64_t end, std::uint8_t large, std::uint8_t small) { std::lock_guard lock(stateMutex); - legacyHaptics.Queue(source, begin, end, large, small); - if (RefreshAudioHaptics()) ++output.sequence; + slots[0].legacyHaptics.Queue(source, begin, end, large, small); + if (RefreshAudioHaptics(slots[0])) ++slots[0].output.sequence; } extern "C" void PadClearLegacyAudioHaptics_nid_postfix(int source) { std::lock_guard lock(stateMutex); - legacyHaptics.Clear(source); - if (RefreshAudioHaptics()) ++output.sequence; + slots[0].legacyHaptics.Clear(source); + if (RefreshAudioHaptics(slots[0])) ++slots[0].output.sequence; +} + +extern "C" void PadSubmitAudioHaptics_nid_postfix(std::uint8_t large, std::uint8_t small) { + PadSubmitAudioHapticsForPort_nid_postfix(0, large, small); +} + +extern "C" void PadSetPortConnected_nid_postfix(int port, bool connected) { + Pad::SetConnected(port, connected); } diff --git a/core/libs/prx/libSceUserService/Export.cpp b/core/libs/prx/libSceUserService/Export.cpp index 513d813d46..e96a561ddf 100644 --- a/core/libs/prx/libSceUserService/Export.cpp +++ b/core/libs/prx/libSceUserService/Export.cpp @@ -3,14 +3,35 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include +#include #include +#include +#include #include "prx/libSceUserService/UserService.hpp" -static std::atomic initialized{false}; -static std::atomic loginReported{false}; +namespace { -static int ReportAccessibilitySetting(int user_id, int32_t* setting, int32_t value) { - if (user_id != USER_SERVICE_INITIAL_USER_ID) { +std::atomic initialized{false}; +std::mutex userMutex; + +struct UserEvent { + std::uint32_t eventType; + int userId; +}; + +std::deque eventQueue; +std::array userConnected{true, false, false, false}; + +int UserIndex(int userId) { + if (userId >= USER_SERVICE_INITIAL_USER_ID && userId < USER_SERVICE_INITIAL_USER_ID + USER_SERVICE_MAX_USERS) { + return userId - USER_SERVICE_INITIAL_USER_ID; + } + return -1; +} + +int ReportAccessibilitySetting(int user_id, int32_t* setting, int32_t value) { + const int index = UserIndex(user_id); + if (index < 0) { return USER_SERVICE_ERROR_NOT_LOGGED_IN; } if (setting == nullptr) { @@ -20,6 +41,17 @@ static int ReportAccessibilitySetting(int user_id, int32_t* setting, int32_t val return USER_SERVICE_OK; } +void PopulateInitialLoginEvents() { + std::lock_guard lock(userMutex); + eventQueue.clear(); + constexpr std::uint32_t EventTypeLogin = 0; + eventQueue.push_back({EventTypeLogin, USER_SERVICE_INITIAL_USER_ID}); + for (int i = 1; i < USER_SERVICE_MAX_USERS; ++i) { + if (userConnected[i]) { + eventQueue.push_back({EventTypeLogin, USER_SERVICE_INITIAL_USER_ID + i}); + } + } +} extern "C" int APS5_VABI sceSystemServiceParamGetInt(int param_id, int* value); @@ -47,15 +79,43 @@ static int ReportKeyboardSetting(int user_id, int32_t* type, int32_t (*value)()) if (type == nullptr) { return USER_SERVICE_ERROR_INVALID_ARGUMENT; } - if (user_id != USER_SERVICE_INITIAL_USER_ID) { + if (UserIndex(user_id) < 0) { return USER_SERVICE_ERROR_UNKNOWN_USER; } *type = value(); return USER_SERVICE_OK; } +} extern "C" { +void UserServiceSetUserConnected_nid_postfix(int userIndex, bool connected) { + if (userIndex <= 0 || userIndex >= USER_SERVICE_MAX_USERS) return; + std::lock_guard lock(userMutex); + if (userConnected[userIndex] != connected) { + userConnected[userIndex] = connected; + if (initialized.load()) { + constexpr std::uint32_t EventTypeLogin = 0; + constexpr std::uint32_t EventTypeLogout = 1; + const std::uint32_t eventType = connected ? EventTypeLogin : EventTypeLogout; + const int userId = USER_SERVICE_INITIAL_USER_ID + userIndex; + bool collapsed = false; + for (auto it = eventQueue.begin(); it != eventQueue.end(); ++it) { + if (it->userId == userId) { + if (it->eventType != eventType) { + eventQueue.erase(it); + } + collapsed = true; + break; + } + } + if (!collapsed) { + eventQueue.push_back({eventType, userId}); + } + } + } +} + int APS5_VABI sceUserServiceGetAccessibilityChatTranscription(int user_id, int32_t* chat_transcription) { return ReportAccessibilitySetting(user_id, chat_transcription, 0); } @@ -91,7 +151,8 @@ int APS5_VABI sceUserServiceGetAccessibilityZoomFollowFocus(int user_id, int32_t } int APS5_VABI sceUserServiceGetAgeLevel(int user_id, uint32_t* age_level) { - if (user_id != USER_SERVICE_INITIAL_USER_ID) { + const int index = UserIndex(user_id); + if (index < 0) { return USER_SERVICE_ERROR_NOT_LOGGED_IN; } if (age_level == nullptr) { @@ -110,12 +171,14 @@ int APS5_VABI sceUserServiceGetEvent(SceUserServiceEvent* event) { if (event == nullptr) { return USER_SERVICE_ERROR_INVALID_ARGUMENT; } - if (loginReported.exchange(true)) { + std::lock_guard lock(userMutex); + if (eventQueue.empty()) { return USER_SERVICE_ERROR_NO_EVENT; } - constexpr std::uint32_t EventTypeLogin = 0; - event->event_type = EventTypeLogin; - event->user_id = USER_SERVICE_INITIAL_USER_ID; + const auto item = eventQueue.front(); + eventQueue.pop_front(); + event->event_type = item.eventType; + event->user_id = item.userId; return USER_SERVICE_OK; } @@ -123,7 +186,8 @@ int APS5_VABI sceUserServiceGetGamePresets(int user_id, UserServiceGamePresets* // System-wide game presets (difficulty, view inversion, subtitles, audio language) of the user's // profile. A fresh console has none set: every field is 0, "not specified", and the title falls // back to its own defaults. this_size is the caller's. - if (presets == nullptr || user_id != USER_SERVICE_INITIAL_USER_ID) { + const int index = UserIndex(user_id); + if (presets == nullptr || index < 0) { return USER_SERVICE_ERROR_INVALID_ARGUMENT; } presets->difficulty = 0; @@ -152,10 +216,14 @@ int APS5_VABI sceUserServiceGetLoginUserIdList(UserServiceLoginUserIdList* user_ if (user_id_list == nullptr) { return USER_SERVICE_ERROR_INVALID_ARGUMENT; } - user_id_list->user_id[0] = USER_SERVICE_INITIAL_USER_ID; - user_id_list->user_id[1] = USER_SERVICE_USER_ID_INVALID; - user_id_list->user_id[2] = USER_SERVICE_USER_ID_INVALID; - user_id_list->user_id[3] = USER_SERVICE_USER_ID_INVALID; + std::lock_guard lock(userMutex); + for (int i = 0; i < 4; ++i) { + if (i < USER_SERVICE_MAX_USERS && userConnected[i]) { + user_id_list->user_id[i] = USER_SERVICE_INITIAL_USER_ID + i; + } else { + user_id_list->user_id[i] = USER_SERVICE_USER_ID_INVALID; + } + } return USER_SERVICE_OK; } @@ -163,22 +231,24 @@ int APS5_VABI sceUserServiceGetUserName(int user_id, char* name, size_t size) { if (name == nullptr) { return USER_SERVICE_ERROR_INVALID_ARGUMENT; } - if (user_id != USER_SERVICE_INITIAL_USER_ID) { + const int index = UserIndex(user_id); + if (index < 0) { return USER_SERVICE_ERROR_UNKNOWN_USER; } if (size < USER_SERVICE_MAX_USER_NAME_LENGTH + 1) { std::memset(name, 0, size); return USER_SERVICE_ERROR_BUFFER_TOO_SHORT; } - std::strncpy(name, USER_SERVICE_INITIAL_USER_NAME, size); + std::strncpy(name, USER_SERVICE_USER_NAMES[index], size); return USER_SERVICE_OK; } int APS5_VABI sceUserServiceGetUserNumber(int user_id, int32_t* number) { - if (number == nullptr || user_id != USER_SERVICE_INITIAL_USER_ID) { + const int index = UserIndex(user_id); + if (number == nullptr || index < 0) { return USER_SERVICE_ERROR_INVALID_ARGUMENT; } - *number = 1; + *number = index + 1; return USER_SERVICE_OK; } @@ -190,7 +260,7 @@ static int MarkInitialized() { if (initialized.exchange(true)) { return USER_SERVICE_ERROR_ALREADY_INITIALIZED; } - loginReported.store(false); + PopulateInitialLoginEvents(); return USER_SERVICE_OK; } @@ -214,6 +284,8 @@ int APS5_VABI sceUserServiceTerminate(void) { if (!initialized.exchange(false)) { return USER_SERVICE_ERROR_NOT_INITIALIZED; } + std::lock_guard lock(userMutex); + eventQueue.clear(); return USER_SERVICE_OK; } @@ -237,18 +309,23 @@ int APS5_VABI sceUserServiceGetRegisteredUserIdList(UserServiceRegisteredUserIdL if (user_id_list == nullptr) { return USER_SERVICE_ERROR_INVALID_ARGUMENT; } - user_id_list->user_id[0] = USER_SERVICE_INITIAL_USER_ID; - for (int i = 1; i < 16; ++i) { - user_id_list->user_id[i] = USER_SERVICE_USER_ID_INVALID; + std::lock_guard lock(userMutex); + for (int i = 0; i < 16; ++i) { + if (i < USER_SERVICE_MAX_USERS && userConnected[i]) { + user_id_list->user_id[i] = USER_SERVICE_INITIAL_USER_ID + i; + } else { + user_id_list->user_id[i] = USER_SERVICE_USER_ID_INVALID; + } } return USER_SERVICE_OK; } int APS5_VABI sceUserServiceGetUserColor(int user_id, int* color) { - if (color == nullptr || user_id != USER_SERVICE_INITIAL_USER_ID) { + const int index = UserIndex(user_id); + if (color == nullptr || index < 0) { return USER_SERVICE_ERROR_INVALID_ARGUMENT; } - *color = 0; + *color = index; return USER_SERVICE_OK; } @@ -265,7 +342,8 @@ int APS5_VABI sceUserServiceGetTraditionalChineseInputType(int user_id, int32_t* } int APS5_VABI sceUserServiceGetNpAccountId(int user_id, uint64_t* account_id) { - if (account_id == nullptr || user_id != USER_SERVICE_INITIAL_USER_ID) { + const int index = UserIndex(user_id); + if (account_id == nullptr || index < 0) { return USER_SERVICE_ERROR_INVALID_ARGUMENT; } *account_id = 0; diff --git a/core/libs/prx/libSceUserService/UserService.hpp b/core/libs/prx/libSceUserService/UserService.hpp index eeb192f8aa..2ef3478df9 100644 --- a/core/libs/prx/libSceUserService/UserService.hpp +++ b/core/libs/prx/libSceUserService/UserService.hpp @@ -24,5 +24,11 @@ constexpr int USER_SERVICE_ERROR_BUFFER_TOO_SHORT = -2137653238; constexpr int USER_SERVICE_ERROR_UNKNOWN_USER = static_cast(0x80960105u); constexpr std::size_t USER_SERVICE_MAX_USER_NAME_LENGTH = 16; constexpr const char* USER_SERVICE_INITIAL_USER_NAME = "Player"; +constexpr int USER_SERVICE_MAX_USERS = 4; +constexpr const char* USER_SERVICE_USER_NAMES[USER_SERVICE_MAX_USERS] = { + "Player", "Player2", "Player3", "Player4" +}; + +extern "C" void UserServiceSetUserConnected_nid_postfix(int userIndex, bool connected); #endif diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index feb620fab0..5acccb7a32 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -23,7 +23,7 @@ target_include_directories(libSceVideoOut PRIVATE ${CMAKE_BINARY_DIR}/core/libs/prx/libSceAgcDriver/generated ) add_dependencies(libSceVideoOut agc_trace_revision) -target_link_libraries(libSceVideoOut PRIVATE libSceAgcDriver libScePad libSceMouse libSceKeyboard libSceIme libc libkernel decoder_png SDL2-static) +target_link_libraries(libSceVideoOut PRIVATE libSceAgcDriver libScePad libSceUserService libSceMouse libSceKeyboard libSceIme libc libkernel decoder_png SDL2-static) if(WIN32) target_link_libraries(libSceVideoOut PRIVATE comctl32 dxgi) endif() @@ -145,8 +145,14 @@ endif() if(BUILD_TESTING) add_test_executable(pad_sdl_tests tests/PadSdl.cpp) - target_link_libraries(pad_sdl_tests PRIVATE SDL2-static) + target_include_directories(pad_sdl_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(pad_sdl_tests PRIVATE libSceVideoOut libScePad libSceUserService libc SDL2-static) + configure_windows_unwind(pad_sdl_tests) add_test(NAME pad_sdl COMMAND pad_sdl_tests) + if(WIN32) + set_tests_properties(pad_sdl PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$;PATH=path_list_prepend:$;PATH=path_list_prepend:$;PATH=path_list_prepend:$") + endif() add_test_executable(pad_input_mapping_tests tests/InputMapping.cpp src/InputMapping.cpp) target_include_directories(pad_input_mapping_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/SDL2/include) target_link_libraries(pad_input_mapping_tests PRIVATE SDL2-static libc) diff --git a/core/libs/prx/libSceVideoOut/include/PadInput.hpp b/core/libs/prx/libSceVideoOut/include/PadInput.hpp index dfa068d16d..2f5eddb56c 100644 --- a/core/libs/prx/libSceVideoOut/include/PadInput.hpp +++ b/core/libs/prx/libSceVideoOut/include/PadInput.hpp @@ -5,7 +5,9 @@ #include "SDL_gamecontroller.h" #include "prx/libScePad/include/KeyboardMouse.hpp" #include "prx/libScePad/include/PadState.hpp" +#include "prx/libScePad/include/Pad.hpp" #include "prx/libSceVideoOut/include/PadOutputPacing.hpp" +#include #include #include "prx/libSceVideoOut/include/Telemetry.hpp" @@ -18,28 +20,37 @@ public: void HandleEvent(const SDL_Event& event, DisplayWindow& window); void Update(const DisplayWindow& window); bool MouseModeEnabled() const { return keyboardMouse.MouseMode(); } + bool SlotOccupied(int slot) const { + return slot >= 0 && slot < PAD_MAX_CONTROLLERS && slots[slot].controller != nullptr; + } private: + struct ControllerSlot { + SDL_GameController* controller = nullptr; + SDL_JoystickID instanceId = -1; + PadInputState state{}; + std::uint32_t outputSequence = 0; + PadOutputState outputState{}; + bool outputPending = false; + std::chrono::steady_clock::time_point nextRumbleRefresh{}; + LightBarPacer lightBar; + TriggerEffectCache triggerEffect; + }; + explicit PadInput(Pad::InputConfiguration configuration); void publish(std::uint64_t frame); + void publish(std::uint64_t frame, int slot); void setMouseMode(bool enabled); - void openFirstAvailableController(); + void openAvailableControllers(); void openController(int deviceIndex); - void closeController(); - PadInputState sampleController() const; + void closeController(int slot); + PadInputState sampleController(int slot) const; void applyOutput(); - void enableSensors(); + void enableSensors(int slot); Pad::KeyboardMouse keyboardMouse; - SDL_GameController* controller = nullptr; - PadInputState controllerState{}; - std::uint32_t outputSequence = 0; - PadOutputState outputState{}; - bool outputPending = false; + std::array slots{}; VideoOutTelemetry::PadChangeFilter telemetryFilter; - std::chrono::steady_clock::time_point nextRumbleRefresh{}; - LightBarPacer lightBar; - TriggerEffectCache triggerEffect; }; #endif diff --git a/core/libs/prx/libSceVideoOut/src/PadInput.cpp b/core/libs/prx/libSceVideoOut/src/PadInput.cpp index d9abeeb048..5e9ac3fcf7 100644 --- a/core/libs/prx/libSceVideoOut/src/PadInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/PadInput.cpp @@ -14,7 +14,9 @@ #include "prx/libSceAgcDriver/Execution/include/Presentation.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libScePad/include/PadState.hpp" +#include "prx/libScePad/include/Pad.hpp" #include "prx/libScePad/include/PadInputTypes.hpp" +#include "prx/libSceUserService/UserService.hpp" #include "prx/libc/include/General.hpp" #include "prx/libSceIme/include/ImeInput.hpp" @@ -22,15 +24,16 @@ PadInput::PadInput() : PadInput(Pad::LoadInputMapping()) { } PadInput::PadInput(Pad::InputConfiguration configuration) : keyboardMouse(std::move(configuration)) { - openFirstAvailableController(); + openAvailableControllers(); } PadInput::~PadInput() { - closeController(); + for (int i = 0; i < PAD_MAX_CONTROLLERS; ++i) { + closeController(i); + } } -void PadInput::openFirstAvailableController() { - if (controller != nullptr) return; +void PadInput::openAvailableControllers() { SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1"); SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1"); if ((SDL_WasInit(SDL_INIT_GAMECONTROLLER) & SDL_INIT_GAMECONTROLLER) == 0) { @@ -42,82 +45,122 @@ void PadInput::openFirstAvailableController() { for (int deviceIndex = 0; deviceIndex < SDL_NumJoysticks(); ++deviceIndex) { if (!SDL_IsGameController(deviceIndex)) continue; openController(deviceIndex); - if (controller != nullptr) return; } } void PadInput::openController(int deviceIndex) { - if (controller != nullptr || !SDL_IsGameController(deviceIndex)) return; - controller = SDL_GameControllerOpen(deviceIndex); - if (controller == nullptr) { + if (!SDL_IsGameController(deviceIndex)) return; + const auto instanceId = SDL_JoystickGetDeviceInstanceID(deviceIndex); + for (int i = 0; i < PAD_MAX_CONTROLLERS; ++i) { + if (slots[i].controller != nullptr && slots[i].instanceId == instanceId) return; + } + int slot = -1; + for (int i = 0; i < PAD_MAX_CONTROLLERS; ++i) { + if (slots[i].controller == nullptr) { + slot = i; + break; + } + } + if (slot == -1) return; + SDL_GameController* gc = SDL_GameControllerOpen(deviceIndex); + if (gc == nullptr) { APS5_LOG_ERR("Pad: could not open game controller %d: %s", deviceIndex, SDL_GetError()); return; } - const char* name = SDL_GameControllerName(controller); - APS5_LOG_OUT("Pad: connected game controller: %s (type %d, sensors accel=%d gyro=%d, touchpads=%d, led=%d, trigger rumble=%d)", - name != nullptr ? name : "unknown", static_cast(SDL_GameControllerGetType(controller)), - SDL_GameControllerHasSensor(controller, SDL_SENSOR_ACCEL) == SDL_TRUE, SDL_GameControllerHasSensor(controller, SDL_SENSOR_GYRO) == SDL_TRUE, - SDL_GameControllerGetNumTouchpads(controller), SDL_GameControllerHasLED(controller) == SDL_TRUE, SDL_GameControllerHasRumbleTriggers(controller) == SDL_TRUE); - enableSensors(); - outputPending = true; - lightBar.Reset(); - triggerEffect.Reset(); + slots[slot].controller = gc; + slots[slot].instanceId = SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(gc)); + slots[slot].state = {}; + slots[slot].outputSequence = 0; + slots[slot].outputState = {}; + slots[slot].outputPending = true; + slots[slot].nextRumbleRefresh = {}; + slots[slot].lightBar.Reset(); + slots[slot].triggerEffect.Reset(); + const char* name = SDL_GameControllerName(gc); + APS5_LOG_OUT("Pad: connected game controller (slot %d): %s (type %d, sensors accel=%d gyro=%d, touchpads=%d, led=%d, trigger rumble=%d)", + slot, name != nullptr ? name : "unknown", static_cast(SDL_GameControllerGetType(gc)), + SDL_GameControllerHasSensor(gc, SDL_SENSOR_ACCEL) == SDL_TRUE, SDL_GameControllerHasSensor(gc, SDL_SENSOR_GYRO) == SDL_TRUE, + SDL_GameControllerGetNumTouchpads(gc), SDL_GameControllerHasLED(gc) == SDL_TRUE, SDL_GameControllerHasRumbleTriggers(gc) == SDL_TRUE); + enableSensors(slot); + PadSetPortConnected_nid_postfix(slot, true); + UserServiceSetUserConnected_nid_postfix(slot, true); } -void PadInput::enableSensors() { +void PadInput::enableSensors(int slot) { + if (slot < 0 || slot >= PAD_MAX_CONTROLLERS) return; + SDL_GameController* controller = slots[slot].controller; if (controller == nullptr) return; - const SDL_bool wanted = outputState.motionEnabled ? SDL_TRUE : SDL_FALSE; + const SDL_bool wanted = slots[slot].outputState.motionEnabled ? SDL_TRUE : SDL_FALSE; if (SDL_GameControllerHasSensor(controller, SDL_SENSOR_ACCEL) == SDL_TRUE) SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_ACCEL, wanted); if (SDL_GameControllerHasSensor(controller, SDL_SENSOR_GYRO) == SDL_TRUE) SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_GYRO, wanted); } -void PadInput::closeController() { - if (controller == nullptr) return; - SDL_GameControllerClose(controller); - controller = nullptr; - controllerState = {}; +void PadInput::closeController(int slot) { + if (slot < 0 || slot >= PAD_MAX_CONTROLLERS) return; + if (slots[slot].controller == nullptr) return; + SDL_GameControllerClose(slots[slot].controller); + slots[slot].controller = nullptr; + slots[slot].instanceId = -1; + slots[slot].state = {}; + slots[slot].outputState = {}; + slots[slot].outputPending = false; + if (slot > 0) { + PadSetPortConnected_nid_postfix(slot, false); + UserServiceSetUserConnected_nid_postfix(slot, false); + } } void PadInput::applyOutput() { - PadOutputState fetched; - if (PadFetchOutput_nid_postfix(&outputSequence, &fetched)) { - const bool motionChanged = fetched.motionEnabled != outputState.motionEnabled; - outputState = fetched; - outputPending = true; - if (motionChanged) enableSensors(); - } - if (controller == nullptr) return; - const auto now = std::chrono::steady_clock::now(); - const bool isPs5 = SDL_GameControllerGetType(controller) == SDL_CONTROLLER_TYPE_PS5; - const auto large = std::max(outputState.vibrationLarge, outputState.audioVibrationLarge); - const auto small = std::max(outputState.vibrationSmall, outputState.audioVibrationSmall); - const bool rumbling = large != 0 || small != 0; - const bool triggerRumble = outputState.trigger[0].fallback != 0 || outputState.trigger[1].fallback != 0; - if (SDL_GameControllerHasLED(controller) == SDL_TRUE) { - const LightBarPacer::Color color = outputState.lightBarValid - ? LightBarPacer::Color{outputState.lightBar[0], outputState.lightBar[1], outputState.lightBar[2]} - : LightBarPacer::Color{0, 64, 255}; - if (lightBar.Due(color, now)) lightBar.Result(color, now, SDL_GameControllerSetLED(controller, color[0], color[1], color[2]) == 0); - } - if (!outputPending) { - if ((rumbling || (triggerRumble && !isPs5)) && now >= nextRumbleRefresh) outputPending = true; - else return; - } - outputPending = false; - nextRumbleRefresh = now + std::chrono::milliseconds(700); - constexpr Uint32 rumbleMs = 2000; - SDL_GameControllerRumble(controller, static_cast(large * 257), static_cast(small * 257), rumbling ? rumbleMs : 0); - if (outputState.triggerTouched) { - if (isPs5) { - TriggerEffectCache::Effect effect{}; - effect[0] = 0x04 | 0x08; - std::memcpy(effect.data() + 10, outputState.trigger[1].effect, 11); - std::memcpy(effect.data() + 21, outputState.trigger[0].effect, 11); - if (triggerEffect.NeedsSend(effect)) { - triggerEffect.Result(effect, SDL_GameControllerSendEffect(controller, effect.data(), static_cast(effect.size())) == 0); + for (int slot = 0; slot < PAD_MAX_CONTROLLERS; ++slot) { + PadOutputState fetched; + if (PadFetchOutputForPort_nid_postfix(slot, &slots[slot].outputSequence, &fetched)) { + const bool motionChanged = fetched.motionEnabled != slots[slot].outputState.motionEnabled; + slots[slot].outputState = fetched; + slots[slot].outputPending = true; + if (motionChanged) enableSensors(slot); + } + SDL_GameController* controller = slots[slot].controller; + if (controller == nullptr) continue; + const auto now = std::chrono::steady_clock::now(); + const bool isPs5 = SDL_GameControllerGetType(controller) == SDL_CONTROLLER_TYPE_PS5; + const auto large = std::max(slots[slot].outputState.vibrationLarge, slots[slot].outputState.audioVibrationLarge); + const auto small = std::max(slots[slot].outputState.vibrationSmall, slots[slot].outputState.audioVibrationSmall); + const bool rumbling = large != 0 || small != 0; + const bool triggerRumble = slots[slot].outputState.trigger[0].fallback != 0 || slots[slot].outputState.trigger[1].fallback != 0; + if (SDL_GameControllerHasLED(controller) == SDL_TRUE) { + struct PlayerLedColor { Uint8 r, g, b; }; + static constexpr PlayerLedColor defaultColors[4] = { + {0, 64, 255}, + {255, 0, 0}, + {0, 255, 0}, + {255, 0, 128} + }; + const auto& def = defaultColors[slot % 4]; + const LightBarPacer::Color color = slots[slot].outputState.lightBarValid + ? LightBarPacer::Color{slots[slot].outputState.lightBar[0], slots[slot].outputState.lightBar[1], slots[slot].outputState.lightBar[2]} + : LightBarPacer::Color{def.r, def.g, def.b}; + if (slots[slot].lightBar.Due(color, now)) slots[slot].lightBar.Result(color, now, SDL_GameControllerSetLED(controller, color[0], color[1], color[2]) == 0); + } + if (!slots[slot].outputPending) { + if ((rumbling || (triggerRumble && !isPs5)) && now >= slots[slot].nextRumbleRefresh) slots[slot].outputPending = true; + else continue; + } + slots[slot].outputPending = false; + slots[slot].nextRumbleRefresh = now + std::chrono::milliseconds(700); + constexpr Uint32 rumbleMs = 2000; + SDL_GameControllerRumble(controller, static_cast(large * 257), static_cast(small * 257), rumbling ? rumbleMs : 0); + if (slots[slot].outputState.triggerTouched) { + if (isPs5) { + TriggerEffectCache::Effect effect{}; + effect[0] = 0x04 | 0x08; + std::memcpy(effect.data() + 10, slots[slot].outputState.trigger[1].effect, 11); + std::memcpy(effect.data() + 21, slots[slot].outputState.trigger[0].effect, 11); + if (slots[slot].triggerEffect.NeedsSend(effect)) { + slots[slot].triggerEffect.Result(effect, SDL_GameControllerSendEffect(controller, effect.data(), static_cast(effect.size())) == 0); + } + } else if (SDL_GameControllerHasRumbleTriggers(controller) == SDL_TRUE) { + SDL_GameControllerRumbleTriggers(controller, static_cast(slots[slot].outputState.trigger[0].fallback * 257), static_cast(slots[slot].outputState.trigger[1].fallback * 257), triggerRumble ? rumbleMs : 0); } - } else if (SDL_GameControllerHasRumbleTriggers(controller) == SDL_TRUE) { - SDL_GameControllerRumbleTriggers(controller, static_cast(outputState.trigger[0].fallback * 257), static_cast(outputState.trigger[1].fallback * 257), triggerRumble ? rumbleMs : 0); } } } @@ -129,10 +172,12 @@ void PadInput::setMouseMode(bool enabled) { keyboardMouse.SetMouseMode(enabled); } -PadInputState PadInput::sampleController() const { +PadInputState PadInput::sampleController(int slot) const { PadInputState result; + if (slot < 0 || slot >= PAD_MAX_CONTROLLERS) return result; + SDL_GameController* controller = slots[slot].controller; if (controller == nullptr) return result; - const auto readButton = [this](SDL_GameControllerButton button) { + const auto readButton = [controller](SDL_GameControllerButton button) { return SDL_GameControllerGetButton(controller, button) != 0; }; const auto addButton = [&result, &readButton](SDL_GameControllerButton source, Pad::PadButton button) { @@ -156,7 +201,7 @@ PadInputState PadInput::sampleController() const { addButton(SDL_CONTROLLER_BUTTON_DPAD_LEFT, Pad::PadButton::Left); addButton(SDL_CONTROLLER_BUTTON_TOUCHPAD, Pad::PadButton::TouchPad); - const auto triggerValue = [this](SDL_GameControllerAxis axis) { + const auto triggerValue = [controller](SDL_GameControllerAxis axis) { const auto value = std::clamp(SDL_GameControllerGetAxis(controller, axis), 0, 32767); return static_cast((value * 255 + 16383) / 32767); }; @@ -165,7 +210,7 @@ PadInputState PadInput::sampleController() const { if (result.analogButtonsL2 != 0) result.buttons |= static_cast(Pad::PadButton::L2); if (result.analogButtonsR2 != 0) result.buttons |= static_cast(Pad::PadButton::R2); - const auto stick = [this](SDL_GameControllerAxis xAxis, SDL_GameControllerAxis yAxis, int deadzonePercent) { + const auto stick = [this, controller](SDL_GameControllerAxis xAxis, SDL_GameControllerAxis yAxis, int deadzonePercent) { return Pad::StickWithDeadzone(SDL_GameControllerGetAxis(controller, xAxis), SDL_GameControllerGetAxis(controller, yAxis), deadzonePercent); }; const auto left = stick(SDL_CONTROLLER_AXIS_LEFTX, SDL_CONTROLLER_AXIS_LEFTY, keyboardMouse.Configuration().settings.leftStickDeadzonePercent); @@ -207,23 +252,34 @@ void PadInput::HandleEvent(const SDL_Event& event, DisplayWindow& window) { openController(event.cdevice.which); return; } - if (event.type == SDL_CONTROLLERDEVICEREMOVED && controller != nullptr) { - const auto instanceId = SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(controller)); - if (instanceId == event.cdevice.which) { - closeController(); - openFirstAvailableController(); - publish(window.FrameNumber()); + if (event.type == SDL_CONTROLLERDEVICEREMOVED) { + for (int slot = 0; slot < PAD_MAX_CONTROLLERS; ++slot) { + if (slots[slot].controller != nullptr && slots[slot].instanceId == event.cdevice.which) { + closeController(slot); + publish(window.FrameNumber(), slot); + break; + } } return; } if (event.type == SDL_CONTROLLERBUTTONDOWN || event.type == SDL_CONTROLLERBUTTONUP || event.type == SDL_CONTROLLERAXISMOTION || event.type == SDL_CONTROLLERDEVICEREMAPPED) { - publish(window.FrameNumber()); + SDL_JoystickID which = -1; + if (event.type == SDL_CONTROLLERBUTTONDOWN || event.type == SDL_CONTROLLERBUTTONUP) which = event.cbutton.which; + else if (event.type == SDL_CONTROLLERAXISMOTION) which = event.caxis.which; + else if (event.type == SDL_CONTROLLERDEVICEREMAPPED) which = event.cdevice.which; + for (int slot = 0; slot < PAD_MAX_CONTROLLERS; ++slot) { + if (slots[slot].controller != nullptr && slots[slot].instanceId == which) { + slots[slot].state = sampleController(slot); + publish(window.FrameNumber(), slot); + return; + } + } return; } if (event.type == SDL_WINDOWEVENT) { if (keyboardMouse.HandleEvent(event, std::chrono::steady_clock::now()).releaseMouse) setMouseMode(false); - publish(window.FrameNumber()); + publish(window.FrameNumber(), 0); return; } // APS5_NO_PAD_INPUT=1 keeps a measurement run from reacting to keys or mouse buttons that reach @@ -239,25 +295,54 @@ void PadInput::HandleEvent(const SDL_Event& event, DisplayWindow& window) { if (requests.togglePerformanceOverlay && window.Handle() != nullptr && event.key.windowID == SDL_GetWindowID(window.Handle())) AgcDriverTogglePerformanceOverlay_nid_postfix(); if (requests.screenshot && window.Handle() != nullptr && event.key.windowID == SDL_GetWindowID(window.Handle())) AgcDriverRequestScreenshot_nid_postfix(); if (requests.toggleMouse) setMouseMode(!keyboardMouse.MouseMode()); - if (event.type != SDL_MOUSEMOTION) publish(window.FrameNumber()); + if (event.type != SDL_MOUSEMOTION) publish(window.FrameNumber(), 0); } void PadInput::Update(const DisplayWindow& window) { - if (controller != nullptr) SDL_GameControllerUpdate(); + bool anyController = false; + for (int slot = 0; slot < PAD_MAX_CONTROLLERS; ++slot) { + if (slots[slot].controller != nullptr) { + anyController = true; + break; + } + } + if (anyController) SDL_GameControllerUpdate(); applyOutput(); - if (controller != nullptr) controllerState = sampleController(); + for (int slot = 0; slot < PAD_MAX_CONTROLLERS; ++slot) { + if (slots[slot].controller != nullptr) { + slots[slot].state = sampleController(slot); + } + } if (keyboardMouse.MouseMode() && SDL_GetKeyboardFocus() == nullptr) { keyboardMouse.Release(); setMouseMode(false); - publish(window.FrameNumber()); + publish(window.FrameNumber(), 0); return; } const bool changed = keyboardMouse.Advance(std::chrono::steady_clock::now()); - if (controller != nullptr || keyboardMouse.MouseMode() || changed) publish(window.FrameNumber()); + if (slots[0].controller != nullptr || keyboardMouse.MouseMode() || changed) { + publish(window.FrameNumber(), 0); + } + for (int slot = 1; slot < PAD_MAX_CONTROLLERS; ++slot) { + if (slots[slot].controller != nullptr) { + publish(window.FrameNumber(), slot); + } + } } void PadInput::publish(std::uint64_t frame) { - const auto state = keyboardMouse.Compose(controllerState); - PadPublishInput_nid_postfix(state); - if (auto* telemetry = VideoOutTelemetry::Instance(); telemetry != nullptr) VideoOutTelemetry::EmitPadIfChanged(*telemetry, telemetryFilter, frame, state); + for (int slot = 0; slot < PAD_MAX_CONTROLLERS; ++slot) { + publish(frame, slot); + } +} + +void PadInput::publish(std::uint64_t frame, int slot) { + if (slot < 0 || slot >= PAD_MAX_CONTROLLERS) return; + if (slot == 0) { + const auto state = keyboardMouse.Compose(slots[0].state); + PadPublishInput_nid_postfix(state); + if (auto* telemetry = VideoOutTelemetry::Instance(); telemetry != nullptr) VideoOutTelemetry::EmitPadIfChanged(*telemetry, telemetryFilter, frame, state); + } else { + PadPublishInputForPort_nid_postfix(slot, slots[slot].state); + } } diff --git a/core/libs/prx/libSceVideoOut/tests/PadSdl.cpp b/core/libs/prx/libSceVideoOut/tests/PadSdl.cpp index cec6638fbd..3bdec619d1 100644 --- a/core/libs/prx/libSceVideoOut/tests/PadSdl.cpp +++ b/core/libs/prx/libSceVideoOut/tests/PadSdl.cpp @@ -1,13 +1,99 @@ #include "SDL_config.h" - +#include "SDL.h" +#include "SDL_joystick.h" +#include "SDL_gamecontroller.h" +#include "prx/libSceVideoOut/include/PadInput.hpp" +#include "prx/libSceVideoOut/include/DisplayWindow.hpp" +#include "prx/libScePad/include/PadState.hpp" #include +#include +#undef main + +namespace { + +static void Require(bool condition) { + if (!condition) { + std::abort(); + } +} + +} int main() { #ifdef SDL_JOYSTICK_HIDAPI std::puts("pad_sdl: SDL drives controllers through HIDAPI"); - return 0; #else std::fputs("pad_sdl: SDL is built without HIDAPI, so a DualSense gets no light bar, touchpad or trigger effects\n", stderr); return 1; #endif + + Require(SDL_Init(SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK) == 0); + + SDL_VirtualJoystickDesc desc{}; + desc.version = SDL_VIRTUAL_JOYSTICK_DESC_VERSION; + desc.type = SDL_JOYSTICK_TYPE_GAMECONTROLLER; + desc.naxes = 6; + desc.nbuttons = 16; + desc.name = "Virtual Controller 0"; + const int dev0 = SDL_JoystickAttachVirtualEx(&desc); + Require(dev0 >= 0); + Require(SDL_IsGameController(dev0)); + + DisplayWindow window; + PadInput padInput; + Require(padInput.SlotOccupied(0)); + Require(!padInput.SlotOccupied(1)); + + SDL_Event add0{}; + add0.type = SDL_CONTROLLERDEVICEADDED; + add0.cdevice.which = dev0; + padInput.HandleEvent(add0, window); + Require(padInput.SlotOccupied(0)); + Require(!padInput.SlotOccupied(1)); + + desc.name = "Virtual Controller 1"; + const int dev1 = SDL_JoystickAttachVirtualEx(&desc); + Require(dev1 >= 0); + Require(SDL_IsGameController(dev1)); + + SDL_Event add1{}; + add1.type = SDL_CONTROLLERDEVICEADDED; + add1.cdevice.which = dev1; + padInput.HandleEvent(add1, window); + Require(padInput.SlotOccupied(0)); + Require(padInput.SlotOccupied(1)); + Require(!padInput.SlotOccupied(2)); + Require(Pad::IsConnected(1)); + + const SDL_JoystickID id0 = SDL_JoystickGetDeviceInstanceID(dev0); + SDL_Event rem0{}; + rem0.type = SDL_CONTROLLERDEVICEREMOVED; + rem0.cdevice.which = id0; + padInput.HandleEvent(rem0, window); + Require(!padInput.SlotOccupied(0)); + Require(padInput.SlotOccupied(1)); + Require(Pad::IsConnected(1)); + + padInput.HandleEvent(add0, window); + Require(padInput.SlotOccupied(0)); + Require(padInput.SlotOccupied(1)); + + const SDL_JoystickID id1 = SDL_JoystickGetDeviceInstanceID(dev1); + SDL_Event rem1{}; + rem1.type = SDL_CONTROLLERDEVICEREMOVED; + rem1.cdevice.which = id1; + padInput.HandleEvent(rem1, window); + Require(padInput.SlotOccupied(0)); + Require(!padInput.SlotOccupied(1)); + Require(!Pad::IsConnected(1)); + + padInput.HandleEvent(rem0, window); + Require(!padInput.SlotOccupied(0)); + Require(!padInput.SlotOccupied(1)); + + Require(SDL_JoystickDetachVirtual(dev1) == 0); + Require(SDL_JoystickDetachVirtual(dev0) == 0); + SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK); + + return 0; } diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 3fa9a2ff30..02a5c831fc 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -410,6 +410,15 @@ if(BUILD_TESTING) set_tests_properties(guest_libc-random-seeding PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_random_device_seed_tests ${LIBS_INCLUDE_DIR}/tests/GuestRandomDeviceSeed.cpp src/Random.cpp) + target_include_directories(guest_random_device_seed_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_random_device_seed_tests PRIVATE libc) + configure_windows_unwind(guest_random_device_seed_tests) + add_test(NAME guest_random_device_seed COMMAND guest_random_device_seed_tests) + if(WIN32) + set_tests_properties(guest_random_device_seed PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_integer_builtins_tests ${LIBS_INCLUDE_DIR}/tests/GuestIntegerBuiltins.cpp) target_include_directories(guest_integer_builtins_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_integer_builtins_tests PRIVATE libc) diff --git a/core/libs/prx/libc/include/General.hpp b/core/libs/prx/libc/include/General.hpp index 1a4febd54a..b45386ab48 100644 --- a/core/libs/prx/libc/include/General.hpp +++ b/core/libs/prx/libc/include/General.hpp @@ -90,6 +90,9 @@ struct PthreadSyncForwards_nid_no_patch { }; extern "C" void RegisterPthreadSyncForwards_nid_no_patch(const PthreadSyncForwards_nid_no_patch* forwards); +using SigactionFn_nid_no_patch = int (APS5_VABI*)(int, const void*, void*); +extern "C" void RegisterSigaction_nid_no_patch(SigactionFn_nid_no_patch fn, std::uint32_t* interruptSet); + #define APS5_INVALID_ARG_EX throw std::invalid_argument(std::string(__func__) + ": invalid argument") #define APS5_DUMMY_FUN \ diff --git a/core/libs/prx/libc/include/GuestLocale.hpp b/core/libs/prx/libc/include/GuestLocale.hpp index 45e29df3f1..92ca9f13b8 100644 --- a/core/libs/prx/libc/include/GuestLocale.hpp +++ b/core/libs/prx/libc/include/GuestLocale.hpp @@ -58,6 +58,40 @@ struct CollateVtable { std::int64_t (APS5_VABI *hash)(const CollateFacet* self, const char* first, const char* last); }; +struct CodecvtVtable { + FacetVtable facet; + bool (APS5_VABI *alwaysNoconv)(const Facet* self); + int (APS5_VABI *maxLength)(const Facet* self); + int (APS5_VABI *encoding)(const Facet* self); + int (APS5_VABI *in)(Facet* self, std::uint64_t* state, const char* first1, const char* last1, const char** next1, char* first3, char* last3, char** next3); + int (APS5_VABI *out)(Facet* self, std::uint64_t* state, const char* first1, const char* last1, const char** next1, char* first3, char* last3, char** next3); + int (APS5_VABI *unshift)(Facet* self, std::uint64_t* state, char* first3, char* last3, char** next3); + std::size_t (APS5_VABI *length)(Facet* self, std::uint64_t* state, const char* first1, const char* last1, std::size_t count); +}; + +struct NumpunctFacet { + Facet base; + const char* grouping; + char decimalPoint; + char thousandsSep; + const char* falsename; + const char* truename; +}; + +struct NumpunctVtable { + FacetVtable facet; + char (APS5_VABI *decimalPoint)(const NumpunctFacet* self); + char (APS5_VABI *thousandsSep)(const NumpunctFacet* self); + String* (APS5_VABI *grouping)(String* result, const NumpunctFacet* self); + String* (APS5_VABI *falsename)(String* result, const NumpunctFacet* self); + String* (APS5_VABI *truename)(String* result, const NumpunctFacet* self); +}; + +struct UnsupportedMethodsVtable { + FacetVtable facet; + void (APS5_VABI *methods[8])(); +}; + struct IosBase { const void* vtable; std::int64_t standardStream; @@ -89,6 +123,16 @@ static_assert(offsetof(CollateFacet, collation) == 0x10); static_assert(sizeof(CollateFacet) == 0x20); static_assert(offsetof(CollateVtable, compare) == 0x20); static_assert(offsetof(CollateVtable, hash) == 0x30); +static_assert(offsetof(CodecvtVtable, alwaysNoconv) == 0x20); +static_assert(offsetof(CodecvtVtable, in) == 0x38); +static_assert(offsetof(CodecvtVtable, length) == 0x50); +static_assert(sizeof(NumpunctFacet) == 0x30); +static_assert(offsetof(NumpunctFacet, grouping) == 0x10); +static_assert(offsetof(NumpunctFacet, decimalPoint) == 0x18); +static_assert(offsetof(NumpunctFacet, falsename) == 0x20); +static_assert(offsetof(NumpunctFacet, truename) == 0x28); +static_assert(offsetof(NumpunctVtable, decimalPoint) == 0x20); +static_assert(offsetof(NumpunctVtable, truename) == 0x40); static_assert(alignof(LocinfoStorage) == 8); } diff --git a/core/libs/prx/libc/include/WindowsFormatting.hpp b/core/libs/prx/libc/include/WindowsFormatting.hpp index 0c8dd2861f..a00cc1eb81 100644 --- a/core/libs/prx/libc/include/WindowsFormatting.hpp +++ b/core/libs/prx/libc/include/WindowsFormatting.hpp @@ -279,8 +279,11 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo } if (const size_t position = ArgumentPosition(format)) args.Seek(position); std::string spec = "%"; - for (; *format && std::strchr("-+ #0'", *format); ++format) + bool unsupportedUnknownFlag = false; + for (; *format && std::strchr("-+ #0'", *format); ++format) { if (*format != '\'') spec += *format; + if (*format != '-') unsupportedUnknownFlag = true; + } if (*format == '*') { ++format; const int width = star(); @@ -316,7 +319,7 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo if ((length == "h" && *format == 'h') || (length == "l" && *format == 'l')) length += *format++; } - const char conversion = *format; + char conversion = *format; if (!conversion) throw std::invalid_argument("Incomplete format conversion"); ++format; const bool integerLength = length.empty() || length == "h" || length == "hh" || @@ -404,6 +407,11 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo else if (length == "h") *static_cast(pointer) = static_cast(count); else if (length.empty()) *static_cast(pointer) = count; else *static_cast(pointer) = count; + } else if (!std::strchr("diouxXaAeEfFgGcsSpnmCS", conversion)) { + if (secure || unsupportedUnknownFlag || spec.find_first_not_of("%-0123456789") != std::string::npos) + throw std::invalid_argument("Unsupported format conversion '" + spec + length + conversion + "' in \"" + std::string(formatStart).substr(0, 160) + "\""); + const std::string text = conversion == '%' ? "%" : std::string("%") + conversion; + output.Value(spec + 's', text.c_str()); } else { throw std::invalid_argument("Unsupported format conversion '" + spec + length + conversion + "' in \"" + std::string(formatStart).substr(0, 160) + "\""); } diff --git a/core/libs/prx/libc/src/DinkumwareInternals.cpp b/core/libs/prx/libc/src/DinkumwareInternals.cpp index 163a7362fa..ab80ee57d9 100644 --- a/core/libs/prx/libc/src/DinkumwareInternals.cpp +++ b/core/libs/prx/libc/src/DinkumwareInternals.cpp @@ -10,7 +10,10 @@ extern "C" { int* APS5_VABI __error_nid_postfix(); double APS5_VABI strtod_nid_postfix(const char* str, char** endptr); +float APS5_VABI strtof_nid_postfix(const char* str, char** endptr); +long double APS5_VABI strtold_nid_postfix(const char* str, char** endptr); long long APS5_VABI strtoll_nid_postfix(const char* str, char** endptr, int base); +unsigned long long APS5_VABI strtoull_nid_postfix(const char* str, char** endptr, int base); double APS5_VABI sin_nid_postfix(double x); double APS5_VABI cos_nid_postfix(double x); float APS5_VABI sinf_nid_postfix(float x); @@ -28,6 +31,7 @@ float APS5_VABI log2f_nid_postfix(float x); namespace { constexpr long long ExponentLimit = 1000000000; +constexpr int InvalidArgument = 22; constexpr int RangeError = 34; bool Hexadecimal(const char* text) { @@ -36,39 +40,115 @@ bool Hexadecimal(const char* text) { return text[0] == '0' && (text[1] == 'x' || text[1] == 'X'); } +template +TFloat ScaledFloat(const char* text, char** end, std::int64_t pten, int* error, TFloat (APS5_VABI *parse)(const char*, char**), const char* function) { + int& guestErrno = *__error_nid_postfix(); + const int saved = guestErrno; + if (error != nullptr) *error = 0; + guestErrno = 0; + char* stop = nullptr; + TFloat value = parse(text, &stop); + if (end != nullptr) *end = stop; + const bool overflowed = std::isinf(value) && guestErrno == RangeError; + if (pten != 0 && stop != text && (std::isfinite(value) || overflowed)) { + if (Hexadecimal(text)) NotImplemented_nid_no_patch(function); + std::string number(text, static_cast(stop)); + long long exponent = 0; + const auto marker = number.find_first_of("eE"); + if (marker != std::string::npos) { + exponent = std::strtoll(number.c_str() + marker + 1, nullptr, 10); + number.resize(marker); + } + exponent = std::clamp(exponent, -ExponentLimit, ExponentLimit) + std::clamp(pten, -ExponentLimit, ExponentLimit); + number += 'e'; + number += std::to_string(exponent); + guestErrno = 0; + value = parse(number.c_str(), nullptr); + } + if (guestErrno == RangeError && (value == 0 || std::isinf(value))) { + if (error != nullptr) *error = 1; + } else if (guestErrno == 0 || guestErrno == RangeError) { + guestErrno = saved; + } + return value; +} + +bool InvalidBase(const char* text, char** end, int base, int* error) { + if (base >= 0 && base != 1 && base <= 36) return false; + if (end != nullptr) *end = const_cast(text); + if (error != nullptr) *error = InvalidArgument; + return true; +} + +template +TInteger ParsedInteger(const char* text, char** end, int base, int* error, TInteger (APS5_VABI *parse)(const char*, char**, int)) { + if (error != nullptr) *error = 0; + if (InvalidBase(text, end, base, error)) return 0; + int& guestErrno = *__error_nid_postfix(); + const int saved = guestErrno; + guestErrno = 0; + const TInteger value = parse(text, end, base); + if (error != nullptr) *error = guestErrno; + if (guestErrno == 0) guestErrno = saved; + return value; +} + } extern "C" { +double APS5_VABI _Stodx_nid_postfix(const char* text, char** end, std::int64_t pten, int* error) { + return ScaledFloat(text, end, pten, error, strtod_nid_postfix, "_Stodx (hexadecimal text with a power-of-ten scale)"); +} + +float APS5_VABI _Stofx_nid_postfix(const char* text, char** end, std::int64_t pten, int* error) { + return ScaledFloat(text, end, pten, error, strtof_nid_postfix, "_Stofx (hexadecimal text with a power-of-ten scale)"); +} + +long double APS5_VABI _Stoldx_nid_postfix(const char* text, char** end, std::int64_t pten, int* error) { + return ScaledFloat(text, end, pten, error, strtold_nid_postfix, "_Stoldx (hexadecimal text with a power-of-ten scale)"); +} + double APS5_VABI _Stod_nid_postfix(const char* text, char** end, std::int64_t pten) { + return _Stodx_nid_postfix(text, end, pten, nullptr); +} + +float APS5_VABI _Stof_nid_postfix(const char* text, char** end, std::int64_t pten) { + return _Stofx_nid_postfix(text, end, pten, nullptr); +} + +long double APS5_VABI _Stold_nid_postfix(const char* text, char** end, std::int64_t pten) { + return _Stoldx_nid_postfix(text, end, pten, nullptr); +} + +long long APS5_VABI _Stollx_nid_postfix(const char* text, char** end, int base, int* error) { + return ParsedInteger(text, end, base, error, strtoll_nid_postfix); +} + +unsigned long long APS5_VABI _Stoullx_nid_postfix(const char* text, char** end, int base, int* error) { + return ParsedInteger(text, end, base, error, strtoull_nid_postfix); +} + +std::uint64_t APS5_VABI _Stoulx_nid_postfix(const char* text, char** end, int base, int* error) { + return _Stoullx_nid_postfix(text, end, base, error); +} + +std::int64_t APS5_VABI _Stolx_nid_postfix(const char* text, char** end, int base, int* error) { + if (error != nullptr) *error = 0; + if (InvalidBase(text, end, base, error)) return 0; int& guestErrno = *__error_nid_postfix(); const int saved = guestErrno; guestErrno = 0; - char* stop = nullptr; - const double value = strtod_nid_postfix(text, &stop); - if (end != nullptr) *end = stop; - const bool overflowed = std::isinf(value) && guestErrno == RangeError; - if (pten == 0 || stop == text || !(std::isfinite(value) || overflowed)) { - if (guestErrno == 0) guestErrno = saved; - return value; - } - if (Hexadecimal(text)) NotImplemented_nid_no_patch("_Stod (hexadecimal text with a power-of-ten scale)"); - std::string number(text, static_cast(stop)); - long long exponent = 0; - const auto marker = number.find_first_of("eE"); - if (marker != std::string::npos) { - exponent = std::strtoll(number.c_str() + marker + 1, nullptr, 10); - number.resize(marker); - } - exponent = std::clamp(exponent, -ExponentLimit, ExponentLimit) + std::clamp(pten, -ExponentLimit, ExponentLimit); - number += 'e'; - number += std::to_string(exponent); - guestErrno = saved; - return strtod_nid_postfix(number.c_str(), nullptr); + const long long value = strtoll_nid_postfix(text, end, base); + if (error != nullptr) *error = guestErrno; + guestErrno = 0; + static_cast(strtoull_nid_postfix(text, nullptr, base)); + if (guestErrno == 0) guestErrno = saved; + return value; } long long APS5_VABI _Stoll_nid_postfix(const char* text, char** end, int base) { - return strtoll_nid_postfix(text, end, base); + return _Stollx_nid_postfix(text, end, base, nullptr); } double APS5_VABI _Sin_nid_postfix(double x, unsigned int quadrant) { diff --git a/core/libs/prx/libc/src/LocaleSupport.cpp b/core/libs/prx/libc/src/LocaleSupport.cpp index b210b93fd9..fe8d116507 100644 --- a/core/libs/prx/libc/src/LocaleSupport.cpp +++ b/core/libs/prx/libc/src/LocaleSupport.cpp @@ -220,6 +220,83 @@ std::int64_t APS5_VABI WideCollateHash(const GuestLocale::CollateFacet*, const c const GuestLocale::CollateVtable g_wideCollateVtable{{DestroyCollate, DeleteCollate, RetainFacet, ReleaseFacet}, WideCollateCompare, WideCollateTransform, WideCollateHash}; +bool APS5_VABI CodecvtAlwaysNoconv(const GuestLocale::Facet*) { + return true; +} + +int APS5_VABI CodecvtConstantOne(const GuestLocale::Facet*) { + return 1; +} + +int APS5_VABI CodecvtNoConversion(GuestLocale::Facet*, std::uint64_t*, const char* first1, const char*, const char** next1, char* first3, char*, char** next3) { + *next1 = first1; + *next3 = first3; + return 3; +} + +int APS5_VABI CodecvtUnshift(GuestLocale::Facet*, std::uint64_t*, char* first3, char*, char** next3) { + *next3 = first3; + return 0; +} + +std::size_t APS5_VABI CodecvtLength(GuestLocale::Facet*, std::uint64_t*, const char* first1, const char* last1, std::size_t count) { + return std::min(static_cast(last1 - first1), count); +} + +const GuestLocale::CodecvtVtable g_codecvtVtable{{DestroyCollate, DeleteCollate, RetainFacet, ReleaseFacet}, CodecvtAlwaysNoconv, CodecvtConstantOne, CodecvtConstantOne, CodecvtNoConversion, CodecvtNoConversion, CodecvtUnshift, CodecvtLength}; + +GuestLocale::String* APS5_VABI AssignCString(GuestLocale::String* result, const char* source) { + if (result == nullptr || source == nullptr) throw std::invalid_argument("numpunct string: null argument"); + const std::size_t length = std::strlen(source); + char* data = result->buffer; + std::size_t capacity = GuestStringInlineCapacity; + if (length > GuestStringInlineCapacity) { + capacity = length | GuestStringCapacityMask; + data = static_cast(AllocateStringStorage(capacity + 1)); + } + if (length != 0) std::memcpy(data, source, length); + data[length] = 0; + if (data != result->buffer) result->pointer = data; + result->size = length; + result->capacity = capacity; + return result; +} + +char APS5_VABI NumpunctDecimalPoint(const GuestLocale::NumpunctFacet* self) { + return self->decimalPoint; +} + +char APS5_VABI NumpunctThousandsSep(const GuestLocale::NumpunctFacet* self) { + return self->thousandsSep; +} + +GuestLocale::String* APS5_VABI NumpunctGrouping(GuestLocale::String* result, const GuestLocale::NumpunctFacet* self) { + return AssignCString(result, self->grouping); +} + +GuestLocale::String* APS5_VABI NumpunctFalsename(GuestLocale::String* result, const GuestLocale::NumpunctFacet* self) { + return AssignCString(result, self->falsename); +} + +GuestLocale::String* APS5_VABI NumpunctTruename(GuestLocale::String* result, const GuestLocale::NumpunctFacet* self) { + return AssignCString(result, self->truename); +} + +const GuestLocale::NumpunctVtable g_numpunctVtable{{DestroyCollate, DeleteCollate, RetainFacet, ReleaseFacet}, NumpunctDecimalPoint, NumpunctThousandsSep, NumpunctGrouping, NumpunctFalsename, NumpunctTruename}; + +void APS5_VABI NumPutMethodUnsupported() { + NotImplemented_nid_no_patch("num_put::do_put"); +} + +void APS5_VABI TimePutMethodUnsupported() { + NotImplemented_nid_no_patch("time_put::do_put"); +} + +const GuestLocale::UnsupportedMethodsVtable g_numPutVtable{{DestroyCollate, DeleteCollate, RetainFacet, ReleaseFacet}, {NumPutMethodUnsupported, NumPutMethodUnsupported, NumPutMethodUnsupported, NumPutMethodUnsupported, NumPutMethodUnsupported, NumPutMethodUnsupported, NumPutMethodUnsupported, NumPutMethodUnsupported}}; +const GuestLocale::UnsupportedMethodsVtable g_timePutVtable{{DestroyCollate, DeleteCollate, RetainFacet, ReleaseFacet}, {TimePutMethodUnsupported, TimePutMethodUnsupported, TimePutMethodUnsupported, TimePutMethodUnsupported, TimePutMethodUnsupported, TimePutMethodUnsupported, TimePutMethodUnsupported, TimePutMethodUnsupported}}; + +constexpr std::size_t GuestTimePutFacetSize = 0xb8; + constexpr std::array MakeClassificationTable() { std::array table{}; for (unsigned int value = 0; value < 256; ++value) { @@ -306,6 +383,8 @@ std::uintptr_t _ZTVSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE_nid std::uint64_t _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 0; std::uint64_t _ZNSt7num_putIwSt19ostreambuf_iteratorIwSt11char_traitsIwEEE2idE_nid_postfix = 0; std::uintptr_t _ZTVSt7num_putIwSt19ostreambuf_iteratorIwSt11char_traitsIwEEE_nid_postfix[14] {}; +std::uint64_t _ZNSt8numpunctIcE2idE_nid_postfix = 0; +std::uint64_t _ZNSt8time_putIcSt19ostreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 0; std::streamoff _ZSt7_BADOFF_nid_postfix = -1; std::fpos_t _ZSt4_Fpz_nid_postfix {}; @@ -405,6 +484,43 @@ std::size_t APS5_VABI _ZNSt7collateIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_pos return 1; } +std::size_t APS5_VABI _ZNSt7codecvtIcc9_MbstatetE7_GetcatEPPKNSt6locale5facetEPKS2__nid_postfix(GuestLocale::Facet** facet, const GuestLocale::Implementation* const*) { + if (facet != nullptr && *facet == nullptr) { + auto* object = static_cast(_Znwm_nid_postfix(sizeof(GuestLocale::Facet))); + *object = {&g_codecvtVtable.facet, 0, 0}; + *facet = object; + } + return 2; +} + +std::size_t APS5_VABI _ZNSt8numpunctIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(GuestLocale::Facet** facet, const GuestLocale::Implementation* const*) { + if (facet != nullptr && *facet == nullptr) { + auto* punct = static_cast(_Znwm_nid_postfix(sizeof(GuestLocale::NumpunctFacet))); + *punct = {{&g_numpunctVtable.facet, 0, 0}, "", '.', ',', "false", "true"}; + *facet = &punct->base; + } + return 4; +} + +std::size_t APS5_VABI _ZNSt7num_putIcSt19ostreambuf_iteratorIcSt11char_traitsIcEEE7_GetcatEPPKNSt6locale5facetEPKS5__nid_postfix(GuestLocale::Facet** facet, const GuestLocale::Implementation* const*) { + if (facet != nullptr && *facet == nullptr) { + auto* object = static_cast(_Znwm_nid_postfix(sizeof(GuestLocale::Facet))); + *object = {&g_numPutVtable.facet, 0, 0}; + *facet = object; + } + return 4; +} + +std::size_t APS5_VABI _ZNSt8time_putIcSt19ostreambuf_iteratorIcSt11char_traitsIcEEE7_GetcatEPPKNSt6locale5facetEPKS5__nid_postfix(GuestLocale::Facet** facet, const GuestLocale::Implementation* const*) { + if (facet != nullptr && *facet == nullptr) { + auto* object = static_cast(_Znwm_nid_postfix(GuestTimePutFacetSize)); + std::memset(object, 0, GuestTimePutFacetSize); + object->vtable = &g_timePutVtable.facet; + *facet = object; + } + return 5; +} + void APS5_VABI _ZNSt8_LocinfoC1EPKc_nid_postfix(GuestLocale::LocinfoStorage* self, const char* localeName) { if (self == nullptr || localeName == nullptr || std::strcmp(localeName, "C") != 0) throw std::invalid_argument("_Locinfo: only the C locale is supported"); new (self) GuestLocale::LocinfoStorage{}; @@ -472,9 +588,11 @@ std::uint32_t APS5_VABI _Towctrans_nid_postfix(std::uint32_t c, std::uint64_t de std::uint64_t _ZNSt7codecvtIwc9_MbstatetE2idE_nid_postfix = 0; alignas(16) unsigned char _ZSt4cout_nid_postfix[0x400] {}; alignas(16) unsigned char _ZSt4cerr_nid_postfix[0x400] {}; +alignas(16) unsigned char _ZSt4clog_nid_postfix[0x400] {}; alignas(16) unsigned char _ZSt3cin_nid_postfix[0x400] {}; alignas(16) unsigned char _ZSt5wcout_nid_postfix[0x400] {}; alignas(16) unsigned char _ZSt5wcerr_nid_postfix[0x400] {}; +alignas(16) unsigned char _ZSt5wclog_nid_postfix[0x400] {}; alignas(16) unsigned char _ZSt4wcin_nid_postfix[0x400] {}; // Facet vectors are read by inlined guest code, so they come from the guest application heap. diff --git a/core/libs/prx/libc/src/Process.cpp b/core/libs/prx/libc/src/Process.cpp index 9d2f6909b0..c200af881d 100644 --- a/core/libs/prx/libc/src/Process.cpp +++ b/core/libs/prx/libc/src/Process.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -281,3 +282,42 @@ int APS5_VABI daemon_nid_postfix(int noChdir, int noClose) { } } + +namespace { +constexpr int SaRestart = 0x2; +std::atomic registeredSigaction{nullptr}; +std::atomic registeredInterruptSet{nullptr}; + +struct InterruptSigaction { + std::uintptr_t handler; + int flags; + std::uint32_t mask[4]; +}; +static_assert(sizeof(InterruptSigaction) == 32 && offsetof(InterruptSigaction, flags) == 8 && offsetof(InterruptSigaction, mask) == 12); +} + +extern "C" { + +void RegisterSigaction_nid_no_patch(SigactionFn_nid_no_patch fn, std::uint32_t* interruptSet) { + registeredInterruptSet.store(interruptSet, std::memory_order_release); + registeredSigaction.store(fn, std::memory_order_release); +} + +int APS5_VABI siginterrupt_nid_postfix(int guest, int flag) { + const auto sigaction = registeredSigaction.load(std::memory_order_acquire); + auto* const interruptSet = registeredInterruptSet.load(std::memory_order_acquire); + if (sigaction == nullptr || interruptSet == nullptr) throw std::runtime_error("siginterrupt: libkernel has not registered sigaction"); + InterruptSigaction action{}; + if (sigaction(guest, nullptr, &action) != 0) return -1; + const auto index = static_cast(guest - 1); + if (flag) { + interruptSet[index >> 5] |= 1u << (index & 31); + action.flags &= ~SaRestart; + } else { + interruptSet[index >> 5] &= ~(1u << (index & 31)); + action.flags |= SaRestart; + } + return sigaction(guest, &action, nullptr); +} + +} diff --git a/core/libs/prx/libc/src/Random.cpp b/core/libs/prx/libc/src/Random.cpp index 5489b4de99..5fee1280e8 100644 --- a/core/libs/prx/libc/src/Random.cpp +++ b/core/libs/prx/libc/src/Random.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/HostEntropy.hpp" #include #include #include @@ -181,6 +182,16 @@ void APS5_VABI srandom_nid_postfix(std::uint64_t seed) { SeedLocked(static_cast(seed)); } +void APS5_VABI srandomdev_nid_postfix() { + std::lock_guard lock(g_randomLock); + const auto words = g_random.type == LinearCongruentialType ? 1 : g_random.degree; + HostEntropy_nid_no_patch(g_random.table + sizeof(std::uint32_t), static_cast(words) * sizeof(std::uint32_t)); + if (g_random.type != LinearCongruentialType) { + g_random.front = g_random.separation; + g_random.rear = 0; + } +} + char* APS5_VABI initstate_nid_postfix(std::uint64_t seed, char* state, std::int64_t size) { if (size < MinimumStateBytes[0]) return nullptr; if (state == nullptr) throw std::invalid_argument("initstate: null state"); diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 23ad18c9d4..31b5d0fd1d 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1210,6 +1210,16 @@ if(BUILD_TESTING) set_tests_properties(guest_sched_priority PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_rtprio_thread_tests ${LIBS_INCLUDE_DIR}/tests/GuestRtprioThread.cpp) + target_include_directories(guest_rtprio_thread_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_rtprio_thread_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_rtprio_thread_tests) + add_test(NAME guest_rtprio_thread COMMAND guest_rtprio_thread_tests) + set_tests_properties(guest_rtprio_thread PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_rtprio_thread PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_operation_mode_tests ${LIBS_INCLUDE_DIR}/tests/GuestOperationMode.cpp) target_include_directories(guest_operation_mode_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_operation_mode_tests PRIVATE libkernel libc) @@ -1336,6 +1346,19 @@ if(BUILD_TESTING) set_tests_properties(guest_named_shm_concurrency PROPERTIES TIMEOUT 20) endif() if(WIN32) + add_test_executable(guest_named_shm_windows_tests ${LIBS_INCLUDE_DIR}/tests/GuestNamedShmWindows.cpp) + target_include_directories(guest_named_shm_windows_tests PRIVATE ${LIBS_INCLUDE_DIR} ${LIBS_INCLUDE_DIR}/nid/include) + target_link_libraries(guest_named_shm_windows_tests PRIVATE libkernel libc advapi32) + configure_windows_unwind(guest_named_shm_windows_tests) + add_test(NAME guest_named_shm_windows COMMAND guest_named_shm_windows_tests) + set_tests_properties(guest_named_shm_windows PROPERTIES TIMEOUT 20 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + add_test(NAME guest_named_shm_windows_short_temp COMMAND guest_named_shm_windows_tests --short-temp) + set_tests_properties(guest_named_shm_windows_short_temp PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + add_test(NAME guest_named_shm_windows_long_short_temp COMMAND guest_named_shm_windows_tests --long-short-temp) + set_tests_properties(guest_named_shm_windows_long_short_temp PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") set_tests_properties(guest_shm_open PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() diff --git a/core/libs/prx/libkernel/Equeue/Equeue.cpp b/core/libs/prx/libkernel/Equeue/Equeue.cpp index bbef6153e4..a0d5532a03 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.cpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.cpp @@ -12,6 +12,64 @@ #include #include +extern "C" int APS5_VABI sceKernelFstat(int d, FileStat* sb); + +static constexpr std::uint64_t FileRepollNs = 1000000; + +struct UnreadFile { + bool measured = false; + std::int64_t bytes = 0; +}; + +static UnreadFile UnreadFileBytes(int file) { + FileStat status{}; + if (sceKernelFstat(file, &status) != 0) { + return {}; + } + const std::int64_t offset = sceKernelLseek(file, 0, 1); + if (offset < 0) { + return {}; + } + return {true, status.st_size - offset}; +} + +static bool RegularFile(int descriptor) { + if (File::IsHostStream(descriptor)) { + return false; + } + FileStat status{}; + return sceKernelFstat(descriptor, &status) == 0 && (status.st_mode & 0170000) == 0100000; +} + +static KernelEqueueEvent FileEvent(int descriptor, int16_t filter, uint16_t flags, void* udata) { + KernelEqueueEvent event{}; + event.event.ident = static_cast(descriptor); + event.event.filter = filter; + event.event.flags = flags; + event.event.udata = udata; + event.filter.repoll = true; + if (filter == EVFILT_WRITE) { + event.filter.pollFunc = [](KernelEqueueEvent* e) { + e->event.data = 0; + return true; + }; + } else { + event.filter.owner = std::make_shared(); + event.filter.pollFunc = [](KernelEqueueEvent* e) { + const auto* unread = static_cast(e->filter.owner.get()); + if (!unread->measured || unread->bytes == 0) { + return false; + } + e->event.data = static_cast(unread->bytes); + return true; + }; + } + event.filter.resetFunc = [](KernelEqueueEvent* e) { + e->triggered = false; + }; + return event; +} + static std::unordered_map g_equeues; static std::mutex g_equeueMutex; static uint64_t g_nextEqueue = 1; @@ -21,6 +79,7 @@ struct KernelEqueuePrivate::WaitSet { std::vector> held; std::vector> queues; uint64_t nearestTimerNs = std::numeric_limits::max(); + bool repoll = false; }; extern "C" { @@ -98,6 +157,28 @@ bool KernelEqueuePrivate::PollEvents() { return polled; } +void KernelEqueuePrivate::MeasureFiles(std::unique_lock& lock) { + std::vector>> files; + for (const auto& e : m_events) { + if (e.enabled && e.filter.repoll && e.event.filter == EVFILT_READ) { + files.emplace_back(static_cast(e.event.ident), e.filter.owner); + } + } + if (files.empty()) { + return; + } + lock.unlock(); + std::vector measured; + measured.reserve(files.size()); + for (const auto& [file, owner] : files) { + measured.push_back(UnreadFileBytes(file)); + } + lock.lock(); + for (std::size_t i = 0; i < files.size(); ++i) { + *static_cast(files[i].second.get()) = measured[i]; + } +} + void KernelEqueuePrivate::WakePollers() { for (const auto waker : m_pollers) { GuestSockets::Wake(waker); @@ -117,6 +198,8 @@ void KernelEqueuePrivate::GatherInterests(WaitSet& wait, std::uintptr_t waker, b } if (e.filter.watched) { GatherWatched(wait, e.filter.watched, waker); + } else if (e.filter.repoll) { + wait.repoll = true; } else if (e.filter.waitDescriptor >= 0) { wait.interests.push_back({e.filter.waitDescriptor, false}); wait.held.push_back(e.filter.owner); @@ -151,6 +234,12 @@ void KernelEqueuePrivate::WaitForDescriptors(std::unique_lock& lock, const auto waker = GuestSockets::CurrentWaker(); WaitSet wait; GatherInterests(wait, waker, false); + if (wait.repoll) { + const std::uint64_t soon = TimedWait::NowNanos() + FileRepollNs; + if (deadlineNanos == 0 || deadlineNanos > soon) { + deadlineNanos = soon; + } + } m_pollers.push_back(waker); if (!wait.queues.empty()) { PollEvents(); @@ -279,6 +368,10 @@ int KernelEqueuePrivate::GetTriggeredEvents(KernelEvent* ev, int num) { if (m_closed) { return SCE_KERNEL_ERROR_EBADF; } + MeasureFiles(lock); + if (m_closed) { + return SCE_KERNEL_ERROR_EBADF; + } TriggerExpiredTimers(MonotonicNs()); PollEvents(); return CollectTriggered(ev, num); @@ -293,6 +386,10 @@ std::size_t KernelEqueuePrivate::CountTriggeredEvents() { if (m_closed) { return 0; } + MeasureFiles(lock); + if (m_closed) { + return 0; + } TriggerExpiredTimers(MonotonicNs()); PollEvents(); return static_cast(std::count_if(m_events.begin(), m_events.end(), [](const KernelEqueueEvent& event) { return event.enabled && event.triggered; })); @@ -319,6 +416,10 @@ int KernelEqueuePrivate::WaitForEventsUntil(KernelEvent* ev, int num, std::uint6 return SCE_KERNEL_ERROR_EBADF; } for (;;) { + MeasureFiles(lock); + if (m_closed) { + return SCE_KERNEL_ERROR_EBADF; + } TriggerExpiredTimers(MonotonicNs()); const bool polling = PollEvents(); const int ret = CollectTriggered(ev, num); @@ -720,15 +821,19 @@ static int AddDescriptorEvent(KernelEqueue eq, int fd, std::size_t size, void* u std::int64_t available = 0; bool eof = false; const bool isPipe = !isSocket && filter == EVFILT_READ && !File::IsHostStream(fd) && GuestSockets::PipeReadState(fd, &available, &eof); - if (!isSocket && !isPipe && DescriptorIsOpen_nid_no_patch(fd)) { - NotImplemented_nid_no_patch(filter == EVFILT_READ ? "sceKernelAddReadEvent: descriptor that is neither a socket nor the read end of a pipe" : "sceKernelAddWriteEvent: non-socket descriptor"); + const bool isFile = !isSocket && !isPipe && RegularFile(fd); + if (!isSocket && !isPipe && !isFile && DescriptorIsOpen_nid_no_patch(fd)) { + NotImplemented_nid_no_patch(filter == EVFILT_READ ? "sceKernelAddReadEvent: descriptor that is neither a socket, the read end of a pipe nor a regular file" : "sceKernelAddWriteEvent: descriptor that is neither a socket nor a regular file"); } if (size > 1 && !isPipe) { NotImplemented_nid_no_patch(filter == EVFILT_READ ? "sceKernelAddReadEvent: low watermark" : "sceKernelAddWriteEvent: low watermark"); } - if (!isSocket && !isPipe) { + if (!isSocket && !isPipe && !isFile) { return SCE_KERNEL_ERROR_EBADF; } + if (isFile) { + return EqueueAddEvent_nid_postfix(eq, FileEvent(fd, filter, EV_ADD, udata)); + } return EqueueAddEvent_nid_postfix(eq, EqueueDescriptorEvent(fd, filter, EV_ADD, udata)); } diff --git a/core/libs/prx/libkernel/Equeue/Equeue.hpp b/core/libs/prx/libkernel/Equeue/Equeue.hpp index ef217632d6..6b9194be56 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.hpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.hpp @@ -57,6 +57,7 @@ struct KernelFilter { EqueueDeleteFunc deleteEventFunc = nullptr; EqueuePollFunc pollFunc = nullptr; int waitDescriptor = -1; + bool repoll = false; std::shared_ptr watched; }; @@ -104,6 +105,7 @@ private: int CollectTriggered(KernelEvent* ev, int num); int CollectOncePerWait(KernelEvent* ev, int num); bool PollEvents(); + void MeasureFiles(std::unique_lock& lock); void WakePollers(); void WaitForDescriptors(std::unique_lock& lock, std::uint64_t deadlineNanos); void GatherInterests(WaitSet& wait, std::uintptr_t waker, bool nested); diff --git a/core/libs/prx/libkernel/File/include/SharedMemory.hpp b/core/libs/prx/libkernel/File/include/SharedMemory.hpp index 5c9ffc9f47..7d9be97385 100644 --- a/core/libs/prx/libkernel/File/include/SharedMemory.hpp +++ b/core/libs/prx/libkernel/File/include/SharedMemory.hpp @@ -14,6 +14,15 @@ std::int64_t WriteSharedMemory_nid_no_patch(int descriptor, const void* buffer, 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); +#ifdef _WIN32 +int SetSharedMemoryMode_nid_no_patch(int descriptor, int mode); +int SharedMemoryDescriptorWritable_nid_no_patch(int descriptor); +void TrackSharedMemoryDescriptor_nid_no_patch(int descriptor, bool writable); +void ForgetSharedMemoryDescriptor_nid_no_patch(int descriptor); +void DuplicateSharedMemoryDescriptor_nid_no_patch(int source, int destination); +int CloseSharedMemoryDescriptor_nid_no_patch(int descriptor); +int ReplaceSharedMemoryDescriptor_nid_no_patch(int source, int destination); +#endif #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); diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index d3fe98dd1f..71668b784b 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -87,6 +87,8 @@ static int NativeWrite(int fd, const void* buf, std::size_t n) { extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); static void IgnoreInvalidParameter(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) {} static int NativeClose(int fd) { + const int shared = File::CloseSharedMemoryDescriptor_nid_no_patch(fd); + if (shared != -2) return shared; const auto previous = _set_thread_local_invalid_parameter_handler(IgnoreInvalidParameter); const int result = ::_close(fd); _set_thread_local_invalid_parameter_handler(previous); @@ -296,10 +298,10 @@ int OpenInDirectory_nid_no_patch(int directory, const char* path, int flags, std int APS5_VABI sceKernelClose(int d) { if (File::IsHostStream(d)) return SCE_KERNEL_ERROR_EPERM; if (GuestSockets::IsOpen(d)) return GuestSockets::Close(d) == 0 ? 0 : SceKernelError(*__error_nid_postfix()); + EqueueDescriptorClosed(d); #ifdef _WIN32 const auto positioned = File::LockPositionedTransfer(d); #endif - EqueueDescriptorClosed(d); File::ForgetRandomDevice(d); File::ForgetDescriptor(d); ForgetJitSharedMemory(d); diff --git a/core/libs/prx/libkernel/File/src/SharedMemory.cpp b/core/libs/prx/libkernel/File/src/SharedMemory.cpp index c5b133d941..9378f47f62 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 #include +#include +#include #ifdef _WIN32 #include @@ -22,6 +24,11 @@ #include #include #include +#include +#include +#include +#include "nid/Sha1.hpp" +extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); #else #include "nid/Sha1.hpp" #include @@ -37,6 +44,32 @@ extern "C" int* APS5_VABI __error_nid_postfix(); namespace { +#ifdef _WIN32 +std::mutex sharedDescriptorAccessMutex; +std::unordered_map sharedDescriptorAccess; + +const std::wstring& WindowsNamedRoot() { + static const std::wstring root = [] { + const auto directory = std::filesystem::temp_directory_path(); + std::vector pathBuffer(32768); + const HANDLE handle = ::CreateFileW(directory.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) + throw std::system_error(static_cast(::GetLastError()), std::system_category(), "opening the named shared-memory temporary root"); + const DWORD length = ::GetFinalPathNameByHandleW(handle, pathBuffer.data(), static_cast(pathBuffer.size()), + FILE_NAME_NORMALIZED | VOLUME_NAME_DOS); + const DWORD error = length == 0 ? ::GetLastError() : length >= pathBuffer.size() ? ERROR_FILENAME_EXCED_RANGE : ERROR_SUCCESS; + ::CloseHandle(handle); + if (error != ERROR_SUCCESS) + throw std::system_error(static_cast(error), std::system_category(), "resolving the named shared-memory temporary root"); + return (std::filesystem::path(std::wstring(pathBuffer.data(), length)) / L"AnyPS5" / L"shm").wstring(); + }(); + return root; +} + +void IgnoreDescriptorParameter(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) {} +#endif + bool IsHostSharedMemoryDescriptor(int descriptor) { #ifdef __linux__ char descriptorPath[64]; @@ -47,6 +80,44 @@ bool IsHostSharedMemoryDescriptor(int descriptor) { objectPath[length] = '\0'; return std::string_view(objectPath).starts_with("/dev/shm/anyps5-shm-") || std::string_view(objectPath).starts_with("/memfd:anyps5 shm_open"); +#elif defined(_WIN32) + { + std::lock_guard lock(sharedDescriptorAccessMutex); + if (!sharedDescriptorAccess.contains(descriptor)) return false; + } + const auto previous = _set_thread_local_invalid_parameter_handler(IgnoreDescriptorParameter); + const auto handleValue = ::_get_osfhandle(descriptor); + _set_thread_local_invalid_parameter_handler(previous); + if (handleValue == -1) return false; + const auto handle = reinterpret_cast(handleValue); + std::vector pathBuffer(32768); + const DWORD length = ::GetFinalPathNameByHandleW(handle, pathBuffer.data(), static_cast(pathBuffer.size()), FILE_NAME_NORMALIZED | VOLUME_NAME_DOS); + if (length == 0 || length >= pathBuffer.size()) return false; + std::wstring objectPath(pathBuffer.data(), length); + std::wstring_view root; + try { + root = WindowsNamedRoot(); + } catch (...) { + return false; + } + if (objectPath.size() <= root.size() || objectPath[root.size()] != L'\\') return false; + if (::CompareStringOrdinal(objectPath.data(), static_cast(root.size()), root.data(), static_cast(root.size()), TRUE) != CSTR_EQUAL) return false; + const std::wstring_view relative(objectPath.data() + root.size() + 1, objectPath.size() - root.size() - 1); + if (relative.find_first_of(L"\\/") != std::wstring_view::npos) return false; + const auto validHex = [](wchar_t character) { + return (character >= L'0' && character <= L'9') || (character >= L'a' && character <= L'f'); + }; + if (relative.size() < 44 || !std::all_of(relative.begin(), relative.begin() + 40, validHex)) return false; + if (relative.substr(40) == L".dat") return true; + constexpr std::wstring_view marker = L"-unlinked-"; + if (!relative.substr(40).starts_with(marker) || !relative.ends_with(L".dat")) return false; + const auto suffix = relative.substr(40 + marker.size(), relative.size() - 40 - marker.size() - 4); + const auto separator = suffix.find(L'-'); + if (separator == std::wstring_view::npos || separator == 0 || separator + 1 == suffix.size()) return false; + const auto decimal = [](std::wstring_view value) { + return std::all_of(value.begin(), value.end(), [](wchar_t character) { return character >= L'0' && character <= L'9'; }); + }; + return decimal(suffix.substr(0, separator)) && decimal(suffix.substr(separator + 1)); #else (void)descriptor; return false; @@ -107,7 +178,6 @@ int GuestError(int error) { } } -#ifndef _WIN32 int NamedGuestError(int error) { switch (error) { case ENOENT: @@ -133,14 +203,17 @@ int NamedGuestError(int error) { } } -std::string NamedHostPath(const char* path, int& error) { +std::string NamedHostName(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) { 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-"; + std::string hostName; +#ifndef _WIN32 + hostName = "/anyps5-shm-"; +#endif for (const auto byte : digest) { hostName += "0123456789abcdef"[byte >> 4]; hostName += "0123456789abcdef"[byte & 15]; @@ -149,9 +222,384 @@ std::string NamedHostPath(const char* path, int& error) { return hostName; } +#ifdef _WIN32 +int WindowsNamedError(DWORD error) { + switch (error) { + case ERROR_FILE_NOT_FOUND: + case ERROR_PATH_NOT_FOUND: return ENOENT; + case ERROR_ACCESS_DENIED: + case ERROR_SHARING_VIOLATION: return EACCES; + case ERROR_FILE_EXISTS: + case ERROR_ALREADY_EXISTS: return EEXIST; + case ERROR_INVALID_NAME: return EINVAL; + case ERROR_TOO_MANY_OPEN_FILES: return EMFILE; + case ERROR_DISK_FULL: return ENOSPC; + case ERROR_NOT_ENOUGH_MEMORY: + case ERROR_OUTOFMEMORY: return ENOMEM; + default: return 0; + } +} + +std::filesystem::path WindowsNamedPath(const std::string& name) { + return std::filesystem::path(WindowsNamedRoot()) / std::filesystem::path(name + ".dat"); +} + +void EnsurePrivateWindowsDirectory(const std::filesystem::path& path, bool objectDirectory = false) { + const std::wstring ownerRights = objectDirectory ? L"0x001701ff" : L"FA"; + const std::wstring policy = L"D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;" + ownerRights + L";;;OW)"; + PSECURITY_DESCRIPTOR descriptor = nullptr; + if (!::ConvertStringSecurityDescriptorToSecurityDescriptorW( + policy.c_str(), SDDL_REVISION_1, &descriptor, nullptr)) + throw std::system_error(static_cast(::GetLastError()), std::system_category(), "creating the named shared-memory access policy"); + PACL dacl = nullptr; + BOOL present = FALSE; + BOOL defaulted = FALSE; + if (!::GetSecurityDescriptorDacl(descriptor, &present, &dacl, &defaulted) || !present || dacl == nullptr) { + const DWORD error = ::GetLastError(); + ::LocalFree(descriptor); + throw std::system_error(static_cast(error == ERROR_SUCCESS ? ERROR_INVALID_SECURITY_DESCR : error), + std::system_category(), "reading the named shared-memory access policy"); + } + + SECURITY_ATTRIBUTES security{sizeof(SECURITY_ATTRIBUTES), descriptor, FALSE}; + if (!::CreateDirectoryW(path.c_str(), &security) && ::GetLastError() != ERROR_ALREADY_EXISTS) { + const DWORD error = ::GetLastError(); + ::LocalFree(descriptor); + throw std::system_error(static_cast(error), std::system_category(), "creating the named shared-memory directory"); + } + const DWORD attributes = ::GetFileAttributesW(path.c_str()); + if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_DIRECTORY) == 0 || + (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { + const DWORD error = attributes == INVALID_FILE_ATTRIBUTES ? ::GetLastError() : ERROR_ACCESS_DENIED; + ::LocalFree(descriptor); + throw std::system_error(static_cast(error), std::system_category(), "validating the named shared-memory directory"); + } + const DWORD result = ::SetNamedSecurityInfoW(const_cast(path.c_str()), SE_FILE_OBJECT, + DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION, + nullptr, nullptr, dacl, nullptr); + ::LocalFree(descriptor); + if (result != ERROR_SUCCESS) throw std::system_error(static_cast(result), std::system_category(), "securing the named shared-memory directory"); +} + +DWORD SetWindowsNamedPermissions(HANDLE handle, int mode) { + std::wstring policy = L"D:P(A;OICI;FA;;;SY)(A;;WD;;;OW)"; + if ((mode & 0400) != 0) policy += L"(A;;GRWD;;;OW)"; + if ((mode & 0200) != 0) policy += L"(A;;GWSDWD;;;OW)"; + PSECURITY_DESCRIPTOR descriptor = nullptr; + if (!::ConvertStringSecurityDescriptorToSecurityDescriptorW(policy.c_str(), SDDL_REVISION_1, &descriptor, nullptr)) + return ::GetLastError(); + PACL dacl = nullptr; + BOOL present = FALSE; + BOOL defaulted = FALSE; + if (!::GetSecurityDescriptorDacl(descriptor, &present, &dacl, &defaulted) || !present || dacl == nullptr) { + const DWORD error = ::GetLastError(); + ::LocalFree(descriptor); + return error == ERROR_SUCCESS ? ERROR_INVALID_SECURITY_DESCR : error; + } + const DWORD result = ::SetSecurityInfo(handle, SE_FILE_OBJECT, + DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION, nullptr, nullptr, dacl, nullptr); + ::LocalFree(descriptor); + return result; +} + +void CleanupUnlinkedWindowsObjects(const std::filesystem::path& objectPath, const std::string& name) { + const auto pattern = objectPath.parent_path() / (std::wstring(name.begin(), name.end()) + L"-unlinked-*.dat"); + WIN32_FIND_DATAW data{}; + const HANDLE search = ::FindFirstFileW(pattern.c_str(), &data); + if (search == INVALID_HANDLE_VALUE) return; + do { + if ((data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0) { + const auto orphanPath = objectPath.parent_path() / data.cFileName; + HANDLE orphan = ::CreateFileW(orphanPath.c_str(), DELETE, 0, nullptr, OPEN_EXISTING, + FILE_FLAG_OPEN_REPARSE_POINT, nullptr); + if (orphan != INVALID_HANDLE_VALUE) { + FILE_DISPOSITION_INFO deletion{TRUE}; + ::SetFileInformationByHandle(orphan, FileDispositionInfo, &deletion, sizeof(deletion)); + ::CloseHandle(orphan); + } + } + } while (::FindNextFileW(search, &data)); + ::FindClose(search); +} + +void ReapUnlinkedWindowsObject(const std::filesystem::path& orphanPath) { + HANDLE orphan = ::CreateFileW(orphanPath.c_str(), DELETE, 0, nullptr, OPEN_EXISTING, + FILE_FLAG_OPEN_REPARSE_POINT, nullptr); + if (orphan == INVALID_HANDLE_VALUE) return; + FILE_DISPOSITION_INFO deletion{TRUE}; + ::SetFileInformationByHandle(orphan, FileDispositionInfo, &deletion, sizeof(deletion)); + ::CloseHandle(orphan); +} + +class WindowsNamedLock { +public: + explicit WindowsNamedLock(const std::string& name) { + Initialize(WindowsNamedPath(name).wstring()); + } + + explicit WindowsNamedLock(HANDLE object) { + BY_HANDLE_FILE_INFORMATION status{}; + if (!::GetFileInformationByHandle(object, &status)) { error = ::GetLastError(); return; } + Initialize(L"object-" + std::to_wstring(status.dwVolumeSerialNumber) + L"-" + + std::to_wstring(status.nFileIndexHigh) + L"-" + std::to_wstring(status.nFileIndexLow)); + } + + ~WindowsNamedLock() { + if (locked) ::ReleaseMutex(handle); + if (handle != nullptr) ::CloseHandle(handle); + } + + DWORD Error() const { return error; } + +private: + void Initialize(const std::wstring& identity) { + const auto bytes = std::vector(reinterpret_cast(identity.data()), + reinterpret_cast(identity.data() + identity.size())); + const auto digest = Nid::Sha1(bytes); + std::wstring lockName = L"Global\\AnyPS5NamedShm-"; + for (const auto byte : digest) { + lockName += L"0123456789abcdef"[byte >> 4]; + lockName += L"0123456789abcdef"[byte & 15]; + } + handle = ::CreateMutexW(nullptr, FALSE, lockName.c_str()); + if (handle == nullptr) { error = ::GetLastError(); return; } + const DWORD result = ::WaitForSingleObject(handle, INFINITE); + if (result == WAIT_OBJECT_0 || result == WAIT_ABANDONED) locked = true; + else error = result == WAIT_FAILED ? ::GetLastError() : ERROR_GEN_FAILURE; + } + HANDLE handle = nullptr; + DWORD error = ERROR_SUCCESS; + bool locked = false; +}; + +int OpenNamedFile(const char* path, int flags, int mode) { + const int savedError = errno; + int validationError = 0; + const auto hostName = NamedHostName(path, validationError); + if (validationError != 0) return Fail(NamedGuestError(validationError) ? NamedGuestError(validationError) : GuestInvalid); + + WindowsNamedLock namespaceLock(hostName); + if (namespaceLock.Error() != ERROR_SUCCESS) { + if (const int guest = WindowsNamedError(namespaceLock.Error())) return Fail(NamedGuestError(guest)); + throw std::runtime_error("shm_open: locking the host named-object namespace failed, error=" + std::to_string(namespaceLock.Error())); + } + + std::filesystem::path objectPath; + try { + objectPath = WindowsNamedPath(hostName); + EnsurePrivateWindowsDirectory(objectPath.parent_path().parent_path()); + EnsurePrivateWindowsDirectory(objectPath.parent_path(), true); + CleanupUnlinkedWindowsObjects(objectPath, hostName); + } catch (const std::system_error& error) { + const auto nativeError = static_cast(error.code().value()); + if (const int guest = WindowsNamedError(nativeError)) return Fail(NamedGuestError(guest)); + throw std::runtime_error("shm_open: preparing the host named-object namespace failed: " + std::string(error.what())); + } + + const bool readWrite = (flags & SCE_KERNEL_O_ACCMODE) == SCE_KERNEL_O_RDWR; + const DWORD access = GENERIC_READ | (readWrite ? GENERIC_WRITE : 0) | WRITE_DAC; + const bool create = (flags & SCE_KERNEL_O_CREAT) != 0; + const DWORD creationAccess = access | (create ? WRITE_DAC | DELETE : 0); + ::SetLastError(ERROR_SUCCESS); + HANDLE handle = ::CreateFileW(objectPath.c_str(), creationAccess, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, create ? CREATE_NEW : OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OPEN_REPARSE_POINT, nullptr); + DWORD createResult = ::GetLastError(); + bool created = handle != INVALID_HANDLE_VALUE && create; + if (handle == INVALID_HANDLE_VALUE && create && createResult == ERROR_FILE_EXISTS && !(flags & SCE_KERNEL_O_EXCL)) { + handle = ::CreateFileW(objectPath.c_str(), access, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OPEN_REPARSE_POINT, nullptr); + createResult = ::GetLastError(); + } + if (handle == INVALID_HANDLE_VALUE) { + const DWORD nativeError = createResult; + if (const int guest = WindowsNamedError(nativeError)) return Fail(NamedGuestError(guest)); + throw std::runtime_error("shm_open: opening the host named object failed, error=" + std::to_string(nativeError)); + } + + BY_HANDLE_FILE_INFORMATION status{}; + if (!::GetFileInformationByHandle(handle, &status) || (status.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { + const DWORD lastError = ::GetLastError(); + const DWORD nativeError = lastError == ERROR_SUCCESS ? ERROR_INVALID_NAME : lastError; + if (created) { + FILE_DISPOSITION_INFO deletion{TRUE}; + ::SetFileInformationByHandle(handle, FileDispositionInfo, &deletion, sizeof(deletion)); + } + ::CloseHandle(handle); + if (const int guest = WindowsNamedError(nativeError)) return Fail(NamedGuestError(guest)); + throw std::runtime_error("shm_open: refusing a reparse-point named object, error=" + std::to_string(nativeError)); + } + + if (created) { + const int permissions = (mode & 0777) & ~File::GetCreationMask_nid_no_patch(); + const DWORD nativeError = SetWindowsNamedPermissions(handle, permissions); + if (nativeError != ERROR_SUCCESS) { + FILE_DISPOSITION_INFO deletion{TRUE}; + ::SetFileInformationByHandle(handle, FileDispositionInfo, &deletion, sizeof(deletion)); + ::CloseHandle(handle); + if (const int guest = WindowsNamedError(nativeError)) return Fail(NamedGuestError(guest)); + throw std::runtime_error("shm_open: setting guest creation permissions failed, error=" + std::to_string(nativeError)); + } + } + + if ((flags & SCE_KERNEL_O_TRUNC) && readWrite) { + WindowsNamedLock objectLock(handle); + if (objectLock.Error() != ERROR_SUCCESS) { + const DWORD nativeError = objectLock.Error(); + ::CloseHandle(handle); + if (const int guest = WindowsNamedError(nativeError)) return Fail(NamedGuestError(guest)); + throw std::runtime_error("shm_open: locking the host named object for truncation failed, error=" + std::to_string(nativeError)); + } + LARGE_INTEGER zero{}; + if (!::SetFilePointerEx(handle, zero, nullptr, FILE_BEGIN) || !::SetEndOfFile(handle)) { + const DWORD nativeError = ::GetLastError(); + ::CloseHandle(handle); + if (const int guest = WindowsNamedError(nativeError)) return Fail(NamedGuestError(guest)); + throw std::runtime_error("shm_open: truncating the host named object failed, error=" + std::to_string(nativeError)); + } + } + + const int nativeFlags = _O_BINARY | (readWrite ? _O_RDWR : _O_RDONLY); + const int descriptor = ::_open_osfhandle(reinterpret_cast(handle), nativeFlags); + if (descriptor >= 0) { + File::TrackSharedMemoryDescriptor_nid_no_patch(descriptor, readWrite); + errno = savedError; + return descriptor; + } + const int nativeError = errno; + ::CloseHandle(handle); + if (const int guest = GuestError(nativeError)) return Fail(guest); + throw std::runtime_error("shm_open: converting the host named object to a descriptor failed, errno=" + std::to_string(nativeError)); +} + +int UnlinkNamedFile(const char* path) { + const int savedError = errno; + int validationError = 0; + const auto hostName = NamedHostName(path, validationError); + if (validationError != 0) return Fail(NamedGuestError(validationError) ? NamedGuestError(validationError) : GuestInvalid); + WindowsNamedLock namespaceLock(hostName); + if (namespaceLock.Error() != ERROR_SUCCESS) { + if (const int guest = WindowsNamedError(namespaceLock.Error())) return Fail(NamedGuestError(guest)); + throw std::runtime_error("shm_unlink: locking the host named-object namespace failed, error=" + std::to_string(namespaceLock.Error())); + } + + std::filesystem::path objectPath; + try { + objectPath = WindowsNamedPath(hostName); + } catch (const std::filesystem::filesystem_error& error) { + const auto nativeError = static_cast(error.code().value()); + if (const int guest = WindowsNamedError(nativeError)) return Fail(NamedGuestError(guest)); + throw std::runtime_error("shm_unlink: locating the host named object failed: " + std::string(error.what())); + } + + HANDLE objectHandle = ::CreateFileW(objectPath.c_str(), FILE_READ_ATTRIBUTES | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, + FILE_FLAG_OPEN_REPARSE_POINT, nullptr); + if (objectHandle == INVALID_HANDLE_VALUE) { + const DWORD nativeError = ::GetLastError(); + if (const int guest = WindowsNamedError(nativeError)) return Fail(NamedGuestError(guest)); + throw std::runtime_error("shm_unlink: opening the host named object failed, error=" + std::to_string(nativeError)); + } + BY_HANDLE_FILE_INFORMATION status{}; + if (!::GetFileInformationByHandle(objectHandle, &status) || + (status.dwFileAttributes & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT)) != 0) { + const DWORD lastError = ::GetLastError(); + const DWORD nativeError = lastError == ERROR_SUCCESS ? ERROR_INVALID_NAME : lastError; + ::CloseHandle(objectHandle); + if (const int guest = WindowsNamedError(nativeError)) return Fail(NamedGuestError(guest)); + throw std::runtime_error("shm_unlink: refusing an invalid host named object, error=" + std::to_string(nativeError)); + } + + static std::atomic sequence{0}; + for (unsigned attempt = 0; attempt < 64; ++attempt) { + const auto orphanPath = objectPath.parent_path() / + (objectPath.stem().wstring() + L"-unlinked-" + std::to_wstring(::GetCurrentProcessId()) + + L"-" + std::to_wstring(sequence.fetch_add(1, std::memory_order_relaxed)) + objectPath.extension().wstring()); + if (!::MoveFileExW(objectPath.c_str(), orphanPath.c_str(), MOVEFILE_WRITE_THROUGH)) { + const DWORD nativeError = ::GetLastError(); + if (nativeError == ERROR_ALREADY_EXISTS || nativeError == ERROR_FILE_EXISTS) continue; + ::CloseHandle(objectHandle); + if (const int guest = WindowsNamedError(nativeError)) return Fail(NamedGuestError(guest)); + throw std::runtime_error("shm_unlink: detaching the host named object failed, error=" + std::to_string(nativeError)); + } + ::CloseHandle(objectHandle); + ReapUnlinkedWindowsObject(orphanPath); + errno = savedError; + return 0; + } + ::CloseHandle(objectHandle); + return Fail(NamedGuestError(EEXIST)); +} + +int SetSharedMemoryModeInternal(int descriptor, int mode) { + if (!IsHostSharedMemoryDescriptor(descriptor)) return -2; + const auto rawHandle = ::_get_osfhandle(descriptor); + if (rawHandle == -1) { errno = EBADF; return -1; } + const auto handle = reinterpret_cast(rawHandle); + WindowsNamedLock objectLock(handle); + if (objectLock.Error() != ERROR_SUCCESS) { + const int error = WindowsNamedError(objectLock.Error()); + errno = error ? error : EIO; + return -1; + } + const DWORD nativeError = SetWindowsNamedPermissions(handle, mode & 0777); + if (nativeError == ERROR_SUCCESS) return 0; + const int error = WindowsNamedError(nativeError); + errno = error ? error : EACCES; + return -1; +} + +std::filesystem::path SharedMemoryDescriptorPath(int descriptor) { + const auto rawHandle = ::_get_osfhandle(descriptor); + std::filesystem::path finalPath; + if (rawHandle != -1) { + std::vector path(32768); + const DWORD length = ::GetFinalPathNameByHandleW(reinterpret_cast(rawHandle), path.data(), + static_cast(path.size()), FILE_NAME_NORMALIZED | VOLUME_NAME_DOS); + if (length > 4 && length < path.size()) { + std::wstring value(path.data(), length); + finalPath = std::filesystem::path(std::move(value)); + } + } + return finalPath; +} + +void ReapSharedMemoryDescriptorPath(const std::filesystem::path& path) { + if (!path.empty() && path.filename().wstring().find(L"-unlinked-") != std::wstring::npos) + ReapUnlinkedWindowsObject(path); +} + +int CloseSharedMemoryDescriptorInternal(int descriptor) { + if (!IsHostSharedMemoryDescriptor(descriptor)) return -2; + const int savedError = errno; + const auto finalPath = SharedMemoryDescriptorPath(descriptor); + int result; + { + std::lock_guard lock(sharedDescriptorAccessMutex); + result = ::_close(descriptor); + if (result == 0) sharedDescriptorAccess.erase(descriptor); + } + if (result == 0) ReapSharedMemoryDescriptorPath(finalPath); + if (result == 0) errno = savedError; + return result; +} + +int ReplaceSharedMemoryDescriptorInternal(int source, int destination) { + if (!IsHostSharedMemoryDescriptor(destination)) return -2; + const int savedError = errno; + const auto finalPath = SharedMemoryDescriptorPath(destination); + const auto previous = _set_thread_local_invalid_parameter_handler(IgnoreDescriptorParameter); + const int result = ::_dup2(source, destination); + _set_thread_local_invalid_parameter_handler(previous); + if (result == 0) { + ReapSharedMemoryDescriptorPath(finalPath); + errno = savedError; + } + return result; +} +#else int OpenNamedFile(const char* path, int flags, int mode) { int validationError = 0; - const auto hostName = NamedHostPath(path, validationError); + const auto hostName = NamedHostName(path, validationError); if (validationError != 0) return Fail(NamedGuestError(validationError) ? NamedGuestError(validationError) : GuestInvalid); HostShmLock namespaceLock; if (namespaceLock.Error() != 0) { @@ -222,6 +670,44 @@ bool IsSharedMemoryDescriptor_nid_no_patch(int descriptor) { return IsHostSharedMemoryDescriptor(descriptor); } +#ifdef _WIN32 +int SetSharedMemoryMode_nid_no_patch(int descriptor, int mode) { + return SetSharedMemoryModeInternal(descriptor, mode); +} + +int CloseSharedMemoryDescriptor_nid_no_patch(int descriptor) { + return CloseSharedMemoryDescriptorInternal(descriptor); +} + +int ReplaceSharedMemoryDescriptor_nid_no_patch(int source, int destination) { + return ReplaceSharedMemoryDescriptorInternal(source, destination); +} + +int SharedMemoryDescriptorWritable_nid_no_patch(int descriptor) { + if (!IsHostSharedMemoryDescriptor(descriptor)) return -2; + std::lock_guard lock(sharedDescriptorAccessMutex); + const auto found = sharedDescriptorAccess.find(descriptor); + return found != sharedDescriptorAccess.end() && found->second ? 1 : 0; +} + +void TrackSharedMemoryDescriptor_nid_no_patch(int descriptor, bool writable) { + std::lock_guard lock(sharedDescriptorAccessMutex); + sharedDescriptorAccess[descriptor] = writable; +} + +void ForgetSharedMemoryDescriptor_nid_no_patch(int descriptor) { + std::lock_guard lock(sharedDescriptorAccessMutex); + sharedDescriptorAccess.erase(descriptor); +} + +void DuplicateSharedMemoryDescriptor_nid_no_patch(int source, int destination) { + std::lock_guard lock(sharedDescriptorAccessMutex); + sharedDescriptorAccess.erase(destination); + const auto found = sharedDescriptorAccess.find(source); + if (found != sharedDescriptorAccess.end()) sharedDescriptorAccess[destination] = found->second; +} +#endif + namespace { class SharedMemoryLock { @@ -230,6 +716,18 @@ public: #ifdef __linux__ struct stat originalStatus{}; if (::fstat(value, &originalStatus) != 0) { shared = true; error = errno; return; } +#elif defined(_WIN32) + if (!IsHostSharedMemoryDescriptor(value)) return; + const auto originalValue = ::_get_osfhandle(value); + if (originalValue == -1) return; + BY_HANDLE_FILE_INFORMATION originalStatus{}; + if (!::GetFileInformationByHandle(reinterpret_cast(originalValue), &originalStatus)) { + shared = true; + error = EBADF; + return; + } + windowsObjectLock = std::make_unique(reinterpret_cast(originalValue)); + if (windowsObjectLock->Error() != ERROR_SUCCESS) { shared = true; error = WindowsNamedError(windowsObjectLock->Error()); if (!error) error = EIO; return; } #endif if (!IsHostSharedMemoryDescriptor(value)) { #ifdef __linux__ @@ -247,10 +745,20 @@ public: if (hostLock->Error() != 0) { error = hostLock->Error(); return; } descriptor = Duplicate(value); if (descriptor < 0) { error = errno; return; } +#ifdef _WIN32 + DuplicateSharedMemoryDescriptor_nid_no_patch(value, descriptor); +#endif if (!IsHostSharedMemoryDescriptor(descriptor)) { error = EBADF; return; } if (!Refresh()) { error = errno; return; } #ifdef __linux__ if (status.st_dev != originalStatus.st_dev || status.st_ino != originalStatus.st_ino) error = EBADF; +#elif defined(_WIN32) + BY_HANDLE_FILE_INFORMATION duplicatedStatus{}; + const auto duplicatedValue = ::_get_osfhandle(descriptor); + if (duplicatedValue == -1 || !::GetFileInformationByHandle(reinterpret_cast(duplicatedValue), &duplicatedStatus) || + duplicatedStatus.dwVolumeSerialNumber != originalStatus.dwVolumeSerialNumber || + duplicatedStatus.nFileIndexHigh != originalStatus.nFileIndexHigh || + duplicatedStatus.nFileIndexLow != originalStatus.nFileIndexLow) error = EBADF; #endif } @@ -258,7 +766,7 @@ public: if (descriptor >= 0) { const int savedError = errno; #ifdef _WIN32 - ::_close(descriptor); + CloseSharedMemoryDescriptor_nid_no_patch(descriptor); #else ::close(descriptor); #endif @@ -292,6 +800,9 @@ private: int descriptor = -1; std::optional hostLock; +#ifdef _WIN32 + std::unique_ptr windowsObjectLock; +#endif bool shared = false; int error = 0; }; @@ -303,7 +814,9 @@ std::int64_t WriteSharedMemory_nid_no_patch(int descriptor, const void* buffer, if (guard.Error() != 0) { errno = guard.Error(); return -1; } if (!guard.Refresh()) return -1; const auto fd = guard.Descriptor(); -#ifndef _WIN32 +#ifdef _WIN32 + if (SharedMemoryDescriptorWritable_nid_no_patch(fd) == 0) { errno = EBADF; return -1; } +#else const int access = ::fcntl(fd, F_GETFL); if (access < 0) return -1; if ((access & O_ACCMODE) == O_RDONLY) { errno = EBADF; return -1; } @@ -331,7 +844,9 @@ std::int64_t PwriteSharedMemory_nid_no_patch(int descriptor, const void* buffer, if (guard.Error() != 0) { errno = guard.Error(); return -1; } if (!guard.Refresh()) return -1; const auto fd = guard.Descriptor(); -#ifndef _WIN32 +#ifdef _WIN32 + if (SharedMemoryDescriptorWritable_nid_no_patch(fd) == 0) { errno = EBADF; return -1; } +#else const int access = ::fcntl(fd, F_GETFL); if (access < 0) return -1; if ((access & O_ACCMODE) == O_RDONLY) { errno = EBADF; return -1; } @@ -341,7 +856,19 @@ std::int64_t PwriteSharedMemory_nid_no_patch(int descriptor, const void* buffer, 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); + const auto handle = reinterpret_cast(::_get_osfhandle(fd)); + if (handle == INVALID_HANDLE_VALUE || ::GetFileType(handle) != FILE_TYPE_DISK) { errno = EBADF; return -1; } + LARGE_INTEGER previous{}; + if (!::SetFilePointerEx(handle, LARGE_INTEGER{}, &previous, FILE_CURRENT)) { errno = EIO; return -1; } + OVERLAPPED overlapped{}; + overlapped.Offset = static_cast(offset); + overlapped.OffsetHigh = static_cast(static_cast(offset) >> 32u); + DWORD written = 0; + const BOOL succeeded = ::WriteFile(handle, buffer, static_cast(std::min(count, std::numeric_limits::max())), &written, &overlapped); + const DWORD writeError = succeeded ? ERROR_SUCCESS : ::GetLastError(); + if (!::SetFilePointerEx(handle, previous, nullptr, FILE_BEGIN)) throw std::runtime_error("pwrite: restoring the host file position failed"); + if (!succeeded) { errno = writeError == ERROR_ACCESS_DENIED ? EBADF : EIO; return -1; } + const auto result = written; #else const auto result = ::pwrite(fd, buffer, static_cast(count), static_cast(offset)); #endif @@ -436,9 +963,19 @@ std::int64_t PwritevSharedMemory_nid_no_patch(int descriptor, const struct iovec #endif int TruncateSharedMemory_nid_no_patch(int descriptor, std::int64_t length) { +#ifdef _WIN32 + const auto positioned = LockPositionedTransfer(descriptor); +#endif SharedMemoryLock guard(descriptor); if (!guard.IsShared()) return -2; - if (guard.Error() != 0) { errno = guard.Error(); return -1; } + if (guard.Error() != 0) { + errno = guard.Error(); +#ifdef _WIN32 + return errno; +#else + return -1; +#endif + } const auto fd = guard.Descriptor(); #ifdef _WIN32 const int result = static_cast(::_chsize_s(fd, length)); @@ -456,7 +993,7 @@ extern "C" int APS5_VABI shm_open_nid_postfix(const char* path, int flags, int m if ((flags & ~AcceptedFlags) != 0) return Fail(GuestInvalid); if (path != anonymousObject) { #ifdef _WIN32 - NotImplemented_nid_no_patch("shm_open of a named object"); + return OpenNamedFile(path, flags, mode); #else return OpenNamedFile(path, flags, mode); #endif @@ -471,12 +1008,10 @@ extern "C" int APS5_VABI shm_open_nid_postfix(const char* path, int flags, int m 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"); - return 0; + return UnlinkNamedFile(path); #else int validationError = 0; - const auto hostName = NamedHostPath(path, validationError); + const auto hostName = NamedHostName(path, validationError); if (validationError != 0) return Fail(NamedGuestError(validationError) ? NamedGuestError(validationError) : GuestInvalid); HostShmLock namespaceLock; if (namespaceLock.Error() != 0) { diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index b1079326d9..61d1e37967 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -734,16 +734,18 @@ int APS5_VABI close_nid_postfix(int d) { return -1; } if (GuestSockets::IsOpen(d)) return GuestSockets::Close(d); + EqueueDescriptorClosed(d); #ifdef _WIN32 const auto positioned = File::LockPositionedTransfer(d); #endif - EqueueDescriptorClosed(d); File::ForgetRandomDevice(d); File::ForgetDescriptor(d); ForgetJitSharedMemory(d); #ifdef _WIN32 File::ForgetDirectoryDescriptor(d); File::ForgetFileLock(d); + const int shared = File::CloseSharedMemoryDescriptor_nid_no_patch(d); + if (shared != -2) return shared; return WithoutParameterHandler([d] { return ::_close(d); }); #else return ::close(d); @@ -784,6 +786,7 @@ int APS5_VABI dup_nid_postfix(int d) { #ifdef _WIN32 const int duplicate = WithoutParameterHandler([d] { return ::_dup(d); }); if (duplicate >= 0) File::DuplicateDirectoryDescriptor(d, duplicate); + if (duplicate >= 0) File::DuplicateSharedMemoryDescriptor_nid_no_patch(d, duplicate); #else const int duplicate = ::dup(d); #endif @@ -816,8 +819,10 @@ int APS5_VABI dup2_nid_postfix(int from, int to) { if (from == to) return to; if (to == 1) PreserveHostStandardOutput(); if (to == 2) PreserveHostStandardError(); + auto positioned = File::LockPositionedTransfer(to); auto* const standard = HostStandardHandle_nid_no_patch(to); - const int duplicated = WithoutParameterHandler([from, to] { return ::_dup2(from, to); }); + int duplicated = File::ReplaceSharedMemoryDescriptor_nid_no_patch(from, to); + if (duplicated == -2) duplicated = WithoutParameterHandler([from, to] { return ::_dup2(from, to); }); KeepStandardHandle_nid_no_patch(to, standard); if (duplicated != 0) return PosixFailure(SceErrorFromErrno(errno) & 0xffff); EqueueDescriptorClosed(to); @@ -826,7 +831,10 @@ int APS5_VABI dup2_nid_postfix(int from, int to) { File::DuplicateDirectoryDescriptor(from, to); File::ForgetFileLock(to); File::ForgetRandomDevice(to); + File::ForgetSharedMemoryDescriptor_nid_no_patch(to); + File::DuplicateSharedMemoryDescriptor_nid_no_patch(from, to); if (sourceIsRandom) File::RememberRandomDevice(to); + positioned.unlock(); File::ShareDescriptor(from, to); DuplicateJitSharedMemory(from, to); return to; @@ -1545,6 +1553,10 @@ int APS5_VABI sceKernelChmod_nid_postfix(const char* path, std::uint16_t mode) { int APS5_VABI sceKernelFchmod(int d, std::uint16_t mode) { if (GuestSockets::IsOpen(d)) return SceErrorFromErrno(GUEST_EINVAL); File::RefuseArchivedFile(d, "fchmod"); +#ifdef _WIN32 + const int sharedMemoryResult = File::SetSharedMemoryMode_nid_no_patch(d, mode); + if (sharedMemoryResult != -2) return sharedMemoryResult == 0 ? 0 : SceErrorFromErrno(errno); +#endif if (NativeFchmod(d, mode & 07777) != 0) return SceErrorFromErrno(errno); return 0; } diff --git a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp index 9bb01c8b3d..c8671c5c4f 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp @@ -145,26 +145,28 @@ struct GuestRtPrio { int APS5_VABI rtprio_thread_nid_postfix(int function, int lwpid, GuestRtPrio* priority) { constexpr int Lookup = 0, Set = 1; - constexpr int GuestEsrch = 3, GuestEfault = 14, GuestEinval = 22; - constexpr std::uint16_t Normal = 3; + constexpr int GuestEsrch = 3, GuestEfault = 14; + constexpr int GuestSchedFifo = 1, GuestSchedRr = 3; + constexpr std::uint16_t Realtime = 2, Normal = 3, Fifo = 10; const auto fail = [](int error) { *__error_nid_postfix() = error; return -1; }; - if (function != Lookup && function != Set) return fail(GuestEinval); + if (function != Lookup && function != Set) return fail(GuestEsrch); if (priority == nullptr) return fail(GuestEfault); auto* self = scePthreadSelf(); - if (lwpid != 0 && static_cast(lwpid) != static_cast(self->tid)) { - if (!LiveGuestThreadExists(lwpid)) return fail(GuestEsrch); - NotImplemented_nid_no_patch("rtprio_thread: a thread other than the calling one"); - } - const int policy = self->schedPolicy.load(std::memory_order_relaxed); - if (policy == 1 || policy == 3) NotImplemented_nid_no_patch("rtprio_thread: a guest thread's real-time class"); + int policy = self->schedPolicy.load(std::memory_order_relaxed); + int schedPriority = self->priority.load(std::memory_order_relaxed); + const bool other = lwpid != 0 && static_cast(lwpid) != static_cast(self->tid); + if (other && !GetLiveGuestThreadSched_nid_no_patch(lwpid, policy, schedPriority)) return fail(GuestEsrch); + const bool realTime = policy == GuestSchedFifo || policy == GuestSchedRr; if (function == Lookup) { - priority->type = Normal; - priority->prio = 0; + priority->type = realTime ? (policy == GuestSchedFifo ? Fifo : Realtime) : Normal; + priority->prio = realTime ? static_cast(schedPriority) : 0; return 0; } + if (other) NotImplemented_nid_no_patch("rtprio_thread: a thread other than the calling one"); + if (realTime) NotImplemented_nid_no_patch("rtprio_thread: a guest thread's real-time class"); if (priority->type == Normal && priority->prio == 0) return 0; NotImplemented_nid_no_patch("rtprio_thread: a real-time priority change"); } diff --git a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp index c4442bd188..0e83376197 100644 --- a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp @@ -181,6 +181,7 @@ bool GuestThreadIsLive(const PthreadPrivate* thread); std::unique_lock LockLiveGuestThread(const PthreadPrivate* thread); bool LiveGuestThreadExists(std::int64_t tid); bool GetLiveGuestThreadAffinity_nid_no_patch(std::int64_t tid, KernelCpumask& mask); +bool GetLiveGuestThreadSched_nid_no_patch(std::int64_t tid, int& policy, int& priority); bool SetLiveGuestThreadAffinity_nid_no_patch(std::int64_t tid, KernelCpumask mask); extern "C" std::size_t GetDefaultStackSize_nid_no_patch(); extern "C" bool SetDefaultStackSize_nid_no_patch(std::size_t size); diff --git a/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp b/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp index 35a043a8e9..36d7301077 100644 --- a/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp @@ -14,6 +14,8 @@ static constexpr int SCE_KERNEL_ERROR_EDEADLK = 0x8002000B; static constexpr int SCE_KERNEL_ERROR_EBUSY = 0x80020010; static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = 0x8002003C; static constexpr int SCE_KERNEL_ERROR_EINVAL = 0x80020016; +static constexpr int FIRST_STORED_RWLOCK_TYPE = 1; +static constexpr int LAST_STORED_RWLOCK_TYPE = 2; static PthreadRwlockPrivate* const DestroyedRwlock = reinterpret_cast(std::uintptr_t{1}); @@ -174,7 +176,7 @@ int APS5_VABI scePthreadRwlockattrInit(PthreadRwlockattr* attr) { } int APS5_VABI scePthreadRwlockattrSettype(PthreadRwlockattr* attr, int type) { - if (!attr || !*attr) throw std::runtime_error("scePthreadRwlockattrSettype: null attr"); + if (!attr || !*attr || type < FIRST_STORED_RWLOCK_TYPE || type > LAST_STORED_RWLOCK_TYPE) return SCE_KERNEL_ERROR_EINVAL; (*attr)->type = type; return SCE_OK; } diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index 4101673e79..3268744271 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -549,6 +549,18 @@ bool GetLiveGuestThreadAffinity_nid_no_patch(std::int64_t tid, KernelCpumask& ma return false; } +bool GetLiveGuestThreadSched_nid_no_patch(std::int64_t tid, int& policy, int& priority) { + auto& registry = CpuClocks(); + std::lock_guard lock(registry.lock); + for (const auto& [id, thread] : registry.threads) { + if (thread->tid != tid || thread->_finished.load(std::memory_order_acquire)) continue; + policy = thread->schedPolicy.load(std::memory_order_relaxed); + priority = thread->priority.load(std::memory_order_relaxed); + return true; + } + return false; +} + bool SetLiveGuestThreadAffinity_nid_no_patch(std::int64_t tid, KernelCpumask mask) { auto& registry = CpuClocks(); std::lock_guard lock(registry.lock); diff --git a/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp b/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp index 4810846552..1e2d388a5c 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp @@ -17,13 +17,18 @@ struct SocketOptionState { int ipTypeOfService = 0; int multicastTimeToLive = 1; bool multicastLoop = true; + int tcpMaximumSegment = 536; + unsigned int tcpKeepIdle = 0; + unsigned int tcpKeepInterval = 0; + unsigned int tcpKeepCount = 0; + int keptFlags = 0; }; bool NativeListening(std::uintptr_t native, bool& listening); int SetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int family, int type, int option, const void* value, std::uint32_t length); int GetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int family, int type, int option, void* value, std::uint32_t* length); -int SetTcpLevelOption(std::uintptr_t native, bool tcp, int option, const void* value, std::uint32_t length); -int GetTcpLevelOption(std::uintptr_t native, bool tcp, int option, void* value, std::uint32_t* length); +int SetTcpLevelOption(std::uintptr_t native, SocketOptionState& state, bool tcp, int option, const void* value, std::uint32_t length); +int GetTcpLevelOption(std::uintptr_t native, SocketOptionState& state, bool tcp, int option, void* value, std::uint32_t* length); int SetIpLevelOption(std::uintptr_t native, SocketOptionState& state, bool ipv4, int option, const void* value, std::uint32_t length); int GetIpLevelOption(SocketOptionState& state, bool ipv4, int option, void* value, std::uint32_t* length); int SetIpv6LevelOption(std::uintptr_t native, bool ipv6, int option, const void* value, std::uint32_t length); diff --git a/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp b/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp index 98224a19bf..b31cd963ac 100644 --- a/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp +++ b/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp @@ -40,7 +40,19 @@ constexpr int GuestNonBlocking = 0x1200; constexpr int GuestNonBlockingState = 0x100; constexpr int GuestUnix = 1; constexpr int GuestTcpNoDelay = 1; +constexpr int GuestReusePort = 0x200; +constexpr int KeptFlags = 0x1 | GuestReuseAddress | 0x40 | GuestReusePort | 0x400 | 0x2000 | 0x10000; +constexpr int WriteOnlyFlags = 0x4000 | 0x8000; constexpr int GuestTcpNoDelayFlag = 4; +constexpr int GuestTcpMaximumSegment = 2; +constexpr int GuestTcpKeepIdle = 0x100; +constexpr int GuestTcpKeepInterval = 0x200; +constexpr int GuestTcpKeepCount = 0x400; +constexpr int MinimumSegment = 216; +constexpr unsigned int TicksPerSecond = 1000; +constexpr unsigned int DefaultKeepIdle = 7200; +constexpr unsigned int DefaultKeepInterval = 75; +constexpr unsigned int DefaultKeepCount = 8; constexpr int GuestIpv6Only = 27; constexpr int GuestIpTypeOfService = 3; constexpr int GuestIpTimeToLive = 4; @@ -89,7 +101,6 @@ int HostError() { return GuestErrorFromHost(errno); } int NativeBoolean(int option) { switch (option) { - case GuestReuseAddress: return SO_REUSEADDR; case GuestKeepAlive: return SO_KEEPALIVE; case GuestDontRoute: return SO_DONTROUTE; case GuestBroadcast: return SO_BROADCAST; @@ -193,6 +204,25 @@ int SetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int fa if (::setsockopt(handle, SOL_SOCKET, NativeBoolean(option), reinterpret_cast(&enabled), sizeof(enabled))) return HostError(); return 0; } + if ((option & (KeptFlags | WriteOnlyFlags)) != 0 && (option & (option - 1)) == 0) { + if (length < sizeof(int)) return ErrorInvalid; + int number; + std::memcpy(&number, value, sizeof(number)); + if ((option & KeptFlags) == 0) return 0; + const int flags = number != 0 ? state.keptFlags | option : state.keptFlags & ~option; + if (option == GuestReuseAddress || option == GuestReusePort) { +#ifdef _WIN32 + const int reuseAddress = (flags & (GuestReuseAddress | GuestReusePort)) != 0 ? 1 : 0; +#else + const int reuseAddress = (flags & GuestReuseAddress) != 0 ? 1 : 0; + const int reusePort = (flags & GuestReusePort) != 0 ? 1 : 0; + if (::setsockopt(handle, SOL_SOCKET, SO_REUSEPORT, reinterpret_cast(&reusePort), sizeof(reusePort))) return HostError(); +#endif + if (::setsockopt(handle, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast(&reuseAddress), sizeof(reuseAddress))) return HostError(); + } + state.keptFlags = flags; + return 0; + } return ErrorNoProtocolOption; } @@ -235,6 +265,8 @@ int GetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int fa number = listening ? GuestAcceptConnection : 0; } else if (option == GuestType) { number = type; + } else if ((option & KeptFlags) != 0 && (option & (option - 1)) == 0) { + number = state.keptFlags & option; } else if (option == GuestNoSigpipe) { number = state.noSignal ? GuestNoSigpipe : 0; } else if (option == GuestNonBlocking) { @@ -258,19 +290,43 @@ int GetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int fa return Copy(value, length, &number, sizeof(number)); } -int SetTcpLevelOption(std::uintptr_t native, bool tcp, int option, const void* value, std::uint32_t length) { +int SetTcpLevelOption(std::uintptr_t native, SocketOptionState& state, bool tcp, int option, const void* value, std::uint32_t length) { if (!tcp) return ErrorInvalid; - if (option != GuestTcpNoDelay) return ErrorNoProtocolOption; + if (option != GuestTcpNoDelay && option != GuestTcpMaximumSegment && option != GuestTcpKeepIdle && option != GuestTcpKeepInterval && + option != GuestTcpKeepCount) + return ErrorNoProtocolOption; if (length < sizeof(int)) return ErrorInvalid; int number; std::memcpy(&number, value, sizeof(number)); + const auto unsignedNumber = static_cast(number); + if (option == GuestTcpMaximumSegment) { + if (number <= 0 || number > state.tcpMaximumSegment || number + 40 < MinimumSegment) return ErrorInvalid; + state.tcpMaximumSegment = number; + return 0; + } + if (option == GuestTcpKeepCount) { + state.tcpKeepCount = unsignedNumber; + return 0; + } + if (option == GuestTcpKeepIdle || option == GuestTcpKeepInterval) { + if (unsignedNumber > UINT32_MAX / TicksPerSecond) return ErrorInvalid; + (option == GuestTcpKeepIdle ? state.tcpKeepIdle : state.tcpKeepInterval) = unsignedNumber; + return 0; + } const int enabled = number != 0 ? 1 : 0; if (::setsockopt(static_cast(native), IPPROTO_TCP, TCP_NODELAY, reinterpret_cast(&enabled), sizeof(enabled))) return HostError(); return 0; } -int GetTcpLevelOption(std::uintptr_t native, bool tcp, int option, void* value, std::uint32_t* length) { +int GetTcpLevelOption(std::uintptr_t native, SocketOptionState& state, bool tcp, int option, void* value, std::uint32_t* length) { if (!tcp) return ErrorInvalid; + if (option == GuestTcpMaximumSegment || option == GuestTcpKeepIdle || option == GuestTcpKeepInterval || option == GuestTcpKeepCount) { + const unsigned int number = option == GuestTcpMaximumSegment ? static_cast(state.tcpMaximumSegment) + : option == GuestTcpKeepIdle ? (state.tcpKeepIdle != 0 ? state.tcpKeepIdle : DefaultKeepIdle) + : option == GuestTcpKeepInterval ? (state.tcpKeepInterval != 0 ? state.tcpKeepInterval : DefaultKeepInterval) + : (state.tcpKeepCount != 0 ? state.tcpKeepCount : DefaultKeepCount); + return Copy(value, length, &number, sizeof(number)); + } if (option != GuestTcpNoDelay) return ErrorNoProtocolOption; int enabled = 0; socklen_t size = sizeof(enabled); diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 5b9b3bb9b0..f556c5e3d8 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -1149,7 +1149,7 @@ int APS5_VABI setsockopt_nid_postfix(int descriptor, int level, int option, std::lock_guard lock(socket->modeMutex); const auto native = static_cast(socket->value); const int error = level == 0 ? GuestSockets::SetIpLevelOption(native, socket->options, socket->family == 2, option, value, length) - : level == 6 ? GuestSockets::SetTcpLevelOption(native, socket->family != GuestUnix && socket->type == 1, option, value, length) + : level == 6 ? GuestSockets::SetTcpLevelOption(native, socket->options, socket->family != GuestUnix && socket->type == 1, option, value, length) : level == 41 ? GuestSockets::SetIpv6LevelOption(native, socket->family == 28, option, value, length) : GuestSockets::SetSocketLevelOption(native, socket->options, socket->family, socket->type, option, value, length); return error == 0 ? 0 : Fail(error); @@ -1164,7 +1164,7 @@ int APS5_VABI getsockopt_nid_postfix(int descriptor, int level, int option, std::lock_guard lock(socket->modeMutex); const auto native = static_cast(socket->value); const int error = level == 0 ? GuestSockets::GetIpLevelOption(socket->options, socket->family == 2, option, value, length) - : level == 6 ? GuestSockets::GetTcpLevelOption(native, socket->family != GuestUnix && socket->type == 1, option, value, length) + : level == 6 ? GuestSockets::GetTcpLevelOption(native, socket->options, socket->family != GuestUnix && socket->type == 1, option, value, length) : level == 41 ? GuestSockets::GetIpv6LevelOption(native, socket->family == 28, option, value, length) : GuestSockets::GetSocketLevelOption(native, socket->options, socket->family, socket->type, option, value, length); return error == 0 ? 0 : Fail(error); @@ -1299,6 +1299,7 @@ int APS5_VABI accept_nid_postfix(int descriptor, void* address, std::uint32_t* l std::lock_guard modeLock(listener->modeMutex); inheritNonblocking = listener->options.nonblocking; accepted->options.noSignal = listener->options.noSignal; + accepted->options.keptFlags = listener->options.keptFlags; accepted->options.sendTimeout = listener->options.sendTimeout; accepted->options.receiveTimeout = listener->options.receiveTimeout; accepted->options.sendBuffer = listener->options.sendBuffer; @@ -1307,6 +1308,9 @@ int APS5_VABI accept_nid_postfix(int descriptor, void* address, std::uint32_t* l accepted->options.receiveLowWater = listener->options.receiveLowWater; accepted->options.ipTimeToLive = listener->options.ipTimeToLive; accepted->options.ipTypeOfService = listener->options.ipTypeOfService; + accepted->options.tcpKeepIdle = listener->options.tcpKeepIdle; + accepted->options.tcpKeepInterval = listener->options.tcpKeepInterval; + accepted->options.tcpKeepCount = listener->options.tcpKeepCount; } if (inheritNonblocking && SetNonBlocking(*accepted)) return -1; const int acceptedDescriptor = Install(std::move(accepted)); diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 61b3fa2bab..397b42409f 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -58,6 +58,7 @@ extern "C" int APS5_VABI sceKernelVirtualQuery(const void* addr, int flags, Virt namespace { constexpr int sceInvalidArgument = static_cast(0x80020016u); +constexpr int errnoOperationNotPermitted = 1; constexpr int errnoNoSuchProcess = 3; constexpr int errnoNoChild = 10; constexpr int errnoBadAddress = 14; @@ -344,6 +345,24 @@ int APS5_VABI getsid_nid_postfix(int pid) { return -1; } +int APS5_VABI setpgid_nid_postfix(int pid, int pgid) { + if (pgid < 0) { + *__error_nid_postfix() = errnoInvalidArgument; + return -1; + } + const int self = getpid_nid_postfix(); + if (pid != 0 && pid != self) { + *__error_nid_postfix() = errnoNoSuchProcess; + return -1; + } + *__error_nid_postfix() = errnoOperationNotPermitted; + return -1; +} + +int APS5_VABI setpgrp_nid_postfix(int pid, int pgrp) { + return setpgid_nid_postfix(pid, pgrp); +} + int APS5_VABI getppid_nid_postfix(void) { #ifdef _WIN32 const DWORD self = GetCurrentProcessId(); diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index 63bf4d14a2..1a281703a4 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -681,3 +681,12 @@ int APS5_VABI _is_signal_return_nid_postfix(std::uint64_t programCounter) { } } + +namespace { +struct RegisterSignalsOnLoad { + RegisterSignalsOnLoad() { + RegisterSigaction_nid_no_patch(reinterpret_cast(sigaction_nid_postfix), _sigintr_nid_postfix.bits); + } +}; +const RegisterSignalsOnLoad gRegisterSignals; +} diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index 8d80547b6a..5d1a9f5a80 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -364,6 +364,24 @@ __attribute__((noinline)) static void APS5_VABI RunChecks() { } char narrow[8] = {}; Require(sscanf_nid_postfix("ab%", "%7[^%ls]", narrow) == 1 && std::strcmp(narrow, "ab") == 0); +#endif +#ifdef _WIN32 + char freebsd[64]; + Require(snprintf_nid_postfix(freebsd, sizeof(freebsd), "%qd|%qu|%qx|%qo") == 15 && + std::strcmp(freebsd, "%qd|%qu|%qx|%qo") == 0); + Require(snprintf_nid_postfix(freebsd, sizeof(freebsd), "%qd|%d", 7) == 5 && std::strcmp(freebsd, "%qd|7") == 0); + Require(snprintf_nid_postfix(freebsd, sizeof(freebsd), "%D|%hD|%O|%U|%jD") == 14 && + std::strcmp(freebsd, "%D|%D|%O|%U|%D") == 0); + Require(snprintf_nid_postfix(freebsd, sizeof(freebsd), "[%5%][%-5%][%y][%5y][%-3y][%hhz]") == 34 && + std::strcmp(freebsd, "[ %][% ][%y][ %y][%y ][%z]") == 0); + for (const char* unsupported : {"%+y", "%#y", "%05y", "%.2y"}) { + bool threw = false; + try { snprintf_nid_postfix(freebsd, sizeof(freebsd), unsupported, 1); } catch (const std::invalid_argument&) { threw = true; } + Require(threw); + } + bool incomplete = false; + try { snprintf_nid_postfix(freebsd, sizeof(freebsd), "100%"); } catch (const std::invalid_argument&) { incomplete = true; } + Require(incomplete); #endif long long count = -1; int smallCount = -1; diff --git a/core/libs/tests/GuestDinkumwareInternals.cpp b/core/libs/tests/GuestDinkumwareInternals.cpp index aa660dabdf..563d08d8cc 100644 --- a/core/libs/tests/GuestDinkumwareInternals.cpp +++ b/core/libs/tests/GuestDinkumwareInternals.cpp @@ -4,11 +4,21 @@ #include #include #include +#include extern "C" { int* APS5_VABI __error_nid_postfix(); double APS5_VABI _Stod_nid_postfix(const char* text, char** end, std::int64_t pten); long long APS5_VABI _Stoll_nid_postfix(const char* text, char** end, int base); +double APS5_VABI _Stodx_nid_postfix(const char* text, char** end, std::int64_t pten, int* error); +float APS5_VABI _Stofx_nid_postfix(const char* text, char** end, std::int64_t pten, int* error); +long double APS5_VABI _Stoldx_nid_postfix(const char* text, char** end, std::int64_t pten, int* error); +float APS5_VABI _Stof_nid_postfix(const char* text, char** end, std::int64_t pten); +long double APS5_VABI _Stold_nid_postfix(const char* text, char** end, std::int64_t pten); +long long APS5_VABI _Stollx_nid_postfix(const char* text, char** end, int base, int* error); +std::int64_t APS5_VABI _Stolx_nid_postfix(const char* text, char** end, int base, int* error); +std::uint64_t APS5_VABI _Stoulx_nid_postfix(const char* text, char** end, int base, int* error); +unsigned long long APS5_VABI _Stoullx_nid_postfix(const char* text, char** end, int base, int* error); double APS5_VABI _Sin_nid_postfix(double x, unsigned int quadrant); float APS5_VABI _FSin_nid_postfix(float x, unsigned int quadrant); long double APS5_VABI _LSin_nid_postfix(long double x, unsigned int quadrant); @@ -19,6 +29,7 @@ float APS5_VABI _FLog_nid_postfix(float x, int base); namespace { constexpr int RangeError = 34; +constexpr int InvalidArgument = 22; void Require(bool condition, const char* message) { if (!condition) { @@ -71,6 +82,101 @@ void TestStoll() { Require(_Stoll_nid_postfix("9223372036854775808", &end, 10) == 9223372036854775807ll && *__error_nid_postfix() == RangeError, "_Stoll saturates"); } +void TestFloatErrors() { + char* end = nullptr; + int error = 99; + *__error_nid_postfix() = 0; + Require(_Stodx_nid_postfix("1.5", &end, 2, &error) == 150.0 && error == 0 && *__error_nid_postfix() == 0, "_Stodx in range"); + error = 99; + Require(std::isinf(_Stodx_nid_postfix("1e400", &end, 0, &error)) && error == 1 && *__error_nid_postfix() == RangeError, "_Stodx overflow"); + error = 99; + *__error_nid_postfix() = 0; + Require(_Stodx_nid_postfix("1e-400", &end, 2, &error) == 0.0 && error == 1 && *__error_nid_postfix() == RangeError, "_Stodx underflow"); + error = 99; + *__error_nid_postfix() = 7; + Require(_Stodx_nid_postfix("1e-400", &end, 300, &error) == 1e-100 && error == 0 && *__error_nid_postfix() == 7, "_Stodx rescued by its scale"); + error = 99; + *__error_nid_postfix() = RangeError; + Require(_Stodx_nid_postfix("2.5", &end, 1, &error) == 25.0 && error == 0 && *__error_nid_postfix() == RangeError, "_Stodx kept a preset errno"); + error = 99; + *__error_nid_postfix() = 0; + Require(std::isinf(_Stofx_nid_postfix("1e39", &end, 0, &error)) && error == 1 && *__error_nid_postfix() == RangeError, "_Stofx overflow"); + error = 99; + *__error_nid_postfix() = 0; + Require(_Stofx_nid_postfix("2.5e-1", &end, 3, &error) == 250.0f && error == 0 && *__error_nid_postfix() == 0, "_Stofx scaled"); + error = 99; + *__error_nid_postfix() = 0; + Require(_Stofx_nid_postfix("1e39", &end, -1, &error) == 1e38f && error == 0 && *__error_nid_postfix() == 0, "_Stofx overflow scaled back into range"); + error = 99; + Require(_Stodx_nid_postfix("1e400", &end, -100, &error) == 1e300 && error == 0 && *__error_nid_postfix() == 0, "_Stodx overflow scaled back into range"); + error = 99; + *__error_nid_postfix() = 0; + Require(_Stoldx_nid_postfix("1e400", &end, 0, &error) == 1e400L && error == 0 && *__error_nid_postfix() == 0, "_Stoldx uses the extended range"); + error = 99; + *__error_nid_postfix() = 0; + Require(_Stodx_nid_postfix("1e-310", &end, 0, &error) == 1e-310 && error == 0 && *__error_nid_postfix() == 0 && end[0] == '\0', "_Stodx subnormal"); + error = 99; + Require(_Stodx_nid_postfix("1e-300", &end, -10, &error) == 1e-310 && error == 0 && *__error_nid_postfix() == 0, "_Stodx scaled into the subnormal range"); + error = 99; + *__error_nid_postfix() = 7; + Require(_Stodx_nid_postfix("1e-310", &end, 0, &error) == 1e-310 && error == 0 && *__error_nid_postfix() == 7, "_Stodx subnormal kept a preset errno"); + error = 99; + *__error_nid_postfix() = 0; + Require(_Stodx_nid_postfix("2e-324", &end, 0, &error) == 0.0 && error == 1 && *__error_nid_postfix() == RangeError, "_Stodx below the smallest subnormal"); + error = 99; + *__error_nid_postfix() = 0; + Require(_Stofx_nid_postfix("1e-45", &end, 0, &error) == 0x1p-149f && error == 0 && *__error_nid_postfix() == 0 && end[0] == '\0', "_Stofx inexact subnormal"); + error = 99; + Require(_Stofx_nid_postfix("1e-35", &end, -10, &error) == 0x1p-149f && error == 0 && *__error_nid_postfix() == 0, "_Stofx scaled into the subnormal range"); + error = 99; + *__error_nid_postfix() = 7; + const float subnormal = _Stofx_nid_postfix("1e-40", &end, 0, &error); + Require(subnormal > 0x1p-134f && subnormal < 0x1p-132f && error == 0 && *__error_nid_postfix() == 7, "_Stofx subnormal kept a preset errno"); + error = 99; + *__error_nid_postfix() = 0; + Require(_Stofx_nid_postfix("1e-46", &end, 0, &error) == 0.0f && error == 1 && *__error_nid_postfix() == RangeError, "_Stofx underflow"); + error = 99; + *__error_nid_postfix() = 0; + Require(_Stofx_nid_postfix("1.17549435e-38", &end, 0, &error) == 0x1p-126f && error == 0 && *__error_nid_postfix() == 0, "_Stofx smallest normal"); + error = 99; + Require(_Stoldx_nid_postfix("1e-4940", &end, 0, &error) == 1e-4940L && error == 0 && *__error_nid_postfix() == 0, "_Stoldx subnormal"); + error = 99; + Require(_Stoldx_nid_postfix("1e-4930", &end, -10, &error) == 1e-4940L && error == 0 && *__error_nid_postfix() == 0, "_Stoldx scaled into the subnormal range"); + error = 99; + Require(_Stoldx_nid_postfix("1e-5000", &end, 0, &error) == 0.0L && error == 1 && *__error_nid_postfix() == RangeError, "_Stoldx underflow"); + Require(_Stof_nid_postfix("1.5", &end, 1) == 15.0f, "_Stof"); + Require(_Stold_nid_postfix("1.5", &end, -1) == 0.15L, "_Stold"); + error = 99; + Require(std::isnan(_Stodx_nid_postfix("nan", &end, 2, &error)) && error == 0 && end[0] == '\0', "_Stodx NaN"); +} + +void TestIntegerErrors() { + char* end = nullptr; + int error = 99; + *__error_nid_postfix() = 5; + Require(_Stollx_nid_postfix("123", &end, 0, &error) == 123 && error == 0 && *__error_nid_postfix() == 5, "_Stollx in range"); + Require(_Stollx_nid_postfix("9223372036854775808", &end, 10, &error) == 9223372036854775807ll && error == RangeError && *__error_nid_postfix() == RangeError, "_Stollx overflow"); + *__error_nid_postfix() = 5; + const char* text = " 12"; + for (int base : {1, -1, 37}) { + error = 99; + Require(_Stollx_nid_postfix(text, &end, base, &error) == 0 && error == InvalidArgument && end == text && *__error_nid_postfix() == 5, "_Stollx invalid base"); + error = 99; + Require(_Stolx_nid_postfix(text, &end, base, &error) == 0 && error == InvalidArgument && end == text && *__error_nid_postfix() == 5, "_Stolx invalid base"); + } + Require(_Stoll_nid_postfix(text, &end, 1) == 0 && end == text && *__error_nid_postfix() == 5, "_Stoll invalid base"); + error = 99; + *__error_nid_postfix() = 0; + Require(_Stolx_nid_postfix("9223372036854775808", &end, 0, &error) == 9223372036854775807ll && error == RangeError && *__error_nid_postfix() == 0, "_Stolx overflow inside the unsigned range"); + Require(_Stolx_nid_postfix("-18446744073709551615", &end, 10, &error) == (-9223372036854775807ll - 1) && error == RangeError && *__error_nid_postfix() == 0, "_Stolx negative overflow inside the unsigned range"); + Require(_Stolx_nid_postfix("18446744073709551616", &end, 10, &error) == 9223372036854775807ll && error == RangeError && *__error_nid_postfix() == RangeError, "_Stolx overflow past the unsigned range"); + error = 99; + *__error_nid_postfix() = 0; + Require(_Stoulx_nid_postfix("-1", &end, 0, &error) == 18446744073709551615ull && error == 0, "_Stoulx wraps a negative value"); + Require(_Stoullx_nid_postfix("-18446744073709551615", &end, 10, &error) == 1 && error == 0 && *__error_nid_postfix() == 0, "_Stoullx wraps a negated maximum"); + Require(_Stoullx_nid_postfix("18446744073709551616", &end, 10, &error) == 18446744073709551615ull && error == RangeError && *__error_nid_postfix() == RangeError, "_Stoullx overflow"); +} + void TestDispatch() { Require(_Sin_nid_postfix(0.5, 0) == std::sin(0.5) && _Sin_nid_postfix(0.5, 1) == std::cos(0.5) && _Sin_nid_postfix(0.5, 7) == std::cos(0.5), "_Sin"); Require(_FSin_nid_postfix(0.5f, 0) == std::sin(0.5f) && _FSin_nid_postfix(0.5f, 2) == std::cos(0.5f), "_FSin"); @@ -84,5 +190,7 @@ void TestDispatch() { int main() { TestStod(); TestStoll(); + TestFloatErrors(); + TestIntegerErrors(); TestDispatch(); } diff --git a/core/libs/tests/GuestDup.cpp b/core/libs/tests/GuestDup.cpp index 0a8016a79b..9d0029e2ca 100644 --- a/core/libs/tests/GuestDup.cpp +++ b/core/libs/tests/GuestDup.cpp @@ -104,6 +104,8 @@ static void BadDescriptors() { #ifdef _WIN32 const auto previous = _set_invalid_parameter_handler(RecordInvalidParameter); #endif + Require(dup2_nid_postfix(pipe[0], closed) == closed); + Require(close_nid_postfix(closed) == 0); RequireFailure(close_nid_postfix(closed), GuestEbadf); RequireFailure(close_nid_postfix(-1), GuestEbadf); #ifdef _WIN32 diff --git a/core/libs/tests/GuestInitOnce.cpp b/core/libs/tests/GuestInitOnce.cpp new file mode 100644 index 0000000000..0a33705835 --- /dev/null +++ b/core/libs/tests/GuestInitOnce.cpp @@ -0,0 +1,53 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI __std_init_once_begin_initialize_clr_nid_postfix(void** once, unsigned long flags, int* pending, void** context); +int APS5_VABI __std_init_once_complete_clr_nid_postfix(void** once, unsigned long flags, void* context); +} + +namespace { + +constexpr unsigned long InitFailed = 4; + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "init once: %s\n", message); + std::abort(); +} + +int Begin(void** once) { + int pending = -1; + Require(__std_init_once_begin_initialize_clr_nid_postfix(once, 0, &pending, nullptr) == 1, "begin returns 1"); + return pending; +} + +} + +int main() { + alignas(8) void* once = nullptr; + Require(Begin(&once) == 1, "the first caller runs the initialization"); + Require(reinterpret_cast(once) == 1, "a running initialization marks the object"); + Require(__std_init_once_complete_clr_nid_postfix(&once, InitFailed, nullptr) == 1, "complete a failed initialization"); + Require(once == nullptr, "a failed initialization resets the object"); + Require(Begin(&once) == 1, "a caller after a failure runs the initialization again"); + + std::atomic waiterDone{false}; + std::atomic waiterPending{-1}; + std::thread waiter([&] { + waiterPending = Begin(&once); + waiterDone = true; + }); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + Require(!waiterDone, "a second caller waits while the initialization runs"); + Require(__std_init_once_complete_clr_nid_postfix(&once, 0, nullptr) == 1, "complete the initialization"); + waiter.join(); + Require(waiterPending == 0, "the waiting caller sees the initialization done"); + Require(reinterpret_cast(once) == 2, "a completed initialization marks the object"); + Require(Begin(&once) == 0, "a later caller does not run the initialization"); + return 0; +} diff --git a/core/libs/tests/GuestLocale.cpp b/core/libs/tests/GuestLocale.cpp index 129dd95f43..863371bf9c 100644 --- a/core/libs/tests/GuestLocale.cpp +++ b/core/libs/tests/GuestLocale.cpp @@ -1,6 +1,8 @@ +#include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/GuestLocale.hpp" #include #include +#include #include #include #include @@ -36,9 +38,14 @@ std::uint32_t APS5_VABI _Towctrans_nid_postfix(std::uint32_t wc, std::uint64_t d int* APS5_VABI __error_nid_postfix(); extern std::uint64_t _ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix; extern std::uint64_t _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix; +extern std::uint64_t _ZNSt8time_putIcSt19ostreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix; extern std::uintptr_t _ZTVSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE_nid_postfix[17]; extern std::uintptr_t _ZTVSt7codecvtIwc9_MbstatetE_nid_postfix[13]; extern std::uintptr_t _ZTVSt7num_putIwSt19ostreambuf_iteratorIwSt11char_traitsIwEEE_nid_postfix[14]; +std::size_t APS5_VABI _ZNSt7codecvtIcc9_MbstatetE7_GetcatEPPKNSt6locale5facetEPKS2__nid_postfix(GuestLocale::Facet** facet, const GuestLocale::Implementation* const* locale); +std::size_t APS5_VABI _ZNSt8numpunctIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(GuestLocale::Facet** facet, const GuestLocale::Implementation* const* locale); +std::size_t APS5_VABI _ZNSt7num_putIcSt19ostreambuf_iteratorIcSt11char_traitsIcEEE7_GetcatEPPKNSt6locale5facetEPKS5__nid_postfix(GuestLocale::Facet** facet, const GuestLocale::Implementation* const* locale); +std::size_t APS5_VABI _ZNSt8time_putIcSt19ostreambuf_iteratorIcSt11char_traitsIcEEE7_GetcatEPPKNSt6locale5facetEPKS5__nid_postfix(GuestLocale::Facet** facet, const GuestLocale::Implementation* const* locale); } @@ -46,6 +53,17 @@ static void Require(bool condition) { if (!condition) throw std::runtime_error("Guest locale check failed"); } +void* APS5_VABI TestHeapAllocate(std::size_t bytes) { return std::malloc(bytes); } +void APS5_VABI TestHeapFree(void* pointer) { std::free(pointer); } +void* APS5_VABI TestHeapCalloc(std::size_t count, std::size_t bytes) { return std::calloc(count, bytes); } +void* APS5_VABI TestHeapReallocate(void* pointer, std::size_t bytes) { return std::realloc(pointer, bytes); } +void* APS5_VABI TestHeapAlign(std::size_t, std::size_t bytes) { return std::malloc(bytes); } +void* APS5_VABI TestHeapRealign(void* pointer, std::size_t bytes, std::size_t) { return std::realloc(pointer, bytes); } +int APS5_VABI TestHeapPosixAlign(void** pointer, std::size_t, std::size_t bytes) { + *pointer = std::malloc(bytes); + return *pointer == nullptr ? 12 : 0; +} + template static void RequireException(TAction action) { bool caught = false; @@ -273,7 +291,60 @@ static void CheckWideNumPutVtableObject() { } } +static void CheckFacetGetcat() { + const GuestLocale::Implementation* locale = _ZSt21_sceLibcClassicLocale_nid_postfix; + GuestLocale::Facet* codecvt = nullptr; + Require(_ZNSt7codecvtIcc9_MbstatetE7_GetcatEPPKNSt6locale5facetEPKS2__nid_postfix(&codecvt, &locale) == 2 && codecvt != nullptr); + auto* codecvtVtable = reinterpret_cast(codecvt->vtable); + Require(codecvtVtable->alwaysNoconv(codecvt) && codecvtVtable->encoding(codecvt) == 1 && codecvtVtable->maxLength(codecvt) == 1); + const char* nextFrom = nullptr; + char target[4]{}; + char* nextTo = nullptr; + Require(codecvtVtable->out(codecvt, nullptr, "ab", "ab" + 2, &nextFrom, target, target + 4, &nextTo) == 3); + Require(nextFrom == "ab" && nextTo == target); + Require(codecvtVtable->in(codecvt, nullptr, "ab", "ab" + 2, &nextFrom, target, target + 4, &nextTo) == 3); + nextTo = reinterpret_cast(0x10); + Require(codecvtVtable->unshift(codecvt, nullptr, nullptr, nullptr, &nextTo) == 0 && nextTo == nullptr); + Require(codecvtVtable->length(codecvt, nullptr, "ab", "ab" + 2, 5) == 2); + Require(codecvtVtable->length(codecvt, nullptr, "ab", "ab" + 2, 1) == 1); + GuestLocale::Facet* punct = nullptr; + Require(_ZNSt8numpunctIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(&punct, &locale) == 4 && punct != nullptr); + auto* punctVtable = reinterpret_cast(punct->vtable); + auto* punctFacet = reinterpret_cast(punct); + Require(punctVtable->decimalPoint(punctFacet) == '.' && punctVtable->thousandsSep(punctFacet) == ','); + GuestLocale::String name{}; + Require(punctVtable->grouping(&name, punctFacet) == &name && name.size == 0); + const char* contents = name.size <= 15 ? name.buffer : name.pointer; + Require(punctVtable->falsename(&name, punctFacet)->size == 5 && std::memcmp(contents, "false", 5) == 0); + contents = name.size <= 15 ? name.buffer : name.pointer; + Require(punctVtable->truename(&name, punctFacet)->size == 4 && std::memcmp(contents, "true", 4) == 0); + GuestLocale::Facet* numPut = nullptr; + Require(_ZNSt7num_putIcSt19ostreambuf_iteratorIcSt11char_traitsIcEEE7_GetcatEPPKNSt6locale5facetEPKS5__nid_postfix(&numPut, &locale) == 4 && numPut != nullptr); + GuestLocale::Facet* timePut = nullptr; + Require(_ZNSt8time_putIcSt19ostreambuf_iteratorIcSt11char_traitsIcEEE7_GetcatEPPKNSt6locale5facetEPKS5__nid_postfix(&timePut, &locale) == 5 && timePut != nullptr); + _ZNSt8time_putIcSt19ostreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 11; + Require(_ZNSt8time_putIcSt19ostreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix == 11); + _ZNSt8time_putIcSt19ostreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix = 0; + for (GuestLocale::Facet* facet : {codecvt, punct, numPut, timePut}) { + facet->vtable->retain(facet); + facet->vtable->retain(facet); + Require(facet->references == 2); + facet->vtable->deleteObject(facet); + } + Require(_ZNSt7codecvtIcc9_MbstatetE7_GetcatEPPKNSt6locale5facetEPKS2__nid_postfix(nullptr, &locale) == 2); + Require(_ZNSt8numpunctIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(&punct, &locale) == 4); +} + int main() { + std::array heapApi{}; + heapApi[0] = reinterpret_cast(&TestHeapAllocate); + heapApi[1] = reinterpret_cast(&TestHeapFree); + heapApi[2] = reinterpret_cast(&TestHeapCalloc); + heapApi[3] = reinterpret_cast(&TestHeapReallocate); + heapApi[4] = reinterpret_cast(&TestHeapAlign); + heapApi[5] = reinterpret_cast(&TestHeapRealign); + heapApi[6] = reinterpret_cast(&TestHeapPosixAlign); + ApplicationHeapRegister_nid_no_patch(heapApi.data()); CheckGuestCalls(); CheckLocinfoAlignment(); CheckCharacterTables(); @@ -285,6 +356,7 @@ int main() { CheckInputFacetObjects(); CheckCodecvtWideVtableObject(); CheckWideNumPutVtableObject(); + CheckFacetGetcat(); _Locksyslock_nid_postfix(); _Locksyslock_nid_postfix(); _Unlocksyslock_nid_postfix(); diff --git a/core/libs/tests/GuestNamedShmWindows.cpp b/core/libs/tests/GuestNamedShmWindows.cpp new file mode 100644 index 0000000000..f159ac1ee8 --- /dev/null +++ b/core/libs/tests/GuestNamedShmWindows.cpp @@ -0,0 +1,625 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/File/include/SharedMemory.hpp" +#include "nid/Sha1.hpp" +#include +#include +#include +#include +#include +#include +#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 shm_unlink_nid_postfix(const char*); +int APS5_VABI close_nid_postfix(int); +int APS5_VABI dup_nid_postfix(int); +int APS5_VABI dup2_nid_postfix(int, int); +std::int64_t APS5_VABI lseek_nid_postfix(int, std::int64_t, int); +int APS5_VABI ftruncate_nid_postfix(int, std::int64_t); +int APS5_VABI fchmod_nid_postfix(int, int); +std::int64_t APS5_VABI pread_nid_postfix(int, void*, std::size_t, std::int64_t); +std::int64_t APS5_VABI pwrite_nid_disambig1_nid_postfix(int, const void*, std::size_t, std::int64_t); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { +constexpr int ReadOnly = 0; +constexpr int ReadWrite = 2; +constexpr int Create = 0x200; +constexpr int Exclusive = 0x800; +constexpr std::int64_t RaceSize = 4096; + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(std::string(message) + " (guest errno=" + std::to_string(*__error_nid_postfix()) + ")"); +} + +void RequireError(int descriptor, int expected, const char* message) { + Require(descriptor == -1 && *__error_nid_postfix() == expected, message); +} + +void RequireNoAdministratorsAccess(HANDLE handle) { + PACL dacl = nullptr; + PSECURITY_DESCRIPTOR descriptor = nullptr; + const DWORD result = ::GetSecurityInfo(handle, SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, + nullptr, nullptr, &dacl, nullptr, &descriptor); + if (result != ERROR_SUCCESS || dacl == nullptr) { + if (descriptor != nullptr) ::LocalFree(descriptor); + Require(false, "reading named shared-memory object permissions failed"); + } + SID_IDENTIFIER_AUTHORITY authority = SECURITY_NT_AUTHORITY; + PSID administrators = nullptr; + if (::AllocateAndInitializeSid(&authority, 2, SECURITY_BUILTIN_DOMAIN_RID, + DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &administrators) == FALSE) { + ::LocalFree(descriptor); + Require(false, "building the Administrators SID failed"); + } + ACL_SIZE_INFORMATION information{}; + const BOOL gotInformation = ::GetAclInformation(dacl, &information, sizeof(information), AclSizeInformation); + bool grantsAdministrators = false; + if (gotInformation) { + for (DWORD index = 0; index < information.AceCount; ++index) { + void* rawAce = nullptr; + if (!::GetAce(dacl, index, &rawAce)) continue; + const auto* header = static_cast(rawAce); + if (header->AceType != ACCESS_ALLOWED_ACE_TYPE) continue; + const auto* ace = static_cast(rawAce); + if (::EqualSid(const_cast(&ace->SidStart), administrators) != FALSE) { + grantsAdministrators = true; + break; + } + } + } + ::FreeSid(administrators); + ::LocalFree(descriptor); + Require(gotInformation && !grantsAdministrators, + "named shared-memory object grants access to the Administrators group"); +} + +void RequireNoUnlinkedFiles(const std::string& name) { + const auto digest = Nid::Sha1(std::vector(name.begin(), name.end())); + std::string prefix; + for (const auto byte : digest) { + prefix += "0123456789abcdef"[byte >> 4]; + prefix += "0123456789abcdef"[byte & 15]; + } + const auto directory = std::filesystem::temp_directory_path() / L"AnyPS5" / L"shm"; + if (!std::filesystem::exists(directory)) return; + for (const auto& entry : std::filesystem::directory_iterator(directory)) { + const auto filename = entry.path().filename().string(); + Require(!filename.starts_with(prefix + "-unlinked-"), + "closing the last descriptor did not reap its unlinked host object"); + } +} + +void VerifyUntrackedDescriptor(int descriptor) { + Require(File::IsSharedMemoryDescriptor_nid_no_patch(descriptor), "named descriptor was not tracked"); + std::vector path(32768); + const DWORD length = ::GetFinalPathNameByHandleW(reinterpret_cast(::_get_osfhandle(descriptor)), + path.data(), static_cast(path.size()), FILE_NAME_NORMALIZED | VOLUME_NAME_DOS); + Require(length > 0 && length < path.size(), "locating the tracked file failed"); + const HANDLE handle = ::CreateFileW(path.data(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + Require(handle != INVALID_HANDLE_VALUE, "opening an untracked handle to the named file failed"); + const int untracked = ::_open_osfhandle(reinterpret_cast(handle), _O_RDONLY | _O_BINARY); + if (untracked < 0) ::CloseHandle(handle); + Require(untracked >= 0, "creating an untracked descriptor failed"); + const bool classified = File::IsSharedMemoryDescriptor_nid_no_patch(untracked); + Require(::_close(untracked) == 0, "closing the untracked descriptor failed"); + Require(!classified, "an untracked descriptor entered the named shared-memory path"); + Require(!File::IsSharedMemoryDescriptor_nid_no_patch(untracked), "a closed untracked descriptor was classified as shared memory"); +} + +void VerifyConcurrentDescriptorReuse(const std::string& name) { + std::barrier start(3); + std::vector okay(2, 1); + std::vector workers; + for (int index = 0; index < 2; ++index) { + workers.emplace_back([&, index] { + start.arrive_and_wait(); + for (int iteration = 0; iteration < 64; ++iteration) { + const int descriptor = shm_open_nid_postfix(name.c_str(), ReadWrite, 0); + if (descriptor < 0) { okay[index] = 0; break; } + if (!File::IsSharedMemoryDescriptor_nid_no_patch(descriptor) || + pwrite_nid_disambig1_nid_postfix(descriptor, "x", 1, 64) != 0) okay[index] = 0; + if (close_nid_postfix(descriptor) != 0) okay[index] = 0; + } + }); + } + start.arrive_and_wait(); + for (auto& worker : workers) worker.join(); + Require(okay[0] && okay[1], "concurrent close/open lost named-descriptor tracking or bounded writes"); +} + +bool RunResizeWriteRace(const std::string& name) { + const int descriptor = shm_open_nid_postfix(name.c_str(), ReadWrite, 0); + if (descriptor < 0) return false; + bool okay = true; + for (int iteration = 0; iteration < 300; ++iteration) { + const auto size = (iteration & 1) ? 0 : RaceSize; + if (ftruncate_nid_postfix(descriptor, size) != 0) { okay = false; break; } + if (pwrite_nid_disambig1_nid_postfix(descriptor, "x", 1, RaceSize / 2) < 0) { okay = false; break; } + struct _stat64 status{}; + if (::_fstat64(descriptor, &status) != 0 || (status.st_size != 0 && status.st_size != RaceSize)) { + okay = false; + break; + } + } + close_nid_postfix(descriptor); + return okay; +} + +void VerifyCrossProcessResizeWrite(const std::string& name, int descriptor) { + wchar_t executable[32768]{}; + const DWORD length = ::GetModuleFileNameW(nullptr, executable, static_cast(std::size(executable))); + Require(length > 0 && length < std::size(executable), "locating the Windows shared-memory test executable failed"); + std::vector workers; + for (int index = 0; index < 2; ++index) { + std::wstring command = L"\"" + std::wstring(executable, length) + L"\" --race " + std::wstring(name.begin(), name.end()); + STARTUPINFOW startup{}; + startup.cb = sizeof(startup); + PROCESS_INFORMATION process{}; + Require(::CreateProcessW(nullptr, command.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW, + nullptr, nullptr, &startup, &process) != FALSE, + "starting a cross-process shared-memory race worker failed"); + workers.push_back(process); + } + const bool localSucceeded = RunResizeWriteRace(name); + bool childrenSucceeded = true; + for (const auto& process : workers) { + childrenSucceeded = childrenSucceeded && ::WaitForSingleObject(process.hProcess, 10000) == WAIT_OBJECT_0; + DWORD exitCode = 1; + childrenSucceeded = childrenSucceeded && ::GetExitCodeProcess(process.hProcess, &exitCode) && exitCode == 0; + ::CloseHandle(process.hThread); + ::CloseHandle(process.hProcess); + } + Require(localSucceeded && childrenSucceeded, + "cross-process truncate/write race produced an invalid shared-object size"); + struct _stat64 status{}; + Require(::_fstat64(descriptor, &status) == 0 && (status.st_size == 0 || status.st_size == RaceSize), + "cross-process truncate/write race left an invalid final size"); + Require(shm_unlink_nid_postfix(name.c_str()) == 0, "cross-process race cleanup failed"); +} + +void VerifyResizeReadPosition(const std::string& name) { + constexpr std::size_t payloadSize = 2u << 20; + const int descriptor = shm_open_nid_postfix(name.c_str(), ReadWrite | Create | Exclusive, 0600); + Require(descriptor >= 0 && ftruncate_nid_postfix(descriptor, payloadSize) == 0, + "creating the resize/read object failed"); + const int resizer = dup_nid_postfix(descriptor); + Require(resizer >= 0, "duplicating the resize/read descriptor failed"); + std::vector payload(payloadSize, 'r'); + Require(pwrite_nid_disambig1_nid_postfix(descriptor, payload.data(), payload.size(), 0) == payloadSize && + lseek_nid_postfix(descriptor, 137, 0) == 137, "preparing the resize/read object failed"); + std::barrier start(2); + bool resized = true; + std::thread resizing([&] { + start.arrive_and_wait(); + for (int iteration = 0; iteration < 256; ++iteration) + if (ftruncate_nid_postfix(resizer, payloadSize + ((iteration & 1) ? 4096 : 0)) != 0) resized = false; + }); + start.arrive_and_wait(); + std::vector output(payloadSize); + bool read = true; + for (int iteration = 0; iteration < 32; ++iteration) + if (pread_nid_postfix(descriptor, output.data(), output.size(), 0) != payloadSize || output != payload) read = false; + resizing.join(); + Require(resized && read && lseek_nid_postfix(descriptor, 0, 1) == 137, + "concurrent resize/read changed the shared file position or contents"); + Require(shm_unlink_nid_postfix(name.c_str()) == 0 && close_nid_postfix(descriptor) == 0 && close_nid_postfix(resizer) == 0, + "resize/read cleanup failed"); + RequireNoUnlinkedFiles(name); +} + +void Run() { + const std::string base = "/anyps5-windows-named-" + std::to_string(::GetCurrentProcessId()) + "-" + std::to_string(::GetTickCount64()); + const std::string name = base + "-lifecycle"; + Require(shm_unlink_nid_postfix(name.c_str()) == -1 && *__error_nid_postfix() == 2, + "new test name unexpectedly existed"); + + *__error_nid_postfix() = 73; + const int original = shm_open_nid_postfix(name.c_str(), ReadWrite | Create | Exclusive, 0600); + Require(original >= 0 && *__error_nid_postfix() == 73, "exclusive creation failed or changed guest errno"); + VerifyUntrackedDescriptor(original); + Require(ftruncate_nid_postfix(original, 64) == 0, "sizing the named object failed"); + VerifyConcurrentDescriptorReuse(name); + const char originalBytes[] = "original"; + Require(pwrite_nid_disambig1_nid_postfix(original, originalBytes, sizeof(originalBytes), 0) == sizeof(originalBytes), + "write to named object failed"); + + *__error_nid_postfix() = 0; + RequireError(shm_open_nid_postfix(name.c_str(), ReadWrite | Create | Exclusive, 0600), 17, + "exclusive creation replaced an existing named object"); + const int reader = shm_open_nid_postfix(name.c_str(), ReadOnly, 0); + char contents[sizeof(originalBytes)]{}; + Require(reader >= 0 && pread_nid_postfix(reader, contents, sizeof(contents), 0) == sizeof(contents) && + std::memcmp(contents, originalBytes, sizeof(contents)) == 0, "reopen did not share the original contents"); + *__error_nid_postfix() = 0; + Require(pwrite_nid_disambig1_nid_postfix(reader, "x", 1, 0) == -1 && *__error_nid_postfix() == 9, + "read-only descriptor accepted a write"); + *__error_nid_postfix() = 0; + Require(pwrite_nid_disambig1_nid_postfix(reader, "x", 1, 64) == -1 && *__error_nid_postfix() == 9, + "read-only descriptor accepted a write at end of file"); + *__error_nid_postfix() = 0; + Require(pwrite_nid_disambig1_nid_postfix(reader, "", 0, 64) == -1 && *__error_nid_postfix() == 9, + "read-only descriptor accepted a zero-length write"); + + Require(shm_unlink_nid_postfix(name.c_str()) == 0, "unlink of named object failed"); + *__error_nid_postfix() = 0; + RequireError(shm_open_nid_postfix(name.c_str(), ReadOnly, 0), 2, "unlinked name remained openable"); + std::memset(contents, 0, sizeof(contents)); + Require(pread_nid_postfix(original, contents, sizeof(contents), 0) == sizeof(contents) && + std::memcmp(contents, originalBytes, sizeof(contents)) == 0, "open descriptor stopped working after unlink"); + const char updatedByte = 'X'; + Require(pwrite_nid_disambig1_nid_postfix(original, &updatedByte, 1, 0) == 1, + "open descriptor stopped accepting writes after unlink"); + + const int replacement = shm_open_nid_postfix(name.c_str(), ReadWrite | Create | Exclusive, 0600); + Require(replacement >= 0, "name could not be recreated while old descriptors remained open"); + Require(ftruncate_nid_postfix(replacement, 64) == 0, "sizing the replacement object failed"); + Require(fchmod_nid_postfix(original, 0400) == 0, + "fchmod failed on the old open object after unlink and name recreation"); + Require(fchmod_nid_postfix(original, 0600) == 0, + "restoring mode on the old open object after name recreation failed"); + std::memset(contents, 0x7f, sizeof(contents)); + Require(pread_nid_postfix(replacement, contents, sizeof(contents), 0) == sizeof(contents) && + std::memcmp(contents, "\0\0\0\0\0\0\0\0", sizeof(contents)) == 0, + "recreated name still referred to the unlinked object"); + Require(pwrite_nid_disambig1_nid_postfix(replacement, "y", 1, 0) == 1, + "fchmod on the old descriptor changed the replacement object's permissions"); + const char replacementBytes[] = "replacement"; + Require(pwrite_nid_disambig1_nid_postfix(replacement, replacementBytes, sizeof(replacementBytes), 0) == sizeof(replacementBytes), + "write to replacement object failed"); + std::memset(contents, 0, sizeof(contents)); + Require(pread_nid_postfix(original, contents, sizeof(contents), 0) == sizeof(contents) && + contents[0] == updatedByte && std::memcmp(contents + 1, originalBytes + 1, sizeof(originalBytes) - 2) == 0, + "replacement write changed the old open object"); + Require(shm_unlink_nid_postfix(name.c_str()) == 0, "replacement unlink failed"); + Require(close_nid_postfix(reader) == 0 && close_nid_postfix(original) == 0 && close_nid_postfix(replacement) == 0, + "closing lifecycle descriptors failed"); + RequireNoUnlinkedFiles(name); + + const std::string duplicateName = base + "-duplicate"; + const int duplicateOriginal = shm_open_nid_postfix(duplicateName.c_str(), ReadWrite | Create | Exclusive, 0600); + Require(duplicateOriginal >= 0 && ftruncate_nid_postfix(duplicateOriginal, 64) == 0, + "creating the duplicate test object failed"); + const int duplicate = dup_nid_postfix(duplicateOriginal); + Require(duplicate >= 0 && pwrite_nid_disambig1_nid_postfix(duplicate, "d", 1, 0) == 1, + "dup lost shared-memory write access"); + const int duplicateReader = shm_open_nid_postfix(duplicateName.c_str(), ReadOnly, 0); + Require(duplicateReader >= 0 && shm_unlink_nid_postfix(duplicateName.c_str()) == 0, + "unlinking the duplicate test object failed"); + Require(close_nid_postfix(duplicateOriginal) == 0, "closing the duplicated original failed"); + Require(dup2_nid_postfix(-1, duplicate) == -1, + "dup2 accepted an invalid source"); + Require(pwrite_nid_disambig1_nid_postfix(duplicate, "e", 1, 0) == 1, + "failed dup2 destroyed its shared-memory target"); + Require(dup2_nid_postfix(duplicateReader, duplicate) == duplicate, + "dup2 could not replace a shared-memory target"); + Require(pwrite_nid_disambig1_nid_postfix(duplicate, "f", 1, 0) == -1 && *__error_nid_postfix() == 9, + "dup2 lost shared-memory read-only access"); + Require(close_nid_postfix(duplicateReader) == 0, "closing the duplicate reader failed"); + const std::string replacementName = base + "-duplicate-replacement"; + const int duplicateReplacement = shm_open_nid_postfix(replacementName.c_str(), ReadWrite | Create | Exclusive, 0600); + Require(duplicateReplacement >= 0 && dup2_nid_postfix(duplicateReplacement, duplicate) == duplicate, + "dup2 could not replace the last unlinked descriptor"); + RequireNoUnlinkedFiles(duplicateName); + Require(shm_unlink_nid_postfix(replacementName.c_str()) == 0 && + close_nid_postfix(duplicateReplacement) == 0 && close_nid_postfix(duplicate) == 0, + "cleaning up dup2 replacement descriptors failed"); + RequireNoUnlinkedFiles(replacementName); + VerifyResizeReadPosition(base + "-resize-read"); + + const std::string concurrentName = base + "-exclusive-race"; + constexpr int contenderCount = 12; + std::vector descriptors(contenderCount, -2); + std::vector errors(contenderCount, 0); + std::vector contenders; + contenders.reserve(contenderCount); + for (int index = 0; index < contenderCount; ++index) { + contenders.emplace_back([&, index] { + descriptors[index] = shm_open_nid_postfix(concurrentName.c_str(), ReadWrite | Create | Exclusive, 0600); + errors[index] = *__error_nid_postfix(); + }); + } + for (auto& contender : contenders) contender.join(); + int created = 0; + for (int index = 0; index < contenderCount; ++index) { + if (descriptors[index] >= 0) { + ++created; + Require(close_nid_postfix(descriptors[index]) == 0, "closing exclusive-race descriptor failed"); + } else { + Require(errors[index] == 17, "exclusive-race loser did not receive EEXIST"); + } + } + Require(created == 1, "exclusive create race produced more or fewer than one winner"); + Require(shm_unlink_nid_postfix(concurrentName.c_str()) == 0, "exclusive-race cleanup failed"); + + const std::string raceName = base + "-cross-process-race"; + const int raceDescriptor = shm_open_nid_postfix(raceName.c_str(), ReadWrite | Create | Exclusive, 0600); + Require(raceDescriptor >= 0 && ftruncate_nid_postfix(raceDescriptor, RaceSize) == 0, + "cross-process race object creation failed"); + VerifyCrossProcessResizeWrite(raceName, raceDescriptor); + Require(close_nid_postfix(raceDescriptor) == 0, "closing cross-process race descriptor failed"); + + const std::string permissionsName = base + "-permissions"; + const int restricted = shm_open_nid_postfix(permissionsName.c_str(), ReadWrite | Create | Exclusive, 0400); + Require(restricted >= 0, "read-only mode creation failed"); + RequireNoAdministratorsAccess(reinterpret_cast(::_get_osfhandle(restricted))); + Require(close_nid_postfix(restricted) == 0, "closing read-only mode object failed"); + *__error_nid_postfix() = 0; + RequireError(shm_open_nid_postfix(permissionsName.c_str(), ReadWrite, 0), 13, + "mode 0400 allowed a later read-write open"); + const int restrictedReader = shm_open_nid_postfix(permissionsName.c_str(), ReadOnly, 0); + Require(restrictedReader >= 0, "mode 0400 rejected a read-only open"); + Require(fchmod_nid_postfix(restrictedReader, 0600) == 0, + "fchmod could not restore write permission on a named object"); + const int restoredWriter = shm_open_nid_postfix(permissionsName.c_str(), ReadWrite, 0); + Require(restoredWriter >= 0, "mode 0600 after fchmod rejected read-write open"); + Require(fchmod_nid_postfix(restoredWriter, 0400) == 0, + "fchmod could not remove write permission on a named object"); + *__error_nid_postfix() = 0; + Require(shm_unlink_nid_postfix(permissionsName.c_str()) == -1 && *__error_nid_postfix() == 13, + "unlink of a read-only named object did not return EACCES"); + Require(fchmod_nid_postfix(restoredWriter, 0600) == 0, + "fchmod could not restore permission before unlink"); + Require(shm_unlink_nid_postfix(permissionsName.c_str()) == 0, + "owner could not unlink a writable named object"); + Require(close_nid_postfix(restoredWriter) == 0, "closing restored writer failed"); + Require(close_nid_postfix(restrictedReader) == 0, "closing restricted reader failed"); + + const std::string noPermissionsName = base + "-mode-zero"; + const int noPermissions = shm_open_nid_postfix(noPermissionsName.c_str(), ReadWrite | Create | Exclusive, 0000); + Require(noPermissions >= 0 && fchmod_nid_postfix(noPermissions, 0600) == 0, + "fchmod could not restore access to a mode 0000 named object"); + Require(shm_unlink_nid_postfix(noPermissionsName.c_str()) == 0 && close_nid_postfix(noPermissions) == 0, + "mode 0000 object cleanup failed"); + + *__error_nid_postfix() = 0; + RequireError(shm_open_nid_postfix(nullptr, ReadWrite, 0), 14, "null name did not return EFAULT"); + *__error_nid_postfix() = 0; + RequireError(shm_open_nid_postfix("relative", ReadWrite, 0), 22, "relative name did not return EINVAL"); + std::puts("Windows named shared-memory lifecycle checks passed"); +} + +bool EnvironmentEntryHasKey(const std::wstring& entry, const wchar_t* key) { + const auto separator = entry.find(L'='); + return separator != std::wstring::npos && separator == std::wcslen(key) && + ::CompareStringOrdinal(entry.data(), static_cast(separator), key, static_cast(separator), TRUE) == CSTR_EQUAL; +} + +std::vector EnvironmentWithTempPath(const std::wstring& path) { + std::vector entries; + LPWCH environment = ::GetEnvironmentStringsW(); + if (environment == nullptr) throw std::runtime_error("reading the Windows environment failed"); + try { + for (const wchar_t* current = environment; *current != L'\0'; current += std::wcslen(current) + 1) { + std::wstring entry(current); + if (!EnvironmentEntryHasKey(entry, L"TMP") && !EnvironmentEntryHasKey(entry, L"TEMP")) + entries.push_back(std::move(entry)); + } + } catch (...) { + ::FreeEnvironmentStringsW(environment); + throw; + } + ::FreeEnvironmentStringsW(environment); + entries.push_back(L"TMP=" + path); + entries.push_back(L"TEMP=" + path); + std::sort(entries.begin(), entries.end(), [](const auto& left, const auto& right) { + return ::CompareStringOrdinal(left.c_str(), -1, right.c_str(), -1, TRUE) == CSTR_LESS_THAN; + }); + std::vector block; + for (const auto& entry : entries) { + block.insert(block.end(), entry.begin(), entry.end()); + block.push_back(L'\0'); + } + block.push_back(L'\0'); + return block; +} + +bool SameDirectory(HANDLE left, HANDLE right) { + BY_HANDLE_FILE_INFORMATION leftInfo{}, rightInfo{}; + return ::GetFileInformationByHandle(left, &leftInfo) && ::GetFileInformationByHandle(right, &rightInfo) && + leftInfo.dwVolumeSerialNumber == rightInfo.dwVolumeSerialNumber && + leftInfo.nFileIndexHigh == rightInfo.nFileIndexHigh && leftInfo.nFileIndexLow == rightInfo.nFileIndexLow; +} + +std::filesystem::path ExtendedPath(const std::filesystem::path& path) { + wchar_t fullPath[32768]{}; + const DWORD length = ::GetFullPathNameW(path.c_str(), static_cast(std::size(fullPath)), fullPath, nullptr); + if (length == 0 || length >= std::size(fullPath)) + throw std::runtime_error("getting the full private temporary path failed"); + std::wstring full(fullPath, length); + if (full.starts_with(L"\\\\?\\")) return full; + if (full.starts_with(L"\\\\")) return L"\\\\?\\UNC\\" + full.substr(2); + return L"\\\\?\\" + full; +} + +std::filesystem::path DosPath(const std::filesystem::path& path) { + const auto& native = path.native(); + if (native.starts_with(L"\\\\?\\UNC\\")) return L"\\\\" + native.substr(8); + if (native.starts_with(L"\\\\?\\")) return native.substr(4); + return path; +} + +void RemovePrivateDirectory(const std::filesystem::path& directory) { + WIN32_FIND_DATAW entry{}; + const HANDLE search = ::FindFirstFileW((directory / L"*").c_str(), &entry); + if (search == INVALID_HANDLE_VALUE) { + if (::GetLastError() != ERROR_FILE_NOT_FOUND) + throw std::runtime_error("enumerating the private temporary directory for cleanup failed"); + } else { + try { + do { + if (std::wcscmp(entry.cFileName, L".") == 0 || std::wcscmp(entry.cFileName, L"..") == 0) continue; + if (entry.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) + throw std::runtime_error("the private temporary directory contains a reparse point"); + const auto child = directory / entry.cFileName; + if (entry.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) RemovePrivateDirectory(child); + else if (!::DeleteFileW(child.c_str())) + throw std::runtime_error("removing a private temporary file failed"); + } while (::FindNextFileW(search, &entry)); + if (::GetLastError() != ERROR_NO_MORE_FILES) + throw std::runtime_error("enumerating the private temporary directory for cleanup failed"); + } catch (...) { + ::FindClose(search); + throw; + } + ::FindClose(search); + } + if (!::RemoveDirectoryW(directory.c_str())) + throw std::runtime_error("removing the private temporary directory failed"); +} + +int RunWithShortTempPath(bool longPath) { + wchar_t tempBuffer[32768]{}; + const DWORD tempLength = ::GetTempPathW(static_cast(std::size(tempBuffer)), tempBuffer); + if (tempLength == 0 || tempLength >= std::size(tempBuffer)) + throw std::runtime_error("locating the Windows temporary directory failed"); + const std::filesystem::path parent(std::wstring(tempBuffer, tempLength)); + std::filesystem::path baseRoot; + bool created = false; + for (unsigned attempt = 0; attempt < 16; ++attempt) { + baseRoot = parent / (L"AnyPS5-Temp-Regression-" + std::to_wstring(::GetCurrentProcessId()) + L"-" + + std::to_wstring(::GetTickCount64()) + L"-" + std::to_wstring(attempt)); + if (::CreateDirectoryW(baseRoot.c_str(), nullptr)) { + created = true; + break; + } + if (::GetLastError() != ERROR_ALREADY_EXISTS) throw std::runtime_error("creating a private temporary directory failed"); + } + if (!created) throw std::runtime_error("creating a unique private temporary directory failed"); + const auto cleanupRoot = ExtendedPath(baseRoot); + std::filesystem::path longRoot = cleanupRoot; + const auto cleanup = [&] { + RemovePrivateDirectory(cleanupRoot); + }; + + try { + std::vector shortBuffer(32768); + DWORD shortLength = ::GetShortPathNameW(longRoot.c_str(), shortBuffer.data(), static_cast(shortBuffer.size())); + if (shortLength == 0) + throw std::runtime_error("GetShortPathNameW failed (Win32 error " + std::to_string(::GetLastError()) + ")"); + if (shortLength >= shortBuffer.size()) + throw std::runtime_error("GetShortPathNameW required a buffer of " + std::to_string(shortLength) + " characters"); + std::filesystem::path shortRoot(std::wstring(shortBuffer.data(), shortLength)); + if (longPath) { + bool aliasAvailable = true; + bool longAliasReady = false; + for (unsigned level = 0; level < 80; ++level) { + longRoot /= L"LongSharedMemoryTempDirectoryRegressionSegment"; + if (!::CreateDirectoryW(longRoot.c_str(), nullptr)) + throw std::runtime_error("creating the nested long temporary directory failed (Win32 error " + std::to_string(::GetLastError()) + ")"); + shortLength = ::GetShortPathNameW(longRoot.c_str(), shortBuffer.data(), static_cast(shortBuffer.size())); + if (shortLength == 0) + throw std::runtime_error("GetShortPathNameW failed for nested path (Win32 error " + std::to_string(::GetLastError()) + ")"); + if (shortLength >= shortBuffer.size()) + throw std::runtime_error("GetShortPathNameW required a buffer of " + std::to_string(shortLength) + " characters"); + shortRoot = std::filesystem::path(std::wstring(shortBuffer.data(), shortLength)); + if (::CompareStringOrdinal(longRoot.filename().c_str(), -1, shortRoot.filename().c_str(), -1, TRUE) == CSTR_EQUAL) { + aliasAvailable = false; + break; + } + if (DosPath(longRoot).native().size() > MAX_PATH + 64 && DosPath(shortRoot).native().size() < MAX_PATH - 64) { + longAliasReady = true; + break; + } + } + if (!aliasAvailable) { + std::puts("long-short-temp regression skipped: this volume did not create an 8.3 alias for the nested directory"); + cleanup(); + return 77; + } + if (!longAliasReady) + throw std::runtime_error("nested private path did not exceed MAX_PATH with a usable short alias"); + } + if (::CompareStringOrdinal(longRoot.filename().c_str(), -1, shortRoot.filename().c_str(), -1, TRUE) == CSTR_EQUAL) { + std::puts("short-temp regression skipped: this volume did not create an 8.3 alias for the private directory"); + cleanup(); + return 77; + } + const HANDLE longHandle = ::CreateFileW(longRoot.c_str(), FILE_READ_ATTRIBUTES, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (longHandle == INVALID_HANDLE_VALUE) throw std::runtime_error("opening the long temporary path failed"); + const HANDLE shortHandle = ::CreateFileW(shortRoot.c_str(), FILE_READ_ATTRIBUTES, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (shortHandle == INVALID_HANDLE_VALUE) { + ::CloseHandle(longHandle); + throw std::runtime_error("opening the 8.3 temporary alias failed"); + } + const bool isSameDirectory = SameDirectory(longHandle, shortHandle); + ::CloseHandle(shortHandle); + ::CloseHandle(longHandle); + if (!isSameDirectory) throw std::runtime_error("the short temporary path did not identify the created directory"); + std::wprintf(L"%ls regression verified 8.3 directory alias (%llu/%llu characters):\n%ls\n%ls\n", + longPath ? L"long-short-temp" : L"short-temp", + static_cast(longRoot.native().size()), + static_cast(shortRoot.native().size()), longRoot.c_str(), shortRoot.c_str()); + std::fflush(stdout); + + wchar_t executable[32768]{}; + const DWORD executableLength = ::GetModuleFileNameW(nullptr, executable, static_cast(std::size(executable))); + if (executableLength == 0 || executableLength >= std::size(executable)) + throw std::runtime_error("locating the Windows shared-memory test executable failed"); + std::wstring command = L"\"" + std::wstring(executable, executableLength) + L"\" --short-temp-child"; + auto environment = EnvironmentWithTempPath(DosPath(shortRoot).native()); + STARTUPINFOW startup{}; + startup.cb = sizeof(startup); + PROCESS_INFORMATION process{}; + if (!::CreateProcessW(nullptr, command.data(), nullptr, nullptr, FALSE, + CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT, environment.data(), nullptr, &startup, &process)) + throw std::runtime_error("starting the short temporary path child failed"); + const DWORD wait = ::WaitForSingleObject(process.hProcess, 16000); + DWORD exitCode = 1; + const bool childSucceeded = wait == WAIT_OBJECT_0 && ::GetExitCodeProcess(process.hProcess, &exitCode) && exitCode == 0; + if (wait != WAIT_OBJECT_0) { + ::TerminateProcess(process.hProcess, 1); + ::WaitForSingleObject(process.hProcess, 1000); + } + if (!childSucceeded) + std::fprintf(stderr, "%s child failed: wait=%lu exit=%lu\n", longPath ? "long-short-temp" : "short-temp", + static_cast(wait), static_cast(exitCode)); + ::CloseHandle(process.hThread); + ::CloseHandle(process.hProcess); + cleanup(); + return childSucceeded ? 0 : 1; + } catch (...) { + cleanup(); + throw; + } +} +} + +int main(int argumentCount, char** arguments) { + try { + if (argumentCount == 3 && std::string_view(arguments[1]) == "--race") + return RunResizeWriteRace(arguments[2]) ? 0 : 1; + if (argumentCount == 2 && std::string_view(arguments[1]) == "--short-temp") + return RunWithShortTempPath(false); + if (argumentCount == 2 && std::string_view(arguments[1]) == "--long-short-temp") + return RunWithShortTempPath(true); + if (argumentCount == 2 && std::string_view(arguments[1]) == "--short-temp-child") { + Run(); + return 0; + } + Run(); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/GuestNetCtl.cpp b/core/libs/tests/GuestNetCtl.cpp index facc12ccf6..9b900f4bd4 100644 --- a/core/libs/tests/GuestNetCtl.cpp +++ b/core/libs/tests/GuestNetCtl.cpp @@ -51,6 +51,9 @@ int APS5_VABI sceNetCtlRegisterCallbackV6(NetCtlCallback, void*, int*); void APS5_VABI sceNetCtlTerm(void); int APS5_VABI sceNetCtlUnregisterCallback(int); int APS5_VABI sceNetCtlUnregisterCallbackV6(int); +int APS5_VABI sceNetCtlRegisterCallbackForNpToolkit(NetCtlCallback, void*, int*); +int APS5_VABI sceNetCtlUnregisterCallbackForNpToolkit(int); +int APS5_VABI sceNetCtlCheckCallbackForNpToolkit(void); } namespace { @@ -283,6 +286,9 @@ int main() { Require(sceNetCtlRegisterCallbackV6(nullptr, nullptr, nullptr) == kErrInvalidAddr); Require(sceNetCtlUnregisterCallback(-1) == invalidId); Require(sceNetCtlUnregisterCallbackV6(-1) == invalidId); + Require(sceNetCtlRegisterCallbackForNpToolkit(nullptr, nullptr, nullptr) == kErrInvalidAddr); + Require(sceNetCtlUnregisterCallbackForNpToolkit(-1) == invalidId); + Require(sceNetCtlCheckCallbackForNpToolkit() == 0); CheckCallbacks(); Require(sceNetCtlGetStateV6(nullptr) == kErrInvalidAddr); diff --git a/core/libs/tests/GuestNetThread.cpp b/core/libs/tests/GuestNetThread.cpp new file mode 100644 index 0000000000..27f3b16a11 --- /dev/null +++ b/core/libs/tests/GuestNetThread.cpp @@ -0,0 +1,86 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceNetThreadCreate(Pthread*, void*, const char*); +void APS5_VABI sceNetThreadExit(); +int APS5_VABI sceNetThreadJoin(Pthread); +int APS5_VABI sceNetEmulationGet(); +int APS5_VABI sceNetEmulationSet(); +int* APS5_VABI sceNetErrnoLoc(); +} + +namespace { + +constexpr int Einval = 22; +constexpr int Eopnotsupp = 45; +constexpr int NetError = static_cast(0x80410100u); + +struct ThreadParam { + void (APS5_VABI *entry)(void*); + void* arg; + std::uint64_t reserved; + std::uint32_t flags; +}; + +struct Run { + std::atomic started{0}; + std::atomic pastExit{false}; +}; + +Run run; + +void Require(bool value, const char* what) { + if (value) return; + std::fprintf(stderr, "%s\n", what); + std::abort(); +} + +void APS5_VABI Count(void* arg) { + ++static_cast(arg)->started; +} + +void APS5_VABI ExitEarly(void* arg) { + auto* run = static_cast(arg); + ++run->started; + sceNetThreadExit(); + run->pastExit = true; +} + +} + +int main() { + ThreadParam counting{Count, &run, 0, 0}; + Pthread thread = nullptr; + Require(sceNetThreadCreate(&thread, &counting, "anyps5-net-thread") == 0 && thread != nullptr, "sceNetThreadCreate failed"); + Require(sceNetThreadJoin(thread) == 0, "sceNetThreadJoin failed"); + Require(run.started == 1, "the thread did not run its entry with its argument"); + + Pthread untouched = reinterpret_cast(&run); + *sceNetErrnoLoc() = 0; + Require(sceNetThreadCreate(&untouched, nullptr, "anyps5-net-thread") == (NetError | Einval), "a null parameter was not refused with EINVAL"); + Require(*sceNetErrnoLoc() == Einval && untouched == reinterpret_cast(&run), "a refused create set no errno or wrote the thread"); + + ThreadParam exiting{ExitEarly, &run, 0, 1}; + Require(sceNetThreadCreate(&thread, &exiting, nullptr) == 0, "sceNetThreadCreate failed without a name"); + Require(sceNetThreadJoin(thread) == 0, "joining an exited thread failed"); + Require(run.started == 2 && !run.pastExit, "sceNetThreadExit did not end the thread"); + + static ThreadParam detached{Count, &run, 0, 0}; + Require(sceNetThreadCreate(nullptr, &detached, "anyps5-net-thread") == 0, "a null thread output was refused"); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (run.started != 3 && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + Require(run.started == 3, "the thread created without an output did not run"); + + *sceNetErrnoLoc() = 0; + Require(sceNetEmulationGet() == (NetError | Eopnotsupp) && *sceNetErrnoLoc() == Eopnotsupp, "sceNetEmulationGet did not return EOPNOTSUPP"); + *sceNetErrnoLoc() = 0; + Require(sceNetEmulationSet() == (NetError | Eopnotsupp) && *sceNetErrnoLoc() == Eopnotsupp, "sceNetEmulationSet did not return EOPNOTSUPP"); + std::puts("net thread tests passed"); + return 0; +} diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index cf69103ad5..c0257b7385 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -43,6 +43,7 @@ int APS5_VABI scePadResetLightBar(int); int APS5_VABI scePadSetLightBar(int, const PadLightBarParam*); int APS5_VABI scePadSetMotionSensorState(int, bool); int APS5_VABI scePadGetControllerType(int, std::int32_t*); +int APS5_VABI scePadGetInfo_nid_postfix(PadInfo*); } static void Require(bool value) { if (!value) std::abort(); } @@ -637,7 +638,14 @@ int main() { Require(scePadGetHandle(user, PAD_PORT_TYPE_SPECIAL, 0) == earlySpecial); Require(scePadClose_nid_postfix(earlySpecial) == PAD_OK); Require(scePadGetHandle(user, PAD_PORT_TYPE_SPECIAL, 0) == noHandle); + PadSetPortConnected_nid_postfix(1, true); Require(scePadInit_nid_postfix() == 0); + PadInfo initInfo{}; + Require(scePadGetInfo_nid_postfix(&initInfo) == PAD_OK); + Require(initInfo.connectedCount[0] == 2); + Require(initInfo.padTypeInfo[0].connectPort == 0); + Require(initInfo.padTypeInfo[1].connectPort == 1); + PadSetPortConnected_nid_postfix(1, false); Require(scePadUnknown_n3kSX62fgNo() == PAD_OK); const int handle = scePadOpen_nid_postfix(user, 0, 0, nullptr); Require(handle > 0); @@ -689,4 +697,42 @@ int main() { Require(scePadSetAngularVelocityDeadbandState(handle + 1, true) == PAD_ERROR_INVALID_HANDLE); Require(scePadSetAngularVelocityBiasCorrectionState(handle, false) == PAD_OK); Require(scePadSetAngularVelocityBiasCorrectionState(handle + 1, false) == PAD_ERROR_INVALID_HANDLE); + PadInfo info{}; + Require(scePadGetInfo_nid_postfix(&info) == PAD_OK); + Require(info.maxConnectCount == PAD_MAX_CONTROLLERS); + Require(info.connectedCount[0] == 1); + Require(info.padTypeInfo[0].connectPort == 0); + + const int user2 = 0x10000001; + Require(scePadGetHandle(user2, 0, 0) == noHandle); + const int handle2 = scePadOpen_nid_postfix(user2, 0, 0, nullptr); + Require(handle2 == PAD_HANDLE_BASE + 1); + Require(scePadGetHandle(user2, 0, 0) == handle2); + + PadInputState p2Input{}; + p2Input.buttons = static_cast(Pad::PadButton::Triangle); + PadPublishInputForPort_nid_postfix(1, p2Input); + PadData p2Data{}; + Require(scePadReadState(handle2, &p2Data) == 0); + Require((p2Data.buttons & static_cast(Pad::PadButton::Triangle)) != 0); + + Require(scePadSetVibrationMode(handle2, 1) == 0); + std::uint32_t p2AccessState = 7; + Require(scePadGetAccessibilityControllerState(handle2, &p2AccessState) == PAD_OK); + Require(p2AccessState == 0); + + PadSetPortConnected_nid_postfix(1, true); + Require(scePadGetInfo_nid_postfix(&info) == PAD_OK); + Require(info.connectedCount[0] == 2); + Require(info.padTypeInfo[0].connectPort == 0); + Require(info.padTypeInfo[1].connectPort == 1); + PadSetPortConnected_nid_postfix(1, false); + Require(scePadGetInfo_nid_postfix(&info) == PAD_OK); + Require(info.connectedCount[0] == 1); + Require(info.padTypeInfo[0].connectPort == 0); + + Require(scePadClose_nid_postfix(handle2) == 0); + Require(scePadGetHandle(user2, 0, 0) == noHandle); + Require(scePadSetAngularVelocityDeadbandState(handle2, false) == PAD_OK); + Require(scePadSetAngularVelocityDeadbandState(handle2 + 1, false) == PAD_ERROR_INVALID_HANDLE); } diff --git a/core/libs/tests/GuestParentProcess.cpp b/core/libs/tests/GuestParentProcess.cpp index 203cfe3a00..3465efc6aa 100644 --- a/core/libs/tests/GuestParentProcess.cpp +++ b/core/libs/tests/GuestParentProcess.cpp @@ -5,10 +5,13 @@ #else #include #endif + extern "C" { int APS5_VABI getpid_nid_postfix(void); int APS5_VABI getppid_nid_postfix(void); int APS5_VABI getsid_nid_postfix(int); +int APS5_VABI setpgid_nid_postfix(int, int); +int APS5_VABI setpgrp_nid_postfix(int, int); int* APS5_VABI __error_nid_postfix(); } static void Require(bool value) { if (!value) std::abort(); } @@ -21,6 +24,72 @@ int main() { Require(*__error_nid_postfix() == 3); Require(getsid_nid_postfix(-1) == -1); Require(*__error_nid_postfix() == 3); + *__error_nid_postfix() = 0; + Require(setpgid_nid_postfix(0, 0) == -1); + Require(*__error_nid_postfix() == 1); + *__error_nid_postfix() = 0; + Require(setpgid_nid_postfix(self, 0) == -1); + Require(*__error_nid_postfix() == 1); + *__error_nid_postfix() = 0; + Require(setpgid_nid_postfix(0, self) == -1); + Require(*__error_nid_postfix() == 1); + *__error_nid_postfix() = 0; + Require(setpgid_nid_postfix(self, self) == -1); + Require(*__error_nid_postfix() == 1); + *__error_nid_postfix() = 0; + Require(setpgid_nid_postfix(0, 123456) == -1); + Require(*__error_nid_postfix() == 1); + *__error_nid_postfix() = 0; + Require(setpgid_nid_postfix(self, 123456) == -1); + Require(*__error_nid_postfix() == 1); + *__error_nid_postfix() = 0; + Require(setpgid_nid_postfix(-1, -1) == -1); + Require(*__error_nid_postfix() == 22); + *__error_nid_postfix() = 0; + Require(setpgid_nid_postfix(0, -1) == -1); + Require(*__error_nid_postfix() == 22); + *__error_nid_postfix() = 0; + Require(setpgid_nid_postfix(self, -1) == -1); + Require(*__error_nid_postfix() == 22); + *__error_nid_postfix() = 0; + Require(setpgid_nid_postfix(-1, 0) == -1); + Require(*__error_nid_postfix() == 3); + *__error_nid_postfix() = 0; + Require(setpgid_nid_postfix(123456, 0) == -1); + Require(*__error_nid_postfix() == 3); + *__error_nid_postfix() = 0; + Require(setpgrp_nid_postfix(0, 0) == -1); + Require(*__error_nid_postfix() == 1); + *__error_nid_postfix() = 0; + Require(setpgrp_nid_postfix(self, 0) == -1); + Require(*__error_nid_postfix() == 1); + *__error_nid_postfix() = 0; + Require(setpgrp_nid_postfix(0, self) == -1); + Require(*__error_nid_postfix() == 1); + *__error_nid_postfix() = 0; + Require(setpgrp_nid_postfix(self, self) == -1); + Require(*__error_nid_postfix() == 1); + *__error_nid_postfix() = 0; + Require(setpgrp_nid_postfix(0, 123456) == -1); + Require(*__error_nid_postfix() == 1); + *__error_nid_postfix() = 0; + Require(setpgrp_nid_postfix(self, 123456) == -1); + Require(*__error_nid_postfix() == 1); + *__error_nid_postfix() = 0; + Require(setpgrp_nid_postfix(-1, -1) == -1); + Require(*__error_nid_postfix() == 22); + *__error_nid_postfix() = 0; + Require(setpgrp_nid_postfix(0, -1) == -1); + Require(*__error_nid_postfix() == 22); + *__error_nid_postfix() = 0; + Require(setpgrp_nid_postfix(self, -1) == -1); + Require(*__error_nid_postfix() == 22); + *__error_nid_postfix() = 0; + Require(setpgrp_nid_postfix(-1, 0) == -1); + Require(*__error_nid_postfix() == 3); + *__error_nid_postfix() = 0; + Require(setpgrp_nid_postfix(123456, 0) == -1); + Require(*__error_nid_postfix() == 3); const int parent = getppid_nid_postfix(); Require(parent > 0 && parent != getpid_nid_postfix()); Require(getppid_nid_postfix() == parent); diff --git a/core/libs/tests/GuestPthreadRwlock.cpp b/core/libs/tests/GuestPthreadRwlock.cpp index 352d614b67..9c8c6d557c 100644 --- a/core/libs/tests/GuestPthreadRwlock.cpp +++ b/core/libs/tests/GuestPthreadRwlock.cpp @@ -36,6 +36,7 @@ int APS5_VABI scePthreadRwlockRdlock(PthreadRwlock* rwlock); int APS5_VABI scePthreadRwlockWrlock(PthreadRwlock* rwlock); int APS5_VABI scePthreadRwlockTryrdlock(PthreadRwlock* rwlock); int APS5_VABI scePthreadRwlockUnlock(PthreadRwlock* rwlock); +int APS5_VABI scePthreadRwlockattrSettype(PthreadRwlockattr* attr, int type); } using TimedLock = int (APS5_VABI *)(PthreadRwlock*, const KernelTimespec*); @@ -402,13 +403,29 @@ static void CheckAttrProcessSharing() { Require(pthread_rwlockattr_destroy_nid_postfix(&attr) == 0); } +static void CheckAttrSettype() { + PthreadRwlockattr attr = nullptr; + Require(pthread_rwlockattr_init_nid_postfix(&attr) == 0); + for (const int type : {1, 2}) { + Require(scePthreadRwlockattrSettype(&attr, type) == SCE_OK); + PthreadRwlock rwlock = nullptr; + Require(pthread_rwlock_init_nid_postfix(&rwlock, &attr) == 0); + Require(pthread_rwlock_wrlock_nid_postfix(&rwlock) == 0 && pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); + Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == 0); + } + for (const int type : {0, 3, -1, static_cast(0x80000000u)}) Require(scePthreadRwlockattrSettype(&attr, type) == SCE_KERNEL_ERROR_EINVAL); + Require(pthread_rwlockattr_destroy_nid_postfix(&attr) == 0); + Require(scePthreadRwlockattrSettype(&attr, 1) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadRwlockattrSettype(nullptr, 1) == SCE_KERNEL_ERROR_EINVAL); +} + static void CheckAttrType() { PthreadRwlockattr attr = nullptr; Require(pthread_rwlockattr_init_nid_postfix(&attr) == 0); int type = -1; Require(pthread_rwlockattr_gettype_np_nid_postfix(&attr, &type) == 0 && type == 0); Require(scePthreadRwlockattrGettype(&attr, &type) == SCE_OK && type == 0); - for (const int set : {1, 2, 0}) { + for (const int set : {1, 2}) { Require(scePthreadRwlockattrSettype(&attr, set) == SCE_OK); Require(pthread_rwlockattr_gettype_np_nid_postfix(&attr, &type) == 0 && type == set); Require(scePthreadRwlockattrGettype(&attr, &type) == SCE_OK && type == set); @@ -426,6 +443,7 @@ static void CheckAttrType() { } int main() { + CheckAttrSettype(); CheckAttrProcessSharing(); CheckAttrType(); CheckWaitingWriterHoldsBackNewReaders(); diff --git a/core/libs/tests/GuestRandomDeviceSeed.cpp b/core/libs/tests/GuestRandomDeviceSeed.cpp new file mode 100644 index 0000000000..0aaee61657 --- /dev/null +++ b/core/libs/tests/GuestRandomDeviceSeed.cpp @@ -0,0 +1,97 @@ +#include "prx/libc/include/HostEntropy.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +std::int64_t APS5_VABI random_nid_postfix(); +void APS5_VABI srandomdev_nid_postfix(); +char* APS5_VABI initstate_nid_postfix(std::uint64_t, char*, std::int64_t); +char* APS5_VABI setstate_nid_postfix(char*); +} + +static void* entropyBuffer; +static std::size_t entropySize; +static bool failEntropy; + +void HostEntropy_nid_no_patch(void* buffer, std::size_t size) { + entropyBuffer = buffer; + entropySize = size; + if (failEntropy) throw std::runtime_error("entropy unavailable"); + auto* bytes = static_cast(buffer); + for (std::size_t i = 0; i < size; ++i) bytes[i] = static_cast(i * 37 + 11); +} + +static void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "%s\n", message); + std::abort(); + } +} + +static std::uint32_t Word(const char* buffer, std::size_t index) { + std::uint32_t value; + std::memcpy(&value, buffer + sizeof(value) * index, sizeof(value)); + return value; +} + +int main() { + constexpr std::array sizes {8, 32, 64, 128, 256}; + constexpr std::array degrees {1, 7, 15, 31, 63}; + constexpr std::array separations {0, 3, 1, 3, 1}; + std::array state{}; + for (std::size_t type = 0; type < sizes.size(); ++type) { + state.fill(static_cast(0xa5)); + char* table = state.data() + 1; + char* previous = initstate_nid_postfix(42, table, static_cast(sizes[type])); + for (int i = 0; i < 5; ++i) random_nid_postfix(); + const std::uint32_t header = Word(table, 0); + srandomdev_nid_postfix(); + Require(entropyBuffer == table + 4, "srandomdev fills the active state after its header"); + Require(entropySize == degrees[type] * 4, "srandomdev fills every generator word"); + Require(Word(table, 0) == header, "srandomdev leaves the saved header untouched"); + for (std::size_t i = 0; i < entropySize; ++i) + Require(static_cast(table[4 + i]) == static_cast(i * 37 + 11), + "srandomdev neither derives words from one seed nor advances the generator"); + Require(state[0] == static_cast(0xa5) && + std::all_of(state.begin() + 1 + sizes[type], state.end(), [](char c) { return c == static_cast(0xa5); }), + "srandomdev preserves the buffer guards"); + const auto seeded = state; + if (type != 0) { + const std::uint32_t sum = Word(table, 1) + Word(table, 1 + separations[type]); + Require(random_nid_postfix() == sum / 2, "srandomdev resets the front and rear positions"); + Require(Word(table, 1 + separations[type]) == sum, "random advances the newly seeded table"); + } else { + std::int64_t x = static_cast(Word(table, 1)); + if (x == 0) x = 123459876; + x = 16807 * (x % 127773) - 2836 * (x / 127773); + if (x < 0) x += 0x7fffffff; + const auto expected = x; + Require(random_nid_postfix() == static_cast(expected), "the linear generator uses the entropy word"); + } + setstate_nid_postfix(previous); + Require(Word(table, 0) == type + (type == 0 ? 0 : 5), "the reseeded state saves its new position"); + state = seeded; + const std::uint32_t resetHeader = static_cast(type); + std::memcpy(table, &resetHeader, sizeof(resetHeader)); + setstate_nid_postfix(table); + const auto beforeFailure = state; + failEntropy = true; + bool threw = false; + try { + srandomdev_nid_postfix(); + } catch (const std::runtime_error&) { + threw = true; + } + failEntropy = false; + Require(threw, "srandomdev propagates entropy failures"); + Require(state == beforeFailure, "an entropy failure before writing keeps the state"); + setstate_nid_postfix(previous); + } +} diff --git a/core/libs/tests/GuestRandomState.cpp b/core/libs/tests/GuestRandomState.cpp index c663f10136..dc4259eb5e 100644 --- a/core/libs/tests/GuestRandomState.cpp +++ b/core/libs/tests/GuestRandomState.cpp @@ -13,6 +13,7 @@ extern "C" { std::int64_t APS5_VABI random_nid_postfix(); void APS5_VABI srandom_nid_postfix(std::uint64_t); +void APS5_VABI srandomdev_nid_postfix(); char* APS5_VABI initstate_nid_postfix(std::uint64_t, char*, std::int64_t); char* APS5_VABI setstate_nid_postfix(char*); int* APS5_VABI __error_nid_postfix(); @@ -232,5 +233,25 @@ int main() { std::sort(concurrent.begin(), concurrent.end()); Require(serial == concurrent, "concurrent calls must draw the serial sequence"); + for (const std::int64_t size : {8, 32, 64, 128, 256}) { + std::array entropyState{}; + char* previous = initstate_nid_postfix(42, entropyState.data(), size); + srandomdev_nid_postfix(); + const std::size_t separation = size == 32 || size == 128 ? 3 : 1; + std::int64_t expected; + if (size == 8) { + std::int64_t x = static_cast(Word(entropyState.data(), 1)); + if (x == 0) x = 123459876; + x = 16807 * (x % 127773) - 2836 * (x / 127773); + if (x < 0) x += 0x7fffffff; + expected = x; + } else { + const std::uint32_t sum = Word(entropyState.data(), 1) + Word(entropyState.data(), 1 + separation); + expected = sum >> 1; + } + Require(random_nid_postfix() == expected, "srandomdev seeds the shared library's active state from host entropy"); + setstate_nid_postfix(previous); + } + return failures == 0 ? 0 : 1; } diff --git a/core/libs/tests/GuestRtprioThread.cpp b/core/libs/tests/GuestRtprioThread.cpp new file mode 100644 index 0000000000..4d7aa6cc78 --- /dev/null +++ b/core/libs/tests/GuestRtprioThread.cpp @@ -0,0 +1,117 @@ +#include "SceTypes.hpp" +#include +#include +#include +#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); +Pthread APS5_VABI scePthreadSelf(void); +int APS5_VABI scePthreadGetthreadid(void); +int APS5_VABI scePthreadGetschedparam(Pthread thread, int* policy, KernelSchedParam* param); +int APS5_VABI scePthreadAttrInit(PthreadAttr* attr); +int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr); +int APS5_VABI scePthreadAttrSetinheritsched(PthreadAttr* attr, int inheritSched); +int APS5_VABI scePthreadAttrSetschedpolicy(PthreadAttr* attr, int policy); +int APS5_VABI scePthreadAttrSetschedparam(PthreadAttr* attr, const KernelSchedParam* param); +struct GuestRtPrio { + std::uint16_t type; + std::uint16_t prio; +}; +int APS5_VABI rtprio_thread_nid_postfix(int function, int lwpid, GuestRtPrio* priority); +int* APS5_VABI __error_nid_postfix(); +} + +static constexpr int SCE_OK = 0; +static constexpr int GUEST_SCHED_FIFO = 1; +static constexpr int GUEST_SCHED_RR = 3; +static constexpr int EXPLICIT_SCHED = 0; +static constexpr int RTP_LOOKUP = 0; +static constexpr int RTP_SET = 1; +static constexpr std::uint16_t RTP_PRIO_REALTIME = 2; +static constexpr std::uint16_t RTP_PRIO_FIFO = 10; +static constexpr int GUEST_ESRCH = 3; +static constexpr int GUEST_EFAULT = 14; + +static void Require(bool value) { if (!value) std::abort(); } + +struct Report { + int policy; + int priority; + std::atomic tid{0}; + GuestRtPrio lookup{7, 7}; + int lookupResult = -1; + std::atomic release{false}; +}; + +static void* APS5_VABI Entry(void* arg) { + auto* report = static_cast(arg); + report->lookupResult = rtprio_thread_nid_postfix(RTP_LOOKUP, 0, &report->lookup); + report->tid.store(scePthreadGetthreadid(), std::memory_order_release); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (!report->release.load(std::memory_order_acquire)) { + Require(std::chrono::steady_clock::now() < deadline); + std::this_thread::yield(); + } + return nullptr; +} + +static int LookupError(int function, int lwpid, GuestRtPrio* priority) { + *__error_nid_postfix() = 0; + Require(rtprio_thread_nid_postfix(function, lwpid, priority) == -1); + return *__error_nid_postfix(); +} + +static void CheckThread(int policy, int priority, std::uint16_t expectedType) { + Report report{policy, priority}; + PthreadAttr attr = nullptr; + KernelSchedParam param{priority}; + Require(scePthreadAttrInit(&attr) == SCE_OK); + Require(scePthreadAttrSetinheritsched(&attr, EXPLICIT_SCHED) == SCE_OK); + Require(scePthreadAttrSetschedpolicy(&attr, policy) == SCE_OK); + Require(scePthreadAttrSetschedparam(&attr, ¶m) == SCE_OK); + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, &attr, Entry, &report, "rtprio") == SCE_OK); + Require(scePthreadAttrDestroy(&attr) == SCE_OK); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (report.tid.load(std::memory_order_acquire) == 0) { + Require(std::chrono::steady_clock::now() < deadline); + std::this_thread::yield(); + } + Require(report.lookupResult == 0 && report.lookup.type == expectedType && report.lookup.prio == priority); + GuestRtPrio fromCreator{7, 7}; + Require(rtprio_thread_nid_postfix(RTP_LOOKUP, report.tid.load(), &fromCreator) == 0); + Require(fromCreator.type == expectedType && fromCreator.prio == priority); + bool threw = false; + try { rtprio_thread_nid_postfix(RTP_SET, report.tid.load(), &fromCreator); } catch (const std::exception&) { threw = true; } + Require(threw); + report.release.store(true, std::memory_order_release); + Require(scePthreadJoin(thread, nullptr) == SCE_OK); + Require(LookupError(RTP_LOOKUP, report.tid.load(), &fromCreator) == GUEST_ESRCH); +} + +int main() { + int policy = -1; + KernelSchedParam param{}; + Require(scePthreadGetschedparam(scePthreadSelf(), &policy, ¶m) == SCE_OK); + Require(policy == GUEST_SCHED_FIFO || policy == GUEST_SCHED_RR); + GuestRtPrio self{7, 7}; + Require(rtprio_thread_nid_postfix(RTP_LOOKUP, 0, &self) == 0); + Require(self.type == (policy == GUEST_SCHED_FIFO ? RTP_PRIO_FIFO : RTP_PRIO_REALTIME) && self.prio == param.sched_priority); + GuestRtPrio byId{7, 7}; + Require(rtprio_thread_nid_postfix(RTP_LOOKUP, scePthreadGetthreadid(), &byId) == 0); + Require(byId.type == self.type && byId.prio == self.prio); + CheckThread(GUEST_SCHED_FIFO, 512, RTP_PRIO_FIFO); + CheckThread(GUEST_SCHED_RR, 600, RTP_PRIO_REALTIME); + CheckThread(GUEST_SCHED_FIFO, 256, RTP_PRIO_FIFO); + Require(LookupError(2, 0, &self) == GUEST_ESRCH); + Require(LookupError(RTP_LOOKUP, 0, nullptr) == GUEST_EFAULT); + Require(LookupError(RTP_LOOKUP, 1, &self) == GUEST_ESRCH); + Require(LookupError(RTP_LOOKUP, 999999, &self) == GUEST_ESRCH); + bool threw = false; + try { rtprio_thread_nid_postfix(RTP_SET, 0, &self); } catch (const std::exception&) { threw = true; } + Require(threw); +} diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 41b0a1d7d4..4e5f27cab2 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -47,6 +47,7 @@ int APS5_VABI sceNetListen(int, int); int APS5_VABI sceNetGetsockname(int, void*, std::uint32_t*); int APS5_VABI sceNetGetpeername(int, void*, std::uint32_t*); int APS5_VABI sceNetGetSockInfo(int, void*, int, int); +int APS5_VABI sceNetGetSockInfo6(int, void*, int, int); int APS5_VABI sceNetConnect(int, const void*, std::uint32_t); int APS5_VABI sceNetAccept(int, void*, std::uint32_t*); std::int64_t APS5_VABI sceNetSend(int, const void*, std::size_t, int); @@ -90,6 +91,10 @@ int APS5_VABI sceNetCtlRegisterCallback(NetCtlCallback, void*, int*); int APS5_VABI sceNetCtlUnregisterCallback(int); int APS5_VABI sceNetCtlRegisterCallbackV6(NetCtlCallback, void*, int*); int APS5_VABI sceNetCtlUnregisterCallbackV6(int); +int APS5_VABI sceNetCtlCheckCallback(void); +int APS5_VABI sceNetCtlRegisterCallbackForNpToolkit(NetCtlCallback, void*, int*); +int APS5_VABI sceNetCtlUnregisterCallbackForNpToolkit(int); +int APS5_VABI sceNetCtlCheckCallbackForNpToolkit(void); std::uint64_t NetCtlReadByteCounter_nid_no_patch(const char*); int APS5_VABI select_nid_postfix(int, void*, void*, void*, const void*); int APS5_VABI open_nid_postfix(const char*, int, int); @@ -170,6 +175,19 @@ public: static void APS5_VABI IgnoreNetCtlEvent(int, void*) {} +struct NetCtlEventRecord { + int calls = 0; + int event = 0; + void* arg = nullptr; +}; + +static void APS5_VABI RecordNetCtlEvent(int event, void* arg) { + auto* record = static_cast(arg); + ++record->calls; + record->event = event; + record->arg = arg; +} + static bool Failed(std::int64_t result, int error) { return result == static_cast(0x80410100u | static_cast(error)) && *sceNetErrnoLoc() == error; } @@ -1631,6 +1649,185 @@ static void CheckSockInfo() { Require(RequireSockInfoList(0) == earlier); } +static std::array RequireSockInfo6(int socket, int type, bool ipv6, bool nonblock, const std::array& local, + const std::array& remote, int requestFlags) { + std::array records; + records.fill(0xa5); + Require(sceNetGetSockInfo6(socket, records.data(), 2, requestFlags) == 1); + std::array expected{}; + std::memcpy(expected.data() + 0x24, &socket, sizeof(socket)); + expected[0x28] = static_cast(type); + if (ipv6) { + std::memcpy(expected.data() + 0x6c, local.data() + 8, 16); + std::memcpy(expected.data() + 0x7c, remote.data() + 8, 16); + } else { + std::memcpy(expected.data() + 0x34, local.data() + 4, 4); + std::memcpy(expected.data() + 0x38, remote.data() + 4, 4); + } + std::memcpy(expected.data() + 0x3c, local.data() + 2, 2); + std::memcpy(expected.data() + 0x3e, remote.data() + 2, 2); + const int flags = 1 | (nonblock ? 0x200000 : 0) | (ipv6 ? 0x400000 : 0); + std::memcpy(expected.data() + 0x48, &flags, sizeof(flags)); + for (const auto [offset, option] : {std::array{0x64, 0x1002}, std::array{0x68, 0x1001}}) { + int size = 0; + std::uint32_t length = sizeof(size); + Require(sceNetGetsockopt(socket, 0xffff, option, &size, &length) == 0 && size > 0); + std::memcpy(expected.data() + offset, &size, sizeof(size)); + } + Require(std::equal(expected.begin(), expected.end(), records.begin())); + Require(std::all_of(records.begin() + 0xa0, records.end(), [](auto byte) { return byte == 0xa5; })); + return expected; +} + +static std::vector RequireSockInfo6List(int flags) { + const int count = sceNetGetSockInfo6(-1, nullptr, 0, flags); + Require(count >= 0); + std::vector records(static_cast(count + 1) * 0xa0, 0xa5); + Require(sceNetGetSockInfo6(-1, records.data(), count + 1, flags) == count); + Require(std::all_of(records.end() - 0xa0, records.end(), [](auto byte) { return byte == 0xa5; })); + std::vector unbounded(records.size(), 0xa5); + Require(sceNetGetSockInfo6(-1, unbounded.data(), INT32_MAX, flags) == count && unbounded == records); + std::vector sockets; + for (int index = 0; index < count; ++index) { + const auto listed = records.begin() + index * 0xa0; + int socket = 0; + int status = 0; + std::memcpy(&socket, &listed[0x24], sizeof(socket)); + std::memcpy(&status, &listed[0x48], sizeof(status)); + Require(sockets.empty() || socket > sockets.back()); + Require((flags & 0x1000) != 0 || (status & 0x400000) != 0); + std::array single; + Require(sceNetGetSockInfo6(socket, single.data(), 1, flags) == 1 && std::equal(single.begin(), single.end(), listed)); + sockets.push_back(socket); + } + for (const int limit : {1, 2}) { + const int written = std::min(count, limit); + std::vector limited(records.size(), 0xa5); + Require(sceNetGetSockInfo6(-1, limited.data(), limit, flags) == written); + Require(std::equal(limited.begin(), limited.begin() + written * 0xa0, records.begin())); + Require(std::all_of(limited.begin() + written * 0xa0, limited.end(), [](auto byte) { return byte == 0xa5; })); + } + return sockets; +} + +static std::vector WithSockets(std::vector sockets, std::initializer_list added) { + sockets.insert(sockets.end(), added); + std::sort(sockets.begin(), sockets.end()); + return sockets; +} + +static void CheckSockInfo6() { + const auto earlier = RequireSockInfo6List(0); + const auto earlierAll = RequireSockInfo6List(0x1000); + Require(RequireSockInfo6List(0x40) == earlier && RequireSockInfo6List(0x1040) == earlierAll); + Require(std::includes(earlierAll.begin(), earlierAll.end(), earlier.begin(), earlier.end())); + + const std::array unnamed{}; + const int datagram = sceNetSocket("sockinfo6-inet", 2, 2, 17); + Require(datagram >= 0); + std::array inetAddress{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(datagram, inetAddress.data(), 16) == 0); + std::uint32_t size = 16; + Require(sceNetGetsockname(datagram, inetAddress.data(), &size) == 0 && size == 16); + const int nonblocking = 1; + Require(sceNetSetsockopt(datagram, 0xffff, 0x1200, &nonblocking, sizeof(nonblocking)) == 0); + RequireSockInfo6(datagram, 2, false, true, inetAddress, unnamed, 0x1000); + RequireSockInfo6(datagram, 2, false, true, inetAddress, unnamed, 0x1040); + + std::array record; + record.fill(0xa5); + const auto untouched = [&record] { return std::all_of(record.begin(), record.end(), [](auto byte) { return byte == 0xa5; }); }; + Require(Failed(sceNetGetSockInfo6(datagram, nullptr, 1, 0x1000), 22)); + Require(Failed(sceNetGetSockInfo6(datagram, record.data(), 0, 0x1000), 22)); + Require(Failed(sceNetGetSockInfo6(datagram, record.data(), -1, 0x1000), 22)); + Require(Failed(sceNetGetSockInfo6(-1, record.data(), 0, 0), 22)); + Require(Failed(sceNetGetSockInfo6(-1, record.data(), -5, 0x1000), 22)); + for (const int flags : {1, 0x41, 0x80, 0x1001, 0x2000, 0x400000, 0x1048}) { + Require(Failed(sceNetGetSockInfo6(datagram, record.data(), 1, flags), 22)); + Require(Failed(sceNetGetSockInfo6(-1, nullptr, 0, flags), 22)); + Require(Failed(sceNetGetSockInfo6(-1, record.data(), 1, flags), 22)); + } + Require(Failed(sceNetGetSockInfo6(datagram, record.data(), 1, 0), 47)); + Require(Failed(sceNetGetSockInfo6(datagram, record.data(), 1, 0x40), 47)); + Require(untouched()); + const int closed = sceNetSocket("sockinfo6-closed", 2, 2, 17); + Require(closed >= 0 && sceNetSocketClose(closed) == 0); + for (const int flags : {0, 0x40, 0x1000, 0x1040}) { + Require(Failed(sceNetGetSockInfo6(closed, record.data(), 1, flags), 9)); + Require(Failed(sceNetGetSockInfo6(INT32_MAX, record.data(), 1, flags), 9)); + } + Require(untouched()); + Require(RequireSockInfo6List(0) == earlier); + Require(RequireSockInfo6List(0x1000) == WithSockets(earlierAll, {datagram})); + + if (ipv6Unavailable) { + Require(sceNetSocketClose(datagram) == 0); + Require(Failed(sceNetGetSockInfo6(datagram, record.data(), 1, 0x1000), 9)); + Require(RequireSockInfo6List(0x1000) == earlierAll); + return; + } + + const int udp = sceNetSocket("sockinfo6-datagram", 28, 2, 17); + Require(udp >= 0); + RequireSockInfo6(udp, 2, true, false, unnamed, unnamed, 0); + std::array udpAddress{28, 28}; + udpAddress[23] = 1; + Require(sceNetBind_nid_postfix(udp, udpAddress.data(), udpAddress.size()) == 0); + size = udpAddress.size(); + Require(sceNetGetsockname(udp, udpAddress.data(), &size) == 0 && size == udpAddress.size()); + Require(udpAddress[2] != 0 || udpAddress[3] != 0); + RequireSockInfo6(udp, 2, true, false, udpAddress, unnamed, 0); + RequireSockInfo6(udp, 2, true, false, udpAddress, unnamed, 0x40); + RequireSockInfo6(udp, 2, true, false, udpAddress, unnamed, 0x1000); + RequireSockInfo6(udp, 2, true, false, udpAddress, unnamed, 0x1040); + + const int listener = sceNetSocket("sockinfo6-listener", 28, 1, 6); + std::array listening{28, 28}; + listening[23] = 1; + Require(listener >= 0 && sceNetBind_nid_postfix(listener, listening.data(), listening.size()) == 0 && sceNetListen(listener, 1) == 0); + size = listening.size(); + Require(sceNetGetsockname(listener, listening.data(), &size) == 0 && size == listening.size()); + RequireSockInfo6(listener, 1, true, false, listening, unnamed, 0); + const int client = sceNetSocket("sockinfo6-client", 28, 1, 6); + Require(client >= 0 && sceNetConnect(client, listening.data(), listening.size()) == 0); + const int accepted = sceNetAccept(listener, nullptr, nullptr); + Require(accepted >= 0); + std::array clientAddress{}; + size = clientAddress.size(); + Require(sceNetGetsockname(client, clientAddress.data(), &size) == 0 && size == clientAddress.size()); + Require(clientAddress[2] != 0 || clientAddress[3] != 0); + RequireSockInfo6(client, 1, true, false, clientAddress, listening, 0); + RequireSockInfo6(accepted, 1, true, false, listening, clientAddress, 0); + + const int peer = sceNetSocket("sockinfo6-peer", 28, 2, 17); + std::array peerAddress{28, 28}; + peerAddress[23] = 1; + Require(peer >= 0 && sceNetBind_nid_postfix(peer, peerAddress.data(), peerAddress.size()) == 0); + size = peerAddress.size(); + Require(sceNetGetsockname(peer, peerAddress.data(), &size) == 0 && size == peerAddress.size()); + Require(sceNetConnect(peer, udpAddress.data(), udpAddress.size()) == 0); + RequireSockInfo6(peer, 2, true, false, peerAddress, udpAddress, 0); + const std::array buffers{65536, 32768}; + Require(sceNetSetsockopt(peer, 0xffff, 0x1200, &nonblocking, sizeof(nonblocking)) == 0); + Require(sceNetSetsockopt(peer, 0xffff, 0x1002, &buffers[0], sizeof(buffers[0])) == 0); + Require(sceNetSetsockopt(peer, 0xffff, 0x1001, &buffers[1], sizeof(buffers[1])) == 0); + const auto configured = RequireSockInfo6(peer, 2, true, true, peerAddress, udpAddress, 0); + Require(std::memcmp(configured.data() + 0x64, buffers.data(), sizeof(buffers)) == 0); + Require(std::all_of(configured.begin() + 0x34, configured.begin() + 0x3c, [](auto byte) { return byte == 0; })); + Require(sceNetGetSockInfo6(peer, record.data(), 1, 0x1040) == 1 && std::equal(configured.begin(), configured.end(), record.begin())); + + Require(RequireSockInfo6List(0) == WithSockets(earlier, {udp, listener, client, accepted, peer})); + Require(RequireSockInfo6List(0x40) == WithSockets(earlier, {udp, listener, client, accepted, peer})); + Require(RequireSockInfo6List(0x1000) == WithSockets(earlierAll, {datagram, udp, listener, client, accepted, peer})); + Require(Failed(sceNetGetSockInfo6(datagram, record.data(), 1, 0), 47)); + + for (const int descriptor : {peer, accepted, client, listener, udp}) Require(sceNetSocketClose(descriptor) == 0); + Require(Failed(sceNetGetSockInfo6(udp, record.data(), 1, 0), 9)); + Require(RequireSockInfo6List(0) == earlier); + Require(sceNetSocketClose(datagram) == 0); + Require(RequireSockInfo6List(0x1000) == earlierAll); +} + static void CheckDatagramDisconnect() { const int peer = sceNetSocket("disconnect-peer", 2, 2, 17); const int other = sceNetSocket("disconnect-other", 2, 2, 17); @@ -1730,6 +1927,7 @@ int main() { CheckDatagramDisconnect(); CheckCloseKernelSocket(); CheckSockInfo(); + CheckSockInfo6(); const int closed = sceNetSocket("closed-by-kernel", 2, 2, 17); Require(closed >= 0); @@ -2219,6 +2417,46 @@ int main() { } Require(sceNetCtlUnregisterCallbackV6(0) == 0); } + { + int cid = -1; + Require(sceNetCtlRegisterCallbackForNpToolkit(nullptr, nullptr, &cid) == static_cast(0x80412107)); + Require(sceNetCtlRegisterCallbackForNpToolkit(IgnoreNetCtlEvent, nullptr, nullptr) == static_cast(0x80412107)); + int cids[8] = {}; + for (int i = 0; i < 8; ++i) { + Require(sceNetCtlRegisterCallbackForNpToolkit(IgnoreNetCtlEvent, nullptr, &cids[i]) == 0); + Require(cids[i] == i); + } + Require(sceNetCtlRegisterCallbackForNpToolkit(IgnoreNetCtlEvent, nullptr, &cid) == static_cast(0x80412103)); + Require(sceNetCtlRegisterCallback(IgnoreNetCtlEvent, nullptr, &cid) == 0 && cid == 0); + Require(sceNetCtlUnregisterCallback(0) == 0); + Require(sceNetCtlUnregisterCallbackForNpToolkit(-1) == static_cast(0x80412105)); + Require(sceNetCtlUnregisterCallbackForNpToolkit(8) == static_cast(0x80412105)); + Require(sceNetCtlUnregisterCallbackForNpToolkit(3) == 0); + Require(sceNetCtlRegisterCallbackForNpToolkit(IgnoreNetCtlEvent, nullptr, &cid) == 0); + Require(cid == 3); + for (int i = 0; i < 8; ++i) { + Require(sceNetCtlUnregisterCallbackForNpToolkit(i) == 0); + } + + NetCtlEventRecord toolkit; + NetCtlEventRecord plain; + Require(sceNetCtlRegisterCallbackForNpToolkit(RecordNetCtlEvent, &toolkit, &cid) == 0 && cid == 0); + Require(sceNetCtlCheckCallback() == 0 && toolkit.calls == 0); + Require(sceNetCtlCheckCallbackForNpToolkit() == 0); + Require(toolkit.calls == 1 && toolkit.arg == &toolkit && (toolkit.event == 1 || toolkit.event == 3)); + int current = -1; + Require(sceNetCtlGetState(¤t) == 0 && toolkit.event == (current == 3 ? 3 : 1)); + Require(sceNetCtlCheckCallbackForNpToolkit() == 0 && toolkit.calls == 1); + int plainCid = -1; + Require(sceNetCtlRegisterCallback(RecordNetCtlEvent, &plain, &plainCid) == 0); + Require(sceNetCtlCheckCallbackForNpToolkit() == 0 && plain.calls == 0); + Require(sceNetCtlCheckCallback() == 0 && plain.calls == 1 && toolkit.calls == 1); + Require(sceNetCtlUnregisterCallback(plainCid) == 0); + Require(sceNetCtlUnregisterCallbackForNpToolkit(cid) == 0); + Require(sceNetCtlRegisterCallbackForNpToolkit(RecordNetCtlEvent, &toolkit, &cid) == 0); + Require(sceNetCtlCheckCallbackForNpToolkit() == 0 && toolkit.calls == 2); + Require(sceNetCtlUnregisterCallbackForNpToolkit(cid) == 0); + } if (ipv6Unavailable) { std::puts("skipped the IPv6 socket checks, the host has no usable IPv6 loopback"); return 77; diff --git a/core/libs/tests/GuestSchedPriority.cpp b/core/libs/tests/GuestSchedPriority.cpp index 0b15d2becd..f83d5a751e 100644 --- a/core/libs/tests/GuestSchedPriority.cpp +++ b/core/libs/tests/GuestSchedPriority.cpp @@ -100,14 +100,12 @@ int main() { Require(setpriority_nid_postfix(PrioProcess, 0, -99) == 0 && getpriority_nid_postfix(PrioProcess, 0) == -20); Require(setpriority_nid_postfix(PrioProcess, 0, 0) == 0 && getpriority_nid_postfix(PrioProcess, 0) == 0); GuestRtPrio rtprio{7, 7}; + Require(rtprio_thread_nid_postfix(0, 0, &rtprio) == 0 && rtprio.type == 10 && rtprio.prio == 700); bool rtprioThrew = false; - try { rtprio_thread_nid_postfix(0, 0, &rtprio); } catch (const std::exception&) { rtprioThrew = true; } - Require(rtprioThrew); - rtprioThrew = false; try { rtprio_thread_nid_postfix(1, 0, &rtprio); } catch (const std::exception&) { rtprioThrew = true; } Require(rtprioThrew); *__error_nid_postfix() = 0; - Require(rtprio_thread_nid_postfix(2, 0, &rtprio) == -1 && *__error_nid_postfix() == 22); + Require(rtprio_thread_nid_postfix(2, 0, &rtprio) == -1 && *__error_nid_postfix() == 3); *__error_nid_postfix() = 0; Require(rtprio_thread_nid_postfix(0, 0, nullptr) == -1 && *__error_nid_postfix() == 14); *__error_nid_postfix() = 0; diff --git a/core/libs/tests/GuestShmOpen.cpp b/core/libs/tests/GuestShmOpen.cpp index ff4f8fb16c..c81cfc704d 100644 --- a/core/libs/tests/GuestShmOpen.cpp +++ b/core/libs/tests/GuestShmOpen.cpp @@ -68,13 +68,7 @@ int main() { RequireInvalid("/anyps5-shm", 0x1); RequireInvalid("/anyps5-shm", 0x202 | 0x8); #ifdef _WIN32 - bool threw = false; - try { - shm_open_nid_postfix("/anyps5-shm", 0x202, 0600); - } catch (const std::runtime_error&) { - threw = true; - } - Require(threw); + RequireInvalid("", 0x202); #else RequireInvalid("", 0x202); #endif diff --git a/core/libs/tests/GuestSignals.cpp b/core/libs/tests/GuestSignals.cpp index fcd41cca6f..9879994a1a 100644 --- a/core/libs/tests/GuestSignals.cpp +++ b/core/libs/tests/GuestSignals.cpp @@ -29,6 +29,7 @@ int APS5_VABI sigsuspend_nid_postfix(const GuestSignalSet*); int APS5_VABI _is_signal_return_nid_postfix(std::uint64_t); unsigned int APS5_VABI GuestAlarm_nid_no_patch(unsigned int); int APS5_VABI sigpending_nid_postfix(GuestSignalSet*); +int APS5_VABI siginterrupt_nid_postfix(int, int); } struct GuestSigaction { std::uintptr_t handler; @@ -467,4 +468,24 @@ int main() { Require(sigaction_nid_postfix(30, nullptr, &interruptible) == 0 && interruptible.flags == 0x2); Require(signal_nid_postfix(30, nullptr) == Callback && signal_nid_postfix(31, nullptr) == Callback && signal_nid_postfix(128, nullptr) == Callback); + Require(signal_nid_postfix(30, Callback) == nullptr); + Require(siginterrupt_nid_postfix(30, 1) == 0 && _sigintr_nid_postfix.bits[0] == (1u << 29)); + Require(sigaction_nid_postfix(30, nullptr, &interruptible) == 0 && + interruptible.handler == reinterpret_cast(Callback) && interruptible.flags == 0); + Require(signal_nid_postfix(30, Callback) == Callback); + Require(sigaction_nid_postfix(30, nullptr, &interruptible) == 0 && interruptible.flags == 0); + Require(siginterrupt_nid_postfix(30, 0) == 0 && _sigintr_nid_postfix.bits[0] == 0); + Require(sigaction_nid_postfix(30, nullptr, &interruptible) == 0 && + interruptible.handler == reinterpret_cast(Callback) && interruptible.flags == 0x2); + Require(siginterrupt_nid_postfix(128, 1) == 0 && _sigintr_nid_postfix.bits[3] == (1u << 31)); + Require(sigaction_nid_postfix(128, nullptr, &interruptible) == 0 && interruptible.handler == 0 && interruptible.flags == 0); + Require(siginterrupt_nid_postfix(128, 0) == 0 && _sigintr_nid_postfix.bits[3] == 0); + Require(sigaction_nid_postfix(128, nullptr, &interruptible) == 0 && interruptible.flags == 0x2); + for (const int badSignal : {0, -1, 129}) { + *__error_nid_postfix() = 0; + Require(siginterrupt_nid_postfix(badSignal, 1) == -1 && *__error_nid_postfix() == 22); + } + Require(_sigintr_nid_postfix.bits[0] == 0 && _sigintr_nid_postfix.bits[3] == 0); + Require(signal_nid_postfix(30, nullptr) == Callback); + Require(sigaction_nid_postfix(128, &restored, nullptr) == 0); } diff --git a/core/libs/tests/GuestSocketEvents.cpp b/core/libs/tests/GuestSocketEvents.cpp index 1ef4eea859..20ecb18f99 100644 --- a/core/libs/tests/GuestSocketEvents.cpp +++ b/core/libs/tests/GuestSocketEvents.cpp @@ -1,9 +1,12 @@ #include "SceTypes.hpp" #include +#include #include #include #include +#include #include +#include #include extern "C" { @@ -19,6 +22,8 @@ int APS5_VABI close_nid_postfix(int); int APS5_VABI pipe_nid_postfix(int*); 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); +int APS5_VABI open_nid_postfix(const char*, int, int); +std::int64_t APS5_VABI lseek_nid_postfix(int, std::int64_t, int); int APS5_VABI sceKernelCreateEqueue(KernelEqueue* eq, const char* name); int APS5_VABI sceKernelDeleteEqueue(KernelEqueue eq); int APS5_VABI sceKernelWaitEqueue(KernelEqueue eq, KernelEvent* ev, int num, int* out, const KernelUseconds* timo); @@ -96,8 +101,83 @@ static void PipeReadEvents() { Require(sceKernelDeleteEqueue(eq) == SCE_OK); } +static void RegularFileEvents() { + const std::string path = "anyps5-equeue-file-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + const int file = open_nid_postfix(path.c_str(), 0x0002 | 0x0200, 0644); + Require(file >= 0); + Require(write_nid_postfix(file, "0123456789", 10) == 10); + Require(lseek_nid_postfix(file, 2, 0) == 2); + KernelEqueue eq = 0; + Require(sceKernelCreateEqueue(&eq, "file") == SCE_OK); + const KernelUseconds shortTimeout = 20000; + const KernelUseconds timeout = 1000000; + int marker = 0; + int count = 0; + std::array events{}; + Require(sceKernelAddReadEvent(eq, file, 1, &marker) == SCE_OK); + Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, &timeout) == SCE_OK); + Require(count == 1 && events[0].ident == static_cast(file) && events[0].filter == EVFILT_READ); + Require(events[0].data == 8 && events[0].udata == &marker); + Require(lseek_nid_postfix(file, 0, 2) == 10); + Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, &shortTimeout) == SCE_KERNEL_ERROR_ETIMEDOUT); + const int appender = open_nid_postfix(path.c_str(), 0x0002, 0); + Require(appender >= 0); + std::thread writer([appender] { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(lseek_nid_postfix(appender, 0, 2) == 10); + Require(write_nid_postfix(appender, "abc", 3) == 3); + }); + Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, nullptr) == SCE_OK); + writer.join(); + Require(count == 1 && events[0].data == 3); + Require(sceKernelDeleteReadEvent(eq, file) == SCE_OK); + Require(sceKernelAddWriteEvent(eq, file, 0, nullptr) == SCE_OK); + Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, &timeout) == SCE_OK); + Require(count == 1 && events[0].filter == EVFILT_WRITE && events[0].data == 0); + Require(sceKernelDeleteWriteEvent(eq, file) == SCE_OK); + Require(Rejects(sceKernelAddReadEvent, eq, file, 64)); + Require(Rejects(sceKernelAddWriteEvent, eq, file, 64)); + Require(sceKernelAddReadEvent(eq, file, 0, nullptr) == SCE_OK); + Require(close_nid_postfix(file) == 0); + Require(sceKernelDeleteReadEvent(eq, file) == SCE_KERNEL_ERROR_ENOENT); + Require(sceKernelAddReadEvent(eq, file, 0, nullptr) == SCE_KERNEL_ERROR_EBADF); + Require(close_nid_postfix(appender) == 0); + Require(sceKernelDeleteEqueue(eq) == SCE_OK); + Require(std::filesystem::remove(path)); +} + +static void CloseWhileWaitingOnFile() { + const std::string path = "anyps5-equeue-close-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + const int created = open_nid_postfix(path.c_str(), 0x0002 | 0x0200, 0644); + Require(created >= 0); + Require(close_nid_postfix(created) == 0); + KernelEqueue eq = 0; + Require(sceKernelCreateEqueue(&eq, "close") == SCE_OK); + std::atomic stop{false}; + std::thread waiter([eq, &stop] { + const KernelUseconds timeout = 1000; + std::array events{}; + int count = 0; + while (!stop.load()) { + Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, &timeout) == SCE_KERNEL_ERROR_ETIMEDOUT); + } + }); + for (int i = 0; i < 2000; ++i) { + const int file = open_nid_postfix(path.c_str(), 0x0002, 0); + Require(file >= 0); + Require(sceKernelAddReadEvent(eq, file, 0, nullptr) == SCE_OK); + Require(close_nid_postfix(file) == 0); + } + stop.store(true); + waiter.join(); + Require(sceKernelDeleteEqueue(eq) == SCE_OK); + Require(std::filesystem::remove(path)); +} + int main() { PipeReadEvents(); + RegularFileEvents(); + CloseWhileWaitingOnFile(); const int listener = socket_nid_postfix(2, 1, 6); Require(listener >= 0); std::array address{16, 2, 0, 0, 127, 0, 0, 1}; diff --git a/core/libs/tests/GuestSocketOptions.cpp b/core/libs/tests/GuestSocketOptions.cpp index b1da1a68d0..f876f686fe 100644 --- a/core/libs/tests/GuestSocketOptions.cpp +++ b/core/libs/tests/GuestSocketOptions.cpp @@ -158,16 +158,41 @@ void BooleanOptionsReportTheirBit() { Require(GetInt(datagram, SoBroadcast) == SoBroadcast, "SO_BROADCAST reads back as its bit"); SetInt(stream, SoNoSigpipe, 1); Require(GetInt(stream, SoNoSigpipe) == SoNoSigpipe, "SO_NOSIGPIPE reads back as its bit"); - for (const int option : {SoReusePort, SoDebug, SoUseLoopback, SoTimestamp}) { - const int one = 1; - Require(SetFails(stream, SolSocket, option, &one, sizeof(one), ErrorNoProtocolOption), "an option no title needs must fail with ENOPROTOOPT (42)"); + for (const int option : {SoReusePort, SoDebug, SoUseLoopback, SoTimestamp, 0x2000, 0x10000}) { + Require(GetInt(stream, option) == 0, "a kept flag starts off"); + SetInt(stream, option, 5); + Require(GetInt(stream, option) == option, "a kept flag reads back as its bit"); + SetInt(stream, option, 0); + Require(GetInt(stream, option) == 0, "a kept flag turns off"); + } + for (const int option : {0x4000, 0x8000}) { + SetInt(stream, option, 1); int value = 0; std::uint32_t size = sizeof(value); - Require(getsockopt_nid_postfix(stream, SolSocket, option, &value, &size) == -1 && *__error_nid_postfix() == ErrorNoProtocolOption, "getsockopt of an option no title needs must fail with ENOPROTOOPT (42)"); + Require(getsockopt_nid_postfix(stream, SolSocket, option, &value, &size) == -1 && *__error_nid_postfix() == ErrorNoProtocolOption, "SO_NO_OFFLOAD and SO_NO_DDP are accepted but cannot be read"); } + const unsigned char narrowFlag = 1; + Require(SetFails(stream, SolSocket, SoDebug, &narrowFlag, 1, ErrorInvalid), "a flag shorter than an int must fail with EINVAL (22)"); Require(close_nid_postfix(stream) == 0 && close_nid_postfix(datagram) == 0, "close failed"); } +void ReusePortSharesAPort() { + const int first = socket_nid_postfix(2, Datagram, 0); + const int second = socket_nid_postfix(2, Datagram, 0); + const int third = socket_nid_postfix(2, Datagram, 0); + Require(first >= 0 && second >= 0 && third >= 0, "socket failed"); + SetInt(first, SoReusePort, 1); + SetInt(second, SoReusePort, 1); + Require(GetInt(first, SoReusePort) == SoReusePort && GetInt(first, 0x4) == 0, "SO_REUSEPORT does not read back as SO_REUSEADDR"); + auto address = Loopback(0); + Require(bind_nid_postfix(first, address.data(), address.size()) == 0, "bind failed"); + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(first, address.data(), &size) == 0, "getsockname failed"); + Require(bind_nid_postfix(second, address.data(), address.size()) == 0, "a second SO_REUSEPORT socket must bind the same port"); + Require(bind_nid_postfix(third, address.data(), address.size()) == -1 && *__error_nid_postfix() == 48, "a socket without SO_REUSEPORT must fail with EADDRINUSE (48)"); + Require(close_nid_postfix(first) == 0 && close_nid_postfix(second) == 0 && close_nid_postfix(third) == 0, "close failed"); +} + void LingerFollowsFreeBsd() { const int stream = socket_nid_postfix(2, Stream, 0); Require(stream >= 0, "socket failed"); @@ -333,6 +358,7 @@ int main() { ReportsTypeAndListening(); ReportsAndClearsTheConnectError(); BooleanOptionsReportTheirBit(); + ReusePortSharesAPort(); LingerFollowsFreeBsd(); LengthsFollowFreeBsd(); BuffersFollowThePs5(); diff --git a/core/libs/tests/GuestSocketStreamOptions.cpp b/core/libs/tests/GuestSocketStreamOptions.cpp index 2d03377a36..58c21ea728 100644 --- a/core/libs/tests/GuestSocketStreamOptions.cpp +++ b/core/libs/tests/GuestSocketStreamOptions.cpp @@ -91,12 +91,13 @@ int main() { Require(client >= 0 && connect_nid_postfix(client, address.data(), address.size()) == 0); const int server = accept_nid_postfix(listener, nullptr, nullptr); Require(server >= 0); - Require(Set(listener, SocketLevel, NoSigpipe, 1) && Set(listener, TcpLevel, TcpNoDelay, 1)); + Require(Set(listener, SocketLevel, NoSigpipe, 1) && Set(listener, TcpLevel, TcpNoDelay, 1) && Set(listener, TcpLevel, 0x100, 60)); const int secondClient = socket_nid_postfix(2, 1, 0); Require(secondClient >= 0 && connect_nid_postfix(secondClient, address.data(), address.size()) == 0); const int inheriting = accept_nid_postfix(listener, nullptr, nullptr); Require(inheriting >= 0); Require(Get(inheriting, SocketLevel, NoSigpipe) == NoSigpipe && Get(inheriting, TcpLevel, TcpNoDelay) == 4); + Require(Get(inheriting, TcpLevel, 0x100) == 60 && Get(server, TcpLevel, 0x100) == 7200); Require(Get(server, SocketLevel, NoSigpipe) == 0 && Get(server, TcpLevel, TcpNoDelay) == 0); Require(close_nid_postfix(inheriting) == 0 && close_nid_postfix(secondClient) == 0); const int datagram = socket_nid_postfix(2, 2, 0); @@ -114,7 +115,21 @@ int main() { int value = 0; Require(Set(client, TcpLevel, TcpNoDelay, 0) && Get(client, TcpLevel, TcpNoDelay) == 0); Require(Fails(setsockopt_nid_postfix(client, TcpLevel, TcpNoDelay, &one, sizeof(one) - 1), Invalid)); - Require(Fails(setsockopt_nid_postfix(client, TcpLevel, TcpMaximumSegment, &one, sizeof(one)), NoProtocolOption)); + Require(Fails(setsockopt_nid_postfix(client, TcpLevel, TcpMaximumSegment, &one, sizeof(one)), Invalid)); + const int fresh = socket_nid_postfix(2, 1, 0); + Require(fresh >= 0 && Get(fresh, TcpLevel, TcpMaximumSegment) == 536); + for (const int segment : {1000, 175, 0, -1}) { + Require(Fails(setsockopt_nid_postfix(fresh, TcpLevel, TcpMaximumSegment, &segment, sizeof(segment)), Invalid)); + } + Require(Set(fresh, TcpLevel, TcpMaximumSegment, 300) && Get(fresh, TcpLevel, TcpMaximumSegment) == 300); + Require(Get(fresh, TcpLevel, 0x100) == 7200 && Get(fresh, TcpLevel, 0x200) == 75 && Get(fresh, TcpLevel, 0x400) == 8); + Require(Set(fresh, TcpLevel, 0x100, 60) && Get(fresh, TcpLevel, 0x100) == 60); + Require(Set(fresh, TcpLevel, 0x100, 0) && Get(fresh, TcpLevel, 0x100) == 7200); + Require(Set(fresh, TcpLevel, 0x200, 10) && Get(fresh, TcpLevel, 0x200) == 10); + Require(Set(fresh, TcpLevel, 0x400, 3) && Get(fresh, TcpLevel, 0x400) == 3); + Require(Set(fresh, TcpLevel, 0x400, -1) && Get(fresh, TcpLevel, 0x400) == -1); + Require(Fails(setsockopt_nid_postfix(fresh, TcpLevel, 0x7777, &one, sizeof(one)), NoProtocolOption)); + Require(close_nid_postfix(fresh) == 0); length = sizeof(value); Require(Fails(setsockopt_nid_postfix(datagram, TcpLevel, TcpNoDelay, &one, sizeof(one)), Invalid)); Require(Fails(getsockopt_nid_postfix(datagram, TcpLevel, TcpNoDelay, &value, &length), Invalid)); diff --git a/core/libs/tests/GuestUnderscoreEntries.cpp b/core/libs/tests/GuestUnderscoreEntries.cpp index edb44de05b..a7e94b5e01 100644 --- a/core/libs/tests/GuestUnderscoreEntries.cpp +++ b/core/libs/tests/GuestUnderscoreEntries.cpp @@ -77,7 +77,7 @@ constexpr int Stream = 1; constexpr int Datagram = 2; constexpr int TcpLevel = 6; constexpr int TcpNoDelay = 1; -constexpr int TcpMaximumSegment = 2; +constexpr int UnknownTcpOption = 0x7777; constexpr int OutOfBand = 0x1; constexpr int Truncated = 0x10; constexpr int Interrupted = 4; @@ -134,7 +134,7 @@ static void StreamSockets() { Require(_setsockopt_nid_postfix(client, TcpLevel, TcpNoDelay, &enabled, sizeof(enabled)) == 0); valueSize = sizeof(value); Require(_getsockopt_nid_postfix(client, TcpLevel, TcpNoDelay, &value, &valueSize) == 0 && value == 4 && valueSize == sizeof(value)); - Require(Fails(_setsockopt_nid_postfix(client, TcpLevel, TcpMaximumSegment, &enabled, sizeof(enabled)), NoProtocolOption)); + Require(Fails(_setsockopt_nid_postfix(client, TcpLevel, UnknownTcpOption, &enabled, sizeof(enabled)), NoProtocolOption)); const char text[] = "underscore stream"; Require(_sendto_nid_postfix(client, text, sizeof(text), 0, nullptr, 0) == sizeof(text)); diff --git a/core/libs/tests/GuestUserService.cpp b/core/libs/tests/GuestUserService.cpp index 83e78239ca..88116c03d8 100644 --- a/core/libs/tests/GuestUserService.cpp +++ b/core/libs/tests/GuestUserService.cpp @@ -2,6 +2,7 @@ #include "prx/libSceUserService/UserService.hpp" #include #include +#include #include #include #include @@ -27,6 +28,8 @@ int APS5_VABI sceUserServiceGetAccessibilityVibration(int user_id, std::int32_t* int APS5_VABI sceUserServiceGetAccessibilityZoom(); int APS5_VABI sceUserServiceGetAccessibilityZoomEnabled(int user_id, std::int32_t* value); int APS5_VABI sceUserServiceGetAccessibilityZoomFollowFocus(int user_id, std::int32_t* value); +int APS5_VABI sceUserServiceGetUserName(int user_id, char* name, std::size_t size); +int APS5_VABI sceUserServiceGetUserNumber(int user_id, std::int32_t* number); } namespace { @@ -179,4 +182,38 @@ int main() { threwZoom = true; } Require(threwZoom); + + char name[32]{}; + Require(sceUserServiceGetUserName(USER_SERVICE_INITIAL_USER_ID, nullptr, sizeof(name)) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceGetUserName(123, name, sizeof(name)) == USER_SERVICE_ERROR_UNKNOWN_USER); + Require(sceUserServiceGetUserName(USER_SERVICE_INITIAL_USER_ID, name, 5) == USER_SERVICE_ERROR_BUFFER_TOO_SHORT); + Require(sceUserServiceGetUserName(USER_SERVICE_INITIAL_USER_ID, name, sizeof(name)) == USER_SERVICE_OK); + Require(std::strcmp(name, "Player") == 0); + Require(sceUserServiceGetUserName(USER_SERVICE_INITIAL_USER_ID + 1, name, sizeof(name)) == USER_SERVICE_OK); + Require(std::strcmp(name, "Player2") == 0); + + std::int32_t number = -1; + Require(sceUserServiceGetUserNumber(USER_SERVICE_INITIAL_USER_ID, nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceGetUserNumber(123, &number) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceGetUserNumber(USER_SERVICE_INITIAL_USER_ID, &number) == USER_SERVICE_OK); + Require(number == 1); + Require(sceUserServiceGetUserNumber(USER_SERVICE_INITIAL_USER_ID + 1, &number) == USER_SERVICE_OK); + Require(number == 2); + + Require(sceUserServiceGetUserColor(USER_SERVICE_INITIAL_USER_ID + 1, &color) == USER_SERVICE_OK); + Require(color == 1); + + UserServiceSetUserConnected_nid_postfix(1, true); + Require(sceUserServiceGetRegisteredUserIdList(®istered) == USER_SERVICE_OK); + Require(registered.user_id[0] == USER_SERVICE_INITIAL_USER_ID); + Require(registered.user_id[1] == USER_SERVICE_INITIAL_USER_ID + 1); + for (int i = 2; i < 16; ++i) { + Require(registered.user_id[i] == USER_SERVICE_USER_ID_INVALID); + } + UserServiceSetUserConnected_nid_postfix(1, false); + Require(sceUserServiceGetRegisteredUserIdList(®istered) == USER_SERVICE_OK); + Require(registered.user_id[0] == USER_SERVICE_INITIAL_USER_ID); + for (int i = 1; i < 16; ++i) { + Require(registered.user_id[i] == USER_SERVICE_USER_ID_INVALID); + } } diff --git a/core/libs/tests/GuestUserServiceLifecycle.cpp b/core/libs/tests/GuestUserServiceLifecycle.cpp index 550acb5353..b1346120f8 100644 --- a/core/libs/tests/GuestUserServiceLifecycle.cpp +++ b/core/libs/tests/GuestUserServiceLifecycle.cpp @@ -82,6 +82,42 @@ int main() { Require(sceUserServiceInitialize(nullptr) == USER_SERVICE_OK); RequireLoginOnce(); + + UserServiceSetUserConnected_nid_postfix(1, true); + SceUserServiceEvent event{}; + Require(sceUserServiceGetEvent(&event) == USER_SERVICE_OK); + Require(event.event_type == 0 && event.user_id == USER_SERVICE_INITIAL_USER_ID + 1); + Require(sceUserServiceGetLoginUserIdList(&list) == USER_SERVICE_OK); + Require(list.user_id[0] == USER_SERVICE_INITIAL_USER_ID); + Require(list.user_id[1] == USER_SERVICE_INITIAL_USER_ID + 1); + + UserServiceSetUserConnected_nid_postfix(1, false); + Require(sceUserServiceGetEvent(&event) == USER_SERVICE_OK); + Require(event.event_type == 1 && event.user_id == USER_SERVICE_INITIAL_USER_ID + 1); + Require(sceUserServiceGetLoginUserIdList(&list) == USER_SERVICE_OK); + Require(list.user_id[0] == USER_SERVICE_INITIAL_USER_ID); + Require(list.user_id[1] == USER_SERVICE_USER_ID_INVALID); + + for (int i = 0; i < 50; ++i) { + UserServiceSetUserConnected_nid_postfix(1, true); + UserServiceSetUserConnected_nid_postfix(1, false); + } + Require(sceUserServiceGetEvent(&event) == USER_SERVICE_ERROR_NO_EVENT); + + UserServiceSetUserConnected_nid_postfix(2, true); + UserServiceSetUserConnected_nid_postfix(2, false); + Require(sceUserServiceGetEvent(&event) == USER_SERVICE_ERROR_NO_EVENT); + + UserServiceSetUserConnected_nid_postfix(2, true); + Require(sceUserServiceGetEvent(&event) == USER_SERVICE_OK); + Require(event.event_type == 0 && event.user_id == USER_SERVICE_INITIAL_USER_ID + 2); + UserServiceSetUserConnected_nid_postfix(2, false); + UserServiceSetUserConnected_nid_postfix(2, true); + Require(sceUserServiceGetEvent(&event) == USER_SERVICE_ERROR_NO_EVENT); + UserServiceSetUserConnected_nid_postfix(2, false); + Require(sceUserServiceGetEvent(&event) == USER_SERVICE_OK); + Require(event.event_type == 1 && event.user_id == USER_SERVICE_INITIAL_USER_ID + 2); + Require(sceUserServiceTerminate() == USER_SERVICE_OK); TestThreadPriority(); diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index fb39edc2d7..55968ce9c2 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -33,6 +33,7 @@ add_library(relinker_core STATIC ${CMAKE_CURRENT_SOURCE_DIR}/io/src/NativePath.cpp ${CMAKE_CURRENT_SOURCE_DIR}/io/src/FileIdentity.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/Amd64OnlyConverter.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/RedZoneFramer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/InstructionScanner.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionDecoder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionRewriter.cpp @@ -301,6 +302,16 @@ if(BUILD_TESTING) add_test(NAME rewrite_boundaries COMMAND rewrite_boundaries_tests) set_tests_properties(rewrite_boundaries PROPERTIES TIMEOUT 20) + add_test_executable(red_zone_framer_tests ${CMAKE_CURRENT_SOURCE_DIR}/codegen/tests/RedZoneFramerTests.cpp) + target_link_libraries(red_zone_framer_tests PRIVATE relinker_core) + add_test(NAME red_zone_framer COMMAND red_zone_framer_tests) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64") + add_test_executable(red_zone_framer_execution_tests ${CMAKE_CURRENT_SOURCE_DIR}/codegen/tests/RedZoneFramerExecutionTests.cpp) + target_link_libraries(red_zone_framer_execution_tests PRIVATE relinker_core) + add_test(NAME red_zone_framer_execution COMMAND red_zone_framer_execution_tests) + set_tests_properties(red_zone_framer_execution PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60) + endif() + 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/cli/include/Cli.hpp b/core/relinker/cli/include/Cli.hpp index cb379408f0..2625b44aac 100644 --- a/core/relinker/cli/include/Cli.hpp +++ b/core/relinker/cli/include/Cli.hpp @@ -16,6 +16,7 @@ struct Args { bool skipSceModule = false; bool toIntel = false; bool toRosetta = false; + bool frameRedZone = false; bool writeRegistry = false; bool toWindows = false; bool toMacos = false; diff --git a/core/relinker/cli/src/CliArgs.cpp b/core/relinker/cli/src/CliArgs.cpp index 3fa915c6d2..47166d9972 100644 --- a/core/relinker/cli/src/CliArgs.cpp +++ b/core/relinker/cli/src/CliArgs.cpp @@ -13,7 +13,7 @@ namespace Cli { const char* Usage() { - return "Usage: relinker [--help] [--windows | --macos] [--windows-diagnostics] [--windows-gui] [--trace-imports] [--windows-fixed-base
] [--linux-fixed-base
] [--macos-fixed-base
] [--skip-syscall-check | --trap-sdk-syscall-stubs] [--skip-sce-module | --sce-module-path ] [--exclude-sce-module ]... [--module-dir ]... [--executable ]... [--to-intel] [--to-rosetta] [unused-filter=0|1|2] [--registry] [--rpath ] [--lazy-binding] [--autorun] [--patches | --no-patches] \n" + return "Usage: relinker [--help] [--windows | --macos] [--windows-diagnostics] [--windows-gui] [--trace-imports] [--windows-fixed-base
] [--linux-fixed-base
] [--macos-fixed-base
] [--skip-syscall-check | --trap-sdk-syscall-stubs] [--skip-sce-module | --sce-module-path ] [--exclude-sce-module ]... [--module-dir ]... [--executable ]... [--to-intel] [--to-rosetta] [--frame-red-zone] [unused-filter=0|1|2] [--registry] [--rpath ] [--lazy-binding] [--autorun] [--patches | --no-patches] \n" " relinker --verify-patches [--verify-output] [--skip-sce-module | --sce-module-path ] [--exclude-sce-module ]... [--module-dir ]... [--executable ]... \n" "Example: relinker input.elf output.elf"; } @@ -105,6 +105,8 @@ Args ParseArgs(int argc, char* argv[]) { args.executables.push_back(argv[++i]); } else if (arg == "--to-intel") { args.toIntel = true; + } else if (arg == "--frame-red-zone") { + args.frameRedZone = true; } else if (arg == "--to-rosetta") { args.toRosetta = true; args.toIntel = true; diff --git a/core/relinker/codegen/include/codegen/CodegenTypes.hpp b/core/relinker/codegen/include/codegen/CodegenTypes.hpp index 021f2bf059..9b116f3657 100644 --- a/core/relinker/codegen/include/codegen/CodegenTypes.hpp +++ b/core/relinker/codegen/include/codegen/CodegenTypes.hpp @@ -76,6 +76,7 @@ struct DecodedInstructionInfo { bool IsTwoByteOpcode; std::uint8_t Opcode; std::uint8_t ModRmRegField; + std::size_t ModRmOffset = 0; }; } diff --git a/core/relinker/codegen/include/codegen/x86/RedZoneFramer.hpp b/core/relinker/codegen/include/codegen/x86/RedZoneFramer.hpp new file mode 100644 index 0000000000..a23fa23206 --- /dev/null +++ b/core/relinker/codegen/include/codegen/x86/RedZoneFramer.hpp @@ -0,0 +1,47 @@ +#ifndef CODEGEN_X86_REDZONEFRAMER_HPP +#define CODEGEN_X86_REDZONEFRAMER_HPP + +#include +#include +#include +#include +#include +#include +#include + +namespace Codegen { + +struct RedZoneFunction { + std::uint64_t Begin = 0; + std::uint64_t End = 0; +}; + +struct RedZoneFrameReport { + std::size_t Functions = 0; + std::size_t RedZoneFunctions = 0; + std::size_t Framed = 0; + std::size_t FramedWithoutFramePointer = 0; + std::size_t Exits = 0; + std::size_t MovedOperands = 0; + std::size_t OperandStubs = 0; + std::size_t StubbedOperands = 0; + std::map Skipped; +}; + +struct RedZoneDisplacement { + Domain::FileByteOffset Offset; + std::vector Bytes; +}; + +std::vector FrameRedZoneFunctions( + const std::vector& fileBytes, + const std::vector& codeSegments, + const std::vector& functions, + const std::vector& existing, + RedZoneFrameReport& report, + std::vector& displacements +); + +} + +#endif diff --git a/core/relinker/codegen/src/x86/RedZoneFramer.cpp b/core/relinker/codegen/src/x86/RedZoneFramer.cpp new file mode 100644 index 0000000000..cfe714faef --- /dev/null +++ b/core/relinker/codegen/src/x86/RedZoneFramer.cpp @@ -0,0 +1,441 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Codegen { + +namespace { + +constexpr std::uint8_t kRegRsp = 4; +constexpr std::uint8_t kRegRbp = 5; +constexpr std::size_t kJumpBytes = 5; +constexpr std::int64_t kRedZoneBytes = 0x80; +constexpr std::int64_t kStackAlignment = 16; +constexpr std::uint8_t kJumpBack[] = {0xe9, 0x00, 0x00, 0x00, 0x00}; + +std::vector lowerRsp(const std::int64_t shift) { + return {0x48, 0x8d, 0x64, 0x24, static_cast(-shift)}; +} + +std::vector raiseRsp(const std::int64_t shift) { + const auto value = static_cast(shift); + return {0x48, 0x8d, 0xa4, 0x24, static_cast(value), static_cast(value >> 8), static_cast(value >> 16), static_cast(value >> 24)}; +} + +struct Insn { + std::size_t Offset; + std::size_t Length; + DecodedInstructionInfo Info; +}; + +std::optional pushedRegister(const std::uint8_t* code, const Insn& insn) { + if (insn.Length == 1 && code[0] >= 0x50 && code[0] <= 0x57 && code[0] != 0x50 + kRegRsp) return static_cast(code[0] - 0x50); + if (insn.Length == 2 && code[0] == 0x41 && code[1] >= 0x50 && code[1] <= 0x57) return static_cast(code[1] - 0x50 + 8); + return std::nullopt; +} + +std::optional poppedRegister(const std::uint8_t* code, const Insn& insn) { + if (insn.Length == 1 && code[0] >= 0x58 && code[0] <= 0x5f && code[0] != 0x58 + kRegRsp) return static_cast(code[0] - 0x58); + if (insn.Length == 2 && code[0] == 0x41 && code[1] >= 0x58 && code[1] <= 0x5f) return static_cast(code[1] - 0x58 + 8); + return std::nullopt; +} + +std::optional rspAdjustment(const std::uint8_t* code, const Insn& insn) { + std::int32_t value = 0; + if (insn.Length == 4 && code[0] == 0x48 && code[1] == 0x83 && (code[2] == 0xec || code[2] == 0xc4)) { + const std::int64_t amount = static_cast(code[3]); + return code[2] == 0xec ? -amount : amount; + } + if (insn.Length == 7 && code[0] == 0x48 && code[1] == 0x81 && (code[2] == 0xec || code[2] == 0xc4)) { + std::memcpy(&value, code + 3, 4); + return code[2] == 0xec ? -std::int64_t{value} : std::int64_t{value}; + } + if (insn.Length == 5 && std::memcmp(code, "\x48\x8d\x64\x24", 4) == 0) return static_cast(code[4]); + if (insn.Length == 8 && std::memcmp(code, "\x48\x8d\xa4\x24", 4) == 0) { + std::memcpy(&value, code + 4, 4); + return value; + } + return std::nullopt; +} + +struct MemoryUse { + bool UsesRsp = false; + bool RegisterRsp = false; + bool RbpBased = false; + std::int64_t RbpDisp = 0; + bool RspBased = false; + std::int64_t RspDisp = 0; + std::uint8_t Mod = 0; + std::size_t DispOffset = 0; + bool Indexed = false; + bool Address = false; +}; + +void extensionBits(const std::uint8_t* code, const Insn& insn, bool& r, bool& x, bool& b) { + const auto& info = insn.Info; + r = x = b = false; + if (info.RexPrefix != 0) { + r = (info.RexPrefix & 4) != 0; + x = (info.RexPrefix & 2) != 0; + b = (info.RexPrefix & 1) != 0; + return; + } + const auto lead = code[info.OpcodeOffset]; + if ((lead == 0xc4 || lead == 0x8f || lead == 0x62) && info.OpcodeOffset + 1 < insn.Length) { + r = (code[info.OpcodeOffset + 1] & 0x80) == 0; + x = (code[info.OpcodeOffset + 1] & 0x40) == 0; + b = (code[info.OpcodeOffset + 1] & 0x20) == 0; + } else if (lead == 0xc5 && info.OpcodeOffset + 1 < insn.Length) { + r = (code[info.OpcodeOffset + 1] & 0x80) == 0; + } +} + +bool twoByteRegisterIsGeneral(const std::uint8_t op) { + return (op >= 0x40 && op <= 0x4f) || op == 0xa3 || op == 0xa4 || op == 0xa5 || op == 0xab || op == 0xac || op == 0xad || op == 0xaf || op == 0xb0 || op == 0xb1 || op == 0xb3 || op == 0xb6 || op == 0xb7 || op == 0xb8 || (op >= 0xbb && op <= 0xbf) || op == 0xc0 || op == 0xc1 || op == 0xc3; +} + +bool twoByteRmIsGeneral(const std::uint8_t op) { + return twoByteRegisterIsGeneral(op) || op == 0x6e || op == 0x7e || op == 0xba; +} + +bool vexGeneralUsesRsp(const std::uint8_t* code, const Insn& insn, const std::uint8_t mod, const std::uint8_t reg, const std::uint8_t rm, const bool r, const bool b) { + const auto at = insn.Info.OpcodeOffset; + const auto lead = code[at]; + if ((lead != 0xc4 && lead != 0xc5) || at + 3 >= insn.Length) return false; + const auto map = lead == 0xc5 ? std::uint8_t{1} : static_cast(code[at + 1] & 0x1f); + const auto vvvv = static_cast((~code[lead == 0xc5 ? at + 1 : at + 2] >> 3) & 0xf); + const auto op = code[lead == 0xc5 ? at + 2 : at + 3]; + const bool general = (map == 2 && (op == 0xf2 || op == 0xf3 || op == 0xf5 || op == 0xf6 || op == 0xf7)) || (map == 3 && op == 0xf0); + return general && ((reg == kRegRsp && !r) || (mod == 3 && rm == kRegRsp && !b) || vvvv == kRegRsp); +} + +MemoryUse memoryUse(const std::uint8_t* code, const Insn& insn) { + MemoryUse use; + const auto& info = insn.Info; + if (!info.HasModRm || info.ModRmOffset == 0 || info.ModRmOffset >= insn.Length) return use; + bool r = false; + bool x = false; + bool b = false; + extensionBits(code, insn, r, x, b); + const auto modrm = info.ModRmByte; + const auto mod = static_cast(modrm >> 6); + const auto reg = static_cast((modrm >> 3) & 7); + const auto rm = static_cast(modrm & 7); + const bool vex = info.RexPrefix == 0 && (code[info.OpcodeOffset] == 0xc4 || code[info.OpcodeOffset] == 0xc5 || code[info.OpcodeOffset] == 0x62 || code[info.OpcodeOffset] == 0x8f); + if (!vex && !info.IsTwoByteOpcode && reg == kRegRsp && !r) { + const auto op = info.Opcode; + if ((op <= 0x3f && (op & 7) <= 3) || (op >= 0x84 && op <= 0x8b) || op == 0x8d) use.UsesRsp = use.RegisterRsp = true; + } + if (!vex && info.IsTwoByteOpcode && ((reg == kRegRsp && !r && twoByteRegisterIsGeneral(info.Opcode)) || (mod == 3 && rm == kRegRsp && !b && twoByteRmIsGeneral(info.Opcode)))) use.UsesRsp = use.RegisterRsp = true; + if (vex && vexGeneralUsesRsp(code, insn, mod, reg, rm, r, b)) use.UsesRsp = use.RegisterRsp = true; + if (mod == 3) { + const bool generalPurpose = !vex && !info.IsTwoByteOpcode && !(info.Opcode >= 0xd8 && info.Opcode <= 0xdf); + if (generalPurpose && rm == kRegRsp && !b) use.UsesRsp = use.RegisterRsp = true; + return use; + } + std::size_t at = info.ModRmOffset + 1; + std::uint8_t base = rm; + bool baseExtended = b; + bool indexed = false; + if (rm == 4) { + if (at >= insn.Length) return use; + const auto sib = code[at++]; + base = sib & 7; + indexed = ((sib >> 3) & 7) != 4 || x; + if (mod == 0 && base == 5) return use; + } else if (mod == 0 && rm == 5) { + return use; + } + std::int64_t disp = 0; + if (mod == 1 && at < insn.Length) disp = static_cast(code[at]); + if (mod == 2 && at + 4 <= insn.Length) { + std::int32_t value = 0; + std::memcpy(&value, code + at, 4); + disp = value; + } + if (base == kRegRsp && !baseExtended) { + use.UsesRsp = true; + use.RspBased = true; + use.RspDisp = disp; + use.Mod = mod; + use.DispOffset = at; + use.Indexed = indexed; + use.Address = !vex && !info.IsTwoByteOpcode && info.Opcode == 0x8d; + } + if (base == kRegRbp && !baseExtended && mod != 0) { + use.RbpBased = true; + use.RbpDisp = disp; + } + return use; +} + +std::optional> movedDisplacement(const MemoryUse& use, const std::int64_t shift) { + const auto moved = use.RspDisp + shift; + if (use.Mod == 1 && moved <= std::numeric_limits::max()) return std::vector{static_cast(moved)}; + if (use.Mod == 2 && moved <= std::numeric_limits::max()) { + const auto value = static_cast(static_cast(moved)); + return std::vector{static_cast(value), static_cast(value >> 8), static_cast(value >> 16), static_cast(value >> 24)}; + } + return std::nullopt; +} + +std::optional> withDisplacement(const std::uint8_t* code, const Insn& insn, const MemoryUse& use, const std::int64_t disp) { + if (disp < std::numeric_limits::min() || disp > std::numeric_limits::max()) return std::nullopt; + const std::size_t oldBytes = use.Mod == 1 ? 1 : use.Mod == 2 ? 4 : 0; + if (use.DispOffset + oldBytes > insn.Length) return std::nullopt; + const bool narrow = disp >= std::numeric_limits::min() && disp <= std::numeric_limits::max(); + std::vector bytes(code, code + use.DispOffset); + bytes[insn.Info.ModRmOffset] = static_cast((bytes[insn.Info.ModRmOffset] & 0x3f) | (narrow ? 0x40 : 0x80)); + const auto value = static_cast(static_cast(disp)); + for (std::size_t index = 0; index < (narrow ? 1u : 4u); ++index) bytes.push_back(static_cast(value >> (8 * index))); + bytes.insert(bytes.end(), code + use.DispOffset + oldBytes, code + insn.Length); + return bytes; +} + +struct OperandStub { + std::size_t First; + std::size_t Last; + std::size_t Begin; + std::size_t End; + std::vector Body; + std::size_t Operands = 0; +}; + +void frameSegment( + const std::vector& fileBytes, + const Domain::ProgramHeader& ph, + const std::vector& functions, + const std::vector& existing, + std::vector& sites, + std::vector& displacements, + RedZoneFrameReport& report +) { + const auto segOffset = static_cast(ph.Offset); + const auto segSize = static_cast(ph.FileSize); + if (segOffset > fileBytes.size() || segSize > fileBytes.size() - segOffset) return; + const std::vector seg(fileBytes.begin() + static_cast(segOffset), fileBytes.begin() + static_cast(segOffset + segSize)); + const auto scanner = MakeInstructionScanner(); + const X64InstructionDecoder decoder; + + std::set targets; + for (const auto& match : scanner->ScanCodeSection(seg, 0, seg.size())) { + const auto info = decoder.DecodeInstruction(seg.data() + match.Offset, match.Length); + if (!info.HasBranchTarget || info.HasRipRelativeDisp || info.FlowKind == ControlFlowKind::Call) continue; + const auto target = static_cast(match.Offset + match.Length) + info.BranchDisp; + if (target >= 0 && static_cast(target) < seg.size()) targets.insert(static_cast(target)); + } + std::vector> taken; + for (const auto& site : existing) { + if (site.Address < ph.MappedAddress || site.Address >= ph.MappedAddress + segSize) continue; + const auto at = static_cast(site.Address - ph.MappedAddress); + taken.emplace_back(at, at + site.Length); + } + const auto overlapsExisting = [&](const std::size_t begin, const std::size_t end) { + return std::any_of(taken.begin(), taken.end(), [&](const auto& range) { return begin < range.second && range.first < end; }); + }; + const auto targetInside = [&](const std::size_t begin, const std::size_t end) { + const auto hit = targets.upper_bound(begin); + return hit != targets.end() && *hit < end; + }; + for (const auto& function : functions) { + if (function.Begin < ph.MappedAddress || function.End <= function.Begin || function.End - ph.MappedAddress > segSize) continue; + const auto start = static_cast(function.Begin - ph.MappedAddress); + const auto limit = static_cast(function.End - ph.MappedAddress); + ++report.Functions; + const auto skip = [&](const char* reason) { ++report.Skipped[reason]; }; + + const std::vector code(seg.begin() + static_cast(start), seg.begin() + static_cast(limit)); + std::vector body; + for (const auto& match : scanner->ScanCodeSection(code, 0, code.size())) { + if (code[match.Offset] == 0xcc && match.Length == 1) break; + body.push_back({start + static_cast(match.Offset), match.Length, decoder.DecodeInstruction(code.data() + match.Offset, match.Length)}); + } + if (body.size() < 3) continue; + const auto at = [&](const std::size_t k) { return seg.data() + body[k].Offset; }; + const bool rbpPrologue = body[0].Length == 1 && at(0)[0] == 0x55 && body[1].Length == 3 && std::memcmp(at(1), "\x48\x89\xe5", 3) == 0; + std::vector pushed; + std::size_t p = rbpPrologue ? 2 : 0; + while (p < body.size()) + if (const auto reg = pushedRegister(at(p), body[p])) { pushed.push_back(*reg); ++p; } else break; + std::int64_t allocation = 0; + if (!rbpPrologue && p < body.size()) + if (const auto adjustment = rspAdjustment(at(p), body[p]); adjustment && *adjustment < 0) { allocation = -*adjustment; ++p; } + std::vector expected(pushed.begin(), pushed.end()); + if (rbpPrologue) expected.insert(expected.begin(), kRegRbp); + const auto popsBefore = [&](const std::size_t k) -> std::optional { + std::size_t q = k; + std::vector popped; + while (q > p && popped.size() < expected.size()) + if (const auto reg = poppedRegister(at(q - 1), body[q - 1])) { popped.push_back(*reg); --q; } else break; + if (popped != expected) return std::nullopt; + return q; + }; + std::set frees; + if (allocation > 0) + for (std::size_t k = p; k < body.size(); ++k) + if (body[k].Info.FlowKind == ControlFlowKind::Return) + if (const auto q = popsBefore(k); q && *q > p && rspAdjustment(at(*q - 1), body[*q - 1]) == allocation) frees.insert(*q - 1); + + bool leaf = true; + bool redZone = false; + const char* reason = nullptr; + std::vector> exits; + std::map>> moves; + std::vector stubbed; + const auto functionEnd = limit; + std::int64_t depth = 0; + bool wholeRedZone = rbpPrologue; + for (std::size_t k = p; k < body.size(); ++k) { + const auto use = memoryUse(at(k), body[k]); + if (!use.RspBased) continue; + if (use.RspDisp < 0) depth = std::max(depth, -use.RspDisp); + if (use.Indexed || (use.Address && use.RspDisp < 0)) wholeRedZone = true; + } + const auto shift = wholeRedZone ? kRedZoneBytes : std::min(kRedZoneBytes, (depth + kStackAlignment - 1) / kStackAlignment * kStackAlignment); + for (std::size_t k = p; k < body.size(); ++k) { + const auto& insn = body[k]; + const auto* code = at(k); + const auto flow = insn.Info.FlowKind; + if (flow == ControlFlowKind::Call || flow == ControlFlowKind::IndirectCall) { leaf = false; break; } + if (frees.contains(k)) continue; + const auto use = memoryUse(code, insn); + if (use.RbpBased && rbpPrologue && use.RbpDisp < -static_cast(8 * pushed.size())) redZone = true; + if (use.RspBased && use.RspDisp < 0) redZone = true; + if (reason != nullptr) continue; + if (code[insn.Info.OpcodeOffset] == 0xc9 || code[insn.Info.OpcodeOffset] == 0xc8) { reason = "leave or enter"; continue; } + if (rbpPrologue) { + if (use.UsesRsp) { reason = "other use of rsp"; continue; } + } else { + if (use.RegisterRsp) { reason = "other use of rsp"; continue; } + if (use.RspBased) { + if (code[insn.Info.OpcodeOffset] == 0x62) { reason = "rsp operand that cannot move with rsp"; continue; } + if (overlapsExisting(insn.Offset, insn.Offset + insn.Length)) { reason = "overlaps another stub"; continue; } + if (auto moved = movedDisplacement(use, shift)) moves.emplace(k, std::pair{insn.Offset + use.DispOffset, std::move(*moved)}); + else stubbed.push_back(k); + } + } + if (flow == ControlFlowKind::IndirectJump) { reason = "indirect jump"; continue; } + if ((flow == ControlFlowKind::UnconditionalJump || flow == ControlFlowKind::ConditionalBranch) && insn.Info.HasBranchTarget) { + const auto target = static_cast(insn.Offset + insn.Length) + insn.Info.BranchDisp; + if (insn.Info.HasRipRelativeDisp || target < static_cast(start) || target >= static_cast(functionEnd)) { reason = "branch leaves the function"; continue; } + } + if (flow == ControlFlowKind::Return) { + if (insn.Length != 1 || code[0] != 0xc3) { reason = "ret with an immediate"; continue; } + const auto pops = popsBefore(k); + if (!pops) { reason = "exit does not pop the prologue's registers"; continue; } + auto q = *pops; + if (allocation > 0) { + if (q == p || !frees.contains(q - 1)) { reason = "exit does not free the prologue's allocation"; continue; } + --q; + } + const auto siteStart = body[q].Offset; + const auto siteEnd = insn.Offset + 1; + if (siteEnd - siteStart < kJumpBytes) { reason = "exit shorter than a jump"; continue; } + if (targetInside(siteStart, siteEnd)) { reason = "branch into an exit"; continue; } + exits.emplace_back(siteStart, siteEnd); + } + } + if (!leaf || !redZone) continue; + ++report.RedZoneFunctions; + const auto prologueEnd = p > 0 ? body[p - 1].Offset + body[p - 1].Length : start; + if (reason == nullptr && ((pushed.empty() && allocation == 0) || prologueEnd - start < kJumpBytes)) reason = "prologue shorter than a jump"; + if (reason == nullptr && targetInside(start, prologueEnd)) reason = "branch into the prologue"; + if (reason == nullptr && exits.empty()) reason = "no ret exit"; + if (reason == nullptr && (overlapsExisting(start, prologueEnd) || std::any_of(exits.begin(), exits.end(), [&](const auto& exit) { return overlapsExisting(exit.first, exit.second); }))) reason = "overlaps another stub"; + std::vector stubs; + const auto overlapsSite = [&](const std::size_t begin, const std::size_t end) { + if (begin < prologueEnd && start < end) return true; + if (std::any_of(exits.begin(), exits.end(), [&](const auto& exit) { return begin < exit.second && exit.first < end; })) return true; + return std::any_of(stubs.begin(), stubs.end(), [&](const OperandStub& stub) { return begin < stub.End && stub.Begin < end; }); + }; + for (const auto k : stubbed) { + if (reason != nullptr) break; + if (!stubs.empty() && k < stubs.back().Last) continue; + OperandStub stub{k, k, body[k].Offset, body[k].Offset, {}}; + while (stub.Last < body.size() && stub.End - stub.Begin < kJumpBytes) { + const auto& covered = body[stub.Last]; + if (covered.Offset != stub.End || covered.Info.FlowKind != ControlFlowKind::Sequential || covered.Info.HasRipRelativeDisp || frees.contains(stub.Last)) break; + const auto use = memoryUse(at(stub.Last), covered); + const auto bytes = use.RspBased ? withDisplacement(at(stub.Last), covered, use, use.RspDisp + shift) : std::vector(at(stub.Last), at(stub.Last) + covered.Length); + if (!bytes) break; + stub.Body.insert(stub.Body.end(), bytes->begin(), bytes->end()); + if (use.RspBased) ++stub.Operands; + stub.End += covered.Length; + ++stub.Last; + } + while (stub.End - stub.Begin < kJumpBytes && stub.First > p) { + const auto j = stub.First - 1; + const auto& prior = body[j]; + if (prior.Offset + prior.Length != stub.Begin || prior.Info.FlowKind != ControlFlowKind::Sequential || prior.Info.HasRipRelativeDisp || frees.contains(j)) break; + const auto use = memoryUse(at(j), prior); + const auto bytes = use.RspBased ? withDisplacement(at(j), prior, use, use.RspDisp + shift) : std::vector(at(j), at(j) + prior.Length); + if (!bytes) break; + stub.Body.insert(stub.Body.begin(), bytes->begin(), bytes->end()); + if (use.RspBased) ++stub.Operands; + stub.Begin = prior.Offset; + stub.First = j; + } + if (stub.End - stub.Begin < kJumpBytes || targetInside(stub.Begin, stub.End) || overlapsSite(stub.Begin, stub.End) || overlapsExisting(stub.Begin, stub.End)) reason = "rsp operand that cannot move and has no room for a stub"; + else stubs.push_back(std::move(stub)); + } + if (reason != nullptr) { skip(reason); continue; } + for (const auto& stub : stubs) + for (auto k = stub.First; k < stub.Last; ++k) moves.erase(k); + + const auto addSite = [&](const std::size_t begin, const std::size_t end, std::vector code) { + TrampolineSite site; + site.Offset = ph.Offset + begin; + site.Address = ph.MappedAddress + begin; + site.Length = end - begin; + site.OriginalBytes.assign(seg.begin() + static_cast(begin), seg.begin() + static_cast(end)); + site.ReturnBranchOffset = code.size(); + code.insert(code.end(), std::begin(kJumpBack), std::end(kJumpBack)); + site.Body = std::move(code); + taken.emplace_back(begin, end); + sites.push_back(std::move(site)); + }; + const auto original = [&](const std::size_t begin, const std::size_t end) { return std::vector(seg.begin() + static_cast(begin), seg.begin() + static_cast(end)); }; + auto entry = original(start, prologueEnd); + const auto lower = lowerRsp(shift); + entry.insert(entry.end(), lower.begin(), lower.end()); + addSite(start, prologueEnd, std::move(entry)); + for (const auto& exit : exits) { + auto code = raiseRsp(shift); + const auto tail = original(exit.first, exit.second); + code.insert(code.end(), tail.begin(), tail.end()); + addSite(exit.first, exit.second, std::move(code)); + } + for (auto& stub : stubs) addSite(stub.Begin, stub.End, std::move(stub.Body)); + for (auto& [k, move] : moves) displacements.push_back({ph.Offset + move.first, std::move(move.second)}); + ++report.Framed; + if (!rbpPrologue) ++report.FramedWithoutFramePointer; + report.Exits += exits.size(); + report.MovedOperands += moves.size(); + report.OperandStubs += stubs.size(); + for (const auto& stub : stubs) report.StubbedOperands += stub.Operands; + } +} + +} + +std::vector FrameRedZoneFunctions( + const std::vector& fileBytes, + const std::vector& codeSegments, + const std::vector& functions, + const std::vector& existing, + RedZoneFrameReport& report, + std::vector& displacements +) { + std::vector sites; + for (const auto& ph : codeSegments) frameSegment(fileBytes, ph, functions, existing, sites, displacements, report); + return sites; +} + +} diff --git a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp index b22c3f8726..0c2f5571c9 100644 --- a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp +++ b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp @@ -393,6 +393,7 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( const std::uint8_t modrm = data[modrmPos]; info.HasModRm = true; info.ModRmByte = modrm; + info.ModRmOffset = modrmPos; info.ModRmRegField = (modrm >> ModRmRegShift) & ModRmRegMask; const std::uint8_t mod = registerOnly ? ModRmModRegister : (modrm >> ModRmModShift) & ModRmModMask; const std::uint8_t rm = modrm & ModRmRmMask; @@ -489,6 +490,7 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( std::uint8_t modrm = data[pos]; info.HasModRm = true; info.ModRmByte = modrm; + info.ModRmOffset = pos; std::uint8_t reg = (modrm >> ModRmRegShift) & ModRmRegMask; info.ModRmRegField = reg; std::uint8_t mod = (modrm >> ModRmModShift) & ModRmModMask; diff --git a/core/relinker/codegen/tests/RedZoneFramerExecutionTests.cpp b/core/relinker/codegen/tests/RedZoneFramerExecutionTests.cpp new file mode 100644 index 0000000000..7d940524b9 --- /dev/null +++ b/core/relinker/codegen/tests/RedZoneFramerExecutionTests.cpp @@ -0,0 +1,286 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if defined(__linux__) && defined(__x86_64__) +#include +#include +#include +#endif + +#if defined(__linux__) && defined(__x86_64__) + +namespace { + +void require(const bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +constexpr std::size_t kArenaBytes = 0x10000; +constexpr std::size_t kStubBase = 0x4000; +constexpr std::size_t kRunnerBase = 0xc000; +constexpr std::uint8_t kClobber = 0xa5; +constexpr std::uint64_t kA = 0x1111, kB = 0x2222, kC = 0x3333, kD = 0x4444; + +std::uint8_t* arena = nullptr; +volatile sig_atomic_t steps = 0; + +void clobberBelowRsp(int, siginfo_t*, void* context) { + auto& registers = static_cast(context)->uc_mcontext.gregs; + const auto rip = static_cast(registers[REG_RIP]); + const auto base = reinterpret_cast(arena); + if (rip < base || rip >= base + kArenaBytes) { + registers[REG_EFL] &= ~static_cast(0x100); + return; + } + std::memset(reinterpret_cast(static_cast(registers[REG_RSP]) - 0x80), kClobber, 0x80); + steps = steps + 1; +} + +const std::vector Runner{ + 0x53, 0x4c, 0x89, 0xc3, 0x48, 0x89, 0xf8, 0x48, 0x89, 0xf7, 0x48, 0x89, 0xd6, 0x48, 0x89, 0xca, + 0x4d, 0x85, 0xc9, 0x74, 0x0a, 0x9c, 0x48, 0x81, 0x0c, 0x24, 0x00, 0x01, 0x00, 0x00, 0x9d, + 0xff, 0xd0, 0x9c, 0x48, 0x81, 0x24, 0x24, 0xff, 0xfe, 0xff, 0xff, 0x9d, 0x5b, 0xc3, +}; + +struct Leaf { + const char* Name; + std::vector Code; + std::uint64_t Expected; +}; + +const std::vector FramelessPushes{0x53, 0x41, 0x56, 0x41, 0x57}; +const std::vector FramelessPops{0x41, 0x5f, 0x41, 0x5e, 0x5b, 0xc3}; + +std::vector frameless(const std::vector& body) { + std::vector bytes = FramelessPushes; + bytes.insert(bytes.end(), body.begin(), body.end()); + bytes.insert(bytes.end(), FramelessPops.begin(), FramelessPops.end()); + return bytes; +} + +std::vector framedLeaves() { + return { + {"frameless leaf spilling 0x10 below rsp", frameless({ + 0x48, 0x89, 0x7c, 0x24, 0xf0, 0x48, 0x89, 0x74, 0x24, 0xf8, 0x31, 0xc0, 0xb9, 0x08, 0x00, 0x00, 0x00, + 0x48, 0x03, 0x44, 0x24, 0xf0, 0x48, 0x03, 0x44, 0x24, 0xf8, 0xff, 0xc9, 0x75, 0xf2, + }), 8 * (kA + kB)}, + {"leaf reaching 0x28 below rsp", frameless({ + 0x48, 0x89, 0x7c, 0x24, 0xd8, 0x48, 0x89, 0x74, 0x24, 0xe8, 0x48, 0x89, 0x54, 0x24, 0xf0, 0x31, 0xc0, 0xb9, 0x08, 0x00, 0x00, 0x00, + 0x48, 0x03, 0x44, 0x24, 0xd8, 0x48, 0x03, 0x44, 0x24, 0xe8, 0x48, 0x03, 0x44, 0x24, 0xf0, 0xff, 0xc9, 0x75, 0xed, + }), 8 * (kA + kB + kC)}, + {"leaf reading its saved rbx at +0x10 moved in place", frameless({ + 0x48, 0x89, 0x7c, 0x24, 0xf0, 0x31, 0xc0, 0xb9, 0x08, 0x00, 0x00, 0x00, + 0x48, 0x03, 0x44, 0x24, 0xf0, 0x48, 0x03, 0x44, 0x24, 0x10, 0xff, 0xc9, 0x75, 0xf2, + }), 8 * (kA + kD)}, + {"leaf taking the address of a slot 0x20 below rsp", frameless({ + 0x4c, 0x8d, 0x74, 0x24, 0xe0, 0x49, 0x89, 0x3e, 0x49, 0x89, 0x76, 0x08, 0x31, 0xc0, 0xb9, 0x08, 0x00, 0x00, 0x00, + 0x49, 0x03, 0x06, 0x49, 0x03, 0x46, 0x08, 0xff, 0xc9, 0x75, 0xf5, + }), 8 * (kA + kB)}, + {"inflate_fast-shaped leaf allocating 0x40 after six pushes", { + 0x55, 0x41, 0x57, 0x41, 0x56, 0x41, 0x55, 0x41, 0x54, 0x53, 0x48, 0x83, 0xec, 0x40, + 0x48, 0x89, 0x7c, 0x24, 0xe0, 0x48, 0x89, 0x7c, 0x24, 0x00, 0x48, 0x89, 0x74, 0x24, 0x08, 0x48, 0x89, 0x54, 0x24, 0x38, + 0xb9, 0x04, 0x00, 0x00, 0x00, 0x31, 0xc0, 0x48, 0x03, 0x44, 0x24, 0x08, 0x48, 0x03, 0x44, 0x24, 0xe0, 0x48, 0x03, 0x44, 0x24, 0x00, + 0xff, 0xc9, 0x75, 0xed, 0x48, 0x8b, 0x54, 0x24, 0x38, + 0x48, 0x83, 0xc4, 0x40, 0x5b, 0x41, 0x5c, 0x41, 0x5d, 0x41, 0x5e, 0x41, 0x5f, 0x5d, 0xc3, + }, 4 * (kB + 2 * kA)}, + }; +} + +std::vector stubbedLeaves() { + return { + {"inflate_fast-shaped leaf with operands at rsp and at 0x60 shifted by 0x20", { + 0x55, 0x41, 0x57, 0x41, 0x56, 0x41, 0x55, 0x41, 0x54, 0x53, 0x48, 0x83, 0xec, 0x70, + 0x48, 0x89, 0x7c, 0x24, 0xe0, 0x48, 0x89, 0x34, 0x24, 0x48, 0x89, 0x54, 0x24, 0x60, 0xb9, 0x04, 0x00, 0x00, 0x00, 0x31, 0xc0, + 0x48, 0x03, 0x44, 0x24, 0xe0, 0x48, 0x03, 0x04, 0x24, 0x48, 0x03, 0x44, 0x24, 0x60, 0xff, 0xc9, 0x75, 0xee, + 0x48, 0x83, 0xc4, 0x70, 0x5b, 0x41, 0x5c, 0x41, 0x5d, 0x41, 0x5e, 0x41, 0x5f, 0x5d, 0xc3, + }, 4 * (kA + kB + kC)}, + {"leaf shifted by the whole red zone reading its saved rbx at +0x8", { + 0x41, 0x57, 0x53, 0x41, 0x56, 0x4c, 0x8d, 0x74, 0x24, 0xe0, 0x49, 0x89, 0x3e, 0x31, 0xc0, 0xb9, 0x08, 0x00, 0x00, 0x00, + 0x49, 0x03, 0x06, 0x48, 0x03, 0x44, 0x24, 0x08, 0xff, 0xc9, 0x75, 0xf4, 0x41, 0x5e, 0x5b, 0x41, 0x5f, 0xc3, + }, 8 * (kA + kD)}, + }; +} + +std::vector backwardStubbedLeaves() { + return { + {"inflate_fast-shaped leaf comparing at rsp between an add and a jne", { + 0x55, 0x41, 0x57, 0x41, 0x56, 0x41, 0x55, 0x41, 0x54, 0x53, 0x48, 0x83, 0xec, 0x40, + 0x48, 0x89, 0x7c, 0x24, 0xe0, 0x48, 0x89, 0x7c, 0x24, 0x00, 0xb9, 0x04, 0x00, 0x00, 0x00, 0x31, 0xc0, + 0x48, 0x03, 0x44, 0x24, 0xe0, 0x49, 0x01, 0xf8, 0x48, 0x39, 0x3c, 0x24, 0x75, 0x03, 0x48, 0x01, 0xf0, 0xff, 0xc9, 0x75, 0xeb, + 0x48, 0x83, 0xc4, 0x40, 0x5b, 0x41, 0x5c, 0x41, 0x5d, 0x41, 0x5e, 0x41, 0x5f, 0x5d, 0xc3, + }, 4 * (kA + kB)}, + {"leaf comparing at rsp after a load below rsp that heads its loop", { + 0x55, 0x41, 0x57, 0x41, 0x56, 0x41, 0x55, 0x41, 0x54, 0x53, 0x48, 0x83, 0xec, 0x40, + 0x48, 0x89, 0x7c, 0x24, 0xe0, 0x48, 0x89, 0x7c, 0x24, 0x00, 0xb9, 0x04, 0x00, 0x00, 0x00, 0x31, 0xc0, + 0x48, 0x03, 0x44, 0x24, 0xe0, 0x48, 0x39, 0x3c, 0x24, 0x75, 0x03, 0x48, 0x01, 0xf0, 0xff, 0xc9, 0x75, 0xee, + 0x48, 0x83, 0xc4, 0x40, 0x5b, 0x41, 0x5c, 0x41, 0x5d, 0x41, 0x5e, 0x41, 0x5f, 0x5d, 0xc3, + }, 4 * (kA + kB)}, + }; +} + +void writeRel32(std::uint8_t* at, const std::uint64_t target, const std::uint64_t next) { + const auto displacement = static_cast(target) - static_cast(next); + require(displacement >= std::numeric_limits::min() && displacement <= std::numeric_limits::max(), "a stub jump is out of rel32 range"); + const auto value = static_cast(static_cast(displacement)); + for (std::size_t index = 0; index < 4; ++index) at[index] = static_cast(value >> (index * 8)); +} + +std::vector callTo(const std::size_t from, const std::size_t target) { + std::vector bytes{0xe8, 0, 0, 0, 0}; + const auto displacement = static_cast(static_cast(target) - static_cast(from + 5)); + for (std::size_t index = 0; index < 4; ++index) bytes[1 + index] = static_cast(displacement >> (index * 8)); + return bytes; +} + +struct Image { + std::vector Bytes; + std::vector Starts; + std::vector Ends; +}; + +Image place(const std::vector& leaves) { + Image image; + std::size_t at = 0x40; + for (const auto& leaf : leaves) { + image.Starts.push_back(at); + at += (leaf.Code.size() + 15) / 16 * 16 + 16; + } + for (const auto start : image.Starts) { + const auto call = callTo(image.Bytes.size(), start); + image.Bytes.insert(image.Bytes.end(), call.begin(), call.end()); + } + image.Bytes.push_back(0xc3); + for (std::size_t index = 0; index < leaves.size(); ++index) { + image.Bytes.resize(image.Starts[index], 0xcc); + image.Bytes.insert(image.Bytes.end(), leaves[index].Code.begin(), leaves[index].Code.end()); + image.Ends.push_back(image.Bytes.size()); + } + image.Bytes.resize((image.Bytes.size() + 15) / 16 * 16 + 16, 0xcc); + require(image.Bytes.size() <= kStubBase, "the leaves do not fit before the stubs"); + return image; +} + +struct Framing { + Codegen::RedZoneFrameReport Report; + std::vector Sites; + std::vector Displacements; +}; + +Framing load(const Image& image, const bool frame) { + std::fill_n(arena, kArenaBytes, 0xcc); + std::copy(image.Bytes.begin(), image.Bytes.end(), arena); + std::copy(Runner.begin(), Runner.end(), arena + kRunnerBase); + Framing framing; + if (!frame) return framing; + const auto base = reinterpret_cast(arena); + const std::vector segments{{1, 5, 0, base, base, image.Bytes.size(), image.Bytes.size(), 16}}; + std::vector functions; + for (std::size_t index = 0; index < image.Starts.size(); ++index) functions.push_back({base + image.Starts[index], base + image.Ends[index]}); + framing.Sites = Codegen::FrameRedZoneFunctions(image.Bytes, segments, functions, {}, framing.Report, framing.Displacements); + for (const auto& moved : framing.Displacements) std::copy(moved.Bytes.begin(), moved.Bytes.end(), arena + moved.Offset); + std::size_t cursor = kStubBase; + for (const auto& site : framing.Sites) { + const auto at = static_cast(site.Address - base); + require(std::equal(site.OriginalBytes.begin(), site.OriginalBytes.end(), arena + at), "a framed site's bytes changed before it was patched"); + cursor = (cursor + 15) / 16 * 16; + require(cursor + site.Body.size() <= kRunnerBase, "the stubs do not fit before the runner"); + auto* stub = arena + cursor; + std::copy(site.Body.begin(), site.Body.end(), stub); + Codegen::ApplyStubRelocations(std::span(stub, site.ReturnBranchOffset), site.Relocations, site.Address, base + cursor, site.Offset); + writeRel32(stub + site.ReturnBranchOffset + 1, site.Address + site.Length, base + cursor + site.ReturnBranchOffset + 5); + std::fill_n(arena + at, site.Length, 0x90); + arena[at] = 0xe9; + writeRel32(arena + at + 1, base + cursor, site.Address + 5); + cursor += site.Body.size(); + } + return framing; +} + +struct Run { + std::uint64_t Result; + int Steps; +}; + +Run run(const std::size_t start, const bool clobber) { + using Entry = std::uint64_t (*)(std::uint64_t, std::uint64_t, std::uint64_t, std::uint64_t, std::uint64_t, std::uint64_t); + const auto entry = reinterpret_cast(arena + kRunnerBase); + steps = 0; + const auto result = entry(reinterpret_cast(arena + start), kA, kB, kC, kD, clobber ? 1 : 0); + return {result, static_cast(steps)}; +} + +std::string hex(const std::uint64_t value) { + std::ostringstream text; + text << "0x" << std::hex << value; + return text.str(); +} + +Codegen::RedZoneFrameReport runLeaves(const std::vector& leaves) { + const auto image = place(leaves); + load(image, false); + std::vector plain, clobbered; + for (std::size_t index = 0; index < leaves.size(); ++index) { + plain.push_back(run(image.Starts[index], false)); + clobbered.push_back(run(image.Starts[index], true)); + require(plain.back().Result == leaves[index].Expected, std::string(leaves[index].Name) + ": the unframed leaf returned " + hex(plain.back().Result) + " without the clobber, not " + hex(leaves[index].Expected)); + require(clobbered.back().Steps > 0, std::string(leaves[index].Name) + ": the clobber did not run"); + require(clobbered.back().Result != leaves[index].Expected, std::string(leaves[index].Name) + ": the unframed leaf survived writes below rsp, so the case proves nothing"); + } + const auto framing = load(image, true); + require(framing.Report.Framed == leaves.size(), "only " + std::to_string(framing.Report.Framed) + " of " + std::to_string(leaves.size()) + " leaves were framed"); + for (std::size_t index = 0; index < leaves.size(); ++index) { + const auto calm = run(image.Starts[index], false); + const auto framed = run(image.Starts[index], true); + require(calm.Result == leaves[index].Expected, std::string(leaves[index].Name) + ": the framed leaf returned " + hex(calm.Result) + " without the clobber, not " + hex(leaves[index].Expected)); + require(framed.Steps > 0, std::string(leaves[index].Name) + ": the clobber did not run over the framed leaf"); + require(framed.Result == leaves[index].Expected, std::string(leaves[index].Name) + ": the framed leaf returned " + hex(framed.Result) + " under writes below rsp, not " + hex(leaves[index].Expected)); + std::cout << leaves[index].Name << ": unframed " << hex(plain[index].Result) << " alone and " << hex(clobbered[index].Result) << " under the clobber (" << clobbered[index].Steps << " steps); framed " << hex(framed.Result) << " under the clobber (" << framed.Steps << " steps)\n"; + } + return framing.Report; +} + +} + +int main() { + try { + struct sigaction action {}; + action.sa_sigaction = clobberBelowRsp; + action.sa_flags = SA_SIGINFO; + sigemptyset(&action.sa_mask); + require(sigaction(SIGTRAP, &action, nullptr) == 0, "cannot install the SIGTRAP handler"); + void* mapped = mmap(nullptr, kArenaBytes, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped == MAP_FAILED) { + std::cout << "skipped: no writable and executable memory\n"; + return 77; + } + arena = static_cast(mapped); + runLeaves(framedLeaves()); + const auto stubbed = runLeaves(stubbedLeaves()); + require(stubbed.OperandStubs == 3 && stubbed.StubbedOperands == 5, "the leaves were framed with " + std::to_string(stubbed.OperandStubs) + " operand stubs carrying " + std::to_string(stubbed.StubbedOperands) + " rsp operands, not 3 carrying 5"); + const auto backward = runLeaves(backwardStubbedLeaves()); + require(backward.OperandStubs == 2 && backward.StubbedOperands == 3 && backward.MovedOperands == 5, "the leaves comparing at rsp before a branch were framed with " + std::to_string(backward.OperandStubs) + " operand stubs carrying " + std::to_string(backward.StubbedOperands) + " rsp operands and " + std::to_string(backward.MovedOperands) + " moved in place, not 2 carrying 3 and 5"); + std::cout << "red-zone framer execution tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << "red-zone framer execution tests failed: " << error.what() << '\n'; + return 1; + } +} + +#else + +int main() { + std::cout << "skipped: the red-zone framer execution tests single-step x86-64 code on Linux\n"; + return 77; +} + +#endif diff --git a/core/relinker/codegen/tests/RedZoneFramerTests.cpp b/core/relinker/codegen/tests/RedZoneFramerTests.cpp new file mode 100644 index 0000000000..31466fcfdb --- /dev/null +++ b/core/relinker/codegen/tests/RedZoneFramerTests.cpp @@ -0,0 +1,625 @@ +#include +#include +#include +#include +#include +#include + +namespace { + +void require(const bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +constexpr std::uint64_t Base = 0x1000; + +std::vector padTo(std::vector bytes, const std::size_t size) { + bytes.resize(size, 0xcc); + return bytes; +} + +void append(std::vector& bytes, const std::vector& more) { + bytes.insert(bytes.end(), more.begin(), more.end()); +} + +std::vector callTo(const std::size_t from, const std::size_t target) { + const auto displacement = static_cast(target) - static_cast(from + 5); + return {0xe8, static_cast(displacement), static_cast(displacement >> 8), static_cast(displacement >> 16), static_cast(displacement >> 24)}; +} + +const std::vector Prologue{0x55, 0x48, 0x89, 0xe5, 0x41, 0x57, 0x53}; +const std::vector Exit{0x5b, 0x41, 0x5f, 0x5d, 0xc3}; +const std::vector StoreBelowPushes{0x48, 0x89, 0x7d, 0xe8}; +const std::vector LoadBelowPushes{0x48, 0x8b, 0x45, 0xe8}; +const std::vector LoadAbovePushes{0x48, 0x8b, 0x45, 0x08}; + +std::vector leaf(const std::vector& body) { + std::vector bytes = Prologue; + append(bytes, body); + append(bytes, Exit); + return bytes; +} + +struct Image { + std::vector bytes; + std::vector segments; + std::vector functions; +}; + +Image image(const std::vector>& functions) { + Image result; + std::vector starts; + std::size_t at = 0x40; + for (const auto& function : functions) { + starts.push_back(at); + at += (function.size() + 15) / 16 * 16 + 16; + } + std::vector caller; + for (const auto start : starts) append(caller, callTo(caller.size(), start)); + caller.push_back(0xc3); + result.bytes = padTo(caller, 0x40); + for (std::size_t index = 0; index < functions.size(); ++index) { + result.bytes = padTo(result.bytes, starts[index]); + append(result.bytes, functions[index]); + result.functions.push_back({Base + starts[index], Base + starts[index] + functions[index].size()}); + } + result.bytes = padTo(result.bytes, (result.bytes.size() + 15) / 16 * 16 + 16); + result.segments.push_back({1, 5, 0, Base, Base, result.bytes.size(), result.bytes.size(), 16}); + return result; +} + +void framesRedZoneLeaf() { + std::vector body = StoreBelowPushes; + append(body, LoadBelowPushes); + const auto input = image({leaf(body)}); + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); + require(report.RedZoneFunctions == 1 && report.Framed == 1 && report.Exits == 1, "a red-zone leaf was not framed"); + require(sites.size() == 2, "a framed leaf needs one prologue and one exit site"); + require(displacements.empty() && report.FramedWithoutFramePointer == 0, "a leaf with a frame pointer had rsp operands moved"); + + const auto& entry = sites[0]; + require(entry.Address == Base + 0x40 && entry.Length == Prologue.size(), "the prologue site does not cover the pushes"); + require(entry.OriginalBytes == Prologue, "the prologue site lost its bytes"); + std::vector entryBody = Prologue; + append(entryBody, {0x48, 0x8d, 0x64, 0x24, 0x80, 0xe9, 0, 0, 0, 0}); + require(entry.Body == entryBody && entry.ReturnBranchOffset == Prologue.size() + 5, "the prologue stub does not lower rsp after the pushes"); + + const auto& exit = sites[1]; + require(exit.Address == Base + 0x40 + Prologue.size() + body.size() && exit.Length == Exit.size(), "the exit site does not cover the pops and ret"); + std::vector exitBody{0x48, 0x8d, 0xa4, 0x24, 0x80, 0, 0, 0}; + append(exitBody, Exit); + append(exitBody, {0xe9, 0, 0, 0, 0}); + require(exit.Body == exitBody && exit.ReturnBranchOffset == 8 + Exit.size(), "the exit stub does not raise rsp before the pops"); +} + +const std::vector FramelessPrologue{0x53, 0x41, 0x56, 0x41, 0x57}; +const std::vector FramelessExit{0x41, 0x5f, 0x41, 0x5e, 0x5b, 0xc3}; +const std::vector StoreBelowRsp{0x48, 0x89, 0x7c, 0x24, 0xf0}; +const std::vector LoadBelowRsp{0x48, 0x8b, 0x44, 0x24, 0xf0}; +const std::vector LoadAboveRsp{0x48, 0x8b, 0x44, 0x24, 0x08}; +const std::vector LoadFarAboveRsp{0x48, 0x8b, 0x44, 0x24, 0x78}; +const std::vector LoadAtRsp{0x48, 0x8b, 0x04, 0x24}; +const std::vector StoreDeepBelowRsp{0x48, 0x89, 0x74, 0x24, 0xd8}; +const std::vector LoadDeepBelowRsp{0x48, 0x8b, 0x44, 0x24, 0xd8}; +const std::vector AddressBelowRsp{0x48, 0x8d, 0x44, 0x24, 0xe0}; +const std::vector AddressAboveRsp{0x48, 0x8d, 0x44, 0x24, 0x08}; +const std::vector StoreIndexedBelowRsp{0x48, 0x89, 0x7c, 0xcc, 0xf0}; +const std::vector StoreImmediateFarAboveRsp{0xc7, 0x44, 0x24, 0x7c, 0x78, 0x56, 0x34, 0x12}; +const std::vector LoadIndexedAtRsp{0x48, 0x8b, 0x04, 0xcc}; +const std::vector AddRdxToRax{0x48, 0x01, 0xd0}; +const std::vector VexStoreAtRsp{0xc5, 0xf8, 0x11, 0x04, 0x24}; +const std::vector EvexStoreBelowRsp{0x62, 0xf1, 0x7c, 0x48, 0x11, 0x44, 0x24, 0xff}; +const std::vector LoadRipRelative{0x48, 0x8b, 0x05, 0x00, 0x00, 0x00, 0x00}; + +std::vector stubBody(std::vector code) { + append(code, {0xe9, 0, 0, 0, 0}); + return code; +} + +std::vector framelessLeaf(const std::vector& prologue, const std::vector& body, const std::vector& exit) { + std::vector bytes = prologue; + append(bytes, body); + append(bytes, exit); + return bytes; +} + +void framesLeafWithoutFramePointer() { + std::vector body = StoreBelowRsp; + append(body, LoadBelowRsp); + const auto input = image({framelessLeaf(FramelessPrologue, body, FramelessExit)}); + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); + require(report.RedZoneFunctions == 1 && report.Framed == 1 && report.FramedWithoutFramePointer == 1 && report.MovedOperands == 2, "a leaf that only pushes was not framed"); + require(sites.size() == 2, "a framed leaf needs one prologue and one exit site"); + const auto& entry = sites[0]; + require(entry.Address == Base + 0x40 && entry.Length == FramelessPrologue.size(), "the prologue site does not cover the pushes"); + std::vector entryBody = FramelessPrologue; + append(entryBody, {0x48, 0x8d, 0x64, 0x24, 0xf0, 0xe9, 0, 0, 0, 0}); + require(entry.Body == entryBody, "the prologue stub does not lower rsp by the leaf's depth below rsp after the pushes"); + const auto& exit = sites[1]; + require(exit.Address == Base + 0x40 + FramelessPrologue.size() + body.size() && exit.OriginalBytes == FramelessExit, "the exit site does not cover the pops and ret"); + std::vector exitBody{0x48, 0x8d, 0xa4, 0x24, 0x10, 0, 0, 0}; + append(exitBody, FramelessExit); + append(exitBody, {0xe9, 0, 0, 0, 0}); + require(exit.Body == exitBody, "the exit stub does not raise rsp by the leaf's depth below rsp before the pops"); + const auto storeDisplacement = 0x40 + FramelessPrologue.size() + 4; + const auto loadDisplacement = storeDisplacement + StoreBelowRsp.size(); + require(displacements.size() == 2, "both rsp operands must move"); + require(displacements[0].Offset == storeDisplacement && displacements[0].Bytes == std::vector{0x00}, "the store below rsp was not moved to the lowered rsp"); + require(displacements[1].Offset == loadDisplacement && displacements[1].Bytes == std::vector{0x00}, "the load below rsp was not moved to the lowered rsp"); +} + +void shiftsByTheDeepestOperandBelowRsp() { + std::vector body = StoreBelowRsp; + append(body, StoreDeepBelowRsp); + append(body, LoadDeepBelowRsp); + const auto input = image({framelessLeaf(FramelessPrologue, body, FramelessExit)}); + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); + require(report.Framed == 1 && sites.size() == 2 && displacements.size() == 3, "a leaf reaching 0x28 bytes below rsp was not framed"); + std::vector entryBody = FramelessPrologue; + append(entryBody, {0x48, 0x8d, 0x64, 0x24, 0xd0, 0xe9, 0, 0, 0, 0}); + require(sites[0].Body == entryBody, "rsp is not lowered by the deepest displacement rounded up to 16"); + require(std::vector(sites[1].Body.begin(), sites[1].Body.begin() + 8) == std::vector{0x48, 0x8d, 0xa4, 0x24, 0x30, 0, 0, 0}, "rsp is not raised by the shift it was lowered by"); + require(displacements[0].Bytes == std::vector{0x20} && displacements[1].Bytes == std::vector{0x08} && displacements[2].Bytes == std::vector{0x08}, "the operands below rsp were not moved by the shift"); +} + +void movesPositiveOperandsThatFitAfterTheShift() { + std::vector body = StoreBelowRsp; + append(body, LoadAboveRsp); + const auto input = image({framelessLeaf(FramelessPrologue, body, FramelessExit)}); + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); + require(report.Framed == 1 && report.MovedOperands == 2 && sites.size() == 2, "a leaf with an 8-bit displacement above rsp that fits after the shift was not framed"); + const auto loadDisplacement = 0x40 + FramelessPrologue.size() + StoreBelowRsp.size() + 4; + require(displacements.size() == 2 && displacements[1].Offset == loadDisplacement && displacements[1].Bytes == std::vector{0x18}, "the load above rsp was not moved in place by the shift"); +} + +void shiftsByTheWholeRedZoneForAddressesBelowRsp() { + std::vector below = StoreBelowRsp; + append(below, AddressBelowRsp); + std::vector above = StoreBelowRsp; + append(above, AddressAboveRsp); + const auto input = image({framelessLeaf(FramelessPrologue, below, FramelessExit), framelessLeaf(FramelessPrologue, above, FramelessExit)}); + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); + require(report.Framed == 2 && sites.size() == 4 && displacements.size() == 4, "leaves that take the address of a stack slot were not framed"); + require(sites[0].Body[FramelessPrologue.size() + 4] == 0x80 && displacements[0].Bytes == std::vector{0x70} && displacements[1].Bytes == std::vector{0x60}, "a leaf that takes the address of a slot below rsp was not shifted by the whole red zone"); + require(sites[2].Body[FramelessPrologue.size() + 4] == 0xf0 && displacements[2].Bytes == std::vector{0x00} && displacements[3].Bytes == std::vector{0x18}, "a leaf that only takes the address of a slot above rsp was shifted by more than its depth"); +} + +void shiftsByTheWholeRedZoneForIndexedOperands() { + std::vector body = StoreIndexedBelowRsp; + append(body, LoadBelowRsp); + const auto input = image({framelessLeaf(FramelessPrologue, body, FramelessExit)}); + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); + require(report.Framed == 1 && sites.size() == 2 && displacements.size() == 2, "a leaf with an indexed operand below rsp was not framed"); + require(sites[0].Body[FramelessPrologue.size() + 4] == 0x80 && displacements[0].Bytes == std::vector{0x70} && displacements[1].Bytes == std::vector{0x70}, "a leaf whose indexed operand can reach deeper than its displacement was not shifted by the whole red zone"); +} + +void stubsOperandsWhoseDisplacementOverflows() { + std::vector body = StoreBelowRsp; + append(body, LoadFarAboveRsp); + append(body, StoreImmediateFarAboveRsp); + const auto input = image({framelessLeaf(FramelessPrologue, body, FramelessExit)}); + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); + require(report.Framed == 1 && report.MovedOperands == 1 && report.OperandStubs == 2 && report.StubbedOperands == 2 && sites.size() == 4, "a leaf whose 8-bit displacements above rsp overflow after the shift was not framed with operand stubs"); + const auto load = Base + 0x40 + FramelessPrologue.size() + StoreBelowRsp.size(); + require(sites[2].Address == load && sites[2].Length == LoadFarAboveRsp.size() && sites[2].OriginalBytes == LoadFarAboveRsp, "the stub site does not cover the load that cannot move in place"); + require(sites[2].Body == stubBody({0x48, 0x8b, 0x84, 0x24, 0x88, 0x00, 0x00, 0x00}) && sites[2].ReturnBranchOffset == 8, "the load was not re-encoded with a 32-bit displacement moved by the shift"); + require(sites[3].Address == load + LoadFarAboveRsp.size() && sites[3].OriginalBytes == StoreImmediateFarAboveRsp, "the stub site does not cover the store of an immediate that cannot move in place"); + require(sites[3].Body == stubBody({0xc7, 0x84, 0x24, 0x8c, 0x00, 0x00, 0x00, 0x78, 0x56, 0x34, 0x12}), "the store lost its immediate when its displacement was widened"); + require(displacements.size() == 1 && displacements[0].Bytes == std::vector{0x00}, "the store below rsp was not moved in place"); +} + +void stubsOperandsAtRspWithTheInstructionsAfterThem() { + std::vector atRsp = StoreBelowRsp; + append(atRsp, LoadAtRsp); + append(atRsp, LoadAboveRsp); + std::vector indexed = StoreBelowRsp; + append(indexed, LoadIndexedAtRsp); + append(indexed, AddRdxToRax); + const auto input = image({framelessLeaf(FramelessPrologue, atRsp, FramelessExit), framelessLeaf(FramelessPrologue, indexed, FramelessExit)}); + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); + require(report.Framed == 2 && report.MovedOperands == 2 && report.OperandStubs == 2 && report.StubbedOperands == 3 && sites.size() == 6, "leaves with operands at rsp were not framed with operand stubs"); + const auto first = 0x40 + FramelessPrologue.size() + StoreBelowRsp.size(); + require(sites[2].Address == Base + first && sites[2].Length == LoadAtRsp.size() + LoadAboveRsp.size(), "the stub site does not run on to the next instruction to make room for its jump"); + require(sites[2].Body == stubBody({0x48, 0x8b, 0x44, 0x24, 0x10, 0x48, 0x8b, 0x44, 0x24, 0x18}), "the load at rsp and the load the stub covers after it were not both moved by the shift"); + for (const auto& displacement : displacements) require(displacement.Offset < first || displacement.Offset >= first + sites[2].Length, "an rsp operand inside a stub site was also moved in place"); + require(sites[3].Body[FramelessPrologue.size() + 4] == 0x80, "a leaf with an indexed operand at rsp was not shifted by the whole red zone"); + require(sites[5].OriginalBytes.size() == LoadIndexedAtRsp.size() + AddRdxToRax.size() && sites[5].Body == stubBody({0x48, 0x8b, 0x84, 0xcc, 0x80, 0x00, 0x00, 0x00, 0x48, 0x01, 0xd0}), "the indexed load at rsp was not re-encoded with a 32-bit displacement of 0x80 ahead of the add it covers"); +} + +void stubsVexOperandsAtRsp() { + std::vector body = StoreBelowRsp; + append(body, VexStoreAtRsp); + const auto input = image({framelessLeaf(FramelessPrologue, body, FramelessExit)}); + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); + require(report.Framed == 1 && report.OperandStubs == 1 && report.StubbedOperands == 1 && sites.size() == 3, "a leaf with a VEX store at rsp was not framed with an operand stub"); + require(sites[2].OriginalBytes == VexStoreAtRsp && sites[2].Body == stubBody({0xc5, 0xf8, 0x11, 0x44, 0x24, 0x10}), "the VEX store at rsp was not re-encoded with an 8-bit displacement moved by the shift"); +} + +void stubsOperandsAtRspWithTheInstructionsBeforeThem() { + std::vector body = StoreBelowRsp; + append(body, LoadAtRsp); + append(body, LoadRipRelative); + const auto input = image({framelessLeaf(FramelessPrologue, body, FramelessExit)}); + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); + require(report.Framed == 1 && report.MovedOperands == 0 && report.OperandStubs == 1 && report.StubbedOperands == 2 && sites.size() == 3 && displacements.empty(), "a leaf whose short operand at rsp sits before a rip-relative instruction was not framed with a stub over the instruction before it"); + const auto store = Base + 0x40 + FramelessPrologue.size(); + require(sites[2].Address == store && sites[2].Length == StoreBelowRsp.size() + LoadAtRsp.size(), "the stub site does not start at the store before the operand at rsp"); + require(sites[2].Body == stubBody({0x48, 0x89, 0x7c, 0x24, 0x00, 0x48, 0x8b, 0x44, 0x24, 0x10}), "the store before the operand and the operand at rsp were not both moved by the shift"); +} + +void skipsLeavesWithOperandsThatCannotMove() { + std::vector evex = StoreBelowRsp; + append(evex, EvexStoreBelowRsp); + std::vector beforeExit = StoreBelowRsp; + append(beforeExit, LoadAtRsp); + std::vector beforeTarget = StoreBelowRsp; + append(beforeTarget, LoadAtRsp); + append(beforeTarget, {0xff, 0xc0, 0x3b, 0x44, 0x24, 0xf0, 0x72, 0xf8}); + const auto input = image({framelessLeaf(FramelessPrologue, evex, FramelessExit), framelessLeaf(FramelessPrologue, beforeExit, FramelessExit), framelessLeaf(FramelessPrologue, beforeTarget, FramelessExit), framelessLeaf({0x41, 0x56, 0x41, 0x57}, StoreBelowRsp, {0x41, 0x5f, 0x41, 0x5e, 0xc3})}); + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); + require(sites.empty() && displacements.empty() && report.RedZoneFunctions == 4, "a leaf whose rsp operands can neither move nor be stubbed, or whose prologue is too short, was framed"); + require(report.Skipped.at("rsp operand that cannot move with rsp") == 1, "an EVEX operand, whose 8-bit displacement is scaled, was not refused"); + require(report.Skipped.at("rsp operand that cannot move and has no room for a stub") == 2, "an operand at rsp followed by an exit or a branch target was not refused for want of room"); + require(report.Skipped.at("prologue shorter than a jump") == 1, "the leaf with a short prologue was not reported with its reason"); +} + +void skipsLeavesThatCopyRsp() { + std::vector withCmov = StoreBelowRsp; + append(withCmov, {0x48, 0x0f, 0x45, 0xc4}); + std::vector withShlx = StoreBelowRsp; + append(withShlx, {0xc4, 0xe2, 0xf1, 0xf7, 0xc4}); + const auto input = image({framelessLeaf(FramelessPrologue, withCmov, FramelessExit), framelessLeaf(FramelessPrologue, withShlx, FramelessExit)}); + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); + require(sites.empty() && displacements.empty() && report.Skipped.at("other use of rsp") == 2, "a leaf that copies rsp into another register was framed"); +} + +const std::vector InflatePrologue{0x55, 0x41, 0x57, 0x41, 0x56, 0x41, 0x55, 0x41, 0x54, 0x53, 0x48, 0x83, 0xec, 0x40}; +const std::vector InflatePops{0x5b, 0x41, 0x5c, 0x41, 0x5d, 0x41, 0x5e, 0x41, 0x5f, 0x5d, 0xc3}; +const std::vector InflateFree{0x48, 0x83, 0xc4, 0x40}; + +std::vector inflateExit() { + std::vector bytes = InflateFree; + append(bytes, InflatePops); + return bytes; +} + +std::vector inflateBody() { + return { + 0x48, 0x89, 0x7c, 0x24, 0xe0, + 0x48, 0x89, 0x7c, 0x24, 0x00, + 0x48, 0x89, 0x74, 0x24, 0x08, + 0x48, 0x89, 0x54, 0x24, 0x38, + 0xb9, 0x04, 0x00, 0x00, 0x00, + 0x31, 0xc0, + 0x48, 0x03, 0x44, 0x24, 0x08, + 0x48, 0x03, 0x44, 0x24, 0xe0, + 0x48, 0x03, 0x44, 0x24, 0x00, + 0xff, 0xc9, + 0x75, 0xed, + 0x48, 0x8b, 0x54, 0x24, 0x38, + }; +} + +std::vector inflateCompareBody(const std::vector& prior, const std::uint8_t back) { + std::vector bytes{ + 0x48, 0x89, 0x7c, 0x24, 0xe0, + 0x48, 0x89, 0x7c, 0x24, 0x00, + 0xb9, 0x04, 0x00, 0x00, 0x00, + 0x31, 0xc0, + 0x48, 0x03, 0x44, 0x24, 0xe0, + }; + append(bytes, prior); + append(bytes, {0x48, 0x39, 0x3c, 0x24, 0x75, 0x03, 0x48, 0x01, 0xf0, 0xff, 0xc9, 0x75, back}); + return bytes; +} + +const std::vector AddRdiToR8{0x49, 0x01, 0xf8}; +const std::vector CompareRdiAtRspShifted{0x48, 0x39, 0x7c, 0x24, 0x20}; + +void stubsShortOperandsAtRspBeforeABranchWithTheInstructionsBeforeThem() { + const auto withAdd = inflateCompareBody(AddRdiToR8, 0xeb); + std::vector withLoad{ + 0x48, 0x89, 0x7c, 0x24, 0xe0, + 0x48, 0x89, 0x7c, 0x24, 0x00, + 0xb9, 0x04, 0x00, 0x00, 0x00, + 0x31, 0xc0, + 0x48, 0x03, 0x44, 0x24, 0xe0, + 0x48, 0x39, 0x3c, 0x24, 0x75, 0x03, 0x48, 0x01, 0xf0, 0xff, 0xc9, 0x75, 0xee, + }; + const auto input = image({framelessLeaf(InflatePrologue, withAdd, inflateExit()), framelessLeaf(InflatePrologue, withLoad, inflateExit())}); + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); + require(report.Framed == 2 && report.OperandStubs == 2 && report.StubbedOperands == 3 && report.MovedOperands == 5 && sites.size() == 6 && report.Skipped.empty(), "inflate_fast's 4-byte cmp at rsp between an instruction and a branch was not framed with a stub over the instruction before it"); + const auto add = input.functions[0].Begin + InflatePrologue.size() + 22; + require(sites[2].Address == add && sites[2].Length == AddRdiToR8.size() + 4, "the stub site does not start at the add before the cmp at rsp"); + std::vector addBody = AddRdiToR8; + append(addBody, CompareRdiAtRspShifted); + require(sites[2].Body == stubBody(addBody), "the add was not copied ahead of the cmp re-encoded with a displacement of the shift"); + const auto load = input.functions[1].Begin + InflatePrologue.size() + 17; + require(sites[5].Address == load && sites[5].Length == 9, "the stub site does not start at the load before the cmp at rsp"); + std::vector loadBody{0x48, 0x03, 0x44, 0x24, 0x00}; + append(loadBody, CompareRdiAtRspShifted); + require(sites[5].Body == stubBody(loadBody), "the load before the cmp, itself an rsp operand, was not moved by the shift in the stub"); + for (const auto& displacement : displacements) { + const auto at = Base + displacement.Offset; + require((at < sites[2].Address || at >= sites[2].Address + sites[2].Length) && (at < sites[5].Address || at >= sites[5].Address + sites[5].Length), "an rsp operand inside a stub site was also moved in place"); + } +} + +void skipsShortOperandsAtRspWhoseWidenedStubCoversABranchTarget() { + std::vector body{ + 0x48, 0x89, 0x7c, 0x24, 0xe0, + 0x48, 0x89, 0x7c, 0x24, 0x00, + 0xb9, 0x04, 0x00, 0x00, 0x00, + 0x31, 0xc0, + }; + append(body, AddRdiToR8); + append(body, {0x48, 0x39, 0x3c, 0x24, 0x75, 0x03, 0x48, 0x01, 0xf0, 0xff, 0xc9, 0x75, 0xf3}); + const auto input = image({framelessLeaf(InflatePrologue, body, inflateExit())}); + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); + require(sites.empty() && displacements.empty() && report.RedZoneFunctions == 1 && report.Skipped.at("rsp operand that cannot move and has no room for a stub") == 1, "a cmp at rsp whose loop branches back to it was framed with a stub that starts at the add before it"); +} + +void framesLeavesThatAllocateBelowTheirPushes() { + const auto body = inflateBody(); + const auto input = image({framelessLeaf(InflatePrologue, body, inflateExit())}); + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); + require(report.RedZoneFunctions == 1 && report.Framed == 1 && report.FramedWithoutFramePointer == 1 && report.Exits == 1 && sites.size() == 2, "a leaf that allocates with sub after its pushes and spills below its allocation was not framed"); + const auto& entry = sites[0]; + require(entry.Address == Base + 0x40 && entry.OriginalBytes == InflatePrologue, "the prologue site does not cover the pushes and the sub"); + std::vector entryBody = InflatePrologue; + append(entryBody, {0x48, 0x8d, 0x64, 0x24, 0xe0, 0xe9, 0, 0, 0, 0}); + require(entry.Body == entryBody, "the prologue stub does not lower rsp by 0x20 after the sub"); + const auto& exit = sites[1]; + require(exit.Address == Base + 0x40 + InflatePrologue.size() + body.size() && exit.OriginalBytes == inflateExit(), "the exit site does not cover the add, the pops and the ret"); + std::vector exitBody{0x48, 0x8d, 0xa4, 0x24, 0x20, 0, 0, 0}; + append(exitBody, inflateExit()); + append(exitBody, {0xe9, 0, 0, 0, 0}); + require(exit.Body == exitBody, "the exit stub does not raise rsp by 0x20 before the add"); + const std::vector moved{0x00, 0x20, 0x28, 0x58, 0x28, 0x00, 0x20, 0x58}; + require(displacements.size() == moved.size() && report.MovedOperands == moved.size(), "not every rsp operand of the allocating leaf was moved"); + for (std::size_t index = 0; index < moved.size(); ++index) + require(displacements[index].Bytes == std::vector{moved[index]}, "an rsp operand of the allocating leaf was not moved by 0x20 from the post-sub rsp"); + require(displacements[0].Offset == 0x40 + InflatePrologue.size() + 4, "the spill below the allocation was not the first operand moved"); +} + +void framesLeavesThatAllocateWithLeaOrALargeSub() { + const std::vector leaPrologue{0x53, 0x41, 0x56, 0x48, 0x8d, 0x64, 0x24, 0xc0}; + const std::vector leaExit{0x48, 0x8d, 0x64, 0x24, 0x40, 0x41, 0x5e, 0x5b, 0xc3}; + const std::vector largePrologue{0x53, 0x48, 0x81, 0xec, 0x00, 0x01, 0x00, 0x00}; + const std::vector largeExit{0x48, 0x81, 0xc4, 0x00, 0x01, 0x00, 0x00, 0x5b, 0xc3}; + std::vector largeBody = StoreBelowRsp; + append(largeBody, {0x48, 0x8b, 0x84, 0x24, 0x80, 0x00, 0x00, 0x00}); + const auto input = image({framelessLeaf(leaPrologue, StoreBelowRsp, leaExit), framelessLeaf(largePrologue, largeBody, largeExit)}); + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); + require(report.Framed == 2 && sites.size() == 4 && displacements.size() == 3, "leaves that allocate with lea or a 32-bit sub were not framed"); + require(sites[0].OriginalBytes == leaPrologue && sites[1].OriginalBytes == leaExit, "the lea allocation and free are not part of the prologue and exit sites"); + require(sites[2].OriginalBytes == largePrologue && sites[3].OriginalBytes == largeExit, "the 32-bit sub and add are not part of the prologue and exit sites"); + require(displacements[2].Bytes == std::vector{0x90, 0x00, 0x00, 0x00}, "the 32-bit displacement into the allocation was not moved by the shift"); +} + +void skipsAllocatingLeavesWithoutAMatchingFree() { + const std::vector otherFree{0x48, 0x83, 0xc4, 0x30}; + std::vector wrongFree = otherFree; + append(wrongFree, InflatePops); + std::vector moreRsp = StoreBelowRsp; + append(moreRsp, {0x48, 0x83, 0xec, 0x08}); + const auto input = image({framelessLeaf(InflatePrologue, StoreBelowRsp, InflatePops), framelessLeaf(InflatePrologue, StoreBelowRsp, wrongFree), framelessLeaf(InflatePrologue, moreRsp, inflateExit())}); + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); + require(sites.empty() && displacements.empty() && report.RedZoneFunctions == 3, "an allocating leaf without a matching free, or with more rsp arithmetic, was framed"); + require(report.Skipped.at("exit does not free the prologue's allocation") == 1, "an exit that does not free the allocation was not reported with its own reason"); + require(report.Skipped.at("other use of rsp") == 2, "an add of another size before the pops, or more rsp arithmetic in the body, was not reported as another use of rsp"); +} + +Image calledThenUncalled(const std::vector& called, const std::vector& uncalled) { + Image result; + result.bytes = callTo(0, 0x40); + result.bytes.push_back(0xc3); + result.bytes = padTo(result.bytes, 0x40); + append(result.bytes, called); + result.bytes.resize(0x60, 0x90); + append(result.bytes, uncalled); + result.bytes = padTo(result.bytes, 0x80); + result.segments.push_back({1, 5, 0, Base, Base, result.bytes.size(), result.bytes.size(), 16}); + result.functions.push_back({Base + 0x40, Base + 0x40 + called.size()}); + result.functions.push_back({Base + 0x60, Base + 0x60 + uncalled.size()}); + return result; +} + +void framesEachFunctionFromItsOwnFde() { + std::vector body = StoreBelowRsp; + append(body, LoadBelowRsp); + const auto input = calledThenUncalled(framelessLeaf(FramelessPrologue, body, FramelessExit), framelessLeaf(FramelessPrologue, StoreBelowRsp, FramelessExit)); + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); + require(report.Functions == 2 && report.Framed == 2 && report.Exits == 2 && sites.size() == 4 && displacements.size() == 3, "two adjacent functions were not framed each by its own FDE"); + require(sites[0].Address < Base + 0x60 && sites[1].Address + sites[1].Length <= Base + 0x60 && displacements[0].Offset < 0x60 && displacements[1].Offset < 0x60, "the first function's frame runs past its FDE end"); + require(sites[2].Address == Base + 0x60 && sites[2].OriginalBytes == FramelessPrologue && displacements[2].Offset > 0x60, "the function reached without a call was not framed from its own FDE start"); +} + +void treatsATailCallPastTheFdeEndAsLeaving() { + std::vector called = FramelessPrologue; + append(called, StoreBelowRsp); + append(called, {0x85, 0xff, 0x75, 0x06}); + append(called, FramelessExit); + append(called, {0x41, 0x5f, 0x41, 0x5e, 0x5b, 0xe9, 0x02, 0x00, 0x00, 0x00}); + const auto input = calledThenUncalled(called, framelessLeaf(FramelessPrologue, StoreBelowRsp, FramelessExit)); + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); + require(report.Skipped.at("branch leaves the function") == 1, "the tail call past the FDE end was not reported as leaving the function"); + require(report.Framed == 1 && sites.size() == 2 && sites[0].Address == Base + 0x60, "the tail-called function was not framed from its own FDE start"); +} + +void framesALeafAfterAJumpWithoutInt3FromItsOwnFde() { + std::vector first = FramelessPrologue; + append(first, StoreBelowRsp); + append(first, {0x31, 0xc0, 0xeb, 0x06}); + append(first, FramelessExit); + append(first, {0x48, 0x3b, 0x44, 0x24, 0xf0, 0x73, 0xf3, 0x48, 0xff, 0xc0, 0xeb, 0xf4}); + require(first.size() == 0x20, "the first leaf must end where the second starts"); + std::vector second = StoreBelowRsp; + append(second, LoadBelowRsp); + const auto input = calledThenUncalled(first, framelessLeaf(FramelessPrologue, second, FramelessExit)); + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); + require(report.Framed == 2 && report.Exits == 2 && sites.size() == 4 && displacements.size() == 4, "a leaf right after a jmp without int3 padding was not framed on its own"); + require(sites[1].Address + sites[1].Length <= Base + 0x60 && displacements[1].Offset < 0x60, "the leaf ending in a jmp took the next leaf into its body"); + require(sites[2].Address == Base + 0x60 && sites[2].OriginalBytes == FramelessPrologue && displacements[2].Offset > 0x60, "the leaf after the jmp was not framed from its own FDE start"); +} + +void keepsColdBlocksAfterARetInsideTheFde() { + std::vector leaf = FramelessPrologue; + append(leaf, StoreBelowRsp); + append(leaf, {0x85, 0xff, 0x74, 0x12}); + append(leaf, LoadBelowRsp); + append(leaf, FramelessExit); + append(leaf, {0x0f, 0x1f, 0x80, 0x00, 0x00, 0x00, 0x00}); + append(leaf, {0x48, 0x8b, 0x54, 0x24, 0xf0}); + append(leaf, FramelessExit); + const auto input = image({leaf}); + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); + require(report.Framed == 1 && report.Exits == 2 && sites.size() == 3, "the exit of a cold block after the ret inside the FDE was not framed"); + require(displacements.size() == 3 && displacements[2].Offset == 0x40 + 0x20 + 4 && displacements[2].Bytes == std::vector{0x00}, "the rsp operand of a cold block after the ret inside the FDE was not moved"); +} + +void keepsLoopsAlignedAfterAJump() { + std::vector loop = FramelessPrologue; + append(loop, StoreBelowRsp); + append(loop, {0x31, 0xc0, 0xeb, 0x04, 0x66, 0x90, 0xff, 0xc0, 0x3b, 0x44, 0x24, 0xf0, 0x72, 0xf8}); + append(loop, FramelessExit); + const auto input = image({loop}); + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); + require(report.Framed == 1 && sites.size() == 2 && displacements.size() == 2, "a leaf whose loop header is aligned after a jump was not framed"); +} + +void leavesOtherFunctions() { + std::vector nonLeaf = Prologue; + append(nonLeaf, StoreBelowPushes); + append(nonLeaf, {0xff, 0xd0}); + append(nonLeaf, Exit); + const auto input = image({leaf(LoadAbovePushes), nonLeaf}); + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {}, report, displacements); + require(sites.empty() && report.RedZoneFunctions == 0, "a leaf without red-zone use or a function that calls was framed"); +} + +void skipsSitesTakenByOtherStubs() { + const auto input = image({leaf(StoreBelowPushes)}); + Codegen::TrampolineSite other{}; + other.Address = Base + 0x40; + other.Length = 5; + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, input.functions, {other}, report, displacements); + require(sites.empty() && report.Skipped.at("overlaps another stub") == 1, "a site over another stub was framed"); +} + +void framesAPointerOnlyFunctionByItsOwnBounds() { + const auto first = leaf(StoreBelowPushes); + std::vector secondBody = StoreBelowPushes; + append(secondBody, LoadBelowPushes); + const auto second = leaf(secondBody); + std::vector bytes = callTo(0, 0x40); + bytes.push_back(0xc3); + bytes = padTo(bytes, 0x40); + append(bytes, first); + const auto secondStart = bytes.size(); + append(bytes, second); + bytes = padTo(bytes, (bytes.size() + 15) / 16 * 16 + 16); + const std::vector segments{{1, 5, 0, Base, Base, bytes.size(), bytes.size(), 16}}; + const std::vector functions{{Base + 0x40, Base + 0x40 + first.size()}, {Base + secondStart, Base + secondStart + second.size()}}; + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(bytes, segments, functions, {}, report, displacements); + require(report.Functions == 2 && report.Framed == 2 && report.Exits == 2, "a function reached only through a pointer was not framed by its own bounds"); + require(sites.size() == 4, "two framed leaves need two prologue and two exit sites"); + require(sites[2].Address == Base + secondStart && sites[2].OriginalBytes == Prologue, "the second function's prologue site is not at its own start"); + require(sites[1].Address + sites[1].Length == Base + 0x40 + first.size(), "the first function's exit runs past its end"); +} + +void leavesCodeWithoutAFunctionRange() { + const auto input = image({leaf(StoreBelowPushes)}); + Codegen::RedZoneFrameReport report; + std::vector displacements; + const auto sites = Codegen::FrameRedZoneFunctions(input.bytes, input.segments, {}, {}, report, displacements); + require(sites.empty() && report.Functions == 0, "code without a function range was framed"); +} + +} + +int main() { + try { + framesRedZoneLeaf(); + leavesOtherFunctions(); + skipsSitesTakenByOtherStubs(); + framesLeafWithoutFramePointer(); + shiftsByTheDeepestOperandBelowRsp(); + movesPositiveOperandsThatFitAfterTheShift(); + shiftsByTheWholeRedZoneForAddressesBelowRsp(); + shiftsByTheWholeRedZoneForIndexedOperands(); + framesLeavesThatAllocateBelowTheirPushes(); + framesLeavesThatAllocateWithLeaOrALargeSub(); + skipsAllocatingLeavesWithoutAMatchingFree(); + stubsOperandsWhoseDisplacementOverflows(); + stubsOperandsAtRspWithTheInstructionsAfterThem(); + stubsVexOperandsAtRsp(); + stubsOperandsAtRspWithTheInstructionsBeforeThem(); + stubsShortOperandsAtRspBeforeABranchWithTheInstructionsBeforeThem(); + skipsShortOperandsAtRspWhoseWidenedStubCoversABranchTarget(); + skipsLeavesWithOperandsThatCannotMove(); + skipsLeavesThatCopyRsp(); + framesEachFunctionFromItsOwnFde(); + treatsATailCallPastTheFdeEndAsLeaving(); + framesALeafAfterAJumpWithoutInt3FromItsOwnFde(); + keepsColdBlocksAfterARetInsideTheFde(); + keepsLoopsAlignedAfterAJump(); + framesAPointerOnlyFunctionByItsOwnBounds(); + leavesCodeWithoutAFunctionRange(); + std::cout << "red-zone framer tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << "red-zone framer tests failed: " << error.what() << '\n'; + return 1; + } +} diff --git a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp index dbcca232a2..2be086cc21 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp @@ -157,6 +157,7 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con std::vector searchCounts; std::vector platformTlsResolvers; std::vector imported(libraries.size()); + std::vector executableImports(guestModules.size()); const bool unscopedImports = std::any_of(imports.begin(), imports.end(), [](const auto& import) { return import.Library.empty(); }); for (const auto& import : imports) { const auto library = Domain::NeededLibraryName(import.Library); @@ -167,6 +168,7 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con const bool guestName = index < guestModules.size() && std::find(guestModules[index].Names.begin(), guestModules[index].Names.end(), import.Library) != guestModules[index].Names.end(); if (import.Library.empty() || name == library || guestName) order.push_back(CheckedRva(index)); + if (import.TargetModule < 0 && !import.Library.empty() && index < guestModules.size() && (name == library || guestName)) executableImports[index] = true; } if (order.empty()) throw Domain::RelinkerException("Import module is not a dependency: " + import.Library); const bool hostModule = !import.Library.empty() && @@ -249,6 +251,11 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con const auto searched = addString("Searched libraries:\n"); const auto indent = addString(" "); const auto enteringElf = addString("Transferring control to ELF entry point\n"); + std::vector deferredImports; + for (std::size_t index = 0; index < guestModules.size(); ++index) { + if (guestModules[index].DeferInitialization && executableImports[index]) + deferredImports.push_back(addString("Deferred module " + guestModules[index].Path + ": the executable imports from it, but it starts only when the title loads it\n")); + } std::vector resolvedPaths; for (std::size_t index = 0; index < libraries.size(); ++index) resolvedPaths.push_back(reserve(PathCapacity)); @@ -717,6 +724,7 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con code.Emit({0x85, 0xc0}); unresolvedBranches.push_back(code.Branch({0x0f, 0x85})); } + for (const auto message : deferredImports) writeString(message); guestStartup.Initialize(code, guestModules, handles); writeString(enteringElf); code.Rip({0x48, 0x8d, 0x0d}, shellLibrary); diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 91c5b09ee7..6f2a15fe2e 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -57,6 +57,7 @@ ConvertedExecutable Convert(const Cli::Args& args, const std::string& inputPath, options.Target = args.toWindows ? Relinker::TargetSystem::Windows : args.toMacos ? Relinker::TargetSystem::MacOs : Relinker::TargetSystem::Linux; options.ToIntel = args.toIntel; options.ToRosetta = args.toRosetta; + options.FrameRedZone = args.frameRedZone; options.SkipSyscallCheck = args.skipSyscallCheck; options.TrapSdkSyscallStubs = args.trapSdkSyscallStubs; options.UnusedFilterLevel = args.unusedFilterLevel; diff --git a/core/relinker/relinker/include/relinker/patching/GamePatch.hpp b/core/relinker/relinker/include/relinker/patching/GamePatch.hpp index 6b76048dfb..e04f9605e3 100644 --- a/core/relinker/relinker/include/relinker/patching/GamePatch.hpp +++ b/core/relinker/relinker/include/relinker/patching/GamePatch.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -83,6 +84,8 @@ struct WriterSites { std::span Traps; std::span RelaData; std::span RelaPltData; + std::span RedZoneSites = {}; + std::span RedZoneDisplacements = {}; }; enum class VerifyStatus { Applicable, Applied, NotChecked, Mismatch }; diff --git a/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp b/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp index 6998e95873..3a45a1ea10 100644 --- a/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp +++ b/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp @@ -15,6 +15,7 @@ struct ConversionOptions { std::uint32_t UnusedFilterLevel = 0; bool ToIntel = false; bool ToRosetta = false; + bool FrameRedZone = false; bool SkipSyscallCheck = false; bool TrapSdkSyscallStubs = false; bool LazyBinding = false; diff --git a/core/relinker/relinker/src/patching/GamePatch.cpp b/core/relinker/relinker/src/patching/GamePatch.cpp index 0d278b51f4..12f6451372 100644 --- a/core/relinker/relinker/src/patching/GamePatch.cpp +++ b/core/relinker/relinker/src/patching/GamePatch.cpp @@ -481,6 +481,8 @@ void CheckOverlaps(const std::string& target, const std::vector& p for (const auto& site : writers.Trampolines) sites.push_back({site.Offset, site.Length, "Intel trampoline site"}); for (const auto& patch : writers.RelinkPatches) sites.push_back({patch.Offset, patch.Bytes.size(), "relinker patch"}); sites.insert(sites.end(), writers.Traps.begin(), writers.Traps.end()); + for (const auto& site : writers.RedZoneSites) sites.push_back({site.Offset, site.Length, "red-zone frame site"}); + for (const auto& moved : writers.RedZoneDisplacements) sites.push_back({moved.Offset, moved.Bytes.size(), "red-zone moved displacement"}); const auto relaViolation = [&](const std::span table, const char* name, const PatchedRange& range) { for (std::size_t position = 0; position + 24 <= table.size(); position += 24) { std::uint64_t relocation = 0; diff --git a/core/relinker/relinker/src/pipeline/Conversion.cpp b/core/relinker/relinker/src/pipeline/Conversion.cpp index 1cf1879466..1f371ecbca 100644 --- a/core/relinker/relinker/src/pipeline/Conversion.cpp +++ b/core/relinker/relinker/src/pipeline/Conversion.cpp @@ -18,6 +18,8 @@ #include #include #include +#include +#include #include #include #include @@ -41,6 +43,7 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti if (options.Target != TargetSystem::Linux && options.Target != TargetSystem::Windows && options.Target != TargetSystem::MacOs) throw Domain::RelinkerException("Unknown target system"); if (options.ToRosetta && (!options.ToIntel || options.Target == TargetSystem::Windows)) throw Domain::RelinkerException("Rosetta conversion requires Intel lowering and a non-Windows target"); if (options.UnusedFilterLevel > 2) throw Domain::RelinkerException("Unused NID filter level must be 0, 1 or 2"); + if (options.FrameRedZone && options.Target != TargetSystem::Windows) throw Domain::RelinkerException("Red-zone framing requires a Windows target: only Windows exception dispatch writes below RSP"); if (input.InputPath.empty() || input.OutputPath.empty()) throw Domain::RelinkerException("Input and output names are required"); if (input.InputPath.lexically_normal() == input.OutputPath.lexically_normal()) throw Domain::RelinkerException("Executable output would overwrite the input executable"); if (!input.Modules.empty() && input.ModuleRoot.empty()) throw Domain::RelinkerException("Module root is required"); @@ -105,7 +108,27 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti throw Domain::RelinkerException("Relinker patch exceeds source image", patch.Offset); for (std::size_t index = 0; index < patch.Bytes.size(); ++index) sourceBytes[patch.Offset + index] = patch.Bytes[index]; } - GamePatch::CheckOverlaps(options.TargetName, patchedRanges, {.RelinkPatches = result.Patches, .RelaData = result.DynamicSection.RelaData, .RelaPltData = result.DynamicSection.RelaPltData}); + const auto intelSites = trampolines.size(); + std::vector displacements; + if (options.FrameRedZone) { + const auto headers = Relinker::ElfReader(sourceBytes).ReadProgramHeaders(); + std::vector codeSegments; + for (const auto& header : headers) if (header.Type == 1 && (header.Flags & 1) != 0) codeSegments.push_back(header); + std::vector functions; + for (const auto& region : Relinker::UnusedNidFilter::ReadExceptionFunctions(sourceBytes, headers, {}, {})) functions.push_back({region.Begin, region.End}); + if (functions.empty()) throw Domain::RelinkerException("Red-zone framing needs function bounds from the executable's .eh_frame, and it has none"); + Codegen::RedZoneFrameReport report; + auto framed = Codegen::FrameRedZoneFunctions(sourceBytes, codeSegments, functions, trampolines, report, displacements); + for (const auto& moved : displacements) { + if (moved.Offset > sourceBytes.size() || moved.Bytes.size() > sourceBytes.size() - moved.Offset) + throw Domain::RelinkerException("Red-zone displacement exceeds source image", moved.Offset); + std::copy(moved.Bytes.begin(), moved.Bytes.end(), sourceBytes.begin() + static_cast(moved.Offset)); + } + { std::ostringstream message; message << "Red-zone framing: " << report.Functions << " functions, " << report.RedZoneFunctions << " leaf functions keep values below RSP, " << report.Framed << " framed (" << report.FramedWithoutFramePointer << " without a frame pointer, " << report.MovedOperands << " rsp operands moved, " << report.StubbedOperands << " in " << report.OperandStubs << " operand stubs; " << report.Exits << " exits)\n"; inputProgress.Message(message.str()); } + for (const auto& [reason, count] : report.Skipped) { std::ostringstream message; message << "Red-zone framing: skipped " << count << ": " << reason << '\n'; inputProgress.Message(message.str()); } + trampolines.insert(trampolines.end(), std::make_move_iterator(framed.begin()), std::make_move_iterator(framed.end())); + } + GamePatch::CheckOverlaps(options.TargetName, patchedRanges, {.RelinkPatches = result.Patches, .RelaData = result.DynamicSection.RelaData, .RelaPltData = result.DynamicSection.RelaPltData, .RedZoneSites = std::span(trampolines).subspan(intelSites), .RedZoneDisplacements = displacements}); if (!options.Patches.empty()) { for (auto& module : input.Modules) { diff --git a/core/relinker/relinker/tests/GamePatchTests.cpp b/core/relinker/relinker/tests/GamePatchTests.cpp index 641b830206..0b6ab99a13 100644 --- a/core/relinker/relinker/tests/GamePatchTests.cpp +++ b/core/relinker/relinker/tests/GamePatchTests.cpp @@ -353,6 +353,14 @@ void overlapWithWriters() { CheckOverlaps("t", patched, {.Traps = trapBefore}); const std::vector window{{0x100f, 4, "window"}}; reject({.Traps = window}, "window"); + const std::vector frame{{0x1011, 0x401011, 5, {}, {}, 0}}; + reject({.RedZoneSites = frame}, "red-zone frame site"); + const std::vector frameBefore{{0x100b, 0x40100b, 5, {}, {}, 0}}; + CheckOverlaps("t", patched, {.RedZoneSites = frameBefore}); + const std::vector moved{{0x100f, {0x10, 0, 0, 0}}}; + reject({.RedZoneDisplacements = moved}, "red-zone moved displacement"); + const std::vector movedBefore{{0x100f, {0x10}}}; + CheckOverlaps("t", patched, {.RedZoneDisplacements = movedBefore}); } void conversion() { @@ -400,6 +408,56 @@ void conversion() { require(repeated.Executable == baseline.Executable && repeated.Modules.front().Bytes == baseline.Modules.front().Bytes, "no entries changed the output"); } +std::vector framedLeafFixture() { + auto bytes = conversionFixture(); + Io::WriteU64(bytes, 64 + 32, 0x80); + Io::WriteU64(bytes, 64 + 40, 0x80); + std::fill(bytes.begin() + 0x1000, bytes.begin() + 0x1080, 0xcc); + const std::vector caller{0xe8, 0x3b, 0x00, 0x00, 0x00, 0xc3}; + const std::vector leaf{0x53, 0x41, 0x56, 0x41, 0x57, 0x48, 0x89, 0x7c, 0x24, 0xf0, 0x48, 0x8b, 0x04, 0x24, 0x48, 0x8b, 0x44, 0x24, 0x08, 0x41, 0x5f, 0x41, 0x5e, 0x5b, 0xc3}; + std::copy(caller.begin(), caller.end(), bytes.begin() + 0x1000); + std::copy(leaf.begin(), leaf.end(), bytes.begin() + 0x1040); + const auto ehFrameHeader = 64 + 7 * 56; + Io::WriteU32(bytes, ehFrameHeader, 0x6474e550); + Io::WriteU32(bytes, ehFrameHeader + 4, 4); + for (const auto field : {8, 16, 24}) Io::WriteU64(bytes, ehFrameHeader + field, 0x2900); + Io::WriteU64(bytes, ehFrameHeader + 32, 32); + Io::WriteU64(bytes, ehFrameHeader + 40, 32); + Io::WriteU64(bytes, ehFrameHeader + 48, 4); + Io::WriteU32(bytes, 0x2800, 9); + const std::vector cie{1, 0, 1, 0x78, 16}; + std::copy(cie.begin(), cie.end(), bytes.begin() + 0x2808); + Io::WriteU32(bytes, 0x2840, 20); + Io::WriteU32(bytes, 0x2844, 0x44); + Io::WriteU64(bytes, 0x2848, 0x1040); + Io::WriteU64(bytes, 0x2850, leaf.size()); + bytes[0x2900] = 1; + bytes[0x2902] = 3; + Io::WriteU64(bytes, 0x2904, 0x2800); + Io::WriteU32(bytes, 0x290c, 1); + Io::WriteU64(bytes, 0x2910, 0x1040); + Io::WriteU64(bytes, 0x2918, 0x2840); + return bytes; +} + +void framedLeafPatches() { + TestProgress progress; + const Conversion converter(progress); + const auto executable = framedLeafFixture(); + ConversionOptions options; + options.TargetName = "custom-name.bin"; + options.Target = TargetSystem::Windows; + options.FrameRedZone = true; + converter.Convert(conversionInput(executable), options); + require(progress.Messages.find("1 framed (1 without a frame pointer, 1 rsp operands moved, 2 in 1 operand stubs; 1 exits)") != std::string::npos, "the fixture's leaf was not framed with a moved displacement and an operand stub: " + progress.Messages); + options.Patches = {entry("custom-name.bin", 0x1010, {0xcc}, {0x90})}; + require(converter.Convert(conversionInput(executable), options).AppliedPatches.size() == 1, "a patch outside the framed leaf was not applied"); + options.Patches = {entry("custom-name.bin", 0x1049, {0xf0}, {0xe8})}; + rejects([&] { converter.Convert(conversionInput(executable), options); }, "overlaps red-zone moved displacement at off=0x1049 length 1"); + options.Patches = {entry("custom-name.bin", 0x104c, {0x04}, {0x0c})}; + rejects([&] { converter.Convert(conversionInput(executable), options); }, "overlaps red-zone frame site at off=0x104a length 9"); +} + void targets() { const std::vector entries{entry("a.elf", 1, {0}, {1}), entry("sce_module/libc.prx", 1, {0}, {1})}; MatchTargets(entries, {"a.elf", "b.elf", "sce_module/libc.prx"}); @@ -588,6 +646,7 @@ int main() { applicationRangeWidths(); overlapWithWriters(); conversion(); + framedLeafPatches(); targets(); verify(); rangeFailureInVerify(); diff --git a/core/relinker/relinker/tests/test_guest_module_dir.py b/core/relinker/relinker/tests/test_guest_module_dir.py index 7f2022a918..95e4f087ac 100644 --- a/core/relinker/relinker/tests/test_guest_module_dir.py +++ b/core/relinker/relinker/tests/test_guest_module_dir.py @@ -10,6 +10,7 @@ from test_guest_intel_trampolines import main_fixture, pe_sections from test_guest_module_directories import module_with_symbol, needed_libraries from test_guest_needed_modules import executable_with_needed, module_with_needed from test_macos_output import MachO +from test_windows_import_modules import executable, provider def pe_export_names(data): @@ -199,6 +200,21 @@ def main(): else: assert "$ORIGIN/app0/Media/Modules/needed.prx.guest.prx" in needed_libraries(output.read_bytes()), needed_libraries(output.read_bytes()) + for directory, deferred in (("Media/Modules", True), ("sce_module", False)): + case = work / f"{windows}-imported-{deferred}" + (case / directory).mkdir(parents=True) + (case / directory / "needed.prx").write_bytes(provider(42)) + result, output = convert(case, windows, ["--module-dir", directory] if deferred else [], executable("needed.prx")) + assert result.returncode == 0, (result.stdout, result.stderr) + line = f"Deferred module app0/{directory}/needed.prx.guest.prx: the executable imports from it, but it starts only when the title loads it\n" + if windows: + assert (line.encode() in output.read_bytes()) == deferred, directory + if os.name == "nt": + run = subprocess.run([str(output)], capture_output=True, text=True, timeout=30) + assert run.returncode == 42 and (line in run.stdout) == deferred, (directory, run.returncode, run.stdout, run.stderr) + else: + assert f"$ORIGIN/app0/{directory}/needed.prx.guest.prx" in needed_libraries(output.read_bytes()), needed_libraries(output.read_bytes()) + case = work / f"{windows}-root" case.mkdir() (case / "root.prx").write_bytes(module_with_symbol(True)) diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index 2f8d98958f..7ef4a942a8 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -178,6 +178,9 @@ function(add_shader_recompiler target runtimeTarget) if(ANYPS5_ENABLE_SPIRV_TOOLS) target_sources(${target} PRIVATE ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/SpirvBackend/src/SpirvOptimizer.cpp) + if(WIN32) + set_source_files_properties(${CMAKE_CURRENT_FUNCTION_LIST_DIR}/SpirvBackend/src/SpirvOptimizer.cpp TARGET_DIRECTORY ${target} PROPERTIES COMPILE_DEFINITIONS _WIN32_WINNT=0x0602) + endif() endif() set(shaderCacheGeneratedDir "${CMAKE_CURRENT_BINARY_DIR}/${target}_generated") diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index b3c679dc2e..f11023d4b4 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -139,6 +139,7 @@ std::vector GuestBuffersDescriptor(const std::vector GuestSamplersDescriptor(const std::vector& resources, const ResourceSnapshot& snapshot) { std::vector result; + result.reserve(resources.size() * 4u); std::uint32_t dwordCount = 0; for (std::size_t i = 0; i < resources.size(); i++) { const std::uint32_t r = resources[i]; @@ -567,6 +568,7 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con auto shaderData = needsShaderData ? ShaderDataDwordsFor(layout, userDataBase, snapshot, partialThreads, plan.imageMetadata, plan.runtimeState) : std::vector{}; allocation.shaderDataUserWords.clear(); if (needsShaderData) { + allocation.shaderDataUserWords.reserve(layout.userDataRegisters.size()); for (const auto reg : layout.userDataRegisters) allocation.shaderDataUserWords.push_back(reg - userDataBase); } std::vector bindings; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 9c74f0b94d..921b9bb653 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -1839,7 +1839,12 @@ ResolvedImageTables ResourceMaterializer::ResolveImageTables(const ShaderInfo& i } std::shared_ptr ResourceMaterializer::ResolveImageTablesShared(const ShaderInfo& info, const ResourceSnapshot& snapshot, std::uint64_t memoKey) { - if (memoKey == 0u || snapshot.tables.tables.empty() || !TableResolutionReuse()) return std::make_shared(ResolveImageTables(info, snapshot, memoKey)); + if (snapshot.tables.tables.empty()) { + static const auto none = std::make_shared(); + static_cast(ResolveImageTables(info, snapshot, memoKey)); + return none; + } + if (memoKey == 0u || !TableResolutionReuse()) return std::make_shared(ResolveImageTables(info, snapshot, memoKey)); auto reducing = reducingSamplers(info, snapshot); auto& memo = resolutionMemo(); auto& totals = tableReuseTotals(); diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 189a647205..fad7606ef9 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -246,6 +246,7 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat translateOptions.floatMode = request.context.floatMode; translateOptions.float32FlushToZero = HardwareFloat32Flush(request); translateOptions.inputInfo = inputInfo; + translateOptions.float32DenormPreserve = request.target.float32DenormPreserve; constexpr InstructionTranslator translator; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvOptimizer.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvOptimizer.hpp index 661caa053c..f50d5c6f37 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvOptimizer.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvOptimizer.hpp @@ -8,6 +8,7 @@ namespace ShaderRecompiler { [[nodiscard]] std::vector ValidateAndOptimizeSpirv(std::span spirv, std::uint32_t vulkanVersion, std::uint32_t spirvVersion, bool allowOffsetTextureOperand = false, bool optimize = true, bool specialize = false); +[[nodiscard]] std::uint64_t OptimizerHostThreads(); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp index 4a319c4f65..2b089a3e8f 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp @@ -3,16 +3,59 @@ #include #include #include +#include #include +#include #include #include #include +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif namespace ShaderRecompiler { namespace { constexpr std::uint32_t ScalarReplacementLimit = 64; +constexpr std::uintptr_t InlineOptimizerStackBytes = 256 * 1024; + +std::atomic optimizerHostThreads{0}; + +bool StackHoldsOptimizer() { +#ifdef _WIN32 + ULONG_PTR low = 0; + ULONG_PTR high = 0; + GetCurrentThreadStackLimits(&low, &high); +#else + struct Bounds { + std::uintptr_t low = 0; + std::uintptr_t high = 0; + Bounds() { + pthread_attr_t attributes; + if (pthread_getattr_np(pthread_self(), &attributes) != 0) return; + void* base = nullptr; + std::size_t size = 0; + if (pthread_attr_getstack(&attributes, &base, &size) == 0 && base != nullptr) { + low = reinterpret_cast(base); + high = low + size; + } + pthread_attr_destroy(&attributes); + } + }; + thread_local const Bounds bounds; + const auto low = bounds.low; + const auto high = bounds.high; +#endif + const auto frame = reinterpret_cast(__builtin_frame_address(0)); + return frame >= low && frame < high && frame - low >= InlineOptimizerStackBytes; +} bool HasLimitedUseTypes(std::span spirv) { if (spirv.size() < 5) throw std::runtime_error("SPIRV-Tools: incomplete module header"); @@ -101,9 +144,7 @@ std::vector CollapseBlockBallots(std::span s return collapsed; } -} - -std::vector ValidateAndOptimizeSpirv(std::span spirv, std::uint32_t vulkanVersion, std::uint32_t spirvVersion, bool allowOffsetTextureOperand, bool optimize, bool specialize) { +std::vector ValidateAndOptimizeSpirvInline(std::span spirv, std::uint32_t vulkanVersion, std::uint32_t spirvVersion, bool allowOffsetTextureOperand, bool optimize, bool specialize) { spv_target_env environment; const auto apiVersion = vulkanVersion & ~0xfffu; std::uint32_t maxSpirvVersion = 0; @@ -222,3 +263,26 @@ std::vector ValidateAndOptimizeSpirv(std::span ValidateAndOptimizeSpirv(std::span spirv, std::uint32_t vulkanVersion, std::uint32_t spirvVersion, bool allowOffsetTextureOperand, bool optimize, bool specialize) { + if (StackHoldsOptimizer()) return ValidateAndOptimizeSpirvInline(spirv, vulkanVersion, spirvVersion, allowOffsetTextureOperand, optimize, specialize); + std::vector result; + std::exception_ptr failure; + std::thread host([&] { + try { + result = ValidateAndOptimizeSpirvInline(spirv, vulkanVersion, spirvVersion, allowOffsetTextureOperand, optimize, specialize); + } catch (...) { + failure = std::current_exception(); + } + }); + optimizerHostThreads.fetch_add(1, std::memory_order_relaxed); + host.join(); + if (failure) std::rethrow_exception(failure); + return result; +} + +std::uint64_t OptimizerHostThreads() { + return optimizerHostThreads.load(std::memory_order_relaxed); +} + +} diff --git a/core/shader/recompiler/Translation/include/Translation/InstructionTranslator.hpp b/core/shader/recompiler/Translation/include/Translation/InstructionTranslator.hpp index 2c190b4239..21f6bc7e0e 100644 --- a/core/shader/recompiler/Translation/include/Translation/InstructionTranslator.hpp +++ b/core/shader/recompiler/Translation/include/Translation/InstructionTranslator.hpp @@ -36,6 +36,7 @@ struct TranslateOptions { bool interpolationFunctions = false; bool nativeFloat16 = false; bool float32FlushToZero = false; + bool float32DenormPreserve = false; bool gcnLegacyVertexInputs = false; std::optional floatMode; ShaderStageInputInfo inputInfo; diff --git a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp index d856fad819..00da5a7db5 100644 --- a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp +++ b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp @@ -25,6 +25,7 @@ public: } void SetEntryRounding(const RoundingState& state) { setRounding(state); } void SetNativeFloat16(bool enabled) { nativeFloat16 = enabled; } + void SetFloat32DenormPreserve(bool enabled) { float32DenormPreserve = enabled; } void SetFloat32FlushToZero(bool enabled) { float32FlushToZero = enabled; } void AddBranchCondition(const BasicBlock& source, BlockInfo& info); void TranslateCodeTableLoad(const RdnaInstruction& instruction, const ControlFlowGraph::CodeTableLoad& table); @@ -81,7 +82,7 @@ private: IrU32 flushTinyF16Conversion(IrF32 value, IrU32 bits); void writeF16Conversion(const RdnaOperand& operand, IrF32 value, std::initializer_list sources); IrU32 readU32(const RdnaOperand& operand); - IrU32 flushF32Denormal(IrU32 bits); + IrU32 flushF32Denormal(IrU32 bits, std::uint32_t flushBit = 1u); IrF32 flushTinyProduct(IrValue* lhs, IrValue* rhs, IrValue* product, IrValue* addend = nullptr, bool afterRounding = true); IrU32 quietNan32(IrU32 bits); IrU32 quietNan16(IrU32 bits); @@ -387,6 +388,7 @@ private: RdnaOpcode currentOpcode = RdnaOpcode::Unknown; std::uint32_t f32DenormalFlush = 0u; bool float32FlushToZero = false; + bool float32DenormPreserve = false; std::uint32_t currentProgramCounter = 0; std::uint32_t currentVectorLimit = 1; }; diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index b15814b78e..08393a3942 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -599,6 +599,7 @@ IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const Con context.SetPixelInput(options.inputInfo.pixel, options.fragmentShaderBarycentricEnabled); context.SetFloatMode(options.floatMode); context.SetNativeFloat16(options.nativeFloat16); + context.SetFloat32DenormPreserve(options.float32DenormPreserve); context.SetFloat32FlushToZero(options.float32FlushToZero); context.SetEntryRounding(entryRounding.at(cfgBlock.id)); for (std::uint32_t index = cfgBlock.instructionBegin; index < cfgBlock.instructionEnd; index++) { diff --git a/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp b/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp index d54715a372..ebec2355da 100644 --- a/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp +++ b/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp @@ -145,8 +145,8 @@ RdnaOperand TranslationContext::plainOperand(const RdnaOperand& operand) { return result; } -IrU32 TranslationContext::flushF32Denormal(IrU32 bits) { - if ((f32DenormalFlush & 1u) == 0u) return bits; +IrU32 TranslationContext::flushF32Denormal(IrU32 bits, std::uint32_t flushBit) { + if ((f32DenormalFlush & flushBit) == 0u) return bits; if (bits.Value().HasImmediate()) { const std::uint32_t value = bits.Value().ImmediateU32(); return (value & 0x7f800000u) == 0u ? IrU32(ir.Constant(value & 0x80000000u)) : bits; @@ -156,7 +156,7 @@ IrU32 TranslationContext::flushF32Denormal(IrU32 bits) { } IrF32 TranslationContext::flushTinyProduct(IrValue* lhs, IrValue* rhs, IrValue* product, IrValue* addend, bool afterRounding) { - if ((f32DenormalFlush & 2u) == 0u || keepsProductOutOfTinyRange(*lhs) || keepsProductOutOfTinyRange(*rhs)) return IrF32(*product); + if ((f32DenormalFlush & 2u) == 0u || ((f32DenormalFlush & 1u) != 0u && (keepsProductOutOfTinyRange(*lhs) || keepsProductOutOfTinyRange(*rhs)))) return IrF32(*product); IrValue& lhsBits = ir.BitCastU32(*lhs); IrValue& rhsBits = ir.BitCastU32(*rhs); const std::uint32_t roundMode = floatMode.has_value() ? floatMode->floatMode & 3u : 0u; @@ -249,7 +249,7 @@ void TranslationContext::writeOperand(const RdnaOperand& operand, IrValue* value } IrType type = value->Type(); if (type == IrType::F32) { - if ((f32DenormalFlush & 2u) != 0u) value = &ir.BitCastF32(flushF32Denormal(IrU32(ir.BitCastU32(*value))).Value()); + if ((f32DenormalFlush & 2u) != 0u) value = &ir.BitCastF32(flushF32Denormal(IrU32(ir.BitCastU32(*value)), 2u).Value()); value = &applyF32ResultModifiers(operand, IrF32(*value)).Value(); type = IrType::F32; } diff --git a/core/shader/recompiler/Translation/src/VectorInstructions.cpp b/core/shader/recompiler/Translation/src/VectorInstructions.cpp index c02cdba51e..31395cd973 100644 --- a/core/shader/recompiler/Translation/src/VectorInstructions.cpp +++ b/core/shader/recompiler/Translation/src/VectorInstructions.cpp @@ -49,6 +49,17 @@ std::uint32_t TranslationContext::f32DenormalFlushFor(const RdnaInstruction& ins case RdnaOpcode::VSubrevF32: case RdnaOpcode::VMulF32: case RdnaOpcode::VMulLegacyF32: + if (floatMode.has_value() && (floatMode->floatMode & 3u) == 0u) { + const std::uint32_t denormals = (floatMode->floatMode >> 4u) & 3u; + if (denormals == 1u || denormals == 2u) { + if (!float32DenormPreserve || float32FlushToZero) { + throw std::runtime_error("f32 denormal mode " + std::to_string(denormals) + " at pc " + std::to_string(inst.programCounter) + " needs DenormPreserve 32, which the device does not offer for this shader"); + } + return denormals == 1u ? 2u : 1u; + } + } + flush = 3u; + break; case RdnaOpcode::VMullitF32: case RdnaOpcode::VMinF32: case RdnaOpcode::VMaxF32: @@ -119,6 +130,12 @@ std::uint32_t TranslationContext::f32DenormalFlushFor(const RdnaInstruction& ins } const std::uint32_t denormals = floatMode.has_value() ? (floatMode->floatMode >> 4u) & 3u : 0u; if (denormals == 1u || denormals == 2u) { + if (inst.op == RdnaOpcode::VMullitF32 && (denormals == 2u || (floatMode->floatMode & 3u) == 0u)) { + if (!float32DenormPreserve || float32FlushToZero) { + throw std::runtime_error("f32 denormal mode " + std::to_string(denormals) + " at pc " + std::to_string(inst.programCounter) + " needs DenormPreserve 32, which the device does not offer for this shader"); + } + return denormals == 1u ? 2u : 1u; + } throw std::runtime_error("f32 denormal mode " + std::to_string(denormals) + " at pc " + std::to_string(inst.programCounter) + " is not implemented"); } if (denormals != 0u) return 0u; diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 144342fceb..4428231cad 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -91,6 +91,7 @@ - [Collapsed color array views](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) preserve the selected slice in the XOR address equation when the view is represented as a shifted 2D surface. Native Vulkan coverage checks R_X draws, array reads, publication and re-upload for slices 1-3; Lost Soul Aside PPSA26358 validates the RGBA16F cube mip chain on Windows/NVIDIA. Other devices and console comparisons remain unverified. Full-array views retain their layer addressing. - A [depth surface](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) is retired when a color target overlaps it, an unserved storage view writes a whole plane, or it is sampled through a view it cannot serve and guest memory over it was written since it was last bound; lookups then skip it, so sampling reads the memory like any texture. Bound again, it keeps its own depth instead of the bytes written over it, except for the depth planes the next entry keeps in guest memory. Without write watching, a view it cannot serve still throws. A retired surface also gives up its depth and stencil planes, so another surface sharing them no longer copies from it. - [Depth planes in guest memory](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): only single-sample `D32_SFLOAT` and `D32_SFLOAT_S8_UINT` planes with the 64 KiB Z swizzle (`SW_MODE` 24) reach guest memory; D16, other swizzles, stencil and multisampled planes keep depth in the host image only. A plane drawn since its last store is stored, uncompressed, in 64 KiB units (128x128 tiles): a buffer upload, a `GuestBuffers` V#, an image lookup or a pending-store flush stores the units it overlaps, a color target or written storage image over the plane all of them. CPU reads and reads through address-based heaps are not caught. Guest writes are ordered when collected (bind, flush, collect epoch), not when made: a collected write reloads the units it touched unless an HTILE clear is pending. A store outside a recorder waits for the GPU. The console keeps HTILE-compressed tiles compressed in memory until a decompress (Mesa `ac_descriptors.h`). +- [Unmapped depth planes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a drawn depth plane whose guest memory the title unmapped before it was stored is not stored; its pending depth is dropped with a one-time warning. What the console does with such a store is not known. Readable planes without write permission retain the unsupported-store failure and their pending results. - [Depth plane copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth plane sampled as a 2D array or a cube, or a D16 plane read as `R16_UINT`, is copied into an image of its own (`DepthPlaneCopy`), kept per base address, extent, format and slice count; a slice comes from the live depth surface when one is bound at the slice's address and from guest memory (detiled) otherwise. A draw or dispatch that binds the plane copies again only the slices that changed and records nothing when none did: a surface slice when its image or write serial differs from the one copied (the serial is bumped when the surface is created, when a draw binds it as its depth target, when a pending fast clear is applied and when a written storage view of the plane is copied back), a guest-memory slice when its bytes were written since they were last read (`GuestMemory::UnchangedSince`), a snapshot write overlaps them, or its address, size or tile mode changed. The surface of the latest depth draw is copied at every use until another depth target is bound, since that draw may still be recorded after the copy. A slice's address is the base plus a whole 64 KiB-block depth slice per layer (`DepthSliceBytes`), and the cube face order is the array order; both are inferred from the titles' use, not measured on hardware. A 2D-array T# whose `BASE_ARRAY` is past slice 0 reads slices `BASE_ARRAY` to `LAST_ARRAY` of the copy, which holds slices 0 to `LAST_ARRAY`, and a 2D binding of it reads slice `BASE_ARRAY`; that follows the colour 2D-array semantics and the titles that hit it (PPSA01556, PPSA03912), and was not measured on hardware for depth surfaces. Planes with mips, a cube or a 2D T# with a base slice other than 0, a `BASE_ARRAY` past `LAST_ARRAY`, a cube with other than 1 or 6 slices, and other views of the planes still throw, and so do these copies of a 16-bit plane with stencil that the device stores as D24 or D32 (16-bit depth with stencil, below). - [Repacked format-43 copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver): a sampled texture of format 43 (10-bit R, 11-bit G and B unsigned floats from bit 0) is uploaded into a `B10G11R11_UFLOAT` image with its R and B fields swapped (`RepackTextureWord`) and viewed with R and B swapped back. A texture whose results a storage image still holds is copied from it again, whole, whenever the image changed (`RefreshDepthCopy`); a 3D one is flushed to guest memory and uploaded instead. Storage reads and writes of format 43 keep the packed `R32_UINT` path. - [SW_64KB_Z_X color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp) (libSceAgcDriver) use the texture equations and 64 KiB block geometry of `SW_64KB_Z_X`, whose addresses are checked against [AMD addrlib GFX10](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp) in the texture tiling tests. Any 256-byte pipe/bank XOR below the block size is accepted, as for `SW_64KB_R_X` targets and Z_X textures; which of those bits the console honours, and the render results, are not measured. @@ -101,6 +102,7 @@ - [8_8 UINT color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) decode as `R8G8_UINT` with the same component-swap mapping as 8_8 UNORM/SNORM (`0xe4` / `0xec`). Follows the existing 8 and 8_8_8_8 UINT paths; not measured on the console. RetroArch (PPSA99169) uses it during menu startup. - [Quad-list primitives](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver): primitive type 19 (DI_PT_QUADLIST, 0x13) translates to Vulkan triangle lists by generating 6 indices per 4-vertex quad in a synthetic 32-bit index buffer during draw input preparation (triangles (0, 1, 2) and (0, 2, 3) for first provoking vertex; (0, 1, 3) and (1, 2, 3) for last provoking vertex). Incomplete trailing vertices (< 4) are discarded. Indirect draws, primitive restart and draw recipes are unsupported for quad-list primitives. Console behavior for incomplete quads has not been measured on hardware. - [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. +- [Color logic operations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver): `CB_COLOR_CONTROL.ROP3` accepts the 28 codes of the source and destination form (`pqrspqrs`) or the brush and destination form (`pqpqrsrs`, the brush is the shader color), as the R6xx 3D register reference describes the field and Mesa's `gfx10.json` `ROP3` enum lists. A code that combines source and brush, which the console emulates in software, throws. - [Unexported pixel inputs](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver): a pixel input whose `SPI_PS_INPUT_CNTL` parameter the previous stage does not export is programmed as a default-value input (`OFFSET` bit 5, `DEFAULT_VAL` 0, FP16 high half too) and reads (0, 0, 0, 0), as radeonsi's `SI_PS_INPUT_CNTL_UNUSED` does and as rect-list draws already do. The console's value for an unwritten parameter cache entry is not measured on hardware. - [Per-sample pixel shading](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver): `PA_SC_MODE_CNTL_1.PS_ITER_SAMPLE` (bit 16, [PS5_Mesa GFX10.3 registers](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/registers/gfx103.json)) on a multisampled draw enables Vulkan sample shading with a minimum rate of 1.0, so the pixel program runs once per sample. The draw throws when the device lacks `sampleRateShading`. On a single-sample draw the bit has no effect and is dropped. That the console runs the pixel program once per covered sample, and what it does with the other multisample registers in that mode, is taken from the register name and Vulkan's definition, not measured on hardware. Without the bit the pixel program runs once per pixel: the recompile request carries the mode (`iteratesSamples`), the ancillary's sample number reads 0 instead of `SampleId`, whose use would make Vulkan shade per sample, and the sample barycentrics and positions are evaluated at sample 0, whose locations the request carries (`sampleLocations`, every sample's in an iterated draw). `DB_EQAA.PS_ITER_SAMPLES`, the number of samples an iterated shader covers, is not read: an iterated draw runs once per sample. - [Unused pixel programs](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver): a bound pixel program is skipped when color masks have no intersection and DB export and execution enables are clear, regardless of retained SPI_SHADER_Z_FORMAT. This follows [AMD PAL DbShaderControl](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9AbiToPipelineRegisters.h) and its [GFX10 register definitions](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/chip/gfx9_plus_merged_registers.h), not a hardware measurement. Enabled DB_ALPHA_TO_MASK conservatively retains the program even with ALPHA_TO_MASK_DISABLE, because DecodeState currently enables Vulkan alpha-to-coverage without applying that override. Console behavior for these combinations has not been measured. @@ -164,7 +166,7 @@ - [HTILE depth fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - HTILE is not modeled. Only RADV's `clear_htile_mask` compute kernel (17 GFX10 words, 64x1x1 groups) is recognized, and only when it leaves ZMASK at 0 (`user[4]` and `user[5]` low nibbles both 0); the dispatch still runs, and the host depth image is cleared to `DB_DEPTH_CLEAR` the next time the surface with that `DB_HTILE_DATA_BASE` address is bound. A clear hits a surface only when its range covers that surface's whole HTILE, which need not start where the clear does. Fast clears that other writers leave in HTILE are taken at bind as the next entry describes. The kernel words are the ones the PR author saw RADV submit; they are not compared with Mesa's source. A second kernel, the 9-word dword fill PPSA01802 (Kena: Bridge of Spirits) clears its depth HTILE with (64x1x1 groups, one `buffer_store_format_x` per thread through the stride-4 V# in `user[0..3]`, the value read by `s_buffer_load_dword` through the V# in `user[4..7]`), is taken as a fill of that range when the store format is 32_UINT, 32_SINT or 32_FLOAT and the value V# covers the dword (NUM_RECORDS times its stride, or NUM_RECORDS bytes when the stride is 0) ([`MatchMemoryValueFill`](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp), then the whole-HTILE and ZMask rules of `NoteDepthMetadataFill`). The value is read from guest memory when the dispatch is decoded, so a value that earlier GPU work in the same submission has not written back yet is not seen. When the [fill HLE](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp) takes the dispatch instead (an output descriptor of one 32-bit value per element, a value descriptor that covers the loaded dword, a 16-byte aligned range), the value is read again when the fill is recorded, after the labels queued before it, and the HTILE fill is noted with that value. - [HTILE written by other kernels](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - when a depth target is bound and its HTILE was written since it was last read, the words over its whole pipe-aligned metadata (`HtileMetadataBytes`, the extent `HtileFillCovers` takes) are read, outside the surfaces mutex since the read waits for recorded work: when every one marks the depth (ZMask 0) or the stencil (SMem 0) cleared, the surface takes that fast clear and the words are stored back as expanded (depth only: ZMask 15 over the full Z range; with stencil: ZMask 15, Z range and SMem, SR1 and SR0 3), so only the title's next clear clears it again. PPSA29141 clears its depth every frame with a compute kernel the fill hooks do not recognize (`InvokeHTileClearComputeShader` in its `ClearMRT` pass); without this its depth kept every earlier frame and moving objects left black trails. PPSA25344 (Unreal Engine 5) reads its HTILE after the depth prepass in its Nanite depth export, which takes a tile with ZMask 0 as holding the clear value; without the expanded words the export put Nanite depth over the prepass and the Nanite material pass painted over the meshes in front. The word layouts are Mesa radv's for GFX10. Metadata with some tiles cleared is not modelled, and the compression state draws leave in HTILE is not tracked, so a title reading its HTILE after a draw sees the stored expanded words. A recognized HTILE fill or clear makes the next bind read the words again. A slice other than 0 of a depth array is read at its own HTILE (`DB_HTILE_DATA_BASE` plus the slice times `HtileMetadataBytes`) but the HTILE word layout of slices 1 and up is not modelled (PPSA05684's tile masks need X8 and Y8 to take Z0 and Z1 as well, slices 4 and up unmeasured), which neither the check that every word is cleared nor the store-back of each word in place needs, so such a slice takes the clear like slice 0. A depth-only HTILE whose tiles all have ZMask 0 but different Min/Max Z (per-tile clear values, PPSA05684's cascades) is taken as a whole-surface clear to `DB_DEPTH_CLEAR`, the value the DB's depth test reads for a cleared tile (`HTILE_Z_CLEAR_REG` in Mesa's `src/amd/common/ac_descriptors.h`: the clear word "only clears HiZ", "the DB register contain[s] the full clear values"); the HiZ test before it, which rejects against each tile's own Min/Max Z range (the HTILE definitions in the same file), is not modelled, so a draw that HiZ would reject over a tile cleared with Min/Max Z 0 is tested against `DB_DEPTH_CLEAR` instead. A multisampled depth target, and any target whose HTILE overlaps a resident native depth guard, skips this read and store-back and goes to the native multisample depth owner, which keeps its own refusal for a fast-clear transition. - [RADV fill-memory kernel](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp) (libSceAgcDriver) - the [fill HLE](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp) takes RADV's 16-byte `fill_memory` compute kernel (`MatchClampedGlobalFill`: 11 GFX10.3 words, 64x1x1 groups, six user SGPRs with the address in `user[2..3]`, the largest store offset in `user[4]` and the value in `user[5]`; each thread stores 16 bytes at `min(thread * 16, user[4])`) as a fill of `min(threads * 16, user[4] + 16)` bytes. RADV initializes and fast-clears DCC keys and other metadata with it, so the key and metadata tracking sees such a fill where it is in the command stream; run as a dispatch, its global stores name no range, and a key fill was only seen once it had landed, after later writes to the surface. Only a 16-byte aligned address and largest offset are taken. RADV's 4-byte variant (fills below 16 bytes), other Mesa versions' encodings and other compilers still run as dispatches. The kernel words are the ones vkQuake's PS5 RADV build (Mesa 25.2) submitted; they are not compared with the NIR builder's output for other targets. -- [Host imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) on Linux skip ranges that are not mapped read-write, which are copied instead. The NVIDIA driver (595.91.07) refuses `VK_EXT_external_memory_host` imports of read-only or executable mappings (and shared mappings of regular files) with `VK_ERROR_OUT_OF_DEVICE_MEMORY`, after which every `vkAllocateMemory` on the device fails the same way. Read-only ranges are never imported, even on drivers that accept them. Shared mappings of regular files are not checked for. On Windows, an alias of adjacent registered ranges is refused if any page is not a shared view; a range made entirely of shared views is aliased run by run, including across sections or nonconsecutive section offsets. Adjacent registered ranges are joined into one import: the largest aligned power-of-two block, from twice the import alignment up to the 8 MiB window, that holds the requested address and that they fill exactly, so a block only grows by doubling as its window fills. A block that would exceed the budget, would replace an import a build holds, or fails (a refused Windows alias, `vkAllocateMemory`) steps down to the next smaller block; a failed block is not tried again until one of its ranges changes. Ranges larger than the window are imported one at a time. A buffer bound across adjacent ranges that are each larger than the window, or bound exactly over adjacent ranges of any size (from the first one's start to the last one's end), joins them into one import when it is bound, and their own imports are retired; a buffer that crosses any other two imports is copied. A heap grown in pieces re-imports its joined run only when a buffer is bound exactly over all of its pieces, or when a sparse descriptor heap over it is bound: a heap a shader binds whole while only the pieces from its base are mapped is bound in place from the import of that run, its views ending at the import's end as the copy's do, once the previous bind of that address saw the same last mapped byte (a heap that grows between every two of its binds stays copied; one bound more often than it grows is joined again after each growth) and at most 64 times per joined run base; past that, with a message, it is not joined again: it is bound in place while an existing import covers its run and copied otherwise, until no registered range starts at that base or the device changes. Ranges larger than the window are taken to be registered whole and never grown in pieces: a title that grows a heap in pieces larger than 8 MiB re-imports the joined run each time a bound buffer crosses into a new piece. The 8 MiB window is a choice: unbounded runs were re-imported whole on every growth of a heap mapped in small pieces, and the NVIDIA driver (610.57.04, 615.78.08) then refused imports or lost the device. Not measured on RADV. +- [Host imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) on Linux skip ranges that are not mapped read-write, which are copied instead. The NVIDIA driver (595.91.07) refuses `VK_EXT_external_memory_host` imports of read-only or executable mappings (and shared mappings of regular files) with `VK_ERROR_OUT_OF_DEVICE_MEMORY`, after which every `vkAllocateMemory` on the device fails the same way. Read-only ranges are never imported, even on drivers that accept them. Shared mappings of regular files are not checked for. On Windows, an alias of adjacent registered ranges is refused if any page is not a shared view; a range made entirely of shared views is aliased run by run, including across sections or nonconsecutive section offsets. Adjacent registered ranges are joined into one import: the largest aligned power-of-two block, from twice the import alignment up to the 8 MiB window, that holds the requested address and that they fill exactly, so a block only grows by doubling as its window fills. A block that would exceed the budget, would replace an import a build holds, or fails (a refused Windows alias, `vkAllocateMemory`) steps down to the next smaller block; a failed block is not tried again until one of its ranges changes. Ranges larger than the window are imported one at a time. A buffer bound across adjacent ranges that are each larger than the window, or bound over adjacent ranges of any size from the first one's start (ending anywhere in the last range it crosses), joins the ranges it crosses into one import when it is bound, and their own imports are retired; a buffer that crosses any other two imports is copied. A heap grown in pieces re-imports its joined run only when a buffer bound from its first piece's start crosses into a piece the joined import does not cover, or when a sparse descriptor heap over it is bound: a heap a shader binds whole while only the pieces from its base are mapped is bound in place from the import of that run, its views ending at the import's end as the copy's do, once the previous bind of that address saw the same last mapped byte (a heap that grows between every two of its binds stays copied; one bound more often than it grows is joined again after each growth) and at most 64 times per joined run base; past that, with a message, it is not joined again: it is bound in place while an existing import covers its run and copied otherwise, until no registered range starts at that base or the device changes. Ranges larger than the window are taken to be registered whole and never grown in pieces: a title that grows a heap in pieces larger than 8 MiB re-imports the joined run each time a bound buffer crosses into a new piece. The 8 MiB window is a choice: unbounded runs were re-imported whole on every growth of a heap mapped in small pieces, and the NVIDIA driver (610.57.04, 615.78.08) then refused imports or lost the device. Not measured on RADV. - [Cube texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) with equal `BASE_ARRAY` and `DEPTH` expose six faces starting at `BASE_ARRAY`. Measured with `image_load`, `image_sample` and `image_get_resinfo` on RX 6600 (gfx1032), using 64x64 R32_UINT faces and base slices 0, 5 and 6. Other ranges containing fewer than six faces still throw: hardware load bounds and filtered-sample addressing differ. A range of six faces or more that does not end on a whole cube keeps every slice up to `DEPTH`, the bound loads use on hardware; sampling the cube it cuts short is out-of-range cube-array sampling. Out-of-range cube-array sampling remains unimplemented. Samplers with `DISABLE_CUBE_WRAP` are created with `VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT`, the flag RADV maps to that bit (Mesa `src/amd/vulkan/radv_sampler.c`, `src/amd/common/ac_descriptors.c`); without `VK_EXT_non_seamless_cube_map` a cube texture sampled through such a sampler throws. PS5 equivalence has not been measured on console hardware. - [Array layer views](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (libSceAgcDriver) - `ArrayLayersServeView` lets a 2D texture at a 2D array image's base address, or a 2D array texture over its first layers, view that image (`Texture::CanCopyFrom`). Mesa-built homebrew (ps5-opengl) writes the view's last slice plus one to `CB_COLOR_ATTRIB3.MIP0_DEPTH`, so a slice-0 render target is a two-layer array image, and the title samples it as a plain 2D texture (SuperTuxKart PPSA69421 drew a black 3D scene and minimap without this). The per-layer layout of a mipmapped array is assumed equal to a standalone surface's; it is not measured on the console. A 2D texture at a later slice's address still gets an image of its own. - [Multisampled CMASK](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a multisampled color target's CMASK is decoded under CB_COLOR_INFO FAST_CLEAR (bit 13, as for a single-sample target) or COMPRESSION (bit 14). Which bit a multisampled target sets is inferred: its fast clears were seen on GTA V's multisampled targets through their CMASK fills, not in a register dump. A COMPRESSION-only target's zero CMASK is not read as cleared (inferred: zero means cleared only under FAST_CLEAR), so only a fill after its first use clears it. A slice of a multisampled array is its own image at its own address with its own CB_COLOR0_CMASK (inferred from the register layout). Its CMASK is main's whole-metablock size. diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index b5780303ec..90276be202 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -10,7 +10,8 @@ - [sceNetConfigAddIfaddr](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - disassembly confirms the same interface/address/broadcast/netmask arguments as SetIfaddr, entering its shared handler with add mode selected. GetIfaddr copies up to two 16-byte interface-address records. GetRouteInfo uses a family, record buffer, signed capacity and byte flags; internal statistics output is 16 bytes. The address-record and internal-statistics fields remain incomplete. -- [Resolver-connect, thread creation, interface and diagnostic queries, WLAN configuration and DHCP queries](../../../core/libs/prx/libSceNet/Export.cpp) retain unsupported implementations. Their restored argument lists still reference incomplete connection, thread, interface, route, WLAN and statistics types. Kernel ioctl payloads need reconstruction from their handlers or consumers; buffer lengths alone do not establish field types. +- [Resolver-connect, interface and diagnostic queries, WLAN configuration and DHCP queries](../../../core/libs/prx/libSceNet/Export.cpp) retain unsupported implementations. Their restored argument lists still reference incomplete connection, interface, route, WLAN and statistics types. Kernel ioctl payloads need reconstruction from their handlers or consumers; buffer lengths alone do not establish field types. +- [sceNetThreadCreate, sceNetThreadExit, sceNetThreadJoin, sceNetEmulationGet and sceNetEmulationSet](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the signatures, the `{ entry, arg, reserved, flags }` parameter block and the `EOPNOTSUPP` of the two emulation calls ("not available on retail units") follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/net/net.cpp) (PS4), not a PS5 measurement. The block is read on the new thread, so it must stay valid until the thread starts, `flags` is ignored (shadPS4 ends the thread with `pthread_exit` when bit 0 is set, which ends it the same way), and a failing `scePthreadCreate` or `scePthreadJoin` result is returned as it is, where shadPS4 also sets the net errno from it - [sceHttp2GetMemoryPoolStats, sceSslGetMemoryPoolStats, sceNetGetMemoryPoolStats](../../../core/libs/prx/libSceNet/Export.cpp) (libSceHttp2, libSceSsl, libSceNet) - the signatures `(int id, stats*)`, the layouts (pool size, peak and current use, and for Http2 and Ssl a 32-bit reserved field) and the error codes were measured on a PS5 (firmware 13.60). The pool size is the one given at init or pool creation. Http2 and Net allocate nothing from these pools. Ssl counts its certificate backend allocations and returned data against its pool; C++ bookkeeping is outside that budget. All three initially report zero use, where the console already reports 0x15C0 bytes for Ssl, 0x1640 for Http2 and 4 for a Net pool. Ssl's allocation sizes and peak usage do not reproduce the console allocator - [sceNetClearDnsCache](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the library has no DNS cache to clear because lookups use the host resolver. The host resolver cache is left unchanged. - [sceNetEtherStrton](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - strings shorter than 17 bytes return EINVAL without changing the output, rather than reading past the terminating NUL as in the [firmware 13.60 probe](https://gist.github.com/orezraey/789290577123af3c45b340d6db6f395c/71e0a6215c7ee86f49fdb31de989dd2b50720b7a). Non-ASCII digit bytes throw because their decoding is unmeasured; other firmware versions remain unverified. Fixed character pairs, ignored separators, alphabetic values beyond f, non-alphanumeric ASCII values of 0x7f, and null-pointer EINVAL follow those measurements, not the strict [shadPS4 parser](https://github.com/shadps4-emu/shadPS4/blob/d615b3887ba29025a59242e1a26f5b68bb176158/src/core/libraries/net/net.cpp#L1274-L1297) @@ -18,7 +19,6 @@ - [sceNetGetRandom](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - generates values with the host random device; the system library maintains its own seeded PRNG state. - [sceNetInetPtonEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - for IPv6 a null `dst` returns `NET_EINVAL` here, where it crashed a payload process on a PS5 (firmware 13.60). The `(int af, const char* src, void* dst, int flags)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp). On PS4 firmware 10.71 and on that PS5, IPv4 text is read as `inet_aton` reads it (`a`, `a.b`, `a.b.c`, hex and octal parts, values wrapping at 32 bits, the text ending at a space, tab or VT), except that a part starting with `09` is decimal (`"0989"` is 989) and an octal part with `8` or `9` fails; flags were ignored (PS4: 0, single bits up to 0x100, 0x10000, 0x80000000, -1; PS5: 1, 2, 4, 0x100, 0x10000, -1). On the PS5 only: LF, FF and CR also end the text, a null `src` returns `NET_EINVAL`, and a null `dst` returns 1 or 0 without writing or setting errno. IPv6 converts as `sceNetInetPton` does - [sceNetMemoryAllocate and sceNetMemoryFree](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - allocations use the guest heap instead of the system network pool. Pool limits, its allocation accounting and free-time fill are not reproduced: a reused block allocated without flag 2 holds 0xdd bytes on the console and its previous contents here. Flag 4 bypasses the system pool reserve threshold; with no pool there is no threshold, so it allocates as other flags do. What the console returns once the pool is exhausted is unmeasured; AnyPS5 returns null without setting an errno. Measured on a PS5 (firmware 13.60): a size of 0 or less returns null and leaves `sceNetErrnoLoc` unchanged, flag 2 zero-fills, flags 1, 4, 8, 0x10, 0x100 and 0x80000000 had no visible effect, and every block of 1 to 65536 bytes was 64-byte aligned -- [sceNetThreadExit](../../../core/libs/prx/libSceNet/Export.cpp) is a zero-argument, non-returning void export in the native module. The protected host stub still declares int and contains `return 0`; changing it to void requires changing that protected body, so the return-type mismatch is retained. - [sceNetConfigCleanUpAllInterfaces](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the native routine consumes no arguments; the throwing implementation remains unchanged. - [sceNetDhcpdStart](../../../core/libs/prx/libSceNet/Export.cpp): six arguments and the three-argument address callback are confirmed. The 24-byte parameter contains five network-order addresses and a client limit. Individual address roles and the two unused thread-option meanings remain unresolved. - [sceNetPppoeStart](../../../core/libs/prx/libSceNet/Export.cpp): seven arguments are confirmed, including the parameter pointer on the stack. The 272-byte parameter contains two DNS addresses, two 128-byte credential arrays, MTU and flags. Callback arguments are user data, event and error; the two unused thread-option meanings remain unresolved. @@ -42,6 +42,7 @@ - [sceNetGetSockInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reports 0 for the name, pid, policy, priority, queue lengths, virtual ports, state, bandwidth, drops and waits, sets no status flag except `SELF` (0x1) and `NONBLOCK` (0x200000), and reports the host's buffer sizes where none was set. An IPv6 socket fails with `EAFNOSUPPORT` and is not counted or listed; shadPS4 lists it with zero addresses. `flags` bits other than 0x40 fail with `EINVAL`, where shadPS4 ignores `flags`; 0x40 adds sockets in TIME_WAIT ([SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs)), but closed sockets are not kept. Otherwise the `s` below 0 count and list, the return value and `EINVAL` for a null `info` or an `n` below 1 follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/06e813ff8c8e2a6d81b02cabfcc9543c3debf78e/src/core/libraries/net/net.cpp#L1498-L1522). Not measured on a console. - [SO_SNDBUF and SO_RCVBUF validation](../../../core/libs/prx/libSceNet/Export.cpp) remains unverified on PS5, and positive sizes remain subject to host buffer limits. Nonpositive sizes fail with `EINVAL` before changing the native socket, following FreeBSD 11.4 [sosetopt](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/kern/uipc_socket.c#L2494-L2509). - [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 +- [sceNetGetSockInfo6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) is not measured on a console; shadPS4 stubs it. It lists only IPv6 sockets and `flags` 0x1000 adds IPv4 sockets, reported with the IPv4 addresses, all-zero IPv6 addresses and no `F_IPV6` (0x400000), which every IPv6 socket sets ([SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs#L251-L343), [flags](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs#L698-L705), [docs](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/docs/net.md#L301-L304)). The default of leaving IPv4 sockets out, the zero IPv6 addresses of an IPv4 socket and accepting 0x40 are inferred from those names, not documented. An IPv4 socket without 0x1000 fails with `EAFNOSUPPORT`. The other gaps are those of `sceNetGetSockInfo`. - [sceNetSocketClose, sceNetEpollControl and sceNetEpollWait](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - close and concurrent registration ordering are unmeasured on a console. Closed descriptors retire their registrations, and a wait discards snapshots invalidated by close, DEL or MOD; descriptor identity follows [Linux epoll(7)](https://man7.org/linux/man-pages/man7/epoll.7.html). ADD, MOD and DEL reject closed or invalid socket descriptors with `EBADF`, as [Linux epoll_ctl(2)](https://man7.org/linux/man-pages/man2/epoll_ctl.2.html) does; this error ordering is unmeasured on a console. Duplicate-descriptor alias lifetime is not modeled by this epoll backend. - [MSG_DONTWAIT](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - console ordering between competing readers and writers is unmeasured. The per-call behavior follows FreeBSD 11.4's `soreceive_generic` and `sosend_generic` (`sys/kern/uipc_socket.c`); `0x80` is FreeBSD's flag value (`sys/sys/socket.h`). On Windows native sockets stay non-blocking. `MSG_DONTWAIT` and guest `SO_NBIO` return one host transfer's result. Blocking stream sends queue the whole buffer with one deadline and return progress after a timeout or host error. SSL uses this path through the native owner, retaining the socket, its mode and timeouts. Readiness waits poll at most every 10 ms for local close, so idle blocked operations wake periodically. - [sceNetRecv, sceNetRecvfrom and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) on a datagram socket with no local address wait on Windows and macOS until a bind, connect or send gives it one, checking the host socket every 10 ms. Winsock fails such a receive with `WSAEINVAL`, where FreeBSD 11.4's `soreceive_dgram` waits for a datagram (`sys/kern/uipc_socket.c`). On macOS the host receive waits, but a receive-side `shutdown` does not wake it. A non-blocking socket fails with `EAGAIN` at once, and the receive timeout ends the wait with `EAGAIN`; when the address comes first, the call waits for a datagram only for what is left of the timeout. A `sceNetShutdown` of the receive side ends the wait and the receive returns 0, and a zero-length receive returns 0 without waiting, as `soreceive_dgram` does when the socket can't receive more or nothing was asked for. An abort or close ends the wait, and the host receive then fails as it did before diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md index e0704a1cc7..add05f78df 100644 --- a/docs/dev/debt/libSceNetCtl.md +++ b/docs/dev/debt/libSceNetCtl.md @@ -12,6 +12,7 @@ - [sceNetCtlGetInfoV6](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl, NID `Jy1EO5GdlcM`) - imported by PPSA08587. `(int code, SceNetCtlInfoV6* info)` with codes 1-5 (IP address, link-local address, default route, primary and secondary DNS) comes only from [SharpProspero](https://github.com/SvenGDK/SharpProspero), whose source is unknown; shadPS4 and fpPS4 only stub it. A null `info` answers `INVALID_ADDR`, a code outside 1-5 `INVALID_CODE`, and a host without an IPv6 address `NOT_CONNECTED`, as `sceNetCtlGetStateV6` reports it. The layout of each answer is unverified, so every code throws on a host with an IPv6 address - [sceNetCtlGetResult](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - `(int eventType, int* errorCode)` reports no error: event types 1-3 (disconnected to IP obtained, the numbering `sceNetCtlCheckCallback` delivers) write 0 to `errorCode`, as `sceNetCtlGetResultV6` does. The error code a console reports for each event, a disconnection's cause in particular, is unconfirmed. Callers found: PPSA20052 (Worms Armageddon: Anniversary Edition 01.003.000) calls it 2 s into boot, and PPSA24191 (Monster Hunter Wilds) without a network, with event 1 (disconnected) as its first call. A null `errorCode` answers `INVALID_ADDR` and other event types throw, as their error code on a console is unknown - [sceNetCtlGetResultV6](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl, NID `H5yARg37U5g`) - `(int eventType, int* errorCode)` comes only from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/NetCtl.cs#L246-L251), whose source is unknown; shadPS4 only stubs it. Callbacks registered with `sceNetCtlRegisterCallbackV6` receive no event, so event types 1-3 write 0 to `errorCode`; what a console writes before the first event is unmeasured. A null `errorCode` answers `INVALID_ADDR`, as `sceNetCtlGetResult` does, and other event types throw, as their error code on a console is unknown +- [sceNetCtlRegisterCallbackForNpToolkit, sceNetCtlUnregisterCallbackForNpToolkit, sceNetCtlCheckCallbackForNpToolkit](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - a table of eight callbacks apart from the IPv4 and IPv6 ones, with the `(int eventType, void* arg)` callback, follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/net_ctl_obj.h), whose NpToolkit check delivers the same events to that table. The three share the register, unregister and dispatch of `sceNetCtlRegisterCallback`, `sceNetCtlUnregisterCallback` and `sceNetCtlCheckCallback`, so they return the same initialization, address, slot-limit and ID errors, and the check reports the state `sceNetCtlCheckCallback` does. Whether the console keeps this table apart, and what `sceNetCtlCheckCallback` does with it, is unverified on hardware - [ApRp exports](../../../core/libs/prx/libSceNetCtl/Export.cpp) are constant-success functions in the supplied native module and consume no arguments. No callers were found in the supplied system-library decompilations, so unused callback, output and configuration argument types remain unresolved. Protected stub bodies are unchanged. ### Functional diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index 4f8fa71b71..df2e0c6004 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -11,7 +11,9 @@ - [libScePad report and tracking exports](../../../core/libs/prx/libScePad/Export.cpp) - read, extended-read, tracker, UI, VR-controller and report argument registers are recovered. Feature reports use a byte report ID and 64-bit byte count, capped at 64. VR trigger effects consume a 56-byte record; the dual-controller call uses two handles and a mask with records at offsets 8 and 64. Remaining payload field layouts use incomplete declarations, including extended samples and kernel-owned extension reports. - [scePadUnknown_n3kSX62fgNo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `n3kSX62fgNo`) - the name is unknown, and the 0 returned after `scePadInit` is inferred, not measured. In the OS 10.01 system module the function takes no arguments, returns `0x80920005` before `scePadInit`, and otherwise sends a per-process value of 1 to ShellCore over Mbus, returning `0x809200ff` if that call fails and 0 if it succeeds, with or without an open handle. The #3177 probe measured `0x809200ff` in a process that is not a game; a game process is assumed to get 0. The ShellCore setting it changes is unknown and not modelled -- [scePadSetAngularVelocityDeadbandState](../../../core/libs/prx/libScePad/src/PadState.cpp) (libScePad) - with the deadband on, each angular velocity axis whose magnitude is below 0.017453292 rad/s (1 degree per second) reads as 0, before the orientation is integrated; it starts off. Threshold, per-axis test and default follow [duaLib](https://github.com/WujekFoliarz/duaLib/blob/03bad1bea2a36561b520846776f2a07ced6773e0/src/source/duaLib.cpp#L44), which reimplements the PC `libScePad.dll`; the console's threshold is not measured, and the deadband is kept per library rather than per handle since one pad handle exists +- [scePadSetAngularVelocityDeadbandState](../../../core/libs/prx/libScePad/src/PadState.cpp) (libScePad) - with the deadband on, each angular velocity axis whose magnitude is below 0.017453292 rad/s (1 degree per second) reads as 0, before the orientation is integrated; it starts off. Threshold, per-axis test and default follow [duaLib](https://github.com/WujekFoliarz/duaLib/blob/03bad1bea2a36561b520846776f2a07ced6773e0/src/source/duaLib.cpp#L44), which reimplements the PC `libScePad.dll`; the console's threshold is not measured, and the deadband is kept per port slot +- [PadQueueLegacyAudioHaptics, PadClearLegacyAudioHaptics](../../../core/libs/prx/libScePad/src/PadState.cpp) (libScePad) - PadQueueLegacyAudioHaptics and PadClearLegacyAudioHaptics route legacy audio haptics only to port 0; multi-port queueing is not modelled +- [scePadGetControllerInformation](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - scePadGetControllerInformation always reports connectedCount = 1 and connected = true; behaviour across multiple open controller handles is not measured on hardware - [scePadOpen and scePadOpenExt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the special port (type 2) opens a handle per user (`PAD_SPECIAL_HANDLE` + slot, 16 slots for the console's 16 accounts, a 17th user gets the unknown-user code 0x809b0081) that every pad call accepts: reads give the rest record (sticks 0x80, orientation w 1, acceleration y 1, `connected` false), information is zeroed, extended information has device class -1, setters succeed without effect. Not measured: the console's own handle values (fresh per open), a PS5, and the calls the PS4 Pro survey (firmware 10.71, no special controller) did not cover; it measured open, `ALREADY_OPENED`, `scePadGetHandle`, both reads, both informations, the five setters above, close, and that two logged-in users get independent handles. `scePadOpenExt` returns `DEVICE_NOT_CONNECTED` (0x80920007) for special ports, unmeasured; PPSA01317 skips a user on any negative result. `scePadOpenExt` throws for other port types - [scePadOpen, scePadGetHandle, scePadRead and scePadReadState for the remote control port](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - whether a console without a media remote opens the system user's remote control port (type 16), returns `DEVICE_NOT_CONNECTED` or fills other fields is not measured. The host has no remote, so the port opens its own handle (`PAD_REMOTE_HANDLE`) that reads one zeroed entry with no buttons and `connected` false; the other pad calls reject it with `INVALID_HANDLE`. SDL's PS5 video driver maps this port's buttons to keys, so a remote that read the DualSense turned Cross into Return (Raptor PPSA94002). - [scePadOpen](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - port types 3 and 4 throw: a payload process on a PS5 (firmware 13.60) let them past the type check, but it cannot open a port for its own logged-in user (0x809b0081), so what they open is unknown. The argument checks it did reach are followed: types 1, 5, 6, 255 and -1, and 16 for a user other than the system user, fail with `INVALID_ARG` before the user is looked at, user -1 with 0x809b0001 on types 0, 2, 3 and 4 and on index 1, and index 1 of a logged-in user with 0x809b0003; whether index 1 of types 3 and 4 fails the same way was not measured; the payload never tried type 16 with the system user (255), which opens the remote control port as below diff --git a/docs/dev/debt/libSceUserService.md b/docs/dev/debt/libSceUserService.md index cd811dc408..f1cb7b914e 100644 --- a/docs/dev/debt/libSceUserService.md +++ b/docs/dev/debt/libSceUserService.md @@ -4,8 +4,9 @@ - [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` - [sceUserServiceInitialize and sceUserServiceInitialize2](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - event delivery has no background service thread, so the confirmed priority and Initialize2 CPU affinity mask do not affect host scheduling. -- [sceUserServiceGetUserColor](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the default profile colour is assumed to be blue (0) -- [sceUserServiceGetUserName](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - `0x80960105`, which a PS5 (firmware 13.60) returns for a user id it does not know, has no known name; it is `USER_SERVICE_ERROR_UNKNOWN_USER` here. On the console a registered user who is not logged in still gets their name; here only the initial user exists +- [sceUserServiceGetUserColor](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the profile colour corresponds to user index (0: blue, 1: red, 2: green, 3: pink); colours 1, 2 and 3 are assumed, not measured +- [sceUserServiceGetUserName](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - `0x80960105`, which a PS5 (firmware 13.60) returns for a user id it does not know, has no known name; it is `USER_SERVICE_ERROR_UNKNOWN_USER` here. On the console a registered user who is not logged in still gets their name; here up to 4 users ("Player", "Player2", "Player3", "Player4") are supported; user names "Player2", "Player3" and "Player4" are assumed, not measured +- [sceUserServiceGetEvent](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - login and logout events queued dynamically on controller hotplug have no source and are assumed, not measured - [sceUserServiceGetAgeLevel](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - on a PS5 (firmware 13.60) the value is the age set in the user's parental restriction (0, 7, 12 and 15 were measured) and `0xFFFFFFFF` without one; a user who is not logged in returns `0x80960009` with or without an output. Parental restrictions are not modelled, so the initial user is always reported as unrestricted. PPSA12544 (Unity) only stores the value for its user profile - [sceUserServiceGetAccessibilityChatTranscription, PressAndHoldDelay, TriggerEffect, Vibration, ZoomEnabled, ZoomFollowFocus](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - report 0, 0, 1, 1, 0 and 1, what a PS5 (firmware 13.60) reports for a user whose accessibility settings are all at their defaults; the console reports the user's own settings (a changed vibration intensity read 3), which are not modelled. What each number means is unknown. A user who is not logged in returns `0x80960009` with or without an output - [sceUserServiceGetKeyboardType, sceUserServiceGetJapaneseInputType, sceUserServiceGetTraditionalChineseInputType](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - AnyPS5 stores no per-user keyboard setting, so the initial user reports the keyboard type of the configured system language (`ANYPS5_LANGUAGE`, English US by default) and 0 for the two input variants, and any other user id returns `USER_SERVICE_ERROR_UNKNOWN_USER`. The language-to-keyboard-type table is the repository owner's (languages 0 to 29); a valid language outside it, such as Ukrainian (30), reports 20 (US English). A value that is not a console language number is rejected by `sceSystemServiceParamGetInt` with an error naming `ANYPS5_LANGUAGE`; the call throws it and leaves the output untouched. The input variant is 0 because the owner's table gives 0 for Japanese and Traditional Chinese, the only languages needing one. `sceConvertKeycodeGetImeKeyboardType` consumes these values. diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 902a014862..6e20905a1e 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -38,6 +38,7 @@ - [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. - [Floating-point compiler-rt builtins](../../../core/libs/prx/libc/src/FloatBuiltins.cpp) (libc) follow the `libc.prx` bundled with PPSA08427 (disassembly, and that binary run side by side on random inputs) where it differs from current compiler-rt: the soft-float `__add`, `__sub`, `__mul` and `__div` (`sf3`, `df3`) round to nearest, raise no exception flags and ignore MXCSR, here done with SSE under a default MXCSR that is restored afterwards; division flushes a quotient below the smallest normal to a signed zero; `__extendsfdf2` and `__truncdfsf2` copy NaN payloads unshifted; float-to-integer conversions do not saturate but shift the significand by a count masked to the shift width. `__mulsc3`, `__muldc3`, `__divsc3` and `__divdc3` fuse the same products as the console with `fma` and scale through the x87 like its `scalbn`, so their values match, but NaN payloads and exception flags can differ, as the host `fma` and `logb` are not the console's VEX FMA code and libm - [random, srandom, initstate, setstate](../../../core/libs/prx/libc/src/Random.cpp) (libc) - not measured: the state `setstate` returns and its null return for a rear position past the degree, `errno` after `initstate` below 8 bytes, a null state (throws here), an `initstate` seed other than 1, concurrent callers (one lock serialises every call here) and a title's own libc. Measured in a payload process on a PS5 (firmware 12.70): a fresh process draws the `srandom(1)` sequence; `srandom` seeds an additive feedback generator through a `good_rand` that reads its argument as an int32, turns 0 into 123459876 and applies `x = 16807 * (x % 127773) - 2836 * (x / 127773)`, adding 0x7fffffff to a negative result, which reproduces seeds 0, 1, 2, 12345, 0xffffffff and 0x80000000; `initstate` returns null at 0 and 7 bytes and selects types 0 to 4 by size as measured at 8, 15, 31, 32, 63, 64, 127, 128, 129, 255, 256, 257 and 1000 bytes. +- [srandomdev](../../../core/libs/prx/libc/src/Random.cpp) (libSceLibcInternal, implemented in libc) is not verified against the console. The firmware 3.20 [export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibcInternal.c) names NID `a2MOZf++Wjg`; it follows FreeBSD 11 [random.c](https://github.com/freebsd/freebsd-src/blob/4ab2e064d7950be84256d671a7ae93f87cc6aa36/lib/libc/stdlib/random.c), filling every active generator word from host OS entropy instead of `KERN_ARND`, then resetting additive feedback positions without warming up. An entropy failure throws rather than leaving `sysctl`'s failure unchecked. Calls share the generator lock. - [Integer compiler-rt builtins](../../../core/libs/prx/libc/src/IntegerBuiltins.cpp) (libc) - the console traps in `__divsi3`, `__modsi3` and the 64- and 128-bit divisions on a zero divisor (`div`) and in `__negv*`, `__absv*` and `__mulv*` on overflow (an abort helper, then `ud2`); these throw instead. As in the `libc.prx` bundled with PPSA08427 (disassembly), `__addv*` and `__subv*` wrap without an overflow check, and `__udivsi3`, `__umodsi3` and `__udivmodsi4` divide in software, giving a quotient of 0 and the dividend as remainder for a zero divisor; `__clzdi2` and `__ctzdi2` of 0 return 32 and `__clzti2` and `__ctzti2` of 0 return 64 - [qsort](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) follows FreeBSD 11 [qsort.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libc/stdlib/qsort.c): Bentley and McIlroy's quicksort, with insertion sort below 7 elements and after a partition pass that swaps nothing, so elements that compare equal end in FreeBSD's order and the comparator is called in the same sequence. That the console's libc uses this FreeBSD code is unverified - [memcpy_s, memmove_s, strcpy_s, strncpy_s, strcat_s, strncat_s, wmemcpy_s, wmemmove_s, wcsncpy_s, wcscat_s, wcsncat_s](../../../core/libs/prx/libc/src/Strings.cpp) (libc) use the C11 Annex K destination size constraints, with RSIZE_MAX taken as (SIZE_MAX >> 1) / sizeof(char16_t) for wide operations and SIZE_MAX >> 1 for byte operations. An oversized destination size returns ERANGE (34) without touching the destination; the console's RSIZE_MAX and exact error value are unverified. Constraint violations return an error without invoking the registered constraint handler, as before. @@ -95,7 +96,7 @@ - [realpath](../../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. - [strtod, strtof, strtold](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `strtod` and `wcstod` (and `atof`, which calls `strtod` here) read a `NAN(...)` sequence as FreeBSD 11's gdtoa [`hexnan`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/hexnan.c) does: hexadecimal digits with an optional `0x`, blanks starting a new 32-bit word, the first 16 digits kept and the payload cut to 52 bits, a zero payload made 1, any other character giving the default NaN but consuming up to the closing parenthesis, and an unterminated sequence keeping its payload while consuming only `nan`. The payload goes under the quiet bit with the sign, as in gdtoa's [`strtod`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/strtod.c#L316-L328). `strtof`, `wcstof`, `strtold` and `wcstold` return the positive default quiet NaN (`0x7fc00000`; exponent `0x7fff`, significand `0xc000000000000000`) whatever the sign and the sequence, and consume a sequence of letters, digits and underscores up to its closing parenthesis. The console's `libSceLibcInternal.sprx`, called from a payload process on a PS5 (firmware 12.70), gives these results for the 64 NaN inputs tried through `strtod` and `strtof`, 50 of them also through `strtold` and `wcstod`, and 18 through `wcstof` and `wcstold`, and, through `strtod`, for `nan(1 2)` and for `nan( ` followed by NUL and `)`; that a title's own `libc.prx` does the same is assumed. In `hexnan`, a blank after a payload digit, while a 32-bit word of the payload is still free, starts a blank skip that has no NUL check, so when only blanks follow up to the terminating NUL the console's `strtod` reads past it: `nan(1 ` followed by NUL and `)` consumes the `)` after the NUL, so the result depends on the bytes after the string, and here that case throws. A blank right after `nan(` is skipped with a NUL check, and the console stopped at the NUL there - [remquol](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) leaves `*quo` unchanged when the result is NaN; the `libc.prx` bundled with PPSA08427 writes a value that depends on earlier x87 state (`remquol(1, 0)` stored 0 on a first call and 4 on later ones) -- [_Stod](../../../core/libs/prx/libc/src/DinkumwareInternals.cpp) (libSceLibcInternal, implemented in libc) throws for hexadecimal text with a non-zero power-of-ten scale. The console's result there does not follow a simple rule: `0x3` with a scale of -1 gives 0.29999999999999999 +- [_Stod, _Stodx, _Stofx, _Stoldx, _Stof and _Stold](../../../core/libs/prx/libc/src/DinkumwareInternals.cpp) (libSceLibcInternal, implemented in libc) throw for hexadecimal text with a non-zero power-of-ten scale. The console's result there does not follow a simple rule: `0x3` with a scale of -1 gives 0.29999999999999999. A nonzero subnormal result sets neither `errno` nor the error argument, as measured in firmware 13.60's `libSceLibcInternal.sprx` run in a Linux host process, not on a console; there about a fifth of decimal inputs with a subnormal result come out one unit apart in the last place (`_Stodx("2.2250738585072014e-308", 0)` is the largest subnormal), which is not reproduced - [Long double math in MathExact.cpp](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) was compared with the `libc.prx` bundled with PPSA08427 only for canonical 80-bit encodings; unnormals, pseudo-denormals and pseudo-infinities passed to arithmetic functions (not the classifiers) follow the host x87 instead of the console - [atanf](../../../core/libs/prx/libc/src/Math.cpp) (libc) is FreeBSD msun's [`s_atanf.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_atanf.c), with `atanlo[3]` read at run time as msun's `volatile` does. Tiny arguments return `x` without raising `FE_INEXACT`. It gives the same bits as the `atanf` of the `libc.prx` bundled with PPSA07923, PPSA05916 and PPSA13934 for all 2^32 arguments, run on a Windows host, and none of those sets `errno`. Whether other titles' builds differ is unverified. - [rename, sceKernelRename](../../../core/libs/prx/libc/include/RenameDot.hpp) (libc, libkernel) fail a last path component of `.` or `..` with `EINVAL`, after FreeBSD 11.4's [`lookup`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_lookup.c), which refuses `.` and, through its `ISDOTDOT` and `ISLASTCN` flags, `..` as the last component for the `DELETE` and `RENAME` operations. `rmdir("dir/.")`, `unlink("dir/.")` and the same calls with `..` follow the same lookup rule in FreeBSD but are not changed here: the guest still resolves the dot component away and acts on `dir` or its parent. Both arguments are checked before any host access, so `rename` of a missing source onto `dir/.` answers `EINVAL`, where FreeBSD's [`kern_renameat`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_syscalls.c) looks the source up first and answers `ENOENT`. A backslash counts as a path separator here (the guest path resolver turns it into `/`); FreeBSD treats it as an ordinary name character. None of this was measured on a console. @@ -136,6 +137,7 @@ - [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 go through the guest's `wcrtomb`; the console's treatment of invalid UTF-16 and locale-specific encoding remains unverified. Linux retains host formatting for formats without wide arguments, `%m` or `%p`, so a NaN there still follows the host (glibc prints its sign, `-nan`, and honors the `+` and space flags, where FreeBSD prints `nan`); argument positions above 99 (FreeBSD's `NL_ARGMAX`) throw when a format requires the guest formatter - [%p and %lp](../../../core/libs/prx/libc/include/WindowsFormatting.hpp) (libc) follow the console's `libSceLibcInternal.sprx` measured from a homebrew payload on firmware 12.70 ([#7549](https://github.com/boykopovar/AnyPS5/issues/7549)), not FreeBSD: bare lowercase hex with no `0x` prefix even under `#`, `0` for a null pointer, `+` and the space flag ignored, the width counting only the digits, `-` padding on the right, `0` padding with zeros when there is no precision, the precision as the minimum number of digits, nothing for a null pointer under `%.0p`, and `l` accepted. Only `snprintf` and `swprintf` of that module were measured, from a payload process rather than a title process, so a width beside `%.0p`, the `0` flag beside a precision (which C17 ignores) and `%.0p` of a non-null pointer follow this formatter's own rules and are not measured; `swprintf` was not run with `%lp`, so the wide family still refuses that conversion and returns -1. Linux routes `%p` and `%lp` to this formatter because glibc prints the FreeBSD form and `(nil)` for a null pointer, which means a format that combines `%p` with a conversion this formatter refuses, such as an argument position above 99, throws there instead of reaching glibc - [collate and collate _Getcat](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) build the facet for the C locale only and throw for any other locale name, the wide entry point following the narrow one it sits next to. The wide facet carries its own vtable, whose `compare`, `transform` and `hash` take the guest's 16-bit characters, so the length of a range is a count of characters: `compare` orders by 16-bit value (the C locale, as Dinkumware's `char_traits::compare` does), `transform` copies the characters into the result and records size and capacity in 16-bit elements (inline capacity 7, heap capacity `length | 7`, `(capacity + 1) * 2` bytes including the terminator), following Dinkumware's [VC12 `basic_string`](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/xstring#L500-L531), and `hash` mixes one whole character per round, so wide text below 0x100 hashes like its narrow spelling and a character above 0xff hashes as one value instead of the two bytes it holds. The console's `libc.prx` exports both `_Getcat` (NIDs BSVJqITGCyI and 2wz4rthdiy8) with identical 0x19d-byte bodies, believed to build the same 0x20-byte facet; the console bodies themselves are unreadable (obfuscated dynsym, and the text of the relinked `libc.prx` is zeroed), so the wide entries follow the narrow algorithm and the C locale instead of the console's wide code, and no console run checks the two. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size. The narrow result capacity (`length | 15`) and the over-aligned storage of results of 4080 characters or more follow the console's `libc.prx` code and are not checked on hardware. No module of the PPSA59322, PPSA76677 or PPSA97358 dumps in this tree imports either `_Getcat`, so neither has been exercised by a title. +- [codecvt, numpunct, num_put and time_put _Getcat](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) build facets matching a bundled console `libc.prx` (dynsym decoded by NID, vtables disassembled): categories 2, 4, 4 and 5, sizes 0x10, 0x30, 0x10 and 0xb8. `codecvt` reproduces the console virtuals (`noconv` conversions, `length` the shorter range); `numpunct` copies its C-locale table (`""`, `.`, `,`, `false`, `true`). `num_put`/`time_put` formatting virtuals throw rather than fabricate output, and `time_put`'s `_Locinfo` table is unpopulated — the console's `do_put` bodies were not mirrored. The facets share the no-op `destroy`/`deleteObject`/`retain`/`release` base used for `collate`, so `numpunct`'s static C-locale strings are never freed. Unlike `collate`, these `_Getcat` entries ignore the locale argument and always build the C-locale facet; the console's behaviour for a non-C name is unverified. `std::clog` and `std::wclog` are 0x400-byte zeroed objects like the rest of the `NSTDSTR` stream set (the console's `libc.prx` sizes `std::clog` at 96 bytes, measured from its dynsym `st_size`), and the `numpunct::id` and `time_put::id` objects are 8-byte zeros like the other facet ids. - [std::__throw_bad_variant_access](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libc) throws `std::bad_variant_access` with the `what()` string `bad_variant_access`, the one [LLVM libc++](https://github.com/llvm/llvm-project/blob/main/libcxx/src/variant.cpp) returns, since the export carries libc++'s `__throw_bad_variant_access` name; the console's string is unverified. Onimusha: Way of the Sword (PPSA27836 01.000.001) imports it from `libc`, and its bundled `libc.prx` does not export it. - [std::bad_optional_access and std::_Throw_bad_optional_access](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libc) provide the exception type, constructor, destructor, and `what()` exports alongside `_Throw_bad_optional_access`, returning the `what()` string `bad_optional_access` matching Dinkumware and libc++ conventions; the console libc.prx string is unverified. PPSA26578 (issue #7624) and PPSA34015 import `_ZSt26_Throw_bad_optional_accessv` (`KwzEip7m6TM`) from `libc`, and fail to load on main with unresolved import (0xC0000139). - [The scanf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc), the `_s` forms included, decodes the input of the 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`) on Linux as UTF-8, mirroring the narrow printf family, where FreeBSD uses the locale's `mbrtowc` (one byte per character in the C locale); the console's locale encoding is unverified. Input that is not UTF-8, a character outside the BMP, a scanset member outside ASCII, positional arguments next to a wide conversion and wide conversions in `fscanf` throw. Each character is stored as one 16-bit unit, widths and `_s` sizes count characters, and a `%lc` field cut short by the end of the input keeps what it read, as FreeBSD's `convert_wchar` does. Windows hands `%ls`, `%lc` and `%l[` to the host, whose `wchar_t` is 16 bits @@ -160,4 +162,6 @@ - [num_put vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc, NID `59oywaaZbJk`) is fourteen zeroed `uintptr_t` slots matching `st_size` 112 on the title `libc.prx` from PPSA16833 (offset-to-top, typeinfo, two destructors, `_Incref`/`_Decref`, and eight `do_put` virtuals). Facet calls through the object fault until those methods are wired. - [ios_base standard streams](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) `_Addstd` queues the stream in the standard stream table of Dinkumware [ios.cpp](https://github.com/ganboing/crt.vc12/blob/master/src/ios.cpp) (`stdstr`, `stdopens`, `_Addstd`, `_Ios_base_dtor`): it takes the first of slots 1 to 7 that is free or already holds the stream, and slot 8 (`NSTDSTR`) when none is, so slot 8 is shared by every stream past the first seven and registration never fails. It stores the 1-based slot index in the guest `IosBase::standardStream` (Dinkumware's `_Stdstr`, where 0 means not a standard stream) and counts the stream objects registered per slot; registering the same stream again reuses its slot and raises its count. Measured on a PS5 (firmware 13.60) from a homebrew payload calling the system `libSceLibcInternal` export: in a process whose own standard streams already hold slots 1 to 7, twelve distinct streams and a repeated one all get 8, written as a 64-bit value at offset 0x8 with no other byte of the object touched; the assignment of slots 1 to 7 to fresh streams is not observable there and follows VC12. The counts are `int`, not VC12's `char`, and the registration and the destructor lock the table where VC12 locks only `_Addstd`; a slot index past the table throws in the destructor instead of indexing out of bounds. The destructor decrements its slot's count and suppresses the rest of the destruction while the count stays positive, as `_Ios_base_dtor` does, and the slots of destroyed streams keep their pointer and stay reserved for that stream, as `stdstr` does. The streams themselves are destroyed by the title's own code as it registers them through `__cxa_atexit`; when the last reference closes, the unsupported stream storage and callback cleanup is still the throw it was and the locale is released as for a non-standard stream. - [std::_Pad, _Thrd_join, _Thrd_detach, _Thrd_id](../../../core/libs/prx/libc/src/Threads.cpp) (libc) follow Dinkumware V6.00 `thr/xthread` and `thr/threadcall.cpp` ([crt.vc12 `f1d47126`](https://github.com/ganboing/crt.vc12/tree/f1d47126/src/thr)), without console verification. `_Thrd_id` throws because whether the console returns `pthread_t` or a kernel thread ID and at what width is unknown; `_Launch` omits `_Cnd_do_broadcast_at_thread_exit` and calls `pthread_create` registered by libkernel with default attributes. The 32-byte pad layout (`_Go()` vtable at 0x0, `_Cnd_t` at 0x8, `_Mtx_t` at 0x10, `bool _Started` at 0x18) is assumed from VC12, unverified. Failing primitives or thread start throw the guest `std::system_error`; null arguments throw. `_Thrd_join` returns `_Thrd_error` (4) on `pthread_join` failure and truncates the exit code to `int`; `_Thrd_detach` returns `_Thrd_error` (4) on `pthread_detach` failure and throws when none is registered +- [siginterrupt](../../../core/libs/prx/libc/src/Process.cpp) (libc) runs FreeBSD 11.4's `siginterrupt.c` sequence through the `sigaction` and the `_sigintr` set that libkernel registers at load: it reads the signal's disposition, sets or clears the signal's bit and `SA_RESTART`, and installs the disposition again. The set is written outside libkernel's registration lock, as FreeBSD's libc writes it without one; a call before libkernel has registered throws. - [num_get::id, time_get::id and the num_get vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) are data objects, as the public SDK's libSceLibcInternal table declares them and as the other facet ids and the `num_put` and `codecvt` vtables are kept: the ids start at zero and the vtable is 17 zeroed slots, the 136-byte size of `KfcTPbeaOqg` in the console's libSceLibcInternal, whose relocations fill the offset-to-top and typeinfo entries, the two destructors, `locale::facet::_Incref` and `_Decref`, and the eleven `do_get` of Dinkumware's crt.vc12 `xlocnum`, so a guest resolves the symbols and reads data, and a facet call through the vtable faults. libSceAmpr exported the three as throwing functions, which a data consumer reads as code; its qualified imports fall back to libc. +- [printf family conversions](../../../core/libs/prx/libc/include/WindowsFormatting.hpp) (libc) - Windows `FormatWindows` follows the title `libc.prx` `_Printf` measured from PPSA13934, PPSA07923, PPSA05916 and PPSA04158: `%q`, `%D`, `%O` and `%U` are unknown conversions; an unknown character is printed with its leading `%`, and width, `-` and discarded length modifiers behave as measured (`%5y` is ` %y`, `%hhz` is `%z`). `%%` takes a width. A trailing `%` reads past the format's NUL and is not reproduced; it throws. Other flags and precision on unknown conversions are unverified and throw. Linux formats these through glibc, which prints `%D`, `%O`, `%U` and unknown conversions as written diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 1519cb3801..c499a3f9d3 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -2,6 +2,7 @@ ### Silent stubs +- [SO_TIMESTAMP, SO_BINTIME](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - set and read back as their bits, as a payload process on a PS5 (firmware 13.60) answered them, but `recvmsg` adds no `SCM_TIMESTAMP` or `SCM_BINTIME` control message, which FreeBSD 11.4's `ip_savecontrol` and `ip6_savecontrol` add to a received datagram while the option is set; what the console's `recvmsg` returns was not measured - [sceKernelGetSocPowerConsumption](../../../core/libs/prx/libkernel/System/src/ThermalSensors.cpp) (libkernel) - returns `EINVAL` and writes nothing, as `sceKernelGetSocSensorTemperature` does when the host has no sensor: the host's package power is not read (Linux RAPL `energy_uj` is readable by root only, and Windows has no general source). The prototype is not documented. PS5LM Plus (PPSA99581) passes a zeroed 128-byte buffer and, on success, reads its first 32-bit word as milliwatts, accepting 1000 to 350000; on failure it shows 0 W - [scePthreadMutexattrSetprotocol, scePthreadMutexattrGetprotocol, scePthreadMutexattrSetprioceiling, scePthreadMutexattrGetprioceiling](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) - `scePthreadMutexattrSetprotocol` accepts 0 to 2 as a PS5 (firmware 13.60) does and returns `EINVAL` otherwise, but the protocol is not applied: priority inheritance and protection are not modelled. On the console a null or uninitialized mutex attribute returns `EINVAL` from the SCE attribute calls, `scePthreadMutexInit` takes an uninitialized attribute as the default one, and a destroyed mutex returns `EINVAL` from `scePthreadMutexDestroy`; a null mutex pointer crashed the payload and still throws - [pthread_mutexattr_setprotocol, pthread_mutexattr_getprotocol, pthread_mutexattr_setprioceiling, pthread_mutexattr_getprioceiling](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) - the protocol and the priority ceiling are stored and reported as in [FreeBSD 11 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.0/lib/libthr/thread/thr_mutexattr.c#L192-L245) (the ceiling is set or read only under `PTHREAD_PRIO_PROTECT`, and setting a protocol resets it to the highest `SCHED_RR` priority). A null, uninitialized or destroyed attribute returns `EINVAL` before the other arguments are checked; this error behavior is unverified on the console. Mutexes do not apply the protocol or ceiling: a priority-inherit or priority-protect mutex locks like a normal one and does not change its owner's priority @@ -154,9 +155,11 @@ - [readv, writev, preadv, pwritev and their sceKernel forms](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) - unreadable iovec-table errors follow FreeBSD 11.4's [`copyinuio`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/subr_uio.c#L401-L434), not console measurements. The table is copied before inspecting its entries; payload-buffer validation is unchanged. A Linux `process_vm_readv` failure other than `EFAULT` throws, including when the host denies that API. - [/dev/random and /dev/urandom](../../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) (libkernel) opened for reading return a descriptor backed by the host null device whose reads are filled with host OS entropy (`getentropy`, `rand_s` on Windows); both paths behave the same, never block, and positioned reads ignore the offset (FreeBSD `random(4)`). Only the exact paths are recognised, `fstat`, `lseek` and `mmap` see the null device, and opening them for writing throws - [pthread_attr_getinheritsched, pthread_attr_setinheritsched, pthread_attr_getscope, pthread_attr_setscope, pthread_attr_getstackaddr](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) follow FreeBSD 11 libthr ([thr_attr.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_attr.c), [thr_create.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_create.c)), not measured on the console: the scope defaults to `PTHREAD_SCOPE_SYSTEM` (2), only it and `PTHREAD_SCOPE_PROCESS` (0) are accepted and other values return `EINVAL`, and an inherit value other than `PTHREAD_INHERIT_SCHED` (4) or `PTHREAD_EXPLICIT_SCHED` (0) returns `ENOTSUP`. `pthread_attr_getstackaddr` returns the configured stack address pointer (null by default). A null output of `pthread_attr_getinheritsched` throws, since libthr writes through it unchecked. A thread keeps the scope it was created with, taken from the creating thread under `PTHREAD_INHERIT_SCHED`, and `pthread_attr_get_np` and `scePthreadAttrGet` report it, but every guest thread is a host thread at system scope, so process scope changes nothing -- [Shared memory](../../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel) on Linux implements anonymous and named objects, bounded descriptor writes and seeks, and `shm_unlink` with effective read/write permission checks from FreeBSD 11. Named paths up to 1023 bytes use `/anyps5-shm-` plus SHA-1; collisions are not detected, and the namespace is shared across processes in the host mount namespace. Named creation applies the guest umask without changing the host mask. Linux procfs and `/dev/shm` are required. Operations use an advisory directory lock; external host operations bypass it, unrelated objects serialize, and descriptor operations require two spare host descriptors and compare device/inode with the initial descriptor identity on both the pinned-object and ordinary-file fallback paths. [Guest mappings](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) support shared and private mappings of these descriptors through the allocation tracker and pin a duplicate descriptor during validation and mapping; mapping requires one spare host descriptor; file-backed mappings use the write-watch scan fallback because aliases and descriptor writes bypass per-mapping tracking. On Windows, anonymous objects are delete-on-close temporary files and `mmap` maps them as [regular files](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp); named objects 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. On Windows, anonymous objects are delete-on-close temporary files and `mmap` maps them as [regular files](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp); named objects are described in the next entry. Invalid non-null name pointers, console behaviour and title compatibility remain unverified. +- [shm_open, shm_unlink](../../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel, Windows) - named objects are regular files in the current user's temporary directory under `AnyPS5/shm`, identified by SHA-1 of the exact guest name. The namespace is per host user and shared by every AnyPS5 process using that temporary root, so two titles using the same name share the object. Namespace operations use a cross-session named mutex, while file-size changes and writes use a mutex keyed by the host file identity; unlink renames the visible object to a unique internal name so already-open handles remain usable while the guest name can be recreated. Orphaned files are reaped when the last tracked guest descriptor closes or opportunistically during a later open. Closing through host stdio wrappers does not update this bookkeeping or reap immediately. The namespace directory has a protected ACL for the owner, SYSTEM and Administrators; newly created objects map the guest owner read/write bits after applying the guest creation mask. `fchmod` updates those owner permissions on the descriptor's file object. Guest group/other permission classes are not represented independently on Windows. Mapping a named object uses the same Windows file mapper as anonymous objects and no test covers it, and no behavior here has been measured on PS5. Lifecycle, cross-process resize/write, and mode behavior are exercised by Windows tests. See Microsoft's [file sharing modes](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea), [DeleteFile](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-deletefilew) and [mutex objects](https://learn.microsoft.com/en-us/windows/win32/sync/mutex-objects). - [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a read event on a listening socket, here and in `kevent`, reports the number of completed connections waiting to be accepted in `data`, as FreeBSD 11.4's [`filt_solisten`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3384-L3392) reports `so_qlen`: on Linux a TCP listener's accept queue from `TCP_INFO` (`tcpi_unacked`, which Linux fills with the accept backlog for a listening socket), and elsewhere, or for a listener that is not TCP, 1 while a connection is waiting, as no host call reports that count. A write event reports the free space of the socket's send buffer in `data`, as FreeBSD 11.4's [`filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3358-L3382) reports `sbspace`: on Linux the host's `SO_SNDBUF` less the bytes `SIOCOUTQ` counts as queued, which include the host's per-packet overhead, so the space shrinks by somewhat more than the bytes sent. Windows has no query for that space and macOS is not wired up, so both report the whole `SO_SNDBUF`, which does not shrink as data is queued. Only guest sockets are supported; other descriptors and a low watermark above 1 throw -- [sceKernelAddReadEvent on pipes](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) is the one exception to the sockets-only rule above: it also takes the read end of a pipe, as `kevent` does through FreeBSD's `filt_piperead`, and ignores the size argument there. Host streams, other descriptors and a write event on a pipe throw +- [sceKernelAddReadEvent on pipes](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) is an exception to the sockets-only rule above: it also takes the read end of a pipe, as `kevent` does through FreeBSD's `filt_piperead`, and ignores the size argument there. Host streams, other descriptors and a write event on a pipe throw +- [sceKernelAddReadEvent and sceKernelAddWriteEvent on regular files](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) are the other exception: a size argument above 1, directories and other vnode types throw, and whether the console's file systems answer as `vfs_kqfilter` does was not measured. A wait holding one of these events polls it every millisecond, as no host call reports every file change, so a write through another descriptor shows up to 1 ms late. Unlike FreeBSD, a seek can make a read event fire again: FreeBSD 11.4's [`kqueue_scan`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_event.c) deactivates a level-triggered read knote once `filt_vfsread` reports `data` 0, and only a vnode `KNOTE` (a write, an extend or a truncate) activates it again; `lseek` posts none, so after a file read to its end is seeked back, the 1 ms poll here reports the new `data` (10 for a 10-byte file seeked to 0) where FreeBSD keeps waiting until the file changes. Otherwise they follow FreeBSD 11.4's [`filt_vfsread` and `filt_vfswrite`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_subr.c#L5122-L5168): a read event reports the size less the descriptor's offset in `data` and fires while that is not 0; a write event always fires with `data` 0 - [close](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) and [setsockopt](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) reach a [libSceNet](../../../core/libs/prx/libSceNet/Export.cpp) socket through hooks libSceNet registers, as `select` polls them; the other libkernel socket calls do not. libSceNet numbers its sockets from 32 in its own table, so a CRT file descriptor and a socket can share a number; `close` then closes the socket. On the console both are one kernel descriptor space - [sceKernelPread, sceKernelPwrite, sceKernelPreadv, sceKernelPwritev](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on Windows transfer with an overlapped `ReadFile`/`WriteFile` and restore the descriptor's file pointer afterwards. Guest scalar transfers, positioned transfers, vectored transfers and seeks share a position lock across duplicated descriptors; a vector holds it for the whole operation. Independently opened descriptors and other processes do not share that lock, so vector transfers over their overlapping file ranges are not coordinated. Handles that are not disk files return `ESPIPE`. A pipe or console `readv` can return a short read when no more input is available; each iovec is transferred separately, so a failure after the first transfer returns the bytes already done - [sceKernelAioSubmitReadCommands, sceKernelAioSubmitWriteCommands and their Multiple variants](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) on Windows transfer through the same positioned path as `sceKernelPread`/`sceKernelPwrite` and restore the descriptor's file pointer afterwards; positioned transfers, plain transfers and seeks of one descriptor are serialized per descriptor (shared across duplicated descriptors), so a concurrent transfer observes a stable file pointer @@ -223,7 +226,10 @@ - [kevent on a pipe](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - host pipes are one-way, where FreeBSD's [`kern_pipe`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L422) opens both ends for reading and writing, so `EVFILT_READ` on the write end and `EVFILT_WRITE` on the read end throw; on macOS `EVFILT_WRITE` on a pipe throws, as the host has no call for a pipe's capacity. `EVFILT_WRITE` reports the free space of the host buffer, not of FreeBSD's 16 KiB one, and on Linux fires when the host says the pipe is writable rather than at `PIPE_BUF` (512) free bytes. Windows pipes cannot be waited on, so a kqueue waiting on one checks it every 1 ms. Otherwise [`filt_piperead`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L1787-L1828) and `filt_pipewrite` are followed: `EV_EOF` once the other end closes, `EPIPE` for a write event added after the read end closed, and `NOTE_LOWAT` ignored. On Windows `fcntl` cannot make a pipe write end non-blocking, so the full-pipe and drain case of `EVFILT_WRITE` is tested on Linux only and the `NtQueryInformationFile` write quota is checked there only for the free space, `EV_EOF` and `EPIPE`. - [kevent on a kqueue](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - `EVFILT_READ` on a kqueue counts the watched queue's enabled notes that are ready when it is evaluated, where [`filt_kqueue`](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/sys/kern/kern_event.c#L363-L370) reports `kq_count`, the queued notes: a queued note counts until that kqueue is scanned, even once disabled or no longer ready (a socket or pipe read empty, a user note cleared, a watched kqueue drained), so FreeBSD can report it readable where this reports nothing. A queued `EV_ONESHOT` note is reported there with its last count and deleted; here it is evaluated again and, if the watched queue is empty by then, neither reported nor deleted. `EV_CLEAR` throws, as FreeBSD re-arms it only when the watched kqueue queues a note. A cycle of watches throws; a note on the queue itself is `EINVAL`. Not measured on a console - [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`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 -- [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - a guest socket has the SOL_SOCKET level, `TCP_NODELAY`, `IPV6_V6ONLY` and four `IPPROTO_IP` options. `SO_DEBUG`, `SO_REUSEPORT` and `SO_TIMESTAMP`, `IP_MULTICAST_IF`, `TCP_MAXSEG` and `TCP_KEEPIDLE` fail with `ENOPROTOOPT` (42), where a payload process on a PS5 (firmware 13.60) answered them, reading back their bit, 0, 536 and 7200; the other options of these levels fail the same way and were not all measured. `SO_REUSEADDR`, `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and a boolean option reads back as its own bit, as there. Winsock does not clear a pending error when `SO_ERROR` reads it, so on Windows a pending error is reported once and again only after a new `connect`; a second failure with the same code without a `connect` in between is hidden +- [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - a guest socket has the SOL_SOCKET level, five `IPPROTO_TCP` options, `IPV6_V6ONLY` and four `IPPROTO_IP` options. `IP_MULTICAST_IF`, `TCP_NOPUSH`, `TCP_NOOPT` and `TCP_KEEPINIT` fail with `ENOPROTOOPT` (42), where a payload process on a PS5 (firmware 13.60) read them as 0, and so does `SO_ACCEPTFILTER` (0x1000), which the console fails with `EINVAL` for a set and a get on a non-listening socket, as FreeBSD 11.4 does; the other options of these levels fail with `ENOPROTOOPT` and were not all measured. `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and a boolean one reads back as its own bit, as there. Winsock does not clear a pending error when `SO_ERROR` reads it, so on Windows a pending error is reported once and again only after a new `connect`; a second failure with the same code without a `connect` in between is hidden +- [SO_DEBUG, SO_REUSEADDR, SO_USELOOPBACK, SO_REUSEPORT, SO_TIMESTAMP, SO_BINTIME and option 0x10000](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept as bits that read back as on a PS5 (firmware 13.60, payload process), and options 0x4000 and 0x8000 are accepted and cannot be read, as there; only `SO_REUSEADDR` and `SO_REUSEPORT` change what the socket does. Both are set on the host, `SO_REUSEPORT` as `SO_REUSEADDR` on Windows, which has no `SO_REUSEPORT` and there also lets the socket bind over one that did not set it; Linux spreads unicast datagrams across the sockets, where FreeBSD gives each to one. Option 0x10000 is only kept: if the console treats it as FreeBSD 12's `SO_REUSEPORT_LB`, two sockets that set it share a port there, where the second `bind` here fails with `EADDRINUSE`. An accepted socket takes the bits from its listener, as FreeBSD 11.4's `sonewconn` copies `so_options`; that was not measured +- [TCP_MAXSEG](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per stream socket, read as 536 at first and failing with `EINVAL` for 1000, 70000 and 100, as a payload process on a PS5 (firmware 13.60) did. No segment set succeeded there, so the rule here, `EINVAL` above the current segment or below 176 and any segment between accepted, follows FreeBSD 11.4's `tcp_default_ctloutput` (`optval + 40 >= V_tcp_minmss`) and is not measured. An `AF_INET6` socket also starts at 536 here, where FreeBSD's `tcp_newtcpcb` uses 1220; not measured. The segment is not set on the host socket, so the host's segment size is its own, and a connected socket still reads the segment it was given instead of the negotiated one +- [TCP_KEEPIDLE, TCP_KEEPINTVL, TCP_KEEPCNT](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per stream socket and read back as a payload process on a PS5 (firmware 13.60) did: 7200, 75 and 8 at first, 0 restoring a keepalive default, and a count stored as an unsigned value. They are not set on the host socket, so the host's keepalive timers are its own while `SO_KEEPALIVE` is applied there. An interval above 4294967 seconds fails with `EINVAL`, after FreeBSD 11.4's `tcp_default_ctloutput` with `hz` taken as 1000, and an accepted socket takes the keepalive values from its listener as `syncache_socket` copies them; neither was measured - [IP_TTL, IP_TOS, IP_MULTICAST_TTL, IP_MULTICAST_LOOP](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per `AF_INET` socket with the defaults (64, 0, 1, 1), the low-byte storage of `IP_TTL` and `IP_TOS`, the one-byte forms and the `EINVAL` above 255 of the multicast options that a payload process on a PS5 (firmware 13.60) showed. `IP_TTL` and the multicast options are also set on the host socket, except a time to live of 0, which hosts refuse; `IP_TOS` is only kept, so the host's packets carry its own type of service, and the host's default time to live (128 on Windows) is not changed to 64. An accepted socket takes `IP_TTL` and `IP_TOS` from its listener, as FreeBSD's `syncache_socket` does, and an `int` `IP_MULTICAST_LOOP` keeps only its low byte, as FreeBSD 11.4's `inp_setmoptions` does; neither was measured. Winsock applies `IP_MULTICAST_LOOP` on the receiving socket, not the sender, so on Windows the receiver's setting decides loopback - [SO_SNDBUF, SO_RCVBUF, SO_SNDLOWAT, SO_RCVLOWAT](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per socket with the defaults, the 524288-byte limit, the `ENOBUFS` above 5592405 bytes and the low-water clipping of a payload process on a PS5 (firmware 13.60); the host socket gets the size asked for, so a request above the limit still enlarges the host's buffer. The host's real buffering, FreeBSD's automatic growth of a connected TCP socket's buffers, and a low-water mark's effect on `select`, `poll` and a receive are not reproduced. An accepted socket takes all four from its listener, as FreeBSD's [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) does; that was not measured - [SO_NOSIGPIPE](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per socket and read back as its bit. On Linux a `send`, `sendto`, `sendmsg` or `write` on that socket passes `MSG_NOSIGNAL` to the host, as [`kern_sendit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L890-L895) and [`soo_write`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L147-L151) leave out the signal, and an accepted socket takes it from its listener, as [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) copies the options. Not measured on a console @@ -238,9 +244,9 @@ - [socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) creates only `SOCK_STREAM` and `SOCK_DGRAM` sockets in `AF_INET` and `AF_INET6`. A type that FreeBSD 11.4's inet domain has no protocol for in any configuration ([`in_proto.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/netinet/in_proto.c) has no `SOCK_RDM` entry and no unknown type) fails with `EPROTOTYPE` when the protocol is 0 and `EPROTONOSUPPORT` otherwise, as [`socreate`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c) does. `SOCK_RAW`, which `in_proto.c` always has (`socreate` creates it for a privileged process), and `SOCK_SEQPACKET`, which it has only in a kernel built with `SCTP`, fail with `EPROTONOSUPPORT`; whether the console allows either is not known. `SOCK_DGRAM` with `IPPROTO_UDPLITE` and `AF_UNIX` (`socketpair` supports it) fail with `EPROTONOSUPPORT` and `EAFNOSUPPORT` where FreeBSD creates the socket - [sigtimedwait](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) waits on a process-wide pending set instead of throwing: a signal raised while blocked stays pending and is taken by `sigwait`, `sigwaitinfo` or `sigtimedwait`, delivered to the handler when `_sigprocmask` unblocks it, or delivered by `pause`. The set covers all 128 signals and the lowest number that is pending and in the set is selected (FreeBSD specifies the lowest number only among real-time signals). Selection, `EAGAIN` on timeout, `EINVAL` for a `tv_nsec` outside 0 to 1000000000 while nothing is pending and `NULL` timeout blocking indefinitely follow FreeBSD's [sigtimedwait(2)](https://man.freebsd.org/cgi/man.cgi?query=sigtimedwait). `sigwait` returns 0 with the number in its output, as POSIX specifies, and `sigwaitinfo` and `sigtimedwait` return the number, as FreeBSD does. The guest `siginfo_t` follows FreeBSD 11's `sys/sys/siginfo.h`: 80 bytes, `si_signo` at 0, `si_code` at 8, `si_pid` at 12, `si_uid` at 16, `si_addr` at 24, `si_value` at 32 and `si_band` at 40; only `si_signo`, `si_code` (`SI_USER`) and `si_pid` (the guest pid) are written, `si_uid` stays 0 because no uid is modelled, and the console's layout is not verified. PPSA85300 imports `sigtimedwait` 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`. The waits are not cancellation points and an unblocked caught signal does not interrupt them with `EINTR`, where FreeBSD's `sigwaitinfo` and `sigtimedwait` may return it. `SIGPIPE` has no host equivalent and is never raised by a host write here (see `read, write and vector I/O`), so VLC's drain finds nothing - [pause, sigsuspend, sigwait, sigwaitinfo](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) take the pending set of `sigtimedwait` instead of throwing. `pause` waits for a pending signal that is not blocked and whose disposition can be delivered, runs its handler on the calling thread and returns -1 with `EINTR`; an ignored signal is discarded and the wait continues, and it never returns for a signal whose handler cannot run. A signal whose registered handler has `SA_SIGINFO` still throws, and one whose disposition is the default action and that has no host equivalent (every signal except 2, 4, 6, 8, 11 and 15) is neither delivered nor discarded: it stays pending until a wait takes it, where FreeBSD runs the default action, which terminates the process for most of them. A pending `SIGALRM` with the default disposition exits the process with status 142 once it is unblocked, as FreeBSD's default action does. 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 wait 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)). `sigsuspend` swaps the process-wide mask of `_sigprocmask` for its argument, waits as `pause` does through the same helper, then restores the mask through `_sigprocmask`, and returns -1 with `EINTR`; a null set throws, as the console faults in libthr instead of returning `EFAULT`. All four words of the argument replace the mask and are restored afterwards, as in `_sigprocmask`. Java 25 LAN / DNS (PPSA99869, #5501) imports it and fails to load without it, after `sysctlnametomib`, `fegetenv` and `mincore`. None of the five waits is a cancellation point -- [getsid](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) returns the calling process ID for pid 0 and the calling process, and `ESRCH` for other process IDs, assuming the single guest process is its own session leader. PPSA74126 imports it and fails to load without it +- [getsid, setpgid, setpgrp](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - getsid returns the calling process ID for pid 0 and the calling process, and `ESRCH` for other process IDs, assuming the single guest process is its own session leader. setpgid and setpgrp follow FreeBSD 11.4's `sys_setpgid` for the only guest process: a negative group ID fails with `EINVAL`, a process ID other than 0 or the caller's PID (including a negative PID) fails with `ESRCH`, and because the single guest process is modeled as its own session leader, `SESS_LEADER(targp)` fails with `EPERM` before normalizing the process group or reaching the same-process-group no-op. PPSA74126 imports `getsid` and fails to load without it - [sigaction](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do; their mask and flags are not applied. `SA_SIGINFO` handlers are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`, and neither are the handlers of other signals (`SIGBUS`, `SIGHUP`, ...), whether they are set by `sigaction` or by `signal`, which records them too (every number from 1 to 128, with `SA_RESTART` and an empty mask replacing what `sigaction` stored; `SIGKILL` and `SIGSTOP` accept only the default handler in both calls, as in FreeBSD's `kern_sigaction`). `raise` and `kill` accept every number from 1 to 128 now: a blocked one stays pending on the set of `sigtimedwait`, an unblocked one whose handler is registered runs it on the calling thread, and an unblocked one that has no host equivalent and no handler still fails with `EINVAL`, where FreeBSD runs its default action. `raise` of an `SA_SIGINFO` handler still throws instead of terminating -- [signal](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records its handler with `SA_RESTART` unless the signal is in the exported `_sigintr` set, as FreeBSD's libc does (`lib/libc/gen/signal.c`). `siginterrupt`, which fills that set, is a libSceLibcInternal export and is not provided: libc does not link libkernel and cannot reach the dispositions, so only a guest that writes the set itself clears `SA_RESTART` this way. +- [signal](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records its handler with `SA_RESTART` unless the signal is in the exported `_sigintr` set, as FreeBSD's libc does (`lib/libc/gen/signal.c`). `siginterrupt`, which fills that set, is a libSceLibcInternal export that libc serves through the `sigaction` and the set libkernel registers at load (see libc). - [kill, killpg](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follow FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128 and for negative `killpg` process groups, signal 0 only checks that the target exists, and its own pid, 0 and minus its own pid (the guest is taken to lead its own process group) signal it through `raise`, so they share `raise`'s limits above. Any other pid, and -1, which skips the caller, report `ESRCH`, so `EPERM` is never reported - [sceKernelFcntl](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) runs the POSIX `fcntl` below and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use. The return values and error codes are unverified on a console - [Creating through a trailing slash](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) - `open` and `sceKernelOpen` with `O_CREAT` on a missing name followed by a slash (`new/`) fail with `EINVAL` on Windows and with the host's `EISDIR` on Linux, where FreeBSD 11.4's `lookup()` (sys/kern/vfs_lookup.c) fails with `ENOENT` because only a directory may be created through a trailing slash @@ -273,7 +279,7 @@ - [chflags](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) follows FreeBSD 11.4's [`kern_chflagsat`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L2366-L2383) and UFS's [`ufs_setattr`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/ufs/ufs/ufs_vnops.c#L509-L556) for a root caller: the path lookup reports `EFAULT`, `ENOENT` and `ENOTDIR`, a flag outside the 17 UFS stores (`VNOVAL` included) `EOPNOTSUPP`, and `SF_SNAPSHOT` `EPERM`. Neither host keeps BSD file flags, so every file is taken to have none (`stat` reports `st_flags` 0): flags 0 succeed without touching the file, and any other flag throws, naming the flags. `UF_READONLY`, `UF_HIDDEN`, `UF_SYSTEM` and `UF_ARCHIVE` are FreeBSD's names for the Windows attributes but are not mapped to them: UFS stores them without enforcing them, `FILE_ATTRIBUTE_READONLY` already carries the owner write bit of `chmod` on Windows, and Linux has no equivalent a process can set without `CAP_LINUX_IMMUTABLE`. The console's filesystem types, `EROFS` on read-only mounts and the change-time update are not modelled - [fchflags](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) has the gaps of `chflags`: a valid non-zero flag throws, and the console's filesystem types, `EROFS` and the change-time update are not modelled. Character devices, the random device, other host descriptors and files read from app0.zar throw, since their console filesystems and the privilege check for devices are not modelled. On Windows, anonymous shared memory objects are temporary files that are not told apart from regular files, so they are treated as files instead of reporting `EINVAL` as FreeBSD 11.4's [`getvnode`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L3850-L3877) does. - [getpriority](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) follows FreeBSD 11.4's [`sys_getpriority`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_resource.c#L93-L165) for the only guest process, which is taken to run at nice 0: `PRIO_PROCESS` and `PRIO_PGRP` with 0 or its own pid (the guest is taken to lead its own process group, see `kill`) and `PRIO_USER` with 0 or its user id 0 return 0, any other id fails with `ESRCH` and any other `which` with `EINVAL`. As in FreeBSD's libc, where `getpriority` is the bare system call, errno is left unchanged on success. The nice value of a title process on the console is unverified, and the host nice of the process or of the threads `APS5_THREAD_PRIORITY=high` raises is not reported. `setpriority` accepts the same target ids (any other id fails with `ESRCH`, any other `which` with `EINVAL`), clamps the value to FreeBSD's `PRIO_MIN`-`PRIO_MAX` (-20 to 20) and stores it as the guest process's nice value, which `getpriority` returns; the host scheduling class is not changed, and whether a title process may lower the value without privilege is unverified -- [rtprio_thread, pthread_setconcurrency, pthread_getconcurrency](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - `rtprio_thread` follows FreeBSD 11.4's `rtprio_thread(2)`. Every guest thread here runs under `SCHED_FIFO` or `SCHED_RR` (priority 256-767), whose rtprio class is real-time; the console's mapping from that priority to the rtprio `prio` range (0-31) is unmeasured, so `RTP_LOOKUP` and `RTP_SET` throw for such a thread rather than report an impossible priority. A thread that is not real-time reports `RTP_PRIO_NORMAL` (3) with 0, and `RTP_SET` accepts only that value. Another function fails with `EINVAL`, a null `rtprio` with `EFAULT`, a thread other than the calling one throws, and an id that is not a live guest thread with `ESRCH`. `pthread_setconcurrency` stores 0 or more and returns `EINVAL` for a negative level (the error itself, as FreeBSD's libthr does, with errno untouched); `pthread_getconcurrency` returns the level, which is a hint and changes nothing about the host's concurrency. Morrowind (`mshivam019/morrowind-ps5`) imports all three at start-up +- [rtprio_thread, pthread_setconcurrency, pthread_getconcurrency](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - `RTP_SET` throws for a real-time thread (sce attributes allow only `SCHED_FIFO`/`SCHED_RR`) and for another thread: a payload process on a PS5 (firmware 12.70) answered type 2, prio 0/15/31 on the calling thread with `EINVAL`; no accepted value or set by another thread's id measured. A non-real-time thread (POSIX attributes store `SCHED_OTHER`), unmeasured, reports 3 with 0, and `RTP_SET` accepts only that. `RTP_LOOKUP` reports the policy's type (10 or 2) and the scheduling priority, as measured, not FreeBSD's 0-31; another function fails with `ESRCH` (2 measured), a null `rtprio` with `EFAULT`, an id of no live thread with `ESRCH` (unknown ids measured). `pthread_setconcurrency` stores 0 or more, returns `EINVAL` (not errno) below 0, as libthr does; `pthread_getconcurrency` returns it, a hint. Morrowind (`mshivam019/morrowind-ps5`) imports them - [sceKernelVirtualQuery](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) returns [VirtualQueryInfo](../../../core/libs/SceTypes.hpp) with a one-byte flag allocation unit so Windows and SysV hosts return the same 72-byte representation, with the name at byte 33 and GPU mask at byte 65. This preserves the existing SysV representation; the console offsets beyond the address, protection and size fields observed in Astro Bot's AGC calls are not hardware-measured. Size and offset assertions plus a raw-byte named-mapping regression guard the host ABI. - [app0.zar files](../../../core/libs/prx/libkernel/File/src/ArchivedFile.cpp) (libkernel) - `sceKernelOpen`, `open`, `sceKernelStat` and `sceKernelCheckReachability` resolve a path missing from `app0/` in `app0.zar`. An archived file is a read-only host null device whose reads, positioned and vectored reads, AIO reads, seeks, `fstat` and `FIONREAD` are served from the archive, with the position shared by `dup`, `dup2` and `F_DUPFD`; writes fail with `EBADF` as on a read-only descriptor, opening one for writing or truncation fails with `EROFS` and `O_CREAT | O_EXCL` with `EEXIST`. `fstat` and `sceKernelStat` report mode `0444` (`0555` for a directory), the node index as inode and the archive file's device and times. Opening an archived directory or a directory present both on disk and in the archive throws, as `getdents` would miss entries. On an archived descriptor `mmap`, `flock`, record locks, `fstatfs`, `ftruncate`, `fsync`, `fdatasync`, `getdirentries`, `fchmod` and `futimes` throw instead of acting on the null device; `mmap` is `noexcept`, so its refusal terminates the process. `lstat` and the other path calls on archived files are not implemented - [sceKernelLoadStartModule](../../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) loads a system library named by its console path, `/system/common/lib/.sprx` or the sandboxed `//common/lib/.sprx` that `sceKernelGetFsSandboxRandomWord` names, from the host `libs/.prx`, as libSceSysmodule loads modules by ID; a host library has no `module_start`, so `res` is 0. Other system directories, such as `/system/priv/lib`, are not mapped. The OpenOrbis piglet sample loads `libScePigletv2VSH.sprx` and `libScePrecompiledShaders.sprx` this way diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index 3eccf99c51..241d10f16f 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -7,13 +7,17 @@ - [Executable guest imports](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) - on Linux and macOS, a relocated executable import resolved to one guest provider requires that module and its dependencies at startup, even when it was discovered through `--module-dir` without a DT_NEEDED entry. Unreferenced symbols and no-op relocations do not require an otherwise deferred module. Console module-start ordering is unmeasured. - [Runtime-loaded modules](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) (`--module-dir`) can be mapped early for bindings. Linux defers their initialization to an explicit load of each module; standard SDK folders keep their mapping initializer, including through the browser conversion API. On Windows and macOS, a module another module needs is initialized at startup without start arguments; console dependency-start ordering is unmeasured. -- [Runtime-loaded modules the executable needs](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) (`--module-dir`) are mapped at startup on Windows and started when the title loads them with `sceKernelLoadStartModule`, with their start arguments, as a Unity player loads `mono-ps4.sprx`. A title that calls such a module before loading it finds its initializers not run; whether the console starts it earlier is not measured. A module the executable only imports from and the title never loads itself gets no `module_start` and no initializers at all under this deferral, and nothing reports it. macOS still starts it at startup. +- [Runtime-loaded modules the executable needs](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) (`--module-dir`) are mapped at startup on Windows and started when the title loads them with `sceKernelLoadStartModule`, with their start arguments, as a Unity player loads `mono-ps4.sprx`. A title that calls such a module before loading it finds its initializers not run; whether the console starts it earlier is not measured. A module the executable only imports from and the title never loads itself gets no `module_start` and no initializers at all under this deferral. The [entry stub](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp) prints `Deferred module ` at startup for each such module that an import of the executable names, whether or not the title loads it later; an import without a module name is not reported. macOS still starts it at startup. - The executable file that [relinker](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) generates opens a console when launched, unless it is relinked with `--windows-gui`, which selects the GUI subsystem. A GUI build started from Explorer has no standard output or error handle; the [entry stub](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp) gives each missing one `anyps5.log` beside the executable, replacing the previous run's file (#3282). Errors about the executable path or about waiting for the previous LoadExec process are written before the log is opened and are lost when the standard error handle is missing. If the log cannot be created, output to the missing handles is lost. - [Guest import registries](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) - declared host imports of bundled modules are recorded with their module name and relocation target, even when another guest module exports the same stripped NID. No-op relocations are omitted. Registries describe import bindings, not implementation correctness or runtime reachability. - Converted Windows main executables [omit `DYNAMIC_BASE`](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) by default, not only with `--windows-fixed-base`: on the console the executable lies inside the 40-bit window that the title's own `sceLibcMspaceCreate` checks (#3043). Relocations are kept, so if `0x140000000` is taken Windows relocates the image, possibly above that window. Verified only on Windows 10. - [SDK memory-management syscall traps](../../../core/relinker/relinker/src/analysis/SyscallScanner.cpp) under `--trap-sdk-syscall-stubs` recognize three exact instruction sequences from [ps5-payload-dev SDK libc/mman.c](https://github.com/ps5-payload-dev/sdk/blob/12cf3dda40/libc/mman.c), observed in Nuvio Maxd 0.5.15 (PPSA99288): syscall 74 and the two syscall 477 paths, with their argument preparation and carry-flag error handling. Matching requires a decoded instruction boundary and the complete sequence inside the scanned code range. Other SDK/compiler output remains rejected. The matched syscall becomes `UD2`; this does not implement mmap or mprotect and a call still faults. - The [instruction decoder](../../../core/relinker/codegen/src/x86/X64InstructionDecoder.cpp) reads a 16-bit displacement for `66 E8`, `66 E9` and `66 0F 80`-`8F` without REX.W, as AMD processors, the console included, do (binutils `objdump -M amd64`; REX.W selects the 32-bit form). AMD also truncates the target of every near relative branch with that prefix, rel8, LOOP and JRCXZ forms included, to 16 bits. Instruction metadata marks these branches as truncated and reports no branch target for them, and stubs refuse to move them. Compilers do not emit these forms. - Converted Linux main executables are [placed at `0x400000`](../../../core/relinker/relinker/src/pipeline/Conversion.cpp) by default, as `ET_EXEC` without ASLR, not only with `--linux-fixed-base`: the console executable lies inside the 40-bit window that the title's own `sceLibcMspaceCreate` checks (#3043). Where the console loads an executable that does not require an address is not measured; `0x400000` is the base that executables reading their GOT through `app0/imports.txt` assume (#4013). The relinker refuses an image that spans 2 GiB or more, so the executable ends below the guest arena at `0x200000000`. With `--lazy-binding` the executable stays position independent. +- `--frame-red-zone` frames only leaf functions that start with a chain of pushes of at least five bytes (`push rbp; mov rbp, rsp; push ...`, or pushes alone, which may be followed by `sub $n, %rsp` or `lea -n(%rsp), %rsp`) and leave through exits of at least five bytes that undo that allocation with `add` or `lea`, pop the same registers and `ret` ([framer](../../../core/relinker/codegen/src/x86/RedZoneFramer.cpp)). Leaves with a frame pointer that allocate, leaves with neither pushes nor an allocation, shorter prologues and exits, exits that free another size, and functions with indirect jumps or other RSP arithmetic keep their red zone. +- In a red-zone leaf without a frame pointer, `--frame-red-zone` lowers RSP after the prologue by the deepest displacement below RSP, rounded up to 16, and moves every RSP operand by the same amount ([framer](../../../core/relinker/codegen/src/x86/RedZoneFramer.cpp)). A leaf that takes the address of a slot below RSP, or uses an RSP operand with an index register, is lowered by the whole 128 bytes, since a pointer or an index can reach deeper than any displacement; values below RSP reached through any other register are not seen. Leaves with a frame pointer are lowered by 128 bytes. Inside a moved frame the CFA rules of the guest's unwind tables are off by the shift. +- An RSP operand that `--frame-red-zone` cannot move in place, an 8-bit displacement the shift takes past 0x7f or one without a displacement, moves to a stub with the instructions after it up to five bytes and is re-encoded there with the moved displacement ([framer](../../../core/relinker/codegen/src/x86/RedZoneFramer.cpp)). The leaf is skipped when that site would take in a branch target, a branch, ret or RIP-relative instruction, or another site, and when the operand is EVEX, whose 8-bit displacement is scaled. A game patch over a framed site or a moved displacement is refused. +- In `--frame-red-zone`, function bounds come from the executable's `.eh_frame` FDEs: each function is decoded from its FDE start to its FDE end, or to int3 padding before it, and the relinker stops with an error when the executable has none. Code outside every FDE, such as a function without unwind information, is not framed ([framer](../../../core/relinker/codegen/src/x86/RedZoneFramer.cpp)). - `--to-intel` lowers MCOMMIT to STC plus NOP padding and RDPRU through a runtime-checked stub in the [matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp): AMD APM Vol. 3 defines RDPRU ECX 0 as MPERF and ECX 1 as APERF, neither readable on Intel hosts, so both selectors return the host TSC as a disclosed monotonic-counter approximation (their ratio always reads 1.0) while other selectors yield zero EDX:EAX. STC asserts CF=1 because under x86-TSO prior stores are already ordered and guest memory is host-DRAM-backed, leaving the commit-success case as the only observable one. Non-zero RDPRU selectors, the TSC-for-MPERF/APERF substitution, and any console path reporting MCOMMIT CF=0 are unverified. - [Unscoped imports on Windows](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp) - an import without a module that no searched library exports under its name is searched again under the NID of that name, because homebrew built with the [ps5-payload-dev SDK](https://github.com/ps5-payload-dev/sdk) imports by symbol name (Quake, from the websrv homebrew launcher; Mari0, #6239) while the libraries in `libs/` export NIDs. A dependency named with `.sprx`, `.suprx` or `.debug_prx` loads the `.prx` library of the same stem from `libs/`, and `libkernel_web` also loads `libkernel.prx`, as `libSceLibcInternal` already loads `libc.prx`: AnyPS5's `libkernel_web.prx` exports only two functions, where the console's `libkernel_web.sprx` is a libkernel variant. A name that matches a library's export as written still binds to that export first. macOS resolves these imports through the host loader by name and is unchanged. - [Unscoped imports on Linux](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) - in an executable or bundled module that names a dependency with `.sprx`, `.suprx` or `.debug_prx`, as homebrew built with the [ps5-payload-dev SDK](https://github.com/ps5-payload-dev/sdk) does, an import without a module that is named by symbol is bound to the NID of that name only. Unlike Windows, the name as written is not tried first, because the host loader also searches the host C library, which exports `malloc` or `pthread_create` under their names. A name that a bundled module exports still binds to that module, as does a bundled module's import of a name the executable exports, and such a dependency loads the `.prx` library of the same stem. Unqualified imports of other executables and bundled modules are still bound by name. diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 134b3b4632..db3130a1f6 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -117,7 +117,7 @@ - [`StoreTexel`](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) writes channel 1 of a two-channel format from the data slot whose DST_SEL is Y, for SINT and UINT alike. With DST_SEL X,Y,Z,W, gfx1035 writes it from the W slot, as adriadam10 measured in #944. - [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. - [v_div_fixup_f64](../../../core/shader/recompiler/Translation/src/Float64Instructions.cpp) flushes its src1 and src2 to a zero of the same sign when bit 0 of the `FLOAT_MODE` f16/f64 denormal field is clear (0 and 2) and keeps a denormal src0 and result in every mode, as `v_div_fixup_f32` does with the f32 field. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) over 1728 triples of 12 f64 values (zeros, denormals, the minimum normal, ±1, the largest finite value, infinity and quiet and signaling NaNs) at IEEE=0 and IEEE=1 with denorm16 0..3. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. -- [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_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. Denormal mode 2 flushes only the sources, and mode 1 only the result: a result below 2^-126 after rounding becomes a zero of its sign, also when a preserved denormal source produced it. Measured on RDNA2 (gfx1030) over 13,824 operand sets, and 1,632 with an inline or literal constant factor, in all four modes at IEEE=0 and IEEE=1 with f32 round to nearest even. Modes 1 and 2 throw when the module cannot declare `DenormPreserve 32` (no `shaderDenormPreserveFloat32`, or the hardware f32 flush in use), and mode 1 also with another f32 rounding. - [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_mad_f32, v_mac_f32, v_madak_f32 and v_madmk_f32](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (gfx10.1 only) leave f32 denormals and NaN bits to the driver, unlike the console. Measured on gfx1010 (RX 5700 XT, #5782): the product is rounded before the add; f32 sources, product and result are flushed to a zero of the same sign in every f32 denormal mode; the product is flushed when tiny after rounding to 24 bits with an unbounded exponent; NaN results follow the fma rule, an invalid product beating a NaN src2; in round mode 2 a sum of zeros of opposite signs is -0. Translating this cost 15× `main` on a `v_mac_f32` chain on the RX 5700 XT and 23× on an RTX 3070, and a never-taken branch around it 6×, so it waits for the SPIR-V float controls of #981. - [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 arithmetic of v_interp_p2_f16, in f64 or, without the Float64 capability, in 64-bit integers. 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 throws with any f16 rounding but nearest even. `FP16_OVFL` set is not measured. @@ -132,6 +132,7 @@ - [v_ldexp_f32 in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the `FLOAT_MODE` f32 denormal field: mode 1 keeps a subnormal input and flushes a result below 2^-126 to a zero of its sign in every f32 round mode, also one that rounds to 2^-126 at subnormal precision (`0x3fffffff` scaled by 2^-127); mode 2 flushes a subnormal input to a zero of its sign and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, as mode 3 does. Measured on RDNA2 (gfx1030, Radeon RX 6800, AMD Windows driver 32.0.21045.5002 through HIP) over 10,260 rows (54 inputs of both signs: zeros, subnormals, normals near 2^-126 and 1.0, the largest finite value, infinities and NaNs, each scaled by 190 exponents from -2^31 to 2^31-1) at IEEE=0 and IEEE=1 with each f32 denormal mode 0 to 3 and f32 round mode 0 to 3, DX10_CLAMP=1, f16 denormals kept, f16 round to nearest even and FP16_OVFL=0; with the `clamp` modifier the result is clamped as in the other modes. The PS5's gfx1013 is not measured. - [32-bit denormal execution mode](../../../core/shader/recompiler/Recompiler.cpp) (shader recompiler) - the hardware f32 flush that the startup probe chooses (`DenormFlushToZero 32`) is declared only in shaders whose `FLOAT_MODE` flushes f32 denormals (field 0, or no known float mode). A shader that keeps them on either side (field 1, 2 or 3) declares `DenormPreserve 32` when the device has `shaderDenormPreserveFloat32` with a `denormBehaviorIndependence` other than NONE; on other devices it gets the driver's default f32 denormal handling, which flushes the denormals it keeps on MoltenVK. - [v_log_f32 and v_log_f16](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) keep the driver's log2 unless, for inputs within 1/8 of 1.0, it is more than 4 ULP from an eight-term series in x - 1, which then replaces it. `v_log_f16` goes through the same f32 lowering and is rounded to half. The series is evaluated for every `v_log` and chosen with `OpSelect` rather than a branch, so its seven fused multiply-adds and two multiplies are an unconditional cost. Measured on RDNA2 over all 3,145,728 f32 inputs within 1/8 of 1.0 (gfx1035, IEEE 0, f32 denormals flushed), the hardware is within 1 ULP of the correctly rounded log2 and the series within 2 ULP of it and within 3 ULP of the hardware (2 except at 0x3f752e47, 0x3f7baab0 and 0x3f7bc4fb), so a kept driver value is within 7 ULP of the hardware and a driver that returns the hardware's bits never triggers the gate; RADV (Mesa 26.2.4) returns the hardware's bits on the 9,280 such inputs measured on gfx1036; NVIDIA (615.71.09) was up to 23,947,836 ULP off at 1 - 2^-24 and is within 6 ULP inside the band after the change; outside it its own log2 stays up to 9 ULP off (measured up to 1.375). Over every f16 input, the change moves one `v_log_f16` result on NVIDIA (0x3bed, 1 ULP, onto the hardware's bits) and none on RADV. +- [v_add_f32, v_sub_f32, v_subrev_f32, v_mul_f32 and v_mul_legacy_f32 in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/VectorInstructions.cpp) throw with a directed f32 round mode, as the host rounds these sums and products to nearest even (a difference that modes 0 and 3 also have then), and when the module cannot declare `DenormPreserve 32` (no `shaderDenormPreserveFloat32`, or the hardware f32 flush in use); the PS5's gfx1013 is not measured. Otherwise mode 1 keeps denormal inputs and flushes a result below 2^-126 to a zero of its sign (a product only when it stays below 2^-126 after rounding to 24 bits with an unbounded exponent), and mode 2 flushes denormal inputs and keeps a denormal result. Measured on RDNA2 (gfx1030, RX 6800) over 1,296 operand pairs, 896 products around 2^-126 and 1,632 with a constant factor, at IEEE 0 and 1 in every f32 denormal mode, with DX10_CLAMP=1, f16 denormals kept and FP16_OVFL=0. - [Ordered not-equal, less-than and greater-than compares](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) (shader recompiler) - `FPOrdNotEqual32` is emitted as `OpFUnordNotEqual`, and `FPOrdLessThan32` and `FPOrdGreaterThan32` as their ordered forms, each with both operands checked for NaN on their bits; the results are the same for every input. The AMD Windows driver 32.0.13058.8001 (Radeon 860M, RDNA 3.5) returns 1 for a NaN operand when any float form of not-equal feeds a ballot on a 32-wide subgroup (#825), and so does a less-than or greater-than against an infinity the driver sees as a constant (#3536), presumably rewritten into a not-equal by its compiler - [16-bit integer multiplies](../../../core/shader/recompiler/Translation/src/IntegerInstructions.cpp) (`v_mul_lo_u16`, `v_pk_mul_lo_u16`) (shader recompiler) - a factor taken from the low half of its source is read as the low 24 bits of the register, not the low 16; the low 16 bits of the product are the same for every input. The AMD Windows driver 32.0.21045.5002 (RX 6700 XT, RDNA 2) stores the full 32-bit product when two values it knows fit in 16 bits are multiplied, the product is reduced to 16 bits and nothing else is merged into the stored value: `v_mul_lo_u16` into a register whose high half is zero, also with factors masked to 16 bits by the program, and `v_pk_mul_lo_u16` whose high lanes are zero wrote 0xfffe0001 for 0xffff * 0xffff where the GPU writes 0x00000001. Reducing the product with `v_and_b32`, `v_bfe_u32` or a shift pair loses the same bits; factors read as 24-bit fields do not. The [constant folder](../../../core/shader/recompiler/Optimization/src/ConstantFolder.cpp) keeps a field read that a multiply uses, even when the program already masked the factor to 16 bits, since folding it away brings the full product back (#8271). Measured against the same instructions run through the [hardware oracle](../HW_ORACLE.md) on that GPU; other drivers are not checked - [f32 compares in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/VectorInstructions.cpp) (`v_cmp_X_f32` and `v_cmpx_X_f32` with X = eq, ge, gt, le, lg, lt, neq, nge, ngt, nle, nlg, nlt) flush their f32 denormal inputs by bit 0 of the `FLOAT_MODE` f32 denormal field: mode 1 keeps them as mode 3 does, and mode 2 flushes them as mode 0 does. Measured on RDNA2 (gfx1030, Radeon RX 6800, AMD Windows driver 32.0.21045.5002 through HIP) over 484 operand pairs of zeros, denormals, minimum normals, ones, the largest finite value, infinities and quiet and signaling NaNs of both signs, in VOPC form, at IEEE=0 and IEEE=1 with each f32 denormal mode 0 to 3, f32 round to nearest even, DX10_CLAMP=1, f16 denormals kept, f16 round to nearest even and FP16_OVFL=0. The PS5's gfx1013 is not measured. diff --git a/docs/user/INPUT_MAPPING.md b/docs/user/INPUT_MAPPING.md index 7fd7a8d16d..98970b5163 100644 --- a/docs/user/INPUT_MAPPING.md +++ b/docs/user/INPUT_MAPPING.md @@ -1,6 +1,6 @@ # Controller, keyboard and mouse input -AnyPS5 accepts SDL-mapped game controllers as well as its built-in keyboard and mouse bindings. Xbox-style controllers and PlayStation controllers recognized by SDL use the standard PS button layout; sticks and analog triggers are passed through, and controllers can be connected or disconnected while the game is running. The first recognized controller is used. AnyPS5 uses its built-in keyboard and mouse bindings when no configuration file is present. To change selected keyboard and mouse bindings, create `anyps5-input.ini` beside the generated game executable. Set `ANYPS5_INPUT_CONFIG` to use a file at another path. Both the game directory and that path may contain any characters, such as `ü` or `日本`. +AnyPS5 accepts SDL-mapped game controllers as well as its built-in keyboard and mouse bindings. Up to 4 controllers are supported simultaneously. The first connected controller is assigned to Player 1 and composed with keyboard and mouse input; additional controllers are sequentially assigned to Players 2, 3, and 4 (matching `libSceUserService` users). Xbox-style controllers and PlayStation controllers recognized by SDL use the standard PS button layout; sticks and analog triggers are passed through, and controllers can be connected or disconnected dynamically while the game is running. AnyPS5 uses its built-in keyboard and mouse bindings when no configuration file is present. To change selected keyboard and mouse bindings, create `anyps5-input.ini` beside the generated game executable. Set `ANYPS5_INPUT_CONFIG` to use a file at another path. Both the game directory and that path may contain any characters, such as `ü` or `日本`. For an Xbox-layout controller, A/B/X/Y map to Cross/Circle/Square/Triangle, LB/RB map to L1/R1, and LT/RT map to analog L2/R2. Start maps to Options. D-pad and stick-click buttons are supported. On a PlayStation controller, touchpad clicks and the positions of up to two fingers on the touchpad are forwarded. On other controllers, View (Back) presses the touchpad at its center. diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 4b643c8a95..3a4dc187ee 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -87,6 +87,7 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` | `--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 and the recognized inline mmap/mprotect sequences from SDK libc/mman.c 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. | +| `--frame-red-zone` | Windows only: give leaf functions that keep values below RSP (the System V red zone) a frame, so an exception Windows delivers on a guest thread cannot overwrite them. Covers leaves with a `push rbp; mov rbp, rsp; push ...` prologue and leaves that only push registers, with or without a `sub` allocation after the pushes; in those, RSP is lowered only as far as the leaf reaches below it and their RSP operands are moved with it, in place or, when their encoding cannot hold the moved displacement, in a stub. Function bounds come from the executable's `.eh_frame`, and the relinker stops with an error when it has none. The relinker prints how many it framed and why it skipped the others, and refuses a game patch over a framed site. | | `--lazy-binding` | Deprecated. Enable lazy symbol binding instead of eager binding. Incompatible with bundled ELF modules. The Linux executable stays position independent (`ET_DYN`) instead of being placed at `0x400000`. | Specify `unused-filter=0|1|2` without `--`, at most once. Unknown options and extra positional arguments are errors. Use `relinker --help` to print the usage syntax to stdout and exit with code `0` without converting files. Invoking `relinker` without arguments prints the usage syntax to stderr and exits with code `1`.