From 56d8e5c4df7bb3d01df8f2ac9c7809ccc79bb8d4 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Tue, 6 Oct 2026 19:05:00 +0200 Subject: [PATCH] fix(libSceNgs2.native): mix through a selected matrix whose levels were never set PPSA22520 routes its title music through port matrix 0 without setting the matrix levels, so the music rendered silent. A matrix without levels now mixes like a port without a matrix, source channel n to destination channel n. The console's initial levels are unknown; the assumption is recorded in TechnicalDebt. Co-Authored-By: Claude Opus 5.5 --- .../libs/prx/libSceNgs2.native/src/Render.cpp | 2 +- .../libSceNgs2.native/tests/Ngs2Render.cpp | 41 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceNgs2.native/src/Render.cpp b/core/libs/prx/libSceNgs2.native/src/Render.cpp index b0133ed2f..cae8b38ae 100644 --- a/core/libs/prx/libSceNgs2.native/src/Render.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Render.cpp @@ -161,7 +161,7 @@ static void RenderSampler(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t s static void RenderVoice(Ngs2Voice& voice, const std::vector& voices, std::uint32_t grain, std::uint32_t systemRate); static void MixPort(Ngs2Voice& voice, const Ngs2Voice& source, const Ngs2Port& port, std::uint32_t grain) { - const auto* matrix = port.matrix < 0 ? nullptr : &source.matrices[port.matrix]; + const auto* matrix = port.matrix < 0 || source.matrices[port.matrix].empty() ? nullptr : &source.matrices[port.matrix]; const std::size_t outputs = matrix == nullptr ? voice.channels : std::min(voice.channels, matrix->size() / source.channels); for (std::size_t dst = 0; dst < outputs; dst++) { for (std::uint32_t src = 0; src < source.channels; src++) { diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp index ee375ba7a..fc0a60250 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp @@ -423,6 +423,46 @@ static void TestLock() { Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } +static void TestUnsetMatrix() { + const auto system = CreateSystem(); + const auto master = Mastering(system, 2); + const auto submixer = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SUBMIXER)); + Control(submixer, SCE_NGS2_SUBMIXER_VOICE_PARAM_SETUP, Ngs2SubmixerVoiceSetupParam{{}, 2, 0}); + Patch(submixer, master); + Event(submixer, SCE_NGS2_VOICE_EVENT_PLAY); + + const std::vector pcm(Grain, 16384); + const auto sampler = Sampler(system, pcm, 0); + Patch(sampler, submixer); + Control(sampler, SCE_NGS2_VOICE_PARAM_PORT_MATRIX, Ngs2VoicePortMatrixParam{{}, 0, 0}); + Control(sampler, SCE_NGS2_VOICE_PARAM_PORT_VOLUME, Ngs2VoicePortVolumeParam{{}, 0, 0.5f}); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + + std::vector out(Grain * 2, -1.0f); + const Ngs2RenderBufferInfo info{out.data(), out.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 2}; + Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 0.25f && out[i * 2 + 1] == 0.0f); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); + + const auto stereoSystem = CreateSystem(); + const auto stereoMaster = Mastering(stereoSystem, 2); + const auto stereo = Voice(CreateRack(stereoSystem, SCE_NGS2_RACK_ID_SAMPLER)); + Control(stereo, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 2, 48000, 0, 0, 0}}); + std::vector frames; + for (std::uint32_t i = 0; i < Grain; i++) { + frames.push_back(16384); + frames.push_back(8192); + } + const Ngs2WaveformBlock block{0, frames.size() * sizeof(std::int16_t), 0, 0, Grain, 0, 0}; + Control(stereo, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, frames.data(), 0, 1, &block}); + Patch(stereo, stereoMaster); + Control(stereo, SCE_NGS2_VOICE_PARAM_PORT_MATRIX, Ngs2VoicePortMatrixParam{{}, 0, 0}); + Event(stereo, SCE_NGS2_VOICE_EVENT_PLAY); + Require(sceNgs2SystemRender(stereoSystem, &info, 1) == SCE_NGS2_OK); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 0.5f && out[i * 2 + 1] == 0.25f); + Require(sceNgs2SystemDestroy(stereoSystem, nullptr) == SCE_NGS2_OK); +} + static void TestAllocator() { const Ngs2BufferAllocator allocator{Allocate, Release, 9}; uintptr_t system = 0; @@ -455,6 +495,7 @@ int main() { TestMasteringGain(); TestStereoIntoSurround(); TestLock(); + TestUnsetMatrix(); TestAllocator(); exitSystem = CreateSystem(); return 0; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 2a74a7879..c0b18b3c9 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -182,6 +182,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceRazorCpuPlotValue, sceRazorCpuWriteBookmark](../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signatures (series, float value) and (label, optional description) from [SharpProspero](https://github.com/SvenGDK/SharpProspero), matching PPSA06320's calls; both do nothing without a capture and return 0 - [sceAjmBatchJobGetInfo](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - writes the result and format sideband of a run job without buffers with the `SIDEBAND_FORMAT` flag (0x400000000000), as [KytyPS5](https://github.com/KytyPS5/KytyPS5) does, not confirmed on a PS5 title; the format reports bitrate 0 for ATRAC9 as in KytyPS5, while [shadPS4](https://github.com/shadps4-emu/shadPS4) derives it from the superframe size; before the instance is initialized it returns the not-initialized result (1) and no format, like the other run jobs here, while KytyPS5 returns result 0 with the format of the uninitialized decoder - [sceAjmDecMp3ParseFrame](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - signature, result layout and rate tables taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title: the layer field is not checked and MPEG-2.5 stops at 64 kbps; the original file length (`parse_ofl`) layout (Xing/Info + LAME tag, VBRI, Fraunhofer `0xB4` block with its CRC, encoder delay = samples per frame + LAME delay + 529) also comes from shadPS4 only, and the Fraunhofer block has no other public description +- [NGS2 port matrix](../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - a port that selects a matrix whose levels were never set mixes as a port without a matrix (source channel n to destination channel n, so a mono source reaches only channel 0). PPSA22520 selects matrix 0 for its title music without setting its levels, and the title is expected to have music on the console, so an unset matrix is assumed not to be silent; the console's initial levels are unknown. A matrix set with fewer levels than the destination needs still mixes only the rows it has - [sceAjmBatchJobGetCodecInfo, sceAjmBatchJobGetGaplessDecode](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `RUN_GET_CODEC_INFO` flag (0x800), the ATRAC9 codec info layout and its next frame size are taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [RPCSX](https://github.com/RPCSX/rpcsx) reimplementations, not confirmed on a PS5 title; a run job without buffers returns result 0, as in shadPS4. The gapless sideband reports the total and skip counts the title set and does not count them down: RPCSX does the same, shadPS4 counts them down and BryKytyPS5 returns its current counts - [AJM Opus decoder](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 24) - the initialize parameter layout (`u32 channels, u32 sample rate, u32`, seen as `2, 48000, 0`) and the little-endian `u16` byte count before each packet come from one title (The Smurfs Dreams); only 48000 Hz and a zero third word are accepted - [sceAjmBatchJobControl](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - RESET (bit 13), INITIALIZE (bit 14) and the sideband input order (gapless decode, then initialize parameters) come from [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4); other flags throw