cellAudio: don't let a silent port reset the untouched baseline every period

A game can leave an audio port started and write nothing but zeros into it.
Those writes still land on the tag slots, overwriting the -0.0f tag with
+0.0f, and count_port_buffer_tags() detects that sign flip as "the buffer was
touched" -- correctly, since it cannot tell silence from data.

The result is a port that reports untouched on most periods and touched on the
few that a write happens to land in. Storing untouched_expected as the
instantaneous count then drops it to 0 on exactly those periods, so on the
next period the same silent port looks like a newly untouched buffer, and the
loop waits out the whole untouched timeout for it. Every time it flickers.

untouched_expected is now a high-water mark, clamped to active_ports so a port
going away lowers it again.

Measured on device, Tom Clancy's H.A.W.X. 2 (BLES00928), main menu, stock
audio settings (time stretching off, buffer 34), with a temporary probe in the
period loop counting branch hits per second. Same scene, same build, only this
change differing:

                       before      after
  wait_untouched         669          0     hits/s (1000us each)
  MIX                     65        188     hits/s
  advance (forced)        37          0     hits/s
  enqueued_buffers         0        5-7
  untouched > expected   743          0     per second
  untouched_expected     0 in 799   1 in 376  of the second's samples

The port itself is unchanged by this: it is still started, still counted as
active, still mixed. A full-block scan of it reads 0 non-zero floats out of
512 on every one of 875 consecutive periods, which is what makes it silent,
and it is the tag flicker rather than the silence that caused the stall.

Audible effect: the audio clock ran at ~55% of real time (103 vs 189 periods
per second) with the ring buffer permanently empty, which is why the whole
title sounded slowed down and stuttering. Note this happens with time
stretching disabled -- the frequency ratio stayed at 1.000 throughout, so the
slowdown is the period rate itself and not resampling.

Not verified: whether any title depends on untouched_expected falling back to
a lower value within a stable port configuration. Nothing in the tree tests
this loop.
This commit is contained in:
Zulux91
2026-08-17 04:26:09 -05:00
parent 62d8208c71
commit 89c6d08ed3
+15 -4
View File
@@ -915,7 +915,7 @@ void cell_audio_thread::operator()()
{
// There's no audio in the buffers, simply advance time and hope the game recovers
cellAudio.trace("advancing time: untouched=%u/%u (expected=%u), enqueued_buffers=%llu", untouched, active_ports, untouched_expected, enqueued_buffers);
untouched_expected = untouched;
untouched_expected = std::min(std::max(untouched, untouched_expected), active_ports);
advance(timestamp);
continue;
}
@@ -931,7 +931,7 @@ void cell_audio_thread::operator()()
// There's no audio in the buffers, simply advance time
cellAudio.trace("enqueuing silence: untouched=%u/%u (expected=%u), enqueued_buffers=%llu", untouched, active_ports, untouched_expected, enqueued_buffers);
ringbuffer->enqueue_silence();
untouched_expected = untouched;
untouched_expected = std::min(std::max(untouched, untouched_expected), active_ports);
advance(timestamp);
continue;
}
@@ -946,8 +946,19 @@ void cell_audio_thread::operator()()
//cellAudio.error("active=%u, untouched=%u, in_progress=%d, incomplete=%d, enqueued_buffers=%u", active_ports, untouched, in_progress, incomplete, enqueued_buffers);
// Store number of untouched buffers for future reference
untouched_expected = untouched;
// Store number of untouched buffers for future reference.
//
// High-water mark rather than the instantaneous count, clamped to the number of
// active ports so that a port going away lowers it again.
//
// A game can leave a port started and write nothing but zeros into it. Those
// writes still overwrite the -0.0f tags with +0.0f, which flips the sign bit and
// makes count_port_buffer_tags() report the buffer as touched on the periods the
// write happens to land in. Storing the instantaneous count then drops this to 0
// on exactly those periods, and on the next period the very same silent port
// looks like a newly untouched buffer -- so the loop waits out the whole
// untouched timeout for it, over and over, for as long as the port exists.
untouched_expected = std::min(std::max(untouched, untouched_expected), active_ports);
// Log if we enqueued untouched/incomplete buffers
if (untouched > 0 || incomplete > 0)