From 5ca3b24c621aeba4c55d72aced0bfa252f779743 Mon Sep 17 00:00:00 2001 From: Dipshet <264011288+Dipshet@users.noreply.github.com> Date: Wed, 5 Aug 2026 20:29:34 +0200 Subject: [PATCH] apu: guard the XMA loop-end frame test against degenerate loop metadata The loop-end test runs before UpdateLoopStatus, so the degenerate-loop guard there never reaches it. With loop_count > 0 and loop_start >= loop_end, the clamped loop end equals the start offset of the stream at packet 0, arming the frame output limit and silently truncating 384 samples from its first frame. Cvar audio_xma_loop_end_guard, default on. --- .../src/native/audio/audio_system.cpp | 13 +++++++++++++ .../src/native/audio/xma/context.cpp | 17 +++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/thirdparty/rexglue-sdk/src/native/audio/audio_system.cpp b/thirdparty/rexglue-sdk/src/native/audio/audio_system.cpp index 1b230216..d2b7435a 100644 --- a/thirdparty/rexglue-sdk/src/native/audio/audio_system.cpp +++ b/thirdparty/rexglue-sdk/src/native/audio/audio_system.cpp @@ -84,6 +84,19 @@ REXCVAR_DEFINE_BOOL(audio_xma_header_straddle_fix, true, "Audio", "combs on the stereo fold - the long-reported doubled/" "robotic cutscene dialogue. Off restores the legacy " "walk for A/B."); +REXCVAR_DEFINE_BOOL(audio_xma_loop_end_guard, true, "Audio", + "Apply the degenerate-loop guard (see " + "audio_xma_loop_guard) to the loop-end frame test as " + "well. That test is evaluated before the loop-status " + "update, so the guard there never reaches it: with " + "loop_count > 0 and loop_start >= loop_end (metadata " + "AC6's streamed voices carry), the clamped loop_end " + "equals a voice's starting read offset for the stream " + "that begins at packet 0, arming the frame output limit " + "and silently discarding 384 samples of that stream's " + "first frame - permanently offsetting one context of a " + "multi-stream voice from its siblings. Off restores the " + "legacy behavior for A/B."); REXCVAR_DEFINE_BOOL(audio_xma_preserve_timeline, true, "Audio", "When the XMA decoder backend rejects a damaged frame, emit that " "frame's worth of silence instead of dropping it. The input read " diff --git a/thirdparty/rexglue-sdk/src/native/audio/xma/context.cpp b/thirdparty/rexglue-sdk/src/native/audio/xma/context.cpp index c9fc0a38..8f5f8380 100644 --- a/thirdparty/rexglue-sdk/src/native/audio/xma/context.cpp +++ b/thirdparty/rexglue-sdk/src/native/audio/xma/context.cpp @@ -20,6 +20,7 @@ REXCVAR_DECLARE(bool, audio_deep_trace); REXCVAR_DECLARE(bool, audio_xma_loop_guard); REXCVAR_DECLARE(bool, audio_xma_preserve_timeline); REXCVAR_DECLARE(bool, audio_xma_header_straddle_fix); +REXCVAR_DECLARE(bool, audio_xma_loop_end_guard); namespace rex::audio { @@ -726,6 +727,22 @@ void XmaContext::Decode(XMA_CONTEXT_DATA* data) { if (data->loop_count > 0) { const uint32_t loop_end = std::max(kBitsPerPacketHeader, data->loop_end); is_loop_end_frame = (data->input_buffer_read_offset == loop_end); + if (is_loop_end_frame && REXCVAR_GET(audio_xma_loop_end_guard) && + data->loop_start >= data->loop_end) { + // This test is evaluated before UpdateLoopStatus, so the degenerate-loop + // guard there (audio_xma_loop_guard) never reaches it. With + // loop_count > 0 and loop_start >= loop_end the clamp above turns + // loop_end into kBitsPerPacketHeader - which is exactly a voice's + // starting read offset for the stream that begins at packet 0, and + // exactly what SwapInputBuffer resets the read offset to - so the + // frame output limit armed on the first frame of that stream (and on + // any post-swap pass that lands at the raw offset), and Consume() + // silently discarded the tail of the frame: 384 samples lost on ONE + // context of a multi-stream voice, which permanently offsets it from + // its siblings. Apply the same real-window requirement here that + // UpdateLoopStatus applies. + is_loop_end_frame = false; + } } UpdateLoopStatus(data);