diff --git a/docs/internal/upstream/martin-pr16-spu-gaussian.md b/docs/internal/upstream/martin-pr16-spu-gaussian.md new file mode 100644 index 00000000..29b9fab1 --- /dev/null +++ b/docs/internal/upstream/martin-pr16-spu-gaussian.md @@ -0,0 +1,29 @@ +# Martin Penkava PR #16: SPU Gaussian interpolation + +Last evaluated: 2026-07-14 + +| Item | Value | +| --- | --- | +| Head repository/branch | `shaneomac1337/psxrecomp`, `smackdown2-fixes` (submitted as `mstan/psxrecomp` PR #16) | +| Source commit | `e069c8a531b14c2ec4566d6a3dd5b1816a5600e6` | +| Source author | Martin Penkava `` | +| Source co-author trailer | Claude Fable 5 `` | +| Local base | `7085721afe338a03cb114321a3576cdff420b732` (`origin/master`) | +| Local branch | `feat/pr16-spu-gaussian-interpolation-mpenkava` | + +## Included scope + +- The 512-entry PS1 SPU Gaussian coefficient table introduced by the source commit. +- Four-tap hardware Gaussian interpolation of decoded ADPCM voice samples. +- Preservation of the preceding ADPCM block's final three samples so interpolation remains continuous at block boundaries. +- A standalone regression test covering the boundary-history tap order and the equivalent in-block window. + +## Deliberate exclusions + +- The reverb engine and reverb register handling from the same source commit are isolated on a different branch. +- The source commit's high-quality shadow/reverb gating is not needed for the dry Gaussian path and is excluded. +- No game-specific addresses, generated code, configuration, or scripts are included. + +## Provenance note + +The source header identifies the coefficient values as the documented PS1 hardware table from No$PSX documentation and DuckStation's `core/spu.cpp`. The values are hardware data; this branch preserves Martin Penkava's implementation provenance and exact authorship trailers. No DuckStation implementation code was imported here. diff --git a/runtime/include/spu_gauss.h b/runtime/include/spu_gauss.h new file mode 100644 index 00000000..8b933c44 --- /dev/null +++ b/runtime/include/spu_gauss.h @@ -0,0 +1,104 @@ +/* spu_gauss.h - PS1 SPU hardware Gaussian interpolation table (512 entries). + * Values from No$PSX docs / DuckStation core/spu.cpp (SPU::Voice::Interpolate). + * Usage, with i = (phase >> 4) & 0xFF and s[0]=current, s[-1..-3]=history: + * out = (g[0x0FF-i]*s[-3] + g[0x1FF-i]*s[-2] + g[0x100+i]*s[-1] + g[i]*s[0]) >> 15 + */ +#ifndef PSX_SPU_GAUSS_H +#define PSX_SPU_GAUSS_H +#include +static const int16_t spu_gauss_table[512] = { + -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, + 0, 0, 0, 0, 0, 0, 0, 1, + 1, 1, 1, 2, 2, 2, 3, 3, + 3, 4, 4, 5, 5, 6, 7, 7, + 8, 9, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 21, 22, 24, + 25, 27, 28, 30, 32, 33, 35, 37, + 39, 41, 44, 46, 48, 51, 53, 56, + 58, 61, 64, 67, 70, 73, 77, 80, + 84, 87, 91, 95, 99, 103, 107, 111, + 116, 120, 125, 130, 135, 140, 145, 150, + 156, 161, 167, 173, 179, 186, 192, 199, + 205, 212, 219, 227, 234, 242, 250, 257, + 266, 274, 283, 291, 300, 309, 319, 328, + 338, 348, 358, 369, 379, 390, 401, 412, + 424, 436, 448, 460, 473, 485, 498, 512, + 525, 539, 553, 567, 582, 597, 612, 627, + 643, 659, 675, 692, 708, 726, 743, 761, + 779, 797, 816, 835, 854, 874, 894, 914, + 935, 956, 977, 999, 1020, 1043, 1066, 1089, + 1112, 1136, 1160, 1184, 1209, 1234, 1260, 1286, + 1312, 1339, 1366, 1394, 1422, 1450, 1479, 1508, + 1537, 1567, 1598, 1628, 1660, 1691, 1723, 1756, + 1789, 1822, 1856, 1890, 1924, 1959, 1995, 2031, + 2067, 2104, 2141, 2179, 2217, 2256, 2295, 2334, + 2374, 2415, 2456, 2497, 2539, 2582, 2624, 2668, + 2712, 2756, 2801, 2846, 2892, 2938, 2985, 3032, + 3079, 3128, 3176, 3225, 3275, 3325, 3376, 3427, + 3479, 3531, 3584, 3637, 3691, 3745, 3799, 3855, + 3910, 3967, 4023, 4081, 4138, 4197, 4255, 4315, + 4374, 4435, 4495, 4557, 4619, 4681, 4744, 4807, + 4871, 4935, 5000, 5065, 5131, 5197, 5264, 5332, + 5399, 5468, 5536, 5606, 5676, 5746, 5817, 5888, + 5959, 6032, 6104, 6177, 6251, 6325, 6400, 6475, + 6550, 6626, 6702, 6779, 6856, 6934, 7012, 7091, + 7170, 7249, 7329, 7409, 7490, 7571, 7653, 7735, + 7817, 7900, 7983, 8066, 8150, 8234, 8319, 8404, + 8489, 8575, 8661, 8748, 8834, 8922, 9009, 9097, + 9185, 9273, 9362, 9451, 9541, 9630, 9720, 9811, + 9901, 9992, 10083, 10174, 10266, 10358, 10450, 10542, + 10635, 10727, 10820, 10913, 11007, 11100, 11194, 11288, + 11382, 11476, 11571, 11665, 11760, 11855, 11950, 12045, + 12140, 12236, 12331, 12427, 12522, 12618, 12714, 12809, + 12905, 13001, 13097, 13193, 13289, 13385, 13481, 13577, + 13673, 13769, 13865, 13961, 14056, 14152, 14248, 14343, + 14439, 14534, 14630, 14725, 14820, 14915, 15010, 15104, + 15199, 15293, 15387, 15481, 15575, 15669, 15762, 15855, + 15948, 16041, 16133, 16226, 16317, 16409, 16500, 16592, + 16682, 16773, 16863, 16953, 17042, 17131, 17220, 17308, + 17396, 17484, 17571, 17658, 17744, 17830, 17916, 18001, + 18086, 18170, 18254, 18337, 18420, 18502, 18584, 18665, + 18746, 18826, 18905, 18985, 19063, 19141, 19219, 19295, + 19372, 19447, 19522, 19597, 19671, 19744, 19816, 19888, + 19959, 20030, 20100, 20169, 20238, 20306, 20373, 20439, + 20505, 20570, 20634, 20698, 20760, 20822, 20884, 20944, + 21004, 21063, 21121, 21178, 21235, 21290, 21345, 21399, + 21452, 21505, 21556, 21607, 21657, 21706, 21754, 21801, + 21848, 21893, 21938, 21982, 22025, 22066, 22107, 22148, + 22187, 22225, 22262, 22299, 22334, 22369, 22402, 22435, + 22467, 22498, 22527, 22556, 22584, 22611, 22637, 22662, + 22686, 22709, 22731, 22752, 22772, 22791, 22809, 22826, + 22842, 22857, 22872, 22885, 22897, 22908, 22918, 22927, + 22935, 22942, 22948, 22953, 22957, 22960, 22962, 22963, +}; + +/* Interpolate the current decoded sample using the SPU's four-tap Gaussian + * filter. previous[] is the final three samples from the preceding ADPCM + * block, oldest first; sample_index must address samples[]. */ +static inline int16_t spu_gaussian_interpolate(const int16_t previous[3], + const int16_t samples[28], + int sample_index, + uint32_t phase) +{ + const int gaussian_index = (int)((phase >> 4) & 0xFFu); + static const int tap_offset[4] = { 3, 2, 1, 0 }; + int32_t accumulator = 0; + + for (int tap = 0; tap < 4; ++tap) { + const int offset = sample_index - tap_offset[tap]; + const int16_t sample = offset >= 0 ? samples[offset] + : previous[3 + offset]; + int coefficient_index; + switch (tap) { + case 0: coefficient_index = 0x0FF - gaussian_index; break; + case 1: coefficient_index = 0x1FF - gaussian_index; break; + case 2: coefficient_index = 0x100 + gaussian_index; break; + default: coefficient_index = gaussian_index; break; + } + accumulator += (int32_t)spu_gauss_table[coefficient_index] * sample; + } + + return (int16_t)(accumulator >> 15); +} +#endif /* PSX_SPU_GAUSS_H */ diff --git a/runtime/src/spu.c b/runtime/src/spu.c index 6dd8af66..c722b996 100644 --- a/runtime/src/spu.c +++ b/runtime/src/spu.c @@ -8,6 +8,7 @@ */ #include "spu.h" +#include "spu_gauss.h" #include "spu_shadow.h" #include "audio_trace.h" #include "psx_cycles.h" @@ -72,6 +73,7 @@ typedef struct { uint32_t cur_addr; uint32_t repeat_addr; int16_t samples[SPU_BLOCK_SAMPLES]; + int16_t previous_samples[3]; int sample_idx; uint32_t phase; int16_t hist1; @@ -319,6 +321,13 @@ static void decode_block(SpuVoice *v) { if (filter > 4) filter = 0; if (shift > 12) shift = 12; + /* Gaussian interpolation reaches three samples into the preceding ADPCM + * block. KEYON clears samples[], so the first block naturally sees + * silence for its unavailable history. */ + v->previous_samples[0] = v->samples[SPU_BLOCK_SAMPLES - 3]; + v->previous_samples[1] = v->samples[SPU_BLOCK_SAMPLES - 2]; + v->previous_samples[2] = v->samples[SPU_BLOCK_SAMPLES - 1]; + int out = 0; for (int b = 0; b < 14; b++) { uint8_t packed = spu_ram[addr + 2u + (uint32_t)b]; @@ -445,7 +454,10 @@ static int16_t voice_next_sample(int idx) { decode_block(v); } - int16_t raw_s = v->samples[v->sample_idx]; + int16_t raw_s = spu_gaussian_interpolate(v->previous_samples, + v->samples, + v->sample_idx, + v->phase); /* Apply envelope (0..0x7FFF as a 15-bit gain). */ int32_t shaped = ((int32_t)raw_s * (int32_t)v->env_level) >> 15; if (shaped > 32767) shaped = 32767; @@ -453,9 +465,9 @@ static int16_t voice_next_sample(int idx) { /* Shadow tap: record the four decoded samples bracketing the current * sample position + the fractional phase + envelope, BEFORE the phase - * advances. The shadow re-interpolates these in float. Cross-block - * neighbours clamp to the block edge (the canon never interpolates across - * blocks either; documented in docs/SHADOW_ENHANCEMENTS.md). */ + * advances. The optional shadow keeps its own cubic, block-clamped window; + * the canonical hardware path above independently uses the preceding + * block's tail for Gaussian interpolation. */ if (s_shadow_tap_on && s_shadow_tap_frame < SPU_SHADOW_TAP_FRAMES) { SpuShadowVoiceTap *t = &s_shadow_tap[s_shadow_tap_frame].voice[idx]; diff --git a/runtime/tests/test_spu_gaussian.c b/runtime/tests/test_spu_gaussian.c new file mode 100644 index 00000000..6bccef4a --- /dev/null +++ b/runtime/tests/test_spu_gaussian.c @@ -0,0 +1,32 @@ +#include "spu_gauss.h" + +#include +#include +#include +#include + +int main(void) +{ + int16_t previous[3] = { 1000, 2000, 3000 }; + int16_t samples[28]; + memset(samples, 0, sizeof(samples)); + samples[0] = 4000; + + /* At the start of a block all three historical taps must come from the + * previous block. These coefficients are table entries 255, 511, 256, 0. */ + assert(spu_gaussian_interpolate(previous, samples, 0, 0) == 1994); + + /* The same four values inside a block must produce the same result. */ + samples[0] = 1000; + samples[1] = 2000; + samples[2] = 3000; + samples[3] = 4000; + assert(spu_gaussian_interpolate(previous, samples, 3, 0) == 1994); + + memset(previous, 0, sizeof(previous)); + memset(samples, 0, sizeof(samples)); + assert(spu_gaussian_interpolate(previous, samples, 0, 0x800u) == 0); + + puts("SPU Gaussian interpolation tests passed"); + return 0; +}