Merge pull request #5538 from JJenkx/fix/ngs2-render-capacity

fix(ngs2): limit render writes to the configured grain
This commit is contained in:
Adrià authored and GitHub committed 2026-10-15 13:29:33 +02:00
commit 5e7d09aafe
3 files changed
+71 -1

No files matched your search

+11 -1
View File
@@ -291,6 +291,16 @@ static void WriteOutput(const Ngs2RenderBufferInfo& output, const std::vector<fl
void Ngs2RenderSystem(Ngs2System& system, const Ngs2RenderBufferInfo* bufferInfo, std::uint32_t numBufferInfo) {
const auto grain = system.option.num_grain_samples;
std::vector<std::size_t> outputBytes(numBufferInfo);
for (std::uint32_t i = 0; i < numBufferInfo; i++) {
const auto& output = bufferInfo[i];
if (output.buffer == nullptr) continue;
if (output.num_channels == 0 || output.num_channels > NGS2_MAX_CHANNELS) {
throw std::invalid_argument("NGS2: render channel count is invalid");
}
outputBytes[i] = static_cast<std::size_t>(grain) * output.num_channels * SampleBytes(output.waveform_type);
if (output.buffer_size < outputBytes[i]) throw std::invalid_argument("NGS2: render buffer " + std::to_string(i) + " is too small");
}
std::vector<Ngs2Voice*> voices;
for (auto* rack : system.racks) {
for (auto& voice : rack->voices) {
@@ -311,8 +321,8 @@ void Ngs2RenderSystem(Ngs2System& system, const Ngs2RenderBufferInfo* bufferInfo
}
}
for (std::uint32_t i = 0; i < numBufferInfo; i++) {
if (bufferInfo[i].buffer != nullptr) std::memset(bufferInfo[i].buffer, 0, bufferInfo[i].buffer_size);
if (!mixes[i].empty()) WriteOutput(bufferInfo[i], mixes[i]);
else if (outputBytes[i] != 0) std::memset(bufferInfo[i].buffer, 0, outputBytes[i]);
}
for (auto* voice : voices) {
if (voice->state == Ngs2PlayState::Stopped) voice->state = Ngs2PlayState::Empty;
@@ -883,6 +883,60 @@ static void TestUnsetMatrix() {
Require(sceNgs2SystemDestroy(stereoSystem, nullptr) == SCE_NGS2_OK);
}
template <typename TSample>
static void TestRenderCapacity(std::uint32_t waveformType, std::uint32_t channels) {
const auto system = CreateSystem();
const auto samples = Grain * channels;
const TSample sentinel = 123;
std::vector<TSample> out(samples * 3, sentinel);
const Ngs2RenderBufferInfo info{out.data(), out.size() * sizeof(TSample), waveformType, channels};
Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK);
Require(std::all_of(out.begin(), out.begin() + samples, [](TSample value) { return value == 0; }));
Require(std::all_of(out.begin() + samples, out.end(), [sentinel](TSample value) { return value == sentinel; }));
const auto master = Mastering(system, channels);
const std::vector<std::int16_t> pcm(Grain, 16384);
const auto sampler = Sampler(system, pcm, 0);
Patch(sampler, master);
Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY);
std::fill(out.begin(), out.end(), sentinel);
Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK);
Require(std::all_of(out.begin(), out.begin() + samples, [sentinel](TSample value) { return value > 0 && value != sentinel; }));
Require(std::all_of(out.begin() + samples, out.end(), [sentinel](TSample value) { return value == sentinel; }));
Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK);
Require(std::all_of(out.begin(), out.begin() + samples, [](TSample value) { return value == 0; }));
Require(std::all_of(out.begin() + samples, out.end(), [sentinel](TSample value) { return value == sentinel; }));
Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK);
}
static void TestSilentRenderValidation() {
const auto system = CreateSystem();
std::vector<float> out(Grain, 123.0f);
Ngs2RenderBufferInfo info{out.data(), out.size() * sizeof(float) - 1, SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 1};
const auto invalid = [&] {
bool rejected = false;
try { sceNgs2SystemRender(system, &info, 1); } catch (const std::invalid_argument&) { rejected = true; }
Require(rejected);
Require(std::all_of(out.begin(), out.end(), [](float value) { return value == 123.0f; }));
};
invalid();
info.buffer_size++;
info.num_channels = 0;
invalid();
info.num_channels = 9;
invalid();
info.num_channels = 1;
info.waveform_type = UINT32_MAX;
bool unsupported = false;
try { sceNgs2SystemRender(system, &info, 1); } catch (const std::runtime_error&) { unsupported = true; }
Require(unsupported);
Require(std::all_of(out.begin(), out.end(), [](float value) { return value == 123.0f; }));
info = {};
Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK);
Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK);
}
static void TestAllocator() {
const Ngs2BufferAllocator allocator{Allocate, Release, 9};
uintptr_t system = 0;
@@ -922,6 +976,11 @@ int main() {
TestLock();
TestDefaultChannelMap();
TestUnsetMatrix();
TestRenderCapacity<float>(SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 1);
TestRenderCapacity<float>(SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 2);
TestRenderCapacity<std::int16_t>(SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1);
TestRenderCapacity<std::int16_t>(SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 2);
TestSilentRenderValidation();
TestAllocator();
exitSystem = CreateSystem();
return 0;
+1
View File
@@ -2,6 +2,7 @@
### Unknown function info
- [NGS2 render buffer capacity](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) is treated as available storage for one configured grain. PPSA01880 passes 303104 bytes for 256 stereo float frames (2048 bytes); rendering writes or clears only those frames and preserves the remaining capacity. Non-null silent outputs now undergo the same supported-format and minimum-size checks as audible outputs, with channel counts limited to 1 through 8. Unused null outputs remain ignored. The console's handling of surplus capacity and invalid silent output descriptors has not been measured; invalid descriptors throw rather than return guessed SDK errors.
- [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.
- [NGS2 ATRAC9 streaming](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) keeps waveform duration and skip counts across compressed input blocks. The shared `APPEND` flag (2), called `ONLY_DATA` in the compressed-data reference, ignores an ATRAC9 continuation block's sample, skip and repeat counts, following [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/bb98f26433fa9e711c219aac1711fe8ecba5bf3a/src/libs/ngs2.cpp); PPSA03747 confirms a 262044-byte initial buffer declaring 11310532 samples, followed by 16384-byte buffers with flags 3. Incomplete superframes are retained until more input arrives. Input-block completion callbacks allow refills; the final callback of a logical waveform waits for its buffered PCM to drain, preserving the existing complete-block repeat behaviour. This buffering, callback timing, decoded-byte accounting and starvation silence are unverified on hardware. A closed ATRAC9 stream ending before its declared samples throws; fragmented ATRAC9 waveforms with repeats are unsupported. PCM streaming retains its existing append validation and early-close behaviour.