From 4a8158b445a0cf9c8a554b4bd8b8a662475c8043 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Mon, 19 Oct 2026 02:56:36 +1000 Subject: [PATCH] SPU: Time the repeat address latch from key on Writing the repeat address stops loop start flags from overwriting it until the next key on. Hardware fetches the first block's header on each of the first three samples of the key on delay, so writes before the last fetch are overwritten and do not latch; later writes latch regardless of pitch or how long the first block plays. This replaces the first-block heuristic and removes is_first_block. --- src/core-tests/spu_tests.cpp | 33 +++++++++++++++++++++++++++++++++ src/core/save_state_version.h | 2 +- src/core/spu.cpp | 24 ++++++++++-------------- 3 files changed, 44 insertions(+), 15 deletions(-) diff --git a/src/core-tests/spu_tests.cpp b/src/core-tests/spu_tests.cpp index e2013d8e1..73561cd40 100644 --- a/src/core-tests/spu_tests.cpp +++ b/src/core-tests/spu_tests.cpp @@ -461,6 +461,26 @@ protected: static u16 Envelope() { return SPU::ReadRegister(0x1C); } + // Keys on voices 1 and 2 on the silent looping block, which has a loop start flag, then writes their repeat + // addresses on the last sample that hardware overwrites and the first sample that latches. + static void CheckRepeatAddressLatch(u32 samples_before_cutoff) + { + for (u32 voice = 1; voice <= 2; voice++) + { + SPU::WriteRegister(voice * 0x10 + 0x04, 0x1000); + SPU::WriteRegister(voice * 0x10 + 0x06, 0x200); + SPU::WriteRegister(voice * 0x10 + 0x0E, 0x200); + } + SPU::WriteRegister(0x188, 6); + RunSamples(samples_before_cutoff); + SPU::WriteRegister(0x1E, 0x300); + RunSamples(1); + SPU::WriteRegister(0x2E, 0x300); + RunSamples(8); + EXPECT_EQ(SPU::ReadRegister(0x1E), 0x200u); + EXPECT_EQ(SPU::ReadRegister(0x2E), 0x300u); + } + template static void CheckCapture(const std::array& adpcm, u16 pitch, const std::array& golden) { @@ -539,6 +559,19 @@ TEST_F(SPUHardwareTest, KeyOffDuringKeyOnDelay) EXPECT_EQ(Envelope(), 0u); } +TEST_F(SPUHardwareTest, RepeatAddressLatchCutoff) +{ + // Hardware fetches the first header on the three samples after KON is serviced, so a write four samples after KON + // is still overwritten, while a write five samples after latches. + CheckRepeatAddressLatch(4); +} + +TEST_F(SPUHardwareTest, RepeatAddressLatchCutoffOnOddSample) +{ + RunSamples(1); + CheckRepeatAddressLatch(3); +} + TEST_F(SPUHardwareTest, UnityPitchCaptureMatchesHardware) { static constexpr std::array adpcm = {0x10, 0x43, 0x76, 0x77, 0x77, 0x46, 0x13, diff --git a/src/core/save_state_version.h b/src/core/save_state_version.h index af4248521..c1af51493 100644 --- a/src/core/save_state_version.h +++ b/src/core/save_state_version.h @@ -6,7 +6,7 @@ #include "common/types.h" inline constexpr u32 SAVE_STATE_MAGIC = 0x43435544; -inline constexpr u32 SAVE_STATE_VERSION = 89; +inline constexpr u32 SAVE_STATE_VERSION = 90; inline constexpr u32 SAVE_STATE_MINIMUM_VERSION = 42; static_assert(SAVE_STATE_VERSION >= SAVE_STATE_MINIMUM_VERSION); diff --git a/src/core/spu.cpp b/src/core/spu.cpp index 7f4926b3d..11902246a 100644 --- a/src/core/spu.cpp +++ b/src/core/spu.cpp @@ -261,7 +261,6 @@ struct Voice ADSRPhase adsr_phase; bool has_samples; bool ignore_loop_address; - bool is_first_block; u8 key_on_delay; VoiceRegisters regs; @@ -602,7 +601,6 @@ void SPU::Reset() std::fill_n(v.regs.index, NUM_VOICE_REGISTERS, u16(0)); v.counter.bits = 0; v.current_block_flags.bits = 0; - v.is_first_block = 0; v.current_block_samples.fill(s16(0)); v.adpcm_last_samples.fill(s32(0)); v.adsr_envelope.Reset(0, 0, false, false, false); @@ -715,9 +713,10 @@ bool SPU::DoCompatibleState(StateWrapper& sw) sw.Do(&v.counter.bits); sw.Do(&v.current_block_flags.bits); if constexpr (COMPATIBILITY) - sw.DoEx(&v.is_first_block, 47, false); - else - sw.Do(&v.is_first_block); + { + if (sw.GetVersion() >= 47 && sw.GetVersion() < 90) + sw.SkipBytes(sizeof(bool)); // is_first_block, removed in version 90 + } sw.DoArray(&v.current_block_samples[NUM_SAMPLES_FROM_LAST_ADPCM_BLOCK], NUM_SAMPLES_PER_ADPCM_BLOCK); sw.DoArray(&v.current_block_samples[0], NUM_SAMPLES_FROM_LAST_ADPCM_BLOCK); sw.Do(&v.adpcm_last_samples); @@ -749,7 +748,7 @@ bool SPU::DoCompatibleState(StateWrapper& sw) bool SPU::DoState(StateWrapper& sw) { - if (sw.GetVersion() < 89) [[unlikely]] + if (sw.GetVersion() < 90) [[unlikely]] return DoCompatibleState(sw); else return DoCompatibleState(sw); @@ -1280,18 +1279,17 @@ void SPU::WriteVoiceRegister(u32 offset, u16 value) case 0x0E: // repeat address { - // There is a short window of time here between the voice being keyed on and the first block finishing decoding - // where setting the repeat address will *NOT* ignore the block/loop start flag. - // - // We always set this flag if the voice is off, because IRQs will keep the voice reading regardless, and we don't - // want the address that we just set to get wiped out by the IRQ looping. + // Writing the repeat address stops loop start flags from overwriting it until the next key on. The exception is + // the start of the key on delay, where the voice fetches the first block's header on each of three samples to + // prime interpolation. A write before the last fetch is overwritten by its loop start flag and does not latch. + // Voices that are off have no delay, so writes to them always latch. // // Games sensitive to this are: // - The Misadventures of Tron Bonne // - Re-Loaded - The Hardcore Sequel (repeated sound effects) // - Valkyrie Profile - const bool ignore_loop_address = !voice.IsOn() || !voice.is_first_block; + const bool ignore_loop_address = (voice.key_on_delay <= 1); DEBUG_LOG("SPU voice {} ADPCM repeat address <- 0x{:04X}", voice_index, value); voice.regs.adpcm_repeat_address = value; voice.ignore_loop_address |= ignore_loop_address; @@ -1725,7 +1723,6 @@ void SPU::Voice::KeyOn() static_cast(0)); has_samples = false; - is_first_block = true; ignore_loop_address = false; // The voice is silent for four samples before the first attack tick. key_on_delay = 4; @@ -2210,7 +2207,6 @@ ALWAYS_INLINE_RELEASE std::tuple SPU::SampleVoice(Voice& voice, u32 vo // next block voice.counter.sample_index -= NUM_SAMPLES_PER_ADPCM_BLOCK; voice.has_samples = false; - voice.is_first_block = false; voice.current_address += 2; // handle flags