From d1160fed9a22835bcdc3bba99ef541fe88ea242e Mon Sep 17 00:00:00 2001 From: RINNECODER <93304526+RINNECODER@users.noreply.github.com> Date: Sat, 17 Oct 2026 20:57:32 -0500 Subject: [PATCH] sync(upstream): integrate Batch 25 pad latching, DPP row scans, and libc extensions --- core/libs/SceTypes.hpp | 6 + .../tests/execution/PixelRowReduction.cpp | 211 ++++++++++++++++++ core/libs/prx/libSceAudioOut/CMakeLists.txt | 10 + core/libs/prx/libSceAudioOut/src/AudioOut.cpp | 8 + .../tests/AudioOutSystemState.cpp | 39 ++++ .../prx/libSceAudioPropagation/Export.cpp | 1 - core/libs/prx/libSceLibcInternal/Spawn.cpp | 66 ++++++ core/libs/prx/libScePad/src/PadState.cpp | 26 ++- core/libs/prx/libc/src/FileAndHeap.cpp | 33 +++ core/libs/prx/libc/src/Mspace.cpp | 44 +++- core/libs/prx/libc/src/Random.cpp | 28 ++- core/libs/prx/libc/src/Strings.cpp | 26 +++ core/libs/prx/libkernel/Time/Time.cpp | 60 +++-- core/libs/tests/GuestAudioPropagation.cpp | 41 ++-- core/libs/tests/GuestPad.cpp | 50 +++++ core/libs/tests/GuestPosixSpawn.cpp | 33 +++ core/libs/tests/GuestQsort.cpp | 69 ++++++ core/libs/tests/GuestRandomState.cpp | 19 ++ core/libs/tests/GuestWideStrings.cpp | 23 ++ core/libs/tests/Mspace.cpp | 43 ++++ .../src/SpirvAlu/SpirvAluEmitterMath.cpp | 68 +++++- docs/dev/debt/libSceAudioOut.md | 1 + docs/dev/debt/libSceAudioPropagation.md | 2 +- docs/dev/debt/libSceLibcInternal.md | 1 + docs/dev/debt/libc.md | 3 + 25 files changed, 871 insertions(+), 40 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PixelRowReduction.cpp create mode 100644 core/libs/prx/libSceAudioOut/tests/AudioOutSystemState.cpp diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index ddb8312bc8a7..bec97b1b7f15 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -572,6 +572,12 @@ struct AudioOutPortState { std::uint64_t activeState; }; +struct AudioOutSystemState { + float loudness; + std::uint32_t pad; + std::uint64_t reserved[7]; +}; + struct Audio3dOpenParameters { std::uint64_t size_this; std::uint32_t granularity; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelRowReduction.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelRowReduction.cpp new file mode 100644 index 000000000000..d9b7114c0ba3 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelRowReduction.cpp @@ -0,0 +1,211 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Sentinel = 0xdeadbeefu; +constexpr std::uint32_t Width = 192; +constexpr std::uint32_t Height = 128; +alignas(256) std::array Pixels{}; +alignas(256) std::array Results{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +struct Reduction { + const char* name; + std::uint32_t identity; + std::uint32_t vector; + std::uint32_t scalar; + std::function combine; +}; + +std::uint32_t Key(std::uint32_t pixel) { + return (pixel * 0x9e3779b1u) >> 22u; +} + +std::vector PixelCode(const Reduction& reduction) { + const std::uint32_t dpp = reduction.vector | 0x001a1afau; + return { + 0x7e040f00, 0x7e060f01, 0xd5430006, 0x0409ff03, 0x000000c0, 0x7e120506, 0xd569000b, 0x0001ff06, + 0x9e3779b1, 0x2c161696, 0xbe98047e, 0x879a187e, 0xbeea287e, 0xd501000d, reduction.identity == 0u ? 0x006a1680u : 0x006a16c1u, + dpp, 0xff01110d, dpp, 0xff01120d, dpp, 0xff01140d, dpp, 0xff01180d, 0xbefe046a, + 0xd760000a, 0x00011f0d, 0xd760000b, 0x00013f0d, reduction.scalar | 0x000c0b0au, + 0xd760000a, 0x00015f0d, reduction.scalar | 0x000c0a0cu, 0xd760000a, 0x00017f0d, reduction.scalar | 0x000c0a0cu, + 0x8f099009, 0x880c090c, 0x7e0a020c, 0xe0702000, 0x80010506, 0x7e0e02f2, 0xf800180f, 0x07070707, + 0xbf810000, + }; +} + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +std::vector> Triangles() { + std::vector> vertices; + constexpr std::uint32_t columns = 6; + constexpr std::uint32_t rows = 4; + for (std::uint32_t cell = 0; cell < columns * rows; ++cell) { + const float width = 2.0f / columns; + const float height = 2.0f / rows; + const float x = -1.0f + width * static_cast(cell % columns); + const float y = -1.0f + height * static_cast(cell / columns); + const float skew = 0.07f * static_cast(cell % 5u); + vertices.push_back({x + (0.05f + skew) * width, y + 0.05f * height, 0.5f, 1.0f}); + vertices.push_back({x + 0.93f * width, y + (0.11f + skew) * height, 0.5f, 1.0f}); + vertices.push_back({x + (0.37f - skew * 0.5f) * width, y + 0.91f * height, 0.5f, 1.0f}); + } + return vertices; +} + +void Draw(AgcDriver::VulkanDevice& device, std::span pixelCode, const ShaderRecompiler::SpirvTarget& target) { + static const auto triangles = Triangles(); + Pixels.fill(std::byte{0}); + Results.fill(Sentinel); + + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(triangles.data(), 16u, static_cast(triangles.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {64u, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixel.posX = true; + pixel.posY = true; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + std::vector pixelUserData(8, 0u); + const auto results = BufferDescriptor(Results.data(), 4u, static_cast(Results.size())); + std::copy(results.begin(), results.end(), pixelUserData.begin() + 4); + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {64u, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 64u, 64u, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(triangles.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +std::uint32_t Check(const Reduction& reduction, std::uint32_t subgroupSize) { + const auto what = std::string("pixel row reduction ") + reduction.name; + std::map> waves; + for (std::uint32_t pixel = 0; pixel < Results.size(); ++pixel) { + if (std::to_integer(Pixels[pixel * 4u]) != 255u) { + Require(Results[pixel] == Sentinel, what + ": uncovered pixel " + std::to_string(pixel) + " stored " + std::to_string(Results[pixel])); + continue; + } + Require(Results[pixel] != Sentinel, what + ": covered pixel " + std::to_string(pixel) + " stored nothing"); + waves[Results[pixel] >> 16u].push_back(pixel); + } + std::uint32_t partial = 0; + std::size_t covered = 0; + for (const auto& [first, pixels] : waves) { + Require(std::find(pixels.begin(), pixels.end(), first) != pixels.end(), what + ": the first lane of the wave of pixel " + std::to_string(pixels.front()) + ", pixel " + std::to_string(first) + ", is not a covered pixel of that wave"); + std::uint32_t expected = reduction.identity; + for (const auto pixel : pixels) expected = reduction.combine(expected, Key(pixel)); + for (const auto pixel : pixels) { + Require((Results[pixel] & 0xffffu) == expected, what + ": pixel " + std::to_string(pixel) + " of a wave of " + std::to_string(pixels.size()) + " covered pixels read " + std::to_string(Results[pixel] & 0xffffu) + ", the wave holds " + std::to_string(expected)); + } + if (pixels.size() < subgroupSize) ++partial; + covered += pixels.size(); + } + Require(covered > 1000, what + ": the triangles covered only " + std::to_string(covered) + " pixels"); + Require(partial != 0u, what + ": no wave had fewer covered pixels than the host subgroup"); + return partial; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto target = device->Target(); + if (!TargetHasCapability(target, spv::CapabilityGroupNonUniformArithmetic)) { + std::puts("skipped, the device has no subgroup arithmetic"); + return VulkanTestSkipped; + } + if (target.subgroupSize > 64u) { + std::printf("skipped, the %u-lane subgroup is wider than the wave\n", target.subgroupSize); + return VulkanTestSkipped; + } + auto wide = target; + wide.subgroupSize = 64u; + const std::array layouts{wide}; + const std::array reductions{{ + {"UMin", 0xffffffffu, 0x26000000u, 0x83800000u, [](std::uint32_t left, std::uint32_t right) { return std::min(left, right); }}, + {"UMax", 0u, 0x28000000u, 0x84800000u, [](std::uint32_t left, std::uint32_t right) { return std::max(left, right); }}, + {"IAdd", 0u, 0x4a000000u, 0x80000000u, [](std::uint32_t left, std::uint32_t right) { return left + right; }}, + {"AND", 0xffffffffu, 0x36000000u, 0x87000000u, [](std::uint32_t left, std::uint32_t right) { return left & right; }}, + {"OR", 0u, 0x38000000u, 0x88000000u, [](std::uint32_t left, std::uint32_t right) { return left | right; }}, + }}; + for (const auto& layout : layouts) { + for (const auto& reduction : reductions) { + for (const bool windowed : {false, true}) { + auto code = PixelCode(reduction); + if (windowed) code.insert(std::find(code.begin(), code.end(), 0x7e0e02f2u), 0x7e008514u); + std::uint32_t partial = 0; + for (std::uint32_t pass = 0; pass < 4; ++pass) { + Draw(*device, code, layout); + partial += Check(reduction, target.subgroupSize); + } + std::printf("%s%s, laid out for %u lanes on %u-lane subgroups: %u partial waves over 4 draws\n", reduction.name, windowed ? " in a v_movreld_b32 window" : "", layout.subgroupSize, target.subgroupSize, partial); + } + } + } + std::puts("pixel row reduction tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAudioOut/CMakeLists.txt b/core/libs/prx/libSceAudioOut/CMakeLists.txt index 69eeff5390f3..38bdb8cf0ede 100644 --- a/core/libs/prx/libSceAudioOut/CMakeLists.txt +++ b/core/libs/prx/libSceAudioOut/CMakeLists.txt @@ -177,4 +177,14 @@ if(BUILD_TESTING) set_tests_properties(audio_out2_shutdown audio_out2_waiting_push_shutdown audio_out2_advance_shutdown audio_out_shutdown PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + add_test_executable(audio_out_system_state_tests tests/AudioOutSystemState.cpp) + target_include_directories(audio_out_system_state_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(audio_out_system_state_tests PRIVATE libSceAudioOut libc) + configure_windows_unwind(audio_out_system_state_tests) + add_test(NAME audio_out_system_state COMMAND audio_out_system_state_tests) + if(WIN32) + set_tests_properties(audio_out_system_state PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp index 5f6d3a37b467..3372e16599b7 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp @@ -611,4 +611,12 @@ int APS5_VABI sceAudioOutExOpen(int userId, std::uint32_t mode) { return allocatePort(PORT_TYPE_EX, highRate ? 1024 : 256, highRate ? 192000 : 48000, Format::S16Stereo); } +int APS5_VABI sceAudioOutGetSystemState(AudioOutSystemState* state) { + if (state == nullptr) { + return -2144993276; + } + state->loudness = 0.0f; + return 0; +} + } diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutSystemState.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutSystemState.cpp new file mode 100644 index 000000000000..4cf928d77027 --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutSystemState.cpp @@ -0,0 +1,39 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include + +extern "C" { +int APS5_VABI sceAudioOutGetSystemState(AudioOutSystemState*); +} + +static void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +namespace { + +constexpr int invalidPointer = static_cast(0x80260004); + +void TestGetSystemState() { + AudioOutSystemState state{}; + state.loudness = -1.0f; + Require(sceAudioOutGetSystemState(&state) == 0, "sceAudioOutGetSystemState must succeed with valid pointer"); + Require(state.loudness == 0.0f, "loudness must be reported as 0.0f"); +} + +void TestInvalidPointer() { + Require(sceAudioOutGetSystemState(nullptr) == invalidPointer, "null pointer must return SCE_AUDIO_OUT_ERROR_INVALID_POINTER"); +} + +} + +int main() { + TestGetSystemState(); + TestInvalidPointer(); + return 0; +} diff --git a/core/libs/prx/libSceAudioPropagation/Export.cpp b/core/libs/prx/libSceAudioPropagation/Export.cpp index c3cdff194d2e..22a39889d02c 100644 --- a/core/libs/prx/libSceAudioPropagation/Export.cpp +++ b/core/libs/prx/libSceAudioPropagation/Export.cpp @@ -311,7 +311,6 @@ std::int32_t APS5_VABI sceAudioPropagationSourceRender(AudioPropagationHandle sy require(info.output != nullptr && info.outputSize != 0, __func__); if (info.format != RenderFormat) throw std::runtime_error(std::string(__func__) + ": render format " + std::to_string(info.format) + " is unknown"); } - for (std::uint32_t index = 0; index < count; ++index) std::memset(infos[index].output, 0, infos[index].outputSize); return 0; } diff --git a/core/libs/prx/libSceLibcInternal/Spawn.cpp b/core/libs/prx/libSceLibcInternal/Spawn.cpp index c4571fb64e34..c81a8fd0cb6d 100644 --- a/core/libs/prx/libSceLibcInternal/Spawn.cpp +++ b/core/libs/prx/libSceLibcInternal/Spawn.cpp @@ -60,6 +60,72 @@ int APS5_VABI posix_spawnattr_setsigdefault_nid_postfix(PosixSpawnAttributes** a return 0; } +int APS5_VABI posix_spawnattr_getflags_nid_postfix(PosixSpawnAttributes* const* attributes, std::int16_t* flags) { + const auto& source = Attributes(attributes, "posix_spawnattr_getflags"); + if (flags == nullptr) throw std::invalid_argument("posix_spawnattr_getflags: null flags"); + *flags = source.flags; + return 0; +} + +int APS5_VABI posix_spawnattr_getpgroup_nid_postfix(PosixSpawnAttributes* const* attributes, std::int32_t* processGroup) { + const auto& source = Attributes(attributes, "posix_spawnattr_getpgroup"); + if (processGroup == nullptr) throw std::invalid_argument("posix_spawnattr_getpgroup: null process group"); + *processGroup = source.processGroup; + return 0; +} + +int APS5_VABI posix_spawnattr_getschedparam_nid_postfix(PosixSpawnAttributes* const* attributes, std::int32_t* schedulingParameter) { + const auto& source = Attributes(attributes, "posix_spawnattr_getschedparam"); + if (schedulingParameter == nullptr) throw std::invalid_argument("posix_spawnattr_getschedparam: null scheduling parameter"); + *schedulingParameter = source.schedulingPriority; + return 0; +} + +int APS5_VABI posix_spawnattr_getschedpolicy_nid_postfix(PosixSpawnAttributes* const* attributes, std::int32_t* schedulingPolicy) { + const auto& source = Attributes(attributes, "posix_spawnattr_getschedpolicy"); + if (schedulingPolicy == nullptr) throw std::invalid_argument("posix_spawnattr_getschedpolicy: null scheduling policy"); + *schedulingPolicy = source.schedulingPolicy; + return 0; +} + +int APS5_VABI posix_spawnattr_getsigdefault_nid_postfix(PosixSpawnAttributes* const* attributes, GuestSignalSet* signalDefault) { + const auto& source = Attributes(attributes, "posix_spawnattr_getsigdefault"); + if (signalDefault == nullptr) throw std::invalid_argument("posix_spawnattr_getsigdefault: null signal set"); + *signalDefault = source.signalDefault; + return 0; +} + +int APS5_VABI posix_spawnattr_getsigmask_nid_postfix(PosixSpawnAttributes* const* attributes, GuestSignalSet* signalMask) { + const auto& source = Attributes(attributes, "posix_spawnattr_getsigmask"); + if (signalMask == nullptr) throw std::invalid_argument("posix_spawnattr_getsigmask: null signal set"); + *signalMask = source.signalMask; + return 0; +} + +int APS5_VABI posix_spawnattr_setpgroup_nid_postfix(PosixSpawnAttributes** attributes, std::int32_t processGroup) { + Attributes(attributes, "posix_spawnattr_setpgroup").processGroup = processGroup; + return 0; +} + +int APS5_VABI posix_spawnattr_setschedparam_nid_postfix(PosixSpawnAttributes** attributes, const std::int32_t* schedulingParameter) { + auto& target = Attributes(attributes, "posix_spawnattr_setschedparam"); + if (schedulingParameter == nullptr) throw std::invalid_argument("posix_spawnattr_setschedparam: null scheduling parameter"); + target.schedulingPriority = *schedulingParameter; + return 0; +} + +int APS5_VABI posix_spawnattr_setschedpolicy_nid_postfix(PosixSpawnAttributes** attributes, std::int32_t schedulingPolicy) { + Attributes(attributes, "posix_spawnattr_setschedpolicy").schedulingPolicy = schedulingPolicy; + return 0; +} + +int APS5_VABI posix_spawnattr_setsigmask_nid_postfix(PosixSpawnAttributes** attributes, const GuestSignalSet* signalMask) { + auto& target = Attributes(attributes, "posix_spawnattr_setsigmask"); + if (signalMask == nullptr) throw std::invalid_argument("posix_spawnattr_setsigmask: null signal set"); + target.signalMask = *signalMask; + return 0; +} + int APS5_VABI posix_spawn_file_actions_init_nid_postfix(PosixSpawnFileActions** actions) { if (actions == nullptr) throw std::invalid_argument("posix_spawn_file_actions_init: null file actions"); auto* created = new (std::nothrow) PosixSpawnFileActions{}; diff --git a/core/libs/prx/libScePad/src/PadState.cpp b/core/libs/prx/libScePad/src/PadState.cpp index 9e03da126f9b..f615a0ff5fc9 100644 --- a/core/libs/prx/libScePad/src/PadState.cpp +++ b/core/libs/prx/libScePad/src/PadState.cpp @@ -16,6 +16,11 @@ namespace { std::mutex stateMutex; PadInputState state; + std::uint32_t pressedSinceRead = 0; + std::uint8_t pressedAnalogL2 = 0; + std::uint8_t pressedAnalogR2 = 0; + std::uint64_t pressedAt = 0; + constexpr std::uint64_t kLatchExpiry = 100000; PadOutputState output; std::chrono::steady_clock::time_point audioHapticEnd; std::uint64_t timestamp = 0; @@ -110,6 +115,7 @@ void Pad::Initialize() { timestamp = sceKernelGetProcessTime(); lastFuseTime = timestamp; queue.clear(); + pressedSinceRead = 0; } initialized = true; } @@ -239,7 +245,14 @@ PadData Pad::ReadState() { if (!initialized) throw std::runtime_error("Pad: read before initialization"); const std::uint64_t now = sceKernelGetProcessTime(); const auto injected = InjectedInput(now); - return BuildData(state, timestamp, now, injected); + PadData data = BuildData(state, timestamp, now, injected); + if (pressedSinceRead != 0 && now - pressedAt <= kLatchExpiry) { + data.buttons |= pressedSinceRead; + if (pressedSinceRead & 0x100u) data.analog_buttons_l2 = std::max(data.analog_buttons_l2, pressedAnalogL2); + if (pressedSinceRead & 0x200u) data.analog_buttons_r2 = std::max(data.analog_buttons_r2, pressedAnalogR2); + } + pressedSinceRead = 0; + return data; } int Pad::Read(PadData* data, int num) { @@ -247,6 +260,7 @@ int Pad::Read(PadData* data, int num) { std::lock_guard lock(stateMutex); if (failure) std::rethrow_exception(failure); if (!initialized) throw std::runtime_error("Pad: read before initialization"); + pressedSinceRead = 0; const std::uint64_t now = sceKernelGetProcessTime(); const auto injected = InjectedInput(now); if (queue.empty()) { @@ -381,8 +395,16 @@ extern "C" void PadPublishInput_nid_postfix(const PadInputState& input) { state.touchLeft == input.touchLeft && state.touchRight == input.touchRight && state.hasMotion == input.hasMotion && state.accel == input.accel && state.gyro == input.gyro && state.touch == input.touch && state.deviceKind == input.deviceKind) return; - state = input; + const std::uint32_t pressed = input.buttons & ~state.buttons; timestamp = sceKernelGetProcessTime(); + if (pressed != 0) { + if (pressedSinceRead != 0 && timestamp - pressedAt > kLatchExpiry) pressedSinceRead = 0; + pressedSinceRead |= pressed; + pressedAt = timestamp; + if (pressed & 0x100u) pressedAnalogL2 = input.analogButtonsL2; + if (pressed & 0x200u) pressedAnalogR2 = input.analogButtonsR2; + } + state = input; if (queue.size() == kQueueCapacity) queue.pop_front(); queue.push_back({input, timestamp}); } diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 3b5dbab6f5ca..afeac7d687de 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -545,6 +545,39 @@ void* APS5_VABI bsearch_nid_postfix(const void* key, const void* base, size_t co return nullptr; } +int APS5_VABI mergesort_nid_postfix(void* base, size_t count, size_t size, GuestCompare compare) { + if (size < 4) { + errno = 22; + return -1; + } + if (count == 0) return 0; + if (!compare) throw std::invalid_argument("mergesort: null comparator"); + if (!base) throw std::invalid_argument("mergesort: null base"); + if (count > (std::numeric_limits::max() - 8) / size) throw std::overflow_error("mergesort: array size overflow"); + auto* const scratch = static_cast(malloc_nid_postfix(count * size + 8)); + if (!scratch) return -1; + auto* source = static_cast(base); + auto* target = scratch; + for (size_t width = 1; width < count; width *= 2) { + for (size_t low = 0; low < count; low += 2 * width) { + const size_t middle = std::min(low + width, count); + const size_t high = std::min(low + 2 * width, count); + size_t left = low, right = middle, out = low; + while (left < middle && right < high) { + const size_t taken = compare(source + left * size, source + right * size) <= 0 ? left++ : right++; + std::memcpy(target + out++ * size, source + taken * size, size); + } + std::memcpy(target + out * size, source + left * size, (middle - left) * size); + out += middle - left; + std::memcpy(target + out * size, source + right * size, (high - right) * size); + } + std::swap(source, target); + } + if (source != base) std::memcpy(base, source, count * size); + free_nid_postfix(scratch); + return 0; +} + void APS5_VABI qsort_nid_postfix(void* base, size_t count, size_t size, int (APS5_VABI *compare)(const void*, const void*)) { if (!compare) throw std::invalid_argument("qsort: null comparator"); if (size == 0) throw std::invalid_argument("qsort: zero element size"); diff --git a/core/libs/prx/libc/src/Mspace.cpp b/core/libs/prx/libc/src/Mspace.cpp index 143d50eba2b9..0029e8e1d9bd 100644 --- a/core/libs/prx/libc/src/Mspace.cpp +++ b/core/libs/prx/libc/src/Mspace.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include extern "C" int* APS5_VABI __error_nid_postfix(); @@ -147,7 +148,6 @@ int FillStats(void* handle, MallocManagedSize* stats) { stats->currentInuseSize = arena->inUse; return 0; } - } extern "C" { @@ -311,4 +311,46 @@ std::size_t APS5_VABI sceLibcMspaceMallocUsableSize_nid_postfix(const void* poin Error(22); return 0; } + +void* APS5_VABI sceLibcPafMspaceCreate_nid_postfix(const char* name, void* base, std::size_t size, unsigned flags, const void* options) { + if (options) throw std::runtime_error("sceLibcPafMspaceCreate: creation options are not supported"); + return sceLibcMspaceCreate_nid_postfix(name, base, size, flags); +} + +int APS5_VABI sceLibcPafMspaceDestroy_nid_postfix(void* handle) { + return sceLibcMspaceDestroy_nid_postfix(handle); +} + +void* APS5_VABI sceLibcPafMspaceMalloc_nid_postfix(void* handle, std::size_t size, const void* owner) { + (void)owner; + return sceLibcMspaceMalloc_nid_postfix(handle, size); +} + +void APS5_VABI sceLibcPafMspaceFree_nid_postfix(void* handle, void* pointer, const void* owner) { + (void)owner; + sceLibcMspaceFree_nid_postfix(handle, pointer); +} + +void* APS5_VABI sceLibcPafMspaceCalloc_nid_postfix(void* handle, std::size_t count, std::size_t size, const void* owner) { + (void)owner; + return sceLibcMspaceCalloc_nid_postfix(handle, count, size); +} + +void* APS5_VABI sceLibcPafMspaceRealloc_nid_postfix(void* handle, void* pointer, std::size_t size, const void* owner) { + (void)owner; + return sceLibcMspaceRealloc_nid_postfix(handle, pointer, size); +} + +void* APS5_VABI sceLibcPafMspaceReallocalign_nid_postfix(void* handle, void* pointer, std::size_t alignment, std::size_t size, const void* owner) { + (void)owner; + return sceLibcMspaceReallocalign_nid_postfix(handle, pointer, alignment, size); +} + +int APS5_VABI sceLibcPafMspaceMallocStats_nid_postfix(void* handle, MallocManagedSize* stats) { + return sceLibcMspaceMallocStats_nid_postfix(handle, stats); +} + +int APS5_VABI sceLibcPafMspaceMallocStatsFast_nid_postfix(void* handle, MallocManagedSize* stats) { + return sceLibcMspaceMallocStatsFast_nid_postfix(handle, stats); +} } diff --git a/core/libs/prx/libc/src/Random.cpp b/core/libs/prx/libc/src/Random.cpp index 49b48e16ca43..787fe00b2b4a 100644 --- a/core/libs/prx/libc/src/Random.cpp +++ b/core/libs/prx/libc/src/Random.cpp @@ -96,14 +96,17 @@ void SaveTypeWordLocked() { } std::uint16_t g_rand48Seed[3]{0x330e, 0xabcd, 0x1234}; +constexpr std::uint64_t DefaultRand48Multiplier = 0x5deece66dull; +constexpr std::uint16_t DefaultRand48Addend = 0xb; +std::uint64_t g_rand48Multiplier = DefaultRand48Multiplier; +std::uint16_t g_rand48Addend = DefaultRand48Addend; +std::uint16_t g_seed48Result[3]{}; std::uint64_t NextRand48(std::uint16_t* seed, const char* function) { if (seed == nullptr) throw std::invalid_argument(std::string(function) + ": null seed"); - constexpr std::uint64_t Multiplier = 0x5deece66dull; - constexpr std::uint64_t Addend = 0xb; constexpr std::uint64_t Mask = (1ull << 48) - 1; std::uint64_t value = (static_cast(seed[2]) << 32) | (static_cast(seed[1]) << 16) | seed[0]; - value = (value * Multiplier + Addend) & Mask; + value = (value * g_rand48Multiplier + g_rand48Addend) & Mask; seed[0] = static_cast(value); seed[1] = static_cast(value >> 16); seed[2] = static_cast(value >> 32); @@ -146,6 +149,25 @@ void APS5_VABI srand48_nid_postfix(std::int64_t seedval) { g_rand48Seed[0] = 0x330e; g_rand48Seed[1] = static_cast(static_cast(seedval)); g_rand48Seed[2] = static_cast(static_cast(seedval) >> 16); + g_rand48Multiplier = DefaultRand48Multiplier; + g_rand48Addend = DefaultRand48Addend; +} + +std::uint16_t* APS5_VABI seed48_nid_postfix(const std::uint16_t* seed) { + if (seed == nullptr) throw std::invalid_argument("seed48: null seed"); + std::lock_guard lock(g_randomLock); + for (int index = 0; index < 3; ++index) g_rand48Seed[index] = g_seed48Result[index] = seed[index]; + g_rand48Multiplier = DefaultRand48Multiplier; + g_rand48Addend = DefaultRand48Addend; + return g_seed48Result; +} + +void APS5_VABI lcong48_nid_postfix(const std::uint16_t* parameters) { + if (parameters == nullptr) throw std::invalid_argument("lcong48: null parameters"); + std::lock_guard lock(g_randomLock); + for (int index = 0; index < 3; ++index) g_rand48Seed[index] = parameters[index]; + g_rand48Multiplier = parameters[3] | (static_cast(parameters[4]) << 16) | (static_cast(parameters[5]) << 32); + g_rand48Addend = parameters[6]; } std::int64_t APS5_VABI random_nid_postfix() { diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index 812f97a7ba69..02b7905e1702 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -152,6 +152,14 @@ char* APS5_VABI strrchr_nid_postfix(const char* s, int c) { return std::strrchr(const_cast(s), c); } +char* APS5_VABI rindex_nid_postfix(const char* s, int c) { + return strrchr_nid_postfix(s, c); +} + +int APS5_VABI flsl_nid_postfix(std::int64_t mask) { + return mask == 0 ? 0 : 64 - std::countl_zero(static_cast(mask)); +} + char* APS5_VABI strstr_nid_postfix(const char* haystack, const char* needle) { return std::strstr(const_cast(haystack), needle); } @@ -200,6 +208,10 @@ unsigned long long APS5_VABI strtoull_nid_postfix(const char* str, char** endptr return std::strtoull(str, endptr, base); } +unsigned long long APS5_VABI strtouq_nid_postfix(const char* str, char** endptr, int base) { + return strtoull_nid_postfix(str, endptr, base); +} + double APS5_VABI strtod_nid_postfix(const char* str, char** endptr) { return ParseFloat(str, endptr, std::strtod); } @@ -483,6 +495,12 @@ size_t APS5_VABI wcsspn_nid_postfix(const char16_t* s, const char16_t* accept) { return count; } +size_t APS5_VABI wcscspn_nid_postfix(const char16_t* s, const char16_t* reject) { + size_t count = 0; + while (s[count] != 0 && !Contains(reject, s[count])) ++count; + return count; +} + char16_t* APS5_VABI wcstok_nid_postfix(char16_t* text, const char16_t* delimiters, char16_t** last) { if (text == nullptr) text = *last; if (text == nullptr) return nullptr; @@ -537,6 +555,14 @@ unsigned long long APS5_VABI wcstoull_nid_postfix(const char16_t* str, char16_t* return ParseAsciiPrefix(str, endptr, [base](const char* text, char** end) { return StopAtPrefixLetter_nid_no_patch(text, end, base) ? 0ULL : std::strtoull(text, end, base); }); } +long long APS5_VABI wcstoimax_nid_postfix(const char16_t* str, char16_t** endptr, int base) { + return wcstoll_nid_postfix(str, endptr, base); +} + +unsigned long long APS5_VABI wcstoumax_nid_postfix(const char16_t* str, char16_t** endptr, int base) { + return wcstoull_nid_postfix(str, endptr, base); +} + std::uint64_t APS5_VABI _WStoul_nid_postfix(const char16_t* str, char16_t** endptr, int base) { if (base < 0 || base == 1 || base > 36) throw std::invalid_argument("_WStoul: unsupported base " + std::to_string(base)); return wcstoul_nid_postfix(str, endptr, base); diff --git a/core/libs/prx/libkernel/Time/Time.cpp b/core/libs/prx/libkernel/Time/Time.cpp index e25277e82109..3d0f7c9cdf89 100644 --- a/core/libs/prx/libkernel/Time/Time.cpp +++ b/core/libs/prx/libkernel/Time/Time.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -28,6 +29,10 @@ #include #include #include +#ifdef __linux__ +#include +#include +#endif // __linux__ #endif extern "C" int* APS5_VABI __error_nid_postfix(); @@ -179,6 +184,39 @@ std::uint64_t APS5_VABI sceKernelGetProcessTimeCounterFrequency() { return 1000000000ULL; } +static std::string DescribeCallSite(const void* caller) { + const auto address = reinterpret_cast(caller); + char text[192]; +#ifdef _WIN32 + const auto image = reinterpret_cast(GetModuleHandleW(nullptr)); + std::snprintf(text, sizeof(text), "exe+0x%llx", static_cast(address - image)); +#elif defined(__linux__) + Dl_info info{}; + link_map* map = nullptr; + if (dladdr1(caller, &info, reinterpret_cast(&map), RTLD_DL_LINKMAP) == 0 || map == nullptr) { + std::snprintf(text, sizeof(text), "0x%llx", static_cast(address)); + return text; + } + const char* path = map->l_name; + const char* slash = path == nullptr ? nullptr : std::strrchr(path, '/'); + const char* module = path == nullptr || *path == '\0' ? "exe" : slash == nullptr ? path : slash + 1; + std::snprintf(text, sizeof(text), "%s+0x%llx", module, static_cast(address - map->l_addr)); +#else + std::snprintf(text, sizeof(text), "0x%llx", static_cast(address)); +#endif + return text; +} + +static unsigned long CurrentThreadIdForTrace() { +#ifdef _WIN32 + return GetCurrentThreadId(); +#elif defined(__linux__) + return static_cast(gettid()); +#else + return 0; +#endif +} + // Debug aid: APS5_TRACE_USLEEP reports every 2000 calls which guest call sites sleep, so a CPU // thread polling for GPU or I/O completion can be found (offsets are relative to the executable). static void TraceSleep(const void* caller, std::uint64_t microseconds) { @@ -192,15 +230,10 @@ static void TraceSleep(const void* caller, std::uint64_t microseconds) { ++site.first; site.second += microseconds; if (++calls % 2000 != 0) return; -#ifdef _WIN32 - const auto image = reinterpret_cast(GetModuleHandleW(nullptr)); -#else - const std::uintptr_t image = 0; -#endif std::vector>> hot(sites.begin(), sites.end()); std::sort(hot.begin(), hot.end(), [](const auto& a, const auto& b) { return a.second.first > b.second.first; }); std::fprintf(stderr, "[usleep] %llu calls:", static_cast(calls)); - for (std::size_t i = 0; i < hot.size() && i < 6; ++i) std::fprintf(stderr, " exe+0x%llx x%llu (%llu us)", static_cast(hot[i].first - image), static_cast(hot[i].second.first), static_cast(hot[i].second.second)); + for (std::size_t i = 0; i < hot.size() && i < 6; ++i) std::fprintf(stderr, " %s x%llu (%llu us)", DescribeCallSite(reinterpret_cast(hot[i].first)).c_str(), static_cast(hot[i].second.first), static_cast(hot[i].second.second)); std::fprintf(stderr, "\n"); } @@ -211,16 +244,13 @@ void KernelTraceWait_nid_postfix(const char* kind, const void* caller, std::uint static std::mutex mutex; static std::unordered_map sites; static std::uint64_t lastReport = 0; - char key[96]; -#ifdef _WIN32 - const auto image = reinterpret_cast(GetModuleHandleW(nullptr)); - const unsigned long thread = GetCurrentThreadId(); -#else - const std::uintptr_t image = 0; - const unsigned long thread = 0; -#endif - std::snprintf(key, sizeof(key), "%s exe+0x%llx", kind, static_cast(reinterpret_cast(caller) - image)); + static std::unordered_map callSites; + char key[224]; + const unsigned long thread = CurrentThreadIdForTrace(); std::lock_guard lock(mutex); + auto callSite = callSites.find(caller); + if (callSite == callSites.end()) callSite = callSites.emplace(caller, DescribeCallSite(caller)).first; + std::snprintf(key, sizeof(key), "%s %s", kind, callSite->second.c_str()); auto& site = sites[key]; ++site.calls; site.timeouts += timedOut ? 1u : 0u; diff --git a/core/libs/tests/GuestAudioPropagation.cpp b/core/libs/tests/GuestAudioPropagation.cpp index 8930cf679d74..9584ba78e518 100644 --- a/core/libs/tests/GuestAudioPropagation.cpp +++ b/core/libs/tests/GuestAudioPropagation.cpp @@ -143,20 +143,16 @@ int main() { Require(sceAudioPropagationSourceCreate(system, &secondSource) == 0, "second source create failed"); float outputs[2][64]; - const auto fill = [&] { std::memset(outputs, 0xab, sizeof(outputs)); }; - const auto zeroed = [&](int index) { - for (const float sample : outputs[index]) { - if (sample != 0.0f) return false; + float expected[2][64]; + const auto fill = [&] { + for (std::size_t channel = 0; channel < 2; ++channel) { + for (std::size_t sample = 0; sample < 64; ++sample) { + outputs[channel][sample] = static_cast(sample + 1) / (channel == 0 ? 128.0f : -128.0f); + } } - return true; - }; - const auto untouched = [&] { - const auto* bytes = reinterpret_cast(outputs); - for (std::size_t index = 0; index < sizeof(outputs); ++index) { - if (bytes[index] != 0xab) return false; - } - return true; + std::memcpy(expected, outputs, sizeof(outputs)); }; + const auto untouched = [&] { return std::memcmp(outputs, expected, sizeof(outputs)) == 0; }; AudioPropagation::RenderInfo infos[2]{}; const auto reset = [&] { infos[0] = {{AudioPropagation::RenderInfoId, AudioPropagation::RenderInfoSize}, source, outputs[0], sizeof(outputs[0]), AudioPropagation::RenderFormat, 0}; @@ -165,13 +161,26 @@ int main() { }; reset(); Require(sceAudioPropagationSourceRender(system, infos, 1) == 0, "render failed"); - Require(zeroed(0), "render left the output unwritten"); - const auto* tail = reinterpret_cast(outputs[1]); - Require(tail[0] == 0xab, "render wrote past the output size"); + Require(untouched(), "empty source erased an existing mix"); reset(); infos[0].outputSize = sizeof(float) * 16; Require(sceAudioPropagationSourceRender(system, infos, 2) == 0, "render of two sources failed"); - Require(outputs[0][15] == 0.0f && outputs[0][16] != 0.0f && zeroed(1), "render did not write each output for its size"); + Require(untouched(), "empty sources modified separate destination mixes or their tails"); + reset(); + infos[1].output = outputs[0]; + Require(sceAudioPropagationSourceRender(system, infos, 2) == 0, "render into a shared mix failed"); + Require(untouched(), "empty sources modified a shared destination mix"); + reset(); + infos[0].outputSize = sizeof(float) * 32; + infos[1].output = outputs[0] + 16; + infos[1].outputSize = sizeof(float) * 32; + Require(sceAudioPropagationSourceRender(system, infos, 2) == 0, "render into overlapping mixes failed"); + Require(untouched(), "empty sources modified overlapping destination mixes"); + reset(); + std::memset(outputs, 0, sizeof(outputs)); + std::memcpy(expected, outputs, sizeof(outputs)); + Require(sceAudioPropagationSourceRender(system, infos, 2) == 0, "render into silence failed"); + Require(untouched(), "empty sources modified a silent destination mix"); reset(); RequireThrows([&] { sceAudioPropagationSourceRender(system, infos, 0); }, "render without infos"); diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index 2392d45159e8..61e824535699 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -191,6 +191,54 @@ static void CheckReadQueue(int handle) { Require(data[0].buttons == 0); } +static void CheckTapBetweenReads() { + const auto cross = static_cast(Pad::PadButton::Cross); + PadPublishInput_nid_postfix(PadInputState{}); + Pad::ReadState(); + PadInputState pressed; + pressed.buttons = cross; + PadPublishInput_nid_postfix(pressed); + PadPublishInput_nid_postfix(PadInputState{}); + Require((Pad::ReadState().buttons & cross) != 0); + Require((Pad::ReadState().buttons & cross) == 0); + PadPublishInput_nid_postfix(pressed); + Require((Pad::ReadState().buttons & cross) != 0); + Require((Pad::ReadState().buttons & cross) != 0); + PadPublishInput_nid_postfix(PadInputState{}); + Require((Pad::ReadState().buttons & cross) == 0); +} + +static void CheckLatchedPressLifetime(int handle) { + const auto cross = static_cast(Pad::PadButton::Cross); + const auto circle = static_cast(Pad::PadButton::Circle); + PadInputState pressed; + PadData data{}; + PadData drained[64]{}; + scePadRead_nid_postfix(handle, drained, 64); + pressed.buttons = cross; + PadPublishInput_nid_postfix(pressed); + PadPublishInput_nid_postfix(PadInputState{}); + Require(scePadRead_nid_postfix(handle, drained, 64) >= 1); + Require(scePadReadState(handle, &data) == PAD_OK); + Require((data.buttons & cross) == 0); + pressed.buttons = cross; + PadPublishInput_nid_postfix(pressed); + PadPublishInput_nid_postfix(PadInputState{}); + std::this_thread::sleep_for(std::chrono::milliseconds(150)); + Require(scePadReadState(handle, &data) == PAD_OK); + Require((data.buttons & cross) == 0); + pressed.buttons = cross; + PadPublishInput_nid_postfix(pressed); + PadPublishInput_nid_postfix(PadInputState{}); + std::this_thread::sleep_for(std::chrono::milliseconds(150)); + pressed.buttons = circle; + PadPublishInput_nid_postfix(pressed); + PadPublishInput_nid_postfix(PadInputState{}); + Require(scePadReadState(handle, &data) == PAD_OK); + Require((data.buttons & cross) == 0); + Require((data.buttons & circle) != 0); +} + static void CheckReadStateHandle(int handle) { PadData data{}; Require(scePadReadState(0, &data) == PAD_ERROR_INVALID_HANDLE); @@ -309,6 +357,8 @@ int main() { CheckTouchContact(); CheckPartialTrigger(); CheckReadQueue(handle); + CheckTapBetweenReads(); + CheckLatchedPressLifetime(handle); CheckReadStateHandle(handle); CheckRemoteController(handle); CheckInjectedState(commandPath); diff --git a/core/libs/tests/GuestPosixSpawn.cpp b/core/libs/tests/GuestPosixSpawn.cpp index 8fe49f711d16..33ae4ebd0f8f 100644 --- a/core/libs/tests/GuestPosixSpawn.cpp +++ b/core/libs/tests/GuestPosixSpawn.cpp @@ -12,6 +12,16 @@ int APS5_VABI posix_spawnattr_init_nid_postfix(PosixSpawnAttributes**); int APS5_VABI posix_spawnattr_destroy_nid_postfix(PosixSpawnAttributes**); int APS5_VABI posix_spawnattr_setflags_nid_postfix(PosixSpawnAttributes**, std::int16_t); int APS5_VABI posix_spawnattr_setsigdefault_nid_postfix(PosixSpawnAttributes**, const GuestSignalSet*); +int APS5_VABI posix_spawnattr_getflags_nid_postfix(PosixSpawnAttributes* const*, std::int16_t*); +int APS5_VABI posix_spawnattr_getpgroup_nid_postfix(PosixSpawnAttributes* const*, std::int32_t*); +int APS5_VABI posix_spawnattr_getschedparam_nid_postfix(PosixSpawnAttributes* const*, std::int32_t*); +int APS5_VABI posix_spawnattr_getschedpolicy_nid_postfix(PosixSpawnAttributes* const*, std::int32_t*); +int APS5_VABI posix_spawnattr_getsigdefault_nid_postfix(PosixSpawnAttributes* const*, GuestSignalSet*); +int APS5_VABI posix_spawnattr_getsigmask_nid_postfix(PosixSpawnAttributes* const*, GuestSignalSet*); +int APS5_VABI posix_spawnattr_setpgroup_nid_postfix(PosixSpawnAttributes**, std::int32_t); +int APS5_VABI posix_spawnattr_setschedparam_nid_postfix(PosixSpawnAttributes**, const std::int32_t*); +int APS5_VABI posix_spawnattr_setschedpolicy_nid_postfix(PosixSpawnAttributes**, std::int32_t); +int APS5_VABI posix_spawnattr_setsigmask_nid_postfix(PosixSpawnAttributes**, const GuestSignalSet*); int APS5_VABI posix_spawn_file_actions_init_nid_postfix(PosixSpawnFileActions**); int APS5_VABI posix_spawn_file_actions_destroy_nid_postfix(PosixSpawnFileActions**); int APS5_VABI posix_spawn_file_actions_addopen_nid_postfix(PosixSpawnFileActions**, int, const char*, int, std::uint16_t); @@ -67,7 +77,30 @@ static void CheckAttributes() { Check(std::memcmp(&attributes->signalDefault, &defaults, sizeof(defaults)) == 0); Check(std::memcmp(&attributes->signalMask, &empty, sizeof(empty)) == 0); + std::int16_t flags = 1; + std::int32_t value = 1; + GuestSignalSet read{{9u, 9u, 9u, 9u}}; + Check(posix_spawnattr_getflags_nid_postfix(&attributes, &flags) == 0 && flags == 0); + Check(posix_spawnattr_getsigdefault_nid_postfix(&attributes, &read) == 0 && std::memcmp(&read, &defaults, sizeof(read)) == 0); + Check(posix_spawnattr_getsigmask_nid_postfix(&attributes, &read) == 0 && std::memcmp(&read, &empty, sizeof(read)) == 0); + Check(posix_spawnattr_setpgroup_nid_postfix(&attributes, 77) == 0 && attributes->processGroup == 77); + Check(posix_spawnattr_getpgroup_nid_postfix(&attributes, &value) == 0 && value == 77); + const std::int32_t priority = 450; + Check(posix_spawnattr_setschedparam_nid_postfix(&attributes, &priority) == 0 && attributes->schedulingPriority == 450); + Check(posix_spawnattr_getschedparam_nid_postfix(&attributes, &value) == 0 && value == 450); + Check(posix_spawnattr_setschedpolicy_nid_postfix(&attributes, 2) == 0 && attributes->schedulingPolicy == 2); + Check(posix_spawnattr_getschedpolicy_nid_postfix(&attributes, &value) == 0 && value == 2); + const GuestSignalSet mask{{1u, 2u, 3u, 4u}}; + Check(posix_spawnattr_setsigmask_nid_postfix(&attributes, &mask) == 0 && std::memcmp(&attributes->signalMask, &mask, sizeof(mask)) == 0); + Check(posix_spawnattr_getsigmask_nid_postfix(&attributes, &read) == 0 && std::memcmp(&read, &mask, sizeof(read)) == 0); + Check(std::memcmp(&attributes->signalDefault, &defaults, sizeof(defaults)) == 0); + PosixSpawnAttributes* uninitialized = nullptr; + Check(ThrowsInvalidArgument([&] { posix_spawnattr_getflags_nid_postfix(&uninitialized, &flags); })); + Check(ThrowsInvalidArgument([&] { posix_spawnattr_getpgroup_nid_postfix(&attributes, nullptr); })); + Check(ThrowsInvalidArgument([&] { posix_spawnattr_setpgroup_nid_postfix(&uninitialized, 1); })); + Check(ThrowsInvalidArgument([&] { posix_spawnattr_setschedparam_nid_postfix(&attributes, nullptr); })); + Check(ThrowsInvalidArgument([&] { posix_spawnattr_setsigmask_nid_postfix(&attributes, nullptr); })); Check(ThrowsInvalidArgument([&] { posix_spawnattr_setflags_nid_postfix(&uninitialized, 1); })); Check(ThrowsInvalidArgument([&] { posix_spawnattr_setsigdefault_nid_postfix(&uninitialized, &defaults); })); Check(ThrowsInvalidArgument([&] { posix_spawnattr_setsigdefault_nid_postfix(&attributes, nullptr); })); diff --git a/core/libs/tests/GuestQsort.cpp b/core/libs/tests/GuestQsort.cpp index 4a75f5c01e77..6124f2540fc1 100644 --- a/core/libs/tests/GuestQsort.cpp +++ b/core/libs/tests/GuestQsort.cpp @@ -1,15 +1,58 @@ +#include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include #include #include #include #include extern "C" { +int APS5_VABI mergesort_nid_postfix(void*, std::size_t, std::size_t, int (APS5_VABI*)(const void*, const void*)); +int* APS5_VABI __error_nid_postfix(); void APS5_VABI qsort_nid_postfix(void*, std::size_t, std::size_t, int (APS5_VABI*)(const void*, const void*)); } namespace { +bool failAllocation = false; + +void* APS5_VABI Allocate(std::size_t size) { return failAllocation ? nullptr : std::malloc(size); } +void APS5_VABI Free(void* ptr) { std::free(ptr); } +void* APS5_VABI Reallocate(void* ptr, std::size_t size) { return std::realloc(ptr, size); } +void* APS5_VABI Calloc(std::size_t count, std::size_t size) { return std::calloc(count, size); } +void* APS5_VABI Align(std::size_t, std::size_t size) { return std::malloc(size); } +void* APS5_VABI Realign(void* ptr, std::size_t size, std::size_t) { return std::realloc(ptr, size); } +int APS5_VABI PosixAlign(void** ptr, std::size_t, std::size_t size) { + *ptr = std::malloc(size); + return *ptr ? 0 : 12; +} + +void InitializeHeap() { + alignas(16) static std::array process{}; + alignas(16) static std::array libc{}; + alignas(16) static std::array replacement{}; + const auto write = [](auto& data, std::size_t offset, auto value) { + std::memcpy(data.data() + offset, &value, sizeof(value)); + }; + write(process, 0, std::uint64_t{0x40}); + write(process, 8, std::uint32_t{0x4942524f}); + write(process, 0x38, libc.data()); + write(libc, 0, std::uint64_t{0x38}); + write(libc, 0x30, replacement.data()); + write(replacement, 0, std::uint64_t{0x78}); + write(replacement, 8, std::uint64_t{2}); + write(replacement, 0x20, reinterpret_cast(&Allocate)); + write(replacement, 0x28, reinterpret_cast(&Free)); + write(replacement, 0x30, reinterpret_cast(&Calloc)); + write(replacement, 0x38, reinterpret_cast(&Reallocate)); + write(replacement, 0x40, reinterpret_cast(&Align)); + write(replacement, 0x48, reinterpret_cast(&Realign)); + write(replacement, 0x50, reinterpret_cast(&PosixAlign)); + ApplicationHeapInitialize_nid_no_patch(process.data()); +} + struct Element { int key; int tag; @@ -66,6 +109,31 @@ bool ExpectSorted(std::size_t size, std::size_t count) { return true; } + +bool CheckMergesort() { + InitializeHeap(); + bool correct = true; + for (const std::size_t count : {0, 1, 2, 3, 7, 16, 33, 1000}) { + std::vector elements; + for (std::size_t index = 0; index < count; ++index) elements.push_back({static_cast((index * 7919) % 5), static_cast(index)}); + correct &= mergesort_nid_postfix(elements.data(), count, sizeof(Element), CompareKeys) == 0; + for (std::size_t index = 1; index < count; ++index) { + const auto& before = elements[index - 1]; + const auto& after = elements[index]; + correct &= before.key < after.key || (before.key == after.key && before.tag < after.tag); + } + } + unsigned char bytes[3] = {3, 1, 2}; + *__error_nid_postfix() = 0; + correct &= mergesort_nid_postfix(bytes, 3, 1, CompareFirstByte) == -1 && *__error_nid_postfix() == 22 && bytes[0] == 3; + correct &= mergesort_nid_postfix(nullptr, 0, 4, CompareKeys) == 0; + Element pair[2] = {{2, 0}, {1, 1}}; + failAllocation = true; + correct &= mergesort_nid_postfix(pair, 2, sizeof(Element), CompareKeys) == -1 && pair[0].key == 2; + failAllocation = false; + if (!correct) std::fprintf(stderr, "mergesort failed\n"); + return correct; +} } int main() { @@ -80,5 +148,6 @@ int main() { for (const std::size_t size : {1, 3, 8, 24}) for (const std::size_t count : {0, 1, 2, 6, 7, 8, 40, 41, 1000}) correct &= ExpectSorted(size, count); + correct &= CheckMergesort(); return correct ? 0 : 1; } diff --git a/core/libs/tests/GuestRandomState.cpp b/core/libs/tests/GuestRandomState.cpp index 59b336db38bd..6f2ab74c7d8d 100644 --- a/core/libs/tests/GuestRandomState.cpp +++ b/core/libs/tests/GuestRandomState.cpp @@ -23,6 +23,8 @@ double APS5_VABI drand48_nid_postfix(); std::int64_t APS5_VABI lrand48_nid_postfix(); std::int64_t APS5_VABI mrand48_nid_postfix(); void APS5_VABI srand48_nid_postfix(std::int64_t); +std::uint16_t* APS5_VABI seed48_nid_postfix(const std::uint16_t*); +void APS5_VABI lcong48_nid_postfix(const std::uint16_t*); } static int failures = 0; @@ -74,6 +76,23 @@ static void CheckRand48() { Require(mrand48_nid_postfix() == jrand48_nid_postfix(expectedSeed), "mrand48 matches jrand48 on initialized seed"); Require(drand48_nid_postfix() == erand48_nid_postfix(expectedSeed), "drand48 fourth value matches erand48"); + const std::uint16_t parameters[7] = {1, 0, 0, 3, 0, 0, 5}; + lcong48_nid_postfix(parameters); + Require(lrand48_nid_postfix() == 0 && mrand48_nid_postfix() == 0, "lcong48 sets seed, multiplier and addend"); + std::uint16_t high[3] = {0, 0, 1}; + Require(jrand48_nid_postfix(high) == 196608 && SeedIs(high, 5, 0, 3), "lcong48 multiplier scales the seed"); + std::uint16_t custom[3] = {2, 0, 0}; + Require(nrand48_nid_postfix(custom) == 0 && SeedIs(custom, 11, 0, 0), "lcong48 parameters apply to nrand48"); + const std::uint16_t newSeed[3] = {0x330e, 0xabcd, 0x1234}; + std::uint16_t* const echoed = seed48_nid_postfix(newSeed); + Require(SeedIs(echoed, 0x330e, 0xabcd, 0x1234), "seed48 returns a copy of the new seed"); + std::uint16_t again[3] = {0x330e, 0xabcd, 0x1234}; + Require(drand48_nid_postfix() == erand48_nid_postfix(again), "seed48 restores the default multiplier and addend"); + lcong48_nid_postfix(parameters); + srand48_nid_postfix(0x1234abcd); + std::uint16_t reset[3] = {0x330e, 0xabcd, 0x1234}; + Require(lrand48_nid_postfix() == nrand48_nid_postfix(reset), "srand48 restores the default multiplier and addend"); + #ifndef _WIN32 std::uint16_t guest[3] = {0x1234, 0x5678, 0x9abc}; unsigned short host[3] = {0x1234, 0x5678, 0x9abc}; diff --git a/core/libs/tests/GuestWideStrings.cpp b/core/libs/tests/GuestWideStrings.cpp index 39b16ff31e49..4502e1fe81fc 100644 --- a/core/libs/tests/GuestWideStrings.cpp +++ b/core/libs/tests/GuestWideStrings.cpp @@ -26,6 +26,7 @@ const char16_t* APS5_VABI wcsrchr_nid_postfix(const char16_t* s, char16_t c); const char16_t* APS5_VABI wcsstr_nid_postfix(const char16_t* haystack, const char16_t* needle); const char16_t* APS5_VABI wcspbrk_nid_postfix(const char16_t* s, const char16_t* accept); std::size_t APS5_VABI wcsspn_nid_postfix(const char16_t* s, const char16_t* accept); +std::size_t APS5_VABI wcscspn_nid_postfix(const char16_t* s, const char16_t* reject); char16_t* APS5_VABI wcstok_nid_postfix(char16_t* text, const char16_t* delimiters, char16_t** last); char16_t* APS5_VABI wmemset_nid_postfix(char16_t* s, char16_t c, std::size_t n); double APS5_VABI wcstod_nid_postfix(const char16_t* str, char16_t** endptr); @@ -33,6 +34,11 @@ float APS5_VABI wcstof_nid_postfix(const char16_t* str, char16_t** endptr); long double APS5_VABI wcstold_nid_postfix(const char16_t* str, char16_t** endptr); long long APS5_VABI wcstol_nid_postfix(const char16_t* str, char16_t** endptr, int base); long long APS5_VABI wcstoll_nid_postfix(const char16_t* str, char16_t** endptr, int base); +long long APS5_VABI wcstoimax_nid_postfix(const char16_t* str, char16_t** endptr, int base); +unsigned long long APS5_VABI wcstoumax_nid_postfix(const char16_t* str, char16_t** endptr, int base); +unsigned long long APS5_VABI strtouq_nid_postfix(const char* str, char** endptr, int base); +char* APS5_VABI rindex_nid_postfix(const char* s, int c); +int APS5_VABI flsl_nid_postfix(std::int64_t mask); unsigned long long APS5_VABI wcstoul_nid_postfix(const char16_t* str, char16_t** endptr, int base); unsigned long long APS5_VABI wcstoull_nid_postfix(const char16_t* str, char16_t** endptr, int base); int APS5_VABI wcscoll_nid_postfix(const char16_t* first, const char16_t* second); @@ -118,6 +124,7 @@ int main() { require(wcspbrk_nid_postfix(haystack, u"xyz") == nullptr); require(wcsspn_nid_postfix(u"aabbc", u"ab") == 4); require(wcsspn_nid_postfix(u"abc", u"") == 0); + require(wcscspn_nid_postfix(u"abcde", u"dx") == 3 && wcscspn_nid_postfix(u"abc", u"") == 3 && wcscspn_nid_postfix(u"", u"a") == 0 && wcscspn_nid_postfix(u"abc", u"a") == 0); char16_t tokens[] = u",,one,,two;three,"; char16_t* last = tokens; @@ -238,5 +245,21 @@ int main() { require(same(transformed, test.expected, test.written)); for (std::size_t index = test.written; index < 8; ++index) require(transformed[index] == 0xaaaa); } + { + const char16_t number[] = u" -0x7fffffffffffffff9"; + char16_t* wideEnd = nullptr; + require(wcstoimax_nid_postfix(number, &wideEnd, 0) == INT64_MIN && wideEnd == number + 22); + const char16_t positive[] = u"+42z"; + require(wcstoumax_nid_postfix(positive, &wideEnd, 10) == 42 && wideEnd == positive + 3); + require(wcstoumax_nid_postfix(u"-1", nullptr, 10) == UINT64_MAX); + char* narrowEnd = nullptr; + const char text[] = "0x1ffffffffffffffffg"; + require(strtouq_nid_postfix(text, &narrowEnd, 16) == UINT64_MAX && narrowEnd == text + 19); + const char path[] = "a/b/c"; + require(rindex_nid_postfix(path, '/') == path + 3 && rindex_nid_postfix(path, 'x') == nullptr); + require(rindex_nid_postfix(path, 0) == path + 5); + require(flsl_nid_postfix(0) == 0 && flsl_nid_postfix(1) == 1 && flsl_nid_postfix(0x80) == 8); + require(flsl_nid_postfix(0x100000000ll) == 33 && flsl_nid_postfix(INT64_MIN) == 64 && flsl_nid_postfix(-1) == 64); + } return 0; } diff --git a/core/libs/tests/Mspace.cpp b/core/libs/tests/Mspace.cpp index 386435ded669..1f2b8cc2bccb 100644 --- a/core/libs/tests/Mspace.cpp +++ b/core/libs/tests/Mspace.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include extern "C" { @@ -22,6 +23,15 @@ void* APS5_VABI sceLibcMspaceAlignedAlloc_nid_postfix(void*, std::size_t, std::s void* APS5_VABI sceLibcMspaceReallocalign_nid_postfix(void*, void*, std::size_t, std::size_t); int APS5_VABI sceLibcMspaceMallocStats_nid_postfix(void*, void*); int APS5_VABI sceLibcMspaceMallocStatsFast_nid_postfix(void*, void*); +void* APS5_VABI sceLibcPafMspaceCreate_nid_postfix(const char*, void*, std::size_t, unsigned, const void*); +int APS5_VABI sceLibcPafMspaceDestroy_nid_postfix(void*); +void* APS5_VABI sceLibcPafMspaceMalloc_nid_postfix(void*, std::size_t, const void*); +void APS5_VABI sceLibcPafMspaceFree_nid_postfix(void*, void*, const void*); +void* APS5_VABI sceLibcPafMspaceCalloc_nid_postfix(void*, std::size_t, std::size_t, const void*); +void* APS5_VABI sceLibcPafMspaceRealloc_nid_postfix(void*, void*, std::size_t, const void*); +void* APS5_VABI sceLibcPafMspaceReallocalign_nid_postfix(void*, void*, std::size_t, std::size_t, const void*); +int APS5_VABI sceLibcPafMspaceMallocStats_nid_postfix(void*, void*); +int APS5_VABI sceLibcPafMspaceMallocStatsFast_nid_postfix(void*, void*); } struct MallocManagedSize { std::uint16_t size; @@ -35,6 +45,38 @@ struct MallocManagedSize { static void Require(bool value) { if (!value) { std::fputs("Mspace check failed\n", stderr); std::abort(); } } +static void CheckPaf() { + alignas(64) static std::array storage{}; + bool rejected = false; + int options = 0; + try { sceLibcPafMspaceCreate_nid_postfix("paf", storage.data(), storage.size(), 0, &options); } + catch (const std::runtime_error&) { rejected = true; } + Require(rejected); + void* arena = sceLibcPafMspaceCreate_nid_postfix("paf", storage.data(), storage.size(), 0, nullptr); + Require(arena == storage.data()); + const int owner = 0; + auto* block = static_cast(sceLibcPafMspaceMalloc_nid_postfix(arena, 40, &owner)); + Require(block != nullptr && sceLibcMspaceMallocUsableSize_nid_postfix(block) >= 40); + std::memset(block, 0x5a, 40); + auto* zeroed = static_cast(sceLibcPafMspaceCalloc_nid_postfix(arena, 4, 8, arena)); + Require(zeroed != nullptr && zeroed[0] == 0 && zeroed[31] == 0); + block = static_cast(sceLibcPafMspaceRealloc_nid_postfix(arena, block, 4000, &owner)); + Require(block != nullptr && block[0] == 0x5a && block[39] == 0x5a); + block = static_cast(sceLibcPafMspaceReallocalign_nid_postfix(arena, block, 256, 64, &owner)); + Require(block != nullptr && reinterpret_cast(block) % 256 == 0 && block[39] == 0x5a); + MallocManagedSize viaPaf{}, viaMspace{}, fastPaf{}, fastMspace{}; + viaPaf.size = viaMspace.size = fastPaf.size = fastMspace.size = sizeof(MallocManagedSize); + viaPaf.version = viaMspace.version = fastPaf.version = fastMspace.version = 1; + Require(sceLibcPafMspaceMallocStats_nid_postfix(arena, &viaPaf) == sceLibcMspaceMallocStats_nid_postfix(arena, &viaMspace)); + Require(sceLibcPafMspaceMallocStatsFast_nid_postfix(arena, &fastPaf) == sceLibcMspaceMallocStatsFast_nid_postfix(arena, &fastMspace)); + Require(viaPaf.currentInuseSize == viaMspace.currentInuseSize && viaPaf.currentInuseSize > 0); + Require(fastPaf.currentInuseSize == fastMspace.currentInuseSize); + sceLibcPafMspaceFree_nid_postfix(arena, block, &owner); + sceLibcPafMspaceFree_nid_postfix(arena, zeroed, arena); + sceLibcPafMspaceFree_nid_postfix(arena, nullptr, &owner); + Require(sceLibcPafMspaceDestroy_nid_postfix(arena) == 0); + Require(sceLibcPafMspaceDestroy_nid_postfix(arena) == -1); +} static void CheckAlignment() { alignas(32) static std::array storage{}; void* arena = sceLibcMspaceCreate_nid_postfix("aligned", storage.data() + 16, 65536, 0); @@ -107,6 +149,7 @@ static void CheckMovedBlockKeepsUsableBytes() { Require(sceLibcMspaceDestroy_nid_postfix(arena) == 0); } int main() { + CheckPaf(); CheckAlignment(); CheckAdjacentBlocks(); CheckMovedBlockKeepsUsableBytes(); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp index b991e7f3087c..0f214efbfebe 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp @@ -308,7 +308,9 @@ bool AllOnesWord(const IrValue* word) { } bool AllLanesBit(const IrValue* bit) { - const auto words = LaneBitWords(Resolved(bit)); + bit = Resolved(bit); + if (Is(bit, IrOpcode::LogicalOr) && EitherOrder(bit, [](const IrValue* inverted, const IrValue* other) { return Is(inverted, IrOpcode::LogicalNot) && Resolved(inverted->Argument(0)) == other; })) return true; + const auto words = LaneBitWords(bit); return words && AllOnesWord((*words)[0]) && AllOnesWord((*words)[1]); } @@ -609,6 +611,69 @@ std::optional EmitHalfWaveReduction(SpirvValueEmitContext& ctx, c return result; } +bool ActiveMaskedWord(const IrValue* word, std::uint32_t depth = 0u) { + word = Resolved(word); + if (word == nullptr || depth > 8u) return false; + if (Is(word, IrOpcode::CompositeExtractU32x4)) { + const auto* ballot = Resolved(word->Argument(0)); + const auto* active = ballot != nullptr && Is(ballot, IrOpcode::Ballot) ? Resolved(ballot->Argument(0)) : nullptr; + return active != nullptr && active->HasImmediate() && active->Type() == IrType::Bool && active->ImmediateBool() && IsU32(word->Argument(1), 0u); + } + return Is(word, IrOpcode::BitwiseAnd32) && (ActiveMaskedWord(word->Argument(0), depth + 1u) || ActiveMaskedWord(word->Argument(1), depth + 1u)); +} + +bool ActiveMaskedBit(const IrValue* bit) { + const auto words = LaneBitWords(Resolved(bit)); + return words && ActiveMaskedWord((*words)[0]) && ActiveMaskedWord((*words)[1]); +} + +std::optional MatchRowScan(const IrValue* value, IrShaderStage stage, bool narrow) { + const auto* top = Unmasked(Resolved(value), narrow); + for (const auto& reduction : WaveReductions) { + if (reduction.floating) continue; + const IrValue* scan = top; + for (const auto control : {0x118u, 0x114u, 0x112u, 0x111u}) { + scan = RowScanStepInput(scan, reduction, control, narrow); + if (scan == nullptr) break; + } + if (scan == nullptr) continue; + const auto* source = Unmasked(scan, narrow); + if (Is(source, IrOpcode::SelectU32) && IsU32(source->Argument(2), reduction.identity) && (ActiveMaskedBit(source->Argument(0)) || EntryLaneWalk{stage}.ZeroBit(source->Argument(0)))) return HalfWaveScan{&reduction, source}; + } + return std::nullopt; +} + +std::optional EmitRowReduction(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t lane) { + auto& state = ctx.state; + const bool split = state.splitSubgroup && state.hostSubgroupSize == 64u && state.program.WaveSize() == 32u; + if ((lane & 15u) != 15u || state.laneCount != 1u || HostSubgroupSingleLane(state) || (!split && (state.splitSubgroup || state.hostSubgroupSize != state.program.WaveSize()))) return std::nullopt; + const auto stage = state.program.Resources().stage; + auto scan = MatchRowScan(inst.Argument(0), stage, false); + if (!scan) scan = MatchRowScan(inst.Argument(0), stage, true); + if (!scan) return std::nullopt; + const auto& capabilities = state.supportedCapabilities; + if (std::find(capabilities.begin(), capabilities.end(), static_cast(spv::CapabilityGroupNonUniformArithmetic)) == capabilities.end()) return std::nullopt; + const auto u32 = TypeU32(state); + const auto boolean = TypeBool(state); + const auto identity = ConstantU32(state, scan->reduction->identity); + const auto row = Binary(state, spv::OpShiftRightLogical, u32, EmitSubgroupLocalInvocationId(state), ConstantU32(state, 4u)); + const auto inRow = Binary(state, spv::OpIEqual, boolean, row, ConstantU32(state, lane >> 4u)); + const auto source = ctx.Def(scan->source); + state.module.EmitCapability(spv::CapabilityGroupNonUniformArithmetic); + const auto reduce = [&](std::uint32_t include) { + const auto keys = Select(state, u32, include, source, identity); + const auto result = state.module.AllocateId(); + state.module.AddFunction(scan->reduction->reduce, u32, result, ConstantU32(state, spv::ScopeSubgroup), static_cast(spv::GroupOperationReduce), keys); + return result; + }; + if (!split) return reduce(inRow); + const auto upper = Binary(state, spv::OpUGreaterThanEqual, boolean, EmitHostSubgroupLane(state, ConstantU32(state, 0u)), ConstantU32(state, 32u)); + const auto lower = Unary(state, spv::OpLogicalNot, boolean, upper); + const auto lowerTotal = reduce(Binary(state, spv::OpLogicalAnd, boolean, inRow, lower)); + const auto upperTotal = reduce(Binary(state, spv::OpLogicalAnd, boolean, inRow, upper)); + return Select(state, u32, upper, upperTotal, lowerTotal); +} + } std::uint32_t EmitConvertU16U32(SpirvEmitterState& state, std::uint32_t arg0) { @@ -1045,6 +1110,7 @@ std::uint32_t EmitReadLane(SpirvValueEmitContext& ctx, const IrValue& inst) { if (selector != nullptr && selector->HasImmediate() && state.program.Resources().stage != IrShaderStage::Compute && state.laneCount == 1u) { const auto index = selector->ImmediateU32() & (state.program.WaveSize() - 1u); if (const auto reduced = EmitHalfWaveReduction(ctx, inst, index)) return *reduced; + if (const auto reduced = EmitRowReduction(ctx, inst, index)) return *reduced; if (HostSubgroupNarrowerThanWave(state) && index >= state.hostSubgroupSize) { if (index >= 32u && UpperLaneWalk{state.program.Resources().stage}.ZeroValue(inst.Argument(0))) return ConstantU32(state, 0u); FailOutsideHostSubgroup(ctx, inst, "v_readlane_b32 of lane " + std::to_string(index)); diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index 75a1c104697e..eecff6db7ddf 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -12,6 +12,7 @@ - [sceAudioOutSetMixLevelPadSpk](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - effect of a negative mix level unknown; only 0 to 32768 (0 dB) is accepted - [sceAudioOutSetMixLevelPadSpk](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - no source applies the level (shadPS4 only stores it); it is assumed to be a linear gain on the port's own output, and the -9 dB default (11626) is applied to every pad speaker port, including those of titles that never call the function - [sceAudioOutGetLastOutputTime](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - signature and behaviour taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [fpPS4](https://github.com/red-prig/fpPS4) reimplementations of the PS4 library, not confirmed on a PS5 title: the time is the process time at which `sceAudioOutOutput` or `sceAudioOutOutputs` accepted the port's last block, not the time the block reached the device +- [sceAudioOutGetSystemState](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - signature and layout taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [fpPS4](https://github.com/red-prig/fpPS4) reimplementations, and matches `sceAudioOut2GetSystemState`; loudness is reported as 0.0f. Imported by PPSA30528, which fails to load without it - [sceAudioOutSysClose, sceAudioOutSysConfigureOutput, sceAudioOutSysOpen](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - imported by PPSA99051 and PPSA99997, which fail to load without them; added without an implementation and declared without parameters. [EVO-PLAYER-PS5](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/da0c0451333bab82e79fffd2de4718e917f9dcb0/projects/evoplayer/core/src/audio/evo_pt.cpp#L166-L240), whose audio code both titles share, calls `sceAudioOutSysOpen`, `sceAudioOutSysConfigureOutput` and `sceAudioOutSysClose` only to open a port for HDMI bitstream passthrough. Its declared arguments are not confirmed on a console. Audio reaches the host device through SDL as PCM, so there is no HDMI output to switch - [sceAudioOut2PortCreate](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - MAIN (type 0) format validation follows PS5 firmware 13.60 measurements: bits 8-11 accept 1, 2, 8 or 12, bits 0-6 accept 0 or 1, bit 7 requires 8 channels, and bits 12-31 do not affect validation. The sample types are still read as float and int16; bit 7's layout, the PCM effect of bits 12-31 and the port `flags` are unverified. Other port types retain unverified format validation, including 6 channels and rejection of bits 12-31 - [sceAudioOut2PortCreate](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - 12 channels are assumed to be L R C LFE Ls Rs Lb Rb Ltf Rtf Ltb Rtb as in KytyPS5's 12-to-8 fold, and 6 and 8 channels to be its first 6 and 8; no title or SDK header confirms the order diff --git a/docs/dev/debt/libSceAudioPropagation.md b/docs/dev/debt/libSceAudioPropagation.md index 0785892d9300..eee79ac53121 100644 --- a/docs/dev/debt/libSceAudioPropagation.md +++ b/docs/dev/debt/libSceAudioPropagation.md @@ -2,7 +2,7 @@ ### Silent stubs -- [libSceAudioPropagation](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) implements only the no-acoustic-propagation path: the system keeps a 16-byte header in the title's CPU memory (no GPU memory), rooms, portals, sources and materials are tracked handles, attributes are validated and have no effect, no rays are requested and sources report no audio paths. Ray results, path calculation and audio paths are accepted only when empty and throw otherwise; `sceAudioPropagationSourceRender` writes silence (zeroes) to each source's output buffer for the given size, without a dry signal. Objects still alive when their system is destroyed become unusable, and their later destroy or unregister is a no-op: PPSA21567 releases its reference-counted room, portal and source owners just before `sceAudioPropagationSystemDestroy`, so the order for every object is not proven. Occlusion, reflections and reverb from level geometry are absent; in-game behaviour remains unverified. +- [libSceAudioPropagation](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) implements only the no-acoustic-propagation path: the system keeps a 16-byte header in the title's CPU memory (no GPU memory), rooms, portals, sources and materials are tracked handles, attributes are validated and have no effect, no rays are requested and sources report no audio paths. Ray results, path calculation and audio paths are accepted only when empty and throw otherwise; `sceAudioPropagationSourceRender` preserves each destination mix because there are no path contributions, following the additive-output contract in [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioPropagation.cs); console behaviour has not been measured. It adds no dry signal. Objects still alive when their system is destroyed become unusable, and their later destroy or unregister is a no-op: PPSA21567 releases its reference-counted room, portal and source owners just before `sceAudioPropagationSystemDestroy`, so the order for every object is not proven. Occlusion, reflections and reverb from level geometry are absent; in-game behaviour remains unverified. ### Unknown function info diff --git a/docs/dev/debt/libSceLibcInternal.md b/docs/dev/debt/libSceLibcInternal.md index 7cc67d0d95c9..be978908f4dc 100644 --- a/docs/dev/debt/libSceLibcInternal.md +++ b/docs/dev/debt/libSceLibcInternal.md @@ -14,4 +14,5 @@ - [fstatvfs](../../../core/libs/prx/libSceLibcInternal/FilesystemStatistics.cpp) (libSceLibcInternal) converts `_fstatfs` to the 88-byte `struct statvfs` as FreeBSD 9.2's `sfs2svfs` does (`f_bsize` from the transfer size, `f_frsize` from the block size, `f_favail` equal to `f_ffree`, `f_fsid` 0, `ST_RDONLY` and `ST_NOSUID` only). Differences: `f_namemax` is the `statfs` value, not `fpathconf(_PC_NAME_MAX)`; negative `f_bavail` and `f_ffree` wrap to unsigned without `EOVERFLOW`, as the same-width copy in FreeBSD does; inode counts are 0 on Windows, inherited from `_fstatfs`; a null output returns `EFAULT` after the descriptor is validated, where FreeBSD would fault. Layout and conversion are unverified against the PS5 library (PS5MC imports `fstatvfs` but was not run) - [posix_spawn and posix_spawnp](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) (libSceLibcInternal) throw: creating a child process is not supported. PPSA99305 (Dusklight) and PPSA99309 (Wind Waker) stop on `main` at unresolved import `44hlATrd47U` (`posix_spawnattr_init`); a run with this PR has not established how far either title gets, and process creation still throws. The attribute and file action calls keep FreeBSD 11.4's objects, so a real `posix_spawn` would have to apply them in the child. Null or uninitialized objects, which FreeBSD dereferences, throw. - [getopt, getopt_long](../../../core/libs/prx/libSceLibcInternal/Getopt.cpp) (libSceLibcInternal) follow FreeBSD 11 [getopt.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/getopt.c) and [getopt_long.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/getopt_long.c) with `GNU_COMPATIBLE`: `getopt` does not permute, `getopt_long` permutes unless `POSIXLY_CORRECT` is set in the guest environment or the option string starts with `+`, and the exported `opterr`, `optind`, `optopt`, `optreset` and `optarg` start at 1, 1, 0, 0 and null. Warnings go to the guest's current `stderr` prefixed with `__progname`, where FreeBSD's `warnx` keeps the `stderr` of its first call. A null `getopt` option string, a null argument vector or argument, a negative `optind` and an `optind` moved inside a pending permutation throw where FreeBSD faults. `getopt_long_only` is not exported. That the console's `libSceLibcInternal` uses this FreeBSD code is unverified +- [posix_spawnattr_getflags, getpgroup, getschedparam, getschedpolicy, getsigdefault, getsigmask, setpgroup, setschedparam, setschedpolicy and setsigmask](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) (libSceLibcInternal) read and write the 0x30-byte attribute object at the offsets of the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x8D860 to 0x8D910: flags at 0, process group at 4, scheduling parameter at 8, policy at 0xC, default and masked signal sets at 0x10 and 0x20), which never fail there and return 0; the layout matched on a PS5 (firmware 13.60). Uninitialized attributes and null pointers, which the console dereferences, throw as the existing setters do - [sys_siglist and sys_nsig](../../../core/libs/prx/libSceLibcInternal/SignalList.cpp) (libSceLibcInternal) hold the 32 descriptions and the count of [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/siglist.c#L73-L107) `siglist.c` (`NSIG` 32, signal 0 to `SIGUSR2`); the console's table was not read diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 52ea543c6f75..8a98294cc819 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -14,6 +14,7 @@ - [Guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) (libc) - the application map area is assumed to end at 0xFC_0000_0000, as `SCE_KERNEL_APP_MAP_AREA_END_ADDR` does on the PS4. On Windows, host allocations made in the arena before libc loads stay, and a fixed guest mapping over one throws - [sceLibcInternalBacktraceForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - `(const char* heapName)` and its return follow the `libc.prx` ASTRO BOT (PPSA21564 01.007.000) bundles: its four calls (0x6801, 0x7F46, 0xE75E, 0xEE02) are in the `malloc_stats` and `sceLibcMspaceMallocStats` paths, each after a `[%s] ...` line printed with the same heap name buffer, and the caller ignores the result and continues. The console's backtrace output is not reproduced: the heap name and the guest return address are written to the host standard error - [sceLibcInternalHeapErrorReportForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - `(void* heap, void* block, uint32 error)` follows the same `libc.prx` (call at 0xF634): it passes the heap, the block and the nonzero result of its block check, and then calls a function that never returns, so the report only has to return. The meaning of the error values is unknown; the three values and the guest return address are written to the host standard error +- [sceLibcPafMspaceCreate, Destroy, Malloc, Free, Calloc, Realloc, Reallocalign, MallocStats and MallocStatsFast](../../../core/libs/prx/libc/src/Mspace.cpp) (libSceLibcInternal, implemented in libc) follow the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x5D680 to 0x5D770): each calls the same internal routine as its `sceLibcMspace` counterpart without the malloc callback hook those add, and `Reallocalign` takes the alignment before the size as `sceLibcMspaceReallocalign` does. `Create` takes a fifth argument, a pointer to a creation options block (a version word, a type of 1 or 2 at offset 2, a field at 0x10) that the `Mspace` form passes as null; a non-null block throws, as its meaning is unknown. `Malloc`, `Free`, `Calloc`, `Realloc` and `Reallocalign` take a trailing owner value that the console XORs with its block magic and stores in each block's footer, where the `Mspace` forms pass the heap itself. Footers are not modelled here, so the owner is ignored. Measured on a PS5 (firmware 13.60) from a homebrew payload, the calls with a matching owner behave as here, but the console ends the process when `Reallocalign` is given a block whose owner differs and when it resizes a block in place with an owner other than the heap, and `MallocStats` returns 1 instead of 0 while the heap holds blocks owned by something other than the heap; none of this is reproduced. `PafMspaceMemalign`, `PosixMemalign`, `MallocUsableSize`, `Trim`, `IsHeapEmpty`, `CheckMemoryBounds`, `ReportMemoryBlocks` and `GetFooterValue` are not implemented - [malloc_usable_size](../../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - with the default allocator it returns the size the block was requested with (the range registered for it), not the rounded-up chunk size Sony's allocator reports; both satisfy "at least the requested size", and bytes past the request would not be write-watched. With an application allocator it calls entry 9 of the replacement table (`malloc_usable_size` in KytyPS5's `MallocReplace` order) and throws when the table leaves it null, like `malloc_stats_fast` - [malloc, calloc, realloc, memalign, aligned_alloc and reallocalign](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - follow FreeBSD 11.4 `contrib/jemalloc/src/jemalloc.c` for failures, not measured on the console: a size the default heap rejects or the guest arena cannot place (including a `calloc` product that overflows) returns null with `errno` `ENOMEM`, and `memalign` and `aligned_alloc` return null with `EINVAL` for an alignment that is zero or not a power of two. FreeBSD's libc has no `memalign`, so setting `errno` there is assumed. A host commit failure for a block that fits in the arena (Windows `ERROR_NOT_ENOUGH_MEMORY`, `ERROR_OUTOFMEMORY` or `ERROR_COMMITMENT_LIMIT`, Linux `ENOMEM`) resets and releases the reserved range and returns null with `ENOMEM`; any other commit failure releases the range and throws. `reallocalign` returns null with `EINVAL` for an invalid alignment and `ENOMEM` like the others, which is an assumption as it is not in FreeBSD. `realloc(ptr, 0)` frees the block and returns null, where FreeBSD 11.4's jemalloc 4.5 resizes it to one byte and returns a block - [reallocf](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libSceLibcInternal, implemented in libc) - follows FreeBSD 11 `lib/libc/stdlib/reallocf.c` on top of this libc's `realloc`, not measured on the console: size 0 frees the block and returns null as `realloc` does here, so the block is not freed twice; when `realloc` fails because the application allocator returns null or the default heap rejects the size, the block is freed through the same allocator, `errno` is `ENOMEM` and null is returned. Other failures (a commit failure that is not out of memory, an unknown pointer) throw, as they do for `realloc` @@ -48,11 +49,13 @@ - [realpath](../../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. - [strtod, strtof, strtold](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `NAN(n-char-sequence)` converts to the quiet NaN that `nan` and `nanf` return, negated after a minus sign, and ignores the sequence as they ignore their tag. This is inferred from C17 7.12.11.2p2, which makes `nan("n")` equivalent to `strtod("NAN(n)", NULL)`, and not observed on the console. No console `nanl` was observed, so `strtold` returns the x87 default quiet NaN (exponent `0x7fff`, significand `0xc000000000000000`) - [nan and nanf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return fixed quiet NaN values (`0x7fc00000` and `0x7ff8000000000000`), ignoring the tag argument as observed in the console's `libc.prx` (both exports load a global constant via `mov rax,[rip+..]; vmovsd xmm0,[rax]; ret`); VLC (PPSA85300) stops on nanf at start-up after strsep. +- [seed48 and lcong48](../../../core/libs/prx/libc/src/Random.cpp) (libSceLibcInternal, implemented in libc) follow the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x6F460 and 0x6F420): the multiplier and addend are shared by the whole `rand48` family, including `erand48`, `nrand48` and `jrand48` on a caller's seed; `lcong48` sets them with the seed, and `srand48` and `seed48` restore 0x5DEECE66D and 0xB. Unlike FreeBSD, `seed48` returns a static copy of the new seed rather than the previous one. A null argument, which the console dereferences, throws - [lgamma](../../../core/libs/prx/libc/src/Math.cpp) (libc) returns the host's `lgamma` but does not store the sign of Γ(x) in the guest's `signgam`, as FreeBSD 11's [`e_lgamma.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/e_lgamma.c) does through `__ieee754_lgamma_r`, because no guest `signgam` is exported. PPSA99004 imports it. Console behaviour is unverified - [sinhf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `sinhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_sinhf.c`](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_sinhf.c) from before `__ldexp_expf`, which squares `expf(|x| / 2)` between `logf(FLT_MAX)` and the overflow threshold, calling msun's [`expm1f`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_expm1f.c) and [`expf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_expf.c), whose paths for the arguments it passes are inlined here. Past the threshold Sony's error helper sets `errno` to `ERANGE` before returning `x * 1e37`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits as this implementation for every finite argument up to the threshold, of both signs; it was not run on Linux. That the system `libSceLibcInternal`, which Ruffle (PPSA68091) and RetroArch (PPSA99169) import while bundling a zero-return `libc.prx`, has the same code is assumed - [asinl](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `asinl` of the `libc.prx` bundled with PPSA28319 (disassembly), FreeBSD msun's [`e_asinl.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/src/e_asinl.c) with the ld80 [`invtrig.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/ld80/invtrig.c) coefficients, which the binary holds as the same 80-bit constants. For |x| > 1, the infinities and NaN, Sony's error helper raises `FE_INVALID` and sets `errno` to `EDOM` before returning `(x - x) / (x - x)`, so a NaN argument also sets `EDOM`; msun itself sets no `errno`. 20 million random arguments in (-1, 1) give the same bits as that code run on a Windows host. The result needs the x87 precision control at 64 bits, as FreeBSD amd64 starts threads (control word `0x37f`), but guest threads do not set it: on Windows a thread started by `_beginthreadex` runs at 53 bits (`0x27f`), where `asinl(0.75)` returns mantissa `0xd91a98ae3406e000` instead of `0xd91a98ae3406e041`. The system `libSceLibcInternal` that RetroArch (PPSA99169) imports is assumed to have the same code - [atanhf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `atanhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_atanhf.c`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_atanhf.c) calling msun's [`log1pf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_log1pf.c), whose paths for the positive arguments it passes are inlined here; the exported `log1pf` is still the host's. For |x| > 1, the infinities and NaN, Sony's error helper raises `FE_INVALID` and sets `errno` to `EDOM` before returning `(x - x) / (x - x)`, and for |x| = 1 it raises `FE_DIVBYZERO` and sets `errno` to `ERANGE` before returning `x / 0`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits, `errno` and floating-point exceptions as this implementation for all 2^32 arguments; it was not run on Linux. That the system `libSceLibcInternal`, which Dusklight (PPSA99305) imports while bundling a zero-return `libc.prx`, has the same code is assumed - [getenv, setenv, unsetenv, putenv](../../../core/libs/prx/libc/src/Environment.cpp) (libc) keep one guest environment that starts empty, like the `environ` libkernel exports, and never reads or changes the host's. A title sees only the variables it sets itself; host variables such as `HOME` and `PATH` name host paths a guest cannot use (PS5MC, PPSA99420, exits with `XBMC_Run returned -1` when it gets the host's `HOME`). Whether the console starts a title with any variables set is not measured +- [mergesort](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libSceLibcInternal, implemented in libc) checks its arguments like the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x762D0): an element size under 4 sets `errno` to `EINVAL` (22) and returns -1, an empty array returns 0, and a failed `malloc` of the count times the size plus 8 bytes returns -1. The sort is a stable bottom-up merge, not FreeBSD's natural merge with its insertion sort for short runs, so the comparator is called in a different order and number of times; the result is the same for a consistent comparator. A null base or comparator and a size product that overflows, which the console does not check, throw - On macOS the [write watch](../../../core/libs/prx/libc/src/GuestWriteWatch.cpp) write-protects watched pages and records the first write to each from a SIGBUS/SIGSEGV handler, because macOS has no counterpart to `userfaultfd` write-protect or `MEM_WRITE_WATCH`. The protection is real, so: - a system call that writes into a protected page fails with `EFAULT` instead of faulting. `GuestArena::HostWrite` lifts the protection for the calls it brackets (file reads and socket receives); a host call that writes guest memory without it (`clock_gettime` and the like into a watched page) fails; - code that changes the protection of watched pages, or maps over them, has to call `GuestWriteWatchProtectionChanged`, and code that asks the kernel for a page's protection has to ask `GuestWriteWatchProtection` first, since a watched page is read-only between a collection and the next write. Nothing calls either yet: the macOS memory paths are not in the tree;