Skip to content

Commit 2c94767

Browse files
Performance optimizations: input hot-path inlining, Vulkan dropdown fix, lazy probe
- Cache isStylusEvent() result (was called 2x per event in dispatch methods) - Simplify dispatch flow: early return for stylus/mouse, no redundant checks - Add AggressiveInlining to all input handler hot paths (mouse, stylus, keyboard) - Convert keyboard key map from 80-case switch to static Dictionary (O(1) lookup) - Add deg_to_rad constant for stylus rotation (avoid repeated division) - Request unbuffered dispatch early in OnCreate for minimum input latency - Remove GC.Collect(0) from high-performance session enter (causes micro-stalls) - Gate Vulkan in renderer dropdown behind IsVulkanRecommended (not IsVulkanSupported) On Adreno 7xx GPUs, Vulkan disables 3 critical features → poor performance. Users no longer see Vulkan as an option unless the probe recommends it. - Lazy VulkanProbe: no longer runs at startup, only when user enables the setting (default=false). Saves startup time and native library loading. Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/62a8048b-6fe1-424d-9c00-a3fbddf6ba74 Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
1 parent bad38da commit 2c94767

7 files changed

Lines changed: 92 additions & 153 deletions

File tree

Lines changed: 44 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
22
// See the LICENCE file in the repository root for full licence text.
33

4+
using System.Collections.Generic;
5+
using System.Runtime.CompilerServices;
46
using Android.Views;
57
using osu.Framework.Input.Handlers;
68
using osu.Framework.Input.StateChanges;
@@ -14,6 +16,42 @@ public class AndroidKeyboardHandler : InputHandler
1416
public override string Description => "Keyboard (Low Latency)";
1517
public override bool IsActive => Enabled.Value;
1618

19+
// Static dictionary for O(1) key mapping instead of 80+ case switch.
20+
private static readonly Dictionary<Keycode, Key> key_map = new Dictionary<Keycode, Key>
21+
{
22+
{ Keycode.A, Key.A }, { Keycode.B, Key.B }, { Keycode.C, Key.C }, { Keycode.D, Key.D },
23+
{ Keycode.E, Key.E }, { Keycode.F, Key.F }, { Keycode.G, Key.G }, { Keycode.H, Key.H },
24+
{ Keycode.I, Key.I }, { Keycode.J, Key.J }, { Keycode.K, Key.K }, { Keycode.L, Key.L },
25+
{ Keycode.M, Key.M }, { Keycode.N, Key.N }, { Keycode.O, Key.O }, { Keycode.P, Key.P },
26+
{ Keycode.Q, Key.Q }, { Keycode.R, Key.R }, { Keycode.S, Key.S }, { Keycode.T, Key.T },
27+
{ Keycode.U, Key.U }, { Keycode.V, Key.V }, { Keycode.W, Key.W }, { Keycode.X, Key.X },
28+
{ Keycode.Y, Key.Y }, { Keycode.Z, Key.Z },
29+
{ Keycode.Num0, Key.Number0 }, { Keycode.Num1, Key.Number1 }, { Keycode.Num2, Key.Number2 },
30+
{ Keycode.Num3, Key.Number3 }, { Keycode.Num4, Key.Number4 }, { Keycode.Num5, Key.Number5 },
31+
{ Keycode.Num6, Key.Number6 }, { Keycode.Num7, Key.Number7 }, { Keycode.Num8, Key.Number8 },
32+
{ Keycode.Num9, Key.Number9 },
33+
{ Keycode.DpadUp, Key.Up }, { Keycode.DpadDown, Key.Down },
34+
{ Keycode.DpadLeft, Key.Left }, { Keycode.DpadRight, Key.Right },
35+
{ Keycode.Enter, Key.Enter }, { Keycode.Escape, Key.Escape },
36+
{ Keycode.Space, Key.Space }, { Keycode.Tab, Key.Tab },
37+
{ Keycode.Del, Key.BackSpace }, { Keycode.ForwardDel, Key.Delete },
38+
{ Keycode.MoveHome, Key.Home }, { Keycode.MoveEnd, Key.End },
39+
{ Keycode.PageUp, Key.PageUp }, { Keycode.PageDown, Key.PageDown },
40+
{ Keycode.ShiftLeft, Key.ShiftLeft }, { Keycode.ShiftRight, Key.ShiftRight },
41+
{ Keycode.CtrlLeft, Key.ControlLeft }, { Keycode.CtrlRight, Key.ControlRight },
42+
{ Keycode.AltLeft, Key.AltLeft }, { Keycode.AltRight, Key.AltRight },
43+
{ Keycode.CapsLock, Key.CapsLock },
44+
{ Keycode.F1, Key.F1 }, { Keycode.F2, Key.F2 }, { Keycode.F3, Key.F3 },
45+
{ Keycode.F4, Key.F4 }, { Keycode.F5, Key.F5 }, { Keycode.F6, Key.F6 },
46+
{ Keycode.F7, Key.F7 }, { Keycode.F8, Key.F8 }, { Keycode.F9, Key.F9 },
47+
{ Keycode.F10, Key.F10 }, { Keycode.F11, Key.F11 }, { Keycode.F12, Key.F12 },
48+
{ Keycode.Grave, Key.Tilde }, { Keycode.Minus, Key.Minus }, { Keycode.Equals, Key.Plus },
49+
{ Keycode.LeftBracket, Key.BracketLeft }, { Keycode.RightBracket, Key.BracketRight },
50+
{ Keycode.Backslash, Key.BackSlash }, { Keycode.Semicolon, Key.Semicolon },
51+
{ Keycode.Apostrophe, Key.Quote }, { Keycode.Comma, Key.Comma },
52+
{ Keycode.Period, Key.Period }, { Keycode.Slash, Key.Slash },
53+
};
54+
1755
public AndroidKeyboardHandler()
1856
{
1957
Enabled.Default = true;
@@ -22,124 +60,32 @@ public AndroidKeyboardHandler()
2260

2361
public override bool Initialize(GameHost host) => true;
2462

63+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
2564
public bool HandleKeyEvent(KeyEvent e)
2665
{
2766
if (!Enabled.Value) return false;
2867

29-
// System keys should ALWAYS fall through to the OS
3068
if (e.KeyCode == Keycode.Back || e.KeyCode == Keycode.Home || e.KeyCode == Keycode.Menu ||
3169
e.KeyCode == Keycode.VolumeUp || e.KeyCode == Keycode.VolumeDown || e.KeyCode == Keycode.VolumeMute ||
3270
e.KeyCode == Keycode.AppSwitch)
3371
return false;
3472

35-
// In DeX, source might include other flags (like Mouse or Stylus).
36-
// We should allow anything that is clearly a keyboard or has a valid keycode.
3773
if (!e.Source.HasFlag(InputSourceType.Keyboard) && !e.Source.HasFlag(InputSourceType.Mouse) && !e.Source.HasFlag(InputSourceType.Stylus) && e.Source != InputSourceType.Unknown)
3874
{
39-
// If it's not a keyboard source, only allow if it's from a device that HAS a keyboard
40-
var device = e.Device;
41-
if (device == null || device.KeyboardType == global::Android.Views.InputKeyboardType.None)
42-
return false;
75+
var device = e.Device;
76+
if (device == null || device.KeyboardType == global::Android.Views.InputKeyboardType.None)
77+
return false;
4378
}
4479

45-
var key = mapKey(e.KeyCode);
46-
if (key == Key.Unknown) return false;
80+
if (!key_map.TryGetValue(e.KeyCode, out var key))
81+
return false;
4782

4883
bool isDown = e.Action == KeyEventActions.Down;
4984

50-
// We want to handle the first press, but skip OS-level repeats to avoid input lag/buffer bloat
5185
if (e.RepeatCount > 0 && isDown) return true;
5286

5387
PendingInputs.Enqueue(new KeyboardKeyInput(key, isDown));
5488
return true;
5589
}
56-
57-
private Key mapKey(Keycode code)
58-
{
59-
switch (code)
60-
{
61-
case Keycode.A: return Key.A;
62-
case Keycode.B: return Key.B;
63-
case Keycode.C: return Key.C;
64-
case Keycode.D: return Key.D;
65-
case Keycode.E: return Key.E;
66-
case Keycode.F: return Key.F;
67-
case Keycode.G: return Key.G;
68-
case Keycode.H: return Key.H;
69-
case Keycode.I: return Key.I;
70-
case Keycode.J: return Key.J;
71-
case Keycode.K: return Key.K;
72-
case Keycode.L: return Key.L;
73-
case Keycode.M: return Key.M;
74-
case Keycode.N: return Key.N;
75-
case Keycode.O: return Key.O;
76-
case Keycode.P: return Key.P;
77-
case Keycode.Q: return Key.Q;
78-
case Keycode.R: return Key.R;
79-
case Keycode.S: return Key.S;
80-
case Keycode.T: return Key.T;
81-
case Keycode.U: return Key.U;
82-
case Keycode.V: return Key.V;
83-
case Keycode.W: return Key.W;
84-
case Keycode.X: return Key.X;
85-
case Keycode.Y: return Key.Y;
86-
case Keycode.Z: return Key.Z;
87-
case Keycode.Num0: return Key.Number0;
88-
case Keycode.Num1: return Key.Number1;
89-
case Keycode.Num2: return Key.Number2;
90-
case Keycode.Num3: return Key.Number3;
91-
case Keycode.Num4: return Key.Number4;
92-
case Keycode.Num5: return Key.Number5;
93-
case Keycode.Num6: return Key.Number6;
94-
case Keycode.Num7: return Key.Number7;
95-
case Keycode.Num8: return Key.Number8;
96-
case Keycode.Num9: return Key.Number9;
97-
case Keycode.DpadUp: return Key.Up;
98-
case Keycode.DpadDown: return Key.Down;
99-
case Keycode.DpadLeft: return Key.Left;
100-
case Keycode.DpadRight: return Key.Right;
101-
case Keycode.Enter: return Key.Enter;
102-
case Keycode.Escape: return Key.Escape;
103-
case Keycode.Space: return Key.Space;
104-
case Keycode.Tab: return Key.Tab;
105-
case Keycode.Del: return Key.BackSpace;
106-
case Keycode.ForwardDel: return Key.Delete;
107-
case Keycode.MoveHome: return Key.Home;
108-
case Keycode.MoveEnd: return Key.End;
109-
case Keycode.PageUp: return Key.PageUp;
110-
case Keycode.PageDown: return Key.PageDown;
111-
case Keycode.ShiftLeft: return Key.ShiftLeft;
112-
case Keycode.ShiftRight: return Key.ShiftRight;
113-
case Keycode.CtrlLeft: return Key.ControlLeft;
114-
case Keycode.CtrlRight: return Key.ControlRight;
115-
case Keycode.AltLeft: return Key.AltLeft;
116-
case Keycode.AltRight: return Key.AltRight;
117-
case Keycode.CapsLock: return Key.CapsLock;
118-
case Keycode.F1: return Key.F1;
119-
case Keycode.F2: return Key.F2;
120-
case Keycode.F3: return Key.F3;
121-
case Keycode.F4: return Key.F4;
122-
case Keycode.F5: return Key.F5;
123-
case Keycode.F6: return Key.F6;
124-
case Keycode.F7: return Key.F7;
125-
case Keycode.F8: return Key.F8;
126-
case Keycode.F9: return Key.F9;
127-
case Keycode.F10: return Key.F10;
128-
case Keycode.F11: return Key.F11;
129-
case Keycode.F12: return Key.F12;
130-
case Keycode.Grave: return Key.Tilde;
131-
case Keycode.Minus: return Key.Minus;
132-
case Keycode.Equals: return Key.Plus;
133-
case Keycode.LeftBracket: return Key.BracketLeft;
134-
case Keycode.RightBracket: return Key.BracketRight;
135-
case Keycode.Backslash: return Key.BackSlash;
136-
case Keycode.Semicolon: return Key.Semicolon;
137-
case Keycode.Apostrophe: return Key.Quote;
138-
case Keycode.Comma: return Key.Comma;
139-
case Keycode.Period: return Key.Period;
140-
case Keycode.Slash: return Key.Slash;
141-
default: return Key.Unknown;
142-
}
143-
}
14490
}
14591
}

osu.Android/Input/AndroidMouseHandler.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
22
// See the LICENCE file in the repository root for full licence text.
33

4+
using System.Runtime.CompilerServices;
45
using Android.Views;
56
using osu.Framework.Input.Handlers;
67
using osu.Framework.Input.StateChanges;
@@ -29,6 +30,7 @@ public AndroidMouseHandler()
2930

3031
public override bool Initialize(GameHost host) => true;
3132

33+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
3234
public bool HandleMotionEvent(MotionEvent e)
3335
{
3436
if (!Enabled.Value) return false;
@@ -45,14 +47,13 @@ public bool HandleMotionEvent(MotionEvent e)
4547
}
4648

4749
for (int i = 0; i < e.HistorySize; i++)
48-
{
4950
handlePointer(e, i);
50-
}
51-
handlePointer(e, -1);
5251

52+
handlePointer(e, -1);
5353
return true;
5454
}
5555

56+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
5657
private void handlePointer(MotionEvent e, int historyIndex)
5758
{
5859
const int pointer_index = 0;

osu.Android/Input/AndroidStylusHandler.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// See the LICENCE file in the repository root for full licence text.
33

44
using System;
5+
using System.Runtime.CompilerServices;
56
using Android.Views;
67
using osu.Framework.Bindables;
78
using osu.Framework.Input.Handlers;
@@ -47,6 +48,8 @@ public class AndroidStylusHandler : InputHandler, ITabletHandler
4748
private float outLeft, outTop, outWidth, outHeight;
4849
private float rotSin, rotCos;
4950

51+
private const float deg_to_rad = MathF.PI / 180f;
52+
5053
public AndroidStylusHandler()
5154
{
5255
Enabled.Default = true;
@@ -114,11 +117,12 @@ private void updateCachedTransform()
114117
outWidth = oSize.X;
115118
outHeight = oSize.Y;
116119

117-
float radians = MathF.PI / 180f * Rotation.Value;
120+
float radians = deg_to_rad * Rotation.Value;
118121
rotSin = MathF.Sin(radians);
119122
rotCos = MathF.Cos(radians);
120123
}
121124

125+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
122126
public bool HandleMotionEvent(MotionEvent e)
123127
{
124128
if (!Enabled.Value) return false;
@@ -141,6 +145,7 @@ public bool HandleMotionEvent(MotionEvent e)
141145
return true;
142146
}
143147

148+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
144149
private void handlePointer(MotionEvent e, int historyIndex)
145150
{
146151
const int pointer_index = 0;

osu.Android/OsuGameActivity.cs

Lines changed: 31 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,14 @@ protected override void OnCreate(Bundle? savedInstanceState)
9393
Window.AddFlags(WindowManagerFlags.Fullscreen);
9494
Window.AddFlags(WindowManagerFlags.KeepScreenOn);
9595

96+
// Request unbuffered touch dispatch early for minimum input latency.
97+
// This applies to all subsequent touch events for this window.
98+
if (OperatingSystem.IsAndroidVersionAtLeast(21))
99+
{
100+
try { Window.DecorView?.RequestUnbufferedDispatch(MotionEvent.Obtain(0, 0, MotionEventActions.Down, 0, 0, 0)); }
101+
catch { /* best-effort; will also be requested per-event in dispatch methods */ }
102+
}
103+
96104
// Hide the system pointer icon to prevent double cursors in DeX or with mouse.
97105
if (OperatingSystem.IsAndroidVersionAtLeast(24))
98106
{
@@ -153,70 +161,51 @@ public override bool DispatchTouchEvent(MotionEvent? e)
153161
{
154162
if (e == null) return base.DispatchTouchEvent(e);
155163

156-
bool handled = false;
164+
bool isStylus = isStylusEvent(e);
157165

158-
if (isStylusEvent(e))
166+
if (isStylus)
159167
{
160168
if (e.ActionMasked == MotionEventActions.Down || e.ActionMasked == MotionEventActions.HoverEnter)
161169
Window?.DecorView?.RequestUnbufferedDispatch(e);
162170

163-
handled = StylusHandler?.HandleMotionEvent(e) ?? false;
171+
bool handled = StylusHandler?.HandleMotionEvent(e) ?? false;
172+
return handled;
164173
}
165-
else if (e.Source.HasFlag(InputSourceType.Mouse))
174+
175+
if (e.Source.HasFlag(InputSourceType.Mouse))
166176
{
167177
if (e.ActionMasked == MotionEventActions.Down)
168178
Window?.DecorView?.RequestUnbufferedDispatch(e);
169179

170-
handled = MouseHandler?.HandleMotionEvent(e) ?? false;
180+
if (MouseHandler?.HandleMotionEvent(e) ?? false)
181+
return true;
171182
}
172183

173-
// Stylus events should NEVER be passed to base.DispatchTouchEvent, as it triggers
174-
// Android's touch-mode which hides the cursor and shows touch effects.
175-
if (isStylusEvent(e))
176-
return handled;
177-
178-
// Mouse events handled by our custom handler should NOT be passed to base, as the
179-
// framework's default AndroidGameView.OnTouchEvent would also process them, causing
180-
// double cursor movement and double clicks.
181-
if (handled && e.Source.HasFlag(InputSourceType.Mouse))
182-
return true;
183-
184-
// Regular touch (finger) and unhandled events fall through to the framework's default handler.
185-
return base.DispatchTouchEvent(e) || handled;
184+
return base.DispatchTouchEvent(e);
186185
}
187186

188187
public override bool DispatchGenericMotionEvent(MotionEvent? e)
189188
{
190189
if (e == null) return base.DispatchGenericMotionEvent(e);
191190

192-
bool handled = false;
191+
bool isStylus = isStylusEvent(e);
193192

194-
if (isStylusEvent(e))
193+
if (isStylus)
195194
{
196-
if (e.ActionMasked == MotionEventActions.Down || e.ActionMasked == MotionEventActions.HoverEnter)
195+
if (e.ActionMasked == MotionEventActions.HoverEnter)
197196
Window?.DecorView?.RequestUnbufferedDispatch(e);
198197

199-
handled = StylusHandler?.HandleMotionEvent(e) ?? false;
198+
bool handled = StylusHandler?.HandleMotionEvent(e) ?? false;
199+
return handled;
200200
}
201-
else if (e.Source.HasFlag(InputSourceType.Mouse))
202-
{
203-
if (e.ActionMasked == MotionEventActions.Down)
204-
Window?.DecorView?.RequestUnbufferedDispatch(e);
205201

206-
handled = MouseHandler?.HandleMotionEvent(e) ?? false;
202+
if (e.Source.HasFlag(InputSourceType.Mouse))
203+
{
204+
if (MouseHandler?.HandleMotionEvent(e) ?? false)
205+
return true;
207206
}
208207

209-
// Stylus hover events should not be passed to base to avoid system-level hover effects
210-
// and touch-mode triggers.
211-
if (isStylusEvent(e))
212-
return handled;
213-
214-
// Mouse events handled by our custom handler should NOT be passed to base to prevent
215-
// double-processing by the framework's default motion handler.
216-
if (handled && e.Source.HasFlag(InputSourceType.Mouse))
217-
return true;
218-
219-
return base.DispatchGenericMotionEvent(e) || handled;
208+
return base.DispatchGenericMotionEvent(e);
220209
}
221210

222211
public override bool OnTouchEvent(MotionEvent? e)
@@ -239,19 +228,21 @@ public override bool OnGenericMotionEvent(MotionEvent? e)
239228
return base.OnGenericMotionEvent(e);
240229
}
241230

231+
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
242232
private bool isStylusEvent(MotionEvent e)
243233
{
244-
// Check source first, as it's the most reliable indicator on some devices.
234+
// Source flag check is cheapest and short-circuits for the common case.
245235
if ((e.Source & InputSourceType.Stylus) == InputSourceType.Stylus)
246236
return true;
247237

248-
// Check tool type for each pointer.
238+
// Fallback: check tool type per pointer for devices that don't set the source flag.
249239
for (int i = 0; i < e.PointerCount; i++)
250240
{
251241
var toolType = e.GetToolType(i);
252242
if (toolType == MotionEventToolType.Stylus || toolType == MotionEventToolType.Eraser)
253243
return true;
254244
}
245+
255246
return false;
256247
}
257248

0 commit comments

Comments
 (0)