Skip to content

Android: Samsung S-Pen stylus support#19076

Open
f4mrfaux wants to merge 4 commits into
libretro:masterfrom
f4mrfaux:spen-hover-fix
Open

Android: Samsung S-Pen stylus support#19076
f4mrfaux wants to merge 4 commits into
libretro:masterfrom
f4mrfaux:spen-hover-fix

Conversation

@f4mrfaux

Copy link
Copy Markdown

Description

Focused, S-Pen-only re-submission of Android Samsung S-Pen stylus support, rebased onto current master.

This addresses the review that closed the earlier version of this PR. The branch has been split down to the S-Pen feature alone — the unrelated files (cheat_manager and other "omnibus" changes, the Pokémon-branded files, config.def.h) and the non-portable strcasestr usage are gone. It builds, and the history is two clean commits on top of current master.

What it does

  • Stylus input through the official RETRO_DEVICE_POINTER path — no ad-hoc mouse/touch emulation.
  • Phantom-tap suppression for Samsung pens (hover guard, proximity tracking, quick-tap gating, menu gesture isolation). Samsung firmware emits ghost touchscreen events on hover; these layers stop them from becoming clicks/strokes.
  • toolType-based stylus/finger classification with input-source fallback.
  • Contact detection (distance-based → instant cursor) separated from click detection (pressure-based, configurable). Hover never synthesizes a click.
  • Pointer index contract for cores: index 0 = cursor, 1 = tip contact, 2 = side button.
  • Both AMOTION_EVENT_BUTTON_STYLUS_PRIMARY and …_SECONDARY barrel buttons (devices vary).
  • Auto-hides the on-screen touch overlay while the pen is active (restored ~2 s after the last stylus event), since overlay hit-zones conflict with stylus taps.

Settings (Android only; menu + i18n wired)

  • input_stylus_enable — master switch (default ON)
  • input_stylus_require_contact_for_click — tip contact required for a click (default ON)
  • input_stylus_hover_moves_pointer — hover moves pointer without asserting PRESSED (default OFF)
  • input_stylus_pressure_sensitivity — click pressure threshold, 1–100 (default 70)

How prior review was addressed

  • Split into a single feature — S-Pen only; no cheat_manager/unrelated files.
  • No Pokémon-branded files.
  • No strcasestr (or other non-portable libc calls) introduced.
  • Builds — verified with an Android aarch64 debug build. Also fixed a config-persistence bug where input_stylus_pressure_sensitivity (a uint) was mistakenly registered among the bool settings, so it now saves/loads correctly.

Files changed

input/drivers/android_input.c, configuration.c, configuration.h, menu/menu_setting.c, menu/menu_displaylist.c, menu/cbs/menu_cbs_sublabel.c, menu/menu_driver.c, msg_hash.h, intl/msg_hash_lbl.h, intl/msg_hash_us.h, runloop.c, docs/S-Pen-Implementation.md.

Related issues

Credits

f4mrfaux and others added 2 commits May 29, 2026 13:43
Comprehensive stylus support for the Android input driver:
- Phantom-tap suppression for Samsung pens (hover guard, proximity
  tracking, quick-tap gating, menu gesture isolation).
- toolType-based stylus/finger classification with source fallback.
- Distance-based contact detection (instant cursor) separated from
  pressure-based click detection (configurable sensitivity).
- Pointer index contract for cores: 0=cursor, 1=tip contact, 2=side button.
- PRIMARY + SECONDARY side-button support.
- Settings (configuration + menu + i18n wiring): input_stylus_enable,
  input_stylus_pressure_sensitivity, input_stylus_require_contact_for_click,
  input_stylus_hover_moves_pointer.
- Auto-hide the on-screen overlay while the stylus is active, restored
  ~2s after the last stylus event (runloop hook next to the existing
  controller-connected hide block).

Squashed from the prior S-Pen commit series; stray version.all /
AndroidManifest version downgrades dropped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Architecture and maintainer reference for the Android S-Pen input driver,
settings, contact/click separation, and the pointer contract for cores.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@LibretroAdmin

Copy link
Copy Markdown
Contributor

If you can commit this to your PR on top, this addresses some of the issues I still have with it
0001-android-S-Pen-review-fixes.patch

Follow-up fixes on top of the S-Pen stylus PR:

- C89: hoist side_pressed/tip_touching/pressure_threshold to the
  android_input_poll_event_type_motion declaration block; convert the
  inline definitions to assignments. Wrap the Android physical-keyboard
  selector in menu_setting.c in its own block so st/current_input are
  declared at block start. RetroArch .c files must build clean under
  gcc -std=c89 -pedantic.
- Add AMOTION_EVENT_AXIS_DISTANCE (24) to the __ANDROID_API__ < 14
  fallback enum. It was used by the contact-distance path but only
  present in current NDK headers, breaking the fallback contract that
  the rest of the new constants already honor.
- runloop.c: when input_overlay_pointer_enable is set, soft-hide the
  overlay via INPUT_OVERLAY_GAMEPAD_HIDDEN instead of unloading it,
  mirroring the gamepad-connected block. Unloading tore down overlay_ptr,
  which is the pointer surface the stylus drives.
@f4mrfaux

Copy link
Copy Markdown
Author

Hardware-validation finding: the soft-hide change breaks hover-driven cursor for stylus + RETRO_DEVICE_POINTER cores

Thanks for the review fixes — the C89 hoist and the AMOTION_EVENT_AXIS_DISTANCE fallback are both clean wins and we've kept them. We ran the third change (runloop.c soft-hide-when-input_overlay_pointer_enable) through hardware testing on a Z Fold 5 with the snes9x core (Mario Paint Europe, S-Pen) and want to flag that it introduces a regression we need to resolve before merge.

The empirical regression

On the working pre-review-fix build, hover-cursor tracking worked: pen movement updated the in-game cursor without any tip contact. With the soft-hide change applied, hover-cursor tracking stops working entirely; the cursor only moves when the tip is pressed. Toggling Display On-Screen Overlay → OFF in the menu restored hover-cursor behavior immediately, which isolated the cause to the overlay being kept resident during stylus activity.

Side-by-side log evidence

Same session, same pen, same core. The user toggled the overlay off mid-session.

[SPEN-HOVER] tgt=(131.8,133.8) est=(147.0,144.0) step=(-15,-10) ... gen=1 count=1   ← overlay ON
[SPEN-HOVER] tgt=(131.9,133.8) est=(132.0,134.0) step=(0,0)     ... gen=1 count=1
...user toggles overlay OFF...
[SPEN-HOVER] tgt=(105.0,163.7) est=(106.0,156.0) step=(-1,8)    ... gen=0 count=0   ← overlay OFF
[SPEN-HOVER] tgt=(119.8,158.2) est=(107.0,169.0) step=(13,-11)  ... gen=0 count=0
[SPEN-HOVER] tgt=(125.7,141.7) est=(124.0,148.0) step=(2,-6)    ... gen=0 count=0

gen= is RETRO_DEVICE_ID_POINTER_PRESSED at index 0; count= is RETRO_DEVICE_ID_POINTER_COUNT. With the overlay loaded + input_overlay_pointer_enable=true, queries to RETRO_DEVICE_POINTER are answered by the overlay's pointer state (no current touch on any overlay hitbox → count=0 semantically, but the live touch-tracker reports count=1 PRESSED=1 for the stylus's last contact frame) instead of the native android_input pointer state where hover updates pointer[0].x/y with count=0 PRESSED=false. Cores that detect hover via coord-change in count-independent POINTER_X/Y with PRESSED=false (the contract documented in the PR body) stop firing.

Why "unloading tore down overlay_ptr" doesn't apply to the S-Pen path

The commit message argues: "Unloading tore down overlay_ptr, which is the pointer surface the stylus drives." That premise doesn't hold for native S-Pen events. The S-Pen path in android_input.c (lines around `is_stylus` handling) writes coordinates directly to `android->pointer[0..2].x/y` and these are returned via the driver's `input_state` callback for `RETRO_DEVICE_POINTER` queries. The overlay's pointer surface is a separate pathway used for finger-driven overlay-as-mouse / overlay-as-lightgun input. The stylus does not pass through overlay_ptr at any point.

What soft-hide preserves is overlay-as-mouse for finger users; what it inadvertently destroys is hover-cursor for stylus users. Auto-hide while the pen is active means there is no finger-on-overlay use case during that window anyway, so preserving overlay_ptr has no benefit and the cost is breaking the feature the PR is built around.

Proposed fix

Revert the runloop change to the original full-input_overlay_unload() behavior on stylus active, and document the invariant so a future maintainer doesn't replay the same review:

-      /* runloop.c: when input_overlay_pointer_enable is set, soft-hide */
-      if (settings->bools.input_overlay_pointer_enable && input_st->overlay_ptr) {
-         if (stylus_active)
-            input_st->overlay_ptr->flags |=  INPUT_OVERLAY_GAMEPAD_HIDDEN;
-         else
-            input_st->overlay_ptr->flags &= ~INPUT_OVERLAY_GAMEPAD_HIDDEN;
-         ...
-      } else if (stylus_active != last_stylus) {
-         if (stylus_active) input_overlay_unload(); else input_overlay_init();
-      }
+      /* Always fully unload the overlay during stylus activity. The stylus
+       * does not flow through overlay_ptr (see android_input.c, native
+       * pointer[0..2] write path), so unloading does not lose stylus input
+       * surface. A loaded overlay with input_overlay_pointer_enable=true
+       * answers RETRO_DEVICE_POINTER queries from its own touch tracker
+       * instead of the native android pointer state, which destroys the
+       * count-independent hover contract that S-Pen cores rely on. */
+      if (stylus_active != last_stylus) {
+         if (stylus_active) input_overlay_unload(); else input_overlay_init();
+      }

Hardware-validated: this restores hover-cursor in Mario Paint (Europe ROM, snes9x stylus/spen-support core) with the on-screen overlay still enabled in settings. The overlay auto-hides during pen activity and restores ~2s after, as the PR originally promised.

Happy to push the revert + comment as a follow-up commit on this PR if that's the path you'd prefer, or to land it as a separate PR for clearer review history — your call.

Hardware testing on Samsung S-Pen + snes9x stylus/spen-support core
(Mario Paint Europe, CRC 266B220E) confirmed the soft-hide path added
in 887f038 breaks hover-driven cursor movement. With pointer enable
true, a loaded overlay intercepts RETRO_DEVICE_POINTER queries and
answers from its own touch tracker (count=1 PRESSED=true) instead of
the native android_input pointer state (count=0 PRESSED=false for
hover), destroying the count-independent coord-change hover contract
that S-Pen-aware cores rely on. Symptom: hover stops moving the in-game
cursor; only tip+drag moves it. Toggling Display On-Screen Overlay OFF
restores hover immediately.

The reasoning behind the soft-hide ("Unloading tore down overlay_ptr,
which is the pointer surface the stylus drives") does not apply to
native S-Pen events: those write directly to android->pointer[0..2].x/y
in android_input.c and bypass overlay_ptr entirely. overlay_ptr IS the
pointer surface for finger-driven overlay-as-mouse/lightgun, but that
use case does not apply during stylus activity (auto-hide is exactly
the window where no finger is on the overlay), so preserving it has no
benefit and the cost is breaking the feature this PR is built around.

Reverts the runloop.c change from 887f038 to the original 47fea8b24
behavior (always input_overlay_unload() on edge). Keeps the C89 and
AMOTION_EVENT_AXIS_DISTANCE fixes from 887f038 intact. Adds an
extended comment block documenting the invariant so a future review
does not replay the same thread.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@f4mrfaux

Copy link
Copy Markdown
Author

Pushed the revert as commit 9dab12487 on top of 887f038ae, leaving the C89 hoist and AMOTION_EVENT_AXIS_DISTANCE fallback intact and reverting only the runloop.c soft-hide branch to the original full-input_overlay_unload() from 47fea8b24. Comment block in the new commit documents the invariant.

If you'd rather amend 887f038ae in place than carry an extra revert commit, happy to force-push a squash — let me know which form the PR is easier to review with.

return android->pointer[idx].full_y;
return android->pointer[idx].confined_y;
case RETRO_DEVICE_ID_POINTER_PRESSED:
#ifdef DEBUG_ANDROID_INPUT

@RobLoach RobLoach Jun 13, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This macro isn't used anywhere else. Should it just be the default debug macro instead? Or just remove it entirely. Seems like your trouble shooting.

action == AMOTION_EVENT_ACTION_UP
|| action == AMOTION_EVENT_ACTION_CANCEL
|| action == AMOTION_EVENT_ACTION_POINTER_UP);
/* C89: All variable declarations at function start */

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We don't need a note about C89 declarations. This is already a norm across the code-base.


---

*This documentation should be updated when modifying the S Pen implementation to ensure future maintainers understand the complete protection strategy.*

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This doc is crazy large, and extremely verbose. Doubt all of it is needed.

/* Calculate pressure threshold from user setting (1-100 -> 0.0248-0.0000) */
/* Higher sensitivity value = lower threshold = more sensitive */
/* At max sensitivity (100), threshold is 0 = instant click on contact */
pressure_threshold = 0.0f + ((100 - settings->uints.input_stylus_pressure_sensitivity) * 0.00025f);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

= 0.0f + ? Likely better to cast instead.

Comment thread runloop.c
Comment on lines +100 to +102
/* Defined in input/drivers/android_input.c. True for ~2s after any S-Pen event;
* used below to auto-hide the on-screen touch overlay while the stylus is in use. */
extern bool android_input_stylus_recently_active(void);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Having this in run_loop.c seems silly. Maybe introduce a android_input.h or something?

Comment thread runloop.c
input_overlay_unload();
else
input_overlay_init();
last_stylus_hidden = stylus_hidden;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is going to be run whether or not the Stylus device is enabled. Intentional?

Comment thread menu/menu_setting.c
return snprintf(s, len, "%d", *setting->value.target.unsigned_integer);
return 0;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is essentially a 1-line function. Why not just call snprintf() directly instead?

@RobLoach

Copy link
Copy Markdown
Member

I ran out of patience on reviewing the vibes here.

@f4mrfaux

Copy link
Copy Markdown
Author

I ran out of patience on reviewing the vibes here.

then task an agent ;)

my implementation works, its been hardware tested. I am happy to keep working on this and refining it but lets keep our comments constructive. your emotions, and patience dont really have a place in the conversation, respectfully.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Android] RetroArch Doesn't Ignore S Pen Hover Actions [Feature Request] <S pen support for mouse / Lightgun>

3 participants