feat(ngs2): implement legacy submixer UserFx callbacks
Invoke the title callback on planar mixed samples and silence. Preserve per-voice callback data, reset registration on setup, and release the render guard on callback failure. Include behavioral tests and source references. Written with AI assistance.
This commit is contained in:
parent
b1c5f10df8
commit
0d4db860a6
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
|
@ -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<float*, NGS2_MAX_CHANNELS> channels{};
|
||||
for (std::uint32_t c = 0; c < voice.channels; ++c) channels[c] = voice.samples.data() + static_cast<std::size_t>(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; });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
#define CORE_LIBS_PRX_LIBSCENGS2_SRC_NGS2INTERNAL_HPP
|
||||
|
||||
#include <cstddef>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
|
|
@ -114,6 +115,9 @@ struct Ngs2Voice {
|
|||
float fbwLevel = 1.0f;
|
||||
float lfeLevel = 1.0f;
|
||||
std::vector<Ngs2UserFx2> userFx;
|
||||
Ngs2UserFxProcessHandler userFxHandler = nullptr;
|
||||
std::array<std::uintptr_t, 3> userFxData{};
|
||||
std::uint32_t userFxFlags = 0;
|
||||
std::vector<float> 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
|
||||
|
|
|
|||
|
|
@ -188,14 +188,18 @@ static void RenderVoice(Ngs2Voice& voice, const std::vector<Ngs2Voice*>& 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<std::size_t>(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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Ngs2SubmixerVoiceUserFxParam>(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<Ngs2SubmixerVoiceSetupParam>(param).flags != 0) throw std::runtime_error("NGS2: submixer setup flags are not implemented");
|
||||
SetupMixer(voice, ParamAs<Ngs2SubmixerVoiceSetupParam>(param).num_io_channels);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,117 @@
|
|||
#include "Ngs2Test.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <stdexcept>
|
||||
|
||||
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<State*>(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<std::uintptr_t>(&state), 123, 456});
|
||||
}
|
||||
|
||||
static std::vector<float> Render(uintptr_t system, std::uint32_t channels) {
|
||||
std::vector<float> 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<std::int16_t, Grain * 2> 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();
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue