[XMA] Fix multiple audio issues in new decoder

- Handle XMA2 frames with headers split across packet boundaries
  (fixes audio cutoff in Capcom games)
- Output buffer minimum space check using actual frame size instead
  of SDC-based formula, allowing tight-buffer contexts to decode
  (fixes PGR4 engine sounds)
- Gate consume-only context clearing with tight-buffer heuristic to
  prevent disrupting playback while still allowing stream completion
  signaling (fixes Borderlands 2 dialog)
- Clamp read offset 0 to packet header size to prevent decode rejection
  loop (fixes Dirt 2 crashes)
This commit is contained in:
Herman S.
2026-02-16 23:27:26 +09:00
parent 7213f7e69b
commit 6e5b2a95b0
2 changed files with 65 additions and 4 deletions

View File

@@ -154,6 +154,11 @@ struct XMA_CONTEXT_DATA {
const bool IsConsumeOnlyContext() const {
return (input_buffer_0_packet_count | input_buffer_1_packet_count) == 0;
}
// Whether the SDC-based minimum exceeds the output buffer size.
const bool HasTightOutputBuffer() const {
return (int32_t)((subframe_decode_count * 2) - 1) >
(int32_t)output_buffer_block_count;
}
};
static_assert_size(XMA_CONTEXT_DATA, 64);

View File

@@ -130,7 +130,16 @@ bool XmaContextNew::Work() {
if (data.IsConsumeOnlyContext()) {
Consume(&output_rb, &data);
if (data.output_buffer_read_offset == data.output_buffer_write_offset) {
// Clearing contexts that match TightBufferOutput heuristic can disrupt
// playback (e.g. audio noise during races in PGR4), so we only clear
// contexts with enough output buffer headroom where empty input reliably
// indicates the stream is finished (e.g. needed for dialog completion in
// Borderlands 2 startup).
// NOTE(has207): this feels wrong to have such a heuristic but it seems to
// work for the known cases. May need to be revisited if failing cases are
// found.
if (!data.HasTightOutputBuffer() &&
data.output_buffer_read_offset == data.output_buffer_write_offset) {
ClearLocked(&data);
}
data.Store(context_ptr);
@@ -138,7 +147,7 @@ bool XmaContextNew::Work() {
}
const int32_t minimum_subframe_decode_count =
(data.subframe_decode_count * 2) - 1;
(kBytesPerFrameChannel / kOutputBytesPerBlock) << data.is_stereo;
// We don't have enough space to even make one pass
// Waiting for decoder to return more space.
@@ -337,6 +346,13 @@ void XmaContextNew::Decode(XMA_CONTEXT_DATA* data) {
static_cast<void*>(current_input_buffer), data->output_buffer_ptr,
data->output_buffer_block_count);
// Games like Dirt 2 can kick the decoder with read offset 0 (pointing into
// the packet header) before filling in a valid offset. Clamp to the first
// valid data position to avoid rejecting the packet entirely.
if (data->input_buffer_read_offset < kBitsPerPacketHeader) {
data->input_buffer_read_offset = kBitsPerPacketHeader;
}
const uint32_t current_input_size = GetCurrentInputBufferSize(data);
const uint32_t current_input_packet_count =
current_input_size / kBytesPerPacket;
@@ -360,10 +376,42 @@ void XmaContextNew::Decode(XMA_CONTEXT_DATA* data) {
const uint32_t relative_offset =
data->input_buffer_read_offset % kBitsPerPacket;
const kPacketInfo packet_info = GetPacketInfo(packet, relative_offset);
kPacketInfo packet_info = GetPacketInfo(packet, relative_offset);
const uint32_t packet_to_skip = xma::GetPacketSkipCount(packet) + 1;
const uint32_t next_packet_index = packet_index + packet_to_skip;
// Frame header split across packet boundary — combine packets to read
// the full 15-bit header and resolve the real frame size.
// Only detected for XMA2 packets where the header provides an authoritative
// frame count. XMA1 packets lack a frame count field so split headers
// cannot be detected — if XMA1 encoders can produce them, those frames
// will still be silently lost.
if (packet_info.current_frame_size_ == 0) {
const uint8_t* next_packet =
GetNextPacket(data, next_packet_index, current_input_packet_count);
if (!next_packet) {
// Matching split-body error handling below; correct error code unknown.
data->error_status = 4;
return;
}
std::memcpy(input_buffer_.data(), packet + kBytesPerPacketHeader,
kBytesPerPacketData);
std::memcpy(input_buffer_.data() + kBytesPerPacketData,
next_packet + kBytesPerPacketHeader, kBytesPerPacketData);
BitStream combined(input_buffer_.data(),
(kBitsPerPacket - kBitsPerPacketHeader) * 2);
combined.SetOffset(relative_offset - kBitsPerPacketHeader);
uint64_t frame_size = combined.Peek(kBitsPerFrameHeader);
if (frame_size == xma::kMaxFrameLength) {
// Matching split-body error handling below; correct error code unknown.
data->error_status = 4;
return;
}
packet_info.current_frame_size_ = (uint32_t)frame_size;
}
BitStream stream =
BitStream(current_input_buffer, (packet_index + 1) * kBitsPerPacket);
stream.SetOffset(data->input_buffer_read_offset);
@@ -623,7 +671,15 @@ const kPacketInfo XmaContextNew::GetPacketInfo(uint8_t* packet,
if (xma::IsPacketXma2Type(packet)) {
const uint8_t xma2_frame_count = xma::GetPacketFrameCount(packet);
if (xma2_frame_count != packet_info.frame_count_) {
if (xma2_frame_count > packet_info.frame_count_) {
// Frame header split across packet boundary — scanner couldn't
// peek the full 15-bit header. Trust the XMA2 header count.
if (packet_info.current_frame_size_ == 0) {
// Current frame is the split-header frame
packet_info.current_frame_ = packet_info.frame_count_;
}
packet_info.frame_count_ = xma2_frame_count;
} else if (xma2_frame_count != packet_info.frame_count_) {
XELOGE(
"XmaContext {}: XMA2 packet header defines different amount of "
"frames than internally found! (Header: {} Found: {})",