Skip to content

Commit 6331e8e

Browse files
perf: Oboe 1x burst buffer + pre-GC collect on high-performance session entry
Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/bc693bb2-a813-42a1-98d9-8fa9d7c0f952 Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
1 parent a876b90 commit 6331e8e

2 files changed

Lines changed: 41 additions & 3 deletions

File tree

osu.Android/Native/oboe_bridge.cpp

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,22 @@ bool OboeBridge::open(int32_t sampleRate) {
147147
// This allows the system to prioritize our audio thread for stable low latency.
148148
stream_->setPerformanceHintEnabled(true);
149149

150-
// Set buffer size to 2x burst size for initial stability.
151-
// LatencyTuner will then attempt to shrink it to 1x burst if stable.
152-
stream_->setBufferSizeInFrames(stream_->getFramesPerBurst() * 2);
150+
// Start at the minimum possible buffer: exactly 1× burst.
151+
//
152+
// On devices with AAudio MMAP support (Pixel 3+, Snapdragon 8 Gen 1+, most
153+
// modern Android), the MMAP path writes directly to the hardware ring buffer.
154+
// Starting at 1× burst achieves the minimum possible end-to-end audio latency
155+
// immediately — no convergence period needed.
156+
//
157+
// Previously we started at 2× burst and relied on LatencyTuner to shrink it
158+
// over ~512ms (128 callbacks × 4ms/callback at 48kHz/192-frame burst).
159+
// That delay meant users experienced ~8ms extra audio latency for the first
160+
// half-second of every gameplay session.
161+
//
162+
// LatencyTuner is still active and will automatically increase the buffer
163+
// if underruns occur (backing off to 2× or more as needed), so stability
164+
// is not compromised on devices that cannot sustain 1× burst.
165+
stream_->setBufferSizeInFrames(stream_->getFramesPerBurst());
153166

154167
// Initialise LatencyTuner for dynamic buffer management.
155168
tuner_ = std::make_unique<oboe::LatencyTuner>(*stream_);

osu.Android/Performance/AndroidHighPerformanceSessionManager.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,31 @@ private void enterSession()
4848
if (!gcLatencyModeSupported)
4949
return;
5050

51+
// Pre-drain accumulated garbage before entering the low-latency window.
52+
// SustainedLowLatency suppresses Gen2 (major) GC, so any garbage already
53+
// on the heap will persist for the entire session. A non-blocking hint here
54+
// asks the runtime to schedule a collection immediately — the call returns
55+
// in microseconds and the GC runs in background. On .NET runtimes that
56+
// support it, this eliminates the most common source of a multi-frame GC
57+
// stall right at the start of gameplay (the "first-note hitbox miss"
58+
// symptom observed across multiple field sessions).
59+
//
60+
// GCCollectionMode.Optimized + blocking:false requires .NET Core 3.0+ / .NET 5+.
61+
// On Mono (older .NET for Android runtimes) it throws NotSupportedException,
62+
// and on some niche OEM runtimes it may throw PlatformNotSupportedException.
63+
// The catch-all deliberately swallows these: the call is a best-effort hint
64+
// and the cost of it failing is exactly zero (the code path below proceeds
65+
// identically).
66+
try
67+
{
68+
GC.Collect(GC.MaxGeneration, GCCollectionMode.Optimized, blocking: false);
69+
}
70+
catch
71+
{
72+
// Non-critical hint; intentionally swallows NotSupportedException /
73+
// PlatformNotSupportedException on older or non-.NET-Core runtimes.
74+
}
75+
5176
try
5277
{
5378
originalGCMode = GCSettings.LatencyMode;

0 commit comments

Comments
 (0)