From 233590462c94076cfdb3289fd2dce33797ea9879 Mon Sep 17 00:00:00 2001 From: Simon <340958984+ee6aba54@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:40:46 +0200 Subject: [PATCH] fix(libSceNgs2): report EMPTY for a streaming sampler that played its queue A playing sampler voice whose waveform still accepts blocks (CONTINUE) reported only INUSE|PLAYING after it had played every queued block, so a title that waits for EMPTY before it reuses the voice never did. PPSA18391 (engine: Clickteam Fusion) reads state_flags & 0x22 != 2 from sceNgs2VoiceGetState as the end of a sound; each of its sound effects played once and never again. The voice now also reports EMPTY (0x20) in that state and keeps playing, so blocks added later still play. A waveform that was closed without CONTINUE does not report it. --- .../libSceNgs2.native/include/Ngs2Types.hpp | 1 + .../libs/prx/libSceNgs2.native/src/Render.cpp | 1 + .../libSceNgs2.native/tests/Ngs2Stream.cpp | 27 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 30 insertions(+) diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index c79d3ad7c..f7931d83f 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -36,6 +36,7 @@ static constexpr std::uint32_t SCE_NGS2_VOICE_STATE_FLAG_INUSE = 1; static constexpr std::uint32_t SCE_NGS2_VOICE_STATE_FLAG_PLAYING = 2; static constexpr std::uint32_t SCE_NGS2_VOICE_STATE_FLAG_PAUSED = 4; static constexpr std::uint32_t SCE_NGS2_VOICE_STATE_FLAG_STOPPED = 8; +static constexpr std::uint32_t SCE_NGS2_VOICE_STATE_FLAG_EMPTY = 0x20; static constexpr std::uint32_t SCE_NGS2_VOICE_PARAM_MATRIX_LEVELS = 0x0001; static constexpr std::uint32_t SCE_NGS2_VOICE_PARAM_PORT_VOLUME = 0x0002; diff --git a/core/libs/prx/libSceNgs2.native/src/Render.cpp b/core/libs/prx/libSceNgs2.native/src/Render.cpp index c93902179..6cf69f714 100644 --- a/core/libs/prx/libSceNgs2.native/src/Render.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Render.cpp @@ -314,6 +314,7 @@ void Ngs2RenderSystem(Ngs2System& system, const Ngs2RenderBufferInfo* bufferInfo for (auto* voice : voices) { if (voice->state == Ngs2PlayState::Stopped) voice->state = Ngs2PlayState::Empty; voice->stateFlags = static_cast(voice->state); + if (voice->state == Ngs2PlayState::Playing && voice->rack->rackId == SCE_NGS2_RACK_ID_SAMPLER && voice->blocks.empty() && voice->acceptsBlocks) voice->stateFlags |= SCE_NGS2_VOICE_STATE_FLAG_EMPTY; } system.renderCount++; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Stream.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Stream.cpp index af1e0b06e..726cf9148 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Stream.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Stream.cpp @@ -158,10 +158,37 @@ static void TestSilenceBlock() { Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } +static void TestDrainedStreamReportsEmpty() { + const auto system = CreateSystem(); + const auto master = Mastering(system, 1); + const auto rack = CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER); + const auto voice = StreamVoice(rack, 0, master); + const std::vector pcm(4, 16384); + constexpr std::uint32_t playing = SCE_NGS2_VOICE_STATE_FLAG_INUSE | SCE_NGS2_VOICE_STATE_FLAG_PLAYING; + + AddBlock(voice, pcm.data(), pcm.size() * 2, 4, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_RESET | SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE); + Event(voice, SCE_NGS2_VOICE_EVENT_PLAY); + RenderI16(system); + Require(Flags(voice) == (playing | SCE_NGS2_VOICE_STATE_FLAG_EMPTY)); + + const std::vector longer(Grain * 2, 8192); + AddBlock(voice, longer.data(), longer.size() * 2, Grain * 2, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE); + const auto out = RenderI16(system); + Require(out[0] == 8192); + Require(Flags(voice) == playing); + + AddBlock(voice, pcm.data(), pcm.size() * 2, 4, 0); + RenderI16(system); + RenderI16(system); + Require((Flags(voice) & SCE_NGS2_VOICE_STATE_FLAG_EMPTY) == 0); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + int main() { TestStreamEndsAtDeclaredSamples(); TestStreamClosedBeforeDeclaredSamples(); TestStreamRejections(); TestSilenceBlock(); + TestDrainedStreamReportsEmpty(); return 0; } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index face0cf0e..6e7eced0d 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -257,6 +257,7 @@ Throughout the project, every function at every stage either **does exactly what - [NGS2 silent waveform blocks](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native): flag `0x10` (named `SILENCE` here; no public reference names it) lets a waveform block declare samples without data. PPSA04867 adds one block with flags `0x11`, `data_size` 0 and `num_samples` 16385, then another with flags `0x11` and a fully specified 0x800-byte, 1024-sample mono block, which takes the ordinary path. A dataless block under the flag outputs `num_samples` frames of silence, reports them through `decoded_data_size`, and finishes like a complete block; a dataless block without the flag still throws as not fitting its data. The flag's real name, its interaction with ATRAC9 and repeats, and the console's `waveform_data` for silent blocks are unverified on hardware - [NGS2 streaming PCM waveforms](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - PPSA21402 streams its music by adding one block whose `num_samples` is the whole track and whose `data_size` is a 16 KiB first piece, with `CONTINUE`, and then adds each following piece with flag `0x2` (called `APPEND` here) and the same `num_samples`. No reference describes flag `0x2`. A 16-bit PCM block without skipped samples or repeats whose `num_samples` exceed its data is assumed to start such a stream; flag `0x2` is assumed to append the block's data to the last streaming block, ignoring its `num_samples`; the voice is assumed to output silence without advancing while it has played all appended data and `CONTINUE` is set; and the waveform is assumed to end at `num_samples` or, once `CONTINUE` is cleared, at the end of the appended data. `sceNgs2VoiceGetState` reports the bytes played as `decoded_data_size`, which the title uses to release its queued data, and the read position as `waveform_data`. Appending to a block that is not a stream, or other formats, skipped samples, repeats and partial frames, throw - [NGS2 matrix levels](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - levels are clamped to -4..4 and NaN is kept, as the PS4 firmware 11.00 `libSceNgs2.sprx` stores them; not yet confirmed on the PS5 library. That library rejects a level count of 0 and keeps the matrix; the PS5 error code is unknown, so a count of 0 throws. `sceNgs2VoiceRunCommands` is not in that library, so its matrix levels command is assumed to clamp the same way +- [NGS2 sampler voice state flag EMPTY](../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - a playing sampler voice that still accepts blocks (`CONTINUE`) and has played all queued blocks reports `INUSE|PLAYING|EMPTY` (0x20) and keeps playing, so blocks added later play on. PPSA18391 (engine: Clickteam Fusion) reads `state_flags & 0x22 != 2` from `sceNgs2VoiceGetState` as the end of a sound and only then reuses the voice. When the console sets and clears EMPTY, and whether it also reports it for other voice types, is unverified on hardware. - [sceAvPlayerStartEx](../../core/libs/prx/libSceAvPlayer/Export.cpp) (libSceAvPlayer) - start info layout unknown; it is ignored and playback starts as with `sceAvPlayerStart` - [sceAvPlayerInit / sceAvPlayerInitEx](../../core/libs/prx/libSceAvPlayer/src/Player.cpp) (libSceAvPlayer) - behaviour without a memory replacement unknown; frame and sample buffers then come from the guest heap - [sceAvPlayerInitEx](../../core/libs/prx/libSceAvPlayer/Export.cpp) (libSceAvPlayer) - the PS5 init data layout (`this_size` 0x230: auto start at 0x74, nine 0x30-byte thread parameter blocks from 0x78, output video frame buffer count at 0x228) follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/avPlayer.cpp), [prosper](https://github.com/mattias800/prosper/blob/main/prosper/src/hle/video/avplayer.cpp), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Media/AvPlayer.cs) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/AvPlayer/AvPlayerExports.cs), which agree on it. PPSA25063 fills the same fields, writes auto start 0 and starts playback with `sceAvPlayerStart`; a title that sets auto start has not been seen. A smaller `this_size` is read with the PS4 offsets. The thread parameters are ignored