From 73d4e3c75abd14feb481e3dc88cdd48d8a7e1b8b Mon Sep 17 00:00:00 2001 From: Stenzek Date: Fri, 9 Oct 2026 21:42:47 +1000 Subject: [PATCH] CDROM: Vectorize XA resampling --- src/core-tests/CMakeLists.txt | 1 + src/core-tests/cdrom_tests.cpp | 151 +++++++++++++++++++++ src/core-tests/core-tests.vcxproj | 1 + src/core-tests/core-tests.vcxproj.filters | 1 + src/core/CMakeLists.txt | 1 + src/core/cdrom.cpp | 83 ++---------- src/core/cdrom.inl | 158 ++++++++++++++++++++++ src/core/core.vcxproj | 1 + src/core/core.vcxproj.filters | 1 + src/core/gte.cpp | 4 +- 10 files changed, 324 insertions(+), 78 deletions(-) create mode 100644 src/core-tests/cdrom_tests.cpp create mode 100644 src/core/cdrom.inl diff --git a/src/core-tests/CMakeLists.txt b/src/core-tests/CMakeLists.txt index afc5d8102..e5e0a65e1 100644 --- a/src/core-tests/CMakeLists.txt +++ b/src/core-tests/CMakeLists.txt @@ -2,6 +2,7 @@ # SPDX-License-Identifier: CC-BY-NC-ND-4.0 + Packaging Restriction add_executable(core-tests + cdrom_tests.cpp cheats_tests.cpp cpu_disasm_tests.cpp gte_tests.cpp diff --git a/src/core-tests/cdrom_tests.cpp b/src/core-tests/cdrom_tests.cpp new file mode 100644 index 000000000..0731bb828 --- /dev/null +++ b/src/core-tests/cdrom_tests.cpp @@ -0,0 +1,151 @@ +// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin +// SPDX-License-Identifier: CC-BY-NC-ND-4.0 + +#include "core/cdrom.inl" + +#include "common/types.h" + +#include + +#include +#include +#include +#include + +namespace { + +using RingBuffer = std::array; + +// Original scalar implementations, which the vectorized versions in cdrom.inl must match exactly. +static s16 ReferenceZigZagInterpolate(const s16* ringbuf, u32 table_index, u32 p) +{ + static constexpr std::array, 7> tables = { + {{0, 0x0, 0x0, 0x0, 0x0, -0x0002, 0x000A, -0x0022, 0x0041, -0x0054, + 0x0034, 0x0009, -0x010A, 0x0400, -0x0A78, 0x234C, 0x6794, -0x1780, 0x0BCD, -0x0623, + 0x0350, -0x016D, 0x006B, 0x000A, -0x0010, 0x0011, -0x0008, 0x0003, -0x0001}, + {0, 0x0, 0x0, -0x0002, 0x0, 0x0003, -0x0013, 0x003C, -0x004B, 0x00A2, + -0x00E3, 0x0132, -0x0043, -0x0267, 0x0C9D, 0x74BB, -0x11B4, 0x09B8, -0x05BF, 0x0372, + -0x01A8, 0x00A6, -0x001B, 0x0005, 0x0006, -0x0008, 0x0003, -0x0001, 0x0}, + {0, 0x0, -0x0001, 0x0003, -0x0002, -0x0005, 0x001F, -0x004A, 0x00B3, -0x0192, + 0x02B1, -0x039E, 0x04F8, -0x05A6, 0x7939, -0x05A6, 0x04F8, -0x039E, 0x02B1, -0x0192, + 0x00B3, -0x004A, 0x001F, -0x0005, -0x0002, 0x0003, -0x0001, 0x0, 0x0}, + {0, -0x0001, 0x0003, -0x0008, 0x0006, 0x0005, -0x001B, 0x00A6, -0x01A8, 0x0372, + -0x05BF, 0x09B8, -0x11B4, 0x74BB, 0x0C9D, -0x0267, -0x0043, 0x0132, -0x00E3, 0x00A2, + -0x004B, 0x003C, -0x0013, 0x0003, 0x0, -0x0002, 0x0, 0x0, 0x0}, + {-0x0001, 0x0003, -0x0008, 0x0011, -0x0010, 0x000A, 0x006B, -0x016D, 0x0350, -0x0623, + 0x0BCD, -0x1780, 0x6794, 0x234C, -0x0A78, 0x0400, -0x010A, 0x0009, 0x0034, -0x0054, + 0x0041, -0x0022, 0x000A, -0x0001, 0x0, 0x0001, 0x0, 0x0, 0x0}, + {0x0002, -0x0008, 0x0010, -0x0023, 0x002B, 0x001A, -0x00EB, 0x027B, -0x0548, 0x0AFA, + -0x16FA, 0x53E0, 0x3C07, -0x1249, 0x080E, -0x0347, 0x015B, -0x0044, -0x0017, 0x0046, + -0x0023, 0x0011, -0x0005, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, + {-0x0005, 0x0011, -0x0023, 0x0046, -0x0017, -0x0044, 0x015B, -0x0347, 0x080E, -0x1249, + 0x3C07, 0x53E0, -0x16FA, 0x0AFA, -0x0548, 0x027B, -0x00EB, 0x001A, 0x002B, -0x0023, + 0x0010, -0x0008, 0x0002, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}}}; + + const s16* table = tables[table_index].data(); + s32 sum = 0; + for (u32 i = 0; i < 29; i++) + sum += (static_cast(ringbuf[(p - i) & 0x1F]) * static_cast(table[i])) >> 15; + + return static_cast(std::clamp(sum, -0x8000, 0x7FFF)); +} + +static s16 ReferenceInterpolate18900(const s16* ringbuf, u32 table_index, u32 p) +{ + static constexpr std::array, 7> tables = {{ + {{0x0, -0x5, 0x11, -0x23, 0x46, -0x17, -0x44, 0x15b, -0x347, 0x80e, -0x1249, 0x3c07, 0x53e0, + -0x16fa, 0xafa, -0x548, 0x27b, -0xeb, 0x1a, 0x2b, -0x23, 0x10, -0x8, 0x2, 0x0}}, + {{0x0, -0x2, 0xa, -0x22, 0x41, -0x54, 0x34, 0x9, -0x10a, 0x400, -0xa78, 0x234c, 0x6794, + -0x1780, 0xbcd, -0x623, 0x350, -0x16d, 0x6b, 0xa, -0x10, 0x11, -0x8, 0x3, -0x1}}, + {{-0x2, 0x0, 0x3, -0x13, 0x3c, -0x4b, 0xa2, -0xe3, 0x132, -0x43, -0x267, 0xc9d, 0x74bb, + -0x11b4, 0x9b8, -0x5bf, 0x372, -0x1a8, 0xa6, -0x1b, 0x5, 0x6, -0x8, 0x3, -0x1}}, + {{-0x1, 0x3, -0x2, -0x5, 0x1f, -0x4a, 0xb3, -0x192, 0x2b1, -0x39e, 0x4f8, -0x5a6, 0x7939, + -0x5a6, 0x4f8, -0x39e, 0x2b1, -0x192, 0xb3, -0x4a, 0x1f, -0x5, -0x2, 0x3, -0x1}}, + {{-0x1, 0x3, -0x8, 0x6, 0x5, -0x1b, 0xa6, -0x1a8, 0x372, -0x5bf, 0x9b8, -0x11b4, 0x74bb, + 0xc9d, -0x267, -0x43, 0x132, -0xe3, 0xa2, -0x4b, 0x3c, -0x13, 0x3, 0x0, -0x2}}, + {{-0x1, 0x3, -0x8, 0x11, -0x10, 0xa, 0x6b, -0x16d, 0x350, -0x623, 0xbcd, -0x1780, 0x6794, + 0x234c, -0xa78, 0x400, -0x10a, 0x9, 0x34, -0x54, 0x41, -0x22, 0xa, -0x2, 0x0}}, + {{0x0, 0x2, -0x8, 0x10, -0x23, 0x2b, 0x1a, -0xeb, 0x27b, -0x548, 0xafa, -0x16fa, 0x53e0, + 0x3c07, -0x1249, 0x80e, -0x347, 0x15b, -0x44, -0x17, 0x46, -0x23, 0x11, -0x5, 0x0}}, + }}; + + const s16* table = tables[table_index].data(); + s32 sum = 0; + for (u32 i = 0; i < 25; i++) + sum += (static_cast(ringbuf[(p + 32 - 25 + i) & 0x1F]) * static_cast(table[i])); + + return static_cast(std::clamp(sum >> 15, -0x8000, 0x7FFF)); +} + +// Compares both filters against their reference for every table and ring buffer position. +static void ExpectMatchesReference(const RingBuffer& ringbuf) +{ + for (u32 table_index = 0; table_index < CDROM::XA_RESAMPLE_NUM_TABLES; table_index++) + { + for (u32 p = 0; p < CDROM::XA_RESAMPLE_RING_BUFFER_SIZE; p++) + { + SCOPED_TRACE(testing::Message() << "table_index=" << table_index << " p=" << p); + EXPECT_EQ(CDROM::XAZigZagInterpolate(ringbuf.data(), table_index, p), + ReferenceZigZagInterpolate(ringbuf.data(), table_index, p)); + EXPECT_EQ(CDROM::XAInterpolate18900(ringbuf.data(), table_index, p), + ReferenceInterpolate18900(ringbuf.data(), table_index, p)); + } + } +} + +} // namespace + +TEST(CDROM, XAResampleMatchesReferenceRandom) +{ + std::mt19937 rng(0x58414450u); + std::uniform_int_distribution dist(std::numeric_limits::min(), std::numeric_limits::max()); + + for (u32 iteration = 0; iteration < 1000; iteration++) + { + RingBuffer ringbuf; + for (s16& sample : ringbuf) + sample = static_cast(dist(rng)); + + SCOPED_TRACE(testing::Message() << "iteration=" << iteration); + ExpectMatchesReference(ringbuf); + } +} + +// Full-scale input, to cover the output clamp and the largest possible products. +TEST(CDROM, XAResampleMatchesReferenceFullScale) +{ + static constexpr s16 min = std::numeric_limits::min(); + static constexpr s16 max = std::numeric_limits::max(); + + RingBuffer ringbuf; + ringbuf.fill(min); + ExpectMatchesReference(ringbuf); + ringbuf.fill(max); + ExpectMatchesReference(ringbuf); + + // Alternating signs at each period that fits in the ring buffer. + for (u32 period = 1; period <= CDROM::XA_RESAMPLE_RING_BUFFER_SIZE / 2; period *= 2) + { + for (u32 i = 0; i < CDROM::XA_RESAMPLE_RING_BUFFER_SIZE; i++) + ringbuf[i] = ((i / period) & 1) ? min : max; + + SCOPED_TRACE(testing::Message() << "period=" << period); + ExpectMatchesReference(ringbuf); + } +} + +// A single non-zero sample picks out one coefficient at a time, catching any misaligned or missing tap. +TEST(CDROM, XAResampleMatchesReferenceImpulse) +{ + for (u32 i = 0; i < CDROM::XA_RESAMPLE_RING_BUFFER_SIZE; i++) + { + for (const s16 value : {std::numeric_limits::min(), std::numeric_limits::max()}) + { + RingBuffer ringbuf = {}; + ringbuf[i] = value; + + SCOPED_TRACE(testing::Message() << "impulse=" << i << " value=" << value); + ExpectMatchesReference(ringbuf); + } + } +} diff --git a/src/core-tests/core-tests.vcxproj b/src/core-tests/core-tests.vcxproj index f22fc016e..686ad0fbf 100644 --- a/src/core-tests/core-tests.vcxproj +++ b/src/core-tests/core-tests.vcxproj @@ -3,6 +3,7 @@ + diff --git a/src/core-tests/core-tests.vcxproj.filters b/src/core-tests/core-tests.vcxproj.filters index c7f5a2c9f..4edee0e8b 100644 --- a/src/core-tests/core-tests.vcxproj.filters +++ b/src/core-tests/core-tests.vcxproj.filters @@ -2,6 +2,7 @@ + diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 063ea73dc..7319abe14 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -19,6 +19,7 @@ add_library(core bus.h cdrom.cpp cdrom.h + cdrom.inl cdrom_async_reader.cpp cdrom_async_reader.h cdrom_subq_replacement.cpp diff --git a/src/core/cdrom.cpp b/src/core/cdrom.cpp index 6f311c855..7c03bcccb 100644 --- a/src/core/cdrom.cpp +++ b/src/core/cdrom.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #include "cdrom.h" +#include "cdrom.inl" #include "cdrom_async_reader.h" #include "cdrom_subq_replacement.h" #include "dma.h" @@ -57,9 +58,6 @@ enum : u32 SUBQ_SECTOR_SKEW = 2, XA_ADPCM_SAMPLES_PER_SECTOR_4BIT = 4032, // 28 words * 8 nibbles per word * 18 chunks XA_ADPCM_SAMPLES_PER_SECTOR_8BIT = 2016, // 28 words * 4 bytes per word * 18 chunks - XA_RESAMPLE_RING_BUFFER_SIZE = 32, - XA_RESAMPLE_ZIGZAG_TABLE_SIZE = 29, - XA_RESAMPLE_NUM_ZIGZAG_TABLES = 7, PRNG_SEED = 0x4B435544u, PARAM_FIFO_SIZE = 16, @@ -3686,38 +3684,6 @@ void CDROM::DecodeXAADPCMChunks(const u8* chunk_ptr, s16* samples) template void CDROM::ResampleXAADPCM(const s16* frames_in, u32 num_frames_in) { - static constexpr auto zigzag_interpolate = [](const s16* ringbuf, u32 table_index, u32 p) -> s16 { - static std::array, 7> tables = { - {{0, 0x0, 0x0, 0x0, 0x0, -0x0002, 0x000A, -0x0022, 0x0041, -0x0054, - 0x0034, 0x0009, -0x010A, 0x0400, -0x0A78, 0x234C, 0x6794, -0x1780, 0x0BCD, -0x0623, - 0x0350, -0x016D, 0x006B, 0x000A, -0x0010, 0x0011, -0x0008, 0x0003, -0x0001}, - {0, 0x0, 0x0, -0x0002, 0x0, 0x0003, -0x0013, 0x003C, -0x004B, 0x00A2, - -0x00E3, 0x0132, -0x0043, -0x0267, 0x0C9D, 0x74BB, -0x11B4, 0x09B8, -0x05BF, 0x0372, - -0x01A8, 0x00A6, -0x001B, 0x0005, 0x0006, -0x0008, 0x0003, -0x0001, 0x0}, - {0, 0x0, -0x0001, 0x0003, -0x0002, -0x0005, 0x001F, -0x004A, 0x00B3, -0x0192, - 0x02B1, -0x039E, 0x04F8, -0x05A6, 0x7939, -0x05A6, 0x04F8, -0x039E, 0x02B1, -0x0192, - 0x00B3, -0x004A, 0x001F, -0x0005, -0x0002, 0x0003, -0x0001, 0x0, 0x0}, - {0, -0x0001, 0x0003, -0x0008, 0x0006, 0x0005, -0x001B, 0x00A6, -0x01A8, 0x0372, - -0x05BF, 0x09B8, -0x11B4, 0x74BB, 0x0C9D, -0x0267, -0x0043, 0x0132, -0x00E3, 0x00A2, - -0x004B, 0x003C, -0x0013, 0x0003, 0x0, -0x0002, 0x0, 0x0, 0x0}, - {-0x0001, 0x0003, -0x0008, 0x0011, -0x0010, 0x000A, 0x006B, -0x016D, 0x0350, -0x0623, - 0x0BCD, -0x1780, 0x6794, 0x234C, -0x0A78, 0x0400, -0x010A, 0x0009, 0x0034, -0x0054, - 0x0041, -0x0022, 0x000A, -0x0001, 0x0, 0x0001, 0x0, 0x0, 0x0}, - {0x0002, -0x0008, 0x0010, -0x0023, 0x002B, 0x001A, -0x00EB, 0x027B, -0x0548, 0x0AFA, - -0x16FA, 0x53E0, 0x3C07, -0x1249, 0x080E, -0x0347, 0x015B, -0x0044, -0x0017, 0x0046, - -0x0023, 0x0011, -0x0005, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, - {-0x0005, 0x0011, -0x0023, 0x0046, -0x0017, -0x0044, 0x015B, -0x0347, 0x080E, -0x1249, - 0x3C07, 0x53E0, -0x16FA, 0x0AFA, -0x0548, 0x027B, -0x00EB, 0x001A, 0x002B, -0x0023, - 0x0010, -0x0008, 0x0002, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}}}; - - const s16* table = tables[table_index].data(); - s32 sum = 0; - for (u32 i = 0; i < 29; i++) - sum += (static_cast(ringbuf[(p - i) & 0x1F]) * static_cast(table[i])) >> 15; - - return static_cast(std::clamp(sum, -0x8000, 0x7FFF)); - }; - s16* const left_ringbuf = s_state.xa_resample_ring_buffer[0].data(); [[maybe_unused]] s16* const right_ringbuf = s_state.xa_resample_ring_buffer[1].data(); u32 p = s_state.xa_resample_p; @@ -3725,21 +3691,19 @@ void CDROM::ResampleXAADPCM(const s16* frames_in, u32 num_frames_in) for (u32 in_sample_index = 0; in_sample_index < num_frames_in; in_sample_index++) { - // TODO: We can vectorize the multiplications in zigzag_interpolate by duplicating the sample in the ringbuffer at - // offset +32, allowing it to wrap once. left_ringbuf[p] = *(frames_in++); if constexpr (STEREO) right_ringbuf[p] = *(frames_in++); - p = (p + 1) % 32; + p = (p + 1) % XA_RESAMPLE_RING_BUFFER_SIZE; sixstep--; if (sixstep == 0) { sixstep = 6; - for (u32 j = 0; j < 7; j++) + for (u32 j = 0; j < XA_RESAMPLE_NUM_TABLES; j++) { - const s16 left_interp = zigzag_interpolate(left_ringbuf, j, p); - const s16 right_interp = STEREO ? zigzag_interpolate(right_ringbuf, j, p) : left_interp; + const s16 left_interp = XAZigZagInterpolate(left_ringbuf, j, p); + const s16 right_interp = STEREO ? XAZigZagInterpolate(right_ringbuf, j, p) : left_interp; AddCDAudioFrame(left_interp, right_interp); } } @@ -3752,37 +3716,6 @@ void CDROM::ResampleXAADPCM(const s16* frames_in, u32 num_frames_in) template void CDROM::ResampleXAADPCM18900(const s16* frames_in, u32 num_frames_in) { - // Weights originally from Mednafen's interpolator. It's unclear where these came from, perhaps it was calculated - // somehow. This doesn't appear to use a zigzag pattern like psx-spx suggests, therefore it is restricted to only - // 18900hz resampling. Duplicating the 18900hz samples to 37800hz sounds even more awful than lower sample rate - // audio should, with a big spike at ~16KHz, especially with music in FMVs. Fortunately, few games actually use - // 18900hz XA. - static constexpr auto interpolate = [](const s16* ringbuf, u32 table_index, u32 p) -> s16 { - static std::array, 7> tables = {{ - {{0x0, -0x5, 0x11, -0x23, 0x46, -0x17, -0x44, 0x15b, -0x347, 0x80e, -0x1249, 0x3c07, 0x53e0, - -0x16fa, 0xafa, -0x548, 0x27b, -0xeb, 0x1a, 0x2b, -0x23, 0x10, -0x8, 0x2, 0x0}}, - {{0x0, -0x2, 0xa, -0x22, 0x41, -0x54, 0x34, 0x9, -0x10a, 0x400, -0xa78, 0x234c, 0x6794, - -0x1780, 0xbcd, -0x623, 0x350, -0x16d, 0x6b, 0xa, -0x10, 0x11, -0x8, 0x3, -0x1}}, - {{-0x2, 0x0, 0x3, -0x13, 0x3c, -0x4b, 0xa2, -0xe3, 0x132, -0x43, -0x267, 0xc9d, 0x74bb, - -0x11b4, 0x9b8, -0x5bf, 0x372, -0x1a8, 0xa6, -0x1b, 0x5, 0x6, -0x8, 0x3, -0x1}}, - {{-0x1, 0x3, -0x2, -0x5, 0x1f, -0x4a, 0xb3, -0x192, 0x2b1, -0x39e, 0x4f8, -0x5a6, 0x7939, - -0x5a6, 0x4f8, -0x39e, 0x2b1, -0x192, 0xb3, -0x4a, 0x1f, -0x5, -0x2, 0x3, -0x1}}, - {{-0x1, 0x3, -0x8, 0x6, 0x5, -0x1b, 0xa6, -0x1a8, 0x372, -0x5bf, 0x9b8, -0x11b4, 0x74bb, - 0xc9d, -0x267, -0x43, 0x132, -0xe3, 0xa2, -0x4b, 0x3c, -0x13, 0x3, 0x0, -0x2}}, - {{-0x1, 0x3, -0x8, 0x11, -0x10, 0xa, 0x6b, -0x16d, 0x350, -0x623, 0xbcd, -0x1780, 0x6794, - 0x234c, -0xa78, 0x400, -0x10a, 0x9, 0x34, -0x54, 0x41, -0x22, 0xa, -0x2, 0x0}}, - {{0x0, 0x2, -0x8, 0x10, -0x23, 0x2b, 0x1a, -0xeb, 0x27b, -0x548, 0xafa, -0x16fa, 0x53e0, - 0x3c07, -0x1249, 0x80e, -0x347, 0x15b, -0x44, -0x17, 0x46, -0x23, 0x11, -0x5, 0x0}}, - }}; - - const s16* table = tables[table_index].data(); - s32 sum = 0; - for (u32 i = 0; i < 25; i++) - sum += (static_cast(ringbuf[(p + 32 - 25 + i) & 0x1F]) * static_cast(table[i])); - - return static_cast(std::clamp(sum >> 15, -0x8000, 0x7FFF)); - }; - s16* const left_ringbuf = s_state.xa_resample_ring_buffer[0].data(); [[maybe_unused]] s16* const right_ringbuf = s_state.xa_resample_ring_buffer[1].data(); u32 p = s_state.xa_resample_p; @@ -3793,7 +3726,7 @@ void CDROM::ResampleXAADPCM18900(const s16* frames_in, u32 num_frames_in) if (sixstep >= 7) { sixstep -= 7; - p = (p + 1) % 32; + p = (p + 1) % XA_RESAMPLE_RING_BUFFER_SIZE; left_ringbuf[p] = *(frames_in++); if constexpr (STEREO) @@ -3802,8 +3735,8 @@ void CDROM::ResampleXAADPCM18900(const s16* frames_in, u32 num_frames_in) in_sample_index++; } - const s16 left_interp = interpolate(left_ringbuf, sixstep, p); - const s16 right_interp = STEREO ? interpolate(right_ringbuf, sixstep, p) : left_interp; + const s16 left_interp = XAInterpolate18900(left_ringbuf, sixstep, p); + const s16 right_interp = STEREO ? XAInterpolate18900(right_ringbuf, sixstep, p) : left_interp; AddCDAudioFrame(left_interp, right_interp); sixstep += 3; } diff --git a/src/core/cdrom.inl b/src/core/cdrom.inl new file mode 100644 index 000000000..fa64c5258 --- /dev/null +++ b/src/core/cdrom.inl @@ -0,0 +1,158 @@ +// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin +// SPDX-License-Identifier: CC-BY-NC-ND-4.0 + +// XA-ADPCM resampling filters. Kept separate from cdrom.cpp so that the tests can include them. + +#include "common/gsvector.h" +#include "common/types.h" + +#include +#include + +namespace CDROM { + +inline constexpr u32 XA_RESAMPLE_RING_BUFFER_SIZE = 32; +inline constexpr u32 XA_RESAMPLE_RING_BUFFER_MASK = XA_RESAMPLE_RING_BUFFER_SIZE - 1; +inline constexpr u32 XA_RESAMPLE_NUM_TABLES = 7; + +#ifdef CPU_ARCH_SIMD + +// Both filters are a dot product of the most recent samples in the ring buffer with a table of coefficients. Reading +// the samples in filter order means wrapping around the ring buffer, which cannot be done with a vector load. Since +// the order of a sum does not matter, the coefficients are rotated to match the ring buffer instead: sample k is +// always multiplied by coefficient ((k + offset) % 32), where offset depends only on the write position. +// +// To make every rotation loadable, each table is padded with zeros to the size of the ring buffer (so that samples +// outside of the filter contribute nothing), and then stored twice back-to-back. The 32 coefficients starting at +// offset are then the table rotated by offset. +using XAResampleTable = std::array; + +template +inline consteval std::array +MakeXAResampleTables(const std::array, XA_RESAMPLE_NUM_TABLES>& taps, bool reverse) +{ + static_assert(NUM_TAPS <= XA_RESAMPLE_RING_BUFFER_SIZE); + + std::array ret = {}; + for (size_t table = 0; table < XA_RESAMPLE_NUM_TABLES; table++) + { + for (size_t i = 0; i < NUM_TAPS; i++) + { + const s16 tap = taps[table][reverse ? (NUM_TAPS - 1 - i) : i]; + ret[table][i] = tap; + ret[table][i + XA_RESAMPLE_RING_BUFFER_SIZE] = tap; + } + } + + return ret; +} + +#endif // CPU_ARCH_SIMD + +// 37800hz -> 44100hz, producing 7 output samples (one per table) for every 6 input samples. +// Computes sum((ringbuf[(p - i) % 32] * taps[table_index][i]) >> 15) for each of the 29 taps. +[[maybe_unused]] inline s16 XAZigZagInterpolate(const s16* ringbuf, u32 table_index, u32 p) +{ + static constexpr u32 NUM_TAPS = 29; + static constexpr std::array, XA_RESAMPLE_NUM_TABLES> taps = { + {{0, 0x0, 0x0, 0x0, 0x0, -0x0002, 0x000A, -0x0022, 0x0041, -0x0054, + 0x0034, 0x0009, -0x010A, 0x0400, -0x0A78, 0x234C, 0x6794, -0x1780, 0x0BCD, -0x0623, + 0x0350, -0x016D, 0x006B, 0x000A, -0x0010, 0x0011, -0x0008, 0x0003, -0x0001}, + {0, 0x0, 0x0, -0x0002, 0x0, 0x0003, -0x0013, 0x003C, -0x004B, 0x00A2, + -0x00E3, 0x0132, -0x0043, -0x0267, 0x0C9D, 0x74BB, -0x11B4, 0x09B8, -0x05BF, 0x0372, + -0x01A8, 0x00A6, -0x001B, 0x0005, 0x0006, -0x0008, 0x0003, -0x0001, 0x0}, + {0, 0x0, -0x0001, 0x0003, -0x0002, -0x0005, 0x001F, -0x004A, 0x00B3, -0x0192, + 0x02B1, -0x039E, 0x04F8, -0x05A6, 0x7939, -0x05A6, 0x04F8, -0x039E, 0x02B1, -0x0192, + 0x00B3, -0x004A, 0x001F, -0x0005, -0x0002, 0x0003, -0x0001, 0x0, 0x0}, + {0, -0x0001, 0x0003, -0x0008, 0x0006, 0x0005, -0x001B, 0x00A6, -0x01A8, 0x0372, + -0x05BF, 0x09B8, -0x11B4, 0x74BB, 0x0C9D, -0x0267, -0x0043, 0x0132, -0x00E3, 0x00A2, + -0x004B, 0x003C, -0x0013, 0x0003, 0x0, -0x0002, 0x0, 0x0, 0x0}, + {-0x0001, 0x0003, -0x0008, 0x0011, -0x0010, 0x000A, 0x006B, -0x016D, 0x0350, -0x0623, + 0x0BCD, -0x1780, 0x6794, 0x234C, -0x0A78, 0x0400, -0x010A, 0x0009, 0x0034, -0x0054, + 0x0041, -0x0022, 0x000A, -0x0001, 0x0, 0x0001, 0x0, 0x0, 0x0}, + {0x0002, -0x0008, 0x0010, -0x0023, 0x002B, 0x001A, -0x00EB, 0x027B, -0x0548, 0x0AFA, + -0x16FA, 0x53E0, 0x3C07, -0x1249, 0x080E, -0x0347, 0x015B, -0x0044, -0x0017, 0x0046, + -0x0023, 0x0011, -0x0005, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, + {-0x0005, 0x0011, -0x0023, 0x0046, -0x0017, -0x0044, 0x015B, -0x0347, 0x080E, -0x1249, + 0x3C07, 0x53E0, -0x16FA, 0x0AFA, -0x0548, 0x027B, -0x00EB, 0x001A, 0x002B, -0x0023, + 0x0010, -0x0008, 0x0002, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}}}; + +#ifdef CPU_ARCH_SIMD + // The taps walk backwards through the ring buffer, so the table is reversed to walk forwards instead. + // ringbuf[k] is paired with taps[(p - k) % 32], which is reversed[(k + NUM_TAPS - 1 - p) % 32]. + static constexpr auto tables = MakeXAResampleTables(taps, true); + const s16* const coeffs = &tables[table_index][(NUM_TAPS - 1 - p) & XA_RESAMPLE_RING_BUFFER_MASK]; + + // Each product is shifted down before it is summed, which rules out letting madd_s16() add adjacent products. + // Zeroing every second coefficient makes one half of each pair zero, leaving the other product intact. + static constexpr GSVector4i even_mask = GSVector4i::cxpr(0x0000FFFF); + + GSVector4i sum = GSVector4i::zero(); + for (u32 k = 0; k < XA_RESAMPLE_RING_BUFFER_SIZE; k += 8) + { + const GSVector4i samples = GSVector4i::load(&ringbuf[k]); + const GSVector4i coeffs_k = GSVector4i::load(&coeffs[k]); + sum = sum.add32(samples.madd_s16(coeffs_k & even_mask).sra32<15>()); + sum = sum.add32(samples.madd_s16(coeffs_k.andnot(even_mask)).sra32<15>()); + } + + return static_cast(std::clamp(sum.addv_s32(), -0x8000, 0x7FFF)); +#else + // Without vector instructions, GSVector would multiply the padding and masked-out coefficients one at a time. + const s16* const table = taps[table_index].data(); + s32 sum = 0; + for (u32 i = 0; i < NUM_TAPS; i++) + sum += (static_cast(ringbuf[(p - i) & XA_RESAMPLE_RING_BUFFER_MASK]) * static_cast(table[i])) >> 15; + + return static_cast(std::clamp(sum, -0x8000, 0x7FFF)); +#endif +} + +// 18900hz -> 44100hz, producing 7 output samples (one per table) for every 3 input samples. +// Computes sum(ringbuf[(p - 25 + i) % 32] * taps[table_index][i]) >> 15 for each of the 25 taps. +[[maybe_unused]] inline s16 XAInterpolate18900(const s16* ringbuf, u32 table_index, u32 p) +{ + // Weights originally from Mednafen's interpolator. It's unclear where these came from, perhaps it was calculated + // somehow. This doesn't appear to use a zigzag pattern like psx-spx suggests, therefore it is restricted to only + // 18900hz resampling. Duplicating the 18900hz samples to 37800hz sounds even more awful than lower sample rate + // audio should, with a big spike at ~16KHz, especially with music in FMVs. Fortunately, few games actually use + // 18900hz XA. + static constexpr u32 NUM_TAPS = 25; + static constexpr std::array, XA_RESAMPLE_NUM_TABLES> taps = {{ + {{0x0, -0x5, 0x11, -0x23, 0x46, -0x17, -0x44, 0x15b, -0x347, 0x80e, -0x1249, 0x3c07, 0x53e0, + -0x16fa, 0xafa, -0x548, 0x27b, -0xeb, 0x1a, 0x2b, -0x23, 0x10, -0x8, 0x2, 0x0}}, + {{0x0, -0x2, 0xa, -0x22, 0x41, -0x54, 0x34, 0x9, -0x10a, 0x400, -0xa78, 0x234c, 0x6794, + -0x1780, 0xbcd, -0x623, 0x350, -0x16d, 0x6b, 0xa, -0x10, 0x11, -0x8, 0x3, -0x1}}, + {{-0x2, 0x0, 0x3, -0x13, 0x3c, -0x4b, 0xa2, -0xe3, 0x132, -0x43, -0x267, 0xc9d, 0x74bb, + -0x11b4, 0x9b8, -0x5bf, 0x372, -0x1a8, 0xa6, -0x1b, 0x5, 0x6, -0x8, 0x3, -0x1}}, + {{-0x1, 0x3, -0x2, -0x5, 0x1f, -0x4a, 0xb3, -0x192, 0x2b1, -0x39e, 0x4f8, -0x5a6, 0x7939, + -0x5a6, 0x4f8, -0x39e, 0x2b1, -0x192, 0xb3, -0x4a, 0x1f, -0x5, -0x2, 0x3, -0x1}}, + {{-0x1, 0x3, -0x8, 0x6, 0x5, -0x1b, 0xa6, -0x1a8, 0x372, -0x5bf, 0x9b8, -0x11b4, 0x74bb, + 0xc9d, -0x267, -0x43, 0x132, -0xe3, 0xa2, -0x4b, 0x3c, -0x13, 0x3, 0x0, -0x2}}, + {{-0x1, 0x3, -0x8, 0x11, -0x10, 0xa, 0x6b, -0x16d, 0x350, -0x623, 0xbcd, -0x1780, 0x6794, + 0x234c, -0xa78, 0x400, -0x10a, 0x9, 0x34, -0x54, 0x41, -0x22, 0xa, -0x2, 0x0}}, + {{0x0, 0x2, -0x8, 0x10, -0x23, 0x2b, 0x1a, -0xeb, 0x27b, -0x548, 0xafa, -0x16fa, 0x53e0, + 0x3c07, -0x1249, 0x80e, -0x347, 0x15b, -0x44, -0x17, 0x46, -0x23, 0x11, -0x5, 0x0}}, + }}; + +#ifdef CPU_ARCH_SIMD + // ringbuf[k] is paired with taps[(k + NUM_TAPS - p) % 32]. + static constexpr auto tables = MakeXAResampleTables(taps, false); + const s16* const coeffs = &tables[table_index][(NUM_TAPS - p) & XA_RESAMPLE_RING_BUFFER_MASK]; + + GSVector4i sum = GSVector4i::zero(); + for (u32 k = 0; k < XA_RESAMPLE_RING_BUFFER_SIZE; k += 8) + sum = sum.add32(GSVector4i::load(&ringbuf[k]).madd_s16(GSVector4i::load(&coeffs[k]))); + + return static_cast(std::clamp(sum.addv_s32() >> 15, -0x8000, 0x7FFF)); +#else + const s16* const table = taps[table_index].data(); + s32 sum = 0; + for (u32 i = 0; i < NUM_TAPS; i++) + sum += static_cast(ringbuf[(p - NUM_TAPS + i) & XA_RESAMPLE_RING_BUFFER_MASK]) * static_cast(table[i]); + + return static_cast(std::clamp(sum >> 15, -0x8000, 0x7FFF)); +#endif +} + +} // namespace CDROM diff --git a/src/core/core.vcxproj b/src/core/core.vcxproj index 6feb13f29..d8b87c063 100644 --- a/src/core/core.vcxproj +++ b/src/core/core.vcxproj @@ -216,6 +216,7 @@ + diff --git a/src/core/core.vcxproj.filters b/src/core/core.vcxproj.filters index e63425b9f..db76728e1 100644 --- a/src/core/core.vcxproj.filters +++ b/src/core/core.vcxproj.filters @@ -171,6 +171,7 @@ + \ No newline at end of file diff --git a/src/core/gte.cpp b/src/core/gte.cpp index 50a5cf78b..a7b010d9c 100644 --- a/src/core/gte.cpp +++ b/src/core/gte.cpp @@ -1282,12 +1282,11 @@ ALWAYS_INLINE void GTE::Execute_GPF(Instruction inst) } namespace GTE { -namespace { /// Wraps an instruction handler, fixing the sf/lm bits at compile time. Once the handler is inlined, the shift amounts /// and saturation limits become constants, instead of being decoded and selected on each execution. template -void SpecializedHandler(Instruction inst) +static void SpecializedHandler(Instruction inst) { inst.sf = BoolToUInt8(sf); inst.lm = lm; @@ -1306,7 +1305,6 @@ ALWAYS_INLINE InstructionImpl GetSpecializedHandler(Instruction inst) return handlers[inst.sf][BoolToUInt8(inst.lm)]; } -} // namespace } // namespace GTE void GTE::ExecuteInstruction(u32 inst_bits)