From 6dbdd927eed09f0b7e12f483c3ac157dd4512c70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 08:01:34 +0200 Subject: [PATCH] feat(ngs2): system sample rate, user data and lock Co-Authored-By: Claude --- .../libSceNgs2.native/src/Ngs2Internal.hpp | 1 + .../libs/prx/libSceNgs2.native/src/System.cpp | 49 +++++++++++++ .../libSceNgs2.native/tests/Ngs2Filter.cpp | 18 +++++ .../libSceNgs2.native/tests/Ngs2Render.cpp | 72 +++++++++++++++++++ .../prx/libSceNgs2.native/tests/Ngs2Test.hpp | 7 +- docs/dev/TechnicalDebt.md | 1 + 6 files changed, 147 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp index 615380f59..dd2dd3cc7 100644 --- a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp +++ b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp @@ -119,6 +119,7 @@ struct Ngs2System { Ngs2BufferAllocator allocator{}; std::uint32_t uid = 0; std::int64_t renderCount = 0; + std::uintptr_t userData = 0; std::vector racks; }; diff --git a/core/libs/prx/libSceNgs2.native/src/System.cpp b/core/libs/prx/libSceNgs2.native/src/System.cpp index 1bb850606..468dcc39a 100644 --- a/core/libs/prx/libSceNgs2.native/src/System.cpp +++ b/core/libs/prx/libSceNgs2.native/src/System.cpp @@ -188,6 +188,55 @@ int APS5_VABI sceNgs2SystemSetGrainSamples(uintptr_t system_handle, uint32_t num return SCE_NGS2_OK; } +int APS5_VABI sceNgs2SystemSetSampleRate(uintptr_t system_handle, uint32_t sample_rate) { + std::lock_guard lock(Ngs2Mutex()); + auto* system = Ngs2FindSystem(system_handle); + if (system == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; + if (sample_rate == 0) APS5_INVALID_ARG_EX; + for (const auto* rack : system->racks) { + for (const auto& voice : rack->voices) { + for (const auto& filter : voice.filters) { + if (filter.enabled && sample_rate != system->option.sample_rate) { + throw std::runtime_error("NGS2: changing the sample rate under an enabled sampler filter is not implemented"); + } + } + } + } + system->option.sample_rate = sample_rate; + return SCE_NGS2_OK; +} + +int APS5_VABI sceNgs2SystemSetUserData(uintptr_t system_handle, uintptr_t user_data) { + std::lock_guard lock(Ngs2Mutex()); + auto* system = Ngs2FindSystem(system_handle); + if (system == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; + system->userData = user_data; + return SCE_NGS2_OK; +} + +int APS5_VABI sceNgs2SystemGetUserData(uintptr_t system_handle, uintptr_t* user_data) { + std::lock_guard lock(Ngs2Mutex()); + const auto* system = Ngs2FindSystem(system_handle); + if (system == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; + if (user_data == nullptr) APS5_INVALID_ARG_EX; + *user_data = system->userData; + return SCE_NGS2_OK; +} + +int APS5_VABI sceNgs2SystemLock(uintptr_t system_handle) { + Ngs2Mutex().lock(); + if (Ngs2FindSystem(system_handle) != nullptr) return SCE_NGS2_OK; + Ngs2Mutex().unlock(); + return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; +} + +int APS5_VABI sceNgs2SystemUnlock(uintptr_t system_handle) { + std::lock_guard lock(Ngs2Mutex()); + if (Ngs2FindSystem(system_handle) == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; + Ngs2Mutex().unlock(); + return SCE_NGS2_OK; +} + int APS5_VABI sceNgs2SystemRender(uintptr_t system_handle, const Ngs2RenderBufferInfo* buffer_info, uint32_t num_buffer_info) { std::lock_guard lock(Ngs2Mutex()); auto* system = Ngs2FindSystem(system_handle); diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Filter.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Filter.cpp index 0c7f193c4..cf47e314b 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Filter.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Filter.cpp @@ -173,10 +173,28 @@ static void TestRejectedParams() { Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } +static void TestRateChangeUnderFilter() { + const auto system = CreateSystem(); + const auto sampler = Sampler(system, std::vector(Grain, 0), 1, 48000); + SetFilter(sampler, Filter(0, 1, 1, 0, 1000.0f, 1.0f, 1.0f)); + Require(sceNgs2SystemSetSampleRate(system, 48000) == SCE_NGS2_OK); + bool rejected = false; + try { + sceNgs2SystemSetSampleRate(system, 96000); + } catch (const std::runtime_error&) { + rejected = true; + } + Require(rejected); + SetFilter(sampler, Filter(0, 1, 0, 0, 0.0f, 1.0f, 1.0f)); + Require(sceNgs2SystemSetSampleRate(system, 96000) == SCE_NGS2_OK); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + int main() { TestLowPassAndTail(); TestBypassMaskAndGain(); TestSystemRateAndZeroCutoff(); TestRejectedParams(); + TestRateChangeUnderFilter(); return 0; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp index ea0f7c9eb..5250c7eee 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp @@ -1,7 +1,11 @@ #include "Ngs2Test.hpp" +#include +#include #include #include +#include +#include #include static uintptr_t Sampler(uintptr_t system, const std::vector& pcm, std::uint32_t repeats) { @@ -152,6 +156,71 @@ static std::int32_t APS5_VABI Release(Ngs2ContextBufferInfo* info) { return SCE_NGS2_OK; } +static void TestSampleRate() { + const auto system = CreateSystem(); + Require(sceNgs2SystemSetSampleRate(0x1234, 96000) == SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE); + bool threw = false; + try { + sceNgs2SystemSetSampleRate(system, 0); + } catch (const std::invalid_argument&) { + threw = true; + } + Require(threw); + Require(sceNgs2SystemSetSampleRate(system, 96000) == SCE_NGS2_OK); + Ngs2SystemInfo info{}; + Require(sceNgs2SystemGetInfo(system, &info, sizeof(info)) == SCE_NGS2_OK && info.sample_rate == 96000); + + const auto master = Mastering(system, 1); + const std::vector pcm{0, 1000, 2000, 3000}; + const auto sampler = Sampler(system, pcm, 1); + Patch(sampler, master); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + const std::int16_t expected[Grain] = {0, 500, 1000, 1500, 2000, 2500, 3000, 1500}; + const auto out = RenderI16(system); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == expected[i]); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +static void TestUserData() { + uintptr_t value = 1; + Require(sceNgs2SystemSetUserData(0x1234, 5) == SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE); + Require(sceNgs2SystemGetUserData(0x1234, &value) == SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE && value == 1); + const auto system = CreateSystem(); + Require(sceNgs2SystemGetUserData(system, &value) == SCE_NGS2_OK && value == 0); + Require(sceNgs2SystemSetUserData(system, 0xfedcba9876543210) == SCE_NGS2_OK); + Require(sceNgs2SystemGetUserData(system, &value) == SCE_NGS2_OK && value == 0xfedcba9876543210); + bool threw = false; + try { + sceNgs2SystemGetUserData(system, nullptr); + } catch (const std::invalid_argument&) { + threw = true; + } + Require(threw); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +static void TestLock() { + const auto system = CreateSystem(); + Require(sceNgs2SystemLock(0x1234) == SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE); + Require(sceNgs2SystemUnlock(0x1234) == SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE); + Ngs2SystemInfo info{}; + std::thread free([&] { Require(sceNgs2SystemGetInfo(system, &info, sizeof(info)) == SCE_NGS2_OK); }); + free.join(); + + Require(sceNgs2SystemLock(system) == SCE_NGS2_OK); + std::atomic done = false; + std::thread blocked([&] { + Require(sceNgs2SystemGetInfo(system, &info, sizeof(info)) == SCE_NGS2_OK); + done = true; + }); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + Require(!done); + Require(sceNgs2SystemUnlock(system) == SCE_NGS2_OK); + blocked.join(); + Require(done); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + static void TestAllocator() { const Ngs2BufferAllocator allocator{Allocate, Release, 9}; uintptr_t system = 0; @@ -171,6 +240,9 @@ int main() { TestPcmBlockEnd(); TestPitchAndRepeat(); TestSubmixerMatrix(); + TestSampleRate(); + TestUserData(); + TestLock(); TestAllocator(); return 0; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp index 223618449..0b1429a67 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp @@ -18,6 +18,11 @@ int APS5_VABI sceNgs2SystemCreateWithAllocator(const Ngs2SystemOption*, const Ng int APS5_VABI sceNgs2SystemDestroy(uintptr_t, Ngs2ContextBufferInfo*); int APS5_VABI sceNgs2SystemGetInfo(uintptr_t, Ngs2SystemInfo*, size_t); int APS5_VABI sceNgs2SystemSetGrainSamples(uintptr_t, uint32_t); +int APS5_VABI sceNgs2SystemSetSampleRate(uintptr_t, uint32_t); +int APS5_VABI sceNgs2SystemSetUserData(uintptr_t, uintptr_t); +int APS5_VABI sceNgs2SystemGetUserData(uintptr_t, uintptr_t*); +int APS5_VABI sceNgs2SystemLock(uintptr_t); +int APS5_VABI sceNgs2SystemUnlock(uintptr_t); int APS5_VABI sceNgs2SystemRender(uintptr_t, const Ngs2RenderBufferInfo*, uint32_t); int APS5_VABI sceNgs2RackQueryBufferSize(uint32_t, const Ngs2RackOption*, Ngs2ContextBufferInfo*); int APS5_VABI sceNgs2RackCreate(uintptr_t, uint32_t, const Ngs2RackOption*, const Ngs2ContextBufferInfo*, uintptr_t*); @@ -40,7 +45,7 @@ inline void Check(bool value, int line) { inline constexpr std::uint32_t Grain = 8; -inline std::vector buffers[16]; +inline std::vector buffers[32]; inline std::size_t usedBuffers = 0; inline Ngs2ContextBufferInfo Buffer(const Ngs2ContextBufferInfo& query) { diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 4e605cf32..9ff11e7d2 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -118,6 +118,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceSystemServiceDisableMediaPlay](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature - [sceSystemServiceReenableMediaPlay](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature - [sceSystemServiceParamGetString](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the system name (parameter 6) is the fixed `PS5`, not the console's name; other parameters and buffers of 1 to 64 bytes throw +- [sceNgs2SystemSetSampleRate, sceNgs2SystemSetUserData, sceNgs2SystemGetUserData, sceNgs2SystemLock, sceNgs2SystemUnlock](../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native) - shadPS4 only checks the system handle (`INVALID_SYSTEM_HANDLE`) and SharpProspero names what each call does; neither gives the other limits. A sample rate of 0 and a null user data pointer throw, any other rate is accepted, and changing the rate while a sampler filter is enabled throws because the filter coefficients were computed for the old rate. Lock and unlock take the same lock as `sceNgs2RackLock` - [sceVoiceSetMuteFlag](../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature - [snwprintf_s](../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - follows C11 K.3.9.1.3 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`. A runtime-constraint violation only returns a negative value: `set_constraint_handler_s` is not exported and the console's default handler is unknown. An invalid multibyte `%s` argument is copied byte by byte instead of being reported as an encoding error, as in `vswprintf` - [libSceUlt queues](../../core/libs/prx/libSceUlt/Export.cpp) - the added `sceUltQueuePop`, `sceUltQueueTryPush`, `sceUltQueueDestroy` and `sceUltQueueDataResourcePoolDestroy` signatures are inferred from the existing push/pop and object APIs. Queue destruction is assumed to reject blocked callers with `ULT_ERROR_BUSY` and otherwise discard queued data; pool exhaustion returns `ULT_ERROR_AGAIN`. These rules, alignment checks and shared slot limits have not been confirmed on PS5 hardware or a title. Non-null queue and data-pool option parameters throw because their layouts are unknown.