diff --git a/src/xenia/apu/xma_context.h b/src/xenia/apu/xma_context.h index d138539ba..d74ae08ac 100644 --- a/src/xenia/apu/xma_context.h +++ b/src/xenia/apu/xma_context.h @@ -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); diff --git a/src/xenia/apu/xma_context_new.cc b/src/xenia/apu/xma_context_new.cc index b92a36c9c..5e4b5dd0c 100644 --- a/src/xenia/apu/xma_context_new.cc +++ b/src/xenia/apu/xma_context_new.cc @@ -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(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: {})",