fix(libSceNgs2.native): throw on reverb rack sizes and sample rates the reverb does not handle

A reverb_size other than the default was accepted and ignored, and below
about 7.5 kHz an allpass length went negative and wrapped to a 16 GB buffer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
JAB-dev 2026-10-09 17:38:18 +02:00
parent 1986905adb
commit 473ca04b2f
4 changed files with 31 additions and 2 deletions

View File

@ -94,6 +94,9 @@ static RackOptions CheckedRackOption(std::uint32_t rackId, const Ngs2RackOption*
throw std::invalid_argument("NGS2: invalid rack voice or channel count");
}
if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) Ngs2CheckCustomRack(options.customSubmixer.custom_rack_option);
if (rackId == SCE_NGS2_RACK_ID_REVERB && options.reverb.reverb_size != 1) {
throw std::runtime_error("NGS2: reverb rack size " + std::to_string(options.reverb.reverb_size) + " is not implemented");
}
return options;
}

View File

@ -4,6 +4,8 @@
#include <cstddef>
#include <cstdint>
#include <numbers>
#include <stdexcept>
#include <string>
#include <vector>
#include "prx/libc/include/General.hpp"
@ -215,7 +217,9 @@ static void ApplyParams(Ngs2ReverbState& reverb, float rate) {
for (std::uint32_t channel = 0; channel < 8; channel++) {
for (std::uint32_t stage = 0; stage < ALLPASSES; stage++) {
auto& allpass = reverb.channels[channel].allpasses[stage];
const auto length = static_cast<std::uint32_t>(static_cast<std::int32_t>(Samples(ALLPASS_TIMES[stage], rate)) + ALLPASS_OFFSETS[channel][stage]);
const auto signedLength = static_cast<std::int32_t>(Samples(ALLPASS_TIMES[stage], rate)) + ALLPASS_OFFSETS[channel][stage];
if (signedLength < 1) throw std::runtime_error("NGS2: reverb at a sample rate of " + std::to_string(reverb.sampleRate) + " Hz is not implemented");
const auto length = static_cast<std::uint32_t>(signedLength);
if (allpass.buffer.size() != length) allpass.buffer.assign(length, 0.0f);
if (allpass.cursor >= length) allpass.cursor = 0;
}

View File

@ -370,7 +370,29 @@ static void TestReverbSetup() {
Require(loudest > 0.0f && loudest < 1e-8f);
Ngs2VoiceState state{};
Require(sceNgs2VoiceGetState(reverb, &state, sizeof(state)) == SCE_NGS2_OK && (state.state_flags & SCE_NGS2_VOICE_STATE_FLAG_INUSE) != 0);
Require(sceNgs2SystemSetSampleRate(system, 7000) == SCE_NGS2_OK);
bool lowRate = false;
try { sceNgs2SystemRender(system, &info, 1); } catch (const std::runtime_error&) { lowRate = true; }
Require(lowRate);
Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK);
Ngs2ReverbRackOption option{};
option.rack_option.size = sizeof(option);
option.rack_option.max_grain_samples = 512;
option.rack_option.max_voices = 1;
option.rack_option.max_input_delay_blocks = 1;
option.rack_option.max_matrices = 1;
option.rack_option.max_ports = 8;
option.max_channels = 8;
option.reverb_size = 1;
Ngs2ContextBufferInfo query{};
Require(sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_REVERB, &option.rack_option, &query) == SCE_NGS2_OK);
for (std::uint32_t size : {0u, 2u}) {
option.reverb_size = size;
bool unsupported = false;
try { sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_REVERB, &option.rack_option, &query); } catch (const std::runtime_error&) { unsupported = true; }
Require(unsupported);
}
}
static void TestSampleRate() {

View File

@ -178,7 +178,7 @@ Throughout the project, every function at every stage either **does exactly what
- [sceAjmBatchJobControl](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - initialize parameters are 8 bytes for ATRAC9, 12 for Opus and none for MP3, as one title passes them; other codecs and sizes, and an output other than the 8-byte result, throw
- [sceAjmBatchJobControl](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - SIDEBAND_GAPLESS_DECODE with RESET overwrites both counts as in [RPCSX](https://github.com/RPCSX/rpcsx); shadPS4 ignores a zero count when the other is nonzero. Without RESET they differ, so it throws
- [NGS2 custom submixer rack](../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - structure layouts from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_custom.h`; module id 0x1f for UserFx2, the voice parameter id `0x40001f00 | module index`, the process flags (1 on the first process call after setup, 2 after a parameter change, 0 otherwise) and the call order (setup per voice at rack creation, process after the inputs are mixed, cleanup at destruction) come only from [KytyPS5](https://github.com/KytyPS5/KytyPS5). Only UserFx2 modules on a single buffer are accepted: other module ids, more buffers, a module control handler, different input and output channel counts, setup flags and default rack options throw. Each module's state is a separate buffer, not the `state_offset` slice of the voice state, and `sceNgs2VoiceGetState` still throws for this rack. As in KytyPS5, process runs only on grains where an input produced samples, so an effect tail stops with the input and a pending flag 2 waits for the next audible grain
- [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 rack options are handled: their 14400-sample reflection buffer is used whatever `reverb_size` says, and the library's half-rate mode is not implemented. Setup only accepts equal input and output channel counts
- [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
- [sceHttpParseResponseHeader](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); parsing and error codes follow [shadPS4's reverse-engineered PS4 library](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp), not confirmed on a PS5 title or hardware. Folded values retain their line breaks and whitespace; an empty first line starts the value scan after its line feed. A consumed byte count above `INT_MAX` throws instead of wrapping.
- [sceHttpParseStatusLine](../../core/libs/prx/libSceHttp/src/Parse.cpp) (libSceHttp) - behaviour and error codes follow the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title; unlike it, no byte past `lineLen` is read
- [sceHttpUriMerge](../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); validation, size formula, absolute/relative split and path join follow the disassembly-like body in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp), not confirmed on a PS5 title or hardware. The base URL goes through our `sceHttpUriParse`, which removes dot segments from the base path, while the relative part is appended unswept; base query and fragment are dropped. The relative reference is only classified by its scheme and a leading `//`, and only references with `//` are validated; as in shadPS4, `//host/x` is returned unchanged instead of taking the base scheme, a reference with a scheme but no `//` (`mailto:x`) is joined onto the base directory, and `?q` and `#f` are appended to the base directory instead of the base path.