[XMA] Reserve headroom per decoded frame and account for it in gating

The 3-bit field at +24bit in DWORD 1 of the XMA context was named
subframe_skip_count but never actually used for skipping subframes.
Testing across multiple titles (PGR4, Halo Reach) shows the field
might control extra output buffer blocks that must be reserved per decoded
frame. Renaming the field to output_buffer_padding to reflect this
observed behavior and adding padding to the minimum output space threshold

Pdding blocks are reserved from the output budget after each frame is
fully consumed, preventing the decoder from overrunning the space
that the game expects to remain free.

This is still likely not entirely correct but reduces some of the
observed noise in current implementation.
This commit is contained in:
Herman S.
2026-02-18 18:05:57 +09:00
parent 83d776fa39
commit d9747704be
2 changed files with 22 additions and 18 deletions

View File

@@ -68,7 +68,11 @@ struct XMA_CONTEXT_DATA {
uint32_t loop_subframe_skip : 3; // +17bit, XMASetLoopData might be
// subframe_decode_count
uint32_t subframe_decode_count : 4; // +20bit
uint32_t subframe_skip_count : 3; // +24bit
uint32_t output_buffer_padding : 3; // +24bit, extra output buffer blocks
// reserved per decoded frame
// NOTE(has207): this is pure guess
// but that's how we're using it
// currently
uint32_t sample_rate : 2; // +27bit enum of sample rates
uint32_t is_stereo : 1; // +29bit
uint32_t unk_dword_1_c : 1; // +30bit

View File

@@ -131,14 +131,6 @@ bool XmaContextNew::Work() {
if (data.IsConsumeOnlyContext()) {
Consume(&output_rb, &data);
// 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);
@@ -148,7 +140,8 @@ bool XmaContextNew::Work() {
}
const int32_t minimum_subframe_decode_count =
(kBytesPerFrameChannel / kOutputBytesPerBlock) << data.is_stereo;
((kBytesPerFrameChannel / kOutputBytesPerBlock) << data.is_stereo) +
data.output_buffer_padding;
// We don't have enough space to even make one pass
// Waiting for decoder to return more space.
@@ -165,12 +158,12 @@ bool XmaContextNew::Work() {
minimum_subframe_decode_count) {
XELOGAPU(
"XmaContext {}: Write Count: {}, Capacity: {} - {} {} Subframes: {} "
"Skip: {}",
"Padding: {}",
id(), (uint32_t)output_rb.write_count(),
remaining_subframe_blocks_in_output_buffer_,
data.input_buffer_0_valid + (data.input_buffer_1_valid << 1),
data.output_buffer_valid, data.subframe_decode_count,
data.subframe_skip_count);
data.output_buffer_padding);
Decode(&data);
Consume(&output_rb, &data);
@@ -273,12 +266,19 @@ void XmaContextNew::Consume(RingBuffer* XE_RESTRICT output_rb,
const int8_t raw_frame_read_offset =
((kBytesPerFrameChannel / kOutputBytesPerBlock) << data->is_stereo) -
current_frame_remaining_subframes_;
// + data->subframe_skip_count;
output_rb->Write(
raw_frame_.data() + (kOutputBytesPerBlock * raw_frame_read_offset),
subframes_to_write * kOutputBytesPerBlock);
remaining_subframe_blocks_in_output_buffer_ -= subframes_to_write;
// Reserve extra blocks as headroom when unk_skip_decode is set.
// Only apply when the frame is fully consumed to avoid double-counting.
const int8_t headroom =
(current_frame_remaining_subframes_ - subframes_to_write == 0)
? data->output_buffer_padding
: 0;
remaining_subframe_blocks_in_output_buffer_ -= subframes_to_write + headroom;
current_frame_remaining_subframes_ -= subframes_to_write;
XELOGAPU("XmaContext {}: Consume: {} - {} - {} - {} - {}", id(),