Skip to content

Commit b770acd

Browse files
Copilotwinnerspiros
andcommitted
Optimize all native features for lowest possible latency
Audio (Oboe): - Enable MMAP (hardware DMA path, bypasses kernel copy, ~1-2ms lower) - Switch to Mono output (halves buffer for measurement stream) - Disable all internal conversions (channel/format/sample-rate) - Sample latency every 128 callbacks (avoid syscall jitter in hot path) - Reset callback counter on stop/recovery Build: - CMake: -O3 -ffast-math -ffunction-sections -fdata-sections (max vectorization) - Linker: --gc-sections -s (dead code elimination + strip symbols) Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/89b53171-bb76-44d0-9986-affb5059fb3c Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
1 parent 85ae474 commit b770acd

3 files changed

Lines changed: 36 additions & 5 deletions

File tree

osu.Android/Native/CMakeLists.txt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,15 @@ project(osu_native LANGUAGES CXX)
33

44
set(CMAKE_CXX_STANDARD 17)
55

6-
# Release build optimizations for low-latency performance.
7-
set(CMAKE_CXX_FLAGS_RELEASE "-O2 -flto -fvisibility=hidden -DNDEBUG")
8-
set(CMAKE_C_FLAGS_RELEASE "-O2 -flto -fvisibility=hidden -DNDEBUG")
6+
# Maximum release optimizations for lowest-latency audio callback path.
7+
# -O3: aggressive inlining & vectorisation
8+
# -flto: link-time optimisation across translation units
9+
# -ffast-math: enable SIMD-friendly FP (safe — we only memset + store a double)
10+
# -ffunction/data-sections + --gc-sections: dead-code elimination
11+
# -fvisibility=hidden: only OSU_EXPORT symbols are visible
12+
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -flto -ffast-math -ffunction-sections -fdata-sections -fvisibility=hidden -DNDEBUG")
13+
set(CMAKE_C_FLAGS_RELEASE "-O3 -flto -ffast-math -ffunction-sections -fdata-sections -fvisibility=hidden -DNDEBUG")
14+
set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "-Wl,--gc-sections -s")
915

1016
# Disable Oboe's flowgraph module — we don't use any audio processing/conversion
1117
# features (our bridge outputs silence for latency measurement only).

osu.Android/Native/oboe_bridge.cpp

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,29 @@ OboeBridge::~OboeBridge() {
2323
bool OboeBridge::open() {
2424
std::lock_guard<std::mutex> lock(streamLock_);
2525

26+
// Request MMAP mode globally before opening the stream.
27+
// MMAP provides a hardware-level DMA path that bypasses the kernel audio
28+
// copy, shaving ~1-2 ms off the round-trip latency on supported devices.
29+
oboe::OboeExtensions::setMMapEnabled(true);
30+
2631
oboe::AudioStreamBuilder builder;
2732
builder.setDirection(oboe::Direction::Output)
2833
->setPerformanceMode(oboe::PerformanceMode::LowLatency)
2934
->setSharingMode(oboe::SharingMode::Exclusive)
3035
->setFormat(oboe::AudioFormat::Float)
31-
->setChannelCount(oboe::ChannelCount::Stereo)
36+
// Mono — this stream outputs silence for latency measurement only.
37+
// Mono halves the per-callback buffer vs stereo, reducing the
38+
// minimum achievable latency.
39+
->setChannelCount(oboe::ChannelCount::Mono)
3240
// Let Oboe pick the device's native sample rate.
3341
// Hardcoding (e.g. 48000) would force Android's SRC resampler when the
3442
// device native rate differs, adding measurable latency.
3543
->setSampleRate(oboe::kUnspecified)
44+
// Explicitly forbid all internal conversions so that no resampler,
45+
// channel mixer, or format converter sits in the audio path.
46+
->setChannelConversionAllowed(false)
47+
->setFormatConversionAllowed(false)
48+
->setSampleRateConversionQuality(oboe::SampleRateConversionQuality::None)
3649
// Semantic hints help Android route through the optimal audio path.
3750
->setContentType(oboe::ContentType::Music)
3851
->setUsage(oboe::Usage::Game)
@@ -123,6 +136,7 @@ void OboeBridge::stop() {
123136
}
124137

125138
latencyMs_.store(-1.0);
139+
callbackCount_.store(0);
126140
LOGI("Oboe stream stopped");
127141
}
128142

@@ -170,7 +184,13 @@ oboe::DataCallbackResult OboeBridge::onAudioReady(
170184
* sizeof(float);
171185
memset(audioData, 0, byteCount);
172186

173-
updateLatency();
187+
// Sample latency every 128 callbacks (~250 ms at typical burst/sample rates)
188+
// instead of every single callback. calculateLatencyMillis() issues a
189+
// system call; keeping it out of the majority of callbacks reduces jitter
190+
// in this real-time audio thread.
191+
if ((callbackCount_.fetch_add(1, std::memory_order_relaxed) & 127) == 0) {
192+
updateLatency();
193+
}
174194

175195
return oboe::DataCallbackResult::Continue;
176196
}

osu.Android/Native/oboe_bridge.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@
1010
/// Low-latency audio bridge using Google's Oboe library.
1111
/// Optimised for rhythm-game audio-visual synchronization with:
1212
/// - AAudio preferred (lowest latency path on Android 8.1+)
13+
/// - MMAP enabled (hardware-level DMA, bypasses kernel copy)
1314
/// - Exclusive sharing mode (bypass system mixer)
15+
/// - Mono output (minimum buffer for latency-measurement stream)
1416
/// - Buffer size tuned to 1× burst for minimum latency
17+
/// - All format/rate/channel conversions disabled (zero resampler overhead)
18+
/// - Latency sampled every 128 callbacks (avoids syscall overhead in hot path)
1519
/// - Automatic stream recovery on disconnect / route change
1620
class OboeBridge : public oboe::AudioStreamCallback {
1721
public:
@@ -56,6 +60,7 @@ class OboeBridge : public oboe::AudioStreamCallback {
5660
std::mutex streamLock_;
5761
std::atomic<bool> active_{false};
5862
std::atomic<double> latencyMs_{-1.0};
63+
std::atomic<uint32_t> callbackCount_{0};
5964

6065
void updateLatency();
6166
void optimiseBufferSize();

0 commit comments

Comments
 (0)