diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index 2e7886f412..ca1b035a39 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -619,6 +619,15 @@ struct Ngs2SamplerVoiceState { }; static_assert(sizeof(Ngs2SamplerVoiceState) == 56); +struct Ngs2CustomSamplerVoiceState { + Ngs2VoiceState voice_state; + std::uint32_t reserved[2]; + std::uint64_t num_decoded_samples; + std::uint64_t reserved2[3]; +}; +static_assert(sizeof(Ngs2CustomSamplerVoiceState) == 48); +static_assert(offsetof(Ngs2CustomSamplerVoiceState, num_decoded_samples) == 16); + struct Ngs2WaveformInfo { Ngs2WaveformFormat format; std::uint32_t data_offset; diff --git a/core/libs/prx/libSceNgs2.native/src/Voice.cpp b/core/libs/prx/libSceNgs2.native/src/Voice.cpp index d90db41275..63a181d705 100644 --- a/core/libs/prx/libSceNgs2.native/src/Voice.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Voice.cpp @@ -452,6 +452,14 @@ int APS5_VABI sceNgs2VoiceGetState(uintptr_t voice_handle, Ngs2VoiceState* state submixer.voice_state.state_flags = voice.stateFlags; return SCE_NGS2_OK; } + case SCE_NGS2_RACK_ID_CUSTOM_SAMPLER: { + if (state_size != sizeof(Ngs2CustomSamplerVoiceState)) return SCE_NGS2_ERROR_INVALID_OUT_SIZE; + auto& sampler = *reinterpret_cast(state); + sampler = {}; + sampler.voice_state.state_flags = voice.stateFlags; + sampler.num_decoded_samples = voice.decodedSamples; + return SCE_NGS2_OK; + } default: throw std::runtime_error("NGS2: voice state of rack " + Ngs2Hex(voice.rack->rackId) + " is not implemented"); } } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp index c810add593..3b21b794c2 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp @@ -281,6 +281,13 @@ static void TestCustomSampler() { const auto out = Render(system); for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 0.5f && out[i * 2 + 1] == 0.75f); + Ngs2CustomSamplerVoiceState state{}; + Require(sceNgs2VoiceGetState(voice, &state.voice_state, sizeof(Ngs2SamplerVoiceState)) == SCE_NGS2_ERROR_INVALID_OUT_SIZE); + Require(sceNgs2VoiceGetState(voice, &state.voice_state, sizeof(state)) == SCE_NGS2_OK); + Require(state.voice_state.state_flags == (SCE_NGS2_VOICE_STATE_FLAG_INUSE | SCE_NGS2_VOICE_STATE_FLAG_PLAYING) && state.num_decoded_samples == Grain); + Render(system); + Require(sceNgs2VoiceGetState(voice, &state.voice_state, sizeof(state)) == SCE_NGS2_OK && state.num_decoded_samples == Grain * 2); + Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK); Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 1826329e43..888bd6d66d 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -25,3 +25,4 @@ - [sceNgs2GeomResetListenerParam, sceNgs2GeomResetSourceParam](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) (libSceNgs2.native) - the structures (12, 16, 16, 60, 96, 108, 32 and 308 bytes) come from [KytyPS5](https://github.com/KytyPS5/KytyPS5)'s `ngs2.cpp` and match [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_geom.h` field for field, but only KytyPS5 gives the behaviour - shadPS4 logs and returns success without writing the output. The defaults (orient front +Z, orient up +Y, sound speed 343, cone 1/360/1/360, rolloff 0/1000000/1/1, doppler factor, full-band, LFE and maximum level 1, two speakers and matrix format 2) are unverified on a PS5 title; KytyPS5 and iStark/PS5PCEM agree on them. PPSA15552 (Dead Cells) calls `sceNgs2GeomResetSourceParam` after creating its racks. A null output address returns `INVALID_OUT_ADDRESS` as elsewhere in the library rather than throwing as KytyPS5 does. `sceNgs2GeomApply` stays unimplemented - [sceNgs2GeomCalcListener](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) supports only a listener at the origin with front +Z, up +Y and flags 0. The identity matrix and preserved finite velocity and positive finite sound speed follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/ngs2.cpp) and [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/d2534bac6325a2c61c0fa88977e5321074304484/src/hle/ngs2_geom.zig) for that case, not a PS5 measurement. Other orientations (including non-unit vectors), translations and flags throw without changing the output; the matrix convention, handedness flags and console error codes remain unverified. Nonfinite input and nonpositive sound speed also throw. Dead Cells (PPSA15552) reaches this call after the custom sampler rack in #4334, but its arguments have not been captured here. Source attenuation, panning and Doppler in `sceNgs2GeomApply` remain unimplemented - [NGS2 custom sampler rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4001) - implements bounded support for the custom sampler rack and its built-in module 0x13 (Custom Voice Filter), unblocking PPSA15552 (Dead Cells) startup. Structure layouts come from [shadPS4's ngs2_custom.h](https://raw.githubusercontent.com/shadps4-emu/shadPS4/main/src/core/libraries/ngs2/ngs2_custom.h) (OrbisNgs2CustomVoiceFilterParam, 40 bytes) and live trace correlation from [prosper's hle_audio.cpp](https://raw.githubusercontent.com/mattias800/prosper/main/prosper/src/hle/audio/hle_audio.cpp) (0x40001300 command and parameters like 10000.0f and 0.707f). Routes parameter updates via the 0x40001300 command family into the Biquad filter engine, supporting only filter type 1; other filter types and unmodeled configurations throw. +- [sceNgs2VoiceGetState custom sampler](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native, rack 0x4001) - only the size and one field come from a title. PPSA15552 (Dead Cells) calls it on custom sampler voices with a 48-byte buffer at two sites (eboot `0x1740f90`, `0x1741009`) and reads only the 64-bit value at offset 16: the first site keeps it and the second subtracts the kept value, so it is filled with the voice's decoded sample count. Other sizes return `INVALID_OUT_SIZE` as for the other racks. The state flags fill the common voice state; the remaining fields stay zero. shadPS4's `OrbisNgs2CustomSamplerVoiceState` is about 80 bytes and does not match the size this title passes, so the field names and the console's value are unverified