Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions osu.Android/Native/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,40 @@ set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "-Wl,--gc-sections -Wl,-z,max-page-size=16
# features (our bridge outputs silence for latency measurement only).
# This reduces the Oboe portion of the binary by ~50%.
set(OBOE_ENABLE_FLOWGRAPH OFF CACHE BOOL "Disable Oboe flowgraph to reduce binary size")
# Skip Oboe's tests/examples — we never ship them. Avoids pulling googletest
# and shaves several seconds off the cold CMake configure/build.
set(BUILD_TESTING OFF CACHE BOOL "Disable Oboe tests" FORCE)
set(OBOE_BUILD_TESTS OFF CACHE BOOL "Disable Oboe tests" FORCE)
set(OBOE_BUILD_EXAMPLES OFF CACHE BOOL "Disable Oboe examples" FORCE)
set(OBOE_BUILD_DOCS OFF CACHE BOOL "Disable Oboe docs" FORCE)

# Download and build Oboe main branch from source to ensure we have the latest
# features and fixes (ADPF performance hints, workload management, spatialization,
# API 36 compatibility) regardless of the build environment.
# main is preferred over pinned tags because Oboe releases infrequently (~yearly)
# and the main branch accumulates significant latency-critical improvements between tags.
# GIT_SHALLOW TRUE: only fetch the tip of main (no history) — significantly faster CI checkout.
include(FetchContent)
FetchContent_Declare(oboe
GIT_REPOSITORY https://github.com/google/oboe.git
GIT_TAG main
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(oboe)

# Patch Oboe's deprecated -Ofast flag to avoid build warnings/errors
# and ensure we use the same optimized flags as the rest of the project.
# Patch Oboe's compile options:
# 1. Replace deprecated `-Ofast` with `-O3` — `-Ofast` was removed in Clang 21
# and emits warnings on newer NDKs.
# 2. Strip `-ffast-math` from Oboe's compile options. In a callback that only
# does memset/store it is harmless functionally, but it injects libm
# `__FINITE_MATH_ONLY__` symbol versions that can break linkage against the
# system libm in rare NDK combos. Our own bridge keeps `-ffast-math` (see
# CMAKE_CXX_FLAGS_RELEASE above) because we link only against libc/log/Vulkan.
get_target_property(OBOE_OPTIONS oboe COMPILE_OPTIONS)
if(OBOE_OPTIONS)
string(REPLACE "-Ofast" "-O3;-ffast-math" OBOE_OPTIONS "${OBOE_OPTIONS}")
list(REMOVE_ITEM OBOE_OPTIONS "-Ofast")
list(REMOVE_ITEM OBOE_OPTIONS "-ffast-math")
list(APPEND OBOE_OPTIONS "-O3")
set_target_properties(oboe PROPERTIES COMPILE_OPTIONS "${OBOE_OPTIONS}")
endif()

Expand Down
28 changes: 22 additions & 6 deletions osu.Android/Native/oboe_bridge.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ bool OboeBridge::open(int32_t sampleRate) {
// Audio is pre-mixed by BASS — tell Android not to spatialize it again.
->setIsContentSpatialized(true)
// Prevent other apps from capturing our audio stream (competitive integrity).
->setAllowedCapturePolicy(oboe::AllowedCapturePolicy::AllowNone)
->setAllowedCapturePolicy(oboe::AllowedCapturePolicy::None)
// Use shared_ptr overload (non-deprecated) for data callback.
->setDataCallback(stabilizedCallback_)
// Non-owning shared_ptr for error callback — OboeBridge outlives the stream.
Expand Down Expand Up @@ -250,9 +250,9 @@ void OboeBridge::setProvider(OboeAudioProvider provider) {
provider_.store(provider, std::memory_order_release);
}

const char* OboeBridge::getLastError() const {
std::string OboeBridge::getLastError() const {
std::lock_guard<std::mutex> lock(errorLock_);
return lastError_.empty() ? nullptr : lastError_.c_str();
return lastError_;
}

oboe::DataCallbackResult OboeBridge::onAudioReady(
Expand Down Expand Up @@ -283,14 +283,19 @@ oboe::DataCallbackResult OboeBridge::onAudioReady(

uint32_t count = callbackCount_.fetch_add(1, std::memory_order_relaxed);

// LatencyTuner once every 128 callbacks (~1.5s @ 192 burst, 48 kHz).

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new comment says “~1.5s @ 192 burst, 48 kHz”, but 128 callbacks * 192 frames / 48kHz is ~0.51s (and the callback frame count may not even be fixed since framesPerDataCallback is unspecified). Please correct or soften the timing estimate to avoid misleading future readers.

Suggested change
// LatencyTuner once every 128 callbacks (~1.5s @ 192 burst, 48 kHz).
// Run the LatencyTuner once every 128 callbacks. The wall-clock interval
// depends on the callback frame count and sample rate (for example, at
// 192 frames/callback and 48 kHz this is about 0.5s).

Copilot uses AI. Check for mistakes.
// updateLatency() issues an AAudio syscall, so throttle it further to every
// 256 callbacks — Tab still sees stable values, but we cut audio-thread
// syscall pressure in half.
if ((count & 127) == 0) {
updateLatency();

// Dynamically tune the buffer size to the lowest stable value.
if (tuner_) {
tuner_->tune();
}

if ((count & 255) == 0)
updateLatency();

// Attempt to set CPU affinity to high-performance cores.
// We do this inside the audio callback to ensure we target the AAudio thread.
// Uses sysfs-based topology detection for accurate big-core identification
Expand Down Expand Up @@ -478,7 +483,15 @@ OSU_EXPORT void nOboeSetProvider(intptr_t ptr, OboeAudioProvider provider) {

OSU_EXPORT const char* nOboeGetLastErrorMessage(intptr_t ptr) {
auto* bridge = reinterpret_cast<OboeBridge*>(ptr);
return bridge ? bridge->getLastError() : nullptr;
if (!bridge) return nullptr;

// Hold a thread_local snapshot so the pointer we hand back to managed code
// remains valid for the duration of the P/Invoke marshalling step, even if
// another thread (Oboe error callback) overwrites `lastError_` immediately
// after we return. Each managed thread gets its own buffer.
thread_local std::string snapshot;
snapshot = bridge->getLastError();
return snapshot.empty() ? nullptr : snapshot.c_str();
}

} // extern "C"
Expand Down Expand Up @@ -512,6 +525,9 @@ OSU_EXPORT int nGetBigCoreMask() {
}

#include <android/performance_hint.h>
// Explicit dependency for gettid() used in nADPFCreateSession below — do not
// rely on transitive includes from Oboe / NDK headers, which may change.
#include <unistd.h>
Comment on lines +528 to +530

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<unistd.h> is already included at the top of this file, so the new mid-file #include <unistd.h> is redundant and the “don’t rely on transitive includes” comment is now misleading. Consider removing the duplicate include (and ideally keeping all includes in one place).

Suggested change
// Explicit dependency for gettid() used in nADPFCreateSession below — do not
// rely on transitive includes from Oboe / NDK headers, which may change.
#include <unistd.h>

Copilot uses AI. Check for mistakes.

extern "C" {
OSU_EXPORT intptr_t nADPFCreateSession(int64_t targetDurationNanos) {
Expand Down
6 changes: 5 additions & 1 deletion osu.Android/Native/oboe_bridge.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ class OboeBridge : public oboe::AudioStreamCallback {
bool isAAudio() const;
bool isMMap() const;
void setProvider(OboeAudioProvider provider);
const char* getLastError() const;
/// Returns a copy of the most recent error message under lock. We return
/// by value (not a pointer to internal storage) so callers can't observe a
/// torn or freed `std::string` if another thread mutates `lastError_`
/// concurrently (Oboe error callbacks fire from an internal thread).
std::string getLastError() const;
Comment on lines +36 to +40

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oboe_bridge.h uses std::string in the public API (getLastError()) but the header doesn’t explicitly include <string>. It currently works only if a transitive include happens to pull it in; please add #include <string> to make the header self-contained.

Copilot uses AI. Check for mistakes.

// oboe::AudioStreamCallback
oboe::DataCallbackResult onAudioReady(
Expand Down
Loading