From 93b887e51469049ae27a24a18a7dee85e207ba2e Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Sun, 25 Oct 2026 01:25:29 -0300 Subject: [PATCH 1/6] feat(ngs2): route custom submixer buffers through Gain --- .../libs/prx/libSceNgs2.native/CMakeLists.txt | 2 +- .../libSceNgs2.native/include/Ngs2Types.hpp | 1 + .../libs/prx/libSceNgs2.native/src/Custom.cpp | 79 +++++-- .../libSceNgs2.native/src/Ngs2Internal.hpp | 8 + .../libs/prx/libSceNgs2.native/src/Render.cpp | 18 +- core/libs/prx/libSceNgs2.native/src/Voice.cpp | 22 +- .../libSceNgs2.native/tests/Ngs2Custom.cpp | 21 +- .../prx/libSceNgs2.native/tests/Ngs2Gain.cpp | 213 ++++++++++++++++++ .../libSceNgs2.native/tests/Ngs2Routing.cpp | 202 +++++++++++++++++ core/libs/prx/libSceNgs2/CMakeLists.txt | 12 + docs/dev/debt/libSceNgs2.native.md | 8 +- 11 files changed, 541 insertions(+), 45 deletions(-) create mode 100644 core/libs/prx/libSceNgs2.native/tests/Ngs2Gain.cpp create mode 100644 core/libs/prx/libSceNgs2.native/tests/Ngs2Routing.cpp diff --git a/core/libs/prx/libSceNgs2.native/CMakeLists.txt b/core/libs/prx/libSceNgs2.native/CMakeLists.txt index 93bb6c445e..422eef37ca 100644 --- a/core/libs/prx/libSceNgs2.native/CMakeLists.txt +++ b/core/libs/prx/libSceNgs2.native/CMakeLists.txt @@ -2,7 +2,7 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/Ngs2Library.cmake) add_sce_ngs2_library(libSceNgs2.native) if(BUILD_TESTING) - foreach(ngs2Test IN ITEMS Render Atrac9 Filter Custom Parse Stream UserFx Report Geom VoiceIndex HandleErrors VoiceHandles) + foreach(ngs2Test IN ITEMS Render Atrac9 Filter Custom Gain Routing Parse Stream UserFx Report Geom VoiceIndex HandleErrors VoiceHandles) string(TOLOWER ${ngs2Test} testName) add_test_executable(ngs2_${testName}_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/Ngs2${ngs2Test}.cpp) target_include_directories(ngs2_${testName}_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src) diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index eda5e07ef3..56a4b938bc 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -27,6 +27,7 @@ static constexpr int SCE_NGS2_ERROR_INVALID_PAN_UNIT_ANGLE = static_cast(0x static constexpr int SCE_NGS2_ERROR_INVALID_PAN_SPEAKER = static_cast(0x804A8451); static constexpr int SCE_NGS2_ERROR_INVALID_VOICE_HANDLE = static_cast(0x804A8203); static constexpr int SCE_NGS2_ERROR_NULL_PARAM_LIST = static_cast(0x804A8360); +static constexpr int SCE_NGS2_ERROR_INVALID_PARAM_ID = static_cast(0x804A835A); static constexpr int SCE_NGS2_ERROR_INVALID_GEOM_LISTENER_ADDRESS = static_cast(0x804A8461); static constexpr int SCE_NGS2_ERROR_INVALID_GEOM_FLAG = static_cast(0x804A8463); static constexpr int SCE_NGS2_ERROR_INVALID_OPTION_SIZE = static_cast(0x804A8013); diff --git a/core/libs/prx/libSceNgs2.native/src/Custom.cpp b/core/libs/prx/libSceNgs2.native/src/Custom.cpp index 5d066d889a..2fb63ebcc8 100644 --- a/core/libs/prx/libSceNgs2.native/src/Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Custom.cpp @@ -89,17 +89,23 @@ int Ngs2CheckCustomRack(std::uint32_t rackId, const Ngs2CustomRackOption& option const int result = CheckBuiltInModule(info); if (result != SCE_NGS2_OK) return result; } - if (option.num_buffers != 1) throw std::runtime_error("NGS2: custom racks with " + std::to_string(option.num_buffers) + " buffers are not implemented"); + if (option.num_buffers != 1 && + (rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER || option.num_modules > 1 || + (option.num_modules == 1 && option.module[0].module_id != SCE_NGS2_CUSTOM_MODULE_ID_GAIN))) { + throw std::runtime_error("NGS2: multiple custom buffers require a submixer with at most one Gain module"); + } for (std::uint32_t m = 0; m < option.num_modules; m++) { const auto moduleId = option.module[m].module_id; if (moduleId == SCE_NGS2_CUSTOM_MODULE_ID_GAIN && option.module[m].option != nullptr) throw std::runtime_error("NGS2: an option for the custom Gain module is not implemented"); - if (moduleId == SCE_NGS2_CUSTOM_MODULE_ID_GAIN && rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) continue; - if (moduleId == SCE_NGS2_CUSTOM_MODULE_ID_GAIN) throw std::runtime_error("NGS2: the custom Gain module on a custom sampler rack is not implemented"); + if (moduleId == SCE_NGS2_CUSTOM_MODULE_ID_GAIN && rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER) { + throw std::runtime_error("NGS2: custom sampler Gain initialization and lifecycle are not implemented"); + } + if (moduleId == SCE_NGS2_CUSTOM_MODULE_ID_GAIN) continue; const char* name = BuiltInModuleName(moduleId); if (name != nullptr) throw std::runtime_error(std::string("NGS2: the custom ") + name + " module is not implemented"); } for (std::uint32_t p = 0; p < option.rack_option.max_ports; p++) { - if (option.port[p].source_buffer_id != 0) APS5_INVALID_ARG_EX; + if (option.port[p].source_buffer_id >= option.num_buffers) APS5_INVALID_ARG_EX; } return SCE_NGS2_OK; } @@ -111,11 +117,14 @@ static Ngs2UserFx2SetupContext UserFxContext(Ngs2Rack& rack, std::size_t module, } void Ngs2SetupUserFx(Ngs2Rack& rack, const Ngs2CustomRackOption& option) { + rack.customPortBuffers.resize(option.rack_option.max_ports); + for (std::size_t p = 0; p < rack.customPortBuffers.size(); p++) rack.customPortBuffers[p] = option.port[p].source_buffer_id; rack.customModuleIds.resize(option.num_modules); rack.customModules.resize(option.num_modules); rack.userFx.resize(option.num_modules); rack.userFxCommon.resize(option.num_modules); for (auto& voice : rack.voices) { + voice.customBuffers.resize(option.num_buffers - 1); voice.userFx.resize(option.num_modules); voice.customUserFx.resize(option.num_modules); voice.customGain.assign(option.num_modules, {}); @@ -164,6 +173,8 @@ static void SetupCustomSubmixer(Ngs2Voice& voice, const Ngs2CustomSubmixerVoiceS if (setup.flags != 0) throw std::runtime_error("NGS2: custom submixer setup flags are not implemented"); if (setup.num_input_channels != setup.num_output_channels) throw std::runtime_error("NGS2: custom submixer channel conversion is not implemented"); if (setup.num_input_channels == 0 || setup.num_input_channels > voice.rack->maxChannels) APS5_INVALID_ARG_EX; + for (auto& gain : voice.customGain) if (gain.configured) gain.lifecycleUnknown = true; + ++voice.waveformRevision; voice.channels = setup.num_input_channels; } @@ -188,22 +199,41 @@ static void SetCustomUserFxParam(Ngs2Voice& voice, std::uint32_t index, const Ng fx.flags = 1; } -static void SetGainParam(Ngs2Voice& voice, std::uint32_t index, const Ngs2CustomVoiceGainParam& param) { - if (index >= voice.customGain.size() || voice.rack->customModuleIds[index] != SCE_NGS2_CUSTOM_MODULE_ID_GAIN) APS5_INVALID_ARG_EX; - for (const float gain : param.gain) { - if (!(gain >= -32.0f && gain <= 32.0f)) throw std::runtime_error("NGS2: a custom Gain level outside -32 to 32 is not implemented"); - } +int Ngs2ApplyCustomGainParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& header) { + const auto index = header.id & 0xff; + if (index >= voice.rack->customModuleIds.size()) return SCE_NGS2_ERROR_INVALID_PARAM_ID; + if (voice.rack->customModuleIds[index] != SCE_NGS2_CUSTOM_MODULE_ID_GAIN) return SCE_NGS2_ERROR_INVALID_PARAM_ID; + const auto& param = ParamAs(header); std::copy(std::begin(param.gain), std::end(param.gain), voice.customGain[index].target.begin()); + voice.customGain[index].configured = true; + return SCE_NGS2_OK; } -static void ProcessGain(Ngs2Voice& voice, Ngs2VoiceCustomGain& gain, const std::vector& channels, std::uint32_t grain) { - if (voice.hasSamples) { - for (std::uint32_t c = 0; c < voice.channels; c++) { - const float from = gain.current[c]; - const float step = (gain.target[c] - from) / static_cast(grain); - for (std::uint32_t i = 0; i < grain; i++) channels[c][i] *= from + step * static_cast(i + 1); +static void ProcessGain(Ngs2Voice& voice, std::size_t module, std::uint32_t grain) { + auto& gain = voice.customGain[module]; + if (!gain.configured) throw std::runtime_error("NGS2: custom Gain processing before its first parameter set is not verified"); + if (gain.lifecycleUnknown) throw std::runtime_error("NGS2: custom Gain processing after setup or stop is not verified"); + const auto& info = voice.rack->customModules[module]; + const auto& source = voice.Samples(info.source_buffer_id); + auto& dest = voice.Samples(info.dest_buffer_id); + for (std::uint32_t c = 0; c < voice.channels; c++) { + const auto offset = static_cast(c) * grain; + const float from = gain.current[c]; + const float to = gain.target[c]; + if (from == to) { + if (to == 0.0f) std::fill_n(dest.data() + offset, grain, 0.0f); + else if (to == 1.0f) { + if (source.data() != dest.data()) std::copy_n(source.data() + offset, grain, dest.data() + offset); + } + else for (std::uint32_t i = 0; i < grain; i++) dest[offset + i] = source[offset + i] * to; + } else { + const float step = (to - from) / static_cast(grain); + for (std::uint32_t i = 0; i < grain; i++) dest[offset + i] = source[offset + i] * (from + step * static_cast(i)); } - voice.hasSamples = std::any_of(voice.samples.begin(), voice.samples.end(), [](float sample) { return sample != 0.0f; }); + } + voice.hasSamples = std::any_of(voice.samples.begin(), voice.samples.end(), [](float sample) { return sample != 0.0f; }); + for (const auto& buffer : voice.customBuffers) { + voice.hasSamples |= std::any_of(buffer.begin(), buffer.end(), [](float sample) { return sample != 0.0f; }); } gain.current = gain.target; } @@ -243,11 +273,6 @@ void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { Ngs2SetFilter(voice, filter); return; } - if ((param.id & ~0x1fu) == SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN) { - if (rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack"); - SetGainParam(voice, param.id & 0x1f, ParamAs(param)); - return; - } if ((param.id & ~0x1fu) == SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2) { if (rackId != SCE_NGS2_RACK_ID_CUSTOM_SAMPLER && rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) { throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack"); @@ -266,12 +291,20 @@ void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { void Ngs2ProcessUserFx(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t sampleRate) { if (voice.userFx.empty() && voice.customUserFx.empty()) return; + const auto revision = voice.waveformRevision; + const auto channelCount = voice.channels; + const auto checkLayout = [&] { + if (voice.waveformRevision != revision || voice.channels != channelCount || voice.samples.size() != static_cast(channelCount) * grain) { + throw std::runtime_error("NGS2: voice setup changes during custom processing are not implemented"); + } + }; + checkLayout(); std::vector channels(voice.channels); for (std::uint32_t c = 0; c < voice.channels; c++) channels[c] = voice.samples.data() + static_cast(c) * grain; for (std::size_t m = 0; m < voice.rack->customModuleIds.size(); m++) { if (voice.rack->customModuleIds[m] == SCE_NGS2_CUSTOM_MODULE_ID_DELAY) continue; if (voice.rack->customModuleIds[m] == SCE_NGS2_CUSTOM_MODULE_ID_GAIN) { - ProcessGain(voice, voice.customGain[m], channels, grain); + ProcessGain(voice, m, grain); } else if (voice.rack->customModuleIds[m] == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2) { if (!voice.hasSamples) continue; auto& fx = voice.userFx[m]; @@ -280,6 +313,7 @@ void Ngs2ProcessUserFx(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t samp Ngs2UserFx2ProcessContext context{channels.data(), voice.rack->userFxCommon[m].data(), fx.param.data(), fx.work.data(), fx.state.data(), module.user_data, fx.flags, voice.channels, voice.channels, grain, sampleRate, 0, {}}; const int result = module.process_handler(&context); + checkLayout(); if (result != SCE_NGS2_OK) throw std::runtime_error("NGS2: the UserFx2 process handler failed with " + std::to_string(result)); fx.flags = 0; } else if (voice.rack->customModuleIds[m] == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX) { @@ -288,6 +322,7 @@ void Ngs2ProcessUserFx(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t samp Ngs2UserFxProcessContext context{channels.data(), fx.data[0], fx.data[1], fx.data[2], fx.flags, voice.channels, grain, sampleRate}; const int result = fx.handler(&context); + checkLayout(); if (result != SCE_NGS2_OK) throw std::runtime_error("NGS2: the UserFx process handler failed with " + std::to_string(result)); fx.flags = 0; voice.hasSamples = std::any_of(voice.samples.begin(), voice.samples.end(), [](float sample) { return sample != 0.0f; }); diff --git a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp index 73270afab9..eda57cdf7e 100644 --- a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp +++ b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp @@ -93,6 +93,7 @@ struct Ngs2Port { Ngs2Voice* dest = nullptr; float volume = 1.0f; std::int32_t matrix = -1; + std::uint32_t destInput = 0; }; struct Ngs2UserFx2 { @@ -111,6 +112,8 @@ struct Ngs2VoiceCustomUserFx { struct Ngs2VoiceCustomGain { std::array current{}; std::array target{}; + bool configured = false; + bool lifecycleUnknown = false; }; struct Ngs2Voice { @@ -147,6 +150,7 @@ struct Ngs2Voice { std::array userFxData{}; std::uint32_t userFxFlags = 0; std::vector samples; + std::vector> customBuffers; bool rendering = false; bool rendered = false; bool hasSamples = false; @@ -155,6 +159,8 @@ struct Ngs2Voice { void ResetSetup(); const std::uint8_t* WaveformData() const; std::uint32_t InputChannels() const { return inputChannels != 0 ? inputChannels : channels; } + std::vector& Samples(std::uint32_t buffer) { return buffer == 0 ? samples : customBuffers.at(buffer - 1); } + const std::vector& Samples(std::uint32_t buffer) const { return buffer == 0 ? samples : customBuffers.at(buffer - 1); } }; struct Ngs2Rack { @@ -175,6 +181,7 @@ struct Ngs2Rack { std::vector> userFxCommon; std::vector customModuleIds; std::vector customModules; + std::vector customPortBuffers; std::thread::id lockOwner{}; std::uint32_t lockDepth = 0; }; @@ -241,6 +248,7 @@ bool Ngs2FinishBlock(Ngs2Voice& voice); int Ngs2CheckCustomRack(std::uint32_t rackId, const Ngs2CustomRackOption& option, std::uint32_t maxInputs); void Ngs2SetupUserFx(Ngs2Rack& rack, const Ngs2CustomRackOption& option); void Ngs2CleanupUserFx(Ngs2Rack& rack); +int Ngs2ApplyCustomGainParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param); void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param); void Ngs2ProcessUserFx(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t sampleRate); void Ngs2SetReverbParams(Ngs2Voice& voice, const Ngs2ReverbI3DL2Param& params); diff --git a/core/libs/prx/libSceNgs2.native/src/Render.cpp b/core/libs/prx/libSceNgs2.native/src/Render.cpp index 3fb3689e23..2590239218 100644 --- a/core/libs/prx/libSceNgs2.native/src/Render.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Render.cpp @@ -219,7 +219,13 @@ float Ngs2DefaultLevel(std::uint32_t sourceChannels, std::uint32_t source, std:: } } -static void MixPort(Ngs2Voice& voice, const Ngs2Voice& source, const Ngs2Port& port, std::uint32_t grain) { +static void MixPort(Ngs2Voice& voice, const Ngs2Voice& source, const Ngs2Port& port, std::uint32_t sourceBuffer, std::uint32_t grain) { + const auto& sourceSamples = source.Samples(sourceBuffer); + auto& destSamples = voice.Samples(port.destInput); + if (sourceSamples.size() != static_cast(grain) * source.channels || + destSamples.size() != static_cast(grain) * voice.InputChannels()) { + throw std::runtime_error("NGS2: channel layout changes during input rendering are not implemented"); + } const auto* matrix = port.matrix < 0 || source.matrices[port.matrix].empty() ? nullptr : &source.matrices[port.matrix]; const auto channels = voice.InputChannels(); const std::size_t outputs = matrix == nullptr ? channels : std::min(channels, matrix->size() / source.channels); @@ -227,18 +233,20 @@ static void MixPort(Ngs2Voice& voice, const Ngs2Voice& source, const Ngs2Port& p for (std::uint32_t src = 0; src < source.channels; src++) { const float level = port.volume * (matrix == nullptr ? Ngs2DefaultLevel(source.channels, src, channels, static_cast(dst)) : (*matrix)[dst * source.channels + src]); if (level == 0.0f) continue; - for (std::uint32_t i = 0; i < grain; i++) voice.samples[dst * grain + i] += source.samples[src * grain + i] * level; + for (std::uint32_t i = 0; i < grain; i++) destSamples[dst * grain + i] += sourceSamples[src * grain + i] * level; } } } static void MixInputs(Ngs2Voice& voice, const std::vector& voices, std::uint32_t grain, std::uint32_t systemRate) { for (auto* source : voices) { - for (const auto& port : source->ports) { + for (std::size_t p = 0; p < source->ports.size(); p++) { + const auto& port = source->ports[p]; if (port.dest != &voice || port.volume == 0.0f) continue; RenderVoice(*source, voices, grain, systemRate); if (!source->hasSamples) continue; - MixPort(voice, *source, port, grain); + const auto sourceBuffer = source->rack->customPortBuffers.empty() ? 0u : source->rack->customPortBuffers[p]; + MixPort(voice, *source, port, sourceBuffer, grain); voice.hasSamples = true; } } @@ -253,6 +261,7 @@ static void RenderVoice(Ngs2Voice& voice, const std::vector& voices, ~RenderingScope() { active = false; } } renderingScope{voice.rendering}; voice.samples.assign(static_cast(grain) * voice.InputChannels(), 0.0f); + for (auto& buffer : voice.customBuffers) buffer.assign(static_cast(grain) * voice.InputChannels(), 0.0f); voice.hasSamples = false; if (voice.state == Ngs2PlayState::Playing && voice.channels != 0) { if (voice.rack->rackId == SCE_NGS2_RACK_ID_SAMPLER || voice.rack->rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER) { @@ -299,6 +308,7 @@ static void WriteOutput(const Ngs2RenderBufferInfo& output, const std::vector(output.buffer); for (std::size_t i = 0; i < mix.size(); i++) { + if (!std::isfinite(mix[i])) throw std::runtime_error("NGS2: integer rendering of nonfinite samples is not implemented"); pcm[i] = static_cast(std::min(std::lrint(std::clamp(mix[i], -1.0f, 1.0f) * 32768.0f), 32767L)); } } diff --git a/core/libs/prx/libSceNgs2.native/src/Voice.cpp b/core/libs/prx/libSceNgs2.native/src/Voice.cpp index 2b02256918..bb3f34ca43 100644 --- a/core/libs/prx/libSceNgs2.native/src/Voice.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Voice.cpp @@ -22,6 +22,10 @@ static constexpr std::uint8_t COMMAND_TYPE_FLOAT_ARRAY = 0x11; static constexpr float MAX_MATRIX_LEVEL = 4.0f; void Ngs2Voice::SetEvent(std::uint32_t eventId) { + if (state != Ngs2PlayState::Empty && + (eventId == SCE_NGS2_VOICE_EVENT_STOP || eventId == SCE_NGS2_VOICE_EVENT_STOP_IMM || eventId == SCE_NGS2_VOICE_EVENT_KILL)) { + for (auto& gain : customGain) if (gain.configured) gain.lifecycleUnknown = true; + } switch (eventId) { case SCE_NGS2_VOICE_EVENT_PLAY: if (state == Ngs2PlayState::Empty || state == Ngs2PlayState::Stopped) { @@ -49,6 +53,7 @@ void Ngs2Voice::SetEvent(std::uint32_t eventId) { } void Ngs2Voice::ResetSetup() { + for (auto& gain : customGain) if (gain.configured) gain.lifecycleUnknown = true; ++waveformRevision; SetEvent(SCE_NGS2_VOICE_EVENT_STOP_IMM); std::fill(ports.begin(), ports.end(), Ngs2Port{}); @@ -119,13 +124,15 @@ static void SetPortMatrix(Ngs2Voice& voice, std::uint32_t port, std::int32_t mat static void Patch(Ngs2Voice& voice, const Ngs2VoicePatchParam& patch) { auto& port = PortAt(voice, patch.port); - if (patch.dest_input_id != 0) throw std::runtime_error("NGS2: voice input " + std::to_string(patch.dest_input_id) + " is not implemented"); if (patch.dest_handle == 0) { + if (patch.dest_input_id != 0) throw std::runtime_error("NGS2: a disconnected port with a nonzero input id is not implemented"); port.dest = nullptr; + port.destInput = 0; return; } auto* dest = Ngs2FindVoice(patch.dest_handle); if (dest == nullptr) { + if (patch.dest_input_id != 0) throw std::runtime_error("NGS2: a rack destination with a nonzero input id is not implemented"); auto* rack = Ngs2FindRack(patch.dest_handle); if (rack != nullptr && !rack->voices.empty()) dest = &rack->voices.front(); } @@ -133,7 +140,12 @@ static void Patch(Ngs2Voice& voice, const Ngs2VoicePatchParam& patch) { dest->rack->rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER) { APS5_INVALID_ARG_EX; } + if (patch.dest_input_id != 0 && + (dest->rack->rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER || patch.dest_input_id >= dest->rack->maxInputs)) { + throw std::runtime_error("NGS2: voice input " + std::to_string(patch.dest_input_id) + " is not implemented for this rack"); + } port.dest = dest; + port.destInput = patch.dest_input_id; } static void ApplyCommonParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { @@ -406,7 +418,12 @@ int APS5_VABI sceNgs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamHe if (found == nullptr) return SCE_NGS2_ERROR_INVALID_VOICE_HANDLE; auto& voice = *found; for (const auto* param = param_list;; param = reinterpret_cast(reinterpret_cast(param) + param->next)) { - ApplyParam(voice, *param); + if ((param->id & ~0xffu) == SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN) { + const auto result = Ngs2ApplyCustomGainParam(voice, *param); + if (result != SCE_NGS2_OK) return result; + } else { + ApplyParam(voice, *param); + } if (param->next == 0) break; } return SCE_NGS2_OK; @@ -499,6 +516,7 @@ int APS5_VABI sceNgs2VoiceGetPortInfo(uintptr_t voice_handle, uint32_t port, Ngs info->matrix_id = source.matrix; info->volume = source.volume; info->dest_handle = reinterpret_cast(source.dest); + info->dest_input_id = source.destInput; return SCE_NGS2_OK; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp index 47a189ed42..4e8f3999b7 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp @@ -521,8 +521,6 @@ static void TestCustomRackChecks() { Require(rack == 0); sampler.custom_rack_option.module[0].extra_buffer_id = 0; Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &sampler.custom_rack_option.rack_option, &info, &rack) == SCE_NGS2_OK); - sampler.custom_rack_option.module[0].module_id = SCE_NGS2_CUSTOM_MODULE_ID_GAIN; - Require(CreateSamplerThrows(sampler)); Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK); for (const auto count : {1u, 2u, 24u}) { sampler.custom_rack_option.num_modules = count; @@ -757,17 +755,14 @@ static void TestCustomGain() { Patch(sampler, custom); Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); - auto out = Render(system); - for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 0.0f && out[i * 2 + 1] == 0.0f); - Ngs2CustomVoiceGainParam gain{}; gain.gain[0] = 2.0f; gain.gain[1] = -0.5f; Control(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 0, gain); - out = Render(system); + auto out = Render(system); for (std::uint32_t i = 0; i < Grain; i++) { - Require(out[i * 2] == 0.5f * 2.0f * static_cast(i + 1) / Grain); - Require(out[i * 2 + 1] == 0.5f * -0.5f * static_cast(i + 1) / Grain); + Require(out[i * 2] == 0.5f * 2.0f * static_cast(i) / Grain); + Require(out[i * 2 + 1] == 0.5f * -0.5f * static_cast(i) / Grain); } out = Render(system); for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 1.0f && out[i * 2 + 1] == -0.25f); @@ -775,13 +770,15 @@ static void TestCustomGain() { gain.gain[0] = 1.0f; Control(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 0, gain); out = Render(system); - for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 0.5f * (2.0f - static_cast(i + 1) / Grain)); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 0.5f * (2.0f - static_cast(i) / Grain)); auto outside = gain; outside.gain[2] = 33.0f; - Require(ControlThrows(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 0, outside)); - Require(ControlThrows(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 1, gain)); - Require(ControlThrows(sampler, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 0, gain)); + Control(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 0, outside); + gain.header = {sizeof(gain), 0, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 1}; + Require(sceNgs2VoiceControl(custom, &gain.header) == SCE_NGS2_ERROR_INVALID_PARAM_ID); + gain.header.id = SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN; + Require(sceNgs2VoiceControl(sampler, &gain.header) == SCE_NGS2_ERROR_INVALID_PARAM_ID); struct ShortGainParam { Ngs2VoiceParamHeader header; float gain[2]; diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Gain.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Gain.cpp new file mode 100644 index 0000000000..0080cb37fb --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Gain.cpp @@ -0,0 +1,213 @@ +#include "Ngs2Test.hpp" + +#include +#include +#include +#include +#include + +static std::int32_t APS5_VABI Generate(Ngs2UserFxProcessContext* context) { + const auto* signal = reinterpret_cast(context->user_data0); + for (std::uint32_t c = 0; c < context->num_channels; c++) + std::fill_n(context->channel_data[c], context->num_grain_samples, signal[c]); + return SCE_NGS2_OK; +} + +struct GainGraph { + std::uint32_t channels; + uintptr_t system = CreateSystem(); + std::array voices{}; + std::array signal{0.5f, -0.25f, 0.125f, -0.0625f, 0.03125f, -0.015625f, 0.0078125f, -0.00390625f}; + + GainGraph(std::uint32_t channelCount = 2, std::uint32_t voiceCount = 1) + : channels(channelCount) { + const auto master = Mastering(system, channels); + Ngs2CustomSubmixerRackOption mixerOption{}; + auto& custom = mixerOption.custom_rack_option; + custom.rack_option.size = sizeof(mixerOption); + custom.rack_option.max_grain_samples = 512; + custom.rack_option.max_voices = voiceCount; + custom.rack_option.max_matrices = 1; + custom.rack_option.max_ports = 1; + custom.num_buffers = 1; + custom.num_modules = 1; + custom.module[0].module_id = SCE_NGS2_CUSTOM_MODULE_ID_GAIN; + mixerOption.max_channels = channels; + mixerOption.max_inputs = 1; + const auto rackId = SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER; + Ngs2ContextBufferInfo query{}; + Require(sceNgs2RackQueryBufferSize(rackId, &custom.rack_option, &query) == SCE_NGS2_OK); + const auto buffer = Buffer(query); + uintptr_t rack = 0; + Require(sceNgs2RackCreate(system, rackId, &custom.rack_option, &buffer, &rack) == SCE_NGS2_OK); + for (std::uint32_t v = 0; v < voiceCount; v++) { + Require(sceNgs2RackGetVoiceHandle(rack, v, &voices[v]) == SCE_NGS2_OK); + Control(voices[v], SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP, + Ngs2CustomSubmixerVoiceSetupParam{{}, channels, channels, 0, 0}); + const auto source = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SUBMIXER)); + Control(source, SCE_NGS2_SUBMIXER_VOICE_PARAM_SETUP, Ngs2SubmixerVoiceSetupParam{{}, channels, 0}); + Control(source, SCE_NGS2_SUBMIXER_VOICE_PARAM_USER_FX, + Ngs2SubmixerVoiceUserFxParam{{}, Generate, reinterpret_cast(signal.data()), 0, 0}); + Patch(source, voices[v]); + Event(source, SCE_NGS2_VOICE_EVENT_PLAY); + Patch(voices[v], master); + Event(voices[v], SCE_NGS2_VOICE_EVENT_PLAY); + } + } + + ~GainGraph() { Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } + + void Set(std::initializer_list values, std::uint32_t voice = 0) { + Ngs2CustomVoiceGainParam param{}; + std::fill_n(param.gain, 37, std::numeric_limits::quiet_NaN()); + std::copy(values.begin(), values.end(), param.gain); + Control(voices[voice], SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN, param); + } + + std::vector Render(std::uint32_t grain = Grain) { + Require(sceNgs2SystemSetGrainSamples(system, grain) == SCE_NGS2_OK); + std::vector out(grain * channels, -123.0f); + const Ngs2RenderBufferInfo info{out.data(), out.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, channels}; + Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK); + return out; + } +}; + +static void ExpectChannels(const std::vector& out, std::initializer_list> channels) { + std::size_t c = 0; + for (const auto& samples : channels) { + Require(out.size() == samples.size() * channels.size()); + for (std::size_t i = 0; i < samples.size(); i++) Require(out[i * channels.size() + c] == samples[i]); + c++; + } +} + +static void ExpectFrame(const std::vector& out, std::size_t index, std::initializer_list frame) { + Require((index + 1) * frame.size() <= out.size()); + Require(std::equal(frame.begin(), frame.end(), out.begin() + index * frame.size())); +} + +static void ExpectConstant(const std::vector& out, std::initializer_list frame) { + Require(out.size() % frame.size() == 0); + for (std::size_t i = 0; i < out.size() / frame.size(); i++) ExpectFrame(out, i, frame); +} + +static void TestRamps() { + GainGraph graph; + graph.Set({64.0f, -64.0f}); + graph.Set({8.0f, -16.0f}); + ExpectChannels(graph.Render(), {{0, 0.5f, 1, 1.5f, 2, 2.5f, 3, 3.5f}, {0, 0.5f, 1, 1.5f, 2, 2.5f, 3, 3.5f}}); + ExpectConstant(graph.Render(), {4, 4}); + graph.Set({8.0f, -16.0f}); + ExpectConstant(graph.Render(), {4, 4}); + graph.Set({16.0f, 16.0f}); + graph.Set({-80.0f, 96.0f}); + ExpectChannels(graph.Render(), {{4, -1.5f, -7, -12.5f, -18, -23.5f, -29, -34.5f}, {4, 0.5f, -3, -6.5f, -10, -13.5f, -17, -20.5f}}); + ExpectConstant(graph.Render(), {-40, -24}); + graph.Set({0, 0}); + ExpectChannels(graph.Render(), {{-40, -35, -30, -25, -20, -15, -10, -5}, {-24, -21, -18, -15, -12, -9, -6, -3}}); + ExpectConstant(graph.Render(), {0, 0}); +} + +static void TestEightChannels() { + GainGraph graph(8); + graph.Set({2, 8, 24, 64, 160, 384, 896, 2048}); + const auto out = graph.Render(); + ExpectFrame(out, 0, {0, 0, 0, 0, 0, 0, 0, 0}); + ExpectFrame(out, 4, {0.5f, -1, 1.5f, -2, 2.5f, -3, 3.5f, -4}); + ExpectFrame(out, 7, {7.0f / 8, -7.0f / 4, 21.0f / 8, -7.0f / 2, 35.0f / 8, -21.0f / 4, 49.0f / 8, -7}); + ExpectConstant(graph.Render(), {1, -2, 3, -4, 5, -6, 7, -8}); +} + +static void TestVoicesAndPause() { + GainGraph graph(1, 2); + graph.Set({8}, 0); + graph.Set({-4}, 1); + ExpectChannels(graph.Render(), {{0, 0.25f, 0.5f, 0.75f, 1, 1.25f, 1.5f, 1.75f}}); + ExpectConstant(graph.Render(), {2}); + Event(graph.voices[0], SCE_NGS2_VOICE_EVENT_PAUSE); + graph.Set({16}, 0); + ExpectConstant(graph.Render(), {-2}); + Event(graph.voices[0], SCE_NGS2_VOICE_EVENT_RESUME); + ExpectChannels(graph.Render(), {{2, 2.5f, 3, 3.5f, 4, 4.5f, 5, 5.5f}}); + ExpectConstant(graph.Render(), {6}); + graph.Set({-12}, 1); + ExpectChannels(graph.Render(), {{6, 5.5f, 5, 4.5f, 4, 3.5f, 3, 2.5f}}); + ExpectConstant(graph.Render(), {2}); +} + +static void TestGrainChanges() { + GainGraph graph(1); + graph.Set({8}); + auto out = graph.Render(64); + Require(out.front() == 0 && out[1] == 1.0f / 16 && out[32] == 2 && out.back() == 63.0f / 16); + ExpectConstant(graph.Render(64), {4}); + graph.Set({-8}); + out = graph.Render(128); + Require(out.front() == 4 && out[1] == 63.0f / 16 && out[64] == 0 && out.back() == -63.0f / 16); + ExpectConstant(graph.Render(), {-4}); +} + +static void TestNonfiniteGains() { + GainGraph graph(1); + graph.Set({std::numeric_limits::infinity()}); + graph.Render(); + graph.Set({std::numeric_limits::infinity()}); + for (float sample : graph.Render()) Require(std::isinf(sample) && sample > 0); + graph.Set({std::numeric_limits::quiet_NaN()}); + for (float sample : graph.Render()) Require(std::isnan(sample)); + for (float sample : graph.Render()) Require(std::isnan(sample)); + graph.Set({2}); + for (float sample : graph.Render()) Require(std::isnan(sample)); + ExpectConstant(graph.Render(), {1}); +} + +static void TestZeroClearsNanSource() { + GainGraph graph(1); + graph.signal[0] = std::numeric_limits::quiet_NaN(); + graph.Set({1}); + for (float sample : graph.Render()) Require(std::isnan(sample)); + graph.Set({0}); + for (float sample : graph.Render()) Require(std::isnan(sample)); + ExpectConstant(graph.Render(), {0}); +} + +static void TestParameterPositions() { + GainGraph graph(1); + graph.Set({8}); + Ngs2CustomVoiceGainParam param{}; + param.header = {sizeof(param), 0, 0}; + param.gain[0] = 16; + for (std::uint32_t position = 0; position < 256; position++) { + if (position == 0) continue; + param.header.id = SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | position; + Require(sceNgs2VoiceControl(graph.voices[0], ¶m.header) == SCE_NGS2_ERROR_INVALID_PARAM_ID); + } + ExpectChannels(graph.Render(), {{0, 0.5f, 1, 1.5f, 2, 2.5f, 3, 3.5f}}); + ExpectConstant(graph.Render(), {4}); +} + +static void TestLinkedParameters() { + GainGraph graph(1); + std::array params{}; + for (auto& param : params) param.header = {sizeof(param), sizeof(param), SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN}; + params[0].gain[0] = 8; + params[1].header.id |= 255; + params[2].gain[0] = 16; + params[2].header.next = 0; + Require(sceNgs2VoiceControl(graph.voices[0], ¶ms[0].header) == SCE_NGS2_ERROR_INVALID_PARAM_ID); + ExpectChannels(graph.Render(), {{0, 0.5f, 1, 1.5f, 2, 2.5f, 3, 3.5f}}); + ExpectConstant(graph.Render(), {4}); +} + +int main() { + TestRamps(); + TestEightChannels(); + TestVoicesAndPause(); + TestGrainChanges(); + TestNonfiniteGains(); + TestParameterPositions(); + TestZeroClearsNanSource(); + TestLinkedParameters(); + return 0; +} diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Routing.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Routing.cpp new file mode 100644 index 0000000000..1979695e65 --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Routing.cpp @@ -0,0 +1,202 @@ +#include "prx/libSceNgs2.native/tests/Ngs2Test.hpp" + +#include +#include +#include +#include + +static Ngs2CustomSubmixerRackOption RackOption(std::uint32_t count, std::uint32_t channels = 2) { + Ngs2CustomSubmixerRackOption option{}; + auto& custom = option.custom_rack_option; + custom.rack_option.size = sizeof(option); + custom.rack_option.max_grain_samples = 512; + custom.rack_option.max_voices = 2; + custom.rack_option.max_matrices = 1; + custom.rack_option.max_ports = count; + custom.num_buffers = count; + for (std::uint32_t port = 0; port < count; port++) custom.port[port].source_buffer_id = port; + option.max_channels = channels; + option.max_inputs = count == 1 ? 1 : 2; + return option; +} + +static void AddGain(Ngs2CustomSubmixerRackOption& option, std::uint32_t source, std::uint32_t dest) { + auto& custom = option.custom_rack_option; + custom.num_modules = 1; + custom.module[0].module_id = SCE_NGS2_CUSTOM_MODULE_ID_GAIN; + custom.module[0].source_buffer_id = source; + custom.module[0].dest_buffer_id = dest; + custom.module[0].extra_buffer_id = custom.num_buffers == 1 ? 0 : 1; +} + +static uintptr_t CustomRack(uintptr_t system, const Ngs2CustomSubmixerRackOption& option) { + Ngs2ContextBufferInfo query{}; + Require(sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER, &option.custom_rack_option.rack_option, &query) == SCE_NGS2_OK); + const auto info = Buffer(query); + uintptr_t rack = 0; + Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER, &option.custom_rack_option.rack_option, &info, &rack) == SCE_NGS2_OK); + return rack; +} + +static uintptr_t CustomVoice(uintptr_t rack, std::uint32_t index = 0, std::uint32_t channels = 2) { + uintptr_t voice = 0; + Require(sceNgs2RackGetVoiceHandle(rack, index, &voice) == SCE_NGS2_OK); + Control(voice, SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP, Ngs2CustomSubmixerVoiceSetupParam{{}, channels, channels, 0, 0}); + Event(voice, SCE_NGS2_VOICE_EVENT_PLAY); + return voice; +} + +static void SetGain(uintptr_t voice, float level) { + Ngs2CustomVoiceGainParam param{}; + for (auto& gain : param.gain) gain = level; + Control(voice, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN, param); +} + +static void Route(uintptr_t source, std::uint32_t port, uintptr_t dest, std::uint32_t input = 0, std::uint32_t channels = 2) { + std::array identity{}; + for (std::uint32_t channel = 0; channel < channels; channel++) identity[channel * channels + channel] = 1.0f; + Control(source, SCE_NGS2_VOICE_PARAM_MATRIX_LEVELS, Ngs2VoiceMatrixLevelsParam{{}, 0, channels * channels, identity.data()}); + Control(source, SCE_NGS2_VOICE_PARAM_PORT_MATRIX, Ngs2VoicePortMatrixParam{{}, port, 0}); + Control(source, SCE_NGS2_VOICE_PARAM_PATCH, Ngs2VoicePatchParam{{}, port, input, dest}); + Ngs2VoicePortInfo info{}; + Require(sceNgs2VoiceGetPortInfo(source, port, &info, sizeof(info)) == SCE_NGS2_OK); + Require(info.dest_handle == dest && info.dest_input_id == input); +} + +static void Output(uintptr_t system, uintptr_t source, std::uint32_t port, std::uint32_t output, std::uint32_t channels = 2) { + const auto master = Mastering(system, channels); + Control(master, SCE_NGS2_MASTERING_VOICE_PARAM_OUTPUT, Ngs2MasteringVoiceOutputParam{{}, output, 0}); + Route(source, port, master, 0, channels); +} + +static uintptr_t Input(uintptr_t system, const std::vector& pcm, std::uint32_t channels = 2) { + const auto voice = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER)); + Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, + Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, channels, 48000, 0, 0, 0}}); + const Ngs2WaveformBlock block{0, pcm.size() * sizeof(std::int16_t), 0, 0, static_cast(pcm.size() / channels), 0, 0}; + Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, pcm.data(), 0, 1, &block}); + Event(voice, SCE_NGS2_VOICE_EVENT_PLAY); + return voice; +} + +static void Render(uintptr_t system, const std::vector& expected, std::uint32_t channels = 2, std::uint32_t grain = Grain) { + Require(sceNgs2SystemSetGrainSamples(system, grain) == SCE_NGS2_OK); + std::vector> samples(expected.size(), std::vector(grain * channels, 9.0f)); + std::vector info; + for (auto& output : samples) info.push_back({output.data(), output.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, channels}); + Require(sceNgs2SystemRender(system, info.data(), static_cast(info.size())) == SCE_NGS2_OK); + for (std::size_t port = 0; port < expected.size(); port++) { + for (const auto sample : samples[port]) Require(std::fabs(sample - expected[port]) <= 0.000001f); + } +} + +static void TestNoModules(std::uint32_t count, std::vector expected) { + const auto system = CreateSystem(); + auto option = RackOption(count); + for (std::uint32_t port = 0; port < count; port++) option.custom_rack_option.port[port].source_buffer_id = count - port - 1; + const auto custom = CustomVoice(CustomRack(system, option)); + for (std::uint32_t port = 0; port < count; port++) Output(system, custom, port, port); + const std::vector pcm0(Grain * 2 * 16, 8192); + const std::vector pcm1(Grain * 2 * 16, -24576); + const auto input0 = Input(system, pcm0); + Route(input0, 0, custom); + uintptr_t input1 = 0; + if (count > 1) { + input1 = Input(system, pcm1); + Route(input1, 0, custom, 1); + } + Render(system, expected); + if (count > 1) { + Route(input1, 0, custom, 0); + expected[count - 1] = -0.5f; + expected[count - 2] = 0.0f; + Render(system, expected); + Route(input1, 0, 0); + expected[count - 1] = 0.25f; + Render(system, expected); + } + Route(input0, 0, 0); + Render(system, std::vector(count, 0.0f)); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +static void TestGainRouting() { + struct Case { + std::uint32_t count, source, dest; + std::vector expected; + }; + const Case cases[] = { + {1, 0, 0, {0.5f}}, + {2, 0, 1, {0.25f, 0.5f}}, + {2, 1, 0, {-1.5f, -0.75f}}, + {3, 0, 2, {0.25f, -0.75f, 0.5f}}, + {3, 2, 1, {0.25f, 0.0f, 0.0f}}, + {4, 1, 3, {0.25f, -0.75f, 0.0f, -1.5f}}, + {4, 3, 0, {0.0f, -0.75f, 0.0f, 0.0f}}, + {4, 1, 1, {0.25f, -1.5f, 0.0f, 0.0f}}, + }; + for (const auto& test : cases) { + const auto system = CreateSystem(); + auto option = RackOption(test.count); + AddGain(option, test.source, test.dest); + const auto custom = CustomVoice(CustomRack(system, option)); + SetGain(custom, 2.0f); + for (std::uint32_t port = 0; port < test.count; port++) Output(system, custom, port, port); + const std::vector silence(test.count, 0.0f); + Render(system, silence); + const std::vector pcm0(Grain * 2 * 16, 8192); + const std::vector pcm1(Grain * 2 * 16, -24576); + const auto input0 = Input(system, pcm0); + Route(input0, 0, custom); + uintptr_t input1 = 0; + if (test.count > 1) { + input1 = Input(system, pcm1); + Route(input1, 0, custom, 1); + } + Render(system, test.expected); + Render(system, test.expected, 2, 64); + Route(input0, 0, 0); + if (input1 != 0) Route(input1, 0, 0); + Render(system, silence); + Render(system, silence); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); + } +} + +static void TestIndependentVoicesAndSilentRamp(std::uint32_t channels) { + const auto system = CreateSystem(); + auto option = RackOption(4, channels); + AddGain(option, 0, 3); + const auto rack = CustomRack(system, option); + const auto first = CustomVoice(rack, 0, channels); + const auto second = CustomVoice(rack, 1, channels); + SetGain(first, 2.0f); + SetGain(second, -1.0f); + Output(system, first, 3, 0, channels); + Output(system, second, 3, 1, channels); + Render(system, {0.0f, 0.0f}, channels); + const std::vector pcm0(Grain * channels * 16, 8192); + const std::vector pcm1(Grain * channels * 16, -24576); + const auto input0 = Input(system, pcm0, channels); + const auto input1 = Input(system, pcm1, channels); + Route(input0, 0, first, 0, channels); + Route(input1, 0, second, 0, channels); + Render(system, {0.5f, 0.75f}, channels); + Route(input0, 0, 0, 0, channels); + Render(system, {0.0f, 0.75f}, channels); + SetGain(first, 4.0f); + Render(system, {0.0f, 0.75f}, channels); + Route(input0, 0, first, 0, channels); + Render(system, {1.0f, 0.75f}, channels); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +int main() { + TestNoModules(1, {0.25f}); + TestNoModules(2, {-0.75f, 0.25f}); + TestNoModules(3, {0.0f, -0.75f, 0.25f}); + TestNoModules(4, {0.0f, 0.0f, -0.75f, 0.25f}); + TestGainRouting(); + for (std::uint32_t channels = 1; channels <= 8; channels++) TestIndependentVoicesAndSilentRamp(channels); + return 0; +} diff --git a/core/libs/prx/libSceNgs2/CMakeLists.txt b/core/libs/prx/libSceNgs2/CMakeLists.txt index 41b6cfe96a..4bbc79d8d2 100644 --- a/core/libs/prx/libSceNgs2/CMakeLists.txt +++ b/core/libs/prx/libSceNgs2/CMakeLists.txt @@ -3,6 +3,18 @@ add_sce_ngs2_library(libSceNgs2) target_sources(libSceNgs2 PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/PendingExports.cpp) if(BUILD_TESTING) + foreach(ngs2Test IN ITEMS Gain Routing) + string(TOLOWER ${ngs2Test} testName) + add_test_executable(ngs2_core_${testName}_tests ${CMAKE_CURRENT_SOURCE_DIR}/../libSceNgs2.native/tests/Ngs2${ngs2Test}.cpp) + target_include_directories(ngs2_core_${testName}_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src) + target_link_libraries(ngs2_core_${testName}_tests PRIVATE libSceNgs2 atrac9 libkernel libc) + configure_windows_unwind(ngs2_core_${testName}_tests) + add_test(NAME ngs2_core_${testName} COMMAND ngs2_core_${testName}_tests) + if(WIN32) + set_tests_properties(ngs2_core_${testName} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() add_test_executable(ngs2_core_render_tests ${CMAKE_CURRENT_SOURCE_DIR}/../libSceNgs2.native/tests/Ngs2Render.cpp) target_include_directories(ngs2_core_render_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src) target_link_libraries(ngs2_core_render_tests PRIVATE libSceNgs2 atrac9 libkernel libc) diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 5997a09d48..bac8dda70a 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -23,13 +23,13 @@ - [NGS2 sampler voice state flag EMPTY](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - a playing sampler voice that still accepts blocks (`CONTINUE`) and has played all queued blocks reports `INUSE|PLAYING|EMPTY` (0x20) and keeps playing, so blocks added later play on. PPSA18391 (engine: Clickteam Fusion) reads `state_flags & 0x22 != 2` from `sceNgs2VoiceGetState` as the end of a sound and only then reuses the voice. When the console sets and clears EMPTY, and whether it also reports it for other voice types, is unverified on hardware. - [NGS2 reverb channel conversion](../../../core/libs/prx/libSceNgs2.native/src/Reverb.cpp) (libSceNgs2.native) - a reverb voice whose `num_input_channels` differs from `num_output_channels` (PPSA03747 sets up 2 inputs and 8 outputs) mixes its ports in the input layout and folds that to mono with the input count's gains, as an equal-count voice does, and runs the comb and allpass stages per output channel. The PS4 library's handling of the dry signal in this case is not checked: here it reaches the output layout through the default channel map, so a stereo input keeps its dry signal on the front left and right and the other outputs carry only the reverb - [NGS2 port matrix](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - a port without a matrix, or with a selected matrix whose levels were never set, mixes through a default channel map: mono reaches both stereo channels at -3 dB and the 5.1 and 7.1 centre, stereo folds to mono at -3 dB per side, and wider sources fold down with 0.5 and -3 dB gains. The map and the equivalence of an unset matrix with no matrix were measured from the PS4 firmware 11.00 `libSceNgs2.sprx`, not yet from the PS5 library. That library only accepts submixer and mastering voices with 1, 2, 6 or 8 channels, so the map into 3, 4, 5 or 7 channels throws. A matrix set with fewer levels than the destination needs still mixes only the rows it has -- [NGS2 custom submixer rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - UserFx2 process flags and setup/process/cleanup order follow only [KytyPS5](https://github.com/KytyPS5/KytyPS5), not a console measurement; layouts follow [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_custom.h`. A module control handler, unequal input and output channels, setup flags and default rack options throw. Module state is a separate buffer, not the `state_offset` slice of voice state; `sceNgs2VoiceGetState` throws for this rack. UserFx2 runs only on grains with input samples, so an effect tail stops with the input +- [NGS2 custom submixer rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - UserFx2 process flags and setup/process/cleanup order follow only [KytyPS5](https://github.com/KytyPS5/KytyPS5), not a console measurement; layouts follow [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_custom.h`. A module control handler, unequal input and output channels, setup flags and default rack options throw. Module state is a separate buffer, not the `state_offset` slice of voice state; `sceNgs2VoiceGetState` throws for this rack. UserFx2 runs only on grains with input samples, so an effect tail stops with the input. Setup changes from custom process callbacks throw before later modules can use the changed layout; inconsistent input-mixing layouts after callbacks also throw. Console reentry behavior is unknown - [NGS2 custom UserFx](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native) - the 48-byte process context, process flags, callback error handling and null-handler behavior retain the existing submixer callback model and are not console-verified. Non-null module options throw because their console error is unknown. UserFx on custom sampler rack 0x4001 throws: its parameter indexing and routing are not implemented. Module id 0x19, the 40-byte voice parameter (handler and three user-data words), parameter id `0x40001900 | index`, and lack of option and module state follow [fma-rc's PS5 12.70 module reading](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6319332943), including parameter dispatch at 0x25ec4. - [NGS2 custom Delay](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, 0x1d) - processing and defaults remain unverified on PS5. The [agreed compatibility path](https://github.com/boykopovar/AnyPS5/pull/9337#issuecomment-6351205140) preserves main's PCM pass-through for Void Terrarium (PPSA03061) until any Delay parameter is set; every `0x40001dxx` parameter throws. Only one-buffer custom submixers with no module state and a 20-byte option are accepted. Option type 0/1, taps 1..8, length 1..20000 and reserved 0 follow [fma-rc's 12.70 module reading](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6319332943). Other shapes and values throw because their console errors are unknown. This preserves host behavior, not console-equivalent Delay processing -- [NGS2 custom module ids](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native) - Reverb (0x1b), PitchShift (0x20) and Convolution (0x23) are not processed: a rack that passes their creation checks throws. Gain (0x15) on custom sampler racks also throws. Per [fma-rc's 12.70 module reading](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6294323150), creation returns `0x804A8302` for a wrong non-zero state size, and `0x804A8013`, `0x804A8099` and `0x804A8098` for a bad Reverb option size or word and PitchShift word. Unknown: check order, errors for a Gain option, missing Reverb or PitchShift options, other PitchShift and Convolution option sizes, and Convolution's state (64 bytes from SDK 3.0). PPSA04540 uses 0x15, 0x19, 0x1b and 0x23 (#8039), PPSA35169 0x20 (#4246) and PPSA33655 0x15, 0x19 and 0x1b -- [NGS2 custom Gain module](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - follows [fma-rc's reading of the 12.70 module](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6319332943): parameter `0x40001500 | module index` holds 37 linear levels, one per channel, default 0, and the module writes source × level into dest, ramping linearly over the grain from the previous level. Unverified: where the ramp starts and ends (here the grain's first sample is one step past the previous level and its last sample is at the new level), whether stopping or setting up the voice again resets the levels, and what the library does with a level outside -32 to 32 (here it throws). On a custom sampler rack, whose parameter index is one higher per fma-rc, Gain throws. A Gain module with an option throws: fma-rc reads Gain as taking none, and the error the library returns for one is unknown. The extra buffer is ignored, as fma-rc reads it +- [NGS2 custom module ids](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native) - Reverb (0x1b), PitchShift (0x20) and Convolution (0x23) are not processed: a rack that passes their creation checks throws. Gain (0x15) on custom sampler racks still throws: its processor initialization and lifecycle are unverified. Per [fma-rc's 12.70 module reading](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6294323150), creation returns `0x804A8302` for a wrong non-zero state size, and `0x804A8013`, `0x804A8099` and `0x804A8098` for a bad Reverb option size or word and PitchShift word. Unknown: check order, errors for a Gain option, missing Reverb or PitchShift options, other PitchShift and Convolution option sizes, and Convolution's state (64 bytes from SDK 3.0). PPSA04540 uses 0x15, 0x19, 0x1b and 0x23 (#8039), PPSA35169 0x20 (#4246) and PPSA33655 0x15, 0x19 and 0x1b +- [NGS2 custom Gain](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) - PS5 rounding and first-parameter processor state are unverified. The initial previous gain retains main's zero convention; the schema default does not verify processor initialization. Processing before a parameter, after re-setup or after stopping a configured voice throws. The unclamped levels, i/N ramp and equal-gain paths follow [the PS5 12.70 reading](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6391072574) (0x33ce0, 0x5310); invalid parameter positions return 0x804A835A (0x250b0). Custom sampler Gain and module options throw: their initialization, lifecycle and option errors remain unknown. Integer rendering of nonfinite samples throws because the console conversion is unknown. - [NGS2 custom mastering rack](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native, rack 0x4003) - follows [fma-rc's reading of the 12.70 module](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6319332943): option size is 0x508, matching the custom submixer rack layout. Creation returns `0x804A8052` above 37 channels; channels 9 to 37 throw because voices hold at most 8 channels. Mastering racks with modules throw: attached modules and non-default option fields are unverified on PS5. Voice control parameters 0x40030000 (setup), 0x40030004 (gain) and 0x40030005 (output) configure channels, levels and destination buffer. `sceNgs2VoiceGetState` fills common voice state flags. PPSA10278 and PPSA04540 create rack 0x4003 without modules (#8039) -- [NGS2 custom rack buffers and channels](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, racks 0x4001 and 0x4002) - racks with 2 to 4 buffers, custom submixers with 9 to 37 channels and submixer `max_inputs` other than 1 or 2 throw: how modules and ports route several buffers is unknown, and voices hold at most 8 channels. Per [fma-rc's reading of the 12.70 module](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6294323150), creation returns `0x804A830B` for a buffer count outside 1 to 4 or below the submixer's `max_inputs`, `0x804A8306` for a module source, extra or dest buffer id at or above the count, and `0x804A8052` for more than 37 submixer channels; the check order is not known. `sceNgs2RackQueryBufferSize` throws for these options, as no source says whether it checks them. A port buffer id other than 0 throws. PPSA04540 creates racks with up to 4 buffers and a 16-channel custom submixer (#8039) +- [NGS2 custom buffers](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) - multiple buffers on samplers, submixer channels above 8, inputs beyond 0/1 and multi-buffer chains beyond zero modules or one Gain throw. Default custom module order, UserFx extra-buffer selection and invalid port-buffer errors remain unknown. Input-to-buffer routing and grain clearing follow [the PS5 12.70 reading](https://github.com/boykopovar/AnyPS5/issues/8039#issuecomment-6391072574) (0x181a0); output-port selection follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/83f8bfd3c9c35fcb135ef6c33dfd88bbfe066ee2/src/libs/ngs2.cpp#L417-L419). These paths have not been compared with console output. Creation validation order and QueryBufferSize validation remain unverified; the query throws for options whose creation returns an error. - [NGS2 reverb rack](../../../core/libs/prx/libSceNgs2.native/src/Reverb.cpp) (libSceNgs2.native, rack 0x2001) - follows the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output at 11.025 to 48 kHz, including parameter changes during playback (differences at float rounding level); not yet compared with the PS5 library. Structure layouts come from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_reverb.h` and the param ids 0x20010000 (setup) and 0x20010001 (I3DL2) from PPSA22520. Only the default `reverb_size` of 1 is handled, with its 14400-sample reflection buffer; other sizes and the library's half-rate mode throw. System sample rates below about 7.5 kHz, where an allpass delay would have no samples, throw. Setup only accepts equal input and output channel counts - [sceNgs2ReportRegisterHandler, sceNgs2ReportUnregisterHandler](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native, NIDs `uBIN24Tv2MI`, `nPzb7Ly-VjE`) keep a registry of report handlers. Register rejects a null handler with an invalid argument and returns `INVALID_OUT_ADDRESS` for a null out handle, then stores the report type, handler and user data under a freshly allocated opaque handle; unregister removes and frees the matching record and throws for a handle that is not registered. The signatures (report type, handler, user data, out handle) and (report handle) follow [SharpProspero](https://github.com/SvenGDK/SharpProspero) and [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_report.h`, and the 24-byte `Ngs2ReportDataHeader` passed to a handler is its `OrbisNgs2ReportDataHeader`. No report is produced yet, so no registered handler is ever invoked and PPSA04540, which imports both and registers a CPU load handler (type 5), still receives none. - [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 null user data pointer throws. `sceNgs2SystemLock` and `sceNgs2SystemUnlock` follow [prosper's notes](https://github.com/mattias800/prosper/blob/d61eb2c0ad2de879ef74df9c4aa050c512804a18/prosper/src/hle/audio/hle_audio.cpp#L4367-L4384) on the PS5 module: Lock takes a per-system lock and counts nested holds, Unlock releases one hold from any thread and returns 0 without a hold, and `sceNgs2SystemRender` only tries the lock: while another thread holds it, Render clears one grain of each output, leaves the render count and voices unchanged and returns 0. The other calls do not wait for the system lock. Peace Walker (PPSA18216) holds it while it spins on a flag that its audio thread clears only after `sceNgs2SystemRender` and eight `sceNgs2VoiceGetStateFlags` calls, so on the console neither can wait for it. Whether the console's voice and rack calls take the lock at all is not known. `sceNgs2RackLock` still holds the library's lock until `sceNgs2RackUnlock`, as before From 19f088c8ff3607eed8fbe13c00ccdb21d82e3002 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Sun, 25 Oct 2026 01:46:22 -0300 Subject: [PATCH 2/6] test(ngs2): cover custom processing reentry boundaries --- .../libs/prx/libSceNgs2.native/CMakeLists.txt | 12 +++ .../tests/Ngs2CustomSafety.cpp | 91 +++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 core/libs/prx/libSceNgs2.native/tests/Ngs2CustomSafety.cpp diff --git a/core/libs/prx/libSceNgs2.native/CMakeLists.txt b/core/libs/prx/libSceNgs2.native/CMakeLists.txt index 422eef37ca..4fd21b4fee 100644 --- a/core/libs/prx/libSceNgs2.native/CMakeLists.txt +++ b/core/libs/prx/libSceNgs2.native/CMakeLists.txt @@ -2,6 +2,18 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/Ngs2Library.cmake) add_sce_ngs2_library(libSceNgs2.native) if(BUILD_TESTING) + add_test_executable(ngs2_custom_safety_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/Ngs2CustomSafety.cpp) + target_include_directories(ngs2_custom_safety_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(ngs2_custom_safety_tests PRIVATE libSceNgs2.native libkernel libc) + configure_windows_unwind(ngs2_custom_safety_tests) + foreach(safetyCase IN ITEMS self_callback upstream_callback setup_lifecycle) + add_test(NAME ngs2_custom_safety_${safetyCase} COMMAND ngs2_custom_safety_tests ${safetyCase}) + set_tests_properties(ngs2_custom_safety_${safetyCase} PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(ngs2_custom_safety_${safetyCase} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() foreach(ngs2Test IN ITEMS Render Atrac9 Filter Custom Gain Routing Parse Stream UserFx Report Geom VoiceIndex HandleErrors VoiceHandles) string(TOLOWER ${ngs2Test} testName) add_test_executable(ngs2_${testName}_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/Ngs2${ngs2Test}.cpp) diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2CustomSafety.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2CustomSafety.cpp new file mode 100644 index 0000000000..688a062a7c --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2CustomSafety.cpp @@ -0,0 +1,91 @@ +#include "prx/libSceNgs2.native/tests/Ngs2Test.hpp" + +#include +#include +#include +#include + +static uintptr_t target = 0; + +static int APS5_VABI ChangeSetup(Ngs2UserFxProcessContext* context) { + std::fill_n(context->channel_data[0], context->num_grain_samples, 1.0f); + Control(target, SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP, Ngs2CustomSubmixerVoiceSetupParam{{}, 8, 8, 0, 0}); + return 0; +} + +static uintptr_t Custom(uintptr_t system, std::uint32_t count, bool chain) { + Ngs2CustomSubmixerRackOption option{}; + auto& custom = option.custom_rack_option; + custom.rack_option.size = sizeof(option); + custom.rack_option.max_grain_samples = 512; + custom.rack_option.max_voices = 1; + custom.rack_option.max_ports = 1; + custom.num_buffers = count; + custom.num_modules = chain ? 2 : 0; + custom.module[0].module_id = SCE_NGS2_CUSTOM_MODULE_ID_USER_FX; + custom.module[1].module_id = SCE_NGS2_CUSTOM_MODULE_ID_GAIN; + option.max_channels = 8; + option.max_inputs = count; + Ngs2ContextBufferInfo query{}; + Require(sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER, &custom.rack_option, &query) == 0); + const auto storage = Buffer(query); + uintptr_t rack = 0; + Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER, &custom.rack_option, &storage, &rack) == 0); + const auto voice = Voice(rack); + Control(voice, SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP, Ngs2CustomSubmixerVoiceSetupParam{{}, 2, 2, 0, 0}); + Event(voice, SCE_NGS2_VOICE_EVENT_PLAY); + return voice; +} + +static bool Rejected(uintptr_t system) { + std::array out{}; + const Ngs2RenderBufferInfo info{out.data(), sizeof(out), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 2}; + try { + Require(sceNgs2SystemRender(system, &info, 1) == 0); + } catch (const std::runtime_error& error) { + std::printf("safely refused: %s\n", error.what()); + return true; + } + return false; +} + +static void TestSelfCallback() { + const auto system = CreateSystem(); + target = Custom(system, 1, true); + Control(target, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX, Ngs2CustomVoiceUserFxParam{{}, ChangeSetup, 0, 0, 0}); + Control(target, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 1, Ngs2CustomVoiceGainParam{}); + Require(Rejected(system)); + Require(sceNgs2SystemDestroy(system, nullptr) == 0); +} + +static void TestUpstreamCallback() { + const auto system = CreateSystem(); + target = Custom(system, 2, false); + const auto source = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SUBMIXER)); + Control(source, SCE_NGS2_SUBMIXER_VOICE_PARAM_SETUP, Ngs2SubmixerVoiceSetupParam{{}, 2, 0}); + Control(source, SCE_NGS2_SUBMIXER_VOICE_PARAM_USER_FX, Ngs2SubmixerVoiceUserFxParam{{}, ChangeSetup, 0, 0, 0}); + Control(source, SCE_NGS2_VOICE_PARAM_PATCH, Ngs2VoicePatchParam{{}, 0, 1, target}); + Event(source, SCE_NGS2_VOICE_EVENT_PLAY); + Require(Rejected(system)); + Require(sceNgs2SystemDestroy(system, nullptr) == 0); +} + +static void TestSetupLifecycle() { + const auto system = CreateSystem(); + target = Custom(system, 1, true); + Control(target, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 1, Ngs2CustomVoiceGainParam{}); + Require(!Rejected(system)); + Control(target, SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP, Ngs2CustomSubmixerVoiceSetupParam{{}, 2, 2, 0, 0}); + Require(Rejected(system)); + Require(sceNgs2SystemDestroy(system, nullptr) == 0); +} + +int main(int argc, char** argv) { + Require(argc == 2); + const std::string_view test = argv[1]; + if (test == "self_callback") TestSelfCallback(); + else if (test == "upstream_callback") TestUpstreamCallback(); + else if (test == "setup_lifecycle") TestSetupLifecycle(); + else return 1; + return 0; +} From 2da1332bff0194443e91c9ab11a644cf6b4f58d8 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Sun, 25 Oct 2026 01:50:47 -0300 Subject: [PATCH 3/6] test(ngs2): expose callback route mutation --- .../libs/prx/libSceNgs2.native/CMakeLists.txt | 2 +- .../tests/Ngs2CustomSafety.cpp | 58 ++++++++++++++++++- core/libs/prx/libSceNgs2/CMakeLists.txt | 12 ++++ 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/CMakeLists.txt b/core/libs/prx/libSceNgs2.native/CMakeLists.txt index 4fd21b4fee..ba4b520798 100644 --- a/core/libs/prx/libSceNgs2.native/CMakeLists.txt +++ b/core/libs/prx/libSceNgs2.native/CMakeLists.txt @@ -6,7 +6,7 @@ if(BUILD_TESTING) target_include_directories(ngs2_custom_safety_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(ngs2_custom_safety_tests PRIVATE libSceNgs2.native libkernel libc) configure_windows_unwind(ngs2_custom_safety_tests) - foreach(safetyCase IN ITEMS self_callback upstream_callback setup_lifecycle) + foreach(safetyCase IN ITEMS self_callback upstream_callback setup_lifecycle repatch_one_buffer repatch_two_buffers) add_test(NAME ngs2_custom_safety_${safetyCase} COMMAND ngs2_custom_safety_tests ${safetyCase}) set_tests_properties(ngs2_custom_safety_${safetyCase} PROPERTIES TIMEOUT 10) if(WIN32) diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2CustomSafety.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2CustomSafety.cpp index 688a062a7c..780b1124e2 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2CustomSafety.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2CustomSafety.cpp @@ -13,7 +13,7 @@ static int APS5_VABI ChangeSetup(Ngs2UserFxProcessContext* context) { return 0; } -static uintptr_t Custom(uintptr_t system, std::uint32_t count, bool chain) { +static uintptr_t Custom(uintptr_t system, std::uint32_t count, bool chain, std::uint32_t outputBuffer = 0) { Ngs2CustomSubmixerRackOption option{}; auto& custom = option.custom_rack_option; custom.rack_option.size = sizeof(option); @@ -21,6 +21,7 @@ static uintptr_t Custom(uintptr_t system, std::uint32_t count, bool chain) { custom.rack_option.max_voices = 1; custom.rack_option.max_ports = 1; custom.num_buffers = count; + custom.port[0].source_buffer_id = outputBuffer; custom.num_modules = chain ? 2 : 0; custom.module[0].module_id = SCE_NGS2_CUSTOM_MODULE_ID_USER_FX; custom.module[1].module_id = SCE_NGS2_CUSTOM_MODULE_ID_GAIN; @@ -80,12 +81,67 @@ static void TestSetupLifecycle() { Require(sceNgs2SystemDestroy(system, nullptr) == 0); } +struct RepatchContext { + uintptr_t dest; + std::uint32_t calls = 0; +}; + +static void APS5_VABI Repatch(const Ngs2VoiceCallbackInfo* info) { + auto& context = *reinterpret_cast(info->callback_data); + Require(++context.calls == 1); + Control(info->voice_handle, SCE_NGS2_VOICE_PARAM_PATCH, Ngs2VoicePatchParam{{}, 0, 1, context.dest}); +} + +static void TestRepatch(std::uint32_t oldBuffers) { + const auto system = CreateSystem(); + const auto firstMaster = Mastering(system, 2); + const auto secondMaster = Mastering(system, 2); + Control(secondMaster, SCE_NGS2_MASTERING_VOICE_PARAM_OUTPUT, Ngs2MasteringVoiceOutputParam{{}, 1, 0}); + const auto first = Custom(system, oldBuffers, false, oldBuffers - 1); + const auto second = Custom(system, 2, false, 1); + Patch(first, firstMaster); + Patch(second, secondMaster); + const auto source = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER)); + Control(source, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, + Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 2, 48000, 0, 0, 0}}); + std::array pcm{}; + pcm.fill(8192); + const Ngs2WaveformBlock block{0, sizeof(pcm), 0, 0, Grain, 0, 0}; + Control(source, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, + Ngs2SamplerVoiceWaveformBlocksParam{{}, pcm.data(), 0, 1, &block}); + RepatchContext context{second}; + Control(source, SCE_NGS2_VOICE_PARAM_CALLBACK, + Ngs2VoiceCallbackParam{{}, Repatch, reinterpret_cast(&context), SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END, 0}); + Patch(source, first); + Event(source, SCE_NGS2_VOICE_EVENT_PLAY); + std::array, 2> outputs{}; + std::array info{}; + for (std::size_t i = 0; i < outputs.size(); i++) { + outputs[i].fill(9.0f); + info[i] = {outputs[i].data(), sizeof(outputs[i]), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 2}; + } + bool rejected = false; + try { + Require(sceNgs2SystemRender(system, info.data(), info.size()) == SCE_NGS2_OK); + } catch (const std::runtime_error& error) { + rejected = std::string_view(error.what()) == "NGS2: patch changes during source rendering are not implemented"; + } + Require(rejected && context.calls == 1); + Ngs2VoicePortInfo port{}; + Require(sceNgs2VoiceGetPortInfo(source, 0, &port, sizeof(port)) == SCE_NGS2_OK); + Require(port.dest_handle == second && port.dest_input_id == 1); + for (const auto& output : outputs) for (const auto sample : output) Require(sample == 9.0f); + Require(sceNgs2SystemDestroy(system, nullptr) == 0); +} + int main(int argc, char** argv) { Require(argc == 2); const std::string_view test = argv[1]; if (test == "self_callback") TestSelfCallback(); else if (test == "upstream_callback") TestUpstreamCallback(); else if (test == "setup_lifecycle") TestSetupLifecycle(); + else if (test == "repatch_one_buffer") TestRepatch(1); + else if (test == "repatch_two_buffers") TestRepatch(2); else return 1; return 0; } diff --git a/core/libs/prx/libSceNgs2/CMakeLists.txt b/core/libs/prx/libSceNgs2/CMakeLists.txt index 4bbc79d8d2..d17094e8ee 100644 --- a/core/libs/prx/libSceNgs2/CMakeLists.txt +++ b/core/libs/prx/libSceNgs2/CMakeLists.txt @@ -3,6 +3,18 @@ add_sce_ngs2_library(libSceNgs2) target_sources(libSceNgs2 PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/PendingExports.cpp) if(BUILD_TESTING) + add_test_executable(ngs2_core_custom_safety_tests ${CMAKE_CURRENT_SOURCE_DIR}/../libSceNgs2.native/tests/Ngs2CustomSafety.cpp) + target_include_directories(ngs2_core_custom_safety_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(ngs2_core_custom_safety_tests PRIVATE libSceNgs2 libkernel libc) + configure_windows_unwind(ngs2_core_custom_safety_tests) + foreach(safetyCase IN ITEMS self_callback upstream_callback setup_lifecycle repatch_one_buffer repatch_two_buffers) + add_test(NAME ngs2_core_custom_safety_${safetyCase} COMMAND ngs2_core_custom_safety_tests ${safetyCase}) + set_tests_properties(ngs2_core_custom_safety_${safetyCase} PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(ngs2_core_custom_safety_${safetyCase} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() foreach(ngs2Test IN ITEMS Gain Routing) string(TOLOWER ${ngs2Test} testName) add_test_executable(ngs2_core_${testName}_tests ${CMAKE_CURRENT_SOURCE_DIR}/../libSceNgs2.native/tests/Ngs2${ngs2Test}.cpp) From a43a610bbf8a8c5b9e7499dd2631247a8597a551 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Sun, 25 Oct 2026 01:52:16 -0300 Subject: [PATCH 4/6] fix(ngs2): reject routes changed during recursive rendering --- .../libs/prx/libSceNgs2.native/src/Render.cpp | 4 +++ .../libSceNgs2.native/tests/Ngs2Routing.cpp | 31 +++++++++++++++++++ docs/dev/debt/libSceNgs2.native.md | 2 +- 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceNgs2.native/src/Render.cpp b/core/libs/prx/libSceNgs2.native/src/Render.cpp index 2590239218..56f4ba9baa 100644 --- a/core/libs/prx/libSceNgs2.native/src/Render.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Render.cpp @@ -243,7 +243,11 @@ static void MixInputs(Ngs2Voice& voice, const std::vector& voices, s for (std::size_t p = 0; p < source->ports.size(); p++) { const auto& port = source->ports[p]; if (port.dest != &voice || port.volume == 0.0f) continue; + const auto destInput = port.destInput; RenderVoice(*source, voices, grain, systemRate); + if (port.dest != &voice || port.destInput != destInput) { + throw std::runtime_error("NGS2: patch changes during source rendering are not implemented"); + } if (!source->hasSamples) continue; const auto sourceBuffer = source->rack->customPortBuffers.empty() ? 0u : source->rack->customPortBuffers[p]; MixPort(voice, *source, port, sourceBuffer, grain); diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Routing.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Routing.cpp index 1979695e65..86789ec1a4 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Routing.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Routing.cpp @@ -163,6 +163,36 @@ static void TestGainRouting() { } } +static void TestUnityCopy() { + const auto system = CreateSystem(); + auto option = RackOption(2); + AddGain(option, 0, 1); + const auto custom = CustomVoice(CustomRack(system, option)); + SetGain(custom, 1.0f); + Output(system, custom, 0, 0); + Output(system, custom, 1, 1); + Render(system, {0.0f, 0.0f}); + std::vector source(Grain * 2), dest(Grain * 2); + for (std::uint32_t i = 0; i < Grain; i++) { + source[i * 2] = 8192; + source[i * 2 + 1] = -16384; + dest[i * 2] = -24576; + dest[i * 2 + 1] = 4096; + } + Route(Input(system, source), 0, custom); + Route(Input(system, dest), 0, custom, 1); + std::array, 2> outputs{}; + std::array info{}; + for (std::size_t i = 0; i < outputs.size(); i++) { + info[i] = {outputs[i].data(), sizeof(outputs[i]), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 2}; + } + Require(sceNgs2SystemRender(system, info.data(), info.size()) == SCE_NGS2_OK); + for (const auto& output : outputs) { + for (std::uint32_t i = 0; i < Grain; i++) Require(output[i * 2] == 0.25f && output[i * 2 + 1] == -0.5f); + } + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + static void TestIndependentVoicesAndSilentRamp(std::uint32_t channels) { const auto system = CreateSystem(); auto option = RackOption(4, channels); @@ -197,6 +227,7 @@ int main() { TestNoModules(3, {0.0f, -0.75f, 0.25f}); TestNoModules(4, {0.0f, 0.0f, -0.75f, 0.25f}); TestGainRouting(); + TestUnityCopy(); for (std::uint32_t channels = 1; channels <= 8; channels++) TestIndependentVoicesAndSilentRamp(channels); return 0; } diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index bac8dda70a..8c56041d71 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -43,7 +43,7 @@ - [sceNgs2VoiceGetOwner](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - the signature `(voiceHandle, outOwnerRack, outVoiceIndex)` follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ngs2.cs#L439-L440) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ngs2/ngs2.cpp#L322-L326), which only logs it; neither gives an error. The errors are unmeasured: a handle that is not a live voice answers `INVALID_VOICE_HANDLE` before the outputs are checked, as in `sceNgs2RackGetVoiceHandle`, and either output pointer null answers `INVALID_OUT_ADDRESS` without writing the other - [sceNgs2VoiceGetMatrixInfo](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a matrix id past the rack's `max_matrices` throws, as the PS5 code for shadPS4's PS4 [`INVALID_MATRIX_INDEX`](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ngs2/ngs2_error.h#L92) is unknown. A matrix without levels reports 0 levels, though it mixes through the default channel map. Signature from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ngs2.cs#L442), layout (a count and 64 levels) from [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ngs2/ngs2.h#L263-L266); levels are the stored, clamped ones. Unmeasured: a null info answers `INVALID_OUT_ADDRESS` and a size other than 260 `INVALID_OUT_SIZE` before the voice is checked, as in `sceNgs2VoiceGetPortInfo` - [sceNgs2CustomRackGetModuleInfo](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - a size over 32 bytes throws, as the longer per-module info in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ngs2/ngs2_custom.h#L419-L442) is not kept. A size under 32 and an index past the rack's modules (any on a non-custom rack) also throw: the PS5 codes for shadPS4's PS4 [`INVALID_MODULE_INDEX` and `INVALID_MODULE_INFO_SIZE`](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ngs2/ngs2_error.h#L117-L118) are unknown. It repeats the ids, state offset and size from rack creation, reserved words 0, as [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/Ngs2.cs#L323-L341) lays it out. Unmeasured: a null info gives `INVALID_OUT_ADDRESS`, then a bad handle `INVALID_RACK_HANDLE` -- [sceNgs2VoiceControl patch](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a destination rack handle with input id 0 resolves to that rack's first voice; this matches the Dead Cells call in #4334, but selection for racks with multiple voices has not been checked against a console. Nonzero destination input ids still throw. `sceNgs2VoiceGetPortInfo` reports the resolved voice handle and zero delay. +- [NGS2 patch destinations](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) - a rack handle with input 0 resolves to its first voice, matching the Dead Cells call in #4334; selection on multi-voice racks remains unverified. Input 1 is supported only for a voice handle in a two-input custom submixer. Nonzero rack-handle inputs, other unsupported inputs and nonzero inputs on disconnected ports throw because their contracts are unknown. GetPortInfo reports the resolved voice handle and selected input, with zero delay. Changes to a port destination or input while its source is rendered throw before mixing: callback routing order is unverified, and combining the old destination with the new input is invalid. - [sceNgs2ParseWaveformFile](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - the signature `(path, offset, info)` is taken from the call site of PPSA21402, which sets only three argument registers; the file is read from `offset` to its end, the data and block offsets are taken to be relative to the start of the file, not of `offset`, and an unreadable file throws. The audio unit fields of a 16-bit PCM waveform are assumed to be one sample frame per unit and per frame; `INVALID_WAVEFORM_DATA` for a `data` chunk that runs past the buffer or ends inside a frame is assumed, and looped PCM and other PCM formats throw - [sceNgs2VoiceControl sampler filter](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - param `0x1000000a`, its layout and meaning follow KytyPS5 only: location 1 type 1 is an RBJ low-pass biquad at the system sample rate after resampling, scaled by `level`, a set `channel_mask` bit bypasses that channel, type 0 turns the filter off. Other locations and types throw. The voice keeps playing a filter tail after its last block until the history is zero, with outputs below 1e-20 flushed to zero (a KytyPS5 choice); the console's tail length is unknown - [sceNgs2PanInit / sceNgs2PanGetVolumeMatrix](../../../core/libs/prx/libSceNgs2.native/src/Pan.cpp) (libSceNgs2.native) - follow the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output for 2, 4, 5 and 7 speakers into 1, 2, 6 and 8 channels; not yet confirmed on the PS5 library. Speakers are taken in the order L R, L R Ls Rs, L R C Ls Rs or L R C Ls Rs Lb Rb and kept in a fixed circular order, not sorted; without angles the defaults are 90/270, 40/110/250/320, 0/40/110/250/320 and 0/40/110/150/210/250/320 degrees. As in that library, defaults converted to radians keep the wrap-around entry in degrees, so angles past the last speaker pan almost fully to it. A speaker count of 0 without angles, which the firmware accepts, a non-finite speaker angle and a speaker angle equal to the unit angle, on which that library loops forever, throw. Angles so large that adding or subtracting a turn leaves them unchanged throw for the same reason From e50cbf349d4e0af8647627640df9c8c9ddf610b7 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Sun, 25 Oct 2026 02:06:55 -0300 Subject: [PATCH 5/6] test(ngs2): distinguish custom safety refusals --- .../tests/Ngs2CustomSafety.cpp | 17 +++++++--- .../prx/libSceNgs2.native/tests/Ngs2Gain.cpp | 31 +++++++++++++++++++ 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2CustomSafety.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2CustomSafety.cpp index 780b1124e2..c1c16813f9 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2CustomSafety.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2CustomSafety.cpp @@ -6,8 +6,10 @@ #include static uintptr_t target = 0; +static std::uint32_t setupCalls = 0; static int APS5_VABI ChangeSetup(Ngs2UserFxProcessContext* context) { + Require(++setupCalls == 1); std::fill_n(context->channel_data[0], context->num_grain_samples, 1.0f); Control(target, SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP, Ngs2CustomSubmixerVoiceSetupParam{{}, 8, 8, 0, 0}); return 0; @@ -38,12 +40,13 @@ static uintptr_t Custom(uintptr_t system, std::uint32_t count, bool chain, std:: return voice; } -static bool Rejected(uintptr_t system) { +static bool Rejected(uintptr_t system, std::string_view expected) { std::array out{}; const Ngs2RenderBufferInfo info{out.data(), sizeof(out), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 2}; try { Require(sceNgs2SystemRender(system, &info, 1) == 0); } catch (const std::runtime_error& error) { + if (std::string_view(error.what()) != expected) throw; std::printf("safely refused: %s\n", error.what()); return true; } @@ -51,15 +54,18 @@ static bool Rejected(uintptr_t system) { } static void TestSelfCallback() { + setupCalls = 0; const auto system = CreateSystem(); target = Custom(system, 1, true); Control(target, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX, Ngs2CustomVoiceUserFxParam{{}, ChangeSetup, 0, 0, 0}); Control(target, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 1, Ngs2CustomVoiceGainParam{}); - Require(Rejected(system)); + Require(Rejected(system, "NGS2: voice setup changes during custom processing are not implemented")); + Require(setupCalls == 1); Require(sceNgs2SystemDestroy(system, nullptr) == 0); } static void TestUpstreamCallback() { + setupCalls = 0; const auto system = CreateSystem(); target = Custom(system, 2, false); const auto source = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SUBMIXER)); @@ -67,7 +73,8 @@ static void TestUpstreamCallback() { Control(source, SCE_NGS2_SUBMIXER_VOICE_PARAM_USER_FX, Ngs2SubmixerVoiceUserFxParam{{}, ChangeSetup, 0, 0, 0}); Control(source, SCE_NGS2_VOICE_PARAM_PATCH, Ngs2VoicePatchParam{{}, 0, 1, target}); Event(source, SCE_NGS2_VOICE_EVENT_PLAY); - Require(Rejected(system)); + Require(Rejected(system, "NGS2: channel layout changes during input rendering are not implemented")); + Require(setupCalls == 1); Require(sceNgs2SystemDestroy(system, nullptr) == 0); } @@ -75,9 +82,9 @@ static void TestSetupLifecycle() { const auto system = CreateSystem(); target = Custom(system, 1, true); Control(target, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 1, Ngs2CustomVoiceGainParam{}); - Require(!Rejected(system)); + Require(!Rejected(system, "NGS2: custom Gain processing after setup or stop is not verified")); Control(target, SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP, Ngs2CustomSubmixerVoiceSetupParam{{}, 2, 2, 0, 0}); - Require(Rejected(system)); + Require(Rejected(system, "NGS2: custom Gain processing after setup or stop is not verified")); Require(sceNgs2SystemDestroy(system, nullptr) == 0); } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Gain.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Gain.cpp index 0080cb37fb..1a2af17ec8 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Gain.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Gain.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include static std::int32_t APS5_VABI Generate(Ngs2UserFxProcessContext* context) { const auto* signal = reinterpret_cast(context->user_data0); @@ -200,6 +202,34 @@ static void TestLinkedParameters() { ExpectConstant(graph.Render(), {4}); } +static void TestIntegerOutput() { + for (const float gain : {1.0f, -1.0f}) { + GainGraph graph(1); + graph.Set({gain}); + graph.Render(); + std::array out{}; + const Ngs2RenderBufferInfo info{out.data(), sizeof(out), SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1}; + Require(sceNgs2SystemRender(graph.system, &info, 1) == SCE_NGS2_OK); + for (const auto sample : out) Require(sample == (gain > 0.0f ? 16384 : -16384)); + } + for (const float gain : {std::numeric_limits::quiet_NaN(), std::numeric_limits::infinity(), -std::numeric_limits::infinity()}) { + GainGraph graph(1); + graph.Set({gain}); + graph.Render(); + std::array out{}; + out.fill(1234); + const Ngs2RenderBufferInfo info{out.data(), sizeof(out), SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1}; + bool rejected = false; + try { + Require(sceNgs2SystemRender(graph.system, &info, 1) == SCE_NGS2_OK); + } catch (const std::runtime_error& error) { + rejected = std::string_view(error.what()) == "NGS2: integer rendering of nonfinite samples is not implemented"; + } + Require(rejected); + for (const auto sample : out) Require(sample == 1234); + } +} + int main() { TestRamps(); TestEightChannels(); @@ -209,5 +239,6 @@ int main() { TestParameterPositions(); TestZeroClearsNanSource(); TestLinkedParameters(); + TestIntegerOutput(); return 0; } From c40ba98945e845b950ecca7819c472942757ad30 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Mon, 26 Oct 2026 09:04:33 -0300 Subject: [PATCH 6/6] test(ngs2): stop asserting unimplemented audio behavior --- .../libs/prx/libSceNgs2.native/CMakeLists.txt | 2 +- .../tests/Ngs2CustomSafety.cpp | 93 +++++++++---------- .../prx/libSceNgs2.native/tests/Ngs2Gain.cpp | 18 ---- core/libs/prx/libSceNgs2/CMakeLists.txt | 2 +- 4 files changed, 45 insertions(+), 70 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/CMakeLists.txt b/core/libs/prx/libSceNgs2.native/CMakeLists.txt index 1147ff7cf9..955cb1a6ca 100644 --- a/core/libs/prx/libSceNgs2.native/CMakeLists.txt +++ b/core/libs/prx/libSceNgs2.native/CMakeLists.txt @@ -6,7 +6,7 @@ if(BUILD_TESTING) target_include_directories(ngs2_custom_safety_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(ngs2_custom_safety_tests PRIVATE libSceNgs2.native libkernel libc) configure_windows_unwind(ngs2_custom_safety_tests) - foreach(safetyCase IN ITEMS self_callback upstream_callback setup_lifecycle repatch_one_buffer repatch_two_buffers) + foreach(safetyCase IN ITEMS custom_callback upstream_callback configured_gain routing_one_buffer routing_two_buffers) add_test(NAME ngs2_custom_safety_${safetyCase} COMMAND ngs2_custom_safety_tests ${safetyCase}) set_tests_properties(ngs2_custom_safety_${safetyCase} PROPERTIES TIMEOUT 10) if(WIN32) diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2CustomSafety.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2CustomSafety.cpp index c1c16813f9..6e0475ad98 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2CustomSafety.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2CustomSafety.cpp @@ -2,16 +2,14 @@ #include #include -#include #include -static uintptr_t target = 0; -static std::uint32_t setupCalls = 0; +static std::uint32_t processCalls = 0; -static int APS5_VABI ChangeSetup(Ngs2UserFxProcessContext* context) { - Require(++setupCalls == 1); +static int APS5_VABI Generate(Ngs2UserFxProcessContext* context) { + Require(++processCalls == 1); std::fill_n(context->channel_data[0], context->num_grain_samples, 1.0f); - Control(target, SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP, Ngs2CustomSubmixerVoiceSetupParam{{}, 8, 8, 0, 0}); + std::fill_n(context->channel_data[1], context->num_grain_samples, -0.5f); return 0; } @@ -40,66 +38,64 @@ static uintptr_t Custom(uintptr_t system, std::uint32_t count, bool chain, std:: return voice; } -static bool Rejected(uintptr_t system, std::string_view expected) { +static void ExpectOutput(uintptr_t system, float left, float right) { std::array out{}; const Ngs2RenderBufferInfo info{out.data(), sizeof(out), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 2}; - try { - Require(sceNgs2SystemRender(system, &info, 1) == 0); - } catch (const std::runtime_error& error) { - if (std::string_view(error.what()) != expected) throw; - std::printf("safely refused: %s\n", error.what()); - return true; + Require(sceNgs2SystemRender(system, &info, 1) == 0); + for (std::uint32_t i = 0; i < Grain; i++) { + Require(out[i * 2] == left && out[i * 2 + 1] == right); } - return false; } -static void TestSelfCallback() { - setupCalls = 0; +static void TestCustomCallback() { + processCalls = 0; const auto system = CreateSystem(); - target = Custom(system, 1, true); - Control(target, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX, Ngs2CustomVoiceUserFxParam{{}, ChangeSetup, 0, 0, 0}); + const auto master = Mastering(system, 2); + const auto target = Custom(system, 1, true); + Patch(target, master); + Control(target, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX, Ngs2CustomVoiceUserFxParam{{}, Generate, 0, 0, 0}); Control(target, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 1, Ngs2CustomVoiceGainParam{}); - Require(Rejected(system, "NGS2: voice setup changes during custom processing are not implemented")); - Require(setupCalls == 1); + ExpectOutput(system, 0.0f, 0.0f); + Require(processCalls == 1); Require(sceNgs2SystemDestroy(system, nullptr) == 0); } static void TestUpstreamCallback() { - setupCalls = 0; + processCalls = 0; const auto system = CreateSystem(); - target = Custom(system, 2, false); + const auto master = Mastering(system, 2); + const auto target = Custom(system, 2, false, 1); + Patch(target, master); const auto source = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SUBMIXER)); Control(source, SCE_NGS2_SUBMIXER_VOICE_PARAM_SETUP, Ngs2SubmixerVoiceSetupParam{{}, 2, 0}); - Control(source, SCE_NGS2_SUBMIXER_VOICE_PARAM_USER_FX, Ngs2SubmixerVoiceUserFxParam{{}, ChangeSetup, 0, 0, 0}); + Control(source, SCE_NGS2_SUBMIXER_VOICE_PARAM_USER_FX, Ngs2SubmixerVoiceUserFxParam{{}, Generate, 0, 0, 0}); Control(source, SCE_NGS2_VOICE_PARAM_PATCH, Ngs2VoicePatchParam{{}, 0, 1, target}); Event(source, SCE_NGS2_VOICE_EVENT_PLAY); - Require(Rejected(system, "NGS2: channel layout changes during input rendering are not implemented")); - Require(setupCalls == 1); + ExpectOutput(system, 1.0f, -0.5f); + Require(processCalls == 1); Require(sceNgs2SystemDestroy(system, nullptr) == 0); } -static void TestSetupLifecycle() { +static void TestConfiguredGain() { const auto system = CreateSystem(); - target = Custom(system, 1, true); + const auto master = Mastering(system, 2); + const auto target = Custom(system, 1, true); + Patch(target, master); Control(target, SCE_NGS2_CUSTOM_VOICE_PARAM_GAIN | 1, Ngs2CustomVoiceGainParam{}); - Require(!Rejected(system, "NGS2: custom Gain processing after setup or stop is not verified")); - Control(target, SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP, Ngs2CustomSubmixerVoiceSetupParam{{}, 2, 2, 0, 0}); - Require(Rejected(system, "NGS2: custom Gain processing after setup or stop is not verified")); + ExpectOutput(system, 0.0f, 0.0f); Require(sceNgs2SystemDestroy(system, nullptr) == 0); } -struct RepatchContext { - uintptr_t dest; +struct CallbackContext { std::uint32_t calls = 0; }; -static void APS5_VABI Repatch(const Ngs2VoiceCallbackInfo* info) { - auto& context = *reinterpret_cast(info->callback_data); +static void APS5_VABI CountBlockEnd(const Ngs2VoiceCallbackInfo* info) { + auto& context = *reinterpret_cast(info->callback_data); Require(++context.calls == 1); - Control(info->voice_handle, SCE_NGS2_VOICE_PARAM_PATCH, Ngs2VoicePatchParam{{}, 0, 1, context.dest}); } -static void TestRepatch(std::uint32_t oldBuffers) { +static void TestRouting(std::uint32_t oldBuffers) { const auto system = CreateSystem(); const auto firstMaster = Mastering(system, 2); const auto secondMaster = Mastering(system, 2); @@ -116,10 +112,11 @@ static void TestRepatch(std::uint32_t oldBuffers) { const Ngs2WaveformBlock block{0, sizeof(pcm), 0, 0, Grain, 0, 0}; Control(source, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, pcm.data(), 0, 1, &block}); - RepatchContext context{second}; + CallbackContext context; Control(source, SCE_NGS2_VOICE_PARAM_CALLBACK, - Ngs2VoiceCallbackParam{{}, Repatch, reinterpret_cast(&context), SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END, 0}); + Ngs2VoiceCallbackParam{{}, CountBlockEnd, reinterpret_cast(&context), SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END, 0}); Patch(source, first); + Control(source, SCE_NGS2_VOICE_PARAM_PATCH, Ngs2VoicePatchParam{{}, 0, 1, second}); Event(source, SCE_NGS2_VOICE_EVENT_PLAY); std::array, 2> outputs{}; std::array info{}; @@ -127,28 +124,24 @@ static void TestRepatch(std::uint32_t oldBuffers) { outputs[i].fill(9.0f); info[i] = {outputs[i].data(), sizeof(outputs[i]), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 2}; } - bool rejected = false; - try { - Require(sceNgs2SystemRender(system, info.data(), info.size()) == SCE_NGS2_OK); - } catch (const std::runtime_error& error) { - rejected = std::string_view(error.what()) == "NGS2: patch changes during source rendering are not implemented"; - } - Require(rejected && context.calls == 1); + Require(sceNgs2SystemRender(system, info.data(), info.size()) == SCE_NGS2_OK); + Require(context.calls == 1); Ngs2VoicePortInfo port{}; Require(sceNgs2VoiceGetPortInfo(source, 0, &port, sizeof(port)) == SCE_NGS2_OK); Require(port.dest_handle == second && port.dest_input_id == 1); - for (const auto& output : outputs) for (const auto sample : output) Require(sample == 9.0f); + for (const auto sample : outputs[0]) Require(sample == 0.0f); + for (const auto sample : outputs[1]) Require(sample == 0.25f); Require(sceNgs2SystemDestroy(system, nullptr) == 0); } int main(int argc, char** argv) { Require(argc == 2); const std::string_view test = argv[1]; - if (test == "self_callback") TestSelfCallback(); + if (test == "custom_callback") TestCustomCallback(); else if (test == "upstream_callback") TestUpstreamCallback(); - else if (test == "setup_lifecycle") TestSetupLifecycle(); - else if (test == "repatch_one_buffer") TestRepatch(1); - else if (test == "repatch_two_buffers") TestRepatch(2); + else if (test == "configured_gain") TestConfiguredGain(); + else if (test == "routing_one_buffer") TestRouting(1); + else if (test == "routing_two_buffers") TestRouting(2); else return 1; return 0; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Gain.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Gain.cpp index 1a2af17ec8..12b15b252b 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Gain.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Gain.cpp @@ -5,8 +5,6 @@ #include #include #include -#include -#include static std::int32_t APS5_VABI Generate(Ngs2UserFxProcessContext* context) { const auto* signal = reinterpret_cast(context->user_data0); @@ -212,22 +210,6 @@ static void TestIntegerOutput() { Require(sceNgs2SystemRender(graph.system, &info, 1) == SCE_NGS2_OK); for (const auto sample : out) Require(sample == (gain > 0.0f ? 16384 : -16384)); } - for (const float gain : {std::numeric_limits::quiet_NaN(), std::numeric_limits::infinity(), -std::numeric_limits::infinity()}) { - GainGraph graph(1); - graph.Set({gain}); - graph.Render(); - std::array out{}; - out.fill(1234); - const Ngs2RenderBufferInfo info{out.data(), sizeof(out), SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1}; - bool rejected = false; - try { - Require(sceNgs2SystemRender(graph.system, &info, 1) == SCE_NGS2_OK); - } catch (const std::runtime_error& error) { - rejected = std::string_view(error.what()) == "NGS2: integer rendering of nonfinite samples is not implemented"; - } - Require(rejected); - for (const auto sample : out) Require(sample == 1234); - } } int main() { diff --git a/core/libs/prx/libSceNgs2/CMakeLists.txt b/core/libs/prx/libSceNgs2/CMakeLists.txt index baf6e6a50f..1a0211601c 100644 --- a/core/libs/prx/libSceNgs2/CMakeLists.txt +++ b/core/libs/prx/libSceNgs2/CMakeLists.txt @@ -7,7 +7,7 @@ if(BUILD_TESTING) target_include_directories(ngs2_core_custom_safety_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(ngs2_core_custom_safety_tests PRIVATE libSceNgs2 libkernel libc) configure_windows_unwind(ngs2_core_custom_safety_tests) - foreach(safetyCase IN ITEMS self_callback upstream_callback setup_lifecycle repatch_one_buffer repatch_two_buffers) + foreach(safetyCase IN ITEMS custom_callback upstream_callback configured_gain routing_one_buffer routing_two_buffers) add_test(NAME ngs2_core_custom_safety_${safetyCase} COMMAND ngs2_core_custom_safety_tests ${safetyCase}) set_tests_properties(ngs2_core_custom_safety_${safetyCase} PROPERTIES TIMEOUT 10) if(WIN32)