[AudioOut2] Block a blocking push only until a queue slot frees

A blocking sceAudioOut2ContextPush waited only when the host stream hit its
128 KiB cap, so a title rendering faster than real time kept about 680 ms
queued and every sound played that far behind (GTA V: queue=2 context,
queued_ms avg=678). The push now waits until the device holds the cushion
plus QueueDepth-1 grains, the level GetQueueLevel already reports as full;
the host cap stays as the limit for non-blocking bursts. GTA V now holds
about 41 ms with no underruns.
This commit is contained in:
ParantezTech 2026-10-04 07:59:11 +03:00
parent 767bca9e26
commit a7cc266702
2 changed files with 61 additions and 0 deletions

View File

@ -139,6 +139,9 @@ public static class AudioOut2Exports
return ahead <= 0 ? 0 : (uint)Math.Min((ahead + GrainTicks - 1) / GrainTicks, QueueDepth);
}
public double DeviceSlotFreeMilliseconds =>
DeviceSlotFreeThreshold(GrainSamples, Frequency, QueueDepth);
// Grains the device holds beyond the cushion, at most QueueDepth.
public uint DeviceQueuedGrains(int queuedMilliseconds)
{
@ -430,6 +433,14 @@ public static class AudioOut2Exports
{
context.DevicePaced = true;
}
if (submitted || advancedGrain == 1)
{
if (blocking != 0)
{
WaitForDeviceSlot(context);
}
}
else
{
// Push is the blocking point of a grain: it waits for the grain an Advance claimed, or
@ -1096,6 +1107,28 @@ public static class AudioOut2Exports
}
}
internal static double DeviceSlotFreeThreshold(uint grainSamples, uint frequency, uint queueDepth) =>
DeviceCushionMilliseconds + (grainSamples * 1000.0 / frequency * Math.Max((long)queueDepth - 1, 0));
private static void WaitForDeviceSlot(ContextState context)
{
var backend = ResolveContextBackend(context, out _);
if (backend is null)
{
return;
}
var limit = context.DeviceSlotFreeMilliseconds;
var deadline = Stopwatch.GetTimestamp() + (Stopwatch.Frequency / 4);
int queued;
while ((queued = backend.QueuedMilliseconds) > limit &&
Stopwatch.GetTimestamp() < deadline &&
!HostSessionControl.IsShutdownRequested)
{
Thread.Sleep((int)Math.Clamp(Math.Ceiling(queued - limit), 1, 50));
}
}
private static uint QueuedGrains(ContextState context)
{
if (context.DevicePaced)

View File

@ -0,0 +1,28 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Libs.Audio;
using Xunit;
namespace SharpEmu.Libs.Tests.Audio;
public sealed class AudioOut2PacingTests
{
[Theory]
[InlineData(256u, 48000u, 1u, 40.0)]
[InlineData(256u, 48000u, 2u, 45.333)]
[InlineData(256u, 48000u, 4u, 56.0)]
[InlineData(1024u, 48000u, 4u, 104.0)]
public void ABlockingPushWaitsUntilTheDeviceHoldsTheCushionAndOneSlotLessThanTheQueue(
uint grainSamples, uint frequency, uint queueDepth, double expected)
{
Assert.Equal(expected, AudioOut2Exports.DeviceSlotFreeThreshold(grainSamples, frequency, queueDepth), 3);
}
[Fact]
public void TheThresholdStaysFarBelowTheHostQueueCap()
{
const double hostCapMilliseconds = 128 * 1024 * 1000.0 / (48000 * 4);
Assert.True(AudioOut2Exports.DeviceSlotFreeThreshold(256, 48000, 2) < hostCapMilliseconds / 10);
}
}