Skip to content

Commit 730b624

Browse files
authored
Merge pull request #269 from winnerspiros/copilot/fix-s-pen-issues-and-update
Fix S Pen stuck top-left and rewrite Oboe redirection via GlobalMixerHandle
2 parents 54a0f3e + e4931bd commit 730b624

19 files changed

Lines changed: 647 additions & 380 deletions

logs.zip

-20.8 KB
Binary file not shown.

native_crash.log

Lines changed: 0 additions & 8 deletions
This file was deleted.

osu.Android.props

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,19 @@
8383
spikes on the Update/Draw threads. Memory usage rises ~20-30% in exchange. -->
8484
<ServerGarbageCollection>true</ServerGarbageCollection>
8585
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
86+
<!-- Defensive explicit-set: we rely on the SDK linker to walk only Android SDK types
87+
(NOT user assemblies — see PublishTrimmed=off rationale above). The .NET Android
88+
SDK default is currently 'SdkOnly' but has changed across SDK versions; making it
89+
explicit here pins the behaviour so a future SDK bump cannot silently flip us to
90+
'Full' linking and break the reflection-heavy code paths in osu.Game / Realm /
91+
Newtonsoft.Json / AutoMapper / Sentry / OsuTK. -->
92+
<AndroidLinkMode>SdkOnly</AndroidLinkMode>
93+
<!-- Defensive explicit-set: keep IL alongside profiled-AOT native code so any method
94+
outside the bundled AOT profile has a JIT fallback (instead of MissingMethodException
95+
at first call). The .NET Android SDK default is 'false' for non-trimmed builds
96+
already, but we make it explicit so a future SDK bump cannot silently start
97+
stripping IL and reproduce the trim-then-AOT crash class we removed above. -->
98+
<AndroidStripILAfterAOT>false</AndroidStripILAfterAOT>
8699
</PropertyGroup>
87100

88101
<ItemGroup>

osu.Android/AndroidManifest.xml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
reaches DispatchKeyEvent and the OS default for root-task activities (moveTaskToBack,
1111
i.e. minimise) takes over instead.
1212
-->
13-
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher" android:largeHeap="true" android:hardwareAccelerated="true" android:extractNativeLibs="false" android:enableOnBackInvokedCallback="false">
13+
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher" android:largeHeap="true" android:hardwareAccelerated="true" android:extractNativeLibs="false" android:enableOnBackInvokedCallback="false" android:appCategory="game">
1414
<provider android:name="androidx.core.content.FileProvider" android:authorities="sh.ppy.osulazer.fileprovider" android:grantUriPermissions="true" android:exported="false">
1515
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/filepaths" />
1616
</provider>
@@ -20,5 +20,14 @@
2020
This enables vendor-specific optimizations (performance boost, thermal management,
2121
display refresh rate priority) on Samsung devices automatically. -->
2222
<meta-data android:name="com.samsung.android.game.biz" android:value="true" />
23+
<!-- Android 13+ Game Mode opt-out (https://developer.android.com/games/gamemode/gamemode-api).
24+
We explicitly disable Battery- and Performance-mode interventions, FPS override, and
25+
downscaling. A rhythm game's input-to-audio latency budget is hard-bound by the
26+
active refresh rate and the audio burst cadence — letting the OS Game Manager cap
27+
our framerate (typical "battery saver" downscale: 60Hz → 30Hz) or downscale our
28+
render resolution (typical "battery saver" downscale: 100% → 75%) breaks both
29+
timing accuracy AND visual hit-circle alignment. We already manage thermals, refresh
30+
rate, and sustained performance ourselves (see OsuGameAndroid.LoadComplete). -->
31+
<meta-data android:name="android.game_mode_config" android:resource="@xml/gamemode_config" />
2332
</application>
2433
</manifest>

osu.Android/Input/AndroidMouseHandler.cs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,18 @@ private void handlePointer(MotionEvent e, int historyIndex)
8686
// into Escape for menu navigation (README: "Mouse back button = Escape"); the
8787
// small overlap on those devices is harmless because Button1 has no default
8888
// binding in osu! and so cannot trigger an unintended gameplay hit.
89-
bool left = (e.ButtonState & MotionEventButtonState.Primary) != 0;
90-
bool right = (e.ButtonState & MotionEventButtonState.Secondary) != 0;
91-
bool middle = (e.ButtonState & MotionEventButtonState.Tertiary) != 0;
92-
bool back = (e.ButtonState & MotionEventButtonState.Back) != 0;
93-
bool forward = (e.ButtonState & MotionEventButtonState.Forward) != 0;
89+
//
90+
// Cache ButtonState into a local before the five bit-tests below: each access
91+
// of `e.ButtonState` is a JNI method call into MotionEvent#getButtonState, and
92+
// on a multi-button mouse a single Move sample with HistorySize=20 ends up
93+
// doing 5 × 21 = 105 redundant JNI crossings per event. Folding to a single
94+
// read drops that to 21 crossings (only the per-sample one we cannot avoid).
95+
var buttonState = e.ButtonState;
96+
bool left = (buttonState & MotionEventButtonState.Primary) != 0;
97+
bool right = (buttonState & MotionEventButtonState.Secondary) != 0;
98+
bool middle = (buttonState & MotionEventButtonState.Tertiary) != 0;
99+
bool back = (buttonState & MotionEventButtonState.Back) != 0;
100+
bool forward = (buttonState & MotionEventButtonState.Forward) != 0;
94101

95102
if (left != lastLeft) { PendingInputs.Enqueue(new MouseButtonInput(MouseButton.Left, left)); lastLeft = left; }
96103
if (right != lastRight) { PendingInputs.Enqueue(new MouseButtonInput(MouseButton.Right, right)); lastRight = right; }

osu.Android/Input/AndroidStylusHandler.cs

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -138,25 +138,33 @@ public bool HandleMotionEvent(MotionEvent e)
138138
{
139139
if (!Enabled.Value) return false;
140140

141-
if (e.ActionMasked == MotionEventActions.HoverExit || e.ActionMasked == MotionEventActions.Up || e.ActionMasked == MotionEventActions.Cancel)
141+
// Cache ActionMasked once: each `e.ActionMasked` access is a JNI call into
142+
// MotionEvent#getActionMasked. On a busy stylus drag the previous code did
143+
// 3 reads per event (here + 2 in handlePointer) and HistorySize+1 calls to
144+
// handlePointer; folding to a single read trims the per-event JNI crossings
145+
// by ~2 + 2*(HistorySize+1) at no cost.
146+
var actionMasked = e.ActionMasked;
147+
148+
if (actionMasked == MotionEventActions.HoverExit || actionMasked == MotionEventActions.Up || actionMasked == MotionEventActions.Cancel)
142149
{
143150
if (lastLeftDown) { PendingInputs.Enqueue(new MouseButtonInput(MouseButton.Left, false)); lastLeftDown = false; }
144151

145-
if (e.ActionMasked != MotionEventActions.HoverExit)
152+
if (actionMasked != MotionEventActions.HoverExit)
146153
return true;
147154
}
148155

149156
// Process all batched historical events for maximum accuracy.
150-
for (int i = 0; i < e.HistorySize; i++)
151-
handlePointer(e, i);
157+
int historySize = e.HistorySize;
158+
for (int i = 0; i < historySize; i++)
159+
handlePointer(e, i, actionMasked);
152160

153-
handlePointer(e, -1);
161+
handlePointer(e, -1, actionMasked);
154162

155163
return true;
156164
}
157165

158166
[MethodImpl(MethodImplOptions.AggressiveInlining)]
159-
private void handlePointer(MotionEvent e, int historyIndex)
167+
private void handlePointer(MotionEvent e, int historyIndex, MotionEventActions actionMasked)
160168
{
161169
const int pointer_index = 0;
162170
if (e.PointerCount <= pointer_index) return;
@@ -210,8 +218,12 @@ private void handlePointer(MotionEvent e, int historyIndex)
210218

211219
// Button state: pressure-based click (primary) with action overrides.
212220
// Uses the cached threshold field rather than `PressureThreshold.Value` to skip the
213-
// per-event bindable read.
214-
var actionMasked = e.ActionMasked;
221+
// per-event bindable read. `actionMasked` is a parameter (cached once at the top of
222+
// HandleMotionEvent) so we avoid the JNI crossing for `e.ActionMasked` here.
223+
// ButtonState is a single JNI read per pointer (vs. desktop mouse which we already
224+
// hoist) — Move-with-Primary is the only path that needs it and stylus side-buttons
225+
// are intentionally NOT mapped to right/middle (see comment block below), so a single
226+
// read is unavoidable but bounded.
215227
var buttonState = e.ButtonState;
216228
bool isLeftDown = pressure >= cachedPressureThreshold;
217229
if (actionMasked == MotionEventActions.Down || actionMasked == MotionEventActions.ButtonPress) isLeftDown = true;

0 commit comments

Comments
 (0)