From 8f4c8bcd75d4d202dcc9c2a0f5b1325604b56db8 Mon Sep 17 00:00:00 2001 From: Rob Loach Date: Wed, 12 Aug 2026 15:41:01 -0400 Subject: [PATCH 1/7] Add pen support to SDL3 input driver --- input/drivers/sdl3_input.c | 176 +++++++++++++++++++++++++++++++------ 1 file changed, 151 insertions(+), 25 deletions(-) diff --git a/input/drivers/sdl3_input.c b/input/drivers/sdl3_input.c index cedb0a69c047..ca569551ae13 100644 --- a/input/drivers/sdl3_input.c +++ b/input/drivers/sdl3_input.c @@ -75,6 +75,24 @@ typedef struct sdl3_input float x; float y; } touches[SDL3_MAX_TOUCH]; + + /* Pen/stylus state, fed by the SDL_EVENT_PEN_* range. Multiple + * pens are merged into one: last writer wins. */ + bool pen_in_proximity; + bool pen_down; + /* The contact is the eraser end of the pen. */ + bool pen_eraser; + /* Barrel button state, bit (button - 1). */ + uint8_t pen_buttons; + /* Last reported position in window coordinates (points). */ + float pen_raw_x; + float pen_raw_y; + /* Position in output pixels, matching mouse_abs_*. */ + float pen_abs_x; + float pen_abs_y; + /* SDL_PEN_AXIS_* values (pressure, tilt, ...). Nothing maps them + * onto the libretro API yet; tracked so a later mapping has them. */ + float pen_axes[SDL_PEN_AXIS_COUNT]; } sdl3_input_t; /* Rebuilt on SDL_EVENT_KEYMAP_CHANGED (e.g. system layout switch). */ @@ -284,7 +302,13 @@ static int16_t sdl3_input_state( int16_t pressed = 0; if (id == RETRO_DEVICE_ID_POINTER_COUNT) - return sdl->num_touches ? sdl->num_touches : (sdl->mouse_l ? 1 : 0); + { + if (sdl->num_touches) + return sdl->num_touches; + if (sdl->pen_in_proximity) + return sdl->pen_down ? 1 : 0; + return sdl->mouse_l ? 1 : 0; + } if (!video_driver_get_viewport_info(&vp)) break; @@ -301,6 +325,18 @@ static int16_t sdl3_input_state( abs_y = (int)(sdl->touches[idx].y * (float)vp.full_height); pressed = 1; } + else if (sdl->pen_in_proximity) + { + /* While the pen is in range SDL also synthesizes mouse + * state from it (SDL_PEN_MOUSEID), so taking the pen + * ahead of the mouse fallback doubles as the filter + * that keeps one stylus tap from registering twice. */ + if (idx != 0) + return 0; + abs_x = (int)sdl->pen_abs_x; + abs_y = (int)sdl->pen_abs_y; + pressed = sdl->pen_down; + } else { if (idx != 0) @@ -350,9 +386,13 @@ static int16_t sdl3_input_state( int16_t res_y = 0; int16_t res_screen_x = 0; int16_t res_screen_y = 0; + /* A pen in range aims the gun; the mouse state SDL + * synthesizes from it tracks the same spot anyway. */ + int abs_x = (int)(sdl->pen_in_proximity ? sdl->pen_abs_x : sdl->mouse_abs_x); + int abs_y = (int)(sdl->pen_in_proximity ? sdl->pen_abs_y : sdl->mouse_abs_y); if (video_driver_translate_coord_viewport_wrap( - &vp, (int)sdl->mouse_abs_x, (int)sdl->mouse_abs_y, + &vp, abs_x, abs_y, &res_x, &res_y, &res_screen_x, &res_screen_y)) { switch (id) @@ -489,9 +529,29 @@ static SDL_Window *sdl3_input_window(void) return NULL; } -static void sdl3_poll_mouse(sdl3_input_t *sdl) +/* SDL reports mouse and pen coordinates in window coordinates + * (points), while the video driver's viewport metrics are in + * output pixels. */ +static float sdl3_window_pixel_density(void) { SDL_Window *win; + + if (!(win = sdl3_input_window())) + win = SDL_GetMouseFocus(); + + if (win) + { + float density = SDL_GetWindowPixelDensity(win); + if (density > 0.0f) + return density; + } + + return 1.0f; +} + +static void sdl3_poll_mouse(sdl3_input_t *sdl) +{ + float density; float dx = 0.0f; float dy = 0.0f; SDL_MouseButtonFlags btn = SDL_GetMouseState(&sdl->mouse_abs_x, &sdl->mouse_abs_y); @@ -513,20 +573,9 @@ static void sdl3_poll_mouse(sdl3_input_t *sdl) sdl->mouse_rel_x -= (float)sdl->mouse_x; sdl->mouse_rel_y -= (float)sdl->mouse_y; - /* SDL reports mouse coordinates in window coordinates (points), - * while the video driver's viewport metrics are in output pixels. */ - if (!(win = sdl3_input_window())) - win = SDL_GetMouseFocus(); - - if (win) - { - float density = SDL_GetWindowPixelDensity(win); - if (density > 0.0f && density != 1.0f) - { - sdl->mouse_abs_x *= density; - sdl->mouse_abs_y *= density; - } - } + density = sdl3_window_pixel_density(); + sdl->mouse_abs_x *= density; + sdl->mouse_abs_y *= density; sdl->mouse_l = (SDL_BUTTON_MASK(SDL_BUTTON_LEFT) & btn) != 0; sdl->mouse_r = (SDL_BUTTON_MASK(SDL_BUTTON_RIGHT) & btn) != 0; @@ -568,6 +617,12 @@ static void sdl3_poll_touch(sdl3_input_t *sdl) int j, num_fingers = 0; SDL_Finger **fingers; + /* The virtual touch device SDL synthesizes from pen contacts. + * The pen is read separately from its own events, so counting + * these fingers too would register every stylus tap twice. */ + if (devices[i] == SDL_PEN_TOUCHID) + continue; + /* Only SDL_TOUCH_DEVICE_DIRECT is a touchscreen. The two indirect * types are trackpads, whose fingers are device or cursor-relative. */ if (SDL_GetTouchDeviceType(devices[i]) != SDL_TOUCH_DEVICE_DIRECT) @@ -598,6 +653,70 @@ static void sdl3_poll_touch(sdl3_input_t *sdl) sdl->num_touch_devices = num_direct; } +/* Consume the pen event range. Unlike fingers there is no polled + * counterpart to read instead (SDL keeps per-pen axis state but has + * no query API for it), so this tracks the events by hand. */ +static void sdl3_poll_pen(sdl3_input_t *sdl) +{ + SDL_Event event; + float density; + + while (SDL_PeepEvents(&event, 1, SDL_GETEVENT, + SDL_EVENT_PEN_PROXIMITY_IN, SDL_EVENT_PEN_AXIS) > 0) + { + switch (event.type) + { + case SDL_EVENT_PEN_PROXIMITY_IN: + sdl->pen_in_proximity = true; + break; + case SDL_EVENT_PEN_PROXIMITY_OUT: + /* Everything is cleared so nothing keeps acting on stale + * button or pressure state once the pen leaves hover + * range; the position is kept as a last-known-good. */ + sdl->pen_in_proximity = false; + sdl->pen_down = false; + sdl->pen_eraser = false; + sdl->pen_buttons = 0; + memset(sdl->pen_axes, 0, sizeof(sdl->pen_axes)); + break; + case SDL_EVENT_PEN_DOWN: + case SDL_EVENT_PEN_UP: + sdl->pen_down = event.ptouch.down; + sdl->pen_eraser = event.ptouch.eraser; + sdl->pen_raw_x = event.ptouch.x; + sdl->pen_raw_y = event.ptouch.y; + break; + case SDL_EVENT_PEN_MOTION: + sdl->pen_raw_x = event.pmotion.x; + sdl->pen_raw_y = event.pmotion.y; + break; + case SDL_EVENT_PEN_BUTTON_DOWN: + case SDL_EVENT_PEN_BUTTON_UP: + /* Buttons are numbered from 1. */ + if (event.pbutton.button >= 1 && event.pbutton.button <= 8) + { + uint8_t bit = 1 << (event.pbutton.button - 1); + if (event.pbutton.down) + sdl->pen_buttons |= bit; + else + sdl->pen_buttons &= ~bit; + } + sdl->pen_raw_x = event.pbutton.x; + sdl->pen_raw_y = event.pbutton.y; + break; + case SDL_EVENT_PEN_AXIS: + if ((int)event.paxis.axis >= 0 + && (int)event.paxis.axis < SDL_PEN_AXIS_COUNT) + sdl->pen_axes[event.paxis.axis] = event.paxis.value; + break; + } + } + + density = sdl3_window_pixel_density(); + sdl->pen_abs_x = sdl->pen_raw_x * density; + sdl->pen_abs_y = sdl->pen_raw_y * density; +} + static uint16_t sdl3_translate_mod(SDL_Keymod smod) { uint16_t mod = 0; @@ -685,14 +804,21 @@ static void sdl3_input_poll(void *data) sdl3_build_scancode_lut(sdl); } - /* Neither range is consumed anywhere: sdl3_poll_touch reads finger - * state by polling instead of by event, and pens aren't wired up at - * all. Both fire at device rate for as long as there's contact, so - * left in the queue they grow until SDL's queue fills and starts - * refusing pushes - at which point the events that do matter (quit, - * keys) get dropped along with them. */ - SDL_FlushEvents(SDL_EVENT_FINGER_DOWN, SDL_EVENT_FINGER_CANCELED); - SDL_FlushEvents(SDL_EVENT_PEN_PROXIMITY_IN, SDL_EVENT_PEN_AXIS); + /* Finger events aren't consumed anywhere: sdl3_poll_touch reads + * finger state by polling instead of by event. They fire at device + * rate for as long as there's contact, so left in the queue they + * grow until SDL's queue fills and starts refusing pushes - at + * which point the events that do matter (quit, keys) get dropped + * along with them. */ + SDL_FlushEvents(SDL_EVENT_FINGER_DOWN, SDL_EVENT_FINGER_CANCELED); + + sdl3_poll_pen(sdl); + + /* The barrel buttons double as right/middle mouse buttons so the + * stock mouse and lightgun binds can reach them; SDL's own + * pen-to-mouse synthesis only covers the tip. */ + sdl->mouse_r |= (sdl->pen_buttons & 1) != 0; + sdl->mouse_m |= (sdl->pen_buttons & 2) != 0; } static void sdl3_grab_mouse(void *data, bool state) From b0a6ab241b850737110ae03ea73cdc6d5e0c7eba Mon Sep 17 00:00:00 2001 From: Rob Loach Date: Wed, 12 Aug 2026 21:36:43 -0400 Subject: [PATCH 2/7] sdl3: Report pen as pointer input only --- input/drivers/sdl3_input.c | 67 +++++++++----------------------------- 1 file changed, 16 insertions(+), 51 deletions(-) diff --git a/input/drivers/sdl3_input.c b/input/drivers/sdl3_input.c index ca569551ae13..159286ece295 100644 --- a/input/drivers/sdl3_input.c +++ b/input/drivers/sdl3_input.c @@ -76,23 +76,19 @@ typedef struct sdl3_input float y; } touches[SDL3_MAX_TOUCH]; - /* Pen/stylus state, fed by the SDL_EVENT_PEN_* range. Multiple - * pens are merged into one: last writer wins. */ + /* Pen/stylus state, fed by the SDL_EVENT_PEN_* range. The pen is + * reported as libretro pointer input, so only what the pointer + * needs is kept: whether it's in range, whether it's touching, + * and where. Multiple pens are merged into one: last writer + * wins. */ bool pen_in_proximity; bool pen_down; - /* The contact is the eraser end of the pen. */ - bool pen_eraser; - /* Barrel button state, bit (button - 1). */ - uint8_t pen_buttons; /* Last reported position in window coordinates (points). */ float pen_raw_x; float pen_raw_y; /* Position in output pixels, matching mouse_abs_*. */ float pen_abs_x; float pen_abs_y; - /* SDL_PEN_AXIS_* values (pressure, tilt, ...). Nothing maps them - * onto the libretro API yet; tracked so a later mapping has them. */ - float pen_axes[SDL_PEN_AXIS_COUNT]; } sdl3_input_t; /* Rebuilt on SDL_EVENT_KEYMAP_CHANGED (e.g. system layout switch). */ @@ -386,13 +382,9 @@ static int16_t sdl3_input_state( int16_t res_y = 0; int16_t res_screen_x = 0; int16_t res_screen_y = 0; - /* A pen in range aims the gun; the mouse state SDL - * synthesizes from it tracks the same spot anyway. */ - int abs_x = (int)(sdl->pen_in_proximity ? sdl->pen_abs_x : sdl->mouse_abs_x); - int abs_y = (int)(sdl->pen_in_proximity ? sdl->pen_abs_y : sdl->mouse_abs_y); if (video_driver_translate_coord_viewport_wrap( - &vp, abs_x, abs_y, + &vp, (int)sdl->mouse_abs_x, (int)sdl->mouse_abs_y, &res_x, &res_y, &res_screen_x, &res_screen_y)) { switch (id) @@ -654,8 +646,10 @@ static void sdl3_poll_touch(sdl3_input_t *sdl) } /* Consume the pen event range. Unlike fingers there is no polled - * counterpart to read instead (SDL keeps per-pen axis state but has - * no query API for it), so this tracks the events by hand. */ + * counterpart to read instead, so this tracks the events by hand. + * Only proximity, contact and position are kept - the rest of the + * range (barrel buttons, pressure and the other axes) has nothing + * to map onto the pointer, and is peeped purely to drain it. */ static void sdl3_poll_pen(sdl3_input_t *sdl) { SDL_Event event; @@ -670,45 +664,22 @@ static void sdl3_poll_pen(sdl3_input_t *sdl) sdl->pen_in_proximity = true; break; case SDL_EVENT_PEN_PROXIMITY_OUT: - /* Everything is cleared so nothing keeps acting on stale - * button or pressure state once the pen leaves hover - * range; the position is kept as a last-known-good. */ + /* Contact is cleared so nothing keeps reading the pointer + * as pressed once the pen leaves hover range; the position + * is kept as a last-known-good. */ sdl->pen_in_proximity = false; sdl->pen_down = false; - sdl->pen_eraser = false; - sdl->pen_buttons = 0; - memset(sdl->pen_axes, 0, sizeof(sdl->pen_axes)); break; case SDL_EVENT_PEN_DOWN: case SDL_EVENT_PEN_UP: - sdl->pen_down = event.ptouch.down; - sdl->pen_eraser = event.ptouch.eraser; - sdl->pen_raw_x = event.ptouch.x; - sdl->pen_raw_y = event.ptouch.y; + sdl->pen_down = event.ptouch.down; + sdl->pen_raw_x = event.ptouch.x; + sdl->pen_raw_y = event.ptouch.y; break; case SDL_EVENT_PEN_MOTION: sdl->pen_raw_x = event.pmotion.x; sdl->pen_raw_y = event.pmotion.y; break; - case SDL_EVENT_PEN_BUTTON_DOWN: - case SDL_EVENT_PEN_BUTTON_UP: - /* Buttons are numbered from 1. */ - if (event.pbutton.button >= 1 && event.pbutton.button <= 8) - { - uint8_t bit = 1 << (event.pbutton.button - 1); - if (event.pbutton.down) - sdl->pen_buttons |= bit; - else - sdl->pen_buttons &= ~bit; - } - sdl->pen_raw_x = event.pbutton.x; - sdl->pen_raw_y = event.pbutton.y; - break; - case SDL_EVENT_PEN_AXIS: - if ((int)event.paxis.axis >= 0 - && (int)event.paxis.axis < SDL_PEN_AXIS_COUNT) - sdl->pen_axes[event.paxis.axis] = event.paxis.value; - break; } } @@ -813,12 +784,6 @@ static void sdl3_input_poll(void *data) SDL_FlushEvents(SDL_EVENT_FINGER_DOWN, SDL_EVENT_FINGER_CANCELED); sdl3_poll_pen(sdl); - - /* The barrel buttons double as right/middle mouse buttons so the - * stock mouse and lightgun binds can reach them; SDL's own - * pen-to-mouse synthesis only covers the tip. */ - sdl->mouse_r |= (sdl->pen_buttons & 1) != 0; - sdl->mouse_m |= (sdl->pen_buttons & 2) != 0; } static void sdl3_grab_mouse(void *data, bool state) From a66335cbeca1dd58717f8b2244adbe0aabb5aa8e Mon Sep 17 00:00:00 2001 From: Rob Loach Date: Thu, 13 Aug 2026 21:15:22 -0400 Subject: [PATCH 3/7] Skip pen density lookup out of range --- input/drivers/sdl3_input.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/input/drivers/sdl3_input.c b/input/drivers/sdl3_input.c index b146e6bc95c1..9887f4d494ba 100644 --- a/input/drivers/sdl3_input.c +++ b/input/drivers/sdl3_input.c @@ -683,6 +683,11 @@ static void sdl3_poll_pen(sdl3_input_t *sdl) } } + /* Nothing reads pen_abs_* outside proximity, so pen-less setups + * (the common case) skip the per-frame density lookup. */ + if (!sdl->pen_in_proximity) + return; + density = sdl3_window_pixel_density(); sdl->pen_abs_x = sdl->pen_raw_x * density; sdl->pen_abs_y = sdl->pen_raw_y * density; From 953d11014ac2d39c2794adc03a2bb22e087de2c6 Mon Sep 17 00:00:00 2001 From: Rob Loach Date: Fri, 14 Aug 2026 01:10:09 -0400 Subject: [PATCH 4/7] Cache SDL_Window in sdl3_input_t --- input/drivers/sdl3_input.c | 39 ++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/input/drivers/sdl3_input.c b/input/drivers/sdl3_input.c index 9887f4d494ba..046908943651 100644 --- a/input/drivers/sdl3_input.c +++ b/input/drivers/sdl3_input.c @@ -89,6 +89,12 @@ typedef struct sdl3_input /* Position in output pixels, matching mouse_abs_*. */ float pen_abs_x; float pen_abs_y; + + /* The SDL_Window input is read against, refreshed once per poll + * (see sdl3_input_poll). Never dereferenced across frames, so a + * window destroyed on a driver switch can't dangle past the next + * refresh. */ + SDL_Window *window; } sdl3_input_t; /* Rebuilt on SDL_EVENT_KEYMAP_CHANGED (e.g. system layout switch). */ @@ -524,16 +530,11 @@ static SDL_Window *sdl3_input_window(void) /* SDL reports mouse and pen coordinates in window coordinates * (points), while the video driver's viewport metrics are in * output pixels. */ -static float sdl3_window_pixel_density(void) +static float sdl3_window_pixel_density(sdl3_input_t *sdl) { - SDL_Window *win; - - if (!(win = sdl3_input_window())) - win = SDL_GetMouseFocus(); - - if (win) + if (sdl->window) { - float density = SDL_GetWindowPixelDensity(win); + float density = SDL_GetWindowPixelDensity(sdl->window); if (density > 0.0f) return density; } @@ -565,7 +566,7 @@ static void sdl3_poll_mouse(sdl3_input_t *sdl) sdl->mouse_rel_x -= (float)sdl->mouse_x; sdl->mouse_rel_y -= (float)sdl->mouse_y; - density = sdl3_window_pixel_density(); + density = sdl3_window_pixel_density(sdl); sdl->mouse_abs_x *= density; sdl->mouse_abs_y *= density; @@ -688,7 +689,7 @@ static void sdl3_poll_pen(sdl3_input_t *sdl) if (!sdl->pen_in_proximity) return; - density = sdl3_window_pixel_density(); + density = sdl3_window_pixel_density(sdl); sdl->pen_abs_x = sdl->pen_raw_x * density; sdl->pen_abs_y = sdl->pen_raw_y * density; } @@ -768,6 +769,11 @@ static void sdl3_input_poll(void *data) * never updates. */ SDL_PumpEvents(); + /* Cache the window for the frame: the mouse and pen coordinate + * scaling both read it. */ + if (!(sdl->window = sdl3_input_window())) + sdl->window = SDL_GetMouseFocus(); + sdl3_poll_mouse(sdl); sdl3_poll_touch(sdl); @@ -829,12 +835,13 @@ static void sdl3_input_poll(void *data) sdl3_build_scancode_lut(sdl); } - /* Finger events aren't consumed anywhere: sdl3_poll_touch reads - * finger state by polling instead of by event. They fire at device - * rate for as long as there's contact, so left in the queue they - * grow until SDL's queue fills and starts refusing pushes - at - * which point the events that do matter (quit, keys) get dropped - * along with them. */ + /* Fingers are reported as pointer input, but from polled state + * (sdl3_poll_touch / SDL_GetTouchFingers) rather than from these + * events, so the event range itself is consumed nowhere. The + * events fire at device rate for as long as there's contact, so + * left in the queue they grow until SDL's queue fills and starts + * refusing pushes - at which point the events that do matter + * (quit, keys) get dropped along with them. */ SDL_FlushEvents(SDL_EVENT_FINGER_DOWN, SDL_EVENT_FINGER_CANCELED); sdl3_poll_pen(sdl); From e3b607965c0c8b64b317bdde42f70814d491d61e Mon Sep 17 00:00:00 2001 From: Rob Loach Date: Thu, 3 Sep 2026 15:13:11 -0400 Subject: [PATCH 5/7] sdl3: Update pen comments --- input/drivers/sdl3_input.c | 59 +++++++++++--------------------------- 1 file changed, 17 insertions(+), 42 deletions(-) diff --git a/input/drivers/sdl3_input.c b/input/drivers/sdl3_input.c index c32dfc2e800c..36c6c073e942 100644 --- a/input/drivers/sdl3_input.c +++ b/input/drivers/sdl3_input.c @@ -76,11 +76,7 @@ typedef struct sdl3_input float y; } touches[SDL3_MAX_TOUCH]; - /* Pen/stylus state, fed by the SDL_EVENT_PEN_* range. The pen is - * reported as libretro pointer input, so only what the pointer - * needs is kept: whether it's in range, whether it's touching, - * and where. Multiple pens are merged into one: last writer - * wins. */ + /* Pen/stylus state, handled through SDL_EVENT_PEN_*. */ bool pen_in_proximity; bool pen_down; /* Last reported position in window coordinates (points). */ @@ -90,10 +86,7 @@ typedef struct sdl3_input float pen_abs_x; float pen_abs_y; - /* The SDL_Window input is read against, refreshed once per poll - * (see sdl3_input_poll). Never dereferenced across frames, so a - * window destroyed on a driver switch can't dangle past the next - * refresh. */ + /* The SDL_Window input is read against. */ SDL_Window *window; } sdl3_input_t; @@ -329,14 +322,10 @@ static int16_t sdl3_input_state( } else if (sdl->pen_in_proximity) { - /* While the pen is in range SDL also synthesizes mouse - * state from it (SDL_PEN_MOUSEID), so taking the pen - * ahead of the mouse fallback doubles as the filter - * that keeps one stylus tap from registering twice. */ if (idx != 0) return 0; - abs_x = (int)sdl->pen_abs_x; - abs_y = (int)sdl->pen_abs_y; + abs_x = (int)sdl->pen_abs_x; + abs_y = (int)sdl->pen_abs_y; pressed = sdl->pen_down; } else @@ -527,9 +516,9 @@ static SDL_Window *sdl3_input_window(void) return NULL; } -/* SDL reports mouse and pen coordinates in window coordinates - * (points), while the video driver's viewport metrics are in - * output pixels. */ +/* SDL reports mouse and pen coordinates in window coordinates, + * while the video driver's viewport metrics are in output + * pixels. */ static float sdl3_window_pixel_density(sdl3_input_t *sdl) { if (sdl->window) @@ -610,9 +599,7 @@ static void sdl3_poll_touch(sdl3_input_t *sdl) int j, num_fingers = 0; SDL_Finger **fingers; - /* The virtual touch device SDL synthesizes from pen contacts. - * The pen is read separately from its own events, so counting - * these fingers too would register every stylus tap twice. */ + /* Pen events are read elsewhere. */ if (devices[i] == SDL_PEN_TOUCHID) continue; @@ -646,11 +633,7 @@ static void sdl3_poll_touch(sdl3_input_t *sdl) sdl->num_touch_devices = num_direct; } -/* Consume the pen event range. Unlike fingers there is no polled - * counterpart to read instead, so this tracks the events by hand. - * Only proximity, contact and position are kept - the rest of the - * range (barrel buttons, pressure and the other axes) has nothing - * to map onto the pointer, and is peeped purely to drain it. */ +/* Polls the pen events. */ static void sdl3_poll_pen(sdl3_input_t *sdl) { SDL_Event event; @@ -665,17 +648,14 @@ static void sdl3_poll_pen(sdl3_input_t *sdl) sdl->pen_in_proximity = true; break; case SDL_EVENT_PEN_PROXIMITY_OUT: - /* Contact is cleared so nothing keeps reading the pointer - * as pressed once the pen leaves hover range; the position - * is kept as a last-known-good. */ sdl->pen_in_proximity = false; - sdl->pen_down = false; + sdl->pen_down = false; break; case SDL_EVENT_PEN_DOWN: case SDL_EVENT_PEN_UP: - sdl->pen_down = event.ptouch.down; sdl->pen_raw_x = event.ptouch.x; sdl->pen_raw_y = event.ptouch.y; + sdl->pen_down = event.ptouch.down; break; case SDL_EVENT_PEN_MOTION: sdl->pen_raw_x = event.pmotion.x; @@ -684,8 +664,7 @@ static void sdl3_poll_pen(sdl3_input_t *sdl) } } - /* Nothing reads pen_abs_* outside proximity, so pen-less setups - * (the common case) skip the per-frame density lookup. */ + /* If the pen isn't in proximity, skip calculating its position. */ if (!sdl->pen_in_proximity) return; @@ -769,8 +748,8 @@ static void sdl3_input_poll(void *data) * never updates. */ SDL_PumpEvents(); - /* Cache the window for the frame: the mouse and pen coordinate - * scaling both read it. */ + /* Find the SDL window, so that window coordinates can be calculated + * properly. */ if (!(sdl->window = sdl3_input_window())) sdl->window = SDL_GetMouseFocus(); @@ -835,13 +814,9 @@ static void sdl3_input_poll(void *data) sdl3_build_scancode_lut(sdl); } - /* Fingers are reported as pointer input, but from polled state - * (sdl3_poll_touch / SDL_GetTouchFingers) rather than from these - * events, so the event range itself is consumed nowhere. The - * events fire at device rate for as long as there's contact, so - * left in the queue they grow until SDL's queue fills and starts - * refusing pushes - at which point the events that do matter - * (quit, keys) get dropped along with them. */ + /* Fingers are reported as pointer input from polled state + * (sdl3_poll_touch / SDL_GetTouchFingers), rather than these + * events, so flush the finger events. */ SDL_FlushEvents(SDL_EVENT_FINGER_DOWN, SDL_EVENT_FINGER_CANCELED); sdl3_poll_pen(sdl); From 10428189420743db6b04a3fabd8744f6e496f5e9 Mon Sep 17 00:00:00 2001 From: Rob Loach Date: Thu, 3 Sep 2026 15:14:12 -0400 Subject: [PATCH 6/7] sdl3: infer pen proximity from events --- input/drivers/sdl3_input.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/input/drivers/sdl3_input.c b/input/drivers/sdl3_input.c index 36c6c073e942..0264e034ccbb 100644 --- a/input/drivers/sdl3_input.c +++ b/input/drivers/sdl3_input.c @@ -322,6 +322,10 @@ static int16_t sdl3_input_state( } else if (sdl->pen_in_proximity) { + /* Reading the pen ahead of the mouse fallback dedups + * the mouse state SDL synthesizes from the pen; a real + * mouse click during pen hover is indistinguishable + * from that and reads as unpressed. */ if (idx != 0) return 0; abs_x = (int)sdl->pen_abs_x; @@ -653,11 +657,17 @@ static void sdl3_poll_pen(sdl3_input_t *sdl) break; case SDL_EVENT_PEN_DOWN: case SDL_EVENT_PEN_UP: + /* Any pen event implies a pen in range; PROXIMITY_IN + * alone can be missed (flushed by a previous driver + * instance on a runtime driver switch, or a second pen + * leaving range clearing the shared flag). */ + sdl->pen_in_proximity = true; sdl->pen_raw_x = event.ptouch.x; sdl->pen_raw_y = event.ptouch.y; sdl->pen_down = event.ptouch.down; break; case SDL_EVENT_PEN_MOTION: + sdl->pen_in_proximity = true; sdl->pen_raw_x = event.pmotion.x; sdl->pen_raw_y = event.pmotion.y; break; From 4b3ab0af168c68215bb35aed233a1887754cb8b9 Mon Sep 17 00:00:00 2001 From: Rob Loach Date: Thu, 3 Sep 2026 15:20:50 -0400 Subject: [PATCH 7/7] sdl3: Unneeded comment --- input/drivers/sdl3_input.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/input/drivers/sdl3_input.c b/input/drivers/sdl3_input.c index 0264e034ccbb..ded7d61b332a 100644 --- a/input/drivers/sdl3_input.c +++ b/input/drivers/sdl3_input.c @@ -657,10 +657,6 @@ static void sdl3_poll_pen(sdl3_input_t *sdl) break; case SDL_EVENT_PEN_DOWN: case SDL_EVENT_PEN_UP: - /* Any pen event implies a pen in range; PROXIMITY_IN - * alone can be missed (flushed by a previous driver - * instance on a runtime driver switch, or a second pen - * leaving range clearing the shared flag). */ sdl->pen_in_proximity = true; sdl->pen_raw_x = event.ptouch.x; sdl->pen_raw_y = event.ptouch.y;