diff --git a/core/Decoder/Jpeg/src/Jpeg.cpp b/core/Decoder/Jpeg/src/Jpeg.cpp index d446841cc44c..7bd331ba9166 100644 --- a/core/Decoder/Jpeg/src/Jpeg.cpp +++ b/core/Decoder/Jpeg/src/Jpeg.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #define STB_IMAGE_STATIC @@ -97,12 +98,12 @@ std::optional Decode(std::span jpeg) { int width = 0; int height = 0; int channels = 0; - stbi_uc* decoded = stbi_load_from_memory(jpeg.data(), static_cast(jpeg.size()), &width, &height, &channels, 0); + const std::unique_ptr decoded( + stbi_load_from_memory(jpeg.data(), static_cast(jpeg.size()), &width, &height, &channels, 0), stbi_image_free); if (!decoded) return std::nullopt; Image image{static_cast(width), static_cast(height), static_cast(channels), {}}; - image.pixels.assign(decoded, decoded + static_cast(image.width) * image.height * image.channels); - stbi_image_free(decoded); + image.pixels.assign(decoded.get(), decoded.get() + static_cast(image.width) * image.height * image.channels); return image; } diff --git a/core/Decoder/Png/src/Png.cpp b/core/Decoder/Png/src/Png.cpp index cb04f25bd5f5..434a589d6cc3 100644 --- a/core/Decoder/Png/src/Png.cpp +++ b/core/Decoder/Png/src/Png.cpp @@ -152,12 +152,12 @@ std::optional Decode(std::span png) { int width = 0; int height = 0; int channels = 0; - stbi_uc* decoded = stbi_load_from_memory(png.data(), static_cast(png.size()), &width, &height, &channels, 4); + const std::unique_ptr decoded( + stbi_load_from_memory(png.data(), static_cast(png.size()), &width, &height, &channels, 4), stbi_image_free); if (!decoded) return std::nullopt; Image image{static_cast(width), static_cast(height), {}}; - image.pixels.assign(decoded, decoded + static_cast(image.width) * image.height * 4); - stbi_image_free(decoded); + image.pixels.assign(decoded.get(), decoded.get() + static_cast(image.width) * image.height * 4); return image; } diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 820ab8dcc8e8..b3973871e690 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -295,6 +295,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_game_live_streaming_tests tests/GuestGameLiveStreaming.cpp) + target_include_directories(guest_game_live_streaming_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_game_live_streaming_tests PRIVATE libSceGameLiveStreaming libc) + configure_windows_unwind(guest_game_live_streaming_tests) + add_test(NAME guest_game_live_streaming COMMAND guest_game_live_streaming_tests) + if(WIN32) + set_tests_properties(guest_game_live_streaming PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_netctl_ap_dialog_tests tests/GuestNetCtlApDialog.cpp) target_include_directories(guest_netctl_ap_dialog_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_netctl_ap_dialog_tests PRIVATE libSceNetCtlApDialog libc) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index e45ab4f8084b..919c4c8ad820 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1334,6 +1334,12 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() set_tests_properties(agc_driver_provoking_vertex PROPERTIES TIMEOUT 30) + add_test(NAME agc_driver_dword_pattern_fill_translated COMMAND agc_driver_dword_pattern_fill_tests) + set_tests_properties(agc_driver_dword_pattern_fill_translated PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_FILL_HLE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_driver_dword_pattern_fill_translated PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_uint8x4_color_target_no_resident COMMAND agc_driver_uint8x4_color_target_tests) set_tests_properties(agc_driver_uint8x4_color_target_no_resident PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_RESIDENT_TARGETS=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) if(WIN32) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp index e9d49f7f5102..e1f2174566d0 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp @@ -22,6 +22,27 @@ bool matchesBufferPatternFillKernel(std::span code) { return code.size() >= kernel.size() && std::equal(kernel.begin(), kernel.end(), code.begin()); } +bool matchesDwordPatternFillKernel(std::span code) { + static constexpr std::array kernel{ + 0xbfa00003u, 0xd7460002u, 0x04010c0au, 0x7da80408u, 0xbf88003fu, 0x7e000c09u, 0xbf070980u, 0x858a807eu, + 0x7e005700u, 0x100000ffu, 0x4f800000u, 0x7e060f00u, 0xd5766a00u, 0x02020609u, 0x7d8a0280u, 0x4c020080u, + 0x02000101u, 0xd56a0001u, 0x00020700u, 0x4c000303u, 0x4a020303u, 0x02000101u, 0xd56a0000u, 0x00020500u, + 0xd5690001u, 0x00020009u, 0x4c060302u, 0x7d860609u, 0x7d8c02f9u, 0x06068c02u, 0x87ea6a0cu, 0x50000080u, + 0xd5286a00u, 0x003200c1u, 0xd5010000u, 0x002a00c1u, 0xd5690000u, 0x00020009u, 0x4c000102u, 0x7d0a0080u, + 0xbe88246au, 0xbf880015u, 0x7d0a0081u, 0xbe8a246au, 0xbf88000cu, 0x7d0a0082u, 0xbeea246au, 0xbf880003u, + 0x7e000207u, 0xe0102000u, 0x80000002u, 0x8afe7e6au, 0xbf880003u, 0x7e000206u, 0xe0102000u, 0x80000002u, + 0xbefe046au, 0x8afe7e0au, 0xbf880003u, 0x7e000205u, 0xe0102000u, 0x80000002u, 0xbefe040au, 0x8afe7e08u, + 0xbf880003u, 0x7e000204u, 0xe0102000u, 0x80000002u, 0xbf810000u}; + static constexpr std::size_t comparesAt = 27; + static constexpr std::array comparesSwapped{0x7d8c02f9u, 0x06068c02u, 0x7d860609u}; + if (code.size() < kernel.size()) return false; + const auto before = std::span(kernel).first(comparesAt); + const auto after = std::span(kernel).subspan(comparesAt + comparesSwapped.size()); + const auto compares = code.subspan(comparesAt, comparesSwapped.size()); + if (!std::equal(before.begin(), before.end(), code.begin()) || !std::equal(after.begin(), after.end(), code.begin() + comparesAt + comparesSwapped.size())) return false; + return std::equal(compares.begin(), compares.end(), kernel.begin() + comparesAt) || std::equal(compares.begin(), compares.end(), comparesSwapped.begin()); +} + } bool Driver::matchesFillKernel(std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute) { @@ -44,17 +65,7 @@ bool Driver::matchesFillKernel(std::span code, const std::v std::optional MatchDwordPatternFill(std::span packet, std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute) { static const bool enabled = std::getenv("APS5_NO_FILL_HLE") == nullptr; if (!enabled || userData.size() != 10 || packet.size() < 5 || !compute.groupIdEnable[0] || compute.numThreads[0] != 64 || compute.numThreads[1] != 1 || compute.numThreads[2] != 1) return std::nullopt; - static constexpr std::array kernel{ - 0xbfa00003u, 0xd7460002u, 0x04010c0au, 0x7da80408u, 0xbf88003fu, 0x7e000c09u, 0xbf070980u, 0x858a807eu, - 0x7e005700u, 0x100000ffu, 0x4f800000u, 0x7e060f00u, 0xd5766a00u, 0x02020609u, 0x7d8a0280u, 0x4c020080u, - 0x02000101u, 0xd56a0001u, 0x00020700u, 0x4c000303u, 0x4a020303u, 0x02000101u, 0xd56a0000u, 0x00020500u, - 0xd5690001u, 0x00020009u, 0x4c060302u, 0x7d860609u, 0x7d8c02f9u, 0x06068c02u, 0x87ea6a0cu, 0x50000080u, - 0xd5286a00u, 0x003200c1u, 0xd5010000u, 0x002a00c1u, 0xd5690000u, 0x00020009u, 0x4c000102u, 0x7d0a0080u, - 0xbe88246au, 0xbf880015u, 0x7d0a0081u, 0xbe8a246au, 0xbf88000cu, 0x7d0a0082u, 0xbeea246au, 0xbf880003u, - 0x7e000207u, 0xe0102000u, 0x80000002u, 0x8afe7e6au, 0xbf880003u, 0x7e000206u, 0xe0102000u, 0x80000002u, - 0xbefe046au, 0x8afe7e0au, 0xbf880003u, 0x7e000205u, 0xe0102000u, 0x80000002u, 0xbefe040au, 0x8afe7e08u, - 0xbf880003u, 0x7e000204u, 0xe0102000u, 0x80000002u, 0xbf810000u}; - if (code.size() < kernel.size() || !std::equal(kernel.begin(), kernel.end(), code.begin())) return std::nullopt; + if (!matchesDwordPatternFillKernel(code)) return std::nullopt; const auto stride = (userData[1] >> 16u) & 0x3fffu; const bool swizzled = ((userData[1] >> 31u) & 1u) != 0; const bool addTid = ((userData[3] >> 23u) & 1u) != 0; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp index 69453c18b326..a16611f653b1 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -32,6 +32,14 @@ bool keepsDrawPipeline(std::span packet, std::uint32_t head case 0x15: return orderedFills(); case 0x63: case 0x64: case 0x9f: return packet.size() >= 5 && !DrawPipeline::Queue0().Overlaps(packet[1] | (static_cast(packet[2]) << 32u), static_cast(packet[4]) * 8); + case 0x3c: case 0x93: { + if (packet.size() < 5) return false; + const std::uint64_t awaited = packet[2] | (static_cast(packet[3]) << 32u); + const auto bytes = Pm4::WaitAwaitedBytes(packet); + if (DrawPipeline::Queue0().Overlaps(awaited, bytes)) return false; + GuestMemory::CheckRange(reinterpret_cast(awaited), bytes, bytes); + return Pm4::WaitSatisfiedUnchecked(packet); + } case 0x10: { const auto operation = (header >> 2u) & 0x3fu; return operation == 0 || operation == 0x09 || operation == 0x0b || operation == 0x0c || operation == 0x1a; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/WorkerAffinity.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/WorkerAffinity.cpp index d9d04c893aa2..d214c5a03939 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/WorkerAffinity.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/WorkerAffinity.cpp @@ -10,9 +10,6 @@ std::uint64_t WorkerAffinityMask() { if (std::getenv("APS5_NO_WORKER_AFFINITY") != nullptr) return std::uint64_t{0}; const auto requested = CpuTopology::MaskFromEnvironment("APS5_WORKER_AFFINITY_MASK"); if (requested != 0) return requested; -#ifndef __linux__ - if (std::getenv("APS5_WORKER_AFFINITY") == nullptr) return std::uint64_t{0}; -#endif const auto& layout = CpuTopology::Get(); return layout.hybrid ? layout.performant : std::uint64_t{0}; }(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index beb0572c5e66..ac7ad7a05a43 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -88,7 +88,7 @@ DepthSamplingState prepareDepthSampling(const Context& context, const DepthTarge if ((target.pipeBankXor & ~0xcf00u) != 0u) return refuse("unsupported native multisample depth XOR"); if (target.address == 0u || (target.address & 65535u) != 0u) return refuse("native multisample depth base is not aligned"); if (target.stencilAddress != 0u || target.htileStencil) return refuse("native multisample stencil import is not implemented"); - if (target.htileAddress == 0u || (target.htileAddress & 255u) != 0u || !target.iterate256 || !target.htilePipeAligned || target.htileVrsEncoding != 0u) return refuse("native multisample depth initial import requires pipe-aligned ITERATE_256 HTILE without VRS"); + if (target.htileAddress == 0u || (target.htileAddress & 255u) != 0u || !target.htilePipeAligned || target.htileVrsEncoding != 0u) return refuse("native multisample depth initial import requires pipe-aligned HTILE without VRS"); if (target.clearDepth != 0.0f && target.clearDepth != 1.0f) return refuse("native multisample depth initial clear value is not implemented"); state.initial = {{{target.address, static_cast(MultisampledDepthBytes_nid_no_patch(target.extent, target.format == VK_FORMAT_D16_UNORM ? 2u : 4u, target.samples)), 0u}, {target.htileAddress, static_cast(HtileMetadataBytes(target.extent)), 0u}}}; for (const auto& range : state.initial) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 75ef48946975..19951be930dc 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -339,7 +339,7 @@ bool materializeCmaskClear(const Context& context, const ColorTarget& color, con return filled; } -void imageBarrier(const Context& context, VkCommandBuffer commands, VkImage image, VkImageLayout oldLayout, VkImageLayout newLayout, VkPipelineStageFlags sourceStage, VkPipelineStageFlags destinationStage, VkAccessFlags sourceAccess, VkAccessFlags destinationAccess) { +void imageBarrier(const Context& context, VkCommandBuffer commands, VkImage image, VkImageLayout oldLayout, VkImageLayout newLayout, VkPipelineStageFlags sourceStage, VkPipelineStageFlags destinationStage, VkAccessFlags sourceAccess, VkAccessFlags destinationAccess, VkImageSubresourceRange subresources = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}) { VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; barrier.srcAccessMask = sourceAccess; barrier.dstAccessMask = destinationAccess; @@ -348,7 +348,7 @@ void imageBarrier(const Context& context, VkCommandBuffer commands, VkImage imag barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.image = image; - barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + barrier.subresourceRange = subresources; context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, sourceStage, destinationStage, 0, 0, nullptr, 0, nullptr, 1, &barrier); } @@ -2263,7 +2263,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& continue; } if (binding.resident != nullptr) { - imageBarrier(context, commands, binding.resident->Image(), VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT); + imageBarrier(context, commands, binding.resident->Image(), VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, {VK_IMAGE_ASPECT_COLOR_BIT, binding.color.mip, 1, binding.resident->ImageDepth() == 1 ? binding.color.arraySlice : 0, binding.resident->ImageDepth() == 1 ? binding.color.layers : 1}); countBarrier(); continue; } @@ -2312,7 +2312,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& continue; } if (binding.resident != nullptr) { - imageBarrier(context, commands, binding.resident->Image(), VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT); + imageBarrier(context, commands, binding.resident->Image(), VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT, {VK_IMAGE_ASPECT_COLOR_BIT, binding.color.mip, 1, binding.resident->ImageDepth() == 1 ? binding.color.arraySlice : 0, binding.resident->ImageDepth() == 1 ? binding.color.layers : 1}); countBarrier(); continue; } diff --git a/core/libs/prx/libSceAgcDriver/Submit/include/Dcb.hpp b/core/libs/prx/libSceAgcDriver/Submit/include/Dcb.hpp index 03cbe0bb98a2..437bb24e0566 100644 --- a/core/libs/prx/libSceAgcDriver/Submit/include/Dcb.hpp +++ b/core/libs/prx/libSceAgcDriver/Submit/include/Dcb.hpp @@ -2,6 +2,7 @@ #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_SUBMIT_INCLUDE_DCB_HPP #include "SceTypes.hpp" +#include namespace AgcDriver { @@ -9,6 +10,9 @@ int SubmitMany(std::uint32_t* const* addresses, const std::uint32_t* sizes, std: } +extern "C" std::uint32_t APS5_VABI sceAgcDriverGetWaitRenderingPacketSizeInDwords(); +extern "C" std::uint32_t APS5_VABI sceAgcDriverGetSetFlipPacketSizeInDwords(); + extern "C" int APS5_VABI sceAgcDriverSubmitDcb(const Packet* packet); extern "C" int APS5_VABI sceAgcDriverAgrSubmitDcb(const Packet* packet); extern "C" int APS5_VABI sceAgcDriverSubmitMultiDcbs(std::uint32_t* const* dcbGpuAddrs, const std::uint32_t* dcbSizesInDwords, std::uint32_t count); diff --git a/core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp b/core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp index deff007c42c8..4f1e620abb8e 100644 --- a/core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp +++ b/core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp @@ -29,6 +29,10 @@ std::uint32_t APS5_VABI sceAgcDriverGetWaitRenderingPacketSizeInDwords() { return AgcDriver::RenderingWaitPacketWords; } +std::uint32_t APS5_VABI sceAgcDriverGetSetFlipPacketSizeInDwords() { + return AgcDriver::FlipPacketWords; +} + std::uint32_t APS5_VABI sceAgcDriverWaitUntilSafeForRendering(std::uint32_t** command, std::uint32_t capacity, std::uint32_t mode, std::uint32_t handle, int index) { if (!command || capacity < AgcDriver::RenderingWaitPacketWords || mode != 0 || index < 0) diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp index cf54e315dd72..3b43e74d6fd9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp @@ -2,6 +2,9 @@ #include "ControlFlow/RequestSerializer.hpp" #include "SpirvBackend/SpirvBda.hpp" #include +#include +#include +#include namespace { @@ -15,6 +18,17 @@ void reject(TAction action, const char* expected) { throw std::runtime_error(std::string("missing contract rejection: ") + expected); } +std::uint32_t CallSites(const std::vector& spirv, const char* name) { + std::uint32_t function = 0; + std::uint32_t calls = 0; + for (std::size_t at = 5; at < spirv.size() && (spirv[at] >> 16u) != 0u; at += spirv[at] >> 16u) { + const auto opcode = spirv[at] & 0xffffu; + if (opcode == spv::OpName && std::strcmp(reinterpret_cast(&spirv[at + 2u]), name) == 0) function = spirv[at + 1u]; + if (opcode == spv::OpFunctionCall && function != 0u && spirv[at + 3u] == function) ++calls; + } + return calls; +} + } void RunBdaContractTests() { @@ -56,4 +70,10 @@ void RunBdaContractTests() { invalid[0] = invalid[0] == 'A' ? 'B' : 'A'; reject([&] { static_cast(serializer.Deserialize(invalid)); }, "request signature"); reject([&] { static_cast(serializer.Deserialize("NVNQQWMAAAA=")); }, "serialization version"); + for (const std::uint32_t bits : {8u, 16u, 32u}) { + const auto shader = MakeBdaWriteTestShader(0x7fff1235fffdULL, 0xa1b2c3d4u, bits, false); + const auto lookups = CallSites(shader, "get_bda_write_pointer"); + const auto loops = CallSites(shader, "write_bda_bytes"); + AgcDriver::Graphics::Require(loops == 1u && lookups == (bits == 32u ? 2u : 1u), "a runtime V# write of " + std::to_string(bits) + " bits has " + std::to_string(lookups) + " write lookups and " + std::to_string(loops) + " byte-loop calls, not its byte path through write_bda_bytes"); + } } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DwordPatternFill.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DwordPatternFill.cpp index 1629b1645863..f7a18fada012 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DwordPatternFill.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DwordPatternFill.cpp @@ -26,6 +26,13 @@ alignas(256) constexpr std::array Code{ 0x7e000207u, 0xe0102000u, 0x80000002u, 0x8afe7e6au, 0xbf880003u, 0x7e000206u, 0xe0102000u, 0x80000002u, 0xbefe046au, 0x8afe7e0au, 0xbf880003u, 0x7e000205u, 0xe0102000u, 0x80000002u, 0xbefe040au, 0x8afe7e08u, 0xbf880003u, 0x7e000204u, 0xe0102000u, 0x80000002u, 0xbf810000u}; +constexpr std::size_t ComparesAt = 27; +constexpr std::array WithCompares(std::array compares) { + auto code = Code; + for (std::size_t word = 0; word < compares.size(); ++word) code[ComparesAt + word] = compares[word]; + return code; +} +alignas(256) constexpr std::array SwappedCode = WithCompares({0x7d8c02f9u, 0x06068c02u, 0x7d860609u}); constexpr std::array Pattern{0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u}; constexpr std::uint32_t Untouched = 0xcafebabeu; alignas(256) std::array Output{}; @@ -35,6 +42,35 @@ std::vector UserData(void* output, std::uint32_t records, std::ui return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (4u << 16u), records, format, Pattern[0], Pattern[1], Pattern[2], Pattern[3], count, period}; } +void SwappedComparesTests() { + const ShaderRecompiler::ShaderComputeStageInfo compute{{64, 1, 1}, 0, {true, false, false}, false, 1}; + const std::array packet{0xc0031500u, 4, 1, 1, 0x41u}; + for (const auto period : {1u, 2u, 4u}) { + const auto data = UserData(Output.data(), 1024, 256, period); + const auto listed = MatchDwordPatternFill(packet, Code, data, compute); + const auto swapped = MatchDwordPatternFill(packet, SwappedCode, data, compute); + Require(listed && swapped && swapped->base == listed->base && swapped->bytes == listed->bytes && swapped->pattern == listed->pattern, "the kernel with its two compares in the other order was not recognized as the same fill"); + } + const auto data = UserData(Output.data(), 1024, 256, 4); + for (std::size_t word = 0; word < SwappedCode.size(); ++word) { + auto altered = SwappedCode; + altered[word] ^= 1u; + Require(!MatchDwordPatternFill(packet, altered, data, compute), "a different shader matched the dword pattern-fill kernel with swapped compares"); + } + Require(!MatchDwordPatternFill(packet, std::span(SwappedCode).first(68), data, compute), "a truncated shader with swapped compares matched"); + constexpr std::array, 5> otherOrders{{ + {0x7d8c02f9u, 0x7d860609u, 0x06068c02u}, + {0x06068c02u, 0x7d8c02f9u, 0x7d860609u}, + {0x7d860609u, 0x06068c02u, 0x7d8c02f9u}, + {0x7d860609u, 0x7d860609u, 0x7d8c02f9u}, + {0x7d8c02f9u, 0x06068c02u, 0x7d8c02f9u}, + }}; + for (const auto& order : otherOrders) { + const auto code = WithCompares(order); + Require(!MatchDwordPatternFill(packet, code, data, compute), "a kernel whose compare words are not one of the two orders matched"); + } +} + void MatchTests() { const ShaderRecompiler::ShaderComputeStageInfo compute{{64, 1, 1}, 0, {true, false, false}, false, 1}; const std::array packet{0xc0031500u, 4, 1, 1, 0x41u}; @@ -74,7 +110,7 @@ void MatchTests() { Require(clamped && clamped->bytes == 256, "the fill exceeded the descriptor's records"); } -std::vector Commands(const std::vector& data, std::uint32_t groups, bool partial, std::uint32_t threads) { +std::vector Commands(std::span code, const std::vector& data, std::uint32_t groups, bool partial, std::uint32_t threads) { std::vector words; const auto registers = [&](std::uint32_t first, std::span values) { words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); @@ -83,7 +119,7 @@ std::vector Commands(const std::vector& data, std: }; const std::array shape{64, 1, 1}; registers(0x207, shape); - const auto address = reinterpret_cast(Code.data()); + const auto address = reinterpret_cast(code.data()); const std::array program{static_cast(address >> 8u), static_cast(address >> 40u)}; registers(0x20c, program); const std::array resource{(static_cast(data.size()) << 1u) | (1u << 7u)}; @@ -111,18 +147,20 @@ void ExecuteTests() { {4, 1024, 256, 5, false, 0}, {0, 1024, 256, 4, true, 70}, }}; - for (const auto& item : cases) { - Output.fill(Untouched); - const auto words = Commands(UserData(Output.data(), item.records, item.count, item.period), item.groups, item.partial, item.threads); - Packet packet{const_cast(words.data()), static_cast(words.size()), 0, {}}; - AgcDriver::Submit(&packet, 0); - AgcDriverWaitIdle_nid_postfix(); - AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Output.data()), sizeof(Output)); - const auto launched = item.partial ? item.threads : item.groups * 64u; - const auto filled = std::min({launched, item.records, item.count}); - for (std::uint32_t i = 0; i < Output.size(); ++i) { - const auto expected = i < filled ? Pattern[std::min(i % item.period, 3u)] : Untouched; - Require(Output[i] == expected, "dword pattern fill mismatch at " + std::to_string(i) + " (period " + std::to_string(item.period) + ", count " + std::to_string(item.count) + ", records " + std::to_string(item.records) + "): got " + std::to_string(Output[i]) + ", expected " + std::to_string(expected)); + for (const std::span code : {std::span(Code), std::span(SwappedCode)}) { + for (const auto& item : cases) { + Output.fill(Untouched); + const auto words = Commands(code, UserData(Output.data(), item.records, item.count, item.period), item.groups, item.partial, item.threads); + Packet packet{const_cast(words.data()), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Output.data()), sizeof(Output)); + const auto launched = item.partial ? item.threads : item.groups * 64u; + const auto filled = std::min({launched, item.records, item.count}); + for (std::uint32_t i = 0; i < Output.size(); ++i) { + const auto expected = i < filled ? Pattern[std::min(i % item.period, 3u)] : Untouched; + Require(Output[i] == expected, "dword pattern fill mismatch at " + std::to_string(i) + " (" + (code.data() == SwappedCode.data() ? "swapped" : "listed") + " compares, period " + std::to_string(item.period) + ", count " + std::to_string(item.count) + ", records " + std::to_string(item.records) + "): got " + std::to_string(Output[i]) + ", expected " + std::to_string(expected)); + } } } } @@ -131,7 +169,10 @@ void ExecuteTests() { int main() { try { - if (std::getenv("APS5_NO_FILL_HLE") == nullptr) MatchTests(); + if (std::getenv("APS5_NO_FILL_HLE") == nullptr) { + MatchTests(); + SwappedComparesTests(); + } { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp index 361f0761abc2..b50de10c6e9d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -534,7 +534,7 @@ std::array DepthSampleWords(const DepthTarget& target) { const auto format = target.format == VK_FORMAT_D16_UNORM ? 7u : 22u; const auto count = static_cast(std::countr_zero(target.samples)); const auto width = target.extent.width - 1u, height = target.extent.height - 1u; - return {static_cast(base), static_cast((base >> 32u) & 255u) | (format << 20u) | ((width & 3u) << 30u), (width >> 2u) | (height << 14u), (14u << 28u) | (24u << 20u) | (count << 16u) | 0x204u, 0u, count << 4u, (1u << 10u) | (1u << 19u) | (1u << 21u) | (static_cast(metadata & 255u) << 24u), static_cast(metadata >> 8u)}; + return {static_cast(base), static_cast((base >> 32u) & 255u) | (format << 20u) | ((width & 3u) << 30u), (width >> 2u) | (height << 14u), (14u << 28u) | (24u << 20u) | (count << 16u) | 0x204u, 0u, count << 4u, (target.iterate256 ? 1u << 10u : 0u) | (1u << 19u) | (1u << 21u) | (static_cast(metadata & 255u) << 24u), static_cast(metadata >> 8u)}; } bool DepthSupported(const Context& context, VkFormat format, std::uint32_t fragments) { @@ -649,7 +649,7 @@ void CheckClearDispatch() { } } -void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, bool one, bool clearing = false, std::optional depthOnlyOverride = std::nullopt, ClearFault fault = ClearFault::None) { +void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, bool one, bool clearing = false, std::optional depthOnlyOverride = std::nullopt, ClearFault fault = ClearFault::None, bool iterate256 = true) { const VkExtent2D extent{5u, 3u}; const auto bytes = static_cast(MultisampledDepthBytes_nid_no_patch(extent, format == VK_FORMAT_D16_UNORM ? 2u : 4u, fragments)); TargetMemory memory(bytes), metadata(65536u); @@ -661,7 +661,8 @@ void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, target.depthSwizzle = 24u; target.pipeBankXor = one ? 0x8c00u : 0u; target.htileDepthOnly = depthOnly; - target.iterate256 = target.htilePipeAligned = true; + target.iterate256 = iterate256; + target.htilePipeAligned = true; Recorder recorder(context); recorder.Activate(); struct Cleanup { @@ -804,7 +805,7 @@ void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, case 8u: changed.pipeBankXor ^= 0x100u; break; case 9u: changed.dccAddress += 256u; break; case 10u: changed.dccPipeAligned = false; break; - case 11u: changed.iterate256 = false; break; + case 11u: changed.iterate256 = !changed.iterate256; break; case 12u: changed.allocatedMipCount = 2u; break; case 13u: changed.mipCount = 2u; break; case 14u: changed.baseLevel = 1u; break; @@ -818,7 +819,7 @@ void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, TargetRefuses([&] { CachedSampledTexture(context, words); }, "guest writes"); TargetRefuses([&] { DepthSurfaceHolds(context, resource, texture.get()); }, "guest writes"); texture.reset(); - std::printf("Native depth format=%u samples=%u clear=%u: imported, rasterized twice, sampled and refused conflicts\n", static_cast(format), fragments, one); + std::printf("Native depth format=%u samples=%u clear=%u iterate256=%u: imported, rasterized twice, sampled and refused conflicts\n", static_cast(format), fragments, one, iterate256); } void CheckHtileRouting(Context context, VkFormat format, std::uint32_t fragments) { @@ -884,7 +885,7 @@ void CheckEligibility(Context context) { case 2u: invalid.lastSlice = 1u; reason = "arrays"; break; case 3u: invalid.pipeBankXor = 0x10u; reason = "XOR"; break; case 4u: invalid.htileAddress = 0u; reason = "HTILE"; break; - case 5u: invalid.iterate256 = false; reason = "HTILE"; break; + case 5u: invalid.htileAddress += 16u; reason = "HTILE"; break; case 6u: invalid.htilePipeAligned = false; reason = "HTILE"; break; case 7u: invalid.htileVrsEncoding = 1u; reason = "VRS"; break; case 8u: invalid.clearDepth = 0.5f; reason = "clear value"; break; @@ -1152,8 +1153,10 @@ int main(int argc, char** argv) { std::uint32_t checked = 0u, unavailable = 0u; for (const auto format : {VK_FORMAT_D16_UNORM, VK_FORMAT_D32_SFLOAT}) { for (const auto samples : {2u, 4u, 8u}) { - if (!DepthSupported(context, format, samples)) { unavailable += 2u; continue; } - for (const bool one : {false, true}) { CheckDepthTarget(context, format, samples, one); ++checked; } + if (!DepthSupported(context, format, samples)) { unavailable += 4u; continue; } + for (const bool one : {false, true}) { + for (const bool iterate256 : {true, false}) { CheckDepthTarget(context, format, samples, one, false, std::nullopt, ClearFault::None, iterate256); ++checked; } + } CheckHtileRouting(context, format, samples); } } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedFill.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedFill.cpp index ff6892f765cc..1c13bc169d44 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedFill.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedFill.cpp @@ -76,11 +76,14 @@ void AppendLabel(std::vector& words, std::uint32_t* target, std:: struct Blocker { std::atomic release{false}; - void Enqueue() { + std::uint32_t* target = nullptr; + ~Blocker() { release = true; } + void Enqueue(std::vector writes = {}) { DrawPipeline::Queue0().Enqueue([this] { while (!release.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + if (target != nullptr) std::atomic_ref(*target).store(1); std::lock_guard gpuLock(AgcDriver::GuestMemory::GpuMutex()); - }, {}); + }, std::move(writes)); } }; @@ -157,6 +160,66 @@ void FillIntoFullPipeline() { for (std::size_t i = 0; i < Output.size(); ++i) Require(Output[i] == (i < 256 ? 0x5a5a5a5au : 0xcafebabeu), "a fill behind a full draw pipeline did not land at " + std::to_string(i)); } +void SatisfiedWaitKeepsUnrelatedQueuedWork() { + alignas(8) static std::uint32_t gate = 1; + alignas(8) static std::uint32_t marker = 0; + marker = 0; + + Blocker blocker; + blocker.Enqueue(); + + const auto address = reinterpret_cast(&gate); + std::vector words{ + 0xc0053c00u, 0x13u, static_cast(address), static_cast(address >> 32u), + 1u, 0xffffffffu, 0x19u}; + AppendLabel(words, &marker, 1); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); + while (DrawPipeline::Queue0().Outstanding() < 2 && std::chrono::steady_clock::now() < deadline) + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + const bool keptQueued = DrawPipeline::Queue0().Outstanding() >= 2; + blocker.release = true; + Finish(); + Require(keptQueued, "a satisfied WAIT_REG_MEM drained unrelated queued work"); + Require(marker == 1, "a label after a satisfied WAIT_REG_MEM did not execute"); +} + +void SatisfiedWaitDrainsOverlappingQueuedWork() { + alignas(8) static std::uint32_t gate = 1; + alignas(8) static std::uint32_t marker = 0; + std::atomic_ref(gate).store(1); + marker = 0; + + Blocker blocker; + blocker.target = &gate; + const auto address = reinterpret_cast(&gate); + blocker.Enqueue({{address, address + sizeof(gate)}}); + + std::vector words{ + 0xc0053c00u, 0x13u, static_cast(address), static_cast(address >> 32u), + 1u, 0xffffffffu, 0x19u}; + AppendLabel(words, &marker, 1); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + const auto drainsBefore = DrawPipeline::DrainRequestsByOpcode(0x3c); + auto submitted = std::async(std::launch::async, [&] { AgcDriver::Submit(&packet, 0); }); + + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); + while (DrawPipeline::DrainRequestsByOpcode(0x3c) == drainsBefore && + DrawPipeline::Queue0().Outstanding() < 2 && std::chrono::steady_clock::now() < deadline) + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + const bool drained = DrawPipeline::DrainRequestsByOpcode(0x3c) > drainsBefore; + const bool labelQueuedEarly = DrawPipeline::Queue0().Outstanding() >= 2; + blocker.release = true; + Require(submitted.wait_for(std::chrono::seconds(20)) == std::future_status::ready, + "WAIT_REG_MEM submission did not finish after the overlapping write"); + submitted.get(); + Finish(); + Require(drained && !labelQueuedEarly, "a satisfied WAIT_REG_MEM skipped an overlapping queued write"); + Require(marker == 1, "a label after an overlapping WAIT_REG_MEM did not execute"); +} + } int main() { @@ -172,6 +235,8 @@ int main() { } InputsAfterQueuedLabels(); FillIntoFullPipeline(); + SatisfiedWaitKeepsUnrelatedQueuedWork(); + SatisfiedWaitDrainsOverlappingQueuedWork(); AgcDriverShutdown_nid_postfix(); std::puts("pipelined fill tests passed"); return 0; diff --git a/core/libs/prx/libSceSysmodule/ModuleTable.hpp b/core/libs/prx/libSceSysmodule/ModuleTable.hpp index 038095889cfe..890a993b2a7a 100644 --- a/core/libs/prx/libSceSysmodule/ModuleTable.hpp +++ b/core/libs/prx/libSceSysmodule/ModuleTable.hpp @@ -160,6 +160,7 @@ inline const std::unordered_map kModuleTable = { {0x0000011a, "libSceNpPartner001"}, {0x00000120, "libSceAudioPropagation"}, {0x0000012f, "libSceFontGs"}, + {0x00000132, "libSceProprietaryVoiceChatHelperPrx"}, {0x00000135, "libSceFontGsm"}, {0x00000137, "libScePsml"}, {0x00000138, "libSceNpPartnerSubscription"}, diff --git a/core/libs/prx/libc/include/CpuTopology.hpp b/core/libs/prx/libc/include/CpuTopology.hpp index a1061cb3c47a..cd37af051e30 100644 --- a/core/libs/prx/libc/include/CpuTopology.hpp +++ b/core/libs/prx/libc/include/CpuTopology.hpp @@ -86,7 +86,9 @@ inline const Layout& Get() { highest = cls; } if (lowest < 0 || lowest == highest) return result; - result.efficient = byClass[lowest]; + for (int cls = 0; cls < highest; ++cls) { + if (seen[cls]) result.efficient |= byClass[cls]; + } result.performant = byClass[highest]; result.hybrid = result.efficient != 0 && result.performant != 0; #elif defined(__linux__) diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index cce74774f94b..8e16706fd0f1 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -32,13 +32,13 @@ int __isoc99_vfscanf(std::FILE*, const char*, std::va_list); namespace { #ifndef _WIN32 -bool HasGuestWideFormat(const char* format) { +bool RequiresGuestFormatting(const char* format) { if (format == nullptr) return false; while ((format = std::strchr(format, '%')) != nullptr) { ++format; if (*format == '%') { ++format; continue; } while (*format && std::strchr("-+ #0'0123456789.*$", *format)) ++format; - if (*format == 'S' || *format == 'C' || *format == 'm' || (*format == 'l' && (format[1] == 's' || format[1] == 'c'))) return true; + if (*format == 'S' || *format == 'C' || *format == 'm' || *format == 'p' || (*format == 'l' && (format[1] == 's' || format[1] == 'c'))) return true; if (*format) ++format; } return false; @@ -319,15 +319,15 @@ int APS5_VABI vasprintf_nid_postfix(char** destination, const char* format, VaLi const char* source = text.c_str(); #else std::string guestText; - const bool guestWide = HasGuestWideFormat(format); + const bool guestFormatting = RequiresGuestFormatting(format); char* text = nullptr; std::va_list copy; va_copy(copy, *reinterpret_cast(args)); - const int count = guestWide ? LibcDetail::FormatWindows(nullptr, 0, format, args, &guestText) : ::vasprintf(&text, format, copy); + const int count = guestFormatting ? LibcDetail::FormatWindows(nullptr, 0, format, args, &guestText) : ::vasprintf(&text, format, copy); va_end(copy); std::unique_ptr owner(text, std::free); if (count < 0) return -1; - const char* source = guestWide ? guestText.c_str() : text; + const char* source = guestFormatting ? guestText.c_str() : text; #endif auto* output = static_cast(ApplicationHeapAllocate_nid_no_patch(static_cast(count) + 1)); if (!output) { @@ -352,7 +352,7 @@ int APS5_VABI vfprintf_nid_postfix(FileStream* stream, const char* format, VaLis const int result = std::fwrite(buffer.data(), 1, static_cast(count), native) == static_cast(count) ? count : -1; #else - if (HasGuestWideFormat(format)) { + if (RequiresGuestFormatting(format)) { std::string buffer; const int count = LibcDetail::FormatWindows(nullptr, 0, format, args, &buffer); const int result = std::fwrite(buffer.data(), 1, static_cast(count), native) == static_cast(count) ? count : -1; @@ -448,7 +448,7 @@ int APS5_VABI printf_nid_postfix(const char* format, ...) { if (!_Stdout_nid_postfix.IsOpen()) { errno = 9; return -1; } std::va_list args; va_start(args, format); - const int result = HasGuestWideFormat(format) ? LibcDetail::PrintWindows(format, args) : std::vprintf(format, args); + const int result = RequiresGuestFormatting(format) ? LibcDetail::PrintWindows(format, args) : std::vprintf(format, args); va_end(args); return result; } @@ -457,7 +457,7 @@ int APS5_VABI libc_printf_nid_postfix(const char* format, ...) { if (!_Stdout_nid_postfix.IsOpen()) { errno = 9; return -1; } std::va_list args; va_start(args, format); - const int result = HasGuestWideFormat(format) ? LibcDetail::PrintWindows(format, args) : std::vprintf(format, args); + const int result = RequiresGuestFormatting(format) ? LibcDetail::PrintWindows(format, args) : std::vprintf(format, args); va_end(args); return result; } @@ -465,7 +465,7 @@ int APS5_VABI libc_printf_nid_postfix(const char* format, ...) { int APS5_VABI snprintf_nid_postfix(char* buffer, size_t size, const char* format, ...) { std::va_list args; va_start(args, format); - const int result = HasGuestWideFormat(format) ? LibcDetail::FormatWindows(buffer, size, format, args) : std::vsnprintf(buffer, size, format, args); + const int result = RequiresGuestFormatting(format) ? LibcDetail::FormatWindows(buffer, size, format, args) : std::vsnprintf(buffer, size, format, args); va_end(args); return result; } @@ -473,7 +473,7 @@ int APS5_VABI snprintf_nid_postfix(char* buffer, size_t size, const char* format int APS5_VABI sprintf_nid_postfix(char* buffer, const char* format, ...) { std::va_list args; va_start(args, format); - const int result = HasGuestWideFormat(format) ? LibcDetail::FormatWindows(buffer, SIZE_MAX, format, args) : std::vsprintf(buffer, format, args); + const int result = RequiresGuestFormatting(format) ? LibcDetail::FormatWindows(buffer, SIZE_MAX, format, args) : std::vsprintf(buffer, format, args); va_end(args); return result; } @@ -543,7 +543,7 @@ int APS5_VABI vprintf_nid_postfix(const char* str, VaList* c) { #ifdef _WIN32 return LibcDetail::PrintWindows(str, c); #else - if (HasGuestWideFormat(str)) return LibcDetail::PrintWindows(str, c); + if (RequiresGuestFormatting(str)) return LibcDetail::PrintWindows(str, c); std::va_list copy; va_copy(copy, *reinterpret_cast(c)); const int result = std::vprintf(str, copy); @@ -556,7 +556,7 @@ int APS5_VABI vsprintf_nid_postfix(char* str, const char* format, VaList* args) #ifdef _WIN32 return LibcDetail::FormatWindows(str, SIZE_MAX, format, args); #else - if (HasGuestWideFormat(format)) return LibcDetail::FormatWindows(str, SIZE_MAX, format, args); + if (RequiresGuestFormatting(format)) return LibcDetail::FormatWindows(str, SIZE_MAX, format, args); std::va_list copy; va_copy(copy, *reinterpret_cast(args)); const int result = std::vsprintf(str, format, copy); @@ -569,7 +569,7 @@ int APS5_VABI vsnprintf_nid_postfix(char* str, size_t size, const char* format, #ifdef _WIN32 return LibcDetail::FormatWindows(str, size, format, c); #else - if (HasGuestWideFormat(format)) return LibcDetail::FormatWindows(str, size, format, c); + if (RequiresGuestFormatting(format)) return LibcDetail::FormatWindows(str, size, format, c); std::va_list copy; va_copy(copy, *reinterpret_cast(c)); const int result = std::vsnprintf(str, size, format, copy); @@ -613,7 +613,7 @@ int APS5_VABI vsscanf_s_nid_postfix(const char* buffer, const char* format, VaLi } int APS5_VABI vsnprintf_s_nid_postfix(char* buffer, size_t size, const char* format, VaList* args) { - if (HasGuestWideFormat(format)) return LibcDetail::FormatWindows(buffer, size, format, args); + if (RequiresGuestFormatting(format)) return LibcDetail::FormatWindows(buffer, size, format, args); std::va_list copy; va_copy(copy, *reinterpret_cast(args)); const int result = std::vsnprintf(buffer, size, format, copy); @@ -622,7 +622,7 @@ int APS5_VABI vsnprintf_s_nid_postfix(char* buffer, size_t size, const char* for } int APS5_VABI vsprintf_s_nid_postfix(char* buffer, size_t size, const char* format, VaList* args) { - if (HasGuestWideFormat(format)) return LibcDetail::FormatWindows(buffer, size, format, args); + if (RequiresGuestFormatting(format)) return LibcDetail::FormatWindows(buffer, size, format, args); std::va_list copy; va_copy(copy, *reinterpret_cast(args)); const int result = std::vsnprintf(buffer, size, format, copy); @@ -633,7 +633,7 @@ int APS5_VABI vsprintf_s_nid_postfix(char* buffer, size_t size, const char* form int APS5_VABI sprintf_s_nid_postfix(char* buffer, size_t size, const char* format, ...) { std::va_list args; va_start(args, format); - const int result = HasGuestWideFormat(format) ? LibcDetail::FormatWindows(buffer, size, format, args) : std::vsnprintf(buffer, size, format, args); + const int result = RequiresGuestFormatting(format) ? LibcDetail::FormatWindows(buffer, size, format, args) : std::vsnprintf(buffer, size, format, args); va_end(args); return result; } @@ -641,7 +641,7 @@ int APS5_VABI sprintf_s_nid_postfix(char* buffer, size_t size, const char* forma int APS5_VABI snprintf_s_nid_postfix(char* buffer, size_t size, const char* format, ...) { std::va_list args; va_start(args, format); - const int result = HasGuestWideFormat(format) ? LibcDetail::FormatWindows(buffer, size, format, args) : std::vsnprintf(buffer, size, format, args); + const int result = RequiresGuestFormatting(format) ? LibcDetail::FormatWindows(buffer, size, format, args) : std::vsnprintf(buffer, size, format, args); va_end(args); return result; } @@ -649,7 +649,7 @@ int APS5_VABI snprintf_s_nid_postfix(char* buffer, size_t size, const char* form int APS5_VABI printf_s_nid_postfix(const char* format, ...) { std::va_list args; va_start(args, format); - const int result = HasGuestWideFormat(format) ? LibcDetail::PrintWindows(format, args) : std::vprintf(format, args); + const int result = RequiresGuestFormatting(format) ? LibcDetail::PrintWindows(format, args) : std::vprintf(format, args); va_end(args); return result; } diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 14a8d9449384..7cd403a6c01c 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -494,8 +494,14 @@ extern "C" std::filesystem::path RelinkedModulePath_nid_no_patch(const std::file const auto relinked = RelinkedModuleName(path); std::error_code error; if (std::filesystem::is_regular_file(relinked, error)) return relinked; + const auto parent = relinked.parent_path().empty() ? std::filesystem::path(".") : relinked.parent_path(); + const auto name = relinked.filename(); + for (const char* directory : {"prx", "sce_module", "sce_modules"}) { + const auto candidate = parent / directory / name; + if (std::filesystem::is_regular_file(candidate, error)) return candidate; + } NameMatch names[] = {{relinked.filename().string()}}; - MatchNames(relinked.parent_path().empty() ? "." : relinked.parent_path(), names); + MatchNames(parent, names); return MatchedModuleOrPath(path, relinked, names[0]); } diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index 81519b0921ec..ab6760449dd7 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -637,9 +637,10 @@ static constexpr const char* JOB_WORKER_PREFIX = "BPE JobWorkerThread"; static std::uint64_t JobWorkerMask() { static const std::uint64_t mask = [] { if (std::getenv("APS5_NO_JOB_AFFINITY") != nullptr) return std::uint64_t{0}; + const char* affinityEnv = std::getenv("APS5_JOB_AFFINITY"); + if (affinityEnv != nullptr && std::strcmp(affinityEnv, "0") == 0) return std::uint64_t{0}; const auto requested = CpuTopology::MaskFromEnvironment("APS5_JOB_AFFINITY_MASK"); if (requested != 0) return requested; - if (std::getenv("APS5_JOB_AFFINITY") == nullptr) return std::uint64_t{0}; const auto& layout = CpuTopology::Get(); return layout.hybrid ? layout.efficient : std::uint64_t{0}; }(); diff --git a/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp b/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp index c81d93d46bd5..8abae2ebcfc1 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp @@ -15,5 +15,7 @@ int SetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int op int GetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, 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 SetIpv6LevelOption(std::uintptr_t native, bool ipv6, int option, const void* value, std::uint32_t length); +int GetIpv6LevelOption(std::uintptr_t native, bool ipv6, int option, void* value, std::uint32_t* length); } diff --git a/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp b/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp index 184219224d1f..f40b09b5ad97 100644 --- a/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp +++ b/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp @@ -33,6 +33,7 @@ constexpr int GuestType = 0x1008; constexpr int GuestNonBlocking = 0x1200; constexpr int GuestTcpNoDelay = 1; constexpr int GuestTcpNoDelayFlag = 4; +constexpr int GuestIpv6Only = 27; constexpr int ErrorInvalid = 22; constexpr int ErrorDomain = 33; @@ -229,4 +230,26 @@ int GetTcpLevelOption(std::uintptr_t native, bool tcp, int option, void* value, return Copy(value, length, &number, sizeof(number)); } +int SetIpv6LevelOption(std::uintptr_t native, bool ipv6, int option, const void* value, std::uint32_t length) { + if (!ipv6) return ErrorInvalid; + if (option != GuestIpv6Only) return ErrorNoProtocolOption; + if (length != sizeof(int)) return ErrorInvalid; + if (!value) return ErrorInvalid; + int number; + std::memcpy(&number, value, sizeof(number)); + const int enabled = number != 0 ? 1 : 0; + if (::setsockopt(static_cast(native), IPPROTO_IPV6, IPV6_V6ONLY, reinterpret_cast(&enabled), sizeof(enabled))) return HostError(); + return 0; +} + +int GetIpv6LevelOption(std::uintptr_t native, bool ipv6, int option, void* value, std::uint32_t* length) { + if (!ipv6) return ErrorInvalid; + if (option != GuestIpv6Only) return ErrorNoProtocolOption; + int enabled = 0; + socklen_t size = sizeof(enabled); + if (::getsockopt(static_cast(native), IPPROTO_IPV6, IPV6_V6ONLY, reinterpret_cast(&enabled), &size)) return HostError(); + const int number = enabled != 0 ? 1 : 0; + return Copy(value, length, &number, sizeof(number)); +} + } diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 870a79378372..6eb0e8ddfae1 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -869,23 +869,25 @@ int APS5_VABI setsockopt_nid_postfix(int descriptor, int level, int option, } const auto socket = Lookup(descriptor); if (!socket) return -1; - if (level != 0xffff && level != 6) return Fail(42); + if (level != 0xffff && level != 6 && level != 41) return Fail(42); std::lock_guard lock(socket->modeMutex); const auto native = static_cast(socket->value); - const int error = level == 6 ? GuestSockets::SetTcpLevelOption(native, socket->family != GuestUnix && socket->type == 1, option, value, length) - : GuestSockets::SetSocketLevelOption(native, socket->options, option, value, length); + const int error = level == 6 ? GuestSockets::SetTcpLevelOption(native, 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, option, value, length); return error == 0 ? 0 : Fail(error); } int APS5_VABI getsockopt_nid_postfix(int descriptor, int level, int option, void* value, std::uint32_t* length) { const auto socket = Lookup(descriptor); if (!socket) return -1; - if (level != 0xffff && level != 6) return Fail(42); + if (level != 0xffff && level != 6 && level != 41) return Fail(42); if (!length) return Fail(22); std::lock_guard lock(socket->modeMutex); const auto native = static_cast(socket->value); - const int error = level == 6 ? GuestSockets::GetTcpLevelOption(native, socket->family != GuestUnix && socket->type == 1, option, value, length) - : GuestSockets::GetSocketLevelOption(native, socket->options, socket->type, option, value, length); + const int error = level == 6 ? GuestSockets::GetTcpLevelOption(native, 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->type, option, value, length); return error == 0 ? 0 : Fail(error); } int APS5_VABI socket_nid_postfix(int family, int type, int protocol) { diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index 288a15665b87..16b6ba1d2302 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -182,7 +182,6 @@ __attribute__((noinline)) static void APS5_VABI RunChecks() { static_cast(reinterpret_cast(buffer))); snprintf_nid_postfix(buffer, sizeof(buffer), "%#08x %.3e %a %g %p", 42u, 1.25, 1.25, 1.25, static_cast(buffer)); Require(std::strcmp(buffer, expected) == 0); -#ifdef _WIN32 char pointers[128]; const auto address = [](std::uintptr_t value) { return reinterpret_cast(value); }; Require(snprintf_nid_postfix(pointers, sizeof(pointers), "%p|%p|%p", address(0), address(0x1234), address(0xdeadbeef12)) == 23 && @@ -194,7 +193,19 @@ __attribute__((noinline)) static void APS5_VABI RunChecks() { address(0x1a), 6, address(0xf), -6, address(0xf)) == 34 && std::strcmp(pointers, "[0x1a][0x1a][0x1a][ 0xf][0xf ]") == 0); Require(snprintf_nid_postfix(pointers, 6, "%p", address(0xabcdef)) == 8 && std::strcmp(pointers, "0xabc") == 0); -#endif + Require(snprintf_nid_postfix(pointers, sizeof(pointers), "%p%ls", address(0), u"") == 3 && std::strcmp(pointers, "0x0") == 0); + Require(snprintf_nid_postfix(pointers, sizeof(pointers), "%%p|%%%p", address(0)) == 7 && std::strcmp(pointers, "%p|%0x0") == 0); + Require(sprintf_nid_postfix(pointers, "%p", address(0)) == 3 && std::strcmp(pointers, "0x0") == 0); + Require(FormatList(pointers, sizeof(pointers), "%2$p:%1$d", 7, address(0)) == 5 && std::strcmp(pointers, "0x0:7") == 0); + Require(FormatList(pointers, sizeof(pointers), "[%1$*2$.*3$p]", address(0x1a), 10, 4) == 12 && std::strcmp(pointers, "[ 0x001a]") == 0); + Require(FormatList(pointers, sizeof(pointers), "%p:%d:%.1f:%d %d %d %d %d %d", address(0), 7, 2.5, 1, 2, 3, 4, 5, 6) == 21 && + std::strcmp(pointers, "0x0:7:2.5:1 2 3 4 5 6") == 0); + Require(FormatListS(pointers, sizeof(pointers), "%p", address(0)) == 3 && std::strcmp(pointers, "0x0") == 0); + Require(snprintf_nid_postfix(nullptr, 0, "%p", address(0)) == 3); + pointers[3] = '!'; + int pointerCount = -1; + Require(FormatList(pointers, 3, "%p%n:end", address(0), &pointerCount) == 7 && pointerCount == 3 && + std::strcmp(pointers, "0x") == 0 && pointers[3] == '!'); snprintf_nid_postfix(buffer, sizeof(buffer), "%.*s", -1, "unlimited"); Require(std::strcmp(buffer, "unlimited") == 0); buffer[5] = '!'; @@ -312,6 +323,8 @@ static void CheckAllocatingFormat() { char* allocated = nullptr; Require(asprintf_nid_postfix(&allocated, "%ls:%S:%lc:%d", u"\u00e9", u"ab", 0x20ac, 7) == 11 && std::strcmp(allocated, "\xc3\xa9:ab:\xe2\x82\xac:7") == 0); free_nid_postfix(allocated); + Require(asprintf_nid_postfix(&allocated, "%p:%d", static_cast(nullptr), 7) == 5 && std::strcmp(allocated, "0x0:7") == 0); + free_nid_postfix(allocated); } int main() { RunChecks(); CheckAllocatingFormat(); return CheckWidePrecision() ? 0 : 1; } diff --git a/core/libs/tests/GuestDynamicLoader.cpp b/core/libs/tests/GuestDynamicLoader.cpp index 6ad61bd7212f..7c0e02565013 100644 --- a/core/libs/tests/GuestDynamicLoader.cpp +++ b/core/libs/tests/GuestDynamicLoader.cpp @@ -96,6 +96,22 @@ int main(int argc, char** argv) { std::filesystem::remove(guest); std::filesystem::remove(relinked); + const std::filesystem::path nestedRoot = "anyps5-relinked-prx-dir-for-test"; + const std::filesystem::path nestedGuest = nestedRoot / "Engine.Render.prx"; + const std::filesystem::path nestedRelinked = nestedRoot / "prx" / "Engine.Render.prx.guest.prx"; + std::filesystem::create_directories(nestedRelinked.parent_path()); + { + std::ofstream elf(nestedGuest, std::ios::binary); + elf.write("\x7f" "ELF", 4); + } + std::filesystem::copy_file(argv[1], nestedRelinked, std::filesystem::copy_options::overwrite_existing); + void* nested = dlopen_nid_postfix(nestedGuest.string().c_str(), 2); + Require(nested != nullptr); + add = reinterpret_cast(dlsym_nid_postfix(nested, "GuestModuleAdd")); + Require(add && add(20, 22) == 42); + Require(dlclose_nid_postfix(nested) == 0); + std::filesystem::remove_all(nestedRoot); + void* pinned = dlopen_nid_postfix(argv[1], 2); add = reinterpret_cast(dlsym_nid_postfix(pinned, "GuestModuleAdd")); Require(add && _sceKernelRtldThreadAtexitIncrement_nid_postfix(reinterpret_cast(add)) == 0); diff --git a/core/libs/tests/GuestGameLiveStreaming.cpp b/core/libs/tests/GuestGameLiveStreaming.cpp new file mode 100644 index 000000000000..a60c2538cde6 --- /dev/null +++ b/core/libs/tests/GuestGameLiveStreaming.cpp @@ -0,0 +1,79 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +struct GameLiveStreamingStatus2 { + std::int32_t userId; + bool isOnAir; + std::uint8_t align[3]; + std::uint32_t spectatorCounts; + std::uint32_t textMessageCounts; + std::uint32_t commandMessageCounts; + std::uint32_t broadcastVideoResolution; + std::uint8_t reserved[48]; +}; +static_assert(sizeof(GameLiveStreamingStatus2) == 72); + +extern "C" { +int APS5_VABI sceGameLiveStreamingInitialize(std::size_t heap_size); +int APS5_VABI sceGameLiveStreamingTerminate(void); +int APS5_VABI sceGameLiveStreamingGetCurrentStatus2(GameLiveStreamingStatus2* status); +int APS5_VABI sceGameLiveStreamingGetProgramInfo(void); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr std::int32_t kInvalidUserId = -1; + +} + +int main() { + bool threwZeroHeap = false; + try { + sceGameLiveStreamingInitialize(0); + } catch (const std::invalid_argument&) { + threwZeroHeap = true; + } + Require(threwZeroHeap); + + Require(sceGameLiveStreamingInitialize(1024 * 1024) == 0); + + bool threwNullStatus = false; + try { + sceGameLiveStreamingGetCurrentStatus2(nullptr); + } catch (const std::invalid_argument&) { + threwNullStatus = true; + } + Require(threwNullStatus); + + GameLiveStreamingStatus2 status{}; + std::memset(&status, 0x7f, sizeof(status)); + Require(sceGameLiveStreamingGetCurrentStatus2(&status) == 0); + Require(status.userId == kInvalidUserId); + Require(!status.isOnAir); + Require(status.spectatorCounts == 0); + Require(status.textMessageCounts == 0); + Require(status.commandMessageCounts == 0); + Require(status.broadcastVideoResolution == 0); + for (std::uint8_t byte : status.align) { + Require(byte == 0); + } + for (std::uint8_t byte : status.reserved) { + Require(byte == 0); + } + + bool threwProgramInfo = false; + try { + sceGameLiveStreamingGetProgramInfo(); + } catch (const std::runtime_error&) { + threwProgramInfo = true; + } + Require(threwProgramInfo); + + Require(sceGameLiveStreamingTerminate() == 0); +} diff --git a/core/libs/tests/GuestRenderingWait.cpp b/core/libs/tests/GuestRenderingWait.cpp index 71d3cbe98630..c598c5de3575 100644 --- a/core/libs/tests/GuestRenderingWait.cpp +++ b/core/libs/tests/GuestRenderingWait.cpp @@ -9,6 +9,7 @@ #include #include extern "C" std::uint32_t APS5_VABI sceAgcDriverGetWaitRenderingPacketSizeInDwords(); +extern "C" std::uint32_t APS5_VABI sceAgcDriverGetSetFlipPacketSizeInDwords(); extern "C" std::uint32_t APS5_VABI sceAgcDriverWaitUntilSafeForRendering(std::uint32_t**, std::uint32_t, std::uint32_t, std::uint32_t, int); static void Require(bool value) { if (!value) std::abort(); } class Output final : public AgcDriver::IVideoOutput, public AgcDriver::IRenderingWait, @@ -54,6 +55,9 @@ int main() { auto* cursor = words.data(); const auto size = sceAgcDriverGetWaitRenderingPacketSizeInDwords(); Require(size == AgcDriver::RenderingWaitPacketWords); + const auto flipPacketSize = sceAgcDriverGetSetFlipPacketSizeInDwords(); + Require(flipPacketSize == AgcDriver::FlipPacketWords); + Require(flipPacketSize == 6); bool rejected = false; try { sceAgcDriverWaitUntilSafeForRendering(&cursor, size - 1, 0, 7, 0); } catch (const std::invalid_argument&) { rejected = true; } @@ -62,6 +66,7 @@ int main() { Require(cursor == words.data() + size); Require(words[0] == AgcDriver::RenderingWaitPacketHeader && words[1] == 7 && words[2] == 0 && words[3] == 0); const std::uint32_t flip[] = {AgcDriver::FlipPacketHeader, 7, 0, 1, 0, 0}; + Require(sizeof(flip) / sizeof(flip[0]) == flipPacketSize); for (auto word : flip) *cursor++ = word; Require(words.back() == 0x12345678); Packet packet{words.data(), 10, 0, {}}; diff --git a/core/libs/tests/GuestSocketStreamOptions.cpp b/core/libs/tests/GuestSocketStreamOptions.cpp index 4809e0042e6a..f85a693f1710 100644 --- a/core/libs/tests/GuestSocketStreamOptions.cpp +++ b/core/libs/tests/GuestSocketStreamOptions.cpp @@ -36,6 +36,9 @@ constexpr int SocketLevel = 0xffff; constexpr int TcpLevel = 6; constexpr int TcpNoDelay = 1; constexpr int TcpMaximumSegment = 2; +constexpr int Ipv6Level = 41; +constexpr int Ipv6UnicastHops = 4; +constexpr int Ipv6Only = 27; constexpr int NoSigpipe = 0x800; constexpr int NonBlockingOption = 0x1200; constexpr int NoSignalFlag = 0x20000; @@ -148,6 +151,28 @@ int main() { Require(BrokenPipes() == 1); #endif + const int dualStack = socket_nid_postfix(28, 1, 0); + Require(dualStack >= 0 && Get(dualStack, Ipv6Level, Ipv6Only) == 1); + Require(Set(dualStack, Ipv6Level, Ipv6Only, 0) && Get(dualStack, Ipv6Level, Ipv6Only) == 0); + std::array mapped{28, 28, address[2], address[3], 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 127, 0, 0, 1}; + Require(connect_nid_postfix(dualStack, mapped.data(), mapped.size()) == 0); + const int mappedPeer = accept_nid_postfix(listener, nullptr, nullptr); + Require(mappedPeer >= 0 && close_nid_postfix(mappedPeer) == 0); + const int datagram6 = socket_nid_postfix(28, 2, 0); + Require(datagram6 >= 0 && Get(datagram6, Ipv6Level, Ipv6Only) == 1); + Require(Set(datagram6, Ipv6Level, Ipv6Only, 9) && Get(datagram6, Ipv6Level, Ipv6Only) == 1); + Require(Fails(setsockopt_nid_postfix(datagram6, Ipv6Level, Ipv6Only, &one, sizeof(one) - 1), Invalid)); + Require(Fails(setsockopt_nid_postfix(datagram6, Ipv6Level, Ipv6UnicastHops, &one, sizeof(one)), NoProtocolOption)); + length = sizeof(value); + Require(Fails(getsockopt_nid_postfix(datagram6, Ipv6Level, Ipv6UnicastHops, &value, &length), NoProtocolOption)); + std::array unspecified6{28, 28}; + Require(bind_nid_postfix(datagram6, unspecified6.data(), unspecified6.size()) == 0); + Require(Fails(setsockopt_nid_postfix(datagram6, Ipv6Level, Ipv6Only, &one, sizeof(one)), Invalid)); + Require(Fails(setsockopt_nid_postfix(datagram, Ipv6Level, Ipv6Only, &one, sizeof(one)), Invalid)); + Require(Fails(getsockopt_nid_postfix(datagram, Ipv6Level, Ipv6Only, &value, &length), Invalid)); + Require(Fails(setsockopt_nid_postfix(local[0], Ipv6Level, Ipv6Only, &one, sizeof(one)), Invalid)); + Require(close_nid_postfix(datagram6) == 0 && close_nid_postfix(dualStack) == 0); + Require(close_nid_postfix(local[0]) == 0 && close_nid_postfix(local[1]) == 0); Require(close_nid_postfix(datagram) == 0 && close_nid_postfix(server) == 0); Require(close_nid_postfix(client) == 0 && close_nid_postfix(listener) == 0); diff --git a/core/libs/tests/GuestSysmodule.cpp b/core/libs/tests/GuestSysmodule.cpp index dba31b2f7bfa..ac2d59886da8 100644 --- a/core/libs/tests/GuestSysmodule.cpp +++ b/core/libs/tests/GuestSysmodule.cpp @@ -51,6 +51,7 @@ constexpr std::uint32_t kInternalFlag = 0x80000000u; constexpr std::uint16_t kAcmModuleId = 0x010b; constexpr std::uint16_t kTrophy2ModuleId = 0x0110; constexpr std::uint16_t kVoiceChatModuleId = 0x0114; +constexpr std::uint16_t kProprietaryVoiceChatHelperModuleId = 0x0132; constexpr std::uint16_t kPsmlModuleId = 0x0137; constexpr std::uint32_t kAgcModuleId = 0x80000094; @@ -238,6 +239,13 @@ int main() { Require(sceSysmoduleUnloadModule(kPsmlModuleId) == 0); Require(sceSysmoduleIsLoaded(kPsmlModuleId) == kModuleUnloaded); + Require(sceSysmoduleIsLoaded(kProprietaryVoiceChatHelperModuleId) == kModuleUnloaded); + Require(sceSysmoduleUnloadModule(kProprietaryVoiceChatHelperModuleId) == kModuleUnloaded); + Require(sceSysmoduleLoadModule(kProprietaryVoiceChatHelperModuleId) == 0); + Require(sceSysmoduleIsLoaded(kProprietaryVoiceChatHelperModuleId) == 0); + Require(sceSysmoduleUnloadModule(kProprietaryVoiceChatHelperModuleId) == 0); + Require(sceSysmoduleIsLoaded(kProprietaryVoiceChatHelperModuleId) == kModuleUnloaded); + Require(sceSysmoduleUnloadModuleInternal(kAgcModuleId) == kModuleUnloaded); Require(sceSysmoduleLoadModuleInternal(kAgcModuleId) == 0); Require(sceSysmoduleLoadModuleInternal(kAgcModuleId) == 0); diff --git a/core/libs/tests/InterpolationReferenceRange.cpp b/core/libs/tests/InterpolationReferenceRange.cpp index 7bf9c80ad325..594adbd1d257 100644 --- a/core/libs/tests/InterpolationReferenceRange.cpp +++ b/core/libs/tests/InterpolationReferenceRange.cpp @@ -42,7 +42,9 @@ int main() { } }; const auto attribute = Collect(5u, 1u, 0u, 2u); - expect(attribute.find("interpolation parameter reference is out of range (attribute 5 component 1 mode 0, 2 inputs)") != std::string::npos, "an attribute past the pixel inputs", attribute); + expect(attribute.find("interpolation parameter reference") == std::string::npos, "an attribute past the pixel inputs becomes a default", attribute); + const auto empty = Collect(0u, 0u, 0u, 0u); + expect(empty.find("interpolation parameter reference") == std::string::npos, "attribute 0 with no SPI inputs becomes a default", empty); const auto component = Collect(0u, 4u, 2u, 1u); expect(component.find("(attribute 0 component 4 mode 2, 1 inputs)") != std::string::npos, "a component past w", component); const auto mode = Collect(1u, 3u, 3u, 3u); diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index 8c1eb2e44db3..7914c57c9e1f 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -73,6 +73,35 @@ void ValidateOptions(const IrProgram& program, ShaderStageInputInfo inputInfo) { } } +void EnsurePixelInterpolatorCoverage(const IrProgram& program, ShaderPixelInputInfo& pixel) { + std::uint32_t needed = pixel.inputNum; + const auto consider = [&](std::uint32_t attribute) { + if (attribute < 32u && attribute + 1u > needed) needed = attribute + 1u; + }; + for (const auto& block : program.Blocks()) { + for (const IrValue* inst : block->Instructions()) { + switch (inst->Opcode()) { + case IrOpcode::GetAttribute: + case IrOpcode::GetInterpolationParameter: + case IrOpcode::GetInterpolationParameterF16: + case IrOpcode::InterpolateHostP1: + case IrOpcode::InterpolateHostP2: { + const IrValue* attribute = inst->Argument(0)->Resolve(); + if (attribute->HasImmediate() && attribute->Type() == IrType::U32) consider(attribute->ImmediateU32()); + break; + } + default: + break; + } + } + } + if (needed <= pixel.inputNum) return; + for (std::uint32_t input = pixel.inputNum; input < needed; ++input) { + pixel.interpolatorSettings[input] = 0x20u; + } + pixel.inputNum = needed; +} + void ValidateValueReferences(const IrProgram& program, ShaderStageInputInfo inputInfo) { for (const auto& block : program.Blocks()) { for (const IrValue* inst : block->Instructions()) { @@ -568,6 +597,9 @@ void ShaderInfoCollector::Collect(IrProgram& program, const ShaderStageInputInfo if (!program.Resources().resourceTrackingComplete || program.Metadata().shaderInfoComplete) { return Fail(!program.Resources().resourceTrackingComplete ? "shader resources were not tracked" : "shader info already collected"); } + if (program.Resources().stage == IrShaderStage::Pixel && inputInfo.pixel != nullptr) { + EnsurePixelInterpolatorCoverage(program, const_cast(*inputInfo.pixel)); + } ValidateOptions(program, inputInfo); ValidateValueReferences(program, inputInfo); auto next = program.Info(); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp index 067d57d18400..3ed3c73718dc 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp @@ -445,19 +445,9 @@ void EmitBdaWrite(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t if (bits != 8u && bits != 16u && bits != 32u) ctx.Fail(inst, "unsupported BDA write width"); const auto storeBytes = [&] { EmitBdaOverflowCheck(state, address, bits / 8u, instruction); - for (std::uint32_t byte = 0u; byte < bits / 8u; ++byte) { - const auto guest = Binary(state, spv::OpIAdd, TypeScalarU64(state), address, BdaConstant(state, byte)); - const auto physical = state.module.AllocateId(); - state.module.AddFunction(spv::OpFunctionCall, TypeScalarU64(state), physical, state.bdaWritePointerFunction, guest, ConstantU32(state, 1u), instruction); - EmitIfCondition(state, Binary(state, spv::OpINotEqual, TypeBool(state), physical, BdaConstant(state, 0u)), [&] { - const auto byteType = state.module.Type(spv::OpTypeInt, 8u, 0u); - const auto pointer = state.module.AllocateId(); - state.module.AddFunction(spv::OpConvertUToPtr, TypePointer(state, spv::StorageClassPhysicalStorageBuffer, byteType), pointer, physical); - const auto shifted = Binary(state, spv::OpShiftRightLogical, TypeU32(state), value, ConstantU32(state, byte * 8u)); - state.module.AddFunction(spv::OpStore, pointer, Unary(state, spv::OpUConvert, byteType, shifted), BdaAccessMask(state, inst), 1u); - state.module.AddFunction(spv::OpFunctionCall, state.module.Type(spv::OpTypeVoid), state.module.AllocateId(), state.bdaNoteWriteFunction, guest); - }); - } + const auto function = state.bdaByteWriteFunctions[BdaCoherent(state, inst)]; + if (function == 0) ctx.Fail(inst, "BDA byte write function is missing"); + state.module.AddFunction(spv::OpFunctionCall, state.module.Type(spv::OpTypeVoid), state.module.AllocateId(), function, address, value, ConstantU32(state, bits / 8u), instruction); }; if (bits != 32u) { storeBytes(); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 75baae397cc2..02c31b44af06 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -186,12 +186,15 @@ std::uint32_t EmitAttributeValue(SpirvEmitterState& state, std::uint32_t attr, s } std::uint32_t EmitInterpolationParameterValue(SpirvEmitterState& state, std::uint32_t attr, std::uint32_t chan, std::uint32_t mode) { - if (PixelParameterIsDefault(state, attr)) { - return mode == 2u ? EmitAttributeValue(state, attr, chan) : ConstantU32(state, 0u); + if (state.inputInfo.pixel == nullptr) { + throw std::runtime_error("interpolation parameter refers to an undefined attribute"); + } + if (attr >= state.inputInfo.pixel->inputNum || PixelParameterIsDefault(state, attr)) { + return mode == 2u ? ConstantU32(state, state.inputInfo.pixel->InputDefaultBits(attr, chan & 3u)) : ConstantU32(state, 0u); } const auto* input = SpirvInputBindingForParameter(state, attr); if (input == nullptr) { - throw std::runtime_error("interpolation parameter refers to an undefined attribute"); + return mode == 2u ? ConstantU32(state, state.inputInfo.pixel->InputDefaultBits(attr, chan & 3u)) : ConstantU32(state, 0u); } if (!input->perVertex) { return EmitAttributeValue(state, attr, chan); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index b011fd51ad8c..0821c51ee71e 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -14,7 +14,7 @@ - [Native multisampled depth clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) accept the recognized HTILE mask kernel's direct full-range dispatch after its GPU writes complete. Every metadata byte needs tracked driver-store coverage since notification, no intervening CPU or depth write, and HTILE words proving the current clear value of zero or one. Partial, overlapping, indirect and unproven producers remain unsupported. Existing sampled views retain the native image; guest depth publication and metadata-fill transitions remain unimplemented. Windows shared imports retain write tracking through separate aliases; private host imports that lose tracking and copied or mirrored HTILE uploads remain unsupported. Clear-word encoding and console equivalence remain unmeasured on PS5 hardware. - [Native depth allocation retirement](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) covers sampling-eligible multisampled owners only. Successful release notifications must cumulatively cover both complete depth and HTILE ranges; protection changes, failed releases and partial coverage keep the original guards. Retired owners leave lookup and metadata routing, while held views and recorded work retain their images. Retirement of single-sample or sampling-ineligible depth attachments and console equivalence remain unimplemented or unmeasured. The regression uses real allocation-registry operations with a no-op host-release callback, so it does not validate the host unmap backend. - [Native MSAA depth attachment feedback](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) is not implemented. A draw sampling a native multisampled depth view while attaching its source image throws before pipeline creation or render-pass recording, including read-only attachment feedback. Sampling that image in a separate draw or dispatch remains supported. The refusal compares the held native image views and their device and attachment identity; single-sample depth behavior is retained. -- [Native multisampled depth sampling](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) accepts numeric D16 or D32, two/four/eight samples, one Z_X slice and mip, and pipe-aligned ITERATE_256 HTILE without stencil or VRS. Sampling eligibility is recorded before the attachment's first host clear: every HTILE word must prove the same initial clear of zero or one, and the complete sample-aware depth and HTILE ranges must be accessible and write-watched. Unsupported initial state or device sampling support retains attachment rendering but sampling throws; eligibility is never retried. Sampling rejects later guest writes, queued external GPU writes, and unproven HTILE clear/fill transitions. Guest publication, arbitrary guest depth import, stencil/raw-bit sampling, arrays and storage access remain unimplemented. The block footprint follows main's AMD Addrlib thin-block rule; the accepted HTILE clear words and PS5 equivalence are not measured on console hardware. PPSA99902's historical resident eight-sample D32 reader is documented in [#419](https://github.com/boykopovar/AnyPS5/pull/419#issuecomment-6105597729). +- [Native multisampled depth sampling](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) accepts numeric D16 or D32, two/four/eight samples, one Z_X slice and mip, and pipe-aligned HTILE without stencil or VRS, with ITERATE_256 on or off: the initial import reads no compressed Z planes, and Mesa's HTILE clear words (`radv_get_htile_fast_clear_value`) and Addrlib's HTILE footprint do not depend on ITERATE_256, which `ac_descriptors.c` ties to TC reads of compressed MSAA HTILE. Sampling eligibility is recorded before the attachment's first host clear: every HTILE word must prove the same initial clear of zero or one, and the complete sample-aware depth and HTILE ranges must be accessible and write-watched. Unsupported initial state or device sampling support retains attachment rendering but sampling throws; eligibility is never retried. Sampling rejects later guest writes, queued external GPU writes, and unproven HTILE clear/fill transitions. Guest publication, arbitrary guest depth import, stencil/raw-bit sampling, arrays and storage access remain unimplemented. The block footprint follows main's AMD Addrlib thin-block rule; the accepted HTILE clear words and PS5 equivalence are not measured on console hardware. PPSA99902's historical resident eight-sample D32 reader is documented in [#419](https://github.com/boykopovar/AnyPS5/pull/419#issuecomment-6105597729). - [sceAgcDriverSetSubmitValidationMode, sceAgcDriverGetSubmitValidationMode, sceAgcDriverSetSubmitValidationConfig, sceAgcDriverGetSubmitValidationConfig, sceAgcDriverSetValidationErrorOutputFrequency, sceAgcDriverGetShaderDebuggingStatus](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) - the five validation calls return `0x8A6C1000` without reading their arguments or writing caller memory, and `sceAgcDriverGetShaderDebuggingStatus` returns 1, as [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L250-L291) reads them from Sony's libSceAgcDriver.sprx: each validation export is `mov eax, 0x8a6c1000; ret` in the copy Dragon Quest VII Reimagined bundles (0x7bb0-0x7bf0), and the debugging status is 1 in the firmware 5.50 library (0x6740). The parameters (a 32-bit mode or frequency, a mode output, a configuration pointer) follow OpenAGC and are never read. Whether later retail firmware enables submit validation or shader debugging is unverified; no title in this repository is known to call them - [Indices far past every vertex fetch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - when a draw's highest index is at least 64Ki vertices past the largest per-vertex record count, every index past that count is moved to it in a copy of the indices, so the zero-padded vertex copies are not sized to the original index (PPSA04159 draws a list of `0xffffffff` with primitive restart off, which asked for 160 GiB). Each attribute fetched by vertex index reads zeros there as at the original index (out-of-bounds select 0 or 1); a draw with another out-of-bounds select keeps its indices. The vertex index the shader sees changes for the moved indices, which a shader that reads it directly would notice - [Depth clear draws](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a draw with `DB_RENDER_CONTROL.DEPTH_CLEAR_ENABLE` is taken to write `DB_DEPTH_CLEAR` to every pixel it covers whatever the depth test, depth bounds and depth bias say: it is drawn with an always-passing depth write and the viewport depth range pinned to the clear value. The draw is rasterized as submitted, so its coverage is the draw's own. Whether the DB still applies the depth test or HiZ to such a draw is not measured diff --git a/docs/dev/debt/libSceSysmodule.md b/docs/dev/debt/libSceSysmodule.md index 739a0ac8db66..80f91983aade 100644 --- a/docs/dev/debt/libSceSysmodule.md +++ b/docs/dev/debt/libSceSysmodule.md @@ -1,5 +1,9 @@ # libSceSysmodule technical debt +### Known modules + +- [kModuleTable](../../../core/libs/prx/libSceSysmodule/ModuleTable.hpp) (libSceSysmodule) - module ID 0x00000132 maps to `libSceProprietaryVoiceChatHelperPrx` as verified in [#5017](https://github.com/boykopovar/AnyPS5/pull/5017) and reported in Discussion [#6344](https://github.com/boykopovar/AnyPS5/discussions/6344) for EA SPORTS FC 27 (PPSA34015). + ### Functional - [sceSysmoduleGetModuleInfoForUnwind](../../../core/libs/prx/libSceSysmodule/Export.cpp) (libSceSysmodule) - on Windows the region and file name of an address inside a loaded image are cached after the first lookup, because `VirtualQuery` on a large image section is slow. An entry is used only while the loader still reports the same image for the address (same base, time stamp and size). A page protection change inside the image after the first lookup, which would split or merge the region `VirtualQuery` reports, is not noticed. Addresses outside loaded images are queried every time diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index ed9bb6154ad5..8ea78156caed 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -73,7 +73,7 @@ - [gethostname](../../../core/libs/prx/libc/src/Hostname.cpp) (libc) reports the fixed guest name `AnyPS5` without querying the host machine name. Buffer copying, null queries and `ENAMETOOLONG` follow [FreeBSD gethostname](https://github.com/freebsd/freebsd-src/blob/main/lib/libc/gen/gethostname.c); the console's configurable hostname and its integration with `sysctl` and name resolution are unverified and not implemented. - [wcsrtombs_s](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached - [c16rtomb, c32rtomb, mbrtoc16 and mbrtoc32](../../../core/libs/prx/libc/src/Multibyte.cpp) (libSceLibcInternal, implemented in libc) follow the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x7D480 to 0x7D920), which converts to and from UTF-8 whatever the locale, with the original sequences of up to six bytes and no check for surrogates or for values above 0x10FFFF when encoding. The conversion state keeps the value in its first eight bytes and the count of bytes still expected (or 0x10 for a pending low surrogate, 1 for a stored high surrogate) as 16 bits at offset 0xA; a null state, which the console dereferences, throws. As on the console, `mbrtoc16` does not store the complete code point when it returns a high surrogate, so the low surrogate of the next call is built from the value the state last held, which is the partial value of an earlier incomplete call or whatever the caller initialised it to, `c16rtomb` returns -1 without setting `errno` when a high surrogate is not followed by a low one, and `mbrtoc16` and `mbrtoc32` report `EILSEQ` as 86. Measured on a PS5 (firmware 13.60) from a homebrew payload, the system `libSceLibcInternal`'s default locale also converts UTF-8 in `wcrtomb`, `wctomb`, `mbrtowc` and `mblen` (`wctomb` of U+263A gives `E2 98 BA`, `mblen` of a lone 0xE9 gives -1); this libc keeps its single-byte C locale, so [wctomb and mblen](../../../core/libs/prx/libc/src/Multibyte.cpp), which call `wcrtomb` and `mbtowc` here as they call the same internal routines on the console, share that difference. Titles bundle their own `libc.prx`, whose locale was not measured -- [The narrow printf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) converts 16-bit guest wide arguments to UTF-8 on both platforms, independently of the host locale. It does not use the guest's single-byte `wcrtomb`; the console's treatment of invalid UTF-16 and locale-specific encoding remains unverified. Linux retains host formatting for formats without wide arguments or `%m`, so a narrow `%p` and a NaN there still follow the host (glibc prints a null pointer as `(nil)`, where FreeBSD prints `0x0`, and a NaN with its sign, `-nan`, and with the `+` and space flags, where FreeBSD prints `nan`); argument positions above 99 (FreeBSD's `NL_ARGMAX`) throw when a format requires the guest formatter +- [The narrow printf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) converts 16-bit guest wide arguments to UTF-8 on both platforms, independently of the host locale. It does not use the guest's single-byte `wcrtomb`; the console's treatment of invalid UTF-16 and locale-specific encoding remains unverified. Pointer conversions use the shared formatter on both platforms and follow [FreeBSD 11.4 vfprintf](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/stdio/vfprintf.c), including `0x0` for a null pointer; this is not verified on the console. Linux retains host formatting for formats without wide arguments, `%m` or `%p`, so a NaN there still follow 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 - [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 as the guest's bytes and sizes the result in bytes (two per character, a 16-bit terminator, the 7 characters that still fit next to it and `length * 2 | 15` plus two bytes of storage beyond that), and `hash` mixes one whole character per round, so wide text below 0x100 hashes like its narrow spelling and a character above 0xff hashes as one value instead of the two bytes it holds. The console's `libc.prx` exports both `_Getcat` (NIDs BSVJqITGCyI and 2wz4rthdiy8) with identical 0x19d-byte bodies, believed to build the same 0x20-byte facet; the console bodies themselves are unreadable (obfuscated dynsym, and the text of the relinked `libc.prx` is zeroed), so the wide entries follow the narrow algorithm and the C locale instead of the console's wide code, and no console run checks the two. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size. The narrow result capacity (`length | 15`) and the over-aligned storage of results of 4080 characters or more follow the console's `libc.prx` code and are not checked on hardware. No module of the PPSA59322, PPSA76677 or PPSA97358 dumps in this tree imports either `_Getcat`, so neither has been exercised by a title. - [The scanf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc), the `_s` forms included, on Linux reads the 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`) in the guest scanner instead of glibc, whose `wchar_t` is 32 bits: each input byte is stored as one 16-bit unit of the same value, as the guest's C-locale `mbrtowc` converts it, and a format without them still goes to the host. `sscanf_s` and `vsscanf_s` take this path on both platforms; Windows hands `%ls`, `%lc` and `%l[` of `sscanf`, `vsscanf` and `fscanf` to the host, whose `wchar_t` is 16 bits. A format that reaches the guest scanner throws for a conversion outside C17's (`%2$C`, `%'ls`). Bytes above 0x7F and locales other than C are not measured on the console - [The scanf family](../../../core/libs/prx/libc/include/WindowsScanning.hpp) (libc) on Windows returns 0, as C17 and FreeBSD do, when input fails after only suppressed conversions have completed (`sscanf("12", "%*d%d", &value)`). Linux hands the format to glibc, which returns `EOF` there. On Windows the host stops a `%x`, `%X`, `%i` or `%p` after the `x` of a `0x` that no hex digit follows (or that the width cuts off), and a float conversion (`%e`, `%f`, `%g`, `%a`) after the `e` of an exponent with no digits (`1e`), where C17 and FreeBSD stop before the `x` or the `e` diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 55feb6d0a529..ae82779850aa 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -180,7 +180,7 @@ - [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits on guest sockets and pipes, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A libSceNet socket joins the same host wait through the native socket libSceNet lends to libkernel. `WSAPoll` rejects `POLLPRI`, so on Windows a socket's `POLLPRI` is asked as `POLLRDBAND` and `POLLIN` as `POLLRDNORM`; Winsock reports urgent data until it is read with `MSG_OOB`, where FreeBSD 11.4's `sopoll_generic` stops once the normal stream passes the mark. Guest `send` and `recv` refuse `MSG_OOB` with `EOPNOTSUPP`, so urgent data can only come from an outside peer and Windows keeps reporting `POLLPRI` after the stream passes the mark. A pipe follows FreeBSD's `pipe_poll`: the read end is readable when data is buffered, either end reports `POLLHUP` once the other end is closed, with the requested `POLLIN` and `POLLRDNORM` bits and without `POLLOUT`, which `pollscan` clears, and the write end is writable when `PIPE_BUF` (512) bytes are free on Windows, or when the host kernel says so on Linux. `POLLOUT` on a read end throws: FreeBSD opens both ends for reading and writing and reports it writable, and host pipes are one-way. Windows pipes cannot be waited on, so while a pipe is polled `poll` checks it with `NtQueryInformationFile` and waits on the sockets in slices of 1 ms. A closed descriptor reports `POLLNVAL`, and an open descriptor that is neither a guest socket, a libSceNet socket nor a pipe (files, stdio) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), an infinite timeout with no socket or pipe to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked - [kevent on a pipe](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - `EVFILT_READ` on the read end of a pipe follows FreeBSD 11's [`filt_piperead`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L1787-L1806): `data` is the number of buffered bytes, and `EV_EOF` is set once the write end is closed. Windows pipes cannot be waited on, so while a kqueue waits on a pipe it checks it with `NtQueryInformationFile` and waits on its sockets in slices of 1 ms; a wait with no pipe is not sliced. `EVFILT_READ` on the write end of a pipe throws: FreeBSD's `pipe` opens both ends for reading and writing ([`kern_pipe`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L422)), and host pipes are one-way. `EVFILT_WRITE` on a pipe and `NOTE_LOWAT` on a pipe, which `filt_piperead` ignores, throw too. - [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. A nonblocking `connect` in progress reports `EINPROGRESS` on Linux but `EAGAIN` on Windows (`WSAEWOULDBLOCK`), where FreeBSD reports `EINPROGRESS`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 -- [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) on a guest socket handle the SOL_SOCKET level and `TCP_NODELAY`, after FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c): `SO_REUSEADDR`, `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and `SO_ERROR`, `SO_TYPE`, `SO_ACCEPTCONN`, `SO_SNDBUF` and `SO_RCVBUF` are read. A boolean option reads back as its own bit, as FreeBSD returns `so_options & option`. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `connect`; a second failure with the same code on the same socket without a `connect` in between would be hidden. `SO_SNDBUF` and `SO_RCVBUF` read the host's value, which is not the size FreeBSD reports (Linux doubles it). `SO_NOSIGPIPE` is kept per socket and reads 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. 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. `SO_NBIO` (`0x1200`) is the console's own option, not FreeBSD's. It switches the socket between blocking and non-blocking as `FIONBIO` does and reads back as 0 or 1, as `sceNetSetsockopt` and `sceNetGetsockopt` treat it here; what the console reads back for it is not measured. `TCP_NODELAY` (`IPPROTO_TCP`, 1) is set on the host socket and reads back as `TF_NODELAY` (4), as [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/tcp_usrreq.c#L1860-L1861) returns it. The `IPPROTO_TCP` level on a datagram or `AF_UNIX` socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/ip_output.c#L943-L944) and [`uipc_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_usrreq.c#L1228-L1229) answer it, and an accepted socket takes `TCP_NODELAY` through the host. `SO_SNDTIMEO` and `SO_RCVTIMEO` follow [`sosetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2657-L2689) and [`sogetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2846-L2850). A 16-byte `timeval` with a negative field or 1000000 microseconds or more fails with `EDOM`, and more than `INT32_MAX` seconds becomes the longest wait. The value is kept and read back through FreeBSD's 32.32 fixed-point conversion ([`tvtosbt`, `sbttotv`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/sys/time.h#L178-L286)), so half a second reads back as 499999 microseconds and one microsecond as 0. A `recv`, `recvfrom`, `recvmsg`, `read` or `readv`, and a `send`, `sendto`, `sendmsg`, `write` or `writev`, on a blocking socket then waits that long for the host to report the socket readable or writable, and fails with `EWOULDBLOCK`; a call with `MSG_DONTWAIT` does not wait. An accepted socket takes both from its listener ([`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L629-L630)). The wait covers only the first transfer: once the socket is writable the host send runs without a limit, where FreeBSD stops a send that cannot finish and returns the bytes sent so far, and a receive whose data another thread takes between the wait and the host call blocks again. `sendfile` ignores the send timeout. The host wait has millisecond steps, so a shorter timeout lasts up to a millisecond longer. A null value fails with `EINVAL`, where FreeBSD's [`kern_setsockopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L1414-L1415) returns `EFAULT`. Every other SOL_SOCKET option (`SO_DEBUG`, `SO_USELOOPBACK`, `SO_REUSEPORT` and `SO_TIMESTAMP`, which FreeBSD stores and no title has been seen to set, the low-water marks among them), every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_KEEPIDLE`, `TCP_KEEPINTVL` and `TCP_KEEPCNT` among them) and every other level (`IPPROTO_IP`, `IPPROTO_IPV6`) fail with `ENOPROTOOPT`. None of this was measured on a console. +- [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) on a guest socket handle the SOL_SOCKET level, `TCP_NODELAY` and `IPV6_V6ONLY`, after FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c): `SO_REUSEADDR`, `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and `SO_ERROR`, `SO_TYPE`, `SO_ACCEPTCONN`, `SO_SNDBUF` and `SO_RCVBUF` are read. A boolean option reads back as its own bit, as FreeBSD returns `so_options & option`. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `connect`; a second failure with the same code on the same socket without a `connect` in between would be hidden. `SO_SNDBUF` and `SO_RCVBUF` read the host's value, which is not the size FreeBSD reports (Linux doubles it). `SO_NOSIGPIPE` is kept per socket and reads 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. 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. `SO_NBIO` (`0x1200`) is the console's own option, not FreeBSD's. It switches the socket between blocking and non-blocking as `FIONBIO` does and reads back as 0 or 1, as `sceNetSetsockopt` and `sceNetGetsockopt` treat it here; what the console reads back for it is not measured. `TCP_NODELAY` (`IPPROTO_TCP`, 1) is set on the host socket and reads back as `TF_NODELAY` (4), as [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/tcp_usrreq.c#L1860-L1861) returns it. The `IPPROTO_TCP` level on a datagram or `AF_UNIX` socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/ip_output.c#L943-L944) and [`uipc_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_usrreq.c#L1228-L1229) answer it, and an accepted socket takes `TCP_NODELAY` through the host. `SO_SNDTIMEO` and `SO_RCVTIMEO` follow [`sosetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2657-L2689) and [`sogetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2846-L2850). A 16-byte `timeval` with a negative field or 1000000 microseconds or more fails with `EDOM`, and more than `INT32_MAX` seconds becomes the longest wait. The value is kept and read back through FreeBSD's 32.32 fixed-point conversion ([`tvtosbt`, `sbttotv`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/sys/time.h#L178-L286)), so half a second reads back as 499999 microseconds and one microsecond as 0. A `recv`, `recvfrom`, `recvmsg`, `read` or `readv`, and a `send`, `sendto`, `sendmsg`, `write` or `writev`, on a blocking socket then waits that long for the host to report the socket readable or writable, and fails with `EWOULDBLOCK`; a call with `MSG_DONTWAIT` does not wait. An accepted socket takes both from its listener ([`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L629-L630)). The wait covers only the first transfer: once the socket is writable the host send runs without a limit, where FreeBSD stops a send that cannot finish and returns the bytes sent so far, and a receive whose data another thread takes between the wait and the host call blocks again. `sendfile` ignores the send timeout. The host wait has millisecond steps, so a shorter timeout lasts up to a millisecond longer. A null value fails with `EINVAL`, where FreeBSD's [`kern_setsockopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L1414-L1415) returns `EFAULT`. `IPV6_V6ONLY` (`IPPROTO_IPV6`, 27) on an `AF_INET6` socket is set on the host socket and reads back as 0 or 1, after FreeBSD's [`ip6_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1716-L1732): a value that is not exactly an `int` fails with `EINVAL` ([L1572](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1572)), as does setting it on a bound socket, which the host refuses too. `socket` turns it on for every `AF_INET6` socket, as FreeBSD's default `net.inet6.ip6.v6only` does, and Java turns it off on the sockets it opens: Java 25 Advanced Network (PPSA99871) stops at `java.net.SocketException: Protocol not available` without it. The `IPPROTO_IPV6` level on an `AF_INET` or `AF_UNIX` socket fails with `EINVAL`, as `ip_ctloutput` and `uipc_ctloutput` answer it. Every other SOL_SOCKET option (`SO_DEBUG`, `SO_USELOOPBACK`, `SO_REUSEPORT` and `SO_TIMESTAMP`, which FreeBSD stores, the low-water marks among them; PPSA99871 sets `SO_REUSEPORT` and carries on when it fails), every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_KEEPIDLE`, `TCP_KEEPINTVL` and `TCP_KEEPCNT` among them), every other `IPPROTO_IPV6` option ([L1928-L1929](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L1928-L1929), [L2158-L2159](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet6/ip6_output.c#L2158-L2159)) and the `IPPROTO_IP` level fail with `ENOPROTOOPT`. None of this was measured on a console. - [connect to AF_UNSPEC](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux a socket bound to an explicit port keeps its local address as well (`127.0.0.1:` stays `127.0.0.1:`), because a bound socket cannot be bound again; one bound to port 0 is bound again to the wildcard address with the port it had - [connect](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - `connect` to port 0 on an unconnected `AF_INET` or `AF_INET6` stream or datagram socket fails with `EADDRNOTAVAIL` (49), as FreeBSD 11.4's `in_pcbconnect_setup()` and `in6_pcbconnect()` do. A connected stream socket keeps its host `EISCONN` answer. Known gaps: on an already connected datagram socket `connect` to port 0 keeps the old peer, where FreeBSD's `soconnect()` disconnects first and then fails with 49; `sendto` and `sendmsg` to port 0 are sent, where `udp_output()` fails with 49 on the unbound path but FreeBSD can send to port 0 from a socket bound to a specific address and port. The answers are taken from the FreeBSD source and are not measured on a console. [sceNetConnect](../../../core/libs/prx/libSceNet/Export.cpp) is not changed - [msync](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `kern_msync` and `vm_map_sync`, not measured on the console: the address is rounded down to the page, a range that wraps is `EINVAL`, as is `MS_ASYNC` with `MS_INVALIDATE`, and a range whose start or any page is not mapped (as `sceKernelVirtualQuery` reports it, reservations included) is `ENOMEM`; a page unmapped from inside a larger mapping still counts as mapped there, since `sceKernelVirtualQuery` keeps reporting the whole mapping. The guest has no file mappings, so a successful call writes nothing back. `MS_INVALIDATE` on a mapped range throws: FreeBSD returns `EBUSY` when the range holds `mlock`ed pages, and locked pages are not tracked diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 7a5f66b60562..47813f3bdebe 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -24,6 +24,7 @@ ### Functional +- [Pixel interpolators past `SPI_PS_IN_CONTROL.NUM_INTERP`](../../../core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp) (shader recompiler) - when the IR reads an attribute at or past the guest interpolator count, the missing `SPI_PS_INPUT_CNTL` slots are filled as DEFAULT (`OFFSET` 0x20, constant 0/1) before info collection, and SPIR-V emission of `GetInterpolationParameter` for a missing binding returns the same defaults instead of throwing, matching Mesa's treatment of a PS input with no VS output. Component and mode bounds still throw. PPSA03541 (UFC 5) prepares pixel shader `0x114073b400` with `NUM_INTERP` 0 while it still issues `v_interp` on attribute 0 (`undefined attribute` after the collector fill alone). Console behaviour for that pairing is not measured. - [f64 compares](../../../core/shader/recompiler/Translation/src/CompareInstructions.cpp) flush denormal inputs to signed zero when bit 0 of the `FLOAT_MODE` f16/f64 denormal field is clear (modes 0 and 2); modes 1 and 3 and an unknown mode keep them. This follows the RDNA2 ISA section 6.4 and LLVM's sign-preserving AMDGPU denormal modes. The gfx1036 audit in [#981](https://github.com/boykopovar/AnyPS5/issues/981#issuecomment-6070046381) reports mode-sensitive `VopcCompareF64` results. Class compares still inspect the original bits. The full four-mode matrix has not been newly measured on AMD hardware or the PS5's gfx1013. - [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) follow the `FLOAT_MODE` f32 denormal field with f32 round to nearest even: mode 1 keeps denormal inputs and flushes a result below 2^-126 to a zero of its sign, a product only when it is still below 2^-126 after rounding to 24 bits with an unbounded exponent, as in mode 0; mode 2 flushes denormal inputs to zeros of their sign and keeps a denormal result. Measured on RDNA2 (gfx1030, Radeon RX 6800, AMD Windows driver 32.0.21045.5002 through HIP) over 1,296 operand pairs of 36 values (zeros, 2^-149 to 3 * 2^-149, 0x00400000, 0x007fffff, normals from 2^-126 to 0x01000001, 2^-24, 2^-23, 1, 0x7f7fffff, infinity and a quiet NaN, of both signs) and 896 products around 2^-126 (16 by 14 magnitudes with all four sign combinations) 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: the exact result rounded in the f32 round mode and flushed as above matches every row without a NaN or infinity input, with a +0 product for a zero factor of `v_mul_legacy_f32`. With a directed f32 round mode, modes 1 and 2 still throw: the host rounds these sums and products to nearest even in every mode, so with a directed round mode they also differ from the hardware in modes 0 and 3 (up to 344 of the 1,296 pairs and 190 of the 896 products per mode). Mode 1 depends on the driver keeping denormal inputs as mode 3 does. The PS5's gfx1013 is not measured. diff --git a/tools/pad_input.py b/tools/pad_input.py index 38522df1452a..89afa1002963 100644 --- a/tools/pad_input.py +++ b/tools/pad_input.py @@ -20,7 +20,7 @@ def previous_sequence(path): try: with path.open("rb") as command: line = command.readline(129) - except FileNotFoundError: + except (FileNotFoundError, IsADirectoryError, PermissionError): return 0 if os.name == "nt" and line.endswith(b"\r\n"): line = line[:-2] + b"\n" @@ -54,6 +54,10 @@ def main(): parser.add_argument("--left-stick", type=int, nargs=2, metavar=("X", "Y")) parser.add_argument("--right-stick", type=int, nargs=2, metavar=("X", "Y")) args = parser.parse_args() + if args.path.is_dir(): + parser.error(f"{args.path} is a directory") + if not args.path.parent.is_dir(): + parser.error(f"directory does not exist: {args.path.parent}") if not 1 <= args.hold_ms <= 5000: parser.error("--hold-ms must be between 1 and 5000") sticks = "" @@ -74,7 +78,7 @@ def main(): pass temporary = None try: - with tempfile.NamedTemporaryFile(mode="w", dir=args.path.parent, delete=False) as output: + with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", newline="\n", dir=args.path.parent, delete=False) as output: temporary = Path(output.name) if metadata is not None: if os.geteuid() == 0: diff --git a/tools/tests/test_pad_input.py b/tools/tests/test_pad_input.py index 38c95145711a..01cb964c0e2f 100644 --- a/tools/tests/test_pad_input.py +++ b/tools/tests/test_pad_input.py @@ -107,6 +107,30 @@ def test_exhausted_sequence_reports_error_and_preserves_command(self): self.assertEqual(self.path.read_text(encoding="utf-8"), original) self.assertEqual(list(self.root.iterdir()), [self.path]) + def test_output_has_consistent_lf_newlines_and_utf8(self): + self.queue(1000, "cross") + content = self.path.read_bytes() + self.assertTrue(content.endswith(b"\n")) + self.assertFalse(content.endswith(b"\r\n")) + self.assertEqual(content.decode("utf-8"), "1000 16384 300\n") + + def test_missing_parent_directory_reports_error(self): + missing = self.root / "nonexistent" / "command" + errors = io.StringIO() + with mock.patch.object(sys, "argv", ["pad_input.py", str(missing), "cross"]), \ + contextlib.redirect_stderr(errors), self.assertRaises(SystemExit) as error: + pad_input.main() + self.assertEqual(error.exception.code, 2) + self.assertIn("directory does not exist", errors.getvalue()) + + def test_directory_path_reports_error(self): + errors = io.StringIO() + with mock.patch.object(sys, "argv", ["pad_input.py", str(self.root), "cross"]), \ + contextlib.redirect_stderr(errors), self.assertRaises(SystemExit) as error: + pad_input.main() + self.assertEqual(error.exception.code, 2) + self.assertIn("is a directory", errors.getvalue()) + if __name__ == "__main__": unittest.main()