diff --git a/core/libs/prx/libSceNgs2.native/CMakeLists.txt b/core/libs/prx/libSceNgs2.native/CMakeLists.txt index 23f9aae1e..8d100ce35 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) + foreach(ngs2Test IN ITEMS Render Atrac9 Filter Custom Parse Stream UserFx) 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 0838450c4..3496f022f 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -49,6 +49,7 @@ static constexpr std::uint32_t SCE_NGS2_SAMPLER_VOICE_PARAM_EXIT_LOOP = 0x100000 static constexpr std::uint32_t SCE_NGS2_SAMPLER_VOICE_PARAM_PITCH = 0x10000005; static constexpr std::uint32_t SCE_NGS2_SAMPLER_VOICE_PARAM_FILTER = 0x1000000a; static constexpr std::uint32_t SCE_NGS2_SUBMIXER_VOICE_PARAM_SETUP = 0x20000000; +static constexpr std::uint32_t SCE_NGS2_SUBMIXER_VOICE_PARAM_USER_FX = 0x20000004; static constexpr std::uint32_t SCE_NGS2_MASTERING_VOICE_PARAM_SETUP = 0x30000000; static constexpr std::uint32_t SCE_NGS2_MASTERING_VOICE_PARAM_GAIN = 0x30000004; static constexpr std::uint32_t SCE_NGS2_MASTERING_VOICE_PARAM_OUTPUT = 0x30000005; @@ -173,6 +174,20 @@ struct Ngs2UserFx2SetupContext { }; static_assert(sizeof(Ngs2UserFx2SetupContext) == 72); +struct Ngs2UserFxProcessContext { + float** channel_data; + std::uintptr_t user_data0; + std::uintptr_t user_data1; + std::uintptr_t user_data2; + std::uint32_t flags; + std::uint32_t num_channels; + std::uint32_t num_grain_samples; + std::uint32_t sample_rate; +}; +static_assert(sizeof(Ngs2UserFxProcessContext) == 48); + +using Ngs2UserFxProcessHandler = std::int32_t (APS5_VABI *)(Ngs2UserFxProcessContext*); + using Ngs2UserFx2CleanupContext = Ngs2UserFx2SetupContext; struct Ngs2UserFx2ControlContext { @@ -406,6 +421,15 @@ struct Ngs2SubmixerVoiceSetupParam { }; static_assert(sizeof(Ngs2SubmixerVoiceSetupParam) == 16); +struct Ngs2SubmixerVoiceUserFxParam { + Ngs2VoiceParamHeader header; + Ngs2UserFxProcessHandler handler; + std::uintptr_t user_data0; + std::uintptr_t user_data1; + std::uintptr_t user_data2; +}; +static_assert(sizeof(Ngs2SubmixerVoiceUserFxParam) == 40); + struct Ngs2CustomSubmixerVoiceSetupParam { Ngs2VoiceParamHeader header; std::uint32_t num_input_channels; diff --git a/core/libs/prx/libSceNgs2.native/src/Custom.cpp b/core/libs/prx/libSceNgs2.native/src/Custom.cpp index d97ae633c..c77dc5205 100644 --- a/core/libs/prx/libSceNgs2.native/src/Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Custom.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -109,3 +110,15 @@ void Ngs2ProcessUserFx(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t samp fx.flags = 0; } } + +void Ngs2ProcessLegacyUserFx(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t sampleRate) { + if (voice.userFxHandler == nullptr) return; + std::array channels{}; + for (std::uint32_t c = 0; c < voice.channels; ++c) channels[c] = voice.samples.data() + static_cast(c) * grain; + Ngs2UserFxProcessContext context{channels.data(), voice.userFxData[0], voice.userFxData[1], voice.userFxData[2], + voice.userFxFlags, voice.channels, grain, sampleRate}; + const int result = voice.userFxHandler(&context); + if (result != SCE_NGS2_OK) throw std::runtime_error("NGS2: the UserFx process handler failed with " + std::to_string(result)); + voice.userFxFlags = 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 3a69ea8bc..ab1486207 100644 --- a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp +++ b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp @@ -2,6 +2,7 @@ #define CORE_LIBS_PRX_LIBSCENGS2_SRC_NGS2INTERNAL_HPP #include +#include #include #include #include @@ -114,6 +115,9 @@ struct Ngs2Voice { float fbwLevel = 1.0f; float lfeLevel = 1.0f; std::vector userFx; + Ngs2UserFxProcessHandler userFxHandler = nullptr; + std::array userFxData{}; + std::uint32_t userFxFlags = 0; std::vector samples; bool rendering = false; bool rendered = false; @@ -176,6 +180,7 @@ void Ngs2SetupUserFx(Ngs2Rack& rack, const Ngs2CustomRackOption& option); void Ngs2CleanupUserFx(Ngs2Rack& rack); void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param); void Ngs2ProcessUserFx(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t sampleRate); +void Ngs2ProcessLegacyUserFx(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t sampleRate); void Ngs2RenderSystem(Ngs2System& system, const Ngs2RenderBufferInfo* bufferInfo, std::uint32_t numBufferInfo); #endif diff --git a/core/libs/prx/libSceNgs2.native/src/Render.cpp b/core/libs/prx/libSceNgs2.native/src/Render.cpp index 8246d3857..b0133ed2f 100644 --- a/core/libs/prx/libSceNgs2.native/src/Render.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Render.cpp @@ -188,14 +188,18 @@ static void RenderVoice(Ngs2Voice& voice, const std::vector& voices, if (voice.rendered) return; if (voice.rendering) throw std::invalid_argument("NGS2: the voice patches form a cycle"); voice.rendering = true; + struct RenderingScope { + bool& active; + ~RenderingScope() { active = false; } + } renderingScope{voice.rendering}; voice.samples.assign(static_cast(grain) * voice.channels, 0.0f); voice.hasSamples = false; if (voice.state == Ngs2PlayState::Playing && voice.channels != 0) { if (voice.rack->rackId == SCE_NGS2_RACK_ID_SAMPLER) RenderSampler(voice, grain, systemRate); else MixInputs(voice, voices, grain, systemRate); + Ngs2ProcessLegacyUserFx(voice, grain, systemRate); if (voice.hasSamples) Ngs2ProcessUserFx(voice, grain, systemRate); } - voice.rendering = false; voice.rendered = true; } diff --git a/core/libs/prx/libSceNgs2.native/src/Voice.cpp b/core/libs/prx/libSceNgs2.native/src/Voice.cpp index 75da6d139..fb21bc22d 100644 --- a/core/libs/prx/libSceNgs2.native/src/Voice.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Voice.cpp @@ -25,6 +25,7 @@ void Ngs2Voice::SetEvent(std::uint32_t eventId) { if (state == Ngs2PlayState::Empty || state == Ngs2PlayState::Stopped) { state = Ngs2PlayState::Playing; stateFlags |= SCE_NGS2_VOICE_STATE_FLAG_INUSE; + if (userFxHandler) userFxFlags = 1; } break; case SCE_NGS2_VOICE_EVENT_STOP: @@ -62,6 +63,9 @@ void Ngs2Voice::ResetSetup() { decodedSamples = 0; decodedBytes = 0; waveformEnd = nullptr; + userFxHandler = nullptr; + userFxData = {}; + userFxFlags = 0; } const std::uint8_t* Ngs2StreamEnd(const Ngs2Voice& voice, const Ngs2Block& block) { @@ -289,6 +293,13 @@ static void ApplyParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { switch (rackId) { case SCE_NGS2_RACK_ID_SAMPLER: ApplySamplerParam(voice, param); return; case SCE_NGS2_RACK_ID_SUBMIXER: + if (param.id == SCE_NGS2_SUBMIXER_VOICE_PARAM_USER_FX) { + const auto& fx = ParamAs(param); + voice.userFxHandler = fx.handler; + voice.userFxData = {fx.user_data0, fx.user_data1, fx.user_data2}; + voice.userFxFlags = fx.handler ? 1 : 0; + return; + } if (param.id != SCE_NGS2_SUBMIXER_VOICE_PARAM_SETUP) break; if (ParamAs(param).flags != 0) throw std::runtime_error("NGS2: submixer setup flags are not implemented"); SetupMixer(voice, ParamAs(param).num_io_channels); diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2UserFx.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2UserFx.cpp new file mode 100644 index 000000000..e72e1dc86 --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2UserFx.cpp @@ -0,0 +1,117 @@ +#include "Ngs2Test.hpp" + +#include +#include + +struct State { + std::uint32_t calls = 0; + std::uint32_t flags = 0; + std::uint32_t channels = 0; + float gain = 1.0f; + float bias = 0.0f; + int result = 0; +}; + +static int APS5_VABI Process(Ngs2UserFxProcessContext* context) { + auto& state = *reinterpret_cast(context->user_data0); + Require(context->user_data1 == 123 && context->user_data2 == 456); + Require(context->num_channels == state.channels && context->num_grain_samples == Grain && context->sample_rate == 48000); + state.calls++; + state.flags = context->flags; + for (std::uint32_t c = 0; c < context->num_channels; ++c) + for (std::uint32_t i = 0; i < context->num_grain_samples; ++i) + context->channel_data[c][i] = context->channel_data[c][i] * state.gain + state.bias * (c + 1); + return state.result; +} + +static void Install(uintptr_t voice, State& state) { + Control(voice, SCE_NGS2_SUBMIXER_VOICE_PARAM_USER_FX, + Ngs2SubmixerVoiceUserFxParam{{}, Process, reinterpret_cast(&state), 123, 456}); +} + +static std::vector Render(uintptr_t system, std::uint32_t channels) { + std::vector output(Grain * channels, -1.0f); + const Ngs2RenderBufferInfo buffer{output.data(), output.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, channels}; + Require(sceNgs2SystemRender(system, &buffer, 1) == 0); + return output; +} + +static void TestGenerator() { + const auto system = CreateSystem(); + const auto master = Mastering(system, 8); + const auto voice = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SUBMIXER)); + Control(voice, SCE_NGS2_SUBMIXER_VOICE_PARAM_SETUP, Ngs2SubmixerVoiceSetupParam{{}, 8, 0}); + State state{0, 0, 8, 1.0f, 0.125f}; + Install(voice, state); + Patch(voice, master); + Event(voice, SCE_NGS2_VOICE_EVENT_PLAY); + auto output = Render(system, 8); + Require(state.calls == 1 && state.flags == 1); + for (std::size_t i = 0; i < output.size(); ++i) Require(output[i] == 0.125f * (i % 8 + 1)); + Render(system, 8); + Require(state.calls == 2 && state.flags == 0); + Event(voice, SCE_NGS2_VOICE_EVENT_PAUSE); + for (float sample : Render(system, 8)) Require(sample == 0.0f); + Require(state.calls == 2); + Event(voice, SCE_NGS2_VOICE_EVENT_RESUME); + Render(system, 8); + Require(state.calls == 3 && state.flags == 0); + Event(voice, SCE_NGS2_VOICE_EVENT_STOP_IMM); + for (float sample : Render(system, 8)) Require(sample == 0.0f); + Event(voice, SCE_NGS2_VOICE_EVENT_PLAY); + Render(system, 8); + Require(state.calls == 4 && state.flags == 1); + Control(voice, SCE_NGS2_SUBMIXER_VOICE_PARAM_USER_FX, Ngs2SubmixerVoiceUserFxParam{}); + for (float sample : Render(system, 8)) Require(sample == 0.0f); + Require(state.calls == 4); + Install(voice, state); + state.result = -5; + bool rejected = false; + try { Render(system, 8); } catch (const std::runtime_error&) { rejected = true; } + Require(rejected); + state.result = 0; + Render(system, 8); + Require(state.calls == 6 && state.flags == 1); + Control(voice, SCE_NGS2_SUBMIXER_VOICE_PARAM_SETUP, Ngs2SubmixerVoiceSetupParam{{}, 8, 0}); + Patch(voice, master); + Event(voice, SCE_NGS2_VOICE_EVENT_PLAY); + for (float sample : Render(system, 8)) Require(sample == 0.0f); + Require(state.calls == 6); + Require(sceNgs2SystemDestroy(system, nullptr) == 0); +} + +static void TestEffectAndIndependentVoices() { + const auto system = CreateSystem(); + const auto master = Mastering(system, 2); + const auto first = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SUBMIXER)); + const auto second = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SUBMIXER)); + for (auto voice : {first, second}) { + Control(voice, SCE_NGS2_SUBMIXER_VOICE_PARAM_SETUP, Ngs2SubmixerVoiceSetupParam{{}, 2, 0}); + Patch(voice, master); + Event(voice, SCE_NGS2_VOICE_EVENT_PLAY); + } + State firstState{0, 0, 2, 0.5f, 0.0f}; + State secondState{0, 0, 2, 1.0f, 0.125f}; + Install(first, firstState); + Install(second, secondState); + const auto sampler = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER)); + Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 2, 48000, 0, 0, 0}}); + std::array pcm{}; + for (std::size_t i = 0; i < pcm.size(); ++i) pcm[i] = i % 2 == 0 ? 16384 : -8192; + const Ngs2WaveformBlock block{0, sizeof(pcm), 0, 0, Grain, 0, 0}; + Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, pcm.data(), 0, 1, &block}); + Patch(sampler, first); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + const auto mixed = Render(system, 2); + for (std::size_t i = 0; i < mixed.size(); ++i) Require(mixed[i] == (i % 2 == 0 ? 0.375f : 0.125f)); + Require(firstState.calls == 1 && secondState.calls == 1 && Flags(sampler) == 0); + const auto tail = Render(system, 2); + for (std::size_t i = 0; i < tail.size(); ++i) Require(tail[i] == (i % 2 == 0 ? 0.125f : 0.25f)); + Require(firstState.calls == 2 && secondState.calls == 2); + Require(sceNgs2SystemDestroy(system, nullptr) == 0); +} + +int main() { + TestGenerator(); + TestEffectAndIndependentVoices(); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index be1890e79..80d703ec1 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -41,6 +41,7 @@ Throughout the project, every function at every stage either **does exactly what - PPSA01317 imports declared without parameters, signatures unknown: the remaining [libSceHmd2](../../core/libs/prx/libSceHmd2/Export.cpp) and [libSceVrTracker2](../../core/libs/prx/libSceVrTracker2/Export.cpp) exports. Names from the aerolib NID list - [libSceMbus](../../core/libs/prx/libSceMbus/Export.cpp) (`libSceDeviceService`) - signatures taken from PPSA01317, which calls `sceDeviceServiceInitialize(3, 0)`, polls `sceDeviceServiceGetEventState(1)` and queries again when bit 0 or 1 is set, and calls `sceDeviceServiceQueryDeviceInfo_(0x7001, 0, 0, infos, 1, &count, &unknown, 0x70)`, treating a negative result as an error and `count` as the number of entries. The host has no devices: the event state is 0 and a query returns 0 with `count` 0 - [NGS2 ATRAC9 waveform loops](../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) accepts one integer forward `smpl` loop. Inclusive RIFF endpoints and encoder-delay adjustment follow [vgmstream riff.c](https://github.com/vgmstream/vgmstream/blob/7dc938fa2f210943b37c7b6511852b516ef432ab/src/meta/riff.c). PPSA03747 supplies a 60-byte sampler chunk with one loop, `samplerData` 24, start 256, inclusive end 299224 and delay 256 for 298969 audible samples. The chunk size and loop count bound the records; `samplerData` is not interpreted. Output positions are assumed to use audible samples and an exclusive end. Blocks split into an optional intro, a repeated range and an optional tail; zero play count means infinite repeats, positive play count includes the first pass. ATRAC9 ranges include a preceding codec frame for decoder history. The output block layout, repeat-count mapping and error codes are unverified on hardware. Multiple, reverse, alternating and fractional loops throw. +- [NGS2 submixer UserFx](../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (control `0x20000004`) invokes the title's callback after input mixing on every playing grain, including silence. The 40-byte control and 48-byte process context follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/libraries/ngs2/ngs2_submixer.h). PPSA03747 installs a peak-meter callback; its code confirms the channel pointers, user data 0, channel count, grain size and sample-rate offsets and returns 0. Other user data words are preserved. Flags are assumed to be 1 after registration/restart and 0 thereafter, following UserFx2's first-process convention; this callback does not inspect them. Null handlers disable the effect, setup clears it, and nonzero callback results throw. These lifecycle and error semantics are unverified on console hardware. - PPSA01341 imports declared without parameters, signatures unknown: [sceAgcSetSemaphoreMemory](../../core/libs/prx/libSceAgc/Unimplemented.cpp), [sceAgcDriverRegisterMultipleResources](../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp). Names from the shadPS4 aerolib NID list - [sceAppContentDownloadDataGetAvailableSpaceKb](../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - a title whose `param.json` declares `downloadDataSize` 0 gets 0 KB and OK; the console's behaviour for a title without a download data area is unverified (it may return an error instead). KytyPS5 does not special-case it, and shadPS4 stubs 1048576 KB - [7CxI50-xlCk, pMxXhNozUX8](../../core/libs/prx/libSceNpPartner001/Export.cpp) (libSceNpPartner001) - unknown names and signatures, imported by PPSA23566; declared without parameters