From 4abfd3777978b2ef827a07d15ea14da115c68b07 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sat, 24 Oct 2026 12:13:04 -0700 Subject: [PATCH] Gate SPU HQ CD bypass on actual CD sample activity A game can leave the SPU CD-enable bit set through voice-only play, which kept the HQ voice mix bypassed. Reported by michaelm-afk. Co-Authored-By: Claude Opus 5.5 (1M context) --- runtime/src/spu_shadow.c | 13 ++++++++++++- runtime/tests/test_spu_shadow_normalization.c | 17 +++++++++++++---- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/runtime/src/spu_shadow.c b/runtime/src/spu_shadow.c index f0e7fc1b..c7320d55 100644 --- a/runtime/src/spu_shadow.c +++ b/runtime/src/spu_shadow.c @@ -28,6 +28,7 @@ static int s_enabled = -1; /* -1 = not yet resolved */ static int s_cfg = 0; /* config/launcher gate (env overrides) */ static ShadowVerifier s_verifier; static int s_cd_bypass; +static uint64_t s_cd_consumed; static SpuShadowInfo s_info; void spu_shadow_set_enabled(int on) { @@ -52,6 +53,7 @@ bool spu_shadow_enabled(void) { void spu_shadow_reset(void) { s_cd_bypass = 0; + s_cd_consumed = 0; shadow_verifier_init(&s_verifier); memset(&s_info, 0, sizeof(s_info)); s_info.enabled = spu_shadow_enabled() ? 1 : 0; @@ -77,7 +79,16 @@ void spu_shadow_process(int16_t* canon, int frames) { * canonical mix immediately, and require fresh proof when CD is disabled. */ SpuGlobalState global; spu_get_global_state(&global); - if (global.ctrl & 1u) { + /* A game may leave CD mixing enabled throughout voice-only gameplay. + * Gate on actual queued/consumed CD samples, including the LAST block + * draining the FIFO. push_frames - queued includes dropped frames too; + * treating an overflow/reset as activity is a conservative extra bypass. */ + SpuDebugInfo cd; + spu_debug_info(&cd); + const uint64_t consumed = cd.cd_push_frames - cd.cd_frames; + const int cd_active = cd.cd_frames != 0 || consumed != s_cd_consumed; + s_cd_consumed = consumed; + if ((global.ctrl & 1u) && cd_active) { if (!s_cd_bypass) { shadow_verifier_init(&s_verifier); s_info.proven = 0; diff --git a/runtime/tests/test_spu_shadow_normalization.c b/runtime/tests/test_spu_shadow_normalization.c index d251820b..75838d11 100644 --- a/runtime/tests/test_spu_shadow_normalization.c +++ b/runtime/tests/test_spu_shadow_normalization.c @@ -10,6 +10,9 @@ static SpuShadowFrameTapPub taps[2048]; static uint16_t ctrl; +static uint64_t pushed; +static uint32_t queued; +void spu_debug_info(SpuDebugInfo* out) { memset(out,0,sizeof(*out));out->cd_push_frames=pushed;out->cd_frames=queued; } void spu_get_global_state(SpuGlobalState* out) { memset(out,0,sizeof(*out));out->ctrl=ctrl; } const void *spu_shadow_tap_buffer(void) { return taps; } int spu_shadow_tap_count(void) { return 2048; } @@ -49,6 +52,8 @@ int main(void) { assert(info.enabled && info.proven); assert(info.last_ratio > .99f && info.last_ratio < 1.01f); assert(info.last_r > .99f); + /* The CD-enable bit alone must not disable an already proven voice mix. */ + ctrl=1;feed(48,0,1);spu_shadow_get_info(&info);assert(info.proven); /* A proven voice-only shadow must not replace newly starting XA/CD. * Synthetic CD samples, no original audio/game state. */ ctrl=1; @@ -57,19 +62,23 @@ int main(void) { memset(taps,0,sizeof(taps)); for(unsigned i=0;i<4096;++i)canon[i]=(int16_t)(4000+(i%511)); memcpy(reference,canon,sizeof(canon)); + pushed+=2048;queued=0; /* Last FIFO block has drained: still CD activity. */ spu_shadow_process(canon,2048); assert(memcmp(canon,reference,sizeof(canon))==0); spu_shadow_get_info(&info);assert(!info.proven); } - ctrl=0; - feed(48,0,2); /* CD exit cannot reuse the pre-CD proof. */ - for(unsigned block=49;block<97;++block)feed(block,0,1); + queued=2048; + feed(48,0,2);spu_shadow_get_info(&info);assert(!info.proven); + queued=0;pushed+=2048;feed(49,0,2); /* Last queued block drains. */ + ctrl=1; + feed(50,0,2); /* CD exit cannot reuse the pre-CD proof. */ + for(unsigned block=51;block<99;++block)feed(block,0,1); spu_shadow_get_info(&info);assert(info.proven); for (unsigned block = 97; block < 289; ++block) feed(block, 1, 0); spu_shadow_get_info(&info); assert(!info.proven && info.pauses > 0); /* Real guard still rejects mismatch. */ spu_shadow_set_enabled(0); spu_shadow_reset(); feed(240, 1, 2); - puts("SPU shadow: normalized proof, output level, immediate CD passthrough, reproof, mismatch fallback and off passed"); + puts("SPU shadow: normalized proof, output level, idle CD enable, first/queued/last CD passthrough, reproof, mismatch fallback and off passed"); return 0; }