From a24350c47ca12528e2a3f00f2a029e7a314ae524 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Fri, 11 Sep 2026 20:07:01 +1000 Subject: [PATCH] SPU: Decode ADPCM nibbles in pairs Consume both nibbles from each ADPCM data byte in sequence, halving loop-control and packed-byte load overhead. --- src/core-tests/spu_tests.cpp | 103 +++++++++++++++++++++++++++++++++++ src/core/spu.cpp | 41 +++++++++----- 2 files changed, 130 insertions(+), 14 deletions(-) diff --git a/src/core-tests/spu_tests.cpp b/src/core-tests/spu_tests.cpp index 61a6dc478..a7a7437ae 100644 --- a/src/core-tests/spu_tests.cpp +++ b/src/core-tests/spu_tests.cpp @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin // SPDX-License-Identifier: CC-BY-NC-ND-4.0 +#include "common/bitutils.h" #include "common/gsvector.h" #include "common/types.h" @@ -286,4 +287,106 @@ TEST(SPU, CombinedCaptureWritesMatchIndividualWrites) } } +struct ADPCMDecodeResult +{ + std::array samples; + std::array last_samples; + + bool operator==(const ADPCMDecodeResult&) const = default; +}; + +static s32 Clamp16ForTest(s32 value) +{ + return (value < -0x8000) ? -0x8000 : (value > 0x7FFF) ? 0x7FFF : value; +} + +static ADPCMDecodeResult DecodeADPCMOriginal(const std::array& data, u8 raw_shift, u8 filter, + std::array history) +{ + static constexpr std::array filter_table_pos = {{0, 60, 115, 98, 122, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}}; + static constexpr std::array filter_table_neg = {{0, 0, -52, -55, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}}; + + ADPCMDecodeResult result = {}; + const u8 shift = (raw_shift > 12) ? 9 : raw_shift; + for (u32 i = 0; i < result.samples.size(); i++) + { + const u8 nibble = (data[i / 2] >> ((i % 2) * 4)) & 0x0F; + s32 sample = static_cast(static_cast(nibble) << 12) >> shift; + sample += (history[0] * filter_table_pos[filter]) >> 6; + sample += (history[1] * filter_table_neg[filter]) >> 6; + history[1] = history[0]; + result.samples[i] = history[0] = static_cast(Clamp16ForTest(sample)); + } + result.last_samples = history; + return result; +} + +static ADPCMDecodeResult DecodeADPCMPaired(const std::array& edata, u8 raw_shift, u8 filter, + std::array history) +{ + static constexpr std::array filter_table_pos = {{0, 60, 115, 98, 122, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}}; + static constexpr std::array filter_table_neg = {{0, 0, -52, -55, -60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}}; + + ADPCMDecodeResult result = {}; + const u8 shift = (raw_shift > 12) ? 9 : raw_shift; + const s32 filter_pos = filter_table_pos[filter]; + const s32 filter_neg = filter_table_neg[filter]; + + // decode pairs of nibbles on each iteration instead of alternating + s32 last_sample_0 = history[0]; + s32 last_sample_1 = history[1]; + s16* output = result.samples.data(); + for (u32 i = 0; i < static_cast(edata.size()); i++) + { + const u8 data = edata.data[i]; + + // extend 4-bit to 16-bit, apply shift from header and mix in previous samples + // this is interleaved and whacky to try to maximize instruction-level parallelism, but basically, it's: + // s32(static_cast(ZeroExtend16(block.GetNibble(i)) << 12) >> shift) + + // (last_samples[0] * filter_pos) >> 6 + // (last_samples[1] * filter_neg) >> 6 + s32 s0 = static_cast(static_cast(ZeroExtend16(data & 0x0F) << 12) >> shift); + s32 s1 = static_cast(static_cast(ZeroExtend16(data >> 4) << 12) >> shift); + s0 += (last_sample_0 * filter_pos) >> 6; + s1 += (last_sample_0 * filter_neg) >> 6; + s0 += (last_sample_1 * filter_neg) >> 6; + s0 = Clamp16ForTest(s0); + s1 += (s0 * filter_pos) >> 6; + s1 = Clamp16ForTest(s1); + + *(output++) = Truncate16(last_sample_1 = s0); + *(output++) = Truncate16(last_sample_0 = s1); + } + + result.last_samples[0] = Truncate16(last_sample_0); + result.last_samples[1] = Truncate16(last_sample_1); + return result; +} + +TEST(SPU, PairedADPCMDecodeMatchesNibbleDecode) +{ + std::mt19937 generator(0x41445043u); + std::uniform_int_distribution byte_distribution(0, 0xFF); + std::uniform_int_distribution sample_distribution(-32768, 32767); + + for (u32 filter = 0; filter < 16; filter++) + { + for (u32 shift = 0; shift < 16; shift++) + { + for (u32 iteration = 0; iteration < 256; iteration++) + { + std::array data; + for (u8& value : data) + value = static_cast(byte_distribution(generator)); + const std::array history = { + {static_cast(sample_distribution(generator)), static_cast(sample_distribution(generator))}}; + + EXPECT_EQ(DecodeADPCMPaired(data, static_cast(shift), static_cast(filter), history), + DecodeADPCMOriginal(data, static_cast(shift), static_cast(filter), history)) + << "filter=" << filter << " shift=" << shift << " iteration=" << iteration; + } + } + } +} + } // namespace diff --git a/src/core/spu.cpp b/src/core/spu.cpp index 613d4b586..f35a84abc 100644 --- a/src/core/spu.cpp +++ b/src/core/spu.cpp @@ -208,15 +208,13 @@ struct ADPCMBlock u8 data[NUM_SAMPLES_PER_ADPCM_BLOCK / 2]; // For both 4bit and 8bit ADPCM, reserved shift values 13..15 will act same as shift=9). - u8 GetShift() const + ALWAYS_INLINE u8 GetShift() const { const u8 shift = shift_filter.shift; return (shift > 12) ? 9 : shift; } - u8 GetFilter() const { return shift_filter.filter; } - - u8 GetNibble(u32 index) const { return (data[index / 2] >> ((index % 2) * 4)) & 0x0F; } + ALWAYS_INLINE u8 GetFilter() const { return shift_filter.filter; } }; struct VolumeEnvelope @@ -1927,21 +1925,36 @@ void SPU::Voice::DecodeBlock(const ADPCMBlock& block) const u8 filter_index = block.GetFilter(); const s32 filter_pos = filter_table_pos[filter_index]; const s32 filter_neg = filter_table_neg[filter_index]; - s16 last_samples[2] = {adpcm_last_samples[0], adpcm_last_samples[1]}; + s32 last_sample_0 = adpcm_last_samples[0]; + s32 last_sample_1 = adpcm_last_samples[1]; + s16* output = ¤t_block_samples[NUM_SAMPLES_FROM_LAST_ADPCM_BLOCK]; - // samples - for (u32 i = 0; i < NUM_SAMPLES_PER_ADPCM_BLOCK; i++) + // decode pairs of nibbles on each iteration instead of alternating + for (u32 i = 0; i < static_cast(std::size(block.data)); i++) { - // extend 4-bit to 16-bit, apply shift from header and mix in previous samples - s32 sample = s32(static_cast(ZeroExtend16(block.GetNibble(i)) << 12) >> shift); - sample += (last_samples[0] * filter_pos) >> 6; - sample += (last_samples[1] * filter_neg) >> 6; + const u8 data = block.data[i]; - last_samples[1] = last_samples[0]; - current_block_samples[NUM_SAMPLES_FROM_LAST_ADPCM_BLOCK + i] = last_samples[0] = static_cast(Clamp16(sample)); + // extend 4-bit to 16-bit, apply shift from header and mix in previous samples + // this is interleaved and whacky to try to maximize instruction-level parallelism, but basically, it's: + // s32(static_cast(ZeroExtend16(block.GetNibble(i)) << 12) >> shift) + + // (last_samples[0] * filter_pos) >> 6 + // (last_samples[1] * filter_neg) >> 6 + s32 s0 = static_cast(static_cast(ZeroExtend16(data & 0x0F) << 12) >> shift); + s32 s1 = static_cast(static_cast(ZeroExtend16(data >> 4) << 12) >> shift); + s0 += (last_sample_0 * filter_pos) >> 6; + s1 += (last_sample_0 * filter_neg) >> 6; + s0 += (last_sample_1 * filter_neg) >> 6; + s0 = Clamp16(s0); + s1 += (s0 * filter_pos) >> 6; + s1 = Clamp16(s1); + + *(output++) = Truncate16(last_sample_1 = s0); + *(output++) = Truncate16(last_sample_0 = s1); } - std::copy(last_samples, last_samples + countof(last_samples), adpcm_last_samples.begin()); + adpcm_last_samples[0] = Truncate16(last_sample_0); + adpcm_last_samples[1] = Truncate16(last_sample_1); + current_block_flags.bits = block.flags.bits; }