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) <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-10-24 12:13:04 -07:00
parent e3ddd8c426
commit 4abfd37779
2 changed files with 25 additions and 5 deletions
+12 -1
View File
@@ -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;
+13 -4
View File
@@ -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;
}