Skip to content

Commit c1d7762

Browse files
Fix S Pen stuck top-left: pick stylus pointer by tool type + drop (0,0) regardless of pressure
Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/b7209fc9-c461-4187-afcb-1b3e2e3f81b0 Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
1 parent f29f6a4 commit c1d7762

1 file changed

Lines changed: 55 additions & 21 deletions

File tree

osu.Android/Input/AndroidStylusHandler.cs

Lines changed: 55 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -173,16 +173,49 @@ public bool HandleMotionEvent(MotionEvent e)
173173
releaseAllButtons();
174174
}
175175

176+
// Locate the actual stylus pointer rather than blindly reading index 0. When
177+
// a finger is also touching the screen (palm-on-screen while writing, common
178+
// with the S Pen), the stylus is frequently delivered at pointer index 1
179+
// and index 0 is the finger. Reading the finger's coordinates and feeding
180+
// them into the stylus pipeline produced exactly the "stuck top-left" snap
181+
// the user reports — when the finger is briefly at (0,0) (the bottom-left
182+
// origin in window coords on some devices, or a transient lift sample) the
183+
// mapped output is the screen origin.
184+
//
185+
// Falling back to 0 keeps the existing behaviour for the well-formed
186+
// single-pointer case where every pointer in the event is the stylus.
187+
int stylusPointerIndex = findStylusPointerIndex(e);
188+
if (stylusPointerIndex < 0) return true;
189+
176190
// Process all batched historical events for maximum accuracy.
177191
int historySize = e.HistorySize;
178192
for (int i = 0; i < historySize; i++)
179-
handlePointer(e, i, actionMasked);
193+
handlePointer(e, i, actionMasked, stylusPointerIndex);
180194

181-
handlePointer(e, -1, actionMasked);
195+
handlePointer(e, -1, actionMasked, stylusPointerIndex);
182196

183197
return true;
184198
}
185199

200+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
201+
private static int findStylusPointerIndex(MotionEvent e)
202+
{
203+
int count = e.PointerCount;
204+
if (count <= 0) return -1;
205+
206+
for (int i = 0; i < count; i++)
207+
{
208+
var toolType = e.GetToolType(i);
209+
if (toolType == MotionEventToolType.Stylus || toolType == MotionEventToolType.Eraser)
210+
return i;
211+
}
212+
213+
// No pointer self-identifies as a stylus (some devices/SDKs lose the tool-type
214+
// tag on hover-only events even when MotionEvent.Source still has the Stylus
215+
// bit). Default to index 0 to preserve the existing single-pointer behaviour.
216+
return 0;
217+
}
218+
186219
[MethodImpl(MethodImplOptions.AggressiveInlining)]
187220
private void releaseAllButtons()
188221
{
@@ -202,26 +235,27 @@ private void releaseAllButtons()
202235
private Vector2 lastTouchPosition;
203236

204237
[MethodImpl(MethodImplOptions.AggressiveInlining)]
205-
private void handlePointer(MotionEvent e, int historyIndex, MotionEventActions actionMasked)
238+
private void handlePointer(MotionEvent e, int historyIndex, MotionEventActions actionMasked, int pointerIndex)
206239
{
207-
const int pointer_index = 0;
208-
if (e.PointerCount <= pointer_index) return;
209-
210-
float rawX = historyIndex < 0 ? e.GetX(pointer_index) : e.GetHistoricalX(pointer_index, historyIndex);
211-
float rawY = historyIndex < 0 ? e.GetY(pointer_index) : e.GetHistoricalY(pointer_index, historyIndex);
212-
float pressure = historyIndex < 0 ? e.GetPressure(pointer_index) : e.GetHistoricalPressure(pointer_index, historyIndex);
213-
214-
// Drop (0, 0, 0) garbage samples. The Samsung digitizer occasionally emits a
215-
// single (rawX=0, rawY=0, pressure=0) sample when the pen wakes up after sleep,
216-
// when the activity regains focus, or as the very first HoverEnter sample
217-
// before the real coordinate is latched. Mapping that sample produces a snap
218-
// to the top-left of the screen — the long-standing "S Pen stuck top-left"
219-
// bug. A real pen sample would always have *some* coordinate (the pen is
220-
// physically *somewhere* on the digitizer to have triggered an event), so a
221-
// strict triple-zero match is a safe filter that doesn't drop legitimate
222-
// edge-of-digitizer samples (which would have pressure > 0 on contact, or
223-
// non-zero hover Y/X off the screen origin).
224-
if (rawX == 0f && rawY == 0f && pressure == 0f)
240+
if (e.PointerCount <= pointerIndex) return;
241+
242+
float rawX = historyIndex < 0 ? e.GetX(pointerIndex) : e.GetHistoricalX(pointerIndex, historyIndex);
243+
float rawY = historyIndex < 0 ? e.GetY(pointerIndex) : e.GetHistoricalY(pointerIndex, historyIndex);
244+
float pressure = historyIndex < 0 ? e.GetPressure(pointerIndex) : e.GetHistoricalPressure(pointerIndex, historyIndex);
245+
246+
// Drop (0, 0) garbage samples regardless of pressure. The Samsung digitizer
247+
// emits a (rawX=0, rawY=0) sample when the pen wakes up after sleep, when
248+
// the activity regains focus, and as the very first HoverEnter/Down sample
249+
// before the real coordinate is latched. Older versions only filtered when
250+
// pressure was also exactly zero — but device logs show contact-down and
251+
// ButtonPress samples occasionally landing at (0, 0) with pressure > 0,
252+
// which would still snap the cursor to the top-left.
253+
//
254+
// A real pen sample is *physically somewhere* on the digitizer to have
255+
// triggered the event, so a strict (rawX==0 && rawY==0) match is a safe
256+
// filter — legitimate edge-of-digitizer samples will always have at least
257+
// sub-pixel float noise on one of the two axes.
258+
if (rawX == 0f && rawY == 0f)
225259
return;
226260

227261
// Auto-expand tablet size if the digitizer reports coordinates beyond current bounds.

0 commit comments

Comments
 (0)