Browse Source

API for pressure-sensitive pens + XInput2/Wayland

This patch adds an API for querying pressure-
sensitive pens, cf. SDL_pen.h:
- Enumerate all pens
- Get pen capabilities, names, GUIDs
- Distinguishes pens and erasers
- Distinguish attached and detached pens
- Pressure and tilt support
- Rotation, distance, throttle wheel support
  (throttle wheel untested)
- Pen type and meta-information reporting
  (partially tested)

Pen event reporting:
- Three new event structures: PenTip, PenMotion, and
  PenButton
- Report location with sub-pixel precision
- Include axis and button status, is-eraser flag

Internal pen tracker, intended to be independent
of platform APIs, cf. SDL_pen_c.h:
- Track known pens
- Handle pen hotplugging

Automatic test:
- testautomation_pen.c

Other features:
- XInput2 implementation, incl. hotplugging
- Wayland implementation, incl. hotplugging
- Backward compatibility: pen events default to
  emulating pens with mouse ID SDL_PEN_MOUSEID
- Can be toggled via SDL_HINT_PEN_NOT_MOUSE
- Test/demo program (testpen)
- Wacom pen feature identification by pen ID

Acknowledgements:
- Ping Cheng (Wacom) provided extensive feedback
  on Wacom pen features and detection so that
  hopefully untested Wacom devices have a
  realistic chance of working out of the box.
Christoph Reichenbach 1 year ago
parent
commit
7c80ac6df7

+ 2 - 0
VisualC-GDK/SDL/SDL.vcxproj

@@ -330,6 +330,7 @@
     <ClInclude Include="..\..\include\SDL3\SDL_opengles2_gl2ext.h" />
     <ClInclude Include="..\..\include\SDL3\SDL_opengles2_gl2platform.h" />
     <ClInclude Include="..\..\include\SDL3\SDL_opengles2_khrplatform.h" />
+    <ClInclude Include="..\..\include\SDL3\SDL_pen.h" />
     <ClInclude Include="..\..\include\SDL3\SDL_pixels.h" />
     <ClInclude Include="..\..\include\SDL3\SDL_platform.h" />
     <ClInclude Include="..\..\include\SDL3\SDL_platform_defines.h" />
@@ -586,6 +587,7 @@
     <ClCompile Include="..\..\src\events\SDL_events.c" />
     <ClCompile Include="..\..\src\events\SDL_keyboard.c" />
     <ClCompile Include="..\..\src\events\SDL_mouse.c" />
+    <ClCompile Include="..\..\src\events\SDL_pen.c" />
     <ClCompile Include="..\..\src\events\SDL_quit.c" />
     <ClCompile Include="..\..\src\events\SDL_touch.c" />
     <ClCompile Include="..\..\src\events\SDL_windowevents.c" />

+ 6 - 0
VisualC-GDK/SDL/SDL.vcxproj.filters

@@ -300,6 +300,9 @@
     <ClInclude Include="..\..\include\SDL3\SDL_opengles2_khrplatform.h">
       <Filter>API Headers</Filter>
     </ClInclude>
+    <ClInclude Include="..\..\include\SDL3\SDL_pen.h">
+      <Filter>API Headers</Filter>
+    </ClInclude>
     <ClInclude Include="..\..\include\SDL3\SDL_pixels.h">
       <Filter>API Headers</Filter>
     </ClInclude>
@@ -940,6 +943,9 @@
     <ClCompile Include="..\..\src\events\SDL_mouse.c">
       <Filter>events</Filter>
     </ClCompile>
+    <ClCompile Include="..\..\src\events\SDL_pen.c">
+      <Filter>events</Filter>
+    </ClCompile>
     <ClCompile Include="..\..\src\events\SDL_quit.c">
       <Filter>events</Filter>
     </ClCompile>

+ 2 - 0
VisualC-WinRT/SDL-UWP.vcxproj

@@ -67,6 +67,7 @@
     <ClInclude Include="..\include\SDL3\SDL_mouse.h" />
     <ClInclude Include="..\include\SDL3\SDL_mutex.h" />
     <ClInclude Include="..\include\SDL3\SDL_opengles2.h" />
+    <ClInclude Include="..\include\SDL3\SDL_pen.h" />
     <ClInclude Include="..\include\SDL3\SDL_pixels.h" />
     <ClInclude Include="..\include\SDL3\SDL_platform.h" />
     <ClInclude Include="..\include\SDL3\SDL_platform_defines.h" />
@@ -306,6 +307,7 @@
     <ClCompile Include="..\src\events\SDL_events.c" />
     <ClCompile Include="..\src\events\SDL_keyboard.c" />
     <ClCompile Include="..\src\events\SDL_mouse.c" />
+    <ClCompile Include="..\src\events\SDL_pen.c" />
     <ClCompile Include="..\src\events\SDL_quit.c" />
     <ClCompile Include="..\src\events\SDL_touch.c" />
     <ClCompile Include="..\src\events\SDL_windowevents.c" />

+ 6 - 0
VisualC-WinRT/SDL-UWP.vcxproj.filters

@@ -105,6 +105,9 @@
     <ClInclude Include="..\include\SDL3\SDL_opengles2.h">
       <Filter>Header Files</Filter>
     </ClInclude>
+    <ClInclude Include="..\include\SDL3\SDL_pen.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
     <ClInclude Include="..\include\SDL3\SDL_pixels.h">
       <Filter>Header Files</Filter>
     </ClInclude>
@@ -552,6 +555,9 @@
     <ClCompile Include="..\src\events\SDL_mouse.c">
       <Filter>Source Files</Filter>
     </ClCompile>
+    <ClCompile Include="..\src\events\SDL_pen.c">
+      <Filter>Source Files</Filter>
+    </ClCompile>
     <ClCompile Include="..\src\events\SDL_quit.c">
       <Filter>Source Files</Filter>
     </ClCompile>

+ 2 - 0
VisualC/SDL/SDL.vcxproj

@@ -280,6 +280,7 @@
     <ClInclude Include="..\..\include\SDL3\SDL_opengles2_gl2ext.h" />
     <ClInclude Include="..\..\include\SDL3\SDL_opengles2_gl2platform.h" />
     <ClInclude Include="..\..\include\SDL3\SDL_opengles2_khrplatform.h" />
+    <ClInclude Include="..\..\include\SDL3\SDL_pen.h" />
     <ClInclude Include="..\..\include\SDL3\SDL_pixels.h" />
     <ClInclude Include="..\..\include\SDL3\SDL_platform.h" />
     <ClInclude Include="..\..\include\SDL3\SDL_platform_defines.h" />
@@ -507,6 +508,7 @@
     <ClCompile Include="..\..\src\events\SDL_events.c" />
     <ClCompile Include="..\..\src\events\SDL_keyboard.c" />
     <ClCompile Include="..\..\src\events\SDL_mouse.c" />
+    <ClCompile Include="..\..\src\events\SDL_pen.c" />
     <ClCompile Include="..\..\src\events\SDL_quit.c" />
     <ClCompile Include="..\..\src\events\SDL_touch.c" />
     <ClCompile Include="..\..\src\events\SDL_windowevents.c" />

+ 6 - 0
VisualC/SDL/SDL.vcxproj.filters

@@ -294,6 +294,9 @@
     <ClInclude Include="..\..\include\SDL3\SDL_opengles2_khrplatform.h">
       <Filter>API Headers</Filter>
     </ClInclude>
+    <ClInclude Include="..\..\include\SDL3\SDL_pen.h">
+      <Filter>API Headers</Filter>
+    </ClInclude>
     <ClInclude Include="..\..\include\SDL3\SDL_pixels.h">
       <Filter>API Headers</Filter>
     </ClInclude>
@@ -921,6 +924,9 @@
     <ClCompile Include="..\..\src\events\SDL_mouse.c">
       <Filter>events</Filter>
     </ClCompile>
+    <ClCompile Include="..\..\src\events\SDL_pen.c">
+      <Filter>events</Filter>
+    </ClCompile>
     <ClCompile Include="..\..\src\events\SDL_quit.c">
       <Filter>events</Filter>
     </ClCompile>

+ 6 - 0
VisualC/tests/testautomation/testautomation.vcxproj

@@ -209,6 +209,12 @@
     <ClCompile Include="..\..\..\test\testautomation_main.c" />
     <ClCompile Include="..\..\..\test\testautomation_math.c" />
     <ClCompile Include="..\..\..\test\testautomation_mouse.c" />
+    <ClCompile Include="..\..\..\test\testautomation_pen.c">
+      <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">$(ProjectDir)\..\..\..\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
+      <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(ProjectDir)\..\..\..\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
+      <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(ProjectDir)\..\..\..\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
+      <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(ProjectDir)\..\..\..\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
+    </ClCompile>
     <ClCompile Include="..\..\..\test\testautomation_pixels.c" />
     <ClCompile Include="..\..\..\test\testautomation_platform.c" />
     <ClCompile Include="..\..\..\test\testautomation_properties.c" />

+ 33 - 0
Xcode/SDL/SDL.xcodeproj/project.pbxproj

@@ -43,6 +43,27 @@
 		00D0D0D810675E46004B05EF /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 007317C10858E15000B2BC32 /* Carbon.framework */; platformFilters = (macos, ); };
 		557D0CFA254586CA003913E3 /* CoreHaptics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F37DC5F225350EBC0002E6F7 /* CoreHaptics.framework */; platformFilters = (ios, maccatalyst, macos, tvos, ); };
 		557D0CFB254586D7003913E3 /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A75FDABD23E28B6200529352 /* GameController.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
+		63125C002A790B12008EF011 /* SDL_pen.h in Headers */ = {isa = PBXBuildFile; fileRef = 63125BFF2A790B12008EF011 /* SDL_pen.h */; };
+		63125C012A790B12008EF011 /* SDL_pen.h in Headers */ = {isa = PBXBuildFile; fileRef = 63125BFF2A790B12008EF011 /* SDL_pen.h */; };
+		63125C022A790B12008EF011 /* SDL_pen.h in Headers */ = {isa = PBXBuildFile; fileRef = 63125BFF2A790B12008EF011 /* SDL_pen.h */; };
+		63125C0A2A790B69008EF011 /* SDL_pen.c in Sources */ = {isa = PBXBuildFile; fileRef = 63125C092A790B69008EF011 /* SDL_pen.c */; };
+		63125C0B2A790B69008EF011 /* SDL_pen.c in Sources */ = {isa = PBXBuildFile; fileRef = 63125C092A790B69008EF011 /* SDL_pen.c */; };
+		63125C0C2A790B69008EF011 /* SDL_pen.c in Sources */ = {isa = PBXBuildFile; fileRef = 63125C092A790B69008EF011 /* SDL_pen.c */; };
+		63125C0D2A790B69008EF011 /* SDL_pen.c in Sources */ = {isa = PBXBuildFile; fileRef = 63125C092A790B69008EF011 /* SDL_pen.c */; };
+		63125C0E2A790B69008EF011 /* SDL_pen.c in Sources */ = {isa = PBXBuildFile; fileRef = 63125C092A790B69008EF011 /* SDL_pen.c */; };
+		63125C0F2A790B69008EF011 /* SDL_pen.c in Sources */ = {isa = PBXBuildFile; fileRef = 63125C092A790B69008EF011 /* SDL_pen.c */; };
+		63125C102A790B69008EF011 /* SDL_pen.c in Sources */ = {isa = PBXBuildFile; fileRef = 63125C092A790B69008EF011 /* SDL_pen.c */; };
+		63125C112A790B69008EF011 /* SDL_pen.c in Sources */ = {isa = PBXBuildFile; fileRef = 63125C092A790B69008EF011 /* SDL_pen.c */; };
+		63125C122A790B69008EF011 /* SDL_pen.c in Sources */ = {isa = PBXBuildFile; fileRef = 63125C092A790B69008EF011 /* SDL_pen.c */; };
+		63125C142A790B9A008EF011 /* SDL_pen_c.h in Headers */ = {isa = PBXBuildFile; fileRef = 63125C132A790B9A008EF011 /* SDL_pen_c.h */; };
+		63125C152A790B9A008EF011 /* SDL_pen_c.h in Headers */ = {isa = PBXBuildFile; fileRef = 63125C132A790B9A008EF011 /* SDL_pen_c.h */; };
+		63125C162A790B9A008EF011 /* SDL_pen_c.h in Headers */ = {isa = PBXBuildFile; fileRef = 63125C132A790B9A008EF011 /* SDL_pen_c.h */; };
+		63125C172A790B9A008EF011 /* SDL_pen_c.h in Headers */ = {isa = PBXBuildFile; fileRef = 63125C132A790B9A008EF011 /* SDL_pen_c.h */; };
+		63125C182A790B9A008EF011 /* SDL_pen_c.h in Headers */ = {isa = PBXBuildFile; fileRef = 63125C132A790B9A008EF011 /* SDL_pen_c.h */; };
+		63125C192A790B9A008EF011 /* SDL_pen_c.h in Headers */ = {isa = PBXBuildFile; fileRef = 63125C132A790B9A008EF011 /* SDL_pen_c.h */; };
+		63125C1A2A790B9A008EF011 /* SDL_pen_c.h in Headers */ = {isa = PBXBuildFile; fileRef = 63125C132A790B9A008EF011 /* SDL_pen_c.h */; };
+		63125C1B2A790B9A008EF011 /* SDL_pen_c.h in Headers */ = {isa = PBXBuildFile; fileRef = 63125C132A790B9A008EF011 /* SDL_pen_c.h */; };
+		63125C1C2A790B9A008EF011 /* SDL_pen_c.h in Headers */ = {isa = PBXBuildFile; fileRef = 63125C132A790B9A008EF011 /* SDL_pen_c.h */; };
 		5616CA4C252BB2A6005D5928 /* SDL_url.c in Sources */ = {isa = PBXBuildFile; fileRef = 5616CA49252BB2A5005D5928 /* SDL_url.c */; };
 		5616CA4D252BB2A6005D5928 /* SDL_sysurl.h in Headers */ = {isa = PBXBuildFile; fileRef = 5616CA4A252BB2A6005D5928 /* SDL_sysurl.h */; };
 		5616CA4E252BB2A6005D5928 /* SDL_sysurl.m in Sources */ = {isa = PBXBuildFile; fileRef = 5616CA4B252BB2A6005D5928 /* SDL_sysurl.m */; };
@@ -52,6 +73,9 @@
 		566E26D8246274CC00718109 /* SDL_locale.c in Sources */ = {isa = PBXBuildFile; fileRef = 566E26CD246274CB00718109 /* SDL_locale.c */; };
 		566E26E1246274CC00718109 /* SDL_syslocale.h in Headers */ = {isa = PBXBuildFile; fileRef = 566E26CE246274CC00718109 /* SDL_syslocale.h */; };
 		56A2373329F9C113003CCA5F /* SDL_sysrwlock.c in Sources */ = {isa = PBXBuildFile; fileRef = 56A2373229F9C113003CCA5F /* SDL_sysrwlock.c */; };
+		63134A222A7902CF0021E9A6 /* SDL_pen.h in Headers */ = {isa = PBXBuildFile; fileRef = 63134A212A7902CF0021E9A6 /* SDL_pen.h */; };
+		63134A252A7902FD0021E9A6 /* SDL_pen_c.h in Headers */ = {isa = PBXBuildFile; fileRef = 63134A232A7902FD0021E9A6 /* SDL_pen_c.h */; };
+		63134A262A7902FD0021E9A6 /* SDL_pen.c in Sources */ = {isa = PBXBuildFile; fileRef = 63134A242A7902FD0021E9A6 /* SDL_pen.c */; };
 		75E0915A241EA924004729E1 /* SDL_virtualjoystick.c in Sources */ = {isa = PBXBuildFile; fileRef = 75E09158241EA924004729E1 /* SDL_virtualjoystick.c */; };
 		75E09163241EA924004729E1 /* SDL_virtualjoystick_c.h in Headers */ = {isa = PBXBuildFile; fileRef = 75E09159241EA924004729E1 /* SDL_virtualjoystick_c.h */; };
 		9846B07C287A9020000C35C8 /* SDL_hidapi_shield.c in Sources */ = {isa = PBXBuildFile; fileRef = 9846B07B287A9020000C35C8 /* SDL_hidapi_shield.c */; };
@@ -534,6 +558,9 @@
 		566E26CD246274CB00718109 /* SDL_locale.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = SDL_locale.c; path = locale/SDL_locale.c; sourceTree = "<group>"; };
 		566E26CE246274CC00718109 /* SDL_syslocale.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SDL_syslocale.h; path = locale/SDL_syslocale.h; sourceTree = "<group>"; };
 		56A2373229F9C113003CCA5F /* SDL_sysrwlock.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_sysrwlock.c; sourceTree = "<group>"; };
+		63134A212A7902CF0021E9A6 /* SDL_pen.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SDL_pen.h; path = SDL3/SDL_pen.h; sourceTree = "<group>"; };
+		63134A232A7902FD0021E9A6 /* SDL_pen_c.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_pen_c.h; sourceTree = "<group>"; };
+		63134A242A7902FD0021E9A6 /* SDL_pen.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_pen.c; sourceTree = "<group>"; };
 		75E09158241EA924004729E1 /* SDL_virtualjoystick.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_virtualjoystick.c; sourceTree = "<group>"; };
 		75E09159241EA924004729E1 /* SDL_virtualjoystick_c.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_virtualjoystick_c.h; sourceTree = "<group>"; };
 		9846B07B287A9020000C35C8 /* SDL_hidapi_shield.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_shield.c; sourceTree = "<group>"; };
@@ -1093,6 +1120,7 @@
 				F3F7D8C92933074B00816151 /* SDL_opengles2_gl2platform.h */,
 				F3F7D8B12933074900816151 /* SDL_opengles2_khrplatform.h */,
 				F3F7D8C72933074B00816151 /* SDL_opengles2.h */,
+				63134A212A7902CF0021E9A6 /* SDL_pen.h */,
 				F3F7D8B52933074A00816151 /* SDL_pixels.h */,
 				F3B38CCB296E2E52005DA6D3 /* SDL_platform_defines.h */,
 				F3F7D8AB2933074900816151 /* SDL_platform.h */,
@@ -2059,6 +2087,8 @@
 				A7D8A93823E2514000DCD162 /* SDL_keyboard.c */,
 				A7D8A92B23E2514000DCD162 /* SDL_mouse_c.h */,
 				A7D8A92A23E2514000DCD162 /* SDL_mouse.c */,
+				63134A232A7902FD0021E9A6 /* SDL_pen_c.h */,
+				63134A242A7902FD0021E9A6 /* SDL_pen.c */,
 				A7D8A93C23E2514000DCD162 /* SDL_quit.c */,
 				A7D8A93723E2514000DCD162 /* SDL_touch_c.h */,
 				A7D8A93E23E2514000DCD162 /* SDL_touch.c */,
@@ -2365,6 +2395,8 @@
 				A7D8B3D423E2514300DCD162 /* yuv_rgb.h in Headers */,
 				A7D8B3C823E2514200DCD162 /* yuv_rgb_sse_func.h in Headers */,
 				A7D8B3CE23E2514300DCD162 /* yuv_rgb_std_func.h in Headers */,
+				63134A222A7902CF0021E9A6 /* SDL_pen.h in Headers */,
+				63134A252A7902FD0021E9A6 /* SDL_pen_c.h in Headers */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -2685,6 +2717,7 @@
 				A7D8AEA023E2514100DCD162 /* SDL_cocoavulkan.m in Sources */,
 				A7D8AB6123E2514100DCD162 /* SDL_offscreenwindow.c in Sources */,
 				566E26D8246274CC00718109 /* SDL_locale.c in Sources */,
+				63134A262A7902FD0021E9A6 /* SDL_pen.c in Sources */,
 				000040E76FDC6AE48CBF0000 /* SDL_hashtable.c in Sources */,
 				0000A4DA2F45A31DC4F00000 /* SDL_sysmain_callbacks.m in Sources */,
 				000028F8113A53F4333E0000 /* SDL_main_callbacks.c in Sources */,

+ 73 - 3
include/SDL3/SDL_events.h

@@ -34,6 +34,7 @@
 #include <SDL3/SDL_joystick.h>
 #include <SDL3/SDL_keyboard.h>
 #include <SDL3/SDL_mouse.h>
+#include <SDL3/SDL_pen.h>
 #include <SDL3/SDL_quit.h>
 #include <SDL3/SDL_stdinc.h>
 #include <SDL3/SDL_touch.h>
@@ -113,6 +114,8 @@ typedef enum
     SDL_EVENT_WINDOW_RESTORED,          /**< Window has been restored to normal size and position */
     SDL_EVENT_WINDOW_MOUSE_ENTER,       /**< Window has gained mouse focus */
     SDL_EVENT_WINDOW_MOUSE_LEAVE,       /**< Window has lost mouse focus */
+    SDL_EVENT_WINDOW_PEN_ENTER,         /**< Window has gained focus of the pressure-sensitive pen with ID "data1" */
+    SDL_EVENT_WINDOW_PEN_LEAVE,         /**< Window has lost focus of the pressure-sensitive pen with ID "data1" */
     SDL_EVENT_WINDOW_FOCUS_GAINED,      /**< Window has gained keyboard focus */
     SDL_EVENT_WINDOW_FOCUS_LOST,        /**< Window has lost keyboard focus */
     SDL_EVENT_WINDOW_CLOSE_REQUESTED,   /**< The window manager requests that the window be closed */
@@ -191,6 +194,13 @@ typedef enum
     /* Sensor events */
     SDL_EVENT_SENSOR_UPDATE = 0x1200,     /**< A sensor was updated */
 
+    /* Pressure-sensitive pen events */
+    SDL_EVENT_PEN_DOWN      = 0x1300,     /**< Pressure-sensitive pen touched drawing surface */
+    SDL_EVENT_PEN_UP,                     /**< Pressure-sensitive pen stopped touching drawing surface */
+    SDL_EVENT_PEN_MOTION,                 /**< Pressure-sensitive pen moved, or angle/pressure changed */
+    SDL_EVENT_PEN_BUTTON_DOWN,            /**< Pressure-sensitive pen button pressed */
+    SDL_EVENT_PEN_BUTTON_UP,              /**< Pressure-sensitive pen button released */
+
     /* Render events */
     SDL_EVENT_RENDER_TARGETS_RESET = 0x2000, /**< The render targets have been reset and their contents need to be updated */
     SDL_EVENT_RENDER_DEVICE_RESET, /**< The device has been reset and all textures need to be recreated */
@@ -296,7 +306,7 @@ typedef struct SDL_MouseMotionEvent
     Uint32 type;        /**< ::SDL_EVENT_MOUSE_MOTION */
     Uint64 timestamp;   /**< In nanoseconds, populated using SDL_GetTicksNS() */
     SDL_WindowID windowID; /**< The window with mouse focus, if any */
-    SDL_MouseID which;  /**< The mouse instance id, or SDL_TOUCH_MOUSEID */
+    SDL_MouseID which;  /**< The mouse instance id, SDL_TOUCH_MOUSEID, or SDL_PEN_MOUSEID */
     Uint32 state;       /**< The current button state */
     float x;            /**< X coordinate, relative to window */
     float y;            /**< Y coordinate, relative to window */
@@ -312,7 +322,7 @@ typedef struct SDL_MouseButtonEvent
     Uint32 type;        /**< ::SDL_EVENT_MOUSE_BUTTON_DOWN or ::SDL_EVENT_MOUSE_BUTTON_UP */
     Uint64 timestamp;   /**< In nanoseconds, populated using SDL_GetTicksNS() */
     SDL_WindowID windowID; /**< The window with mouse focus, if any */
-    SDL_MouseID which;  /**< The mouse instance id, or SDL_TOUCH_MOUSEID */
+    SDL_MouseID which;  /**< The mouse instance id, SDL_TOUCH_MOUSEID, or SDL_PEN_MOUSEID */
     Uint8 button;       /**< The mouse button index */
     Uint8 state;        /**< ::SDL_PRESSED or ::SDL_RELEASED */
     Uint8 clicks;       /**< 1 for single-click, 2 for double-click, etc. */
@@ -329,7 +339,7 @@ typedef struct SDL_MouseWheelEvent
     Uint32 type;        /**< ::SDL_EVENT_MOUSE_WHEEL */
     Uint64 timestamp;   /**< In nanoseconds, populated using SDL_GetTicksNS() */
     SDL_WindowID windowID; /**< The window with mouse focus, if any */
-    SDL_MouseID which;  /**< The mouse instance id, or SDL_TOUCH_MOUSEID */
+    SDL_MouseID which;  /**< The mouse instance id, SDL_TOUCH_MOUSEID, or SDL_PEN_MOUSEID */
     float x;            /**< The amount scrolled horizontally, positive to the right and negative to the left */
     float y;            /**< The amount scrolled vertically, positive away from the user and negative toward the user */
     Uint32 direction;   /**< Set to one of the SDL_MOUSEWHEEL_* defines. When FLIPPED the values in X and Y will be opposite. Multiply by -1 to change them back */
@@ -512,6 +522,63 @@ typedef struct SDL_TouchFingerEvent
 
 
 #define SDL_DROPEVENT_DATA_SIZE 64
+/**
+ *  Pressure-sensitive pen touched or stopped touching surface (event.ptip.*)
+ */
+typedef struct SDL_PenTipEvent
+{
+    Uint32 type;        /**< ::SDL_EVENT_PEN_DOWN or ::SDL_EVENT_PEN_UP */
+    Uint64 timestamp;   /**< In nanoseconds, populated using SDL_GetTicksNS() */
+    Uint32 windowID;    /**< The window with pen focus, if any */
+    SDL_PenID which;    /**< The pen instance id */
+    Uint8 tip;          /**< ::SDL_PEN_TIP_INK when using a regular pen tip, or ::SDL_PEN_TIP_ERASER if the pen is being used as an eraser (e.g., flipped to use the eraser tip)  */
+    Uint8 state;        /**< ::SDL_PRESSED on ::SDL_EVENT_PEN_DOWN and ::SDL_RELEASED on ::SDL_EVENT_PEN_UP */
+    Uint16 pen_state;   /**< Pen button masks (where SDL_BUTTON(1) is the first button, SDL_BUTTON(2) is the second button etc.),
+			   ::SDL_PEN_DOWN_MASK is set if the pen is touching the surface, and
+			   ::SDL_PEN_ERASER_MASK is set if the pen is (used as) an eraser. */
+    float x;            /**< X coordinate, relative to window */
+    float y;            /**< Y coordinate, relative to window */
+    float axes[SDL_PEN_NUM_AXES];   /**< Pen axes such as pressure and tilt (ordered as per ::SDL_PenAxis) */
+} SDL_PenTipEvent;
+
+/**
+ *  Pressure-sensitive pen motion / pressure / angle event structure (event.pmotion.*)
+ */
+typedef struct SDL_PenMotionEvent
+{
+    Uint32 type;        /**< ::SDL_EVENT_PEN_MOTION */
+    Uint64 timestamp;   /**< In nanoseconds, populated using SDL_GetTicksNS() */
+    Uint32 windowID;    /**< The window with pen focus, if any */
+    SDL_PenID which;    /**< The pen instance id */
+    Uint8 padding1;
+    Uint8 padding2;
+    Uint16 pen_state;   /**< Pen button masks (where SDL_BUTTON(1) is the first button, SDL_BUTTON(2) is the second button etc.),
+			   ::SDL_PEN_DOWN_MASK is set if the pen is touching the surface, and
+			   ::SDL_PEN_ERASER_MASK is set if the pen is (used as) an eraser. */
+    float x;            /**< X coordinate, relative to window */
+    float y;            /**< Y coordinate, relative to window */
+    float axes[SDL_PEN_NUM_AXES];   /**< Pen axes such as pressure and tilt (ordered as per ::SDL_PenAxis) */
+} SDL_PenMotionEvent;
+
+/**
+ *  Pressure-sensitive pen button event structure (event.pbutton.*)
+ */
+typedef struct SDL_PenButtonEvent
+{
+    Uint32 type;        /**< ::SDL_EVENT_PEN_BUTTON_DOWN or ::SDL_EVENT_PEN_BUTTON_UP */
+    Uint64 timestamp;   /**< In nanoseconds, populated using SDL_GetTicksNS() */
+    Uint32 windowID;    /**< The window with pen focus, if any */
+    SDL_PenID which;    /**< The pen instance id */
+    Uint8 button;       /**< The pen button index (1 represents the pen tip for compatibility with mouse events) */
+    Uint8 state;        /**< ::SDL_PRESSED or ::SDL_RELEASED */
+    Uint16 pen_state;   /**< Pen button masks (where SDL_BUTTON(1) is the first button, SDL_BUTTON(2) is the second button etc.),
+			   ::SDL_PEN_DOWN_MASK is set if the pen is touching the surface, and
+			   ::SDL_PEN_ERASER_MASK is set if the pen is (used as) an eraser. */
+    float x;            /**< X coordinate, relative to window */
+    float y;            /**< Y coordinate, relative to window */
+    float axes[SDL_PEN_NUM_AXES]; /**< Pen axes such as pressure and tilt (ordered as per ::SDL_PenAxis) */
+} SDL_PenButtonEvent;
+
 /**
  *  An event used to drop text or request a file open by the system (event.drop.*)
  *
@@ -603,6 +670,9 @@ typedef union SDL_Event
     SDL_QuitEvent quit;                     /**< Quit request event data */
     SDL_UserEvent user;                     /**< Custom event data */
     SDL_TouchFingerEvent tfinger;           /**< Touch finger event data */
+    SDL_PenTipEvent ptip;                   /**< Pen tip touching or leaving drawing surface */
+    SDL_PenMotionEvent pmotion;             /**< Pen change in position, pressure, or angle */
+    SDL_PenButtonEvent pbutton;             /**< Pen button press */
     SDL_DropEvent drop;                     /**< Drag and drop event data */
     SDL_ClipboardEvent clipboard;           /**< Clipboard event data */
 

+ 34 - 0
include/SDL3/SDL_hints.h

@@ -1220,6 +1220,40 @@ extern "C" {
  */
 #define SDL_HINT_MOUSE_AUTO_CAPTURE    "SDL_MOUSE_AUTO_CAPTURE"
 
+/**
+ *  Treat pen movement as separate from mouse movement
+ *
+ *  By default, pens report both ::SDL_MouseMotionEvent and ::SDL_PenMotionEvent updates
+ *  (analogously for button presses).  This hint allows decoupling mouse and pen updates.
+ *
+ *  This variable toggles between the following behaviour:
+ *    "0"       - (Default) Pen acts as a mouse with mouse ID ::SDL_PEN_MOUSEID.
+ *                Use case: client application is not pen aware, user wants to
+ *                use pen instead of mouse to interact.
+ *    "1"       - Pen reports mouse clicks and movement events but does not update
+ *                SDL-internal mouse state (buttons pressed, current mouse location).
+ *                Use case: client application is not pen aware, user frequently
+ *                alternates between pen and "real" mouse.
+ *    "2"       - Pen reports no mouse events.
+ *                Use case: pen-aware client application uses this hint to allow user to
+ *                toggle between pen+mouse mode ("2") and pen-only mode ("1" or "0").
+ */
+#define SDL_HINT_PEN_NOT_MOUSE    "SDL_HINT_PEN_NOT_MOUSE"
+
+/**
+ *  Pen mouse button emulation triggers only when the pen touches the tablet surface
+ *
+ *    "0"       - The pen reports mouse button press/release immediately when the pen
+ *                button is pressed/released, and the pen tip touching the surface counts
+ *                as left mouse button press.
+ *    "1"       - (Default) Mouse button presses are sent when the pen first touches
+ *                the tablet (analogously for releases).  Not pressing a pen button
+ *                simulates mouse button 1, pressing the first pen button simulates
+ *                mouse button 2 etc.;  it is not possible to report multiple buttons
+ *                as pressed at the same time.
+ */
+#define SDL_HINT_PEN_DELAY_MOUSE_BUTTON    "SDL_HINT_PEN_DELAY_MOUSE_BUTTON"
+
 /**
  *  Tell SDL not to catch the SIGINT or SIGTERM signals.
  *

+ 285 - 0
include/SDL3/SDL_pen.h

@@ -0,0 +1,285 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2023 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+
+/**
+ *  \file SDL_pen.h
+ *
+ *  Include file for SDL pen event handling.
+ *
+ *  This file describes operations for pressure-sensitive pen (stylus and/or eraser) handling, e.g., for input
+ *    and drawing tablets or suitably equipped mobile / tablet devices.
+ *
+ *  To get started with pens:
+ *  - Listen to ::SDL_PenMotionEvent and ::SDL_PenButtonEvent
+ *  - To avoid treating pen events as mouse events, ignore  ::SDL_MouseMotionEvent and ::SDL_MouseButtonEvent
+ *    whenever "which" == ::SDL_PEN_MOUSEID.
+ *
+ *  This header file describes advanced functionality that can be useful for managing user configuration
+ *    and understanding the capabilities of the attached pens.
+ *
+ *  We primarily identify pens by ::SDL_PenID.  The implementation makes a best effort to relate each :SDL_PenID
+ *    to the same physical device during a session.  Formerly valid ::SDL_PenID values remain valid
+ *    even if a device disappears.
+ *
+ *  For identifying pens across sessions, the API provides the type ::SDL_GUID .
+ */
+
+#ifndef SDL_pen_h_
+#define SDL_pen_h_
+
+#include "SDL_error.h"
+#include "SDL_guid.h"
+#include "SDL_stdinc.h"
+
+/* Set up for C function definitions, even when using C++ */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef Uint32 SDL_PenID; /**< SDL_PenIDs identify pens uniquely within a session */
+
+#define SDL_PEN_INVALID ((Uint32)0) /**< Reserved invalid ::SDL_PenID is valid */
+
+#define SDL_PEN_MOUSEID ((Uint32)-2) /**< Device ID for mouse events triggered by pen events */
+
+#define SDL_PEN_INFO_UNKNOWN (-1) /**< Marks unknown information when querying the pen */
+
+/**
+ * Pen axis indices
+ *
+ * Below are the valid indices to the "axis" array from ::SDL_PenMotionEvent and ::SDL_PenButtonEvent.
+ * The axis indices form a contiguous range of ints from 0 to ::SDL_PEN_AXIS_LAST, inclusive.
+ * All "axis[]" entries are either normalised to  0..1 or report a (positive or negative)
+ * angle in degrees, with 0.0 representing the centre.
+ * Not all pens/backends support all axes: unsupported entries are always "0.0f".
+ *
+ * To convert angles for tilt and rotation into vector representation, use
+ * \link SDL_sinf \endlink on the XTILT, YTILT, or ROTATION component, e.g., "SDL_sinf(xtilt * SDL_PI_F / 180.0)".
+ */
+typedef enum
+{
+    SDL_PEN_AXIS_PRESSURE = 0,               /**< Pen pressure.  Unidirectional: 0..1.0 */
+    SDL_PEN_AXIS_XTILT,                      /**< Pen horizontal tilt angle.  Bidirectional: -90.0..90.0 (left-to-right).
+						The physical max/min tilt may be smaller than -90.0 / 90.0, cf. \link SDL_PenCapabilityInfo \endlink */
+    SDL_PEN_AXIS_YTILT,                      /**< Pen vertical tilt angle.  Bidirectional: -90.0..90.0 (top-to-down).
+						The physical max/min tilt may be smaller than -90.0 / 90.0, cf. \link SDL_PenCapabilityInfo \endlink */
+    SDL_PEN_AXIS_DISTANCE,                   /**< Pen distance to drawing surface.  Unidirectional: 0.0..1.0 */
+    SDL_PEN_AXIS_ROTATION,                   /**< Pen barrel rotation.  Bidirectional: -180..179.9 (clockwise, 0 is facing up, -180.0 is facing down). */
+    SDL_PEN_AXIS_SLIDER,                     /**< Pen finger wheel or slider (e.g., Airbrush Pen).  Unidirectional: 0..1.0 */
+    SDL_PEN_NUM_AXES,                        /**< Last valid axis index */
+    SDL_PEN_AXIS_LAST = SDL_PEN_NUM_AXES - 1 /**< Last axis index plus 1 */
+} SDL_PenAxis;
+
+/* Pen flags.  These share a bitmask space with ::SDL_BUTTON_LEFT and friends. */
+#define SDL_PEN_FLAG_DOWN_BIT_INDEX   13 /* Bit for storing that pen is touching the surface */
+#define SDL_PEN_FLAG_INK_BIT_INDEX    14 /* Bit for storing has-non-eraser-capability status */
+#define SDL_PEN_FLAG_ERASER_BIT_INDEX 15 /* Bit for storing is-eraser or has-eraser-capability property */
+#define SDL_PEN_FLAG_AXIS_BIT_OFFSET  16 /* Bit for storing has-axis-0 property */
+
+#define SDL_PEN_CAPABILITY(capbit)    (1ul << (capbit))
+#define SDL_PEN_AXIS_CAPABILITY(axis) SDL_PEN_CAPABILITY((axis) + SDL_PEN_FLAG_AXIS_BIT_OFFSET)
+
+/**
+ * Pen tips
+ * @{
+ */
+#define SDL_PEN_TIP_INK    SDL_PEN_FLAG_INK_BIT_INDEX     /**< Regular pen tip (for drawing) touched the surface */
+#define SDL_PEN_TIP_ERASER SDL_PEN_FLAG_ERASER_BIT_INDEX  /**< Eraser pen tip touched the surface */
+/** @} */
+
+
+/**
+ * \defgroup SDL_PEN_CAPABILITIES Pen capabilities
+ * Pen capabilities reported by ::SDL_GetPenCapabilities
+ * @{
+ */
+#define SDL_PEN_DOWN_MASK          SDL_PEN_CAPABILITY(SDL_PEN_FLAG_DOWN_BIT_INDEX)   /**< Pen tip is currently touching the drawing surface. */
+#define SDL_PEN_INK_MASK           SDL_PEN_CAPABILITY(SDL_PEN_FLAG_INK_BIT_INDEX)    /**< Pen has a regular drawing tip (::SDL_GetPenCapabilities).  For events (::SDL_PenButtonEvent, ::SDL_PenMotionEvent, ::SDL_GetPenStatus) this flag is mutually exclusive with ::SDL_PEN_ERASER_MASK .  */
+#define SDL_PEN_ERASER_MASK        SDL_PEN_CAPABILITY(SDL_PEN_FLAG_ERASER_BIT_INDEX) /**< Pen has an eraser tip (::SDL_GetPenCapabilities) or is being used as eraser (::SDL_PenButtonEvent , ::SDL_PenMotionEvent , ::SDL_GetPenStatus)  */
+#define SDL_PEN_AXIS_PRESSURE_MASK SDL_PEN_AXIS_CAPABILITY(SDL_PEN_AXIS_PRESSURE)    /**< Pen provides pressure information in axis ::SDL_PEN_AXIS_PRESSURE */
+#define SDL_PEN_AXIS_XTILT_MASK    SDL_PEN_AXIS_CAPABILITY(SDL_PEN_AXIS_XTILT)       /**< Pen provides horizontal tilt information in axis ::SDL_PEN_AXIS_XTILT */
+#define SDL_PEN_AXIS_YTILT_MASK    SDL_PEN_AXIS_CAPABILITY(SDL_PEN_AXIS_YTILT)       /**< Pen provides vertical tilt information in axis ::SDL_PEN_AXIS_YTILT */
+#define SDL_PEN_AXIS_DISTANCE_MASK SDL_PEN_AXIS_CAPABILITY(SDL_PEN_AXIS_DISTANCE)    /**< Pen provides distance to drawing tablet in ::SDL_PEN_AXIS_DISTANCE */
+#define SDL_PEN_AXIS_ROTATION_MASK SDL_PEN_AXIS_CAPABILITY(SDL_PEN_AXIS_ROTATION)    /**< Pen provides barrel rotation information in axis ::SDL_PEN_AXIS_ROTATION */
+#define SDL_PEN_AXIS_SLIDER_MASK   SDL_PEN_AXIS_CAPABILITY(SDL_PEN_AXIS_SLIDER)      /**< Pen provides slider / finger wheel or similar in axis ::SDL_PEN_AXIS_SLIDER */
+
+#define SDL_PEN_AXIS_BIDIRECTIONAL_MASKS (SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK)
+/**< Masks for all axes that may be bidirectional */
+/** @} */
+
+/**
+ * Pen types
+ *
+ * Some pens identify as a particular type of drawing device (e.g., an airbrush or a pencil).
+ *
+ */
+typedef enum
+{
+    SDL_PEN_TYPE_ERASER = 1,                  /**< Eraser */
+    SDL_PEN_TYPE_PEN,                         /**< Generic pen; this is the default. */
+    SDL_PEN_TYPE_PENCIL,                      /**< Pencil */
+    SDL_PEN_TYPE_BRUSH,                       /**< Brush-like device */
+    SDL_PEN_TYPE_AIRBRUSH,                    /**< Airbrush device that "sprays" ink */
+    SDL_PEN_TYPE_LAST = SDL_PEN_TYPE_AIRBRUSH /**< Last valid pen type */
+} SDL_PenSubtype;
+
+
+/* Function prototypes */
+
+/**
+ * Retrieves all pens that are connected to the system.
+ *
+ * Yields an array of ::SDL_PenID values.  These identify and track pens throughout a session.
+ * To track pens across sessions (program restart), use ::SDL_GUID .
+ *
+ * \param[out] count The number of pens in the array (number of array elements minus 1, i.e., not
+ *     counting the terminator 0).
+ *
+ * \returns A 0 terminated array of ::SDL_PenID values, or NULL on error.
+ *     The array must be freed with ::SDL_free().
+ *     On a NULL return, ::SDL_GetError() is set.
+ *
+ * \since This function is available since SDL 3.TBD
+ */
+extern DECLSPEC SDL_PenID *SDLCALL SDL_GetPens(int *count);
+
+/**
+ * Retrieves the pen's current status.
+ *
+ * If the pen is detached (cf. ::SDL_PenConnected), this operation may return
+ * default values.
+ *
+ * \param instance_id The pen to query.
+ * \param[out] x Out-mode parameter for pen x coordinate.  May be NULL.
+ * \param[out] y Out-mode parameter for pen y coordinate.  May be NULL.
+ * \param[out] axes Out-mode parameter for axis information.  May be null.  The axes are in the same order as for
+ *     ::SDL_PenAxis .
+ * \param num_axes Maximum number of axes to write to "axes".
+ *
+ * \returns a bit mask with the current pen button states (::SDL_BUTTON_LMASK etc.),
+ *     possibly ::SDL_PEN_DOWN_MASK, and exactly one of
+ *     ::SDL_PEN_INK_MASK or ::SDL_PEN_ERASER_MASK , or 0 on error (see ::SDL_GetError()).
+ *
+ * \since This function is available since SDL 3.TBD
+ */
+extern DECLSPEC Uint32 SDLCALL SDL_GetPenStatus(SDL_PenID instance_id, float *x, float *y, float *axes, size_t num_axes);
+
+/**
+ * Retrieves an ::SDL_PenID for the given ::SDL_GUID.
+ *
+ * \param guid A pen GUID.
+ *
+ * \returns A valid ::SDL_PenID, or ::SDL_PEN_INVALID if there is no matching SDL_PenID.
+ *
+ * \since This function is available since SDL 3.TBD
+ *
+ * \sa SDL_GUID()
+ */
+extern DECLSPEC SDL_PenID SDLCALL SDL_GetPenFromGUID(SDL_GUID guid);
+
+/**
+ * Retrieves the ::SDL_GUID for a given ::SDL_PenID.
+ *
+ * \param instance_id The pen to query.
+ *
+ * \returns The corresponding pen GUID; persistent across multiple sessions.
+ *     If "instance_id" is ::SDL_PEN_INVALID, returns an all-zeroes GUID.
+ *
+ * \since This function is available since SDL 3.TBD
+ *
+ * \sa SDL_PenForID()
+ */
+extern DECLSPEC SDL_GUID SDLCALL SDL_GetPenGUID(SDL_PenID instance_id);
+
+/**
+ * Checks whether a pen is still attached.
+ *
+ * If a pen is detached, it will not show up for ::SDL_GetPens().
+ * Other operations will still be available but may return default values.
+ *
+ * \param instance_id A pen ID.
+ * \returns SDL_TRUE if "instance_id" is valid and the corresponding pen is attached, or
+ *     SDL_FALSE otherwise.
+ *
+ * \since This function is available since SDL 3.TBD
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_PenConnected(SDL_PenID instance_id);
+
+/**
+ * Retrieves a human-readable description for a ::SDL_PenID.
+ *
+ * \param instance_id The pen to query.
+ *
+ * \returns A string that contains the name of the pen, intended for human consumption.
+ *     The string might or might not be localised, depending on platform settings.
+ *     It is not guaranteed to be unique; use ::SDL_GetPenGUID() for (best-effort)
+ *     unique identifiers.
+ *     The pointer is managed by the SDL pen subsystem and must not be deallocated.
+ *     The pointer remains valid until SDL is shut down.
+ *     Returns NULL on error (cf. ::SDL_GetError())
+ *
+ * \since This function is available since SDL 3.TBD
+ */
+extern DECLSPEC const char *SDLCALL SDL_GetPenName(SDL_PenID instance_id);
+
+/**
+ * Pen capabilities, as reported by ::SDL_GetPenCapabilities()
+ */
+typedef struct SDL_PenCapabilityInfo
+{
+    float max_tilt;    /**< Physical maximum tilt angle, for XTILT and YTILT, or SDL_PEN_INFO_UNKNOWN .  Pens cannot typically tilt all the way to 90 degrees, so this value is usually less than 90.0. */
+    Uint32 wacom_id;   /**< For Wacom devices: wacom tool type ID, otherwise 0 (useful e.g. with libwacom) */
+    Sint8 num_buttons; /**< Number of pen buttons (not counting the pen tip), or SDL_PEN_INFO_UNKNOWN */
+} SDL_PenCapabilityInfo;
+
+/**
+ * Retrieves capability flags for a given ::SDL_PenID.
+ *
+ * \param instance_id The pen to query.
+ * \param[out] capabilities Detail information about pen capabilities, such as the number of buttons
+ *
+ * \returns a set of capability flags, cf. \link SDL_PEN_CAPABILITIES \endlink.  Returns 0 on error
+ *     (cf. ::SDL_GetError())
+ *
+ * \since This function is available since SDL 3.TBD
+ */
+extern DECLSPEC Uint32 SDLCALL SDL_GetPenCapabilities(SDL_PenID instance_id, SDL_PenCapabilityInfo *capabilities);
+
+/**
+ * Retrieves the pen type for a given ::SDL_PenID.
+ *
+ * \param instance_id The pen to query.
+ * \returns The corresponding pen type (cf. ::SDL_PenSubtype) or 0 on error.  Note that the pen type does not
+ *     dictate whether the pen tip is ::SDL_PEN_TIP_INK or ::SDL_PEN_TIP_ERASER; to determine whether a pen
+ *     is being used for drawing or in eraser mode, check either the pen tip on ::SDL_EVENT_PEN_DOWN, or the
+ *     flag ::SDL_PEN_ERASER_MASK in the pen state.
+ * \since This function is available since SDL 3.TBD
+ */
+extern DECLSPEC SDL_PenSubtype SDLCALL SDL_GetPenType(SDL_PenID instance_id);
+
+/* Ends C function definitions when using C++ */
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* SDL_pen_h_ */
+
+/* vi: set ts=4 sw=4 expandtab: */

+ 16 - 0
src/dynapi/SDL_dynapi.sym

@@ -940,6 +940,22 @@ SDL3_0.0.0 {
     SDL_GetVideoCaptureDevices;
     SDL_GetGamepadButtonLabelForType;
     SDL_GetGamepadButtonLabel;
+    SDL_GetPens;
+    SDL_GetPenStatus;
+    SDL_GetPenFromGUID;
+    SDL_GetPenGUID;
+    SDL_PenConnected;
+    SDL_GetPenName;
+    SDL_GetPenCapabilities;
+    SDL_GetPenType;
+    SDL_GetPens;
+    SDL_GetPenStatus;
+    SDL_GetPenFromGUID;
+    SDL_GetPenGUID;
+    SDL_PenConnected;
+    SDL_GetPenName;
+    SDL_GetPenCapabilities;
+    SDL_GetPenType;
     # extra symbols go here (don't modify this line)
   local: *;
 };

+ 16 - 0
src/dynapi/SDL_dynapi_overrides.h

@@ -965,3 +965,19 @@
 #define SDL_GetVideoCaptureDevices SDL_GetVideoCaptureDevices_REAL
 #define SDL_GetGamepadButtonLabelForType SDL_GetGamepadButtonLabelForType_REAL
 #define SDL_GetGamepadButtonLabel SDL_GetGamepadButtonLabel_REAL
+#define SDL_GetPens SDL_GetPens_REAL
+#define SDL_GetPenStatus SDL_GetPenStatus_REAL
+#define SDL_GetPenFromGUID SDL_GetPenFromGUID_REAL
+#define SDL_GetPenGUID SDL_GetPenGUID_REAL
+#define SDL_PenConnected SDL_PenConnected_REAL
+#define SDL_GetPenName SDL_GetPenName_REAL
+#define SDL_GetPenCapabilities SDL_GetPenCapabilities_REAL
+#define SDL_GetPenType SDL_GetPenType_REAL
+#define SDL_GetPens SDL_GetPens_REAL
+#define SDL_GetPenStatus SDL_GetPenStatus_REAL
+#define SDL_GetPenFromGUID SDL_GetPenFromGUID_REAL
+#define SDL_GetPenGUID SDL_GetPenGUID_REAL
+#define SDL_PenConnected SDL_PenConnected_REAL
+#define SDL_GetPenName SDL_GetPenName_REAL
+#define SDL_GetPenCapabilities SDL_GetPenCapabilities_REAL
+#define SDL_GetPenType SDL_GetPenType_REAL

+ 8 - 0
src/dynapi/SDL_dynapi_procs.h

@@ -998,3 +998,11 @@ SDL_DYNAPI_PROC(void,SDL_CloseVideoCapture,(SDL_VideoCaptureDevice *a),(a),)
 SDL_DYNAPI_PROC(SDL_VideoCaptureDeviceID*,SDL_GetVideoCaptureDevices,(int *a),(a),return)
 SDL_DYNAPI_PROC(SDL_GamepadButtonLabel,SDL_GetGamepadButtonLabelForType,(SDL_GamepadType a, SDL_GamepadButton b),(a,b),return)
 SDL_DYNAPI_PROC(SDL_GamepadButtonLabel,SDL_GetGamepadButtonLabel,(SDL_Gamepad *a, SDL_GamepadButton b),(a,b),return)
+SDL_DYNAPI_PROC(SDL_PenID*,SDL_GetPens,(int *a),(a),return)
+SDL_DYNAPI_PROC(Uint32,SDL_GetPenStatus,(SDL_PenID a, float *b, float *c, float *d, size_t e),(a,b,c,d,e),return)
+SDL_DYNAPI_PROC(SDL_PenID,SDL_GetPenFromGUID,(SDL_GUID a),(a),return)
+SDL_DYNAPI_PROC(SDL_GUID,SDL_GetPenGUID,(SDL_PenID a),(a),return)
+SDL_DYNAPI_PROC(SDL_bool,SDL_PenConnected,(SDL_PenID a),(a),return)
+SDL_DYNAPI_PROC(const char*,SDL_GetPenName,(SDL_PenID a),(a),return)
+SDL_DYNAPI_PROC(Uint32,SDL_GetPenCapabilities,(SDL_PenID a, SDL_PenCapabilityInfo *b),(a,b),return)
+SDL_DYNAPI_PROC(SDL_PenSubtype,SDL_GetPenType,(SDL_PenID a),(a),return)

+ 1 - 1
src/dynapi/gendynapi.py

@@ -379,7 +379,7 @@ def check_comment():
         if header != 'SDL_stdinc.h':
             parameter_name = i['parameter_name']
             for n in parameter_name:
-                if n != "" and "\\param " + n not in comment:
+                if n != "" and "\\param " + n not in comment and "\\param[out] " + n not in comment:
                     check_comment_header()
                     print("  In file %s: function %s() missing '\\param %s'" % (header, name, n));
 

+ 52 - 2
src/events/SDL_events.c

@@ -192,7 +192,7 @@ static void SDLCALL SDL_PollSentinelChanged(void *userdata, const char *name, co
  * Verbosity of logged events as defined in SDL_HINT_EVENT_LOGGING:
  *  - 0: (default) no logging
  *  - 1: logging of most events
- *  - 2: as above, plus mouse and finger motion
+ *  - 2: as above, plus mouse, pen, and finger motion
  */
 static int SDL_EventLoggingVerbosity = 0;
 
@@ -206,10 +206,11 @@ static void SDL_LogEvent(const SDL_Event *event)
     char name[64];
     char details[128];
 
-    /* sensor/mouse/finger motion are spammy, ignore these if they aren't demanded. */
+    /* sensor/mouse/pen/finger motion are spammy, ignore these if they aren't demanded. */
     if ((SDL_EventLoggingVerbosity < 2) &&
         ((event->type == SDL_EVENT_MOUSE_MOTION) ||
          (event->type == SDL_EVENT_FINGER_MOTION) ||
+         (event->type == SDL_EVENT_PEN_MOTION) ||
          (event->type == SDL_EVENT_GAMEPAD_TOUCHPAD_MOTION) ||
          (event->type == SDL_EVENT_GAMEPAD_SENSOR_UPDATE) ||
          (event->type == SDL_EVENT_SENSOR_UPDATE))) {
@@ -302,6 +303,8 @@ static void SDL_LogEvent(const SDL_Event *event)
         SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_RESTORED);
         SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_MOUSE_ENTER);
         SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_MOUSE_LEAVE);
+        SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_PEN_ENTER);
+        SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_PEN_LEAVE);
         SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_FOCUS_GAINED);
         SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_FOCUS_LOST);
         SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_CLOSE_REQUESTED);
@@ -470,6 +473,53 @@ static void SDL_LogEvent(const SDL_Event *event)
         break;
 #undef PRINT_FINGER_EVENT
 
+#define PRINT_PTIP_EVENT(event)                                                                                    \
+    (void)SDL_snprintf(details, sizeof(details), " (timestamp=%u windowid=%u which=%u tip=%u state=%s x=%g y=%g)", \
+                       (uint)event->ptip.timestamp, (uint)event->ptip.windowID,                                    \
+                       (uint)event->ptip.which, (uint)event->ptip.tip,                                             \
+                       event->ptip.state == SDL_PRESSED ? "down" : "up",                                           \
+                       event->ptip.x, event->ptip.y)
+        SDL_EVENT_CASE(SDL_EVENT_PEN_DOWN)
+        PRINT_PTIP_EVENT(event);
+        break;
+        SDL_EVENT_CASE(SDL_EVENT_PEN_UP)
+        PRINT_PTIP_EVENT(event);
+        break;
+#undef PRINT_PTIP_EVENT
+
+        SDL_EVENT_CASE(SDL_EVENT_PEN_MOTION)
+        (void)SDL_snprintf(details, sizeof(details), " (timestamp=%u windowid=%u which=%u state=%08x x=%g y=%g [%g, %g, %g, %g, %g, %g])",
+                           (uint)event->pmotion.timestamp, (uint)event->pmotion.windowID,
+                           (uint)event->pmotion.which, (uint)event->pmotion.pen_state,
+                           event->pmotion.x, event->pmotion.y,
+                           event->pmotion.axes[SDL_PEN_AXIS_PRESSURE],
+                           event->pmotion.axes[SDL_PEN_AXIS_XTILT],
+                           event->pmotion.axes[SDL_PEN_AXIS_YTILT],
+                           event->pmotion.axes[SDL_PEN_AXIS_DISTANCE],
+                           event->pmotion.axes[SDL_PEN_AXIS_ROTATION],
+                           event->pmotion.axes[SDL_PEN_AXIS_SLIDER]);
+        break;
+
+#define PRINT_PBUTTON_EVENT(event)                                                                                                               \
+    (void)SDL_snprintf(details, sizeof(details), " (timestamp=%u windowid=%u which=%u tip=%u state=%s x=%g y=%g axes=[%g, %g, %g, %g, %g, %g])", \
+                       (uint)event->pbutton.timestamp, (uint)event->pbutton.windowID,                                                            \
+                       (uint)event->pbutton.which, (uint)event->pbutton.button,                                                                  \
+                       event->pbutton.state == SDL_PRESSED ? "pressed" : "released",                                                             \
+                       event->pbutton.x, event->pbutton.y,                                                                                       \
+                       event->pbutton.axes[SDL_PEN_AXIS_PRESSURE],                                                                               \
+                       event->pbutton.axes[SDL_PEN_AXIS_XTILT],                                                                                  \
+                       event->pbutton.axes[SDL_PEN_AXIS_YTILT],                                                                                  \
+                       event->pbutton.axes[SDL_PEN_AXIS_DISTANCE],                                                                               \
+                       event->pbutton.axes[SDL_PEN_AXIS_ROTATION],                                                                               \
+                       event->pbutton.axes[SDL_PEN_AXIS_SLIDER])
+        SDL_EVENT_CASE(SDL_EVENT_PEN_BUTTON_DOWN)
+        PRINT_PBUTTON_EVENT(event);
+        break;
+        SDL_EVENT_CASE(SDL_EVENT_PEN_BUTTON_UP)
+        PRINT_PBUTTON_EVENT(event);
+        break;
+#undef PRINT_PBUTTON_EVENT
+
 #define PRINT_DROP_EVENT(event) (void)SDL_snprintf(details, sizeof(details), " (data='%s' timestamp=%u windowid=%u x=%f y=%f)", event->drop.data, (uint)event->drop.timestamp, (uint)event->drop.windowID, event->drop.x, event->drop.y)
         SDL_EVENT_CASE(SDL_EVENT_DROP_FILE)
         PRINT_DROP_EVENT(event);

+ 18 - 6
src/events/SDL_mouse.c

@@ -22,9 +22,11 @@
 
 /* General mouse handling code for SDL */
 
-#include "SDL_events_c.h"
 #include "../SDL_hints_c.h"
 #include "../video/SDL_sysvideo.h"
+#include "SDL_events_c.h"
+#include "SDL_mouse_c.h"
+#include "SDL_pen_c.h"
 #if defined(__WIN32__) || defined(__GDK__)
 #include "../core/windows/SDL_windows.h" // For GetDoubleClickTime()
 #endif
@@ -221,6 +223,8 @@ void SDL_PostInitMouse(void)
             SDL_DestroySurface(surface);
         }
     }
+
+    SDL_PenInit();
 }
 
 void SDL_SetDefaultCursor(SDL_Cursor *cursor)
@@ -351,17 +355,25 @@ void SDL_SetMouseFocus(SDL_Window *window)
     SDL_SetCursor(NULL);
 }
 
-/* Check to see if we need to synthesize focus events */
-static SDL_bool SDL_UpdateMouseFocus(SDL_Window *window, float x, float y, Uint32 buttonstate, SDL_bool send_mouse_motion)
+SDL_bool SDL_MousePositionInWindow(SDL_Window *window, SDL_MouseID mouseID, float x, float y)
 {
-    SDL_Mouse *mouse = SDL_GetMouse();
-    SDL_bool inWindow = SDL_TRUE;
+    if (!window) {
+        return SDL_FALSE;
+    }
 
     if (window && !(window->flags & SDL_WINDOW_MOUSE_CAPTURE)) {
         if (x < 0.0f || y < 0.0f || x >= (float)window->w || y >= (float)window->h) {
-            inWindow = SDL_FALSE;
+            return SDL_FALSE;
         }
     }
+    return SDL_TRUE;
+}
+
+/* Check to see if we need to synthesize focus events */
+static SDL_bool SDL_UpdateMouseFocus(SDL_Window *window, float x, float y, Uint32 buttonstate, SDL_bool send_mouse_motion)
+{
+    SDL_Mouse *mouse = SDL_GetMouse();
+    SDL_bool inWindow = SDL_MousePositionInWindow(window, mouse->mouseID, x, y);
 
     if (!inWindow) {
         if (window == mouse->focus) {

+ 3 - 0
src/events/SDL_mouse_c.h

@@ -163,6 +163,9 @@ extern void SDL_PerformWarpMouseInWindow(SDL_Window *window, float x, float y, S
 extern void SDL_ResetMouse(void);
 #endif /* 0 */
 
+/* Check if mouse position is within window or captured by window */
+extern SDL_bool SDL_MousePositionInWindow(SDL_Window *window, SDL_MouseID mouseID, float x, float y);
+
 /* Shutdown the mouse subsystem */
 extern void SDL_QuitMouse(void);
 

+ 1083 - 0
src/events/SDL_pen.c

@@ -0,0 +1,1083 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2023 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+#include "SDL_internal.h"
+
+/* Pressure-sensitive pen handling code for SDL */
+
+#include "../SDL_hints_c.h"
+#include "SDL_events_c.h"
+#include "SDL_pen_c.h"
+
+#define PEN_MOUSE_EMULATE   0 /* pen behaves like mouse */
+#define PEN_MOUSE_STATELESS 1 /* pen does not update mouse state */
+#define PEN_MOUSE_DISABLED  2 /* pen does not send mouse events */
+
+/* flags that are not SDL_PEN_FLAG_ */
+#define PEN_FLAGS_CAPABILITIES (~(SDL_PEN_FLAG_NEW | SDL_PEN_FLAG_DETACHED | SDL_PEN_FLAG_STALE))
+
+#define PEN_GET_PUBLIC_STATUS_MASK(pen) (((pen)->header.flags & (SDL_PEN_ERASER_MASK | SDL_PEN_DOWN_MASK)))
+
+static int pen_mouse_emulation_mode = PEN_MOUSE_EMULATE; /* SDL_HINT_PEN_NOT_MOUSE */
+
+static int pen_delay_mouse_button_mode = 1; /* SDL_HINT_PEN_DELAY_MOUSE_BUTTON */
+
+#ifndef SDL_THREADS_DISABLED
+static SDL_Mutex *SDL_pen_access_lock;
+#  define SDL_LOCK_PENS()   SDL_LockMutex(SDL_pen_access_lock)
+#  define SDL_UNLOCK_PENS() SDL_UnlockMutex(SDL_pen_access_lock)
+#else
+#  define SDL_LOCK_PENS()
+#  define SDL_UNLOCK_PENS()
+#endif
+
+static struct
+{
+    SDL_Pen *pens;         /* if "sorted" is SDL_TRUE:
+                              sorted by: (is-attached, id):
+                              - first all attached pens, in ascending ID order
+                              - then all detached pens, in ascending ID order */
+    size_t pens_allocated; /* # entries allocated to "pens" */
+    size_t pens_known;     /* <= pens_allocated; this includes detached pens */
+    size_t pens_attached;  /* <= pens_known; all attached pens are at the beginning of "pens" */
+    SDL_bool sorted;       /* This is SDL_FALSE in the period between SDL_PenGCMark() and SDL_PenGCSWeep() */
+} pen_handler;
+
+static SDL_PenID pen_invalid = { SDL_PEN_INVALID };
+
+static SDL_GUID pen_guid_zero = { { 0 } };
+
+#define SDL_LOAD_LOCK_PEN(penvar, instance_id, err_return) \
+    SDL_Pen *penvar;                              \
+    if (instance_id == SDL_PEN_INVALID) {         \
+        SDL_SetError("Invalid SDL_PenID");        \
+        return (err_return);                      \
+    }                                             \
+    SDL_LOCK_PENS();\
+    penvar = SDL_GetPenPtr(instance_id);          \
+    if (!(penvar)) {                              \
+        SDL_SetError("Stale SDL_PenID");          \
+        return (err_return);                      \
+    }
+
+static int SDL_GUIDCompare(SDL_GUID lhs, SDL_GUID rhs)
+{
+    return SDL_memcmp(lhs.data, rhs.data, sizeof(lhs.data));
+}
+
+static int SDLCALL pen_compare(const void *lhs, const void *rhs)
+{
+    int left_inactive = (((const SDL_Pen *)lhs)->header.flags & SDL_PEN_FLAG_DETACHED);
+    int right_inactive = (((const SDL_Pen *)rhs)->header.flags & SDL_PEN_FLAG_DETACHED);
+    if (left_inactive && !right_inactive) {
+        return 1;
+    }
+    if (!left_inactive && right_inactive) {
+        return -1;
+    }
+    return ((const SDL_Pen *)lhs)->header.id - ((const SDL_Pen *)rhs)->header.id;
+}
+
+static int SDLCALL pen_header_compare(const void *lhs, const void *rhs)
+{
+    const struct SDL_Pen_header *l = lhs;
+    const struct SDL_Pen_header *r = rhs;
+    int l_detached = l->flags & SDL_PEN_FLAG_DETACHED;
+    int r_detached = r->flags & SDL_PEN_FLAG_DETACHED;
+
+    if (l_detached != r_detached) {
+        if (l_detached) {
+            return -1;
+        }
+        return 1;
+    }
+
+    return l->id - r->id;
+}
+
+SDL_Pen *SDL_GetPenPtr(const Uint32 instance_id)
+{
+    unsigned int i;
+
+    if (!pen_handler.pens) {
+        return NULL;
+    }
+
+    if (pen_handler.sorted) {
+        struct SDL_Pen_header key = { 0, 0 };
+        SDL_Pen *pen;
+
+        key.id = instance_id;
+
+        pen = SDL_bsearch(&key, pen_handler.pens,
+                          pen_handler.pens_known, sizeof(SDL_Pen),
+                          pen_header_compare);
+        if (pen) {
+            return pen;
+        }
+        /* If the pen is not active, fall through */
+    }
+
+    /* fall back to linear search */
+    for (i = 0; i < pen_handler.pens_known; ++i) {
+        if (pen_handler.pens[i].header.id == instance_id) {
+            return &pen_handler.pens[i];
+        }
+    }
+    return NULL;
+}
+
+SDL_PenID *SDL_GetPens(int *count)
+{
+    int i;
+    int pens_nr = (int)pen_handler.pens_attached;
+    SDL_PenID *pens = SDL_calloc(pens_nr + 1, sizeof(SDL_PenID));
+    if (!pens) { /* OOM */
+        return pens;
+    }
+
+    for (i = 0; i < pens_nr; ++i) {
+        pens[i] = pen_handler.pens[i].header.id;
+    }
+
+    if (count) {
+        *count = pens_nr;
+    }
+    return pens;
+}
+
+SDL_PenID SDL_GetPenFromGUID(SDL_GUID guid)
+{
+    unsigned int i;
+    /* Must do linear search */
+    SDL_LOCK_PENS();
+    for (i = 0; i < pen_handler.pens_known; ++i) {
+        SDL_Pen *pen = &pen_handler.pens[i];
+
+        if (0 == SDL_GUIDCompare(guid, pen->guid)) {
+            SDL_UNLOCK_PENS();
+            return pen->header.id;
+        }
+    }
+    SDL_UNLOCK_PENS();
+    return pen_invalid;
+}
+
+SDL_bool SDL_PenConnected(SDL_PenID instance_id)
+{
+    SDL_Pen *pen;
+    SDL_bool result;
+
+    if (instance_id == SDL_PEN_INVALID) {
+        return SDL_FALSE;
+    }
+
+    SDL_LOCK_PENS();
+    pen = SDL_GetPenPtr(instance_id);
+    if (!pen) {
+        SDL_UNLOCK_PENS();
+        return SDL_FALSE;
+    }
+    result = (pen->header.flags & SDL_PEN_FLAG_DETACHED) ? SDL_FALSE : SDL_TRUE;
+    SDL_UNLOCK_PENS();
+    return result;
+}
+
+SDL_GUID SDL_GetPenGUID(SDL_PenID instance_id)
+{
+    SDL_GUID result;
+    SDL_LOAD_LOCK_PEN(pen, instance_id, pen_guid_zero);
+    result = pen->guid;
+    SDL_UNLOCK_PENS();
+    return result;
+}
+
+const char *SDL_GetPenName(SDL_PenID instance_id)
+{
+    const char *result;
+    SDL_LOAD_LOCK_PEN(pen, instance_id, NULL);
+    result = pen->name; /* Allocated separately from the pen table, so it is safe to hand to client code  */
+    SDL_UNLOCK_PENS();
+    return result;
+}
+
+SDL_PenSubtype SDL_GetPenType(SDL_PenID instance_id)
+{
+    SDL_PenSubtype result;
+    SDL_LOAD_LOCK_PEN(pen, instance_id, 0u);
+    result = pen->type;
+    SDL_UNLOCK_PENS();
+    return result;
+}
+
+Uint32 SDL_GetPenCapabilities(SDL_PenID instance_id, SDL_PenCapabilityInfo *info)
+{
+    Uint32 result;
+    SDL_LOAD_LOCK_PEN(pen, instance_id, 0u);
+    if (info) {
+        *info = pen->info;
+    }
+    result = pen->header.flags & PEN_FLAGS_CAPABILITIES;
+    SDL_UNLOCK_PENS();
+    return result;
+}
+
+Uint32 SDL_GetPenStatus(SDL_PenID instance_id, float *x, float *y, float *axes, size_t num_axes)
+{
+    Uint32 result;
+    SDL_LOAD_LOCK_PEN(pen, instance_id, 0u);
+
+    if (x) {
+        *x = pen->last.x;
+    }
+    if (y) {
+        *y = pen->last.y;
+    }
+    if (axes && num_axes) {
+        size_t axes_to_copy = SDL_min(num_axes, SDL_PEN_NUM_AXES);
+        SDL_memcpy(axes, pen->last.axes, sizeof(float) * axes_to_copy);
+    }
+    result = pen->last.buttons | (pen->header.flags & (SDL_PEN_INK_MASK | SDL_PEN_ERASER_MASK | SDL_PEN_DOWN_MASK));
+    SDL_UNLOCK_PENS();
+    return result;
+}
+
+/* Backend functionality */
+
+/* Sort all pens.  Only safe during SDL_LOCK_PENS. */
+static void pen_sort(void)
+{
+    SDL_qsort(pen_handler.pens,
+              pen_handler.pens_known,
+              sizeof(SDL_Pen),
+              pen_compare);
+    pen_handler.sorted = SDL_TRUE;
+}
+
+SDL_Pen *SDL_PenModifyBegin(const Uint32 instance_id)
+{
+    SDL_PenID id = { 0 };
+    const size_t alloc_growth_constant = 1; /* Expect few pens */
+    SDL_Pen *pen;
+
+    id = instance_id;
+
+    if (id == SDL_PEN_INVALID) {
+        SDL_SetError("Invalid SDL_PenID");
+        return NULL;
+    }
+
+    SDL_LOCK_PENS();
+    pen = SDL_GetPenPtr(id);
+
+    if (!pen) {
+        if (!pen_handler.pens || pen_handler.pens_known == pen_handler.pens_allocated) {
+            size_t pens_to_allocate = pen_handler.pens_allocated + alloc_growth_constant;
+            SDL_Pen *pens;
+            if (pen_handler.pens) {
+                pens = SDL_realloc(pen_handler.pens, sizeof(SDL_Pen) * pens_to_allocate);
+                SDL_memset(pens + pen_handler.pens_known, 0,
+                           sizeof(SDL_Pen) * (pens_to_allocate - pen_handler.pens_allocated));
+            } else {
+                pens = SDL_calloc(sizeof(SDL_Pen), pens_to_allocate);
+            }
+            pen_handler.pens = pens;
+            pen_handler.pens_allocated = pens_to_allocate;
+        }
+        pen = &pen_handler.pens[pen_handler.pens_known];
+        pen_handler.pens_known += 1;
+
+        /* Default pen initialisation */
+        pen->header.id = id;
+        pen->header.flags = SDL_PEN_FLAG_NEW;
+        pen->info.num_buttons = SDL_PEN_INFO_UNKNOWN;
+        pen->info.max_tilt = SDL_PEN_INFO_UNKNOWN;
+        pen->type = SDL_PEN_TYPE_PEN;
+        pen->name = SDL_calloc(1, SDL_PEN_MAX_NAME); /* Never deallocated */
+    }
+    return pen;
+}
+
+void SDL_PenModifyAddCapabilities(SDL_Pen *pen, Uint32 capabilities)
+{
+    if (capabilities & SDL_PEN_ERASER_MASK) {
+	pen->header.flags &= ~SDL_PEN_INK_MASK;
+    } else if (capabilities & SDL_PEN_INK_MASK) {
+	pen->header.flags &= ~SDL_PEN_ERASER_MASK;
+    }
+    pen->header.flags |= (capabilities & PEN_FLAGS_CAPABILITIES);
+}
+
+static void pen_hotplug_attach(SDL_Pen *pen)
+{
+    if (!pen->header.window) {
+        /* Attach to default window */
+        const SDL_Mouse *mouse = SDL_GetMouse();
+        SDL_SendPenWindowEvent(0, pen->header.id, mouse->focus);
+    }
+}
+
+static void pen_hotplug_detach(SDL_Pen *pen)
+{
+    SDL_SendPenWindowEvent(0, pen->header.id, NULL);
+}
+
+void SDL_PenModifyEnd(SDL_Pen *pen, SDL_bool attach)
+{
+    SDL_bool is_new = pen->header.flags & SDL_PEN_FLAG_NEW;
+    SDL_bool was_attached = !(pen->header.flags & (SDL_PEN_FLAG_DETACHED | SDL_PEN_FLAG_NEW));
+    SDL_bool broke_sort_order = SDL_FALSE;
+
+    if (pen->type == SDL_PEN_TYPE_NONE) {
+        /* remove pen */
+        if (!is_new) {
+            if (!(pen->header.flags & SDL_PEN_FLAG_ERROR)) {
+                SDL_Log("Error: attempt to remove known pen %lu", (unsigned long) pen->header.id);
+                pen->header.flags |= SDL_PEN_FLAG_ERROR;
+            }
+
+            /* Treat as detached pen of unknown type instead */
+            pen->type = SDL_PEN_TYPE_PEN;
+            attach = SDL_FALSE;
+        } else {
+            pen_handler.pens_known -= 1;
+            SDL_memset(pen, 0, sizeof(SDL_Pen));
+            SDL_UNLOCK_PENS();
+            return;
+        }
+    }
+
+    pen->header.flags &= ~(SDL_PEN_FLAG_NEW | SDL_PEN_FLAG_STALE | SDL_PEN_FLAG_DETACHED);
+    if (attach == SDL_FALSE) {
+        pen->header.flags |= SDL_PEN_FLAG_DETACHED;
+        if (was_attached) {
+            broke_sort_order = SDL_TRUE;
+            if (!is_new) {
+                pen_handler.pens_attached -= 1;
+            }
+            pen_hotplug_detach(pen);
+        }
+    } else if (!was_attached || is_new) {
+        broke_sort_order = SDL_TRUE;
+        pen_handler.pens_attached += 1;
+        pen_hotplug_attach(pen);
+    }
+
+    if (is_new) {
+        /* default: name */
+        if (!pen->name[0]) {
+            SDL_snprintf(pen->name, SDL_PEN_MAX_NAME,
+                         "%s %lu",
+                         pen->type == SDL_PEN_TYPE_ERASER ? "Eraser" : "Pen",
+                         (unsigned long) pen->header.id);
+        }
+
+        /* default: enabled axes */
+        if (!(pen->header.flags & (SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK))) {
+            pen->info.max_tilt = 0; /* no tilt => no max_tilt */
+        }
+
+        /* sanity-check GUID */
+        if (0 == SDL_GUIDCompare(pen->guid, pen_guid_zero)) {
+            SDL_Log("Error: pen %lu: has GUID 0", (unsigned long) pen->header.id);
+        }
+
+        /* pen or eraser? */
+        if (pen->type == SDL_PEN_TYPE_ERASER || pen->header.flags & SDL_PEN_ERASER_MASK) {
+            pen->header.flags = (pen->header.flags & ~SDL_PEN_INK_MASK) | SDL_PEN_ERASER_MASK;
+            pen->type = SDL_PEN_TYPE_ERASER;
+        } else {
+            pen->header.flags = (pen->header.flags & ~SDL_PEN_ERASER_MASK) | SDL_PEN_INK_MASK;
+        }
+
+        broke_sort_order = SDL_TRUE;
+    }
+    if (broke_sort_order && pen_handler.sorted) {
+        /* Maintain sortedness invariant */
+        pen_sort();
+    }
+    SDL_UNLOCK_PENS();
+}
+
+void SDL_PenGCMark(void)
+{
+    unsigned int i;
+    SDL_LOCK_PENS();
+    for (i = 0; i < pen_handler.pens_known; ++i) {
+        SDL_Pen *pen = &pen_handler.pens[i];
+        pen->header.flags |= SDL_PEN_FLAG_STALE;
+    }
+    pen_handler.sorted = SDL_FALSE;
+    SDL_UNLOCK_PENS();
+}
+
+void SDL_PenGCSweep(void *context, void (*free_deviceinfo)(Uint32, void *, void *))
+{
+    unsigned int i;
+    pen_handler.pens_attached = 0;
+
+    SDL_LOCK_PENS();
+    /* We don't actually free the SDL_Pen entries, so that we can still answer queries about
+       formerly active SDL_PenIDs later.  */
+    for (i = 0; i < pen_handler.pens_known; ++i) {
+        SDL_Pen *pen = &pen_handler.pens[i];
+
+        if (pen->header.flags & SDL_PEN_FLAG_STALE) {
+            pen->header.flags |= SDL_PEN_FLAG_DETACHED;
+            pen_hotplug_detach(pen);
+            if (pen->deviceinfo) {
+                free_deviceinfo(pen->header.id, pen->deviceinfo, context);
+                pen->deviceinfo = NULL;
+            }
+        } else {
+            pen_handler.pens_attached += 1;
+        }
+
+        pen->header.flags &= ~SDL_PEN_FLAG_STALE;
+    }
+    pen_sort();
+    /* We could test for changes in the above and send a hotplugging event here */
+    SDL_UNLOCK_PENS();
+}
+
+static void pen_relative_coordinates(SDL_Window *window, SDL_bool window_relative, float *x, float *y)
+{
+    int win_x, win_y;
+
+    if (window_relative) {
+        return;
+    }
+    SDL_GetWindowPosition(window, &win_x, &win_y);
+    *x -= win_x;
+    *y -= win_y;
+}
+
+/* Initialises timestamp, windowID, which, pen_state, x, y, and axes */
+static void event_setup(const SDL_Pen *pen, const SDL_Window *window, Uint64 timestamp, const SDL_PenStatusInfo *status, SDL_Event *event)
+{
+    Uint16 last_buttons = pen->last.buttons;
+
+    if (timestamp == 0) {
+        /* Generate timestamp ourselves, if needed, so that we report the same timestamp
+           for the pen event and for any emulated mouse event */
+        timestamp = SDL_GetTicksNS();
+    }
+
+    /* This code assumes that all of the SDL_Pen*Event structures have the same layout for these fields.
+     * This is checked by testautomation_pen.c, pen_memoryLayout(). */
+    event->pmotion.timestamp = timestamp;
+    event->pmotion.windowID = window ? window->id : 0;
+    event->pmotion.which = pen->header.id;
+    event->pmotion.pen_state = (Uint16)last_buttons | PEN_GET_PUBLIC_STATUS_MASK(pen);
+    event->pmotion.x = status->x;
+    event->pmotion.y = status->y;
+    SDL_memcpy(event->pmotion.axes, status->axes, SDL_PEN_NUM_AXES * sizeof(float));
+}
+
+
+int SDL_SendPenMotion(Uint64 timestamp,
+                      SDL_PenID instance_id,
+                      SDL_bool window_relative,
+                      const SDL_PenStatusInfo *status)
+{
+    const SDL_Mouse *mouse = SDL_GetMouse();
+    int i;
+    SDL_Pen *pen = SDL_GetPenPtr(instance_id);
+    SDL_Event event;
+    SDL_bool posted = SDL_FALSE;
+    float x = status->x;
+    float y = status->y;
+    float last_x = pen->last.x;
+    float last_y = pen->last.y;
+    /* Suppress mouse updates for axis changes or sub-pixel movement: */
+    SDL_bool send_mouse_update;
+    SDL_bool axes_changed = SDL_FALSE;
+    SDL_Window *window;
+
+    if (!pen) {
+        return SDL_FALSE;
+    }
+    window = pen->header.window;
+    if (!window) {
+        return SDL_FALSE;
+    }
+
+    pen_relative_coordinates(window, window_relative, &x, &y);
+
+    /* Check if the event actually modifies any cached axis or location, update as neeed */
+    if (x != last_x || y != last_y) {
+        axes_changed = SDL_TRUE;
+        pen->last.x = status->x;
+        pen->last.y = status->y;
+    }
+    for (i = 0; i < SDL_PEN_NUM_AXES; ++i) {
+        if ((pen->header.flags & SDL_PEN_AXIS_CAPABILITY(i)) && (status->axes[i] != pen->last.axes[i])) {
+            axes_changed = SDL_TRUE;
+            pen->last.axes[i] = status->axes[i];
+        }
+    }
+    if (!axes_changed) {
+        /* No-op event */
+        return SDL_FALSE;
+    }
+
+    send_mouse_update = (x != last_x) || (y != last_y);
+
+    if (!(SDL_MousePositionInWindow(window, mouse->mouseID, x, y))) {
+        return SDL_FALSE;
+    }
+
+    if (SDL_EventEnabled(SDL_EVENT_PEN_MOTION)) {
+	event_setup(pen, window, timestamp, status, &event);
+        event.pmotion.type = SDL_EVENT_PEN_MOTION;
+
+        posted = SDL_PushEvent(&event) > 0;
+
+        if (!posted) {
+            return SDL_FALSE;
+        }
+    }
+
+    if (send_mouse_update) {
+        switch (pen_mouse_emulation_mode) {
+        case PEN_MOUSE_EMULATE:
+            return (SDL_SendMouseMotion(0, window, SDL_PEN_MOUSEID, 0, x, y)) || posted;
+
+        case PEN_MOUSE_STATELESS:
+            /* Report mouse event but don't update mouse state */
+            if (SDL_EventEnabled(SDL_EVENT_MOUSE_MOTION)) {
+                event.motion.windowID = event.pmotion.windowID;
+                event.motion.timestamp = timestamp;
+                event.motion.which = SDL_PEN_MOUSEID;
+                event.motion.type = SDL_EVENT_MOUSE_MOTION;
+                event.motion.state = pen->last.buttons | PEN_GET_PUBLIC_STATUS_MASK(pen);
+                event.motion.x = x;
+                event.motion.y = y;
+                event.motion.xrel = last_x - x;
+                event.motion.yrel = last_y - y;
+                return (SDL_PushEvent(&event) > 0) || posted;
+            }
+
+        default:
+            break;
+        }
+    }
+    return posted;
+}
+
+int SDL_SendPenTipEvent(Uint64 timestamp, SDL_PenID instance_id, Uint8 state)
+{
+    SDL_Mouse *mouse = SDL_GetMouse();
+    SDL_Pen *pen = SDL_GetPenPtr(instance_id);
+    SDL_Event event;
+    SDL_bool posted = SDL_FALSE;
+    SDL_PenStatusInfo *last = &pen->last;
+    int mouse_button = SDL_BUTTON_LEFT;
+    SDL_Window *window;
+
+    if (!pen) {
+        return SDL_FALSE;
+    }
+    window = pen->header.window;
+
+    if ((state == SDL_PRESSED) && !(window && SDL_MousePositionInWindow(window, mouse->mouseID, last->x, last->y))) {
+        return SDL_FALSE;
+    }
+
+    if (state == SDL_PRESSED) {
+	event.pbutton.type = SDL_EVENT_PEN_DOWN;
+	pen->header.flags |= SDL_PEN_DOWN_MASK;
+    } else {
+	event.pbutton.type = SDL_EVENT_PEN_UP;
+	pen->header.flags &= ~SDL_PEN_DOWN_MASK;
+    }
+
+    if (SDL_EventEnabled(event.ptip.type)) {
+	event_setup(pen, window, timestamp, &pen->last, &event);
+
+	/* Used as eraser?  Report eraser event, otherwise ink event */
+        event.ptip.tip = (pen->header.flags & SDL_PEN_ERASER_MASK) ? SDL_PEN_TIP_ERASER : SDL_PEN_TIP_INK;
+        event.ptip.state = state == SDL_PRESSED ? SDL_PRESSED : SDL_RELEASED;
+
+        posted = SDL_PushEvent(&event) > 0;
+
+        if (!posted) {
+            return SDL_FALSE;
+        }
+    }
+
+    /* Mouse emulation */
+    if (pen_delay_mouse_button_mode) {
+        /* Send button events when pen touches / leaves surface */
+	mouse_button = pen->last_mouse_button;
+	if (0 == mouse_button) {
+	    mouse_button = SDL_BUTTON_LEFT; /* No current button? Instead report left mouse button */
+	}
+    }
+
+    switch (pen_mouse_emulation_mode) {
+    case PEN_MOUSE_EMULATE:
+        return (SDL_SendMouseButton(timestamp, window, SDL_PEN_MOUSEID, state, mouse_button)) || posted;
+
+    case PEN_MOUSE_STATELESS:
+        /* Report mouse event without updating mouse state */
+        event.button.type = state == SDL_PRESSED ? SDL_EVENT_MOUSE_BUTTON_DOWN : SDL_EVENT_MOUSE_BUTTON_UP;
+        if (SDL_EventEnabled(event.button.type)) {
+            event.button.windowID = window ? window->id : 0;
+            event.button.timestamp = timestamp;
+            event.button.which = SDL_PEN_MOUSEID;
+
+            event.button.state = state;
+            event.button.button = mouse_button;
+            event.button.clicks = 1;
+            event.button.x = last->x;
+            event.button.y = last->y;
+            return (SDL_PushEvent(&event) > 0) || posted;
+        }
+        break;
+
+    default:
+        break;
+    }
+    return posted;
+}
+
+int SDL_SendPenButton(Uint64 timestamp,
+                      SDL_PenID instance_id,
+                      Uint8 state, Uint8 button)
+{
+    SDL_Mouse *mouse = SDL_GetMouse();
+    SDL_Pen *pen = SDL_GetPenPtr(instance_id);
+    SDL_Event event;
+    SDL_bool posted = SDL_FALSE;
+    SDL_PenStatusInfo *last = &pen->last;
+    int mouse_button = button + 1; /* For mouse emulation, PEN_DOWN counts as button 1, so the first actual button is mouse button 2 */
+    SDL_Window *window;
+
+    if (!pen) {
+        return SDL_FALSE;
+    }
+    window = pen->header.window;
+
+    if ((state == SDL_PRESSED) && !(window && SDL_MousePositionInWindow(window, mouse->mouseID, last->x, last->y))) {
+        return SDL_FALSE;
+    }
+
+    if (state == SDL_PRESSED) {
+	event.pbutton.type = SDL_EVENT_PEN_BUTTON_DOWN;
+	pen->last.buttons |= (1 << (button - 1));
+    } else {
+	event.pbutton.type = SDL_EVENT_PEN_BUTTON_UP;
+	pen->last.buttons &= ~(1 << (button - 1));
+    }
+
+    if (SDL_EventEnabled(event.pbutton.type)) {
+	event_setup(pen, window, timestamp, &pen->last, &event);
+
+        event.pbutton.button = button;
+        event.pbutton.state = state == SDL_PRESSED ? SDL_PRESSED : SDL_RELEASED;
+
+        posted = SDL_PushEvent(&event) > 0;
+
+        if (!posted) {
+            return SDL_FALSE;
+        }
+    }
+
+    /* Mouse emulation */
+    if (pen_delay_mouse_button_mode) {
+        /* Can only change active mouse button while not touching the surface */
+        if (!(pen->header.flags & SDL_PEN_DOWN_MASK)) {
+	    if (state == SDL_RELEASED) {
+		pen->last_mouse_button = 0;
+	    } else {
+		pen->last_mouse_button = mouse_button;
+	    }
+	}
+	/* Defer emulation event */
+	return SDL_TRUE;
+    }
+
+    switch (pen_mouse_emulation_mode) {
+    case PEN_MOUSE_EMULATE:
+        return (SDL_SendMouseButton(timestamp, window, SDL_PEN_MOUSEID, state, mouse_button)) || posted;
+
+    case PEN_MOUSE_STATELESS:
+        /* Report mouse event without updating mouse state */
+        event.button.type = state == SDL_PRESSED ? SDL_EVENT_MOUSE_BUTTON_DOWN : SDL_EVENT_MOUSE_BUTTON_UP;
+        if (SDL_EventEnabled(event.button.type)) {
+            event.button.windowID = window ? window->id : 0;
+            event.button.timestamp = timestamp;
+            event.button.which = SDL_PEN_MOUSEID;
+
+            event.button.state = state;
+            event.button.button = mouse_button;
+            event.button.clicks = 1;
+            event.button.x = last->x;
+            event.button.y = last->y;
+            return (SDL_PushEvent(&event) > 0) || posted;
+        }
+        break;
+
+    default:
+        break;
+    }
+    return posted;
+}
+
+int SDL_SendPenWindowEvent(Uint64 timestamp, SDL_PenID instance_id, SDL_Window *window)
+{
+    SDL_EventType event_id = window ? SDL_EVENT_WINDOW_PEN_ENTER : SDL_EVENT_WINDOW_PEN_LEAVE;
+    SDL_Event event = { 0 };
+    SDL_Pen *pen = SDL_GetPenPtr(instance_id);
+    SDL_bool posted;
+
+    if (!pen) {
+        return SDL_FALSE;
+    }
+
+    if (pen->header.window == window) { /* (TRIVIAL-EVENT) Nothing new to report */
+        return SDL_FALSE;
+    }
+
+    if (timestamp == 0) {
+        /* Generate timestamp ourselves, if needed, so that we report the same timestamp
+           for the pen event and for any emulated mouse event */
+        timestamp = SDL_GetTicksNS();
+    }
+
+    event.window.type = event_id;
+    /* If window == NULL and not (TRIVIAL-EVENT), then pen->header.window != NULL */
+    event.window.timestamp = timestamp;
+    event.window.windowID = window ? window->id : pen->header.window->id;
+    event.window.data1 = instance_id;
+    posted = (SDL_PushEvent(&event) > 0);
+
+    /* Update after assembling event */
+    pen->header.window = window;
+
+    switch (pen_mouse_emulation_mode) {
+    case PEN_MOUSE_EMULATE:
+        SDL_SetMouseFocus(event_id == SDL_EVENT_WINDOW_PEN_ENTER ? window : NULL);
+        break;
+
+    case PEN_MOUSE_STATELESS:
+        /* Send event without going through mouse API */
+        if (event_id == SDL_EVENT_WINDOW_PEN_ENTER) {
+            event.window.type = SDL_EVENT_WINDOW_MOUSE_ENTER;
+        } else {
+            event.window.type = SDL_EVENT_WINDOW_MOUSE_LEAVE;
+        }
+        posted = posted || (SDL_PushEvent(&event) > 0);
+        break;
+
+    default:
+        break;
+    }
+
+    return posted;
+}
+
+static void SDLCALL SDL_PenUpdateHint(void *userdata, const char *name, const char *oldvalue, const char *newvalue)
+{
+    int *var = userdata;
+    if (newvalue == NULL) {
+        return;
+    }
+
+    if (0 == SDL_strcmp("2", newvalue)) {
+        *var = 2;
+    } else if (0 == SDL_strcmp("1", newvalue)) {
+        *var = 1;
+    } else if (0 == SDL_strcmp("0", newvalue)) {
+        *var = 0;
+    } else {
+        SDL_Log("Unexpected value for pen hint: '%s'", newvalue);
+    }
+}
+
+void SDL_PenInit(void)
+{
+#if (SDL_PEN_DEBUG_NOID | SDL_PEN_DEBUG_NONWACOM | SDL_PEN_DEBUG_UNKNOWN_WACOM)
+    printf("[pen] Debugging enabled: noid=%d  nonwacom=%d  unknown-wacom=%d\n",
+           SDL_PEN_DEBUG_NOID, SDL_PEN_DEBUG_NONWACOM, SDL_PEN_DEBUG_UNKNOWN_WACOM);
+#endif
+    SDL_AddHintCallback(SDL_HINT_PEN_NOT_MOUSE,
+                        SDL_PenUpdateHint, &pen_mouse_emulation_mode);
+
+    SDL_AddHintCallback(SDL_HINT_PEN_DELAY_MOUSE_BUTTON,
+                        SDL_PenUpdateHint, &pen_delay_mouse_button_mode);
+#ifndef SDL_THREADS_DISABLED
+    SDL_pen_access_lock = SDL_CreateMutex();
+#endif
+}
+
+SDL_bool SDL_PenPerformHitTest(void)
+{
+    return pen_mouse_emulation_mode == PEN_MOUSE_EMULATE;
+}
+
+/* Vendor-specific bits */
+
+/* Default pen names */
+#define PEN_NAME_AES      0
+#define PEN_NAME_ART      1
+#define PEN_NAME_AIRBRUSH 2
+#define PEN_NAME_GENERAL  3
+#define PEN_NAME_GRIP     4
+#define PEN_NAME_INKING   5
+#define PEN_NAME_PRO      6
+#define PEN_NAME_PRO2     7
+#define PEN_NAME_PRO3     8
+#define PEN_NAME_PRO3D    9
+#define PEN_NAME_PRO_SLIM 10
+#define PEN_NAME_STROKE   11
+
+#define PEN_NAME_LAST PEN_NAME_STROKE
+#define PEN_NUM_NAMES (PEN_NAME_LAST + 1)
+
+const static char *default_pen_names[PEN_NUM_NAMES] = {
+    /* PEN_NAME_AES */
+    "AES Pen",
+    /* PEN_NAME_ART */
+    "Art Pen",
+    /* PEN_NAME_AIRBRUSH */
+    "Airbrush Pen",
+    /* PEN_NAME_GENERAL */
+    "Pen",
+    /* PEN_NAME_GRIP */
+    "Grip Pen",
+    /* PEN_NAME_INKING */
+    "Inking Pen",
+    /* PEN_NAME_PRO */
+    "Pro Pen",
+    /* PEN_NAME_PRO2 */
+    "Pro Pen 2",
+    /* PEN_NAME_PRO3 */
+    "Pro Pen 3",
+    /* PEN_NAME_PRO3D */
+    "Pro Pen 3D",
+    /* PEN_NAME_PRO_SLIM */
+    "Pro Pen Slim",
+    /* PEN_NAME_STROKE */
+    "Stroke Pen"
+};
+
+#define PEN_SPEC_TYPE_SHIFT    0
+#define PEN_SPEC_TYPE_MASK     0x0000000fu
+#define PEN_SPEC_BUTTONS_SHIFT 4
+#define PEN_SPEC_BUTTONS_MASK  0x000000f0u
+#define PEN_SPEC_NAME_SHIFT    8
+#define PEN_SPEC_NAME_MASK     0x00000f00u
+#define PEN_SPEC_AXES_SHIFT    0
+#define PEN_SPEC_AXES_MASK     0xffff0000u
+
+#define PEN_WACOM_ID_INVALID 0xffffffffu /* Generic "invalid ID" marker */
+
+#define PEN_SPEC(name, buttons, type, axes) (0 | (PEN_SPEC_NAME_MASK & ((name) << PEN_SPEC_NAME_SHIFT)) | (PEN_SPEC_BUTTONS_MASK & ((buttons) << PEN_SPEC_BUTTONS_SHIFT)) | (PEN_SPEC_TYPE_MASK & ((type) << PEN_SPEC_TYPE_SHIFT)) | (PEN_SPEC_AXES_MASK & ((axes) << PEN_SPEC_AXES_SHIFT)))
+
+/* Returns a suitable pen name string from default_pen_names on success, otherwise NULL. */
+static const char *pen_wacom_identify_tool(Uint32 requested_wacom_id, int *num_buttons, int *tool_type, int *axes)
+{
+    int i;
+
+    /* List of known Wacom pens, extracted from libwacom.stylus and wacom_wac.c in the Linux kernel.
+       Could be complemented by dlopen()ing libwacom, in the future (if new pens get added).  */
+    struct
+    {
+        /* Compress properties to 8 bytes per device in order to keep memory cost well below 1k.
+           Could be compressed further with more complex code.  */
+        Uint32 wacom_id; /* Must be != PEN_WACOM_ID_INVALID */
+        Uint32 properties;
+    } tools[] = {
+        {  0x0001, PEN_SPEC(PEN_NAME_AES,      1, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK) },
+        {  0x0011, PEN_SPEC(PEN_NAME_AES,      1, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK) },
+        {  0x0019, PEN_SPEC(PEN_NAME_AES,      1, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK) },
+        {  0x0021, PEN_SPEC(PEN_NAME_AES,      1, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK) },
+        {  0x0031, PEN_SPEC(PEN_NAME_AES,      1, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK) },
+        {  0x0039, PEN_SPEC(PEN_NAME_AES,      1, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK) },
+        {  0x0049, PEN_SPEC(PEN_NAME_GENERAL,  1, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK) },
+        {  0x0071, PEN_SPEC(PEN_NAME_GENERAL,  1, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK) },
+        {  0x0200, PEN_SPEC(PEN_NAME_PRO3,     3, SDL_PEN_TYPE_PEN,      SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        {  0x0221, PEN_SPEC(PEN_NAME_AES,      1, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK) },
+        {  0x0231, PEN_SPEC(PEN_NAME_AES,      1, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK) },
+        {  0x0271, PEN_SPEC(PEN_NAME_GENERAL,  1, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK) },
+        {  0x0421, PEN_SPEC(PEN_NAME_AES,      1, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK) },
+        {  0x0431, PEN_SPEC(PEN_NAME_AES,      1, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK) },
+        {  0x0621, PEN_SPEC(PEN_NAME_AES,      1, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK) },
+        {  0x0631, PEN_SPEC(PEN_NAME_AES,      1, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK) },
+        {  0x0801, PEN_SPEC(PEN_NAME_INKING,   0, SDL_PEN_TYPE_PENCIL,   SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        {  0x0802, PEN_SPEC(PEN_NAME_GRIP,     2, SDL_PEN_TYPE_PEN,      SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        {  0x0804, PEN_SPEC(PEN_NAME_ART,      2, SDL_PEN_TYPE_PEN,      SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK | SDL_PEN_AXIS_ROTATION_MASK) },
+        {  0x080a, PEN_SPEC(PEN_NAME_GRIP,     2, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        {  0x080c, PEN_SPEC(PEN_NAME_ART,      2, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        {  0x0812, PEN_SPEC(PEN_NAME_INKING,   0, SDL_PEN_TYPE_PENCIL,   SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        {  0x0813, PEN_SPEC(PEN_NAME_GENERAL,  2, SDL_PEN_TYPE_PEN,      SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        {  0x081b, PEN_SPEC(PEN_NAME_GENERAL,  2, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        {  0x0822, PEN_SPEC(PEN_NAME_GENERAL,  2, SDL_PEN_TYPE_PEN,      SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        {  0x0823, PEN_SPEC(PEN_NAME_GRIP,     2, SDL_PEN_TYPE_PEN,      SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        {  0x082a, PEN_SPEC(PEN_NAME_GENERAL,  2, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        {  0x082b, PEN_SPEC(PEN_NAME_GRIP,     2, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        {  0x0832, PEN_SPEC(PEN_NAME_STROKE,   0, SDL_PEN_TYPE_BRUSH,    SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        {  0x0842, PEN_SPEC(PEN_NAME_PRO2,     2, SDL_PEN_TYPE_PEN,      SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        {  0x084a, PEN_SPEC(PEN_NAME_PRO2,     2, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        {  0x0852, PEN_SPEC(PEN_NAME_GRIP,     2, SDL_PEN_TYPE_PEN,      SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        {  0x085a, PEN_SPEC(PEN_NAME_GRIP,     2, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        {  0x0862, PEN_SPEC(PEN_NAME_GENERAL,  2, SDL_PEN_TYPE_PEN,      SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        {  0x0885, PEN_SPEC(PEN_NAME_ART,      0, SDL_PEN_TYPE_PEN,      SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK | SDL_PEN_AXIS_ROTATION_MASK) },
+        {  0x08e2, PEN_SPEC(PEN_NAME_GENERAL,  2, SDL_PEN_TYPE_PEN,      SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        {  0x0902, PEN_SPEC(PEN_NAME_AIRBRUSH, 1, SDL_PEN_TYPE_AIRBRUSH, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK | SDL_PEN_AXIS_SLIDER_MASK) },
+        {  0x090a, PEN_SPEC(PEN_NAME_AIRBRUSH, 1, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        {  0x0912, PEN_SPEC(PEN_NAME_AIRBRUSH, 1, SDL_PEN_TYPE_AIRBRUSH, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK | SDL_PEN_AXIS_SLIDER_MASK) },
+        {  0x0913, PEN_SPEC(PEN_NAME_AIRBRUSH, 1, SDL_PEN_TYPE_AIRBRUSH, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        {  0x091a, PEN_SPEC(PEN_NAME_AIRBRUSH, 1, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        {  0x091b, PEN_SPEC(PEN_NAME_AIRBRUSH, 1, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        {  0x0d12, PEN_SPEC(PEN_NAME_AIRBRUSH, 1, SDL_PEN_TYPE_AIRBRUSH, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK | SDL_PEN_AXIS_SLIDER_MASK) },
+        {  0x0d1a, PEN_SPEC(PEN_NAME_AIRBRUSH, 1, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        {  0x8051, PEN_SPEC(PEN_NAME_AES,      0, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK) },
+        {  0x805b, PEN_SPEC(PEN_NAME_AES,      1, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK) },
+        {  0x806b, PEN_SPEC(PEN_NAME_AES,      1, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK) },
+        {  0x807b, PEN_SPEC(PEN_NAME_GENERAL,  1, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK) },
+        {  0x826b, PEN_SPEC(PEN_NAME_AES,      1, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK) },
+        {  0x846b, PEN_SPEC(PEN_NAME_AES,      1, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK) },
+        {  0x2802, PEN_SPEC(PEN_NAME_INKING,   0, SDL_PEN_TYPE_PENCIL,   SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        {  0x4200, PEN_SPEC(PEN_NAME_PRO3,     3, SDL_PEN_TYPE_PEN,      SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        {  0x4802, PEN_SPEC(PEN_NAME_GENERAL,  2, SDL_PEN_TYPE_PEN,      SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        {  0x480a, PEN_SPEC(PEN_NAME_GENERAL,  2, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        {  0x8842, PEN_SPEC(PEN_NAME_PRO3D,    3, SDL_PEN_TYPE_PEN,      SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        { 0x10802, PEN_SPEC(PEN_NAME_GRIP,     2, SDL_PEN_TYPE_PEN,      SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        { 0x10804, PEN_SPEC(PEN_NAME_ART,      2, SDL_PEN_TYPE_PEN,      SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK | SDL_PEN_AXIS_ROTATION_MASK) },
+        { 0x1080a, PEN_SPEC(PEN_NAME_GRIP,     2, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        { 0x1080c, PEN_SPEC(PEN_NAME_ART,      2, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        { 0x10842, PEN_SPEC(PEN_NAME_PRO_SLIM, 2, SDL_PEN_TYPE_PEN,      SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        { 0x1084a, PEN_SPEC(PEN_NAME_PRO_SLIM, 2, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        { 0x10902, PEN_SPEC(PEN_NAME_AIRBRUSH, 1, SDL_PEN_TYPE_AIRBRUSH, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK | SDL_PEN_AXIS_SLIDER_MASK) },
+        { 0x1090a, PEN_SPEC(PEN_NAME_AIRBRUSH, 1, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        { 0x12802, PEN_SPEC(PEN_NAME_INKING,   0, SDL_PEN_TYPE_PENCIL,   SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        { 0x14802, PEN_SPEC(PEN_NAME_GENERAL,  2, SDL_PEN_TYPE_PEN,      SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        { 0x1480a, PEN_SPEC(PEN_NAME_GENERAL,  2, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        { 0x16802, PEN_SPEC(PEN_NAME_PRO,      2, SDL_PEN_TYPE_PEN,      SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        { 0x1680a, PEN_SPEC(PEN_NAME_PRO,      2, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        { 0x18802, PEN_SPEC(PEN_NAME_GENERAL,  2, SDL_PEN_TYPE_PEN,      SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        { 0x1880a, PEN_SPEC(PEN_NAME_GENERAL,  2, SDL_PEN_TYPE_ERASER,   SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) },
+        { 0, 0 }
+    };
+
+    /* The list of pens is sorted, so we could do binary search, but this call should be pretty rare. */
+    for (i = 0; tools[i].wacom_id; ++i) {
+        if (tools[i].wacom_id == requested_wacom_id) {
+            Uint32 properties = tools[i].properties;
+            int name_index = (properties & PEN_SPEC_NAME_MASK) >> PEN_SPEC_NAME_SHIFT;
+
+            *num_buttons = (properties & PEN_SPEC_BUTTONS_MASK) >> PEN_SPEC_BUTTONS_SHIFT;
+            *tool_type = (properties & PEN_SPEC_TYPE_MASK) >> PEN_SPEC_TYPE_SHIFT;
+            *axes = (properties & PEN_SPEC_AXES_MASK) >> PEN_SPEC_AXES_SHIFT;
+
+            return default_pen_names[name_index];
+        }
+    }
+    return NULL;
+}
+
+void SDL_PenUpdateGUIDForGeneric(SDL_GUID *guid, Uint32 upper, Uint32 lower)
+{
+    int i;
+
+    for (i = 0; i < 4; ++i) {
+        guid->data[8 + i] = (lower >> (i * 8)) & 0xff;
+    }
+
+    for (i = 0; i < 4; ++i) {
+        guid->data[12 + i] = (upper >> (i * 8)) & 0xff;
+    }
+}
+
+void SDL_PenUpdateGUIDForType(SDL_GUID *guid, SDL_PenSubtype pentype)
+{
+    guid->data[7] = pentype;
+}
+
+void SDL_PenUpdateGUIDForWacom(SDL_GUID *guid, Uint32 wacom_devicetype_id, Uint32 wacom_serial_id)
+{
+    int i;
+
+    for (i = 0; i < 4; ++i) {
+        guid->data[0 + i] = (wacom_serial_id >> (i * 8)) & 0xff;
+    }
+
+    for (i = 0; i < 3; ++i) { /* 24 bit values */
+        guid->data[4 + i] = (wacom_devicetype_id >> (i * 8)) & 0xff;
+    }
+}
+
+int SDL_PenModifyForWacomID(SDL_Pen *pen, Uint32 wacom_devicetype_id, Uint32 *axis_flags)
+{
+    const char *name = NULL;
+    int num_buttons;
+    int tool_type;
+    int axes;
+
+#if SDL_PEN_DEBUG_UNKNOWN_WACOM
+    wacom_devicetype_id = PEN_WACOM_ID_INVALID; /* force detection to fail */
+#endif
+
+#if defined(__LINUX__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
+    /* According to Ping Cheng, the curent Wacom for Linux maintainer, device IDs on Linux
+       squeeze a "0" nibble after the 3rd (least significant) nibble.
+       This may also affect the *BSDs, so they are heuristically included here.
+       On those platforms, we first try the "patched" ID: */
+    if (0 == (wacom_devicetype_id & 0x0000f000u)) {
+        const Uint32 lower_mask = 0xfffu;
+        int wacom_devicetype_alt_id = ((wacom_devicetype_id & ~lower_mask) >> 4) | (wacom_devicetype_id & lower_mask);
+
+        name = pen_wacom_identify_tool(wacom_devicetype_alt_id, &num_buttons, &tool_type, &axes);
+        if (name) {
+            wacom_devicetype_id = wacom_devicetype_alt_id;
+        }
+    }
+#endif
+    if (name == NULL) {
+        name = pen_wacom_identify_tool(wacom_devicetype_id, &num_buttons, &tool_type, &axes);
+    }
+
+    if (!name) {
+        return SDL_FALSE;
+    }
+
+    *axis_flags = axes;
+
+    /* Override defaults */
+    if (pen->info.num_buttons == SDL_PEN_INFO_UNKNOWN) {
+        pen->info.num_buttons = num_buttons;
+    }
+    if (pen->type == SDL_PEN_TYPE_PEN) {
+        pen->type = (SDL_PenSubtype)tool_type;
+    }
+    if (pen->info.max_tilt == SDL_PEN_INFO_UNKNOWN) {
+        /* supposedly: 64 degrees left, 63 right, as reported by the Wacom X11 driver */
+	pen->info.max_tilt = 64.0f;
+    }
+    pen->info.wacom_id = wacom_devicetype_id;
+    if (0 == pen->name[0]) {
+        SDL_snprintf(pen->name, SDL_PEN_MAX_NAME,
+                     "Wacom %s%s", name, (tool_type == SDL_PEN_TYPE_ERASER) ? " Eraser" : "");
+    }
+    return SDL_TRUE;
+}

+ 336 - 0
src/events/SDL_pen_c.h

@@ -0,0 +1,336 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2023 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+
+#include "../SDL_internal.h"
+
+#ifndef SDL_pen_c_h_
+#define SDL_pen_c_h_
+
+#include "../../include/SDL3/SDL_pen.h"
+#include "SDL_mouse_c.h"
+
+/* For testing alternate code paths: */
+#define SDL_PEN_DEBUG_NOID 0           /* Pretend that pen device does not supply ID / ID is some default value \
+                                          affects: SDL_x11pen.c                                                 \
+                                                   SDL_waylandevents.c  */
+#define SDL_PEN_DEBUG_NONWACOM 0       /* Pretend that no attached device is a Wacom device \
+                                          affects: SDL_x11pen.c                             \
+                                                   SDL_waylandevents.c  */
+#define SDL_PEN_DEBUG_UNKNOWN_WACOM 0  /* Pretend that any attached Wacom device is of an unknown make \
+                                          affects: SDL_PenModifyFromWacomID() */
+#define SDL_PEN_DEBUG_NOSERIAL_WACOM 0 /* Pretend that any attached Wacom device has serial number 0 \
+                                          affects: SDL_x11pen.c                                      \
+                                                   SDL_waylandevents.c  */
+
+#define SDL_PEN_TYPE_NONE 0 /**< Pen type for non-pens (use to cancel pen registration) */
+
+#define SDL_PEN_MAX_NAME 64
+
+#define SDL_PEN_FLAG_ERROR    (1ul << 28) /* Printed an internal API usage error about this pen (used to prevent spamming) */
+#define SDL_PEN_FLAG_NEW      (1ul << 29) /* Pen was registered in most recent call to SDL_PenRegisterBegin() */
+#define SDL_PEN_FLAG_DETACHED (1ul << 30) /* Detached (not re-registered before last SDL_PenGCSweep()) */
+#define SDL_PEN_FLAG_STALE    (1ul << 31) /* Not re-registered since last SDL_PenGCMark() */
+
+typedef struct SDL_PenStatusInfo
+{
+    float x, y;
+    float axes[SDL_PEN_NUM_AXES];
+    Uint32 buttons; /* SDL_BUTTON(1) | SDL_BUTTON(2) | ... | SDL_PEN_DOWN_MASK */
+} SDL_PenStatusInfo;
+
+/**
+ * Internal (backend driver-independent) pen representation
+ *
+ * Implementation-specific backend drivers may read and write most of this structure, and
+ * are expected to initialise parts of it when registering a new pen.  They must not write
+ * to the "header" section.
+ */
+typedef struct SDL_Pen
+{
+    /* Backend driver MUST NOT not write to: */
+    struct SDL_Pen_header
+    {
+        SDL_PenID id;       /* id determines sort order unless SDL_PEN_FLAG_DETACHED is set */
+        Uint32 flags;       /* SDL_PEN_FLAG_* | SDK_PEN_DOWN_MASK | SDL_PEN_INK_MASK | SDL_PEN_ERASER_MASK | SDL_PEN_AXIS_* */
+        SDL_Window *window; /* Current SDL window for this pen, or NULL */
+    } header;
+
+    SDL_PenStatusInfo last; /* Last reported status, normally read-only for backend */
+
+    /* Backend: MUST initialise this block when pen is first registered: */
+    SDL_GUID guid; /* GUID, MUST be set by backend.
+                         MUST be unique (no other pen ID with same GUID).
+                         SHOULD be persistent across sessions. */
+
+    /* Backend: SHOULD initialise this block when pen is first registered if it can
+       Otherwise: Set to sane default values during SDL_PenModifyEnd() */
+    SDL_PenCapabilityInfo info; /* Detail information about the pen (buttons, tilt) */
+    SDL_PenSubtype type;
+    Uint8 last_mouse_button;    /* For mouse button emulation: last emulated button */
+    char *name;                 /* Preallocated; set via SDL_strlcpy(pen->name, src, SDL_PEN_MAX_NAME) */
+                                /* We hand this exact pointer to client code, so it must not be modified after
+                                   creation. */
+
+    void *deviceinfo; /* implementation-specific information */
+} SDL_Pen;
+
+/* ---- API for backend driver only ---- */
+
+/**
+ * (Only for backend driver) Look up a pen by pen ID
+ *
+ * \param instance_id A Uint32 pen identifier (driver-dependent meaning).  Must not be 0 = SDL_PEN_INVALID.
+ * The same ID is exposed to clients as SDL_PenID.
+ *
+ * The pen pointer is only valid until the next call to SDL_PenModifyEnd() or SDL_PenGCSweep()
+ *
+ * \return pen, if it exists, or NULL
+ */
+extern SDL_Pen *SDL_GetPenPtr(Uint32 instance_id);
+
+/**
+ * (Only for backend driver) Start registering a new pen or updating an existing pen.
+ *
+ * Acquires the pen mutex, which is held until the next call to SDL_PenModifyEnd() .
+ *
+ * If the PenID already exists, returns the existing entry.  Otherwise initialise fresh SDL_Pen.
+ * For new pens, sets SDL_PEN_FLAG_NEW.
+ *
+ * Usage:
+ * - SDL_PenModifyStart()
+ * - update pen object, in any order:
+ *     - SDL_PenModifyAddCapabilities()
+ *     - pen->guid (MUST be set for new pens, e.g. via ::SDL_PenUpdateGUIDForGeneric and related operations)
+ *     - pen->info.num_buttons
+ *     - pen->info.max_tilt
+ *     - pen->type
+ *     - pen->name
+ *     - pen->deviceinfo (backend-specific)
+ * - SDL_PenModifyEnd()
+ *
+ * For new pens, sets defaults for:
+ *   - num_buttons (SDL_PEN_INFO_UNKNOWN)
+ *   - max_tilt (SDL_PEN_INFO_UNKNOWN)
+ *   - pen_type (SDL_PEN_TYPE_PEN)
+ *   - Zeroes all other (non-header) fields
+ *
+ * \param instance_id Pen ID to allocate (must not be 0 = SDL_PEN_ID_INVALID)
+ * \returns SDL_Pen pointer; only valid until the call to SDL_PenModifyEnd()
+ */
+extern SDL_Pen *SDL_PenModifyBegin(Uint32 instance_id);
+
+/**
+ * (Only for backend driver) Add capabilities to a pen (cf. SDL_PenModifyBegin()).
+ *
+ * Adds capabilities to a pen obtained via SDL_PenModifyBegin().  Can be called more than once.
+ *
+ * \param pen The pen to update
+ * \param capabilities Capabilities flags, out of: SDL_PEN_AXIS_*, SDL_PEN_ERASER_MASK, SDL_PEN_INK_MASK
+ *     Setting SDL_PEN_ERASER_MASK will clear SDL_PEN_INK_MASK, and vice versa.
+ */
+extern void SDL_PenModifyAddCapabilities(SDL_Pen *pen, Uint32 capabilities);
+
+/**
+ * Set up a pen structure for a Wacom device.
+ *
+ * Some backends (e.g., XInput2, Wayland) can only partially identify the capabilities of a given
+ * pen but can identify Wacom pens and obtain their Wacom-specific device type identifiers.
+ * This function partly automates device setup in those cases.
+ *
+ * This function does NOT set up the pen's GUID.  Use ::SD_PenModifyGUIDForWacom instead.
+ *
+ * This function does NOT call SDL_PenModifyAddCapabilities() ifself, since some backends may
+ * not have access to all pen axes (e.g., Xinput2).
+ *
+ * \param pen The pen to initialise
+ * \param wacom_devicetype_id The Wacom-specific device type identifier
+ * \param[out] axis_flags The set of physically supported axes for this pen, suitable for passing to
+ *    SDL_PenModifyAddCapabilities()
+ *
+ * \returns SDL_TRUE if the device ID could be identified, otherwise SDL_FALSE
+ */
+extern int SDL_PenModifyForWacomID(SDL_Pen *pen, Uint32 wacom_devicetype_id, Uint32 *axis_flags);
+
+/**
+ * Updates a GUID for a generic pen device.
+ *
+ * Assumes that the GUID has been pre-initialised (typically to 0).
+ * Idempotent, and commutative with ::SDL_PenUpdateGUIDForWacom and ::SDL_PenUpdateGUIDForType
+ *
+ * \param[out] guid The GUID to update
+ * \param upper Upper half of the device ID (assume lower entropy than "lower"; pass 0 if not available)
+ * \param lower Lower half of the device ID (assume higher entropy than "upper")
+ */
+extern void SDL_PenUpdateGUIDForGeneric(SDL_GUID *guid, Uint32 upper, Uint32 lower);
+
+/**
+ * Updates a GUID based on a pen type
+ *
+ * Assumes that the GUID has been pre-initialised (typically to 0).
+ * Idempotent, and commutative with ::SDL_PenUpdateGUIDForWacom and ::SDL_PenUpdateGUIDForGeneric
+ *
+ * \param[out] guid The GUID to update
+ * \param pentype The pen type to insert
+ */
+extern void SDL_PenUpdateGUIDForType(SDL_GUID *guid, SDL_PenSubtype pentype);
+
+/**
+ * Updates a GUID for a Wacom pen device.
+ *
+ * Assumes that the GUID has been pre-initialised (typically to 0).
+ * Idempotent, and commutative with ::SDL_PenUpdateGUIDForType and ::SDL_PenUpdateGUIDForGeneric
+ *
+ * This update is identical to the one written by ::SDL_PenModifyFromWacomID .
+ *
+ * \param[out] guid The GUID to update
+ * \param wacom_devicetype_id The Wacom-specific device type identifier
+ * \param wacom_serial_id The Wacom-specific serial number
+ */
+extern void SDL_PenUpdateGUIDForWacom(SDL_GUID *guid, Uint32 wacom_devicetype_id, Uint32 wacom_serial_id);
+
+/**
+ * (Only for backend driver) Finish updating a pen.
+ *
+ * Releases the pen mutex acquired by SDL_PenModifyBegin() .
+ *
+ * If pen->type == SDL_PEN_TYPE_NONE, removes the pen entirely (only
+ * for new pens).  This allows backends to start registering a
+ * potential pen device and to abort if the device turns out to not be
+ * a pen.
+ *
+ * For new pens, this call will also set the following:
+ *   - name (default name, if not yet set)
+ *
+ * \param pen The pen to register.  That pointer is no longer valid after this call.
+ * \param attach Whether the pen should be attached (SDL_TRUE) or detached (SDL_FALSE).
+ *
+ * If the pen is detached or removed, it is the caller's responsibility to free
+ * and null "deviceinfo".
+ */
+extern void SDL_PenModifyEnd(SDL_Pen *pen, SDL_bool attach);
+
+/**
+ * (Only for backend driver) Mark all current pens for garbage collection.
+ *
+ * Must not be called while the pen mutex is held (by SDL_PenModifyBegin() ).
+ *
+ * SDL_PenGCMark() / SDL_PenGCSweep() provide a simple mechanism for
+ * detaching all known pens that are not discoverable.  This allows
+ * backends to use the same code for pen discovery and for
+ * hotplugging:
+ *
+ *  - SDL_PenGCMark() and start backend-specific discovery
+ *  - for each discovered pen: SDL_PenModifyBegin() + SDL_PenModifyEnd() (this will retain existing state)
+ *  - SDL_PenGCSweep()  (will now detach all pens that were not re-registered).
+ */
+extern void SDL_PenGCMark(void);
+
+/**
+ * (Only for backend driver) Detach pens that haven't been reported attached since the last call to SDL_PenGCMark().
+ *
+ * Must not be called while the pen mutex is held (by SDL_PenModifyBegin() ).
+ *
+ * See SDL_PenGCMark() for details.
+ *
+ * \param context Extra parameter to pass through to "free_deviceinfo"
+ * \param free_deviceinfo Operation to call on any non-NULL "backend.deviceinfo".
+ *
+ * \sa SDL_PenGCMark()
+ */
+extern void SDL_PenGCSweep(void *context, void (*free_deviceinfo)(Uint32 instance_id, void *deviceinfo, void *context));
+
+/**
+ * (Only for backend driver) Send a pen motion event.
+ *
+ * Suppresses pen motion events that do not change the current pen state.
+ * May also send a mouse motion event, if mouse emulation is enabled and the pen position has
+ * changed sufficiently for the motion to be visible to mouse event listeners.
+ *
+ * \param timestamp Event timestamp in nanoseconds, or 0 to ask SDL to use SDL_GetTicksNS() .
+ *        While 0 is safe to report, your backends may be able to  report more precise
+ *        timing information.
+ *        Keep in mind that you should never report timestamps that are greater than
+ *        SDL_GetTicksNS() . In particular, SDL_GetTicksNS() reports nanoseconds since the start
+ *        of the SDL session, and your backend may use a different starting point as "timestamp zero".
+ * \param instance_id Pen
+ * \param window_relative Coordinates are already window-relative
+ * \param status Coordinates and axes (buttons are ignored)
+ *
+ * \returns SDL_TRUE if at least one event was sent
+ */
+extern int SDL_SendPenMotion(Uint64 timestamp, SDL_PenID instance_id, SDL_bool window_relative, const SDL_PenStatusInfo *status);
+
+/**
+ * (Only for backend driver) Send a pen button event
+ *
+ * \param timestamp Event timestamp in nanoseconds, or 0 to ask SDL to use SDL_GetTicksNS() .
+ *        See SDL_SendPenMotion() for a discussion about how to handle timestamps.
+ * \param instance_id Pen
+ * \param state SDL_PRESSED or SDL_RELEASED
+ * \param button Button number: 1 (first physical button) etc.
+ *
+ * \returns SDL_TRUE if at least one event was sent
+ */
+extern int SDL_SendPenButton(Uint64 timestamp, SDL_PenID instance_id, Uint8 state, Uint8 button);
+
+/**
+ * (Only for backend driver) Send a pen tip event (touching or no longer touching the surface)
+ *
+ * Note: the backend should perform hit testing on window decoration elements to allow the pen
+ * to e.g. resize/move the window, just as for mouse events, unless ::SDL_SendPenTipEvent is false.
+ *
+ * \param timestamp Event timestamp in nanoseconds, or 0 to ask SDL to use SDL_GetTicksNS() .
+ *        See SDL_SendPenMotion() for a discussion about how to handle timestamps.
+ * \param instance_id Pen
+ * \param state SDL_PRESSED (for PEN_DOWN) or SDL_RELEASED (for PEN_UP)
+ *
+ * \returns SDL_TRUE if at least one event was sent
+ */
+extern int SDL_SendPenTipEvent(Uint64 timestamp, SDL_PenID instance_id, Uint8 state);
+
+/**
+ * (Only for backend driver) Check if a PEN_DOWN event should perform hit box testing.
+ *
+ * \returns SDL_TRUE if and only if the backend should perform hit testing
+ */
+extern SDL_bool SDL_PenPerformHitTest(void);
+
+/**
+ * (Only for backend driver) Send a pen window event.
+ *
+ * Tracks when a pen has entered/left a window.
+ * Don't call this when reporting new pens or removing known pens; those cases are handled automatically.
+ *
+ * \param timestamp Event timestamp in nanoseconds, or 0 to ask SDL to use SDL_GetTicksNS() .
+ *        See SDL_SendPenMotion() for a discussion about how to handle timestamps.
+ * \param instance_id Pen
+ * \param window Window to enter, or NULL to exit
+ */
+extern int SDL_SendPenWindowEvent(Uint64 timestamp, SDL_PenID instance_id, SDL_Window *window);
+
+/**
+ * Initialises the pen subsystem.
+ */
+extern void SDL_PenInit(void);
+
+#endif /* SDL_pen_c_h_ */
+
+/* vi: set ts=4 sw=4 expandtab: */

+ 356 - 107
src/video/wayland/SDL_waylandevents.c

@@ -572,13 +572,15 @@ static void pointer_handle_leave(void *data, struct wl_pointer *pointer,
     }
 }
 
-static SDL_bool ProcessHitTest(struct SDL_WaylandInput *input, uint32_t serial)
+static SDL_bool ProcessHitTest(SDL_WindowData *window_data,
+			       struct wl_seat *seat,
+			       wl_fixed_t sx_w, wl_fixed_t sy_w,
+			       uint32_t serial)
 {
-    SDL_WindowData *window_data = input->pointer_focus;
     SDL_Window *window = window_data->sdlwindow;
 
     if (window->hit_test) {
-        const SDL_Point point = { wl_fixed_to_int(input->sx_w), wl_fixed_to_int(input->sy_w) };
+        const SDL_Point point = { wl_fixed_to_int(sx_w), wl_fixed_to_int(sy_w) };
         const SDL_HitTestResult rc = window->hit_test(window, &point, window->hit_test_data);
 
         static const uint32_t directions[] = {
@@ -602,14 +604,14 @@ static SDL_bool ProcessHitTest(struct SDL_WaylandInput *input, uint32_t serial)
 #ifdef HAVE_LIBDECOR_H
             if (window_data->shell_surface_type == WAYLAND_SURFACE_LIBDECOR) {
                 if (window_data->shell_surface.libdecor.frame) {
-                    libdecor_frame_move(window_data->shell_surface.libdecor.frame, input->seat, serial);
+                    libdecor_frame_move(window_data->shell_surface.libdecor.frame, seat, serial);
                 }
             } else
 #endif
                 if (window_data->shell_surface_type == WAYLAND_SURFACE_XDG_TOPLEVEL) {
                 if (window_data->shell_surface.xdg.roleobj.toplevel) {
                     xdg_toplevel_move(window_data->shell_surface.xdg.roleobj.toplevel,
-                                      input->seat,
+                                      seat,
                                       serial);
                 }
             }
@@ -626,14 +628,14 @@ static SDL_bool ProcessHitTest(struct SDL_WaylandInput *input, uint32_t serial)
 #ifdef HAVE_LIBDECOR_H
             if (window_data->shell_surface_type == WAYLAND_SURFACE_LIBDECOR) {
                 if (window_data->shell_surface.libdecor.frame) {
-                    libdecor_frame_resize(window_data->shell_surface.libdecor.frame, input->seat, serial, directions_libdecor[rc - SDL_HITTEST_RESIZE_TOPLEFT]);
+                    libdecor_frame_resize(window_data->shell_surface.libdecor.frame, seat, serial, directions_libdecor[rc - SDL_HITTEST_RESIZE_TOPLEFT]);
                 }
             } else
 #endif
                 if (window_data->shell_surface_type == WAYLAND_SURFACE_XDG_TOPLEVEL) {
                 if (window_data->shell_surface.xdg.roleobj.toplevel) {
                     xdg_toplevel_resize(window_data->shell_surface.xdg.roleobj.toplevel,
-                                        input->seat,
+                                        seat,
                                         serial,
                                         directions[rc - SDL_HITTEST_RESIZE_TOPLEFT]);
                 }
@@ -660,7 +662,7 @@ static void pointer_handle_button_common(struct SDL_WaylandInput *input, uint32_
         switch (button) {
         case BTN_LEFT:
             sdl_button = SDL_BUTTON_LEFT;
-            if (ProcessHitTest(input, serial)) {
+            if (ProcessHitTest(input->pointer_focus, input->seat, input->sx_w, input->sy_w, serial)) {
                 return; /* don't pass this event on to app. */
             }
             break;
@@ -832,19 +834,19 @@ static void pointer_handle_axis(void *data, struct wl_pointer *pointer,
 }
 
 static void pointer_handle_axis_relative_direction(void *data, struct wl_pointer *pointer,
-                    uint32_t axis, uint32_t axis_relative_direction)
+                                                   uint32_t axis, uint32_t axis_relative_direction)
 {
     struct SDL_WaylandInput *input = data;
     if (axis != WL_POINTER_AXIS_VERTICAL_SCROLL) {
         return;
     }
     switch (axis_relative_direction) {
-        case WL_POINTER_AXIS_RELATIVE_DIRECTION_IDENTICAL:
-            input->pointer_curr_axis_info.direction = SDL_MOUSEWHEEL_NORMAL;
-            break;
-        case WL_POINTER_AXIS_RELATIVE_DIRECTION_INVERTED:
-            input->pointer_curr_axis_info.direction = SDL_MOUSEWHEEL_FLIPPED;
-            break;
+    case WL_POINTER_AXIS_RELATIVE_DIRECTION_IDENTICAL:
+        input->pointer_curr_axis_info.direction = SDL_MOUSEWHEEL_NORMAL;
+        break;
+    case WL_POINTER_AXIS_RELATIVE_DIRECTION_INVERTED:
+        input->pointer_curr_axis_info.direction = SDL_MOUSEWHEEL_FLIPPED;
+        break;
     }
 }
 
@@ -928,11 +930,11 @@ static const struct wl_pointer_listener pointer_listener = {
     pointer_handle_motion,
     pointer_handle_button,
     pointer_handle_axis,
-    pointer_handle_frame,         /* Version 5 */
-    pointer_handle_axis_source,   /* Version 5 */
-    pointer_handle_axis_stop,     /* Version 5 */
-    pointer_handle_axis_discrete, /* Version 5 */
-    pointer_handle_axis_value120,  /* Version 8 */
+    pointer_handle_frame,                  /* Version 5 */
+    pointer_handle_axis_source,            /* Version 5 */
+    pointer_handle_axis_stop,              /* Version 5 */
+    pointer_handle_axis_discrete,          /* Version 5 */
+    pointer_handle_axis_value120,          /* Version 8 */
     pointer_handle_axis_relative_direction /* Version 9 */
 };
 
@@ -2361,40 +2363,202 @@ void Wayland_add_text_input_manager(SDL_VideoData *d, uint32_t id, uint32_t vers
     }
 }
 
+static SDL_PenID Wayland_get_penid(void *data, struct zwp_tablet_tool_v2 *tool)
+{
+    struct SDL_WaylandTool *sdltool = data;
+    return sdltool->penid;
+}
+
+/* For registering pens */
+static SDL_Pen *Wayland_get_current_pen(void *data, struct zwp_tablet_tool_v2 *tool)
+{
+    struct SDL_WaylandTool *sdltool = data;
+    struct SDL_WaylandTabletInput *input = sdltool->tablet;
+
+    if (!input->current_pen.builder) {
+        /* Starting new pen or updating one? */
+        SDL_PenID penid = sdltool->penid;
+
+        if (penid == 0) {
+            /* Found completely new pen? */
+            penid = ++input->num_pens;
+            sdltool->penid = penid;
+        }
+        input->current_pen.builder = SDL_GetPenPtr(penid);
+        if (!input->current_pen.builder) {
+            /* Must register as new pen */
+            input->current_pen.builder = SDL_PenModifyBegin(penid);
+        }
+    }
+    return input->current_pen.builder;
+}
+
 static void tablet_tool_handle_type(void *data, struct zwp_tablet_tool_v2 *tool, uint32_t type)
 {
-    /* unimplemented */
+    SDL_Pen *pen = Wayland_get_current_pen(data, tool);
+
+    switch (type) {
+    case ZWP_TABLET_TOOL_V2_TYPE_ERASER:
+        pen->type = SDL_PEN_TYPE_ERASER;
+        break;
+
+    case ZWP_TABLET_TOOL_V2_TYPE_PEN:
+        pen->type = SDL_PEN_TYPE_PEN;
+        break;
+
+    case ZWP_TABLET_TOOL_V2_TYPE_PENCIL:
+        pen->type = SDL_PEN_TYPE_PENCIL;
+        break;
+
+    case ZWP_TABLET_TOOL_V2_TYPE_AIRBRUSH:
+        pen->type = SDL_PEN_TYPE_AIRBRUSH;
+        break;
+
+    case ZWP_TABLET_TOOL_V2_TYPE_BRUSH:
+        pen->type = SDL_PEN_TYPE_BRUSH;
+        break;
+
+    case ZWP_TABLET_TOOL_V2_TYPE_FINGER:
+    case ZWP_TABLET_TOOL_V2_TYPE_MOUSE:
+    case ZWP_TABLET_TOOL_V2_TYPE_LENS:
+    default:
+        pen->type = SDL_PEN_TYPE_NONE; /* Mark for deregistration */
+    }
+
+    SDL_PenUpdateGUIDForType(&pen->guid, pen->type);
 }
 
 static void tablet_tool_handle_hardware_serial(void *data, struct zwp_tablet_tool_v2 *tool, uint32_t serial_hi, uint32_t serial_lo)
 {
-    /* unimplemented */
+#if !(SDL_PEN_DEBUG_NOID)
+    struct SDL_WaylandTool *sdltool = data;
+    struct SDL_WaylandTabletInput *input = sdltool->tablet;
+
+    if (!input->current_pen.builder_guid_complete) {
+        SDL_Pen *pen = Wayland_get_current_pen(data, tool);
+        SDL_PenUpdateGUIDForGeneric(&pen->guid, serial_hi, serial_lo);
+        if (serial_hi || serial_lo) {
+            input->current_pen.builder_guid_complete = SDL_TRUE;
+        }
+    }
+#endif
 }
 
 static void tablet_tool_handle_hardware_id_wacom(void *data, struct zwp_tablet_tool_v2 *tool, uint32_t id_hi, uint32_t id_lo)
 {
-    /* unimplemented */
+#if !(SDL_PEN_DEBUG_NOID | SDL_PEN_DEBUG_NONWACOM)
+    struct SDL_WaylandTool *sdltool = data;
+    struct SDL_WaylandTabletInput *input = sdltool->tablet;
+    SDL_Pen *pen = Wayland_get_current_pen(data, tool);
+    Uint32 axis_flags;
+
+#if SDL_PEN_DEBUG_NOSERIAL_WACOM /* Check: have we disabled pen serial ID decoding for testing? */
+    id_hi = 0;
+#endif
+
+    SDL_PenUpdateGUIDForWacom(&pen->guid, id_lo, id_hi);
+    if (id_hi) { /* Have a serial number? */
+        input->current_pen.builder_guid_complete = SDL_TRUE;
+    }
+
+    if (SDL_PenModifyForWacomID(pen, id_lo, &axis_flags)) {
+        SDL_PenModifyAddCapabilities(pen, axis_flags);
+    }
+#endif
 }
 
 static void tablet_tool_handle_capability(void *data, struct zwp_tablet_tool_v2 *tool, uint32_t capability)
 {
-    /* unimplemented */
+    SDL_Pen *pen = Wayland_get_current_pen(data, tool);
+
+    switch (capability) {
+    case ZWP_TABLET_TOOL_V2_CAPABILITY_TILT:
+        SDL_PenModifyAddCapabilities(pen, SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK);
+        break;
+
+    case ZWP_TABLET_TOOL_V2_CAPABILITY_PRESSURE:
+        SDL_PenModifyAddCapabilities(pen, SDL_PEN_AXIS_PRESSURE_MASK);
+        break;
+
+    case ZWP_TABLET_TOOL_V2_CAPABILITY_DISTANCE:
+        SDL_PenModifyAddCapabilities(pen, SDL_PEN_AXIS_DISTANCE_MASK);
+        break;
+
+    case ZWP_TABLET_TOOL_V2_CAPABILITY_ROTATION:
+        SDL_PenModifyAddCapabilities(pen, SDL_PEN_AXIS_ROTATION_MASK);
+        break;
+
+    case ZWP_TABLET_TOOL_V2_CAPABILITY_SLIDER:
+        SDL_PenModifyAddCapabilities(pen, SDL_PEN_AXIS_SLIDER_MASK);
+        break;
+
+    case ZWP_TABLET_TOOL_V2_CAPABILITY_WHEEL:
+        /* Presumably for tools other than pens? */
+        break;
+
+    default:
+        break;
+    }
+}
+
+static void Wayland_tool_builder_reset(struct SDL_WaylandTabletInput *input)
+{
+    input->current_pen.builder = NULL;
+    input->current_pen.builder_guid_complete = SDL_FALSE;
 }
 
 static void tablet_tool_handle_done(void *data, struct zwp_tablet_tool_v2 *tool)
 {
-    /* unimplemented */
+    SDL_Pen *pen = Wayland_get_current_pen(data, tool);
+    struct SDL_WaylandTool *sdltool = data;
+    struct SDL_WaylandTabletInput *input = sdltool->tablet;
+
+    if (!input->current_pen.builder_guid_complete) {
+        /* No complete GUID?  Use tablet and tool device index */
+        SDL_PenUpdateGUIDForGeneric(&pen->guid, input->id, sdltool->penid);
+    }
+
+    SDL_PenModifyEnd(pen, SDL_TRUE);
+
+    Wayland_tool_builder_reset(input);
 }
 
+static void Wayland_tool_destroy(struct zwp_tablet_tool_v2 *tool)
+{
+    if (tool) {
+        struct SDL_WaylandTool *waypen = zwp_tablet_tool_v2_get_user_data(tool);
+        if (waypen) {
+            SDL_free(waypen);
+        }
+        zwp_tablet_tool_v2_destroy(tool);
+    }
+}
+
+static void tablet_object_list_remove(struct SDL_WaylandTabletObjectListNode *head, void *object);
+
 static void tablet_tool_handle_removed(void *data, struct zwp_tablet_tool_v2 *tool)
 {
-    /* unimplemented */
+    struct SDL_WaylandTool *waypen = zwp_tablet_tool_v2_get_user_data(tool);
+    struct SDL_WaylandTool *sdltool = data;
+    struct SDL_WaylandTabletInput *input = sdltool->tablet;
+    SDL_Pen *pen = Wayland_get_current_pen(data, tool);
+    if (pen) {
+        SDL_PenModifyEnd(pen, SDL_FALSE);
+        Wayland_tool_builder_reset(waypen->tablet);
+        Wayland_tool_destroy(tool);
+    } else {
+        zwp_tablet_tool_v2_destroy(tool);
+    }
+
+    tablet_object_list_remove(input->tools, tool);
 }
 
 static void tablet_tool_handle_proximity_in(void *data, struct zwp_tablet_tool_v2 *tool, uint32_t serial, struct zwp_tablet_v2 *tablet, struct wl_surface *surface)
 {
-    struct SDL_WaylandTabletInput *input = data;
+    struct SDL_WaylandTool *sdltool = data;
+    struct SDL_WaylandTabletInput *input = sdltool->tablet;
     SDL_WindowData *window;
+    SDL_PenID penid = Wayland_get_penid(data, tool);
 
     if (!surface) {
         return;
@@ -2410,155 +2574,214 @@ static void tablet_tool_handle_proximity_in(void *data, struct zwp_tablet_tool_v
         input->tool_focus = window;
         input->tool_prox_serial = serial;
 
-        input->is_down = SDL_FALSE;
-
-        input->btn_stylus = SDL_FALSE;
-        input->btn_stylus2 = SDL_FALSE;
-        input->btn_stylus3 = SDL_FALSE;
-
-        SDL_SetMouseFocus(window->sdlwindow);
+        if (penid) {
+            SDL_SendPenWindowEvent(0, penid, window->sdlwindow);
+        } else {
+            SDL_SetMouseFocus(window->sdlwindow);
+        }
         SDL_SetCursor(NULL);
     }
 }
 
 static void tablet_tool_handle_proximity_out(void *data, struct zwp_tablet_tool_v2 *tool)
 {
-    struct SDL_WaylandTabletInput *input = data;
-
+    struct SDL_WaylandTool *sdltool = data;
+    struct SDL_WaylandTabletInput *input = sdltool->tablet;
+    SDL_PenID penid = Wayland_get_penid(data, tool);
     if (input->tool_focus) {
-        SDL_SetMouseFocus(NULL);
+        if (penid) {
+            SDL_SendPenWindowEvent(0, penid, NULL);
+        } else {
+            SDL_SetMouseFocus(NULL);
+        }
         input->tool_focus = NULL;
     }
 }
 
-static uint32_t tablet_tool_btn_to_sdl_button(struct SDL_WaylandTabletInput *input)
-{
-    unsigned int tool_btn = input->btn_stylus3 << 2 | input->btn_stylus2 << 1 | input->btn_stylus << 0;
-    switch (tool_btn) {
-    case 0b000:
-        return SDL_BUTTON_LEFT;
-    case 0b001:
-        return SDL_BUTTON_RIGHT;
-    case 0b010:
-        return SDL_BUTTON_MIDDLE;
-    case 0b100:
-        return SDL_BUTTON_X1;
-    default:
-        return SDL_BUTTON_LEFT;
-    }
-}
-
 static void tablet_tool_handle_down(void *data, struct zwp_tablet_tool_v2 *tool, uint32_t serial)
 {
-    struct SDL_WaylandTabletInput *input = data;
-    SDL_WindowData *window = input->tool_focus;
-    input->is_down = SDL_TRUE;
-    Wayland_UpdateImplicitGrabSerial(input->sdlWaylandInput, serial);
-    if (!window) {
-        /* tablet_tool_handle_proximity_out gets called when moving over the libdecoration csd.
-         * that sets input->tool_focus (window) to NULL, but handle_{down,up} events are still
-         * received. To prevent SIGSEGV this returns when this is the case.
-         */
-        return;
-    }
+    struct SDL_WaylandTool *sdltool = data;
+    struct SDL_WaylandTabletInput *input = sdltool->tablet;
+
+    input->current_pen.buttons_pressed |= SDL_PEN_DOWN_MASK;
 
-    SDL_SendMouseButton(0, window->sdlwindow, 0, SDL_PRESSED, tablet_tool_btn_to_sdl_button(input));
+    input->current_pen.serial = serial;
 }
 
 static void tablet_tool_handle_up(void *data, struct zwp_tablet_tool_v2 *tool)
 {
-    struct SDL_WaylandTabletInput *input = data;
-    SDL_WindowData *window = input->tool_focus;
-
-    input->is_down = SDL_FALSE;
-
-    if (!window) {
-        /* tablet_tool_handle_proximity_out gets called when moving over the libdecoration csd.
-         * that sets input->tool_focus (window) to NULL, but handle_{down,up} events are still
-         * received. To prevent SIGSEGV this returns when this is the case.
-         */
-        return;
-    }
-
-    SDL_SendMouseButton(0, window->sdlwindow, 0, SDL_RELEASED, tablet_tool_btn_to_sdl_button(input));
+    struct SDL_WaylandTool *sdltool = data;
+    struct SDL_WaylandTabletInput *input = sdltool->tablet;
+    input->current_pen.buttons_released |= SDL_PEN_DOWN_MASK;
 }
 
 static void tablet_tool_handle_motion(void *data, struct zwp_tablet_tool_v2 *tool, wl_fixed_t sx_w, wl_fixed_t sy_w)
 {
-    struct SDL_WaylandTabletInput *input = data;
+    struct SDL_WaylandTool *sdltool = data;
+    struct SDL_WaylandTabletInput *input = sdltool->tablet;
     SDL_WindowData *window = input->tool_focus;
+    SDL_PenID penid = Wayland_get_penid(data, tool);
 
     input->sx_w = sx_w;
     input->sy_w = sy_w;
+
     if (input->tool_focus) {
-        float sx = (float)(wl_fixed_to_double(sx_w) * window->pointer_scale_x);
-        float sy = (float)(wl_fixed_to_double(sy_w) * window->pointer_scale_y);
-        SDL_SendMouseMotion(0, window->sdlwindow, 0, 0, sx, sy);
+        const float sx_f = (float)wl_fixed_to_double(sx_w);
+        const float sy_f = (float)wl_fixed_to_double(sy_w);
+        const float sx = sx_f * window->pointer_scale_x;
+        const float sy = sy_f * window->pointer_scale_y;
+
+        if (penid != SDL_PEN_INVALID) {
+            input->current_pen.update_status.x = sx;
+            input->current_pen.update_status.y = sy;
+            input->current_pen.update_window = window;
+        } else {
+            /* Plain mouse event */
+            SDL_SendMouseMotion(0, window->sdlwindow, 0, 0, sx, sy);
+        }
     }
 }
 
 static void tablet_tool_handle_pressure(void *data, struct zwp_tablet_tool_v2 *tool, uint32_t pressure)
 {
-    /* unimplemented */
+    struct SDL_WaylandTool *sdltool = data;
+    struct SDL_WaylandTabletInput *input = sdltool->tablet;
+    input->current_pen.update_status.axes[SDL_PEN_AXIS_PRESSURE] = pressure / 65535.0f;
+    if (pressure) {
+        input->current_pen.update_status.axes[SDL_PEN_AXIS_DISTANCE] = 0.0f;
+    }
 }
 
 static void tablet_tool_handle_distance(void *data, struct zwp_tablet_tool_v2 *tool, uint32_t distance)
 {
-    /* unimplemented */
+    struct SDL_WaylandTool *sdltool = data;
+    struct SDL_WaylandTabletInput *input = sdltool->tablet;
+    input->current_pen.update_status.axes[SDL_PEN_AXIS_DISTANCE] = distance / 65535.0f;
+    if (distance) {
+        input->current_pen.update_status.axes[SDL_PEN_AXIS_PRESSURE] = 0.0f;
+    }
 }
 
 static void tablet_tool_handle_tilt(void *data, struct zwp_tablet_tool_v2 *tool, wl_fixed_t xtilt, wl_fixed_t ytilt)
 {
-    /* unimplemented */
+    struct SDL_WaylandTool *sdltool = data;
+    struct SDL_WaylandTabletInput *input = sdltool->tablet;
+
+    input->current_pen.update_status.axes[SDL_PEN_AXIS_XTILT] = (float)(wl_fixed_to_double(xtilt));
+    input->current_pen.update_status.axes[SDL_PEN_AXIS_YTILT] = (float)(wl_fixed_to_double(ytilt));
 }
 
 static void tablet_tool_handle_button(void *data, struct zwp_tablet_tool_v2 *tool, uint32_t serial, uint32_t button, uint32_t state)
 {
-    struct SDL_WaylandTabletInput *input = (struct SDL_WaylandTabletInput*)data;
-
-    if (input->is_down) {
-        tablet_tool_handle_up(data, tool);
-        input->is_down = SDL_TRUE;
-    }
+    struct SDL_WaylandTool *sdltool = data;
+    struct SDL_WaylandTabletInput *input = sdltool->tablet;
+    Uint16 mask = 0;
+    SDL_bool pressed = state == ZWP_TABLET_PAD_V2_BUTTON_STATE_PRESSED ? SDL_TRUE : SDL_FALSE;
 
-    Wayland_UpdateImplicitGrabSerial(input->sdlWaylandInput, serial);
+    /* record event serial number to report it later in tablet_tool_handle_frame() */
+    input->current_pen.serial = serial;
 
     switch (button) {
     /* see %{_includedir}/linux/input-event-codes.h */
     case 0x14b: /* BTN_STYLUS */
-        input->btn_stylus = (state == ZWP_TABLET_PAD_V2_BUTTON_STATE_PRESSED);
+        mask = SDL_BUTTON_LMASK;
         break;
     case 0x14c: /* BTN_STYLUS2 */
-        input->btn_stylus2 = (state == ZWP_TABLET_PAD_V2_BUTTON_STATE_PRESSED);
+        mask = SDL_BUTTON_MMASK;
         break;
     case 0x149: /* BTN_STYLUS3 */
-        input->btn_stylus3 = (state == ZWP_TABLET_PAD_V2_BUTTON_STATE_PRESSED);
+        mask = SDL_BUTTON_RMASK;
         break;
     }
 
-    if (input->is_down) {
-        tablet_tool_handle_down(data, tool, serial);
+    if (pressed) {
+        input->current_pen.buttons_pressed |= mask;
+    } else {
+        input->current_pen.buttons_released |= mask;
     }
 }
 
 static void tablet_tool_handle_rotation(void *data, struct zwp_tablet_tool_v2 *tool, wl_fixed_t degrees)
 {
-    /* unimplemented */
+    struct SDL_WaylandTool *sdltool = data;
+    struct SDL_WaylandTabletInput *input = sdltool->tablet;
+    float rotation = (float)(wl_fixed_to_double(degrees));
+
+    /* map to -180.0f ... 179.0f range: */
+    input->current_pen.update_status.axes[SDL_PEN_AXIS_ROTATION] = rotation > 180.0f ? rotation - 360.0f : rotation;
 }
 
 static void tablet_tool_handle_slider(void *data, struct zwp_tablet_tool_v2 *tool, int32_t position)
 {
-    /* unimplemented */
+    struct SDL_WaylandTool *sdltool = data;
+    struct SDL_WaylandTabletInput *input = sdltool->tablet;
+    input->current_pen.update_status.axes[SDL_PEN_AXIS_SLIDER] = position / 65535.0;
 }
 
 static void tablet_tool_handle_wheel(void *data, struct zwp_tablet_tool_v2 *tool, int32_t degrees, int32_t clicks)
 {
-    /* unimplemented */
+    /* not supported at the moment */
 }
 
 static void tablet_tool_handle_frame(void *data, struct zwp_tablet_tool_v2 *tool, uint32_t time)
 {
-    /* unimplemented */
+    struct SDL_WaylandTool *sdltool = data;
+    struct SDL_WaylandTabletInput *input = sdltool->tablet;
+    SDL_PenID penid = Wayland_get_penid(data, tool);
+    SDL_WindowData *window = input->current_pen.update_window;
+    SDL_PenStatusInfo *status = &input->current_pen.update_status;
+    int button;
+    int button_mask;
+    Uint64 timestamp = Wayland_GetEventTimestamp(SDL_MS_TO_NS(time));
+
+    if (penid == 0 || !window) { /* Not a pen, or event reported out of focus */
+        return;
+    }
+    /* window == input->tool_focus */
+
+    /* All newly released buttons + PEN_UP event */
+    button_mask = input->current_pen.buttons_released;
+    if (button_mask & SDL_PEN_DOWN_MASK) {
+	/* Perform hit test, if appropriate */
+	if (!SDL_PenPerformHitTest()
+	    || !ProcessHitTest(window, input->sdlWaylandInput->seat, input->sx_w, input->sy_w, input->current_pen.serial)) {
+	    SDL_SendPenTipEvent(timestamp, penid, SDL_RELEASED);
+	}
+    }
+    button_mask &= ~SDL_PEN_DOWN_MASK;
+
+    for (button = 1; button_mask; ++button, button_mask >>= 1) {
+        if (button_mask & 1) {
+            SDL_SendPenButton(timestamp, penid, SDL_RELEASED, button);
+        }
+    }
+
+    /* All newly pressed buttons + PEN_DOWN event */
+    button_mask = input->current_pen.buttons_pressed;
+    if (button_mask & SDL_PEN_DOWN_MASK) {
+	/* Perform hit test, if appropriate */
+	if (!SDL_PenPerformHitTest()
+	    || !ProcessHitTest(window, input->sdlWaylandInput->seat, input->sx_w, input->sy_w, input->current_pen.serial)) {
+	    SDL_SendPenTipEvent(timestamp, penid, SDL_PRESSED);
+	}
+    }
+    button_mask &= ~SDL_PEN_DOWN_MASK;
+
+    for (button = 1; button_mask; ++button, button_mask >>= 1) {
+        if (button_mask & 1) {
+            SDL_SendPenButton(timestamp, penid, SDL_PRESSED, button);
+        }
+    }
+
+    SDL_SendPenMotion(timestamp, penid, SDL_TRUE, status);
+
+    /* Wayland_UpdateImplicitGrabSerial will ignore serial 0, so it is safe to call with the default value */
+    Wayland_UpdateImplicitGrabSerial(input->sdlWaylandInput, input->current_pen.serial);
+
+    /* Reset masks for next tool frame */
+    input->current_pen.buttons_pressed = 0;
+    input->current_pen.buttons_released = 0;
+    input->current_pen.serial = 0;
 }
 
 static const struct zwp_tablet_tool_v2_listener tablet_tool_listener = {
@@ -2624,6 +2847,26 @@ static void tablet_object_list_destroy(struct SDL_WaylandTabletObjectListNode *h
     }
 }
 
+void tablet_object_list_remove(struct SDL_WaylandTabletObjectListNode *head, void *object)
+{
+    struct SDL_WaylandTabletObjectListNode **head_p = &head;
+    while (*head_p && (*head_p)->object != object) {
+        head_p = &((*head_p)->next);
+    }
+
+    if (*head_p) {
+        struct SDL_WaylandTabletObjectListNode *object_head = *head_p;
+
+        if (object_head == head) {
+            /* Must not remove head node */
+            head->object = NULL;
+        } else {
+            *head_p = object_head->next;
+            SDL_free(object_head);
+        }
+    }
+}
+
 static void tablet_seat_handle_tablet_added(void *data, struct zwp_tablet_seat_v2 *seat, struct zwp_tablet_v2 *tablet)
 {
     struct SDL_WaylandTabletInput *input = data;
@@ -2634,9 +2877,12 @@ static void tablet_seat_handle_tablet_added(void *data, struct zwp_tablet_seat_v
 static void tablet_seat_handle_tool_added(void *data, struct zwp_tablet_seat_v2 *seat, struct zwp_tablet_tool_v2 *tool)
 {
     struct SDL_WaylandTabletInput *input = data;
+    struct SDL_WaylandTool *sdltool = SDL_calloc(1, sizeof(struct SDL_WaylandTool));
+
+    zwp_tablet_tool_v2_add_listener(tool, &tablet_tool_listener, sdltool);
+    zwp_tablet_tool_v2_set_user_data(tool, sdltool);
 
-    zwp_tablet_tool_v2_add_listener(tool, &tablet_tool_listener, data);
-    zwp_tablet_tool_v2_set_user_data(tool, data);
+    sdltool->tablet = input;
 
     tablet_object_list_append(input->tools, tool);
 }
@@ -2657,6 +2903,7 @@ static const struct zwp_tablet_seat_v2_listener tablet_seat_listener = {
 void Wayland_input_add_tablet(struct SDL_WaylandInput *input, struct SDL_WaylandTabletManager *tablet_manager)
 {
     struct SDL_WaylandTabletInput *tablet_input;
+    static Uint32 num_tablets = 0;
 
     if (!tablet_manager || !input || !input->seat) {
         return;
@@ -2675,6 +2922,7 @@ void Wayland_input_add_tablet(struct SDL_WaylandInput *input, struct SDL_Wayland
     tablet_input->tablets = tablet_object_list_new_node(NULL);
     tablet_input->tools = tablet_object_list_new_node(NULL);
     tablet_input->pads = tablet_object_list_new_node(NULL);
+    tablet_input->id = num_tablets++;
 
     zwp_tablet_seat_v2_add_listener((struct zwp_tablet_seat_v2 *)tablet_input->seat, &tablet_seat_listener, tablet_input);
 }
@@ -2683,7 +2931,7 @@ void Wayland_input_add_tablet(struct SDL_WaylandInput *input, struct SDL_Wayland
 void Wayland_input_destroy_tablet(struct SDL_WaylandInput *input)
 {
     tablet_object_list_destroy(input->tablet->pads, TABLET_OBJECT_LIST_DELETER(zwp_tablet_pad_v2_destroy));
-    tablet_object_list_destroy(input->tablet->tools, TABLET_OBJECT_LIST_DELETER(zwp_tablet_tool_v2_destroy));
+    tablet_object_list_destroy(input->tablet->tools, TABLET_OBJECT_LIST_DELETER(Wayland_tool_destroy));
     tablet_object_list_destroy(input->tablet->tablets, TABLET_OBJECT_LIST_DELETER(zwp_tablet_v2_destroy));
 
     zwp_tablet_seat_v2_destroy(input->tablet->seat);
@@ -2808,7 +3056,8 @@ void Wayland_display_destroy_input(SDL_VideoData *d)
             wl_touch_destroy(input->touch);
         }
 
-        wl_list_for_each_safe (tp, tmp, &touch_points, link) {
+        wl_list_for_each_safe(tp, tmp, &touch_points, link)
+        {
             WAYLAND_wl_list_remove(&tp->link);
             SDL_free(tp);
         }

+ 26 - 12
src/video/wayland/SDL_waylandevents_c.h

@@ -25,6 +25,7 @@
 #define SDL_waylandevents_h_
 
 #include "../../events/SDL_mouse_c.h"
+#include "../../events/SDL_pen_c.h"
 
 #include "SDL_waylandvideo.h"
 #include "SDL_waylandwindow.h"
@@ -55,31 +56,38 @@ struct SDL_WaylandTabletInput
     struct SDL_WaylandTabletObjectListNode *tools;
     struct SDL_WaylandTabletObjectListNode *pads;
 
+    Uint32 id;
+    Uint32 num_pens; /* next pen ID is num_pens+1 */
+    struct SDL_WaylandCurrentPen
+    {
+        SDL_Pen *builder;                /* pen that is being defined or receiving updates, if any */
+        SDL_bool builder_guid_complete;  /* have complete/precise GUID information */
+        SDL_PenStatusInfo update_status; /* collects pen update information before sending event */
+	Uint16 buttons_pressed;        /* Mask of newly pressed buttons, plus SDL_PEN_DOWN_MASK for PEN_DOWN */
+        Uint16 buttons_released;       /* Mask of newly pressed buttons, plus SDL_PEN_DOWN_MASK for PEN_UP */
+        Uint32 serial;                 /* Most recent serial event number observed, or 0 */
+        SDL_WindowData *update_window; /* NULL while no event is in progress, otherwise the affected window */
+    } current_pen;
+
     SDL_WindowData *tool_focus;
     uint32_t tool_prox_serial;
 
-    /* Last motion location */
+    /* Last motion end location (kept separate from sx_w, sy_w for the mouse pointer) */
     wl_fixed_t sx_w;
     wl_fixed_t sy_w;
-
-    SDL_bool is_down;
-
-    SDL_bool btn_stylus;
-    SDL_bool btn_stylus2;
-    SDL_bool btn_stylus3;
 };
 
 typedef struct
 {
-    int32_t repeat_rate;      /* Repeat rate in range of [1, 1000] character(s) per second */
-    int32_t repeat_delay_ms;  /* Time to first repeat event in milliseconds */
+    int32_t repeat_rate;     /* Repeat rate in range of [1, 1000] character(s) per second */
+    int32_t repeat_delay_ms; /* Time to first repeat event in milliseconds */
     SDL_bool is_initialized;
 
     SDL_bool is_key_down;
     uint32_t key;
-    Uint64 wl_press_time_ns;   /* Key press time as reported by the Wayland API */
-    Uint64 sdl_press_time_ns;  /* Key press time expressed in SDL ticks */
-    Uint64 next_repeat_ns;     /* Next repeat event in nanoseconds */
+    Uint64 wl_press_time_ns;  /* Key press time as reported by the Wayland API */
+    Uint64 sdl_press_time_ns; /* Key press time expressed in SDL ticks */
+    Uint64 next_repeat_ns;    /* Next repeat event in nanoseconds */
     uint32_t scancode;
     char text[8];
 } SDL_WaylandKeyboardRepeat;
@@ -169,6 +177,12 @@ struct SDL_WaylandInput
     SDL_Keymod locked_modifiers;
 };
 
+struct SDL_WaylandTool
+{
+    SDL_PenID penid;
+    struct SDL_WaylandTabletInput *tablet;
+};
+
 extern Uint64 Wayland_GetTouchTimestamp(struct SDL_WaylandInput *input, Uint32 wl_timestamp_ms);
 
 extern void Wayland_PumpEvents(SDL_VideoDevice *_this);

+ 109 - 76
src/video/x11/SDL_x11events.c

@@ -179,14 +179,14 @@ static SDL_bool X11_KeyRepeat(Display *display, XEvent *event)
     return d.found;
 }
 
-static SDL_bool X11_IsWheelEvent(Display *display, XEvent *event, int *xticks, int *yticks)
+static SDL_bool X11_IsWheelEvent(Display *display, int button, int *xticks, int *yticks)
 {
     /* according to the xlib docs, no specific mouse wheel events exist.
        However, the defacto standard is that the vertical wheel is X buttons
        4 (up) and 5 (down) and a horizontal wheel is 6 (left) and 7 (right). */
 
     /* Xlib defines "Button1" through 5, so we just use literals here. */
-    switch (event->xbutton.button) {
+    switch (button) {
     case 4:
         *yticks = 1;
         return SDL_TRUE;
@@ -333,13 +333,15 @@ void SDL_SetX11EventHook(SDL_X11EventHook callback, void *userdata)
 }
 
 #ifdef SDL_VIDEO_DRIVER_X11_SUPPORTS_GENERIC_EVENTS
-static void X11_HandleGenericEvent(SDL_VideoData *videodata, XEvent *xev)
+static void X11_HandleGenericEvent(SDL_VideoDevice *_this, XEvent *xev)
 {
+    SDL_VideoData *videodata = (SDL_VideoData *)_this->driverdata;
+
     /* event is a union, so cookie == &event, but this is type safe. */
     XGenericEventCookie *cookie = &xev->xcookie;
     if (X11_XGetEventData(videodata->display, cookie)) {
         if (!g_X11EventHook || g_X11EventHook(g_X11EventHookData, xev)) {
-            X11_HandleXinput2Event(videodata, cookie);
+            X11_HandleXinput2Event(_this, cookie);
         }
         X11_XFreeEventData(videodata->display, cookie);
     }
@@ -557,12 +559,12 @@ static void InitiateWindowResize(SDL_VideoDevice *_this, const SDL_WindowData *d
     X11_XSync(display, 0);
 }
 
-static SDL_bool ProcessHitTest(SDL_VideoDevice *_this, const SDL_WindowData *data, const XEvent *xev)
+SDL_bool X11_ProcessHitTest(SDL_VideoDevice *_this, const SDL_WindowData *data, const float x, const float y)
 {
     SDL_Window *window = data->window;
 
     if (window->hit_test) {
-        const SDL_Point point = { xev->xbutton.x, xev->xbutton.y };
+        const SDL_Point point = { x, y };
         const SDL_HitTestResult rc = window->hit_test(window, &point, window->hit_test_data);
         static const int directions[] = {
             _NET_WM_MOVERESIZE_SIZE_TOPLEFT, _NET_WM_MOVERESIZE_SIZE_TOP,
@@ -761,14 +763,14 @@ static Bool isReparentNotify(Display *display, XEvent *ev, XPointer arg)
 
 static SDL_bool IsHighLatin1(const char *string, int length)
 {
-	while (length-- > 0) {
-		Uint8 ch = (Uint8)*string;
-		if (ch >= 0x80) {
-			return SDL_TRUE;
-		}
-		++string;
-	}
-	return SDL_FALSE;
+    while (length-- > 0) {
+        Uint8 ch = (Uint8)*string;
+        if (ch >= 0x80) {
+            return SDL_TRUE;
+        }
+        ++string;
+    }
+    return SDL_FALSE;
 }
 
 static int XLookupStringAsUTF8(XKeyEvent *event_struct, char *buffer_return, int bytes_buffer, KeySym *keysym_return, XComposeStatus *status_in_out)
@@ -787,6 +789,78 @@ static int XLookupStringAsUTF8(XKeyEvent *event_struct, char *buffer_return, int
     return result;
 }
 
+SDL_WindowData *X11_FindWindow(SDL_VideoDevice *_this, Window window)
+{
+    const SDL_VideoData *videodata = (SDL_VideoData *)_this->driverdata;
+    int i;
+
+    if (videodata && videodata->windowlist) {
+        for (i = 0; i < videodata->numwindows; ++i) {
+            if ((videodata->windowlist[i] != NULL) &&
+                (videodata->windowlist[i]->xwindow == window)) {
+                return videodata->windowlist[i];
+            }
+        }
+    }
+    return NULL;
+}
+
+void X11_HandleButtonPress(SDL_VideoDevice *_this, SDL_WindowData *windowdata, int button, const float x, const float y, const unsigned long time)
+{
+    SDL_Window *window = windowdata->window;
+    const SDL_VideoData *videodata = (SDL_VideoData *)_this->driverdata;
+    Display *display = videodata->display;
+    int xticks = 0, yticks = 0;
+#ifdef DEBUG_XEVENTS
+    printf("window %p: ButtonPress (X11 button = %d)\n", window, button);
+#endif
+    if (X11_IsWheelEvent(display, button, &xticks, &yticks)) {
+        SDL_SendMouseWheel(0, window, 0, (float)-xticks, (float)yticks, SDL_MOUSEWHEEL_NORMAL);
+    } else {
+        SDL_bool ignore_click = SDL_FALSE;
+        if (button == Button1) {
+            if (X11_ProcessHitTest(_this, windowdata, x, y)) {
+                SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_HIT_TEST, 0, 0);
+                return; /* don't pass this event on to app. */
+            }
+        } else if (button > 7) {
+            /* X button values 4-7 are used for scrolling, so X1 is 8, X2 is 9, ...
+               => subtract (8-SDL_BUTTON_X1) to get value SDL expects */
+            button -= (8 - SDL_BUTTON_X1);
+        }
+        if (windowdata->last_focus_event_time) {
+            const int X11_FOCUS_CLICK_TIMEOUT = 10;
+            if (SDL_GetTicks() < (windowdata->last_focus_event_time + X11_FOCUS_CLICK_TIMEOUT)) {
+                ignore_click = !SDL_GetHintBoolean(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, SDL_FALSE);
+            }
+            windowdata->last_focus_event_time = 0;
+        }
+        if (!ignore_click) {
+            SDL_SendMouseButton(0, window, 0, SDL_PRESSED, button);
+        }
+    }
+    X11_UpdateUserTime(windowdata, time);
+}
+
+void X11_HandleButtonRelease(SDL_VideoDevice *_this, SDL_WindowData *windowdata, int button)
+{
+    SDL_Window *window = windowdata->window;
+    const SDL_VideoData *videodata = (SDL_VideoData *)_this->driverdata;
+    Display *display = videodata->display;
+    /* The X server sends a Release event for each Press for wheels. Ignore them. */
+    int xticks = 0, yticks = 0;
+#ifdef DEBUG_XEVENTS
+    printf("window %p: ButtonRelease (X11 button = %d)\n", data, xevent->xbutton.button);
+#endif
+    if (!X11_IsWheelEvent(display, button, &xticks, &yticks)) {
+        if (button > 7) {
+            /* see explanation at case ButtonPress */
+            button -= (8 - SDL_BUTTON_X1);
+        }
+        SDL_SendMouseButton(0, window, 0, SDL_RELEASED, button);
+    }
+}
+
 void X11_GetBorderValues(SDL_WindowData *data)
 {
     SDL_VideoData *videodata = data->videodata;
@@ -861,7 +935,7 @@ static void X11_DispatchEvent(SDL_VideoDevice *_this, XEvent *xevent)
 
 #ifdef SDL_VIDEO_DRIVER_X11_SUPPORTS_GENERIC_EVENTS
     if (xevent->type == GenericEvent) {
-        X11_HandleGenericEvent(videodata, xevent);
+        X11_HandleGenericEvent(_this, xevent);
         return;
     }
 #endif
@@ -886,19 +960,19 @@ static void X11_DispatchEvent(SDL_VideoDevice *_this, XEvent *xevent)
 
 #ifdef SDL_VIDEO_DRIVER_X11_XFIXES
     if (SDL_X11_HAVE_XFIXES &&
-            xevent->type == X11_GetXFixesSelectionNotifyEvent()) {
-        XFixesSelectionNotifyEvent *ev = (XFixesSelectionNotifyEvent *) xevent;
+        xevent->type == X11_GetXFixesSelectionNotifyEvent()) {
+        XFixesSelectionNotifyEvent *ev = (XFixesSelectionNotifyEvent *)xevent;
 
         /* !!! FIXME: cache atoms */
         Atom XA_CLIPBOARD = X11_XInternAtom(display, "CLIPBOARD", 0);
 
 #ifdef DEBUG_XEVENTS
         printf("window CLIPBOARD: XFixesSelectionNotify (selection = %s)\n",
-                X11_XGetAtomName(display, ev->selection));
+               X11_XGetAtomName(display, ev->selection));
 #endif
 
         if (ev->selection == XA_PRIMARY ||
-                (XA_CLIPBOARD != None && ev->selection == XA_CLIPBOARD)) {
+            (XA_CLIPBOARD != None && ev->selection == XA_CLIPBOARD)) {
             SDL_SendClipboardUpdate();
             return;
         }
@@ -911,16 +985,8 @@ static void X11_DispatchEvent(SDL_VideoDevice *_this, XEvent *xevent)
         return;
     }
 
-    data = NULL;
-    if (videodata && videodata->windowlist) {
-        for (i = 0; i < videodata->numwindows; ++i) {
-            if ((videodata->windowlist[i] != NULL) &&
-                (videodata->windowlist[i]->xwindow == xevent->xany.window)) {
-                data = videodata->windowlist[i];
-                break;
-            }
-        }
-    }
+    data = X11_FindWindow(_this, xevent->xany.window);
+
     if (!data) {
         /* The window for KeymapNotify, etc events is 0 */
         if (xevent->type == KeymapNotify) {
@@ -1227,8 +1293,9 @@ static void X11_DispatchEvent(SDL_VideoDevice *_this, XEvent *xevent)
                xevent->xconfigure.x, xevent->xconfigure.y,
                xevent->xconfigure.width, xevent->xconfigure.height);
 #endif
-        /* Real configure notify events are relative to the parent, synthetic events are absolute. */
-        if (!xevent->xconfigure.send_event) {
+            /* Real configure notify events are relative to the parent, synthetic events are absolute. */
+            if (!xevent->xconfigure.send_event)
+        {
             unsigned int NumChildren;
             Window ChildReturn, Root, Parent;
             Window *Children;
@@ -1318,7 +1385,7 @@ static void X11_DispatchEvent(SDL_VideoDevice *_this, XEvent *xevent)
                 root_y = xevent->xclient.data.l[2] & 0xffff;
                 /* Translate from root to current window position */
                 X11_XTranslateCoordinates(display, DefaultRootWindow(display), data->xwindow,
-                        root_x, root_y, &window_x, &window_y, &ChildReturn);
+                                          root_x, root_y, &window_x, &window_y, &ChildReturn);
 
                 SDL_SendDropPosition(data->window, (float)window_x, (float)window_y);
             }
@@ -1402,6 +1469,12 @@ static void X11_DispatchEvent(SDL_VideoDevice *_this, XEvent *xevent)
         SDL_SendWindowEvent(data->window, SDL_EVENT_WINDOW_EXPOSED, 0, 0);
     } break;
 
+    /* Use XInput2 instead of the xevents API if possible, for:
+       - MotionNotify
+       - ButtonPress
+       - ButtonRelease
+       XInput2 has more precise information, e.g., to distinguish different input devices. */
+#ifndef SDL_VIDEO_DRIVER_X11_XINPUT2
     case MotionNotify:
     {
         SDL_Mouse *mouse = SDL_GetMouse();
@@ -1416,55 +1489,15 @@ static void X11_DispatchEvent(SDL_VideoDevice *_this, XEvent *xevent)
 
     case ButtonPress:
     {
-        int xticks = 0, yticks = 0;
-#ifdef DEBUG_XEVENTS
-        printf("window %p: ButtonPress (X11 button = %d)\n", data, xevent->xbutton.button);
-#endif
-        if (X11_IsWheelEvent(display, xevent, &xticks, &yticks)) {
-            SDL_SendMouseWheel(0, data->window, 0, (float)-xticks, (float)yticks, SDL_MOUSEWHEEL_NORMAL);
-        } else {
-            SDL_bool ignore_click = SDL_FALSE;
-            int button = xevent->xbutton.button;
-            if (button == Button1) {
-                if (ProcessHitTest(_this, data, xevent)) {
-                    SDL_SendWindowEvent(data->window, SDL_EVENT_WINDOW_HIT_TEST, 0, 0);
-                    break; /* don't pass this event on to app. */
-                }
-            } else if (button > 7) {
-                /* X button values 4-7 are used for scrolling, so X1 is 8, X2 is 9, ...
-                   => subtract (8-SDL_BUTTON_X1) to get value SDL expects */
-                button -= (8 - SDL_BUTTON_X1);
-            }
-            if (data->last_focus_event_time) {
-                const int X11_FOCUS_CLICK_TIMEOUT = 10;
-                if (SDL_GetTicks() < (data->last_focus_event_time + X11_FOCUS_CLICK_TIMEOUT)) {
-                    ignore_click = !SDL_GetHintBoolean(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, SDL_FALSE);
-                }
-                data->last_focus_event_time = 0;
-            }
-            if (!ignore_click) {
-                SDL_SendMouseButton(0, data->window, 0, SDL_PRESSED, button);
-            }
-        }
-        X11_UpdateUserTime(data, xevent->xbutton.time);
+        X11_HandleButtonPress(_this, data, xevent->xbutton.button,
+                              xevent->xbutton.x, xevent->xbutton.y, xevent->xbutton.time);
     } break;
 
     case ButtonRelease:
     {
-        int button = xevent->xbutton.button;
-        /* The X server sends a Release event for each Press for wheels. Ignore them. */
-        int xticks = 0, yticks = 0;
-#ifdef DEBUG_XEVENTS
-        printf("window %p: ButtonRelease (X11 button = %d)\n", data, xevent->xbutton.button);
-#endif
-        if (!X11_IsWheelEvent(display, xevent, &xticks, &yticks)) {
-            if (button > 7) {
-                /* see explanation at case ButtonPress */
-                button -= (8 - SDL_BUTTON_X1);
-            }
-            SDL_SendMouseButton(0, data->window, 0, SDL_RELEASED, button);
-        }
+        X11_HandleButtonRelease(_this, data, xevent->xbutton.button);
     } break;
+#endif /* !SDL_VIDEO_DRIVER_X11_XINPUT2 */
 
     case PropertyNotify:
     {

+ 4 - 0
src/video/x11/SDL_x11events.h

@@ -29,5 +29,9 @@ extern void X11_SendWakeupEvent(SDL_VideoDevice *_this, SDL_Window *window);
 extern int X11_SuspendScreenSaver(SDL_VideoDevice *_this);
 extern void X11_ReconcileKeyboardState(SDL_VideoDevice *_this);
 extern void X11_GetBorderValues(SDL_WindowData *data);
+extern void X11_HandleButtonPress(SDL_VideoDevice *_this, SDL_WindowData *wdata, int button, const float x, const float y, const unsigned long time);
+extern void X11_HandleButtonRelease(SDL_VideoDevice *_this, SDL_WindowData *wdata, int button);
+extern SDL_WindowData *X11_FindWindow(SDL_VideoDevice *_this, Window window);
+extern SDL_bool X11_ProcessHitTest(SDL_VideoDevice *_this, const SDL_WindowData *data, const float x, const float y);
 
 #endif /* SDL_x11events_h_ */

+ 694 - 0
src/video/x11/SDL_x11pen.c

@@ -0,0 +1,694 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2023 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+#include "../../SDL_internal.h"
+
+#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2
+
+#include "../../events/SDL_pen_c.h"
+#include "../SDL_sysvideo.h"
+#include "SDL_pen.h"
+#include "SDL_x11pen.h"
+#include "SDL_x11video.h"
+#include "SDL_x11xinput2.h"
+
+#define PEN_ERASER_ID_MAXLEN 256      /* Max # characters of device name to scan */
+#define PEN_ERASER_NAME_TAG  "eraser" /* String constant to identify erasers */
+
+#define DEBUG_PEN (SDL_PEN_DEBUG_NOID | SDL_PEN_DEBUG_NONWACOM | SDL_PEN_DEBUG_UNKNOWN_WACOM | SDL_PEN_DEBUG_NOSERIAL_WACOM)
+
+#define SDL_PEN_AXIS_VALUATOR_MISSING -1
+
+/* X11-specific information attached to each pen */
+typedef struct xinput2_pen
+{
+    float axis_min[SDL_PEN_NUM_AXES];
+    float axis_max[SDL_PEN_NUM_AXES];
+    float slider_bias;                         /* shift value to add to PEN_AXIS_SLIDER (before normalisation) */
+    float rotation_bias;                       /* rotation to add to PEN_AXIS_ROTATION  (after normalisation) */
+    Sint8 valuator_for_axis[SDL_PEN_NUM_AXES]; /* SDL_PEN_AXIS_VALUATOR_MISSING if not supported */
+} xinput2_pen;
+
+/* X11 atoms */
+static struct
+{
+    int initialized; /* initialised to 0 */
+    Atom device_product_id;
+    Atom abs_pressure;
+    Atom abs_tilt_x;
+    Atom abs_tilt_y;
+    Atom wacom_serial_ids;
+    Atom wacom_tool_type;
+} pen_atoms;
+
+/*
+ * Mapping from X11 device IDs to pen IDs
+ *
+ * In X11, the same device ID may represent any number of pens.  We
+ * thus cannot directly use device IDs as pen IDs.
+ */
+static struct
+{
+    int num_pens_known; /* Number of currently known pens (based on their GUID); used to give pen ID to new pens */
+    int num_entries;    /* Number of X11 device IDs that correspond to pens */
+
+    struct pen_device_id_mapping
+    {
+        Uint32 deviceid;
+        Uint32 pen_id;
+    } * entries; /* Current pen to device ID mappings */
+} pen_map;
+
+typedef enum
+{
+    SDL_PEN_VENDOR_UNKNOWN = 0,
+    SDL_PEN_VENDOR_WACOM
+} sdl_pen_vendor;
+
+/* Information to identify pens during discovery */
+typedef struct
+{
+    sdl_pen_vendor vendor;
+    SDL_GUID guid;
+    SDL_PenSubtype heuristic_type; /* Distinguish pen+eraser devices with shared bus ID */
+    Uint32 devicetype_id, serial;  /* used by PEN_VENDOR_WACOM */
+    Uint32 deviceid;
+} pen_identity;
+
+int X11_PenIDFromDeviceID(int deviceid)
+{
+    int i;
+    for (i = 0; i < pen_map.num_entries; ++i) {
+        if (pen_map.entries[i].deviceid == deviceid) {
+            return pen_map.entries[i].pen_id;
+        }
+    }
+    return SDL_PEN_INVALID;
+}
+
+static void pen_atoms_ensure_initialized(SDL_VideoDevice *_this)
+{
+    SDL_VideoData *data = (SDL_VideoData *)_this->driverdata;
+
+    if (pen_atoms.initialized) {
+        return;
+    }
+    /* Create atoms if they don't exist yet to pre-empt hotplugging updates */
+    pen_atoms.device_product_id = X11_XInternAtom(data->display, "Device Product ID", False);
+    pen_atoms.wacom_serial_ids = X11_XInternAtom(data->display, "Wacom Serial IDs", False);
+    pen_atoms.wacom_tool_type = X11_XInternAtom(data->display, "Wacom Tool Type", False);
+    pen_atoms.abs_pressure = X11_XInternAtom(data->display, "Abs Pressure", True);
+    pen_atoms.abs_tilt_x = X11_XInternAtom(data->display, "Abs Tilt X", True);
+    pen_atoms.abs_tilt_y = X11_XInternAtom(data->display, "Abs Tilt Y", True);
+
+    pen_atoms.initialized = 1;
+}
+
+/* Read out an integer property and store into a preallocated Sint32 array, extending 8 and 16 bit values suitably.
+   Returns number of Sint32s written (<= max_words), or 0 on error. */
+static size_t xinput2_pen_get_int_property(SDL_VideoDevice *_this, int deviceid, Atom property, Sint32 *dest, size_t max_words)
+{
+    const SDL_VideoData *data = (SDL_VideoData *)_this->driverdata;
+    Atom type_return;
+    int format_return;
+    unsigned long num_items_return;
+    unsigned long bytes_after_return;
+    unsigned char *output;
+
+    if (property == None) {
+        return 0;
+    }
+
+    if (Success != X11_XIGetProperty(data->display, deviceid,
+                                     property,
+                                     0, max_words, False,
+                                     XA_INTEGER, &type_return, &format_return,
+                                     &num_items_return, &bytes_after_return,
+                                     &output) ||
+        num_items_return == 0 || output == NULL) {
+        return 0;
+    }
+
+    if (type_return == XA_INTEGER) {
+        int k;
+        const int to_copy = SDL_min(max_words, num_items_return);
+
+        if (format_return == 8) {
+            Sint8 *numdata = (Sint8 *)output;
+            for (k = 0; k < to_copy; ++k) {
+                dest[k] = numdata[k];
+            }
+        } else if (format_return == 16) {
+            Sint16 *numdata = (Sint16 *)output;
+            for (k = 0; k < to_copy; ++k) {
+                dest[k] = numdata[k];
+            }
+        } else {
+            SDL_memcpy(dest, output, sizeof(Sint32) * to_copy);
+        }
+        X11_XFree(output);
+        return to_copy;
+    }
+    return 0; /* type mismatch */
+}
+
+/* 32 bit vendor + device ID from evdev */
+static Uint32 xinput2_pen_evdevid(SDL_VideoDevice *_this, int deviceid)
+{
+#if !(SDL_PEN_DEBUG_NOID)
+    Sint32 ids[2];
+
+    pen_atoms_ensure_initialized(_this);
+
+    if (2 != xinput2_pen_get_int_property(_this, deviceid, pen_atoms.device_product_id, ids, 2)) {
+        return 0;
+    }
+    return ((ids[0] << 16) | (ids[1] & 0xffff));
+#else /* Testing: pretend that we have no ID (not sure if this can happen IRL) */
+    return 0;
+#endif
+}
+
+/* Gets reasonably-unique GUID for the device */
+static void xinput2_pen_update_generic_guid(SDL_VideoDevice *_this, pen_identity *pident, int deviceid)
+{
+    Uint32 evdevid = xinput2_pen_evdevid(_this, deviceid); /* also initialises pen_atoms  */
+
+    if (!evdevid) {
+        /* Fallback: if no evdevid is available; try to at least distinguish devices within the
+           current session.  This is a poor GUID and our last resort. */
+        evdevid = deviceid;
+    }
+    SDL_PenUpdateGUIDForGeneric(&pident->guid, 0, evdevid);
+}
+
+/* Identify Wacom devices (if SDL_TRUE is returned) and extract their device type and serial IDs */
+static SDL_bool xinput2_wacom_deviceid(SDL_VideoDevice *_this, int deviceid, Uint32 *wacom_devicetype_id, Uint32 *wacom_serial)
+{
+#if !(SDL_PEN_DEBUG_NONWACOM) /* Can be disabled for testing */
+    Sint32 serial_id_buf[3];
+    int result;
+
+    pen_atoms_ensure_initialized(_this);
+
+    if ((result = xinput2_pen_get_int_property(_this, deviceid, pen_atoms.wacom_serial_ids, serial_id_buf, 3)) == 3) {
+        *wacom_devicetype_id = serial_id_buf[2];
+        *wacom_serial = serial_id_buf[1];
+#if SDL_PEN_DEBUG_NOSERIAL_WACOM /* Disabled for testing? */
+        *wacom_serial = 0;
+#endif
+        return SDL_TRUE;
+    }
+#endif
+    return SDL_FALSE;
+}
+
+/* Heuristically determines if device is an eraser */
+static SDL_bool xinput2_pen_is_eraser(SDL_VideoDevice *_this, int deviceid, char *devicename)
+{
+    SDL_VideoData *data = (SDL_VideoData *)_this->driverdata;
+    char dev_name[PEN_ERASER_ID_MAXLEN];
+    int k;
+
+    pen_atoms_ensure_initialized(_this);
+
+    if (pen_atoms.wacom_tool_type != None) {
+        Atom type_return;
+        int format_return;
+        unsigned long num_items_return;
+        unsigned long bytes_after_return;
+        unsigned char *tooltype_name_info = NULL;
+
+        /* Try Wacom-specific method */
+        if (Success == X11_XIGetProperty(data->display, deviceid,
+                                         pen_atoms.wacom_tool_type,
+                                         0, 32, False,
+                                         AnyPropertyType, &type_return, &format_return,
+                                         &num_items_return, &bytes_after_return,
+                                         &tooltype_name_info) &&
+            tooltype_name_info != NULL && num_items_return > 0) {
+
+            SDL_bool result = SDL_FALSE;
+            char *tooltype_name = NULL;
+
+            if (type_return == XA_ATOM) {
+                /* Atom instead of string?  Un-intern */
+                Atom atom = *((Atom *)tooltype_name_info);
+                if (atom != None) {
+                    tooltype_name = X11_XGetAtomName(data->display, atom);
+                }
+            } else if (type_return == XA_STRING && format_return == 8) {
+                tooltype_name = (char *)tooltype_name_info;
+            }
+
+            if (tooltype_name) {
+                if (0 == SDL_strcasecmp(tooltype_name, PEN_ERASER_NAME_TAG)) {
+                    result = SDL_TRUE;
+                }
+                X11_XFree(tooltype_name_info);
+
+                return result;
+            }
+        }
+    }
+    /* Non-Wacom device? */
+
+    /* We assume that a device is an eraser if its name contains the string "eraser".
+     * Unfortunately there doesn't seem to be a clean way to distinguish these cases (as of 2022-03). */
+
+    SDL_strlcpy(dev_name, devicename, PEN_ERASER_ID_MAXLEN);
+    /* lowercase device name string so we can use strstr() */
+    for (k = 0; dev_name[k]; ++k) {
+        dev_name[k] = tolower(dev_name[k]);
+    }
+
+    return (SDL_strstr(dev_name, PEN_ERASER_NAME_TAG)) ? SDL_TRUE : SDL_FALSE;
+}
+
+/* Gets GUID and other identifying information for the device using the best known method */
+static pen_identity xinput2_identify_pen(SDL_VideoDevice *_this, int deviceid, char *name)
+{
+    pen_identity pident;
+
+    pident.devicetype_id = 0ul;
+    pident.serial = 0ul;
+    pident.deviceid = deviceid;
+    pident.heuristic_type = SDL_PEN_TYPE_PEN;
+    SDL_memset(pident.guid.data, 0, sizeof(pident.guid.data));
+
+    if (xinput2_pen_is_eraser(_this, deviceid, name)) {
+        pident.heuristic_type = SDL_PEN_TYPE_ERASER;
+    }
+
+    if (xinput2_wacom_deviceid(_this, deviceid, &pident.devicetype_id, &pident.serial)) {
+        pident.vendor = SDL_PEN_VENDOR_WACOM;
+        SDL_PenUpdateGUIDForWacom(&pident.guid, pident.devicetype_id, pident.serial);
+
+#if DEBUG_PEN
+        printf("[pen] Pen %d reports Wacom device_id %x\n",
+               deviceid, pident.devicetype_id);
+#endif
+
+    } else {
+        pident.vendor = SDL_PEN_VENDOR_UNKNOWN;
+    }
+    if (!pident.serial) {
+        /* If the pen has a serial number, we can move it across tablets and retain its identity.
+           Otherwise, we use the evdev ID as part of its GUID, which may mean that we identify it with the tablet. */
+        xinput2_pen_update_generic_guid(_this, &pident, deviceid);
+    }
+    SDL_PenUpdateGUIDForType(&pident.guid, pident.heuristic_type);
+    return pident;
+}
+
+static void xinput2_pen_free_deviceinfo(Uint32 deviceid, void *x11_peninfo, void *context)
+{
+    SDL_free(x11_peninfo);
+}
+
+static void xinput2_merge_deviceinfo(xinput2_pen *dest, xinput2_pen *src)
+{
+    *dest = *src;
+}
+
+/**
+ * Fill in vendor-specific device information, if available
+ *
+ * For Wacom pens: identify number of buttons and extra axis (if present)
+ *
+ * \param _this global state
+ * \param dev The device to analyse
+ * \param pen The pen to initialise
+ * \param pident Pen identity information
+ * \param[out] valuator_5 Meaning of the valuator with offset 5, if any
+ *   (written only if known and if the device has a 6th axis,
+ *   e.g., for the Wacom Art Pen and Wacom Airbrush Pen)
+ * \param[out] axes Bitmask of all possibly supported axes
+ *
+ * This function identifies Wacom device types through a Wacom-specific device ID.
+ * It then fills in pen details from an internal database.
+ * If the device seems to be a Wacom pen/eraser but can't be identified, the function
+ * leaves "axes" untouched and sets the other outputs to common defaults.
+ *
+ * There is no explicit support for other vendors, though vendors that
+ * emulate the Wacom API might be supported.
+ *
+ * Unsupported devices will keep the default settings.
+ */
+static void xinput2_vendor_peninfo(SDL_VideoDevice *_this, const XIDeviceInfo *dev, SDL_Pen *pen, pen_identity pident, int *valuator_5, Uint32 *axes)
+{
+    switch (pident.vendor) {
+    case SDL_PEN_VENDOR_WACOM:
+    {
+        if (SDL_PenModifyForWacomID(pen, pident.devicetype_id, axes)) {
+            if (*axes & SDL_PEN_AXIS_SLIDER_MASK) {
+                /* Air Brush Pen or eraser */
+                *valuator_5 = SDL_PEN_AXIS_SLIDER;
+            } else if (*axes & SDL_PEN_AXIS_ROTATION_MASK) {
+                /* Art Pen or eraser, or 6D Art Pen */
+                *valuator_5 = SDL_PEN_AXIS_ROTATION;
+            }
+            return;
+        } else {
+#if DEBUG_PEN
+            printf("[pen] Could not identify wacom pen %d with device id %x, using default settings\n",
+                   pident.deviceid, pident.devicetype_id);
+#endif
+            break;
+        }
+    }
+
+    default:
+#if DEBUG_PEN
+        printf("[pen] Pen %d is not from a known vendor\n", pident.deviceid);
+#endif
+        break;
+    }
+
+    /* Fall back to default heuristics for identifying device type */
+
+    SDL_strlcpy(pen->name, dev->name, SDL_PEN_MAX_NAME);
+
+    pen->type = pident.heuristic_type;
+}
+
+/* Does this device have a valuator for pressure sensitivity? */
+static SDL_bool xinput2_device_is_pen(const XIDeviceInfo *dev)
+{
+    int classct;
+    for (classct = 0; classct < dev->num_classes; ++classct) {
+        const XIAnyClassInfo *classinfo = dev->classes[classct];
+
+        switch (classinfo->type) {
+        case XIValuatorClass:
+        {
+            XIValuatorClassInfo *val_classinfo = (XIValuatorClassInfo *)classinfo;
+            Atom vname = val_classinfo->label;
+
+            if (vname == pen_atoms.abs_pressure) {
+                return SDL_TRUE;
+            }
+        }
+        }
+    }
+    return SDL_FALSE;
+}
+
+void X11_InitPen(SDL_VideoDevice *_this)
+{
+    SDL_VideoData *data = (SDL_VideoData *)_this->driverdata;
+    int i;
+    XIDeviceInfo *device_info;
+    int num_device_info;
+
+    device_info = X11_XIQueryDevice(data->display, XIAllDevices, &num_device_info);
+    if (!device_info) {
+        return;
+    }
+
+    /* Reset the device id -> pen map */
+    if (pen_map.entries) {
+        SDL_free(pen_map.entries);
+        pen_map.entries = NULL;
+        pen_map.num_entries = 0;
+    }
+
+    SDL_PenGCMark();
+
+    for (i = 0; i < num_device_info; ++i) {
+        const XIDeviceInfo *dev = &device_info[i];
+        int classct;
+        xinput2_pen pen_device;
+        Uint32 capabilities = 0;
+        Uint32 axis_mask = ~0;    /* Permitted axes (default: all) */
+        int valuator_5_axis = -1; /* For Wacom devices, the 6th valuator (offset 5) has a model-specific meaning */
+        pen_identity pident;
+        SDL_PenID pen_id;
+        SDL_Pen *pen;
+        int old_num_pens_known = pen_map.num_pens_known;
+        int k;
+
+        /* Only track physical devices that are enabled */
+        if (dev->use != XISlavePointer || dev->enabled == 0 || !xinput2_device_is_pen(dev)) {
+            continue;
+        }
+
+        pen_device.slider_bias = 0.0f;
+        pen_device.rotation_bias = 0.0f;
+        for (k = 0; k < SDL_PEN_NUM_AXES; ++k) {
+            pen_device.valuator_for_axis[k] = SDL_PEN_AXIS_VALUATOR_MISSING;
+        }
+
+        pident = xinput2_identify_pen(_this, dev->deviceid, dev->name);
+
+        pen_id = SDL_GetPenFromGUID(pident.guid);
+        if (pen_id == SDL_PEN_INVALID) {
+            /* We have never met this pen */
+            pen_id = ++pen_map.num_pens_known; /* start at 1 */
+        }
+        pen = SDL_PenModifyBegin(pen_id);
+
+        /* Complement XF86 driver information with vendor-specific details */
+        xinput2_vendor_peninfo(_this, dev, pen, pident, &valuator_5_axis, &axis_mask);
+
+        for (classct = 0; classct < dev->num_classes; ++classct) {
+            const XIAnyClassInfo *classinfo = dev->classes[classct];
+
+            switch (classinfo->type) {
+            case XIValuatorClass:
+            {
+                XIValuatorClassInfo *val_classinfo = (XIValuatorClassInfo *)classinfo;
+                Sint8 valuator_nr = val_classinfo->number;
+                Atom vname = val_classinfo->label;
+                int axis = -1;
+
+                float min = val_classinfo->min;
+                float max = val_classinfo->max;
+
+                if (vname == pen_atoms.abs_pressure) {
+                    axis = SDL_PEN_AXIS_PRESSURE;
+                } else if (vname == pen_atoms.abs_tilt_x) {
+                    axis = SDL_PEN_AXIS_XTILT;
+                } else if (vname == pen_atoms.abs_tilt_y) {
+                    axis = SDL_PEN_AXIS_YTILT;
+                }
+
+                if (axis == -1 && valuator_nr == 5) {
+                    /* Wacom model-specific axis support */
+		    /* The meaning of the various axes is highly underspecitied in Xinput2.
+		     * As of 2023-08-26, Wacom seems to be the only vendor to support these axes, so the code below
+		     * captures the de-facto standard. */
+                    axis = valuator_5_axis;
+
+                    switch (axis) {
+                    case SDL_PEN_AXIS_SLIDER:
+                        /* cf. xinput2_wacom_peninfo for how this axis is used.
+                           In all current cases, our API wants this value in 0..1, but the xf86 driver
+                           starts at a negative offset, so we normalise here. */
+                        pen_device.slider_bias = -min;
+                        max -= min;
+                        min = 0.0f;
+                        break;
+
+                    case SDL_PEN_AXIS_ROTATION:
+                        /* The "0" value points to the left, rather than up, so we must
+                           rotate 90 degrees counter-clockwise to have 0 point to the top. */
+
+                        pen_device.rotation_bias = -90.0f;
+                        break;
+
+                    default:
+                        break;
+                    }
+                }
+
+                if (axis >= 0) {
+                    capabilities |= SDL_PEN_AXIS_CAPABILITY(axis);
+
+                    pen_device.valuator_for_axis[axis] = valuator_nr;
+                    pen_device.axis_min[axis] = min;
+                    pen_device.axis_max[axis] = max;
+                }
+                break;
+            }
+            default:
+                break;
+            }
+        }
+
+        /* We have a pen if and only if the device measures pressure */
+        if (capabilities & SDL_PEN_AXIS_PRESSURE_MASK) {
+            xinput2_pen *xinput2_deviceinfo;
+            Uint64 guid_a, guid_b;
+
+            /* Done collecting data, write to pen */
+            SDL_PenModifyAddCapabilities(pen, capabilities);
+            pen->guid = pident.guid;
+
+            if (pen->deviceinfo) {
+                /* Updating a known pen */
+                xinput2_deviceinfo = (xinput2_pen *)pen->deviceinfo;
+                xinput2_merge_deviceinfo(xinput2_deviceinfo, &pen_device);
+            } else {
+                /* Registering a new pen */
+                xinput2_deviceinfo = SDL_malloc(sizeof(xinput2_pen));
+                SDL_memcpy(xinput2_deviceinfo, &pen_device, sizeof(xinput2_pen));
+            }
+            pen->deviceinfo = xinput2_deviceinfo;
+
+#if DEBUG_PEN
+            printf("[pen] pen %d [%04x] valuators pressure=%d, xtilt=%d, ytilt=%d [%s]\n",
+                   pen->header.id, pen->header.flags,
+                   pen_device.valuator_for_axis[SDL_PEN_AXIS_PRESSURE],
+                   pen_device.valuator_for_axis[SDL_PEN_AXIS_XTILT],
+                   pen_device.valuator_for_axis[SDL_PEN_AXIS_YTILT],
+                   pen->name);
+#endif
+            SDL_memcpy(&guid_a, &pen->guid.data[0], 8);
+            SDL_memcpy(&guid_b, &pen->guid.data[8], 8);
+            if (!(guid_a | guid_b)) {
+#if DEBUG_PEN
+                printf("[pen]    (pen eliminated due to zero GUID)\n");
+#endif
+                pen->type = SDL_PEN_TYPE_NONE;
+            }
+
+        } else {
+            /* Not a pen, mark for deletion */
+            pen->type = SDL_PEN_TYPE_NONE;
+        }
+        SDL_PenModifyEnd(pen, SDL_TRUE);
+
+        if (pen->type != SDL_PEN_TYPE_NONE) {
+            const int map_pos = pen_map.num_entries;
+
+            /* We found a pen: add mapping */
+            if (pen_map.entries == NULL) {
+                pen_map.entries = SDL_calloc(sizeof(struct pen_device_id_mapping), 1);
+                pen_map.num_entries = 1;
+            } else {
+                pen_map.num_entries += 1;
+                pen_map.entries = SDL_realloc(pen_map.entries,
+                                              pen_map.num_entries * (sizeof(struct pen_device_id_mapping)));
+            }
+            pen_map.entries[map_pos].deviceid = dev->deviceid;
+            pen_map.entries[map_pos].pen_id = pen_id;
+        } else {
+            /* Revert pen number allocation */
+            pen_map.num_pens_known = old_num_pens_known;
+        }
+    }
+    X11_XIFreeDeviceInfo(device_info);
+
+    SDL_PenGCSweep(NULL, xinput2_pen_free_deviceinfo);
+}
+
+static void xinput2_normalize_pen_axes(const SDL_Pen *peninfo,
+                                       const xinput2_pen *xpen,
+                                       /* inout-mode paramters: */
+                                       float *coords)
+{
+    int axis;
+
+    /* Normalise axes */
+    for (axis = 0; axis < SDL_PEN_NUM_AXES; ++axis) {
+        int valuator = xpen->valuator_for_axis[axis];
+        if (valuator != SDL_PEN_AXIS_VALUATOR_MISSING) {
+            float value = coords[axis];
+            float min = xpen->axis_min[axis];
+            float max = xpen->axis_max[axis];
+
+            if (axis == SDL_PEN_AXIS_SLIDER) {
+                value += xpen->slider_bias;
+            }
+
+            /* min ... 0 ... max */
+            if (min < 0.0) {
+                /* Normalise so that 0 remains 0.0 */
+                if (value < 0) {
+                    value = value / (-min);
+                } else {
+                    if (max == 0.0) {
+                        value = 0.0f;
+                    } else {
+                        value = value / max;
+                    }
+                }
+            } else {
+                /* 0 ... min ... max */
+                /* including 0.0 = min */
+                if (max == 0.0) {
+                    value = 0.0f;
+                } else {
+                    value = (value - min) / max;
+                }
+            }
+
+            switch (axis) {
+            case SDL_PEN_AXIS_XTILT:
+            case SDL_PEN_AXIS_YTILT:
+                if (peninfo->info.max_tilt > 0.0f) {
+                    value *= peninfo->info.max_tilt; /* normalise to physical max */
+                }
+                break;
+
+            case SDL_PEN_AXIS_ROTATION:
+                /* normalised to -1..1, so let's convert to degrees */
+                value *= 180.0;
+                value += xpen->rotation_bias;
+
+                /* handle simple over/underflow */
+                if (value >= 180.0f) {
+                    value -= 360.0f;
+                } else if (value < -180.0f) {
+                    value += 360.0f;
+                }
+                break;
+
+            default:
+                break;
+            }
+            coords[axis] = value;
+        }
+    }
+}
+
+void X11_PenAxesFromValuators(const SDL_Pen *peninfo,
+                              const double *input_values, const unsigned char *mask, const int mask_len,
+                              /* out-mode parameters: */
+                              float axis_values[SDL_PEN_NUM_AXES])
+{
+    const xinput2_pen *pen = (xinput2_pen *)peninfo->deviceinfo;
+    int i;
+
+    for (i = 0; i < SDL_PEN_NUM_AXES; ++i) {
+        const int valuator = pen->valuator_for_axis[i];
+        if (valuator == SDL_PEN_AXIS_VALUATOR_MISSING || valuator >= mask_len * 8 || !(XIMaskIsSet(mask, valuator))) {
+            axis_values[i] = 0.0f;
+        } else {
+            axis_values[i] = input_values[valuator];
+        }
+    }
+    xinput2_normalize_pen_axes(peninfo, pen, axis_values);
+}
+
+#endif /* SDL_VIDEO_DRIVER_X11_XINPUT2 */
+
+/* vi: set ts=4 sw=4 expandtab: */

+ 54 - 0
src/video/x11/SDL_x11pen.h

@@ -0,0 +1,54 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2023 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+#include "../../SDL_internal.h"
+
+#ifndef SDL_x11pen_h_
+#define SDL_x11pen_h_
+
+#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2
+
+#include "SDL_x11video.h"
+#include "../../events/SDL_pen_c.h"
+
+/* Pressure-sensitive pen */
+
+/* Forward definition for SDL_x11video.h */
+struct SDL_VideoData;
+
+/* Function definitions */
+
+/* Detect XINPUT2 devices that are pens / erasers, or update the list after hotplugging */
+extern void X11_InitPen(SDL_VideoDevice *_this);
+
+/* Converts XINPUT2 valuators into pen axis information, including normalisation */
+extern void X11_PenAxesFromValuators(const SDL_Pen *pen,
+                                     const double *input_values, const unsigned char *mask, const int mask_len,
+                                     /* out-mode parameters: */
+                                     float axis_values[SDL_PEN_NUM_AXES]);
+
+/* Map X11 device ID to pen ID */
+extern int X11_PenIDFromDeviceID(int deviceid);
+
+#endif /* SDL_VIDEO_DRIVER_X11_XINPUT2 */
+
+#endif /* SDL_x11pen_h_ */
+
+/* vi: set ts=4 sw=4 expandtab: */

+ 1 - 0
src/video/x11/SDL_x11sym.h

@@ -289,6 +289,7 @@ SDL_X11_SYM(Status,XIQueryVersion,(Display *a,int *b,int *c),(a,b,c),return)
 SDL_X11_SYM(XIEventMask*,XIGetSelectedEvents,(Display *a,Window b,int *c),(a,b,c),return)
 SDL_X11_SYM(Bool,XIGetClientPointer,(Display *a,Window b,int *c),(a,b,c),return)
 SDL_X11_SYM(Bool,XIWarpPointer,(Display *a,int b,Window c,Window d,double e,double f,int g,int h,double i,double j),(a,b,c,d,e,f,g,h,i,j),return)
+SDL_X11_SYM(Status,XIGetProperty,(Display *a,int b,Atom c,long d,long e,Bool f, Atom g, Atom *h, int *i, unsigned long *j, unsigned long *k, unsigned char **l),(a,b,c,d,e,f,g,h,i,j,k,l),return);
 #endif
 
 /* XRandR support */

+ 9 - 4
src/video/x11/SDL_x11video.c

@@ -24,16 +24,17 @@
 
 #include <unistd.h> /* For getpid() and readlink() */
 
-#include "../SDL_sysvideo.h"
-#include "../SDL_pixels_c.h"
 #include "../../core/linux/SDL_system_theme.h"
+#include "../SDL_pixels_c.h"
+#include "../SDL_sysvideo.h"
 
-#include "SDL_x11video.h"
 #include "SDL_x11framebuffer.h"
+#include "SDL_x11pen.h"
 #include "SDL_x11shape.h"
 #include "SDL_x11touch.h"
-#include "SDL_x11xinput2.h"
+#include "SDL_x11video.h"
 #include "SDL_x11xfixes.h"
+#include "SDL_x11xinput2.h"
 
 #ifdef SDL_VIDEO_OPENGL_EGL
 #include "SDL_x11opengles.h"
@@ -432,6 +433,10 @@ int X11_VideoInit(SDL_VideoDevice *_this)
 
     X11_InitTouch(_this);
 
+#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2
+    X11_InitPen(_this);
+#endif
+
     return 0;
 }
 

+ 22 - 16
src/video/x11/SDL_x11window.c

@@ -592,8 +592,8 @@ int X11_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window)
         X11_ConstrainPopup(window);
     }
     SDL_RelativeToGlobalForWindow(window,
-                                      window->windowed.x, window->windowed.y,
-                                      &win_x, &win_y);
+                                  window->windowed.x, window->windowed.y,
+                                  &win_x, &win_y);
 
     /* Always create this with the window->windowed.* fields; if we're
        creating a windowed mode window, that's fine. If we're creating a
@@ -754,12 +754,20 @@ int X11_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window)
 
     X11_Xinput2SelectTouch(_this, window);
 
-    X11_XSelectInput(display, w,
-                     (FocusChangeMask | EnterWindowMask | LeaveWindowMask |
-                      ExposureMask | ButtonPressMask | ButtonReleaseMask |
-                      PointerMotionMask | KeyPressMask | KeyReleaseMask |
-                      PropertyChangeMask | StructureNotifyMask |
-                      KeymapStateMask | fevent));
+    {
+        unsigned int x11_pointer_events = ButtonPressMask | ButtonReleaseMask | PointerMotionMask;
+        if (X11_Xinput2SelectMouse(_this, window)) {
+            /* If XInput2 can handle pointer events, we don't track them here */
+            x11_pointer_events = 0;
+        }
+
+        X11_XSelectInput(display, w,
+                         (FocusChangeMask | EnterWindowMask | LeaveWindowMask | ExposureMask |
+                          x11_pointer_events |
+                          KeyPressMask | KeyReleaseMask |
+                          PropertyChangeMask | StructureNotifyMask |
+                          KeymapStateMask | fevent));
+    }
 
     /* For _ICC_PROFILE. */
     X11_XSelectInput(display, RootWindow(display, screen), PropertyChangeMask);
@@ -832,7 +840,8 @@ static int X11_CatchAnyError(Display *d, XErrorEvent *e)
     return 0;
 }
 
-enum check_method {
+enum check_method
+{
     COMPARE_POSITION = 1,
     COMPARE_SIZE = 2,
     COMPARE_DOUBLE_ATTEMPT = 3,
@@ -842,8 +851,8 @@ enum check_method {
 /* Wait a brief time, or not, to see if the window manager decided to move/resize the window.
  * Send MOVED and RESIZED window events */
 static void X11_WaitAndSendWindowEvents(SDL_Window *window, int param_timeout, enum check_method method,
-            int orig_x, int orig_y, int dest_x, int dest_y,
-            int orig_w, int orig_h, int dest_w, int dest_h)
+                                        int orig_x, int orig_y, int dest_x, int dest_y,
+                                        int orig_w, int orig_h, int dest_w, int dest_h)
 {
     SDL_WindowData *data = window->driverdata;
     Display *display = data->videodata->display;
@@ -934,7 +943,6 @@ static void X11_WaitAndSendWindowEvents(SDL_Window *window, int param_timeout, e
     caught_x11_error = SDL_FALSE;
 }
 
-
 int X11_SetWindowIcon(SDL_VideoDevice *_this, SDL_Window *window, SDL_Surface *icon)
 {
     SDL_WindowData *data = window->driverdata;
@@ -974,8 +982,8 @@ int X11_SetWindowIcon(SDL_VideoDevice *_this, SDL_Window *window, SDL_Surface *i
         }
 
         X11_XChangeProperty(display, data->xwindow, _NET_WM_ICON, XA_CARDINAL,
-                                32, PropModeReplace, (unsigned char *)propdata,
-                                propsize);
+                            32, PropModeReplace, (unsigned char *)propdata,
+                            propsize);
         SDL_free(propdata);
 
         if (caught_x11_error) {
@@ -1351,7 +1359,6 @@ void X11_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window)
     if (data->border_left == 0 && data->border_right == 0 && data->border_top == 0 && data->border_bottom == 0) {
         X11_GetBorderValues(data);
     }
-
 }
 
 void X11_HideWindow(SDL_VideoDevice *_this, SDL_Window *window)
@@ -1485,7 +1492,6 @@ static void X11_SetWindowMaximized(SDL_VideoDevice *_this, SDL_Window *window, S
         /* Send MOVED/RESIZED event, if needed. Compare with initial position and size. Timeout 1000 */
         X11_WaitAndSendWindowEvents(window, 1000, COMPARE_ORIG, orig_x, orig_y, 0, 0, orig_w, orig_h, 0, 0);
 
-
     } else {
         X11_SetNetWMState(_this, data->xwindow, window->flags);
     }

+ 169 - 11
src/video/x11/SDL_x11xinput2.c

@@ -22,9 +22,12 @@
 
 #ifdef SDL_VIDEO_DRIVER_X11
 
+#include "SDL_x11pen.h"
 #include "SDL_x11video.h"
 #include "SDL_x11xinput2.h"
+#include "../../events/SDL_events_c.h"
 #include "../../events/SDL_mouse_c.h"
+#include "../../events/SDL_pen_c.h"
 #include "../../events/SDL_touch_c.h"
 
 #define MAX_AXIS 16
@@ -74,19 +77,25 @@ static SDL_bool xinput2_version_atleast(const int version, const int wantmajor,
     return version >= ((wantmajor * 1000) + wantminor);
 }
 
-#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH
-static SDL_Window *xinput2_get_sdlwindow(SDL_VideoData *videodata, Window window)
+static SDL_WindowData *xinput2_get_sdlwindowdata(SDL_VideoData *videodata, Window window)
 {
     int i;
     for (i = 0; i < videodata->numwindows; i++) {
         SDL_WindowData *d = videodata->windowlist[i];
         if (d->xwindow == window) {
-            return d->window;
+            return d;
         }
     }
     return NULL;
 }
 
+static SDL_Window *xinput2_get_sdlwindow(SDL_VideoData *videodata, Window window)
+{
+    const SDL_WindowData *windowdata = xinput2_get_sdlwindowdata(videodata, window);
+    return windowdata ? windowdata->window : NULL;
+}
+
+#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH
 static void xinput2_normalize_touch_coordinates(SDL_Window *window, double in_x, double in_y, float *out_x, float *out_y)
 {
     if (window) {
@@ -262,18 +271,51 @@ static SDL_XInput2DeviceInfo *xinput2_get_device_info(SDL_VideoData *videodata,
 
     return devinfo;
 }
+
+static void xinput2_pen_ensure_window(SDL_VideoDevice *_this, const SDL_Pen *pen, Window window)
+{
+    /* When "flipping" a Wacom eraser pen, we get an XI_DeviceChanged event
+     * with the newly-activated pen, but this event is global for the display.
+     * We won't get a window until the pen starts triggering motion or
+     * button events, so we instead hook the pen to its window at that point. */
+    const SDL_WindowData *windowdata = X11_FindWindow(_this, window);
+    if (windowdata) {
+        SDL_SendPenWindowEvent(0, pen->header.id, windowdata->window);
+    }
+}
 #endif
 
-int X11_HandleXinput2Event(SDL_VideoData *videodata, XGenericEventCookie *cookie)
+int X11_HandleXinput2Event(SDL_VideoDevice *_this, XGenericEventCookie *cookie)
 {
 #ifdef SDL_VIDEO_DRIVER_X11_XINPUT2
+    SDL_VideoData *videodata = (SDL_VideoData *)_this->driverdata;
+
     if (cookie->extension != xinput2_opcode) {
         return 0;
     }
     switch (cookie->evtype) {
+    case XI_PropertyEvent:
+    case XI_DeviceChanged:
+    {
+        X11_InitPen(_this);
+    } break;
+
+    case XI_Enter:
+    case XI_Leave:
+    {
+        const XIEnterEvent *enterev = (const XIEnterEvent *)cookie->data;
+        const SDL_WindowData *windowdata = X11_FindWindow(_this, enterev->event);
+        const SDL_Pen *pen = SDL_GetPenPtr(X11_PenIDFromDeviceID(enterev->sourceid));
+        SDL_Window *window = (windowdata && (cookie->evtype == XI_Enter)) ? windowdata->window : NULL;
+        if (pen) {
+            SDL_SendPenWindowEvent(0, pen->header.id, window);
+        }
+    } break;
+
     case XI_RawMotion:
     {
         const XIRawEvent *rawev = (const XIRawEvent *)cookie->data;
+        const SDL_bool is_pen = X11_PenIDFromDeviceID(rawev->sourceid) != SDL_PEN_INVALID;
         SDL_Mouse *mouse = SDL_GetMouse();
         SDL_XInput2DeviceInfo *devinfo;
         double coords[2];
@@ -281,6 +323,11 @@ int X11_HandleXinput2Event(SDL_VideoData *videodata, XGenericEventCookie *cookie
         int i;
 
         videodata->global_mouse_changed = SDL_TRUE;
+        if (is_pen) {
+            return 0; /* Pens check for XI_Motion instead */
+        }
+
+        /* Non-pen: */
 
         if (!mouse->relative_mode || mouse->relative_mode_warp) {
             return 0;
@@ -317,6 +364,7 @@ int X11_HandleXinput2Event(SDL_VideoData *videodata, XGenericEventCookie *cookie
                 xinput2_remove_device_info(videodata, hierev->info[i].deviceid);
             }
         }
+        X11_InitPen(_this);
     } break;
 
     case XI_RawButtonPress:
@@ -326,17 +374,94 @@ int X11_HandleXinput2Event(SDL_VideoData *videodata, XGenericEventCookie *cookie
     case XI_RawTouchUpdate:
     case XI_RawTouchEnd:
 #endif
+    {
         videodata->global_mouse_changed = SDL_TRUE;
-        break;
+    } break;
 
-#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH
-        /* With multitouch, register to receive XI_Motion (which desctivates MotionNotify),
-         * so that we can distinguish real mouse motions from synthetic one.  */
+    case XI_ButtonPress:
+    case XI_ButtonRelease:
+    {
+        const XIDeviceEvent *xev = (const XIDeviceEvent *)cookie->data;
+        const SDL_Pen *pen = SDL_GetPenPtr(X11_PenIDFromDeviceID(xev->deviceid));
+        const int button = xev->detail;
+        const SDL_bool pressed = (cookie->evtype == XI_ButtonPress) ? SDL_TRUE : SDL_FALSE;
+
+        if (pen) {
+            xinput2_pen_ensure_window(_this, pen, xev->event);
+
+            /* Only report button event; if there was also pen movement / pressure changes, we expect
+               an XI_Motion event first anyway */
+	    if (button == 1) {
+		/* button 1 is the pen tip */
+		if (pressed && SDL_PenPerformHitTest()) {
+		    /* Check whether we should handle window resize / move events */
+		    const SDL_WindowData *windowdata = X11_FindWindow(_this, xev->event);
+
+		    if (X11_ProcessHitTest(_this, windowdata, pen->last.x, pen->last.y)) {
+			SDL_SendWindowEvent(windowdata->window, SDL_EVENT_WINDOW_HIT_TEST, 0, 0);
+			return 1; /* Don't pass on this event */
+		    }
+		}
+		SDL_SendPenTipEvent(0, pen->header.id,
+				    pressed ? SDL_PRESSED : SDL_RELEASED);
+	    } else {
+		SDL_SendPenButton(0, pen->header.id,
+				  pressed ? SDL_PRESSED : SDL_RELEASED,
+				  button - 1);
+	    }
+            return 1;
+        } else {
+            /* Otherwise assume a regular mouse */
+            SDL_WindowData *windowdata = xinput2_get_sdlwindowdata(videodata, xev->event);
+
+            if (xev->deviceid != xev->sourceid) {
+                /* Discard events from "Master" devices to avoid duplicates. */
+                return 1;
+            }
+
+            if (pressed) {
+                X11_HandleButtonPress(_this, windowdata, button,
+                                      xev->event_x, xev->event_y, xev->time);
+            } else {
+                X11_HandleButtonRelease(_this, windowdata, button);
+            }
+        }
+    } break;
+
+        /* Register to receive XI_Motion (which deactivates MotionNotify), so that we can distinguish
+           real mouse motions from synthetic ones, for multitouch and pen support. */
     case XI_Motion:
     {
         const XIDeviceEvent *xev = (const XIDeviceEvent *)cookie->data;
+        const SDL_Pen *pen = SDL_GetPenPtr(X11_PenIDFromDeviceID(xev->deviceid));
+#if SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH
         int pointer_emulated = (xev->flags & XIPointerEmulated);
+#endif /* SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH */
+
+        if (xev->deviceid != xev->sourceid) {
+            /* Discard events from "Master" devices to avoid duplicates. */
+            return 1;
+        }
+
+        if (pen) {
+            SDL_PenStatusInfo pen_status;
+
+            pen_status.x = xev->event_x;
+            pen_status.y = xev->event_y;
+
+            X11_PenAxesFromValuators(pen,
+                                     xev->valuators.values, xev->valuators.mask, xev->valuators.mask_len,
+                                     &pen_status.axes[0]);
+
+            xinput2_pen_ensure_window(_this, pen, xev->event);
 
+            SDL_SendPenMotion(0, pen->header.id,
+                              SDL_TRUE,
+                              &pen_status);
+            return 1;
+        }
+
+#if SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH
         if (!pointer_emulated) {
             SDL_Mouse *mouse = SDL_GetMouse();
             if (!mouse->relative_mode || mouse->relative_mode_warp) {
@@ -347,8 +472,10 @@ int X11_HandleXinput2Event(SDL_VideoData *videodata, XGenericEventCookie *cookie
             }
         }
         return 1;
+#endif /* SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH */
     } break;
 
+#if SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH
     case XI_TouchBegin:
     {
         const XIDeviceEvent *xev = (const XIDeviceEvent *)cookie->data;
@@ -376,10 +503,9 @@ int X11_HandleXinput2Event(SDL_VideoData *videodata, XGenericEventCookie *cookie
         SDL_SendTouchMotion(0, xev->sourceid, xev->detail, window, x, y, 1.0);
         return 1;
     } break;
-
-#endif
+#endif /* SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH */
     }
-#endif
+#endif /* SDL_VIDEO_DRIVER_X11_XINPUT2 */
     return 0;
 }
 
@@ -460,6 +586,38 @@ int X11_Xinput2IsInitialized(void)
 #endif
 }
 
+SDL_bool X11_Xinput2SelectMouse(SDL_VideoDevice *_this, SDL_Window *window)
+{
+#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2
+    const SDL_VideoData *data = (SDL_VideoData *)_this->driverdata;
+    XIEventMask eventmask;
+    unsigned char mask[4] = { 0, 0, 0, 0 };
+    SDL_WindowData *window_data = (SDL_WindowData *)window->driverdata;
+
+    eventmask.mask_len = sizeof(mask);
+    eventmask.mask = mask;
+    eventmask.deviceid = XIAllDevices;
+
+    XISetMask(mask, XI_ButtonPress);
+    XISetMask(mask, XI_ButtonRelease);
+    XISetMask(mask, XI_Motion);
+    XISetMask(mask, XI_Enter);
+    XISetMask(mask, XI_Leave);
+    /* Hotplugging: */
+    XISetMask(mask, XI_DeviceChanged);
+    XISetMask(mask, XI_HierarchyChanged);
+    XISetMask(mask, XI_PropertyEvent); /* E.g., when swapping tablet pens */
+
+    if (X11_XISelectEvents(data->display,
+                           window_data->xwindow,
+                           &eventmask, 1) == Success) {
+        return SDL_TRUE;
+    }
+    SDL_LogWarn(SDL_LOG_CATEGORY_INPUT, "Could not enable XInput2 mouse event handling\n");
+#endif
+    return SDL_FALSE;
+}
+
 int X11_Xinput2IsMultitouchSupported(void)
 {
 #ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH

+ 2 - 1
src/video/x11/SDL_x11xinput2.h

@@ -32,11 +32,12 @@ typedef struct XGenericEventCookie XGenericEventCookie;
 
 extern void X11_InitXinput2(SDL_VideoDevice *_this);
 extern void X11_InitXinput2Multitouch(SDL_VideoDevice *_this);
-extern int X11_HandleXinput2Event(SDL_VideoData *videodata, XGenericEventCookie *cookie);
+extern int X11_HandleXinput2Event(SDL_VideoDevice *_this, XGenericEventCookie *cookie);
 extern int X11_Xinput2IsInitialized(void);
 extern int X11_Xinput2IsMultitouchSupported(void);
 extern void X11_Xinput2SelectTouch(SDL_VideoDevice *_this, SDL_Window *window);
 extern void X11_Xinput2GrabTouch(SDL_VideoDevice *_this, SDL_Window *window);
 extern void X11_Xinput2UngrabTouch(SDL_VideoDevice *_this, SDL_Window *window);
+extern SDL_bool X11_Xinput2SelectMouse(SDL_VideoDevice *_this, SDL_Window *window);
 
 #endif /* SDL_x11xinput2_h_ */

+ 1 - 0
test/CMakeLists.txt

@@ -328,6 +328,7 @@ add_sdl_test_executable(testgles2 SOURCES testgles2.c)
 add_sdl_test_executable(testgles2_sdf NEEDS_RESOURCES TESTUTILS SOURCES testgles2_sdf.c)
 add_sdl_test_executable(testhaptic SOURCES testhaptic.c)
 add_sdl_test_executable(testhotplug SOURCES testhotplug.c)
+add_sdl_test_executable(testpen SOURCES testpen.c)
 add_sdl_test_executable(testrumble SOURCES testrumble.c)
 add_sdl_test_executable(testthread NONINTERACTIVE NONINTERACTIVE_TIMEOUT 40 SOURCES testthread.c)
 add_sdl_test_executable(testiconv NEEDS_RESOURCES TESTUTILS SOURCES testiconv.c)

+ 1 - 0
test/testautomation.c

@@ -33,6 +33,7 @@ static SDLTest_TestSuiteReference *testSuites[] = {
     &mainTestSuite,
     &mathTestSuite,
     &mouseTestSuite,
+    &penTestSuite,
     &pixelsTestSuite,
     &platformTestSuite,
     &propertiesTestSuite,

+ 1908 - 0
test/testautomation_pen.c

@@ -0,0 +1,1908 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2023 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+
+#include <stddef.h>
+
+/**
+ * Pen test suite
+ */
+
+#define SDL_internal_h_ /* Inhibit dynamic symbol redefinitions that clash with ours */
+
+/* ================= System Under Test (SUT) ================== */
+/* Renaming SUT operations to avoid link-time symbol clashes */
+#define SDL_GetPens            SDL_SUT_GetPens
+#define SDL_GetPenStatus       SDL_SUT_GetPenStatus
+#define SDL_GetPenFromGUID     SDL_SUT_GetPenFromGUID
+#define SDL_GetPenGUID         SDL_SUT_GetPenGUID
+#define SDL_PenConnected       SDL_SUT_PenConnected
+#define SDL_GetPenName         SDL_SUT_GetPenName
+#define SDL_GetPenCapabilities SDL_SUT_GetPenCapabilities
+#define SDL_GetPenType         SDL_SUT_GetPenType
+
+#define SDL_GetPenPtr                SDL_SUT_GetPenPtr
+#define SDL_PenModifyBegin           SDL_SUT_PenModifyBegin
+#define SDL_PenModifyAddCapabilities SDL_SUT_PenModifyAddCapabilities
+#define SDL_PenModifyForWacomID      SDL_SUT_PenModifyForWacomID
+#define SDL_PenUpdateGUIDForWacom    SDL_SUT_PenUpdateGUIDForWacom
+#define SDL_PenUpdateGUIDForType     SDL_SUT_PenUpdateGUIDForType
+#define SDL_PenUpdateGUIDForGeneric  SDL_SUT_PenUpdateGUIDForGeneric
+#define SDL_PenModifyEnd             SDL_SUT_PenModifyEnd
+#define SDL_PenGCMark                SDL_SUT_PenGCMark
+#define SDL_PenGCSweep               SDL_SUT_PenGCSweep
+#define SDL_SendPenMotion            SDL_SUT_SendPenMotion
+#define SDL_SendPenButton            SDL_SUT_SendPenButton
+#define SDL_SendPenTipEvent          SDL_SUT_SendPenTipEvent
+#define SDL_SendPenWindowEvent       SDL_SUT_SendPenWindowEvent
+#define SDL_PenPerformHitTest        SDL_SUT_PenPerformHitTest
+#define SDL_PenInit                  SDL_SUT_PenInit
+
+/* ================= Mock API ================== */
+
+#include <stdlib.h>
+#include <SDL3/SDL.h>
+#include <SDL3/SDL_test.h>
+/* For SDL_Window, SDL_Mouse, SDL_MouseID: */
+#include "../src/events/SDL_mouse_c.h"
+/* Divert calls to mock mouse API: */
+#define SDL_SendMouseMotion       SDL_Mock_SendMouseMotion
+#define SDL_SendMouseButton       SDL_Mock_SendMouseButton
+#define SDL_GetMouse              SDL_Mock_GetMouse
+#define SDL_MousePositionInWindow SDL_Mock_MousePositionInWindow
+#define SDL_SetMouseFocus         SDL_Mock_SetMouseFocus
+
+/* Mock mouse API */
+static int SDL_SendMouseMotion(Uint64 timestamp, SDL_Window *window, SDL_MouseID mouseID, int relative, float x, float y);
+static int SDL_SendMouseButton(Uint64 timestamp, SDL_Window *window, SDL_MouseID mouseID, Uint8 state, Uint8 button);
+static SDL_Mouse *SDL_GetMouse(void);
+static SDL_bool SDL_MousePositionInWindow(SDL_Window *window, SDL_MouseID mouseID, float x, float y);
+static void SDL_SetMouseFocus(SDL_Window *window);
+
+/* Import SUT code with macro-renamed function names  */
+#define SDL_waylanddyn_h_ /* hack: suppress spurious build problem with libdecor.h on Wayland */
+#include "../src/events/SDL_pen.c"
+#include "../src/events/SDL_pen_c.h"
+
+
+/* ================= Internal SDL API Compatibility ================== */
+/* Mock implementations of Pen -> Mouse calls */
+/* Not thread-safe! */
+
+static SDL_bool SDL_MousePositionInWindow(SDL_Window *window, SDL_MouseID mouseID, float x, float y)
+{
+    return SDL_TRUE;
+}
+
+static int _mouseemu_last_event = 0;
+static float _mouseemu_last_x = 0.0f;
+static float _mouseemu_last_y = 0.0f;
+static int _mouseemu_last_mouseid = 0;
+static int _mouseemu_last_button = 0;
+static int _mouseemu_last_relative = 0;
+static int _mouseemu_last_focus = -1;
+
+static int SDL_SendMouseButton(Uint64 timestamp, SDL_Window *window, SDL_MouseID mouseID, Uint8 state, Uint8 button)
+{
+    if (mouseID == SDL_PEN_MOUSEID) {
+        _mouseemu_last_event = (state == SDL_PRESSED) ? SDL_EVENT_MOUSE_BUTTON_DOWN : SDL_EVENT_MOUSE_BUTTON_UP;
+        _mouseemu_last_button = button;
+        _mouseemu_last_mouseid = mouseID;
+    }
+    return 1;
+}
+
+static int SDL_SendMouseMotion(Uint64 timestamp, SDL_Window *window, SDL_MouseID mouseID, int relative, float x, float y)
+{
+    if (mouseID == SDL_PEN_MOUSEID) {
+        _mouseemu_last_event = SDL_EVENT_MOUSE_MOTION;
+        _mouseemu_last_x = x;
+        _mouseemu_last_y = y;
+        _mouseemu_last_mouseid = mouseID;
+        _mouseemu_last_relative = relative;
+    }
+    return 1;
+}
+
+static SDL_Mouse *SDL_GetMouse(void)
+{
+    static SDL_Mouse dummy_mouse;
+
+    dummy_mouse.focus = NULL;
+    dummy_mouse.mouseID = 0;
+
+    return &dummy_mouse;
+}
+
+static void SDL_SetMouseFocus(SDL_Window *window)
+{
+    _mouseemu_last_focus = window ? 1 : 0;
+}
+
+/* ================= Test Case Support ================== */
+
+#define PEN_NUM_TEST_IDS 8
+
+/* Helper functions */
+
+/* Iterate over all pens to find index for pen ID, otherwise -1 */
+static int _pen_iterationFindsPenIDAt(SDL_PenID needle)
+{
+    int i;
+    int num_pens = -1;
+
+    SDL_PenID *pens = SDL_GetPens(&num_pens);
+    /* Check for (a) consistency and (b) ability to handle NULL parameter */
+    SDL_PenID *pens2 = SDL_GetPens(NULL);
+
+    SDLTest_AssertCheck(num_pens >= 0,
+                        "SDL_GetPens() yielded %d pens", num_pens);
+    SDLTest_AssertCheck(pens[num_pens] == 0,
+                        "SDL_GetPens() not 0 terminated (num_pens = %d)", num_pens);
+    SDLTest_AssertCheck(pens2[num_pens] == 0,
+                        "SDL_GetPens(NULL) not 0 terminated (num_pens = %d)", num_pens);
+
+    for (i = 0; i < num_pens; ++i) {
+        SDLTest_AssertCheck(pens[i] == pens2[i],
+                            "SDL_GetPens(&i) and SDL_GetPens(NULL) disagree at index %d/%d", i, num_pens);
+        SDLTest_AssertCheck(pens[i] != SDL_PEN_INVALID,
+                            "Invalid pen ID %08lx at index %d/%d after SDL_GetPens()", (unsigned long) pens[i], i, num_pens);
+    }
+    SDL_free(pens2);
+
+    for (i = 0; pens[i]; ++i) {
+        SDL_PenID pen_id = pens[i];
+
+        SDLTest_AssertCheck(pen_id != SDL_PEN_INVALID,
+                            "Invalid pen ID %08lx at index %d/%d after SDL_GetPens()", (unsigned long) pen_id, i, num_pens);
+        if (pen_id == needle) {
+            SDL_free(pens);
+            return i;
+        }
+    }
+    SDL_free(pens);
+    return -1;
+}
+
+/* Retrieve number of pens and sanity-check SDL_GetPens() */
+static int
+_num_pens(void)
+{
+    int num_pens = -1;
+    SDL_PenID *pens = SDL_GetPens(&num_pens);
+    SDLTest_AssertCheck(pens != NULL,
+                        "SDL_GetPens() => NULL");
+    SDLTest_AssertCheck(num_pens >= 0,
+                        "SDL_GetPens() reports %d pens", num_pens);
+    SDLTest_AssertCheck(pens[num_pens] == 0,
+                        "SDL_GetPens()[%d] != 0", num_pens);
+    SDL_free(pens);
+    return num_pens;
+}
+
+/* Assert number of pens is as expected */
+static void _AssertCheck_num_pens(int expected, char *location)
+{
+    int num_pens = _num_pens();
+    SDLTest_AssertCheck(expected == num_pens,
+                        "Expected SDL_GetPens() =>count = %d, actual = %d: %s", expected, num_pens, location);
+}
+
+/* ---------------------------------------- */
+/* Test device deallocation */
+
+typedef struct /* Collection of pen (de)allocation information  */
+{
+    unsigned int deallocated_id_flags;         /* ith bits set to 1 if the ith test_id is deallocated */
+    unsigned int deallocated_deviceinfo_flags; /* ith bits set to 1 if deviceinfo as *int with value i was deallocated */
+    SDL_PenID ids[PEN_NUM_TEST_IDS];
+    SDL_GUID guids[PEN_NUM_TEST_IDS];
+    SDL_Window *window;
+    int num_ids;
+    int initial_pen_count;
+} pen_testdata;
+
+/* SDL_PenGCSweep(): callback for tracking pen deallocation */
+static void _pen_testdata_callback(Uint32 deviceid, void *deviceinfo, void *tracker_ref)
+{
+    pen_testdata *tracker = (pen_testdata *)tracker_ref;
+    int offset = -1;
+    int i;
+
+    for (i = 0; i < tracker->num_ids; ++i) {
+        if (deviceid == tracker->ids[i]) {
+            tracker->deallocated_id_flags |= (1 << i);
+        }
+    }
+
+    SDLTest_AssertCheck(deviceinfo != NULL,
+                        "Device %lu has deviceinfo",
+                        (unsigned long) deviceid);
+    offset = *((int *)deviceinfo);
+    SDLTest_AssertCheck(offset >= 0 && offset <= 31,
+                        "Device %lu has well-formed deviceinfo %d",
+                        (unsigned long) deviceid, offset);
+    tracker->deallocated_deviceinfo_flags |= 1 << offset;
+    SDL_free(deviceinfo);
+}
+
+/* GC Sweep tracking: update "tracker->deallocated_id_flags" and "tracker->deallocated_deviceinfo_flags" to record deallocations */
+static void _pen_trackGCSweep(pen_testdata *tracker)
+{
+    tracker->deallocated_id_flags = 0;
+    tracker->deallocated_deviceinfo_flags = 0;
+    SDL_PenGCSweep(tracker, _pen_testdata_callback);
+}
+
+/* Finds a number of unused pen IDs (does not allocate them).  Also initialises GUIDs. */
+static void _pen_unusedIDs(pen_testdata *tracker, int count)
+{
+    static int guidmod = 0; /* Ensure uniqueness as long as we use no more than 256 test pens */
+    Uint32 synthetic_penid = 1000u;
+    int index = 0;
+
+    tracker->num_ids = count;
+    SDLTest_AssertCheck(count < PEN_NUM_TEST_IDS, "Test setup: Valid number of test IDs requested: %d", (int)count);
+
+    while (count--) {
+        int k;
+
+        while (SDL_GetPenPtr(synthetic_penid)) {
+            ++synthetic_penid;
+        }
+        tracker->ids[index] = synthetic_penid;
+        for (k = 0; k < 15; ++k) {
+            tracker->guids[index].data[k] = (16 * k) + index;
+        }
+        tracker->guids[index].data[15] = ++guidmod;
+
+        ++synthetic_penid;
+        ++index;
+    }
+}
+
+#define DEVICEINFO_UNCHANGED -17
+
+/* Allocate deviceinfo for pen */
+static void _pen_setDeviceinfo(SDL_Pen *pen, int deviceinfo)
+{
+    if (deviceinfo == DEVICEINFO_UNCHANGED) {
+        SDLTest_AssertCheck(pen->deviceinfo != NULL,
+                            "pen->deviceinfo was already set for %p (%lu), as expected",
+                            pen, (unsigned long) pen->header.id);
+    } else {
+        int *data = (int *)SDL_malloc(sizeof(int));
+        *data = deviceinfo;
+
+        SDLTest_AssertCheck(pen->deviceinfo == NULL,
+                            "pen->deviceinfo was NULL for %p (%lu) when requesting deviceinfo %d",
+                            pen, (unsigned long) pen->header.id, deviceinfo);
+
+        pen->deviceinfo = data;
+    }
+    SDL_PenModifyEnd(pen, SDL_TRUE);
+}
+
+/* ---------------------------------------- */
+/* Back up and restore device information */
+
+typedef struct deviceinfo_backup
+{
+    Uint32 deviceid;
+    void *deviceinfo;
+    struct deviceinfo_backup *next;
+} deviceinfo_backup;
+
+/* SDL_PenGCSweep(): Helper callback for collecting all deviceinfo records */
+static void _pen_accumulate_gc_sweep(Uint32 deviceid, void *deviceinfo, void *backup_ref)
+{
+    deviceinfo_backup **db_ref = (deviceinfo_backup **)backup_ref;
+    deviceinfo_backup *next = *db_ref;
+
+    *db_ref = SDL_calloc(sizeof(deviceinfo_backup), 1);
+    (*db_ref)->deviceid = deviceid;
+    (*db_ref)->deviceinfo = deviceinfo;
+    (*db_ref)->next = next;
+}
+
+/* SDL_PenGCSweep(): Helper callback that must never be called  */
+static void _pen_assert_impossible(Uint32 deviceid, void *deviceinfo, void *backup_ref)
+{
+    SDLTest_AssertCheck(0, "Deallocation for deviceid %lu during enableAndRestore: not expected",
+                        (unsigned long) deviceid);
+}
+
+/* Disable all pens and store their status */
+static deviceinfo_backup *_pen_disableAndBackup(void)
+{
+    deviceinfo_backup *backup = NULL;
+
+    SDL_PenGCMark();
+    SDL_PenGCSweep(&backup, _pen_accumulate_gc_sweep);
+    return backup;
+}
+
+/* Restore all pens to their previous status */
+static void _pen_enableAndRestore(deviceinfo_backup *backup, int test_marksweep)
+{
+    if (test_marksweep) {
+        SDL_PenGCMark();
+    }
+    while (backup) {
+        SDL_Pen *disabledpen = SDL_GetPenPtr(backup->deviceid);
+        deviceinfo_backup *next = backup->next;
+
+        SDL_PenModifyEnd(SDL_PenModifyBegin(disabledpen->header.id),
+                         SDL_TRUE);
+        disabledpen->deviceinfo = backup->deviceinfo;
+
+        SDL_free(backup);
+        backup = next;
+    }
+    if (test_marksweep) {
+        SDL_PenGCSweep(NULL, _pen_assert_impossible);
+    }
+}
+
+static struct SDL_Window _test_window = { 0 };
+
+/* ---------------------------------------- */
+/* Default set-up and tear down routines    */
+
+/* Back up existing pens, allocate fresh ones but don't assign them yet */
+static deviceinfo_backup *_setup_test(pen_testdata *ptest, int pens_for_testing)
+{
+    int i;
+    deviceinfo_backup *backup;
+
+    /* Get number of pens */
+    SDL_free(SDL_GetPens(&ptest->initial_pen_count));
+
+    /* Provide fake window for window enter/exit simulation */
+    _test_window.id = 0x7e57da7a;
+    _test_window.w = 1600;
+    _test_window.h = 1200;
+    ptest->window = &_test_window;
+
+    /* Grab unused pen IDs for testing */
+    _pen_unusedIDs(ptest, pens_for_testing);
+    for (i = 0; i < pens_for_testing; ++i) {
+        int index = _pen_iterationFindsPenIDAt(ptest->ids[i]);
+        SDLTest_AssertCheck(-1 == index,
+                            "Registered PenID(%lu) since index %d == -1",
+                            (unsigned long) ptest->ids[i], index);
+    }
+
+    /* Remove existing pens, but back up */
+    backup = _pen_disableAndBackup();
+
+    _AssertCheck_num_pens(0, "after disabling and backing up all current pens");
+    SDLTest_AssertPass("Removed existing pens");
+
+    return backup;
+}
+
+static void _teardown_test_general(pen_testdata *ptest, deviceinfo_backup *backup, int with_gc_test)
+{
+    /* Restore previously existing pens */
+    _pen_enableAndRestore(backup, with_gc_test);
+
+    /* validate */
+    SDLTest_AssertPass("Restored pens to pre-test state");
+    _AssertCheck_num_pens(ptest->initial_pen_count, "after restoring all initial pens");
+}
+
+static void _teardown_test(pen_testdata *ptest, deviceinfo_backup *backup)
+{
+    _teardown_test_general(ptest, backup, 0);
+}
+
+static void _teardown_test_with_gc(pen_testdata *ptest, deviceinfo_backup *backup)
+{
+    _teardown_test_general(ptest, backup, 1);
+}
+
+/* ---------------------------------------- */
+/* Pen simulation                           */
+
+#define SIMPEN_ACTION_DONE           0
+#define SIMPEN_ACTION_MOVE_X         1
+#define SIMPEN_ACTION_MOVE_Y         2
+#define SIMPEN_ACTION_AXIS           3
+#define SIMPEN_ACTION_MOTION_EVENT   4 /* epxlicit motion event */
+#define SIMPEN_ACTION_MOTION_EVENT_S 5 /* send motion event but expect it to be suppressed */
+#define SIMPEN_ACTION_PRESS          6 /* implicit update event */
+#define SIMPEN_ACTION_RELEASE        7 /* implicit update event */
+#define SIMPEN_ACTION_DOWN           8 /* implicit update event */
+#define SIMPEN_ACTION_UP             9 /* implicit update event */
+#define SIMPEN_ACTION_ERASER_MODE    10
+
+/* Individual action in pen simulation script */
+typedef struct simulated_pen_action
+{
+    int type;
+    int pen_index; /* index into the list of simulated pens */
+    int index;     /* button or axis number, if needed */
+    float update;  /* x,y; for AXIS, update[0] is the updated axis */
+} simulated_pen_action;
+
+static simulated_pen_action _simpen_event(int type, int pen_index, int index, float v, int line_nr)
+{
+    simulated_pen_action action;
+    action.type = type;
+    action.pen_index = pen_index;
+    action.index = index;
+    action.update = v;
+
+    /* Sanity check-- turned out to be necessary */
+    if ((type == SIMPEN_ACTION_PRESS || type == SIMPEN_ACTION_RELEASE) && index == 0) {
+	SDL_Log("Error: SIMPEN_EVENT_BUTTON must have button > 0  (first button has number 1!), in line %d!", line_nr);
+        exit(1);
+    }
+    return action;
+}
+
+/* STEP is passed in later (C macros use dynamic scoping) */
+
+#define SIMPEN_DONE() \
+    STEP _simpen_event(SIMPEN_ACTION_DONE, 0, 0, 0.0f, __LINE__)
+#define SIMPEN_MOVE(pen_index, x, y)                                         \
+    STEP _simpen_event(SIMPEN_ACTION_MOVE_X, (pen_index), 0, (x), __LINE__); \
+    STEP _simpen_event(SIMPEN_ACTION_MOVE_Y, (pen_index), 0, (y), __LINE__)
+
+#define SIMPEN_AXIS(pen_index, axis, y) \
+    STEP _simpen_event(SIMPEN_ACTION_AXIS, (pen_index), (axis), (y), __LINE__)
+
+#define SIMPEN_EVENT_MOTION(pen_index) \
+    STEP _simpen_event(SIMPEN_ACTION_MOTION_EVENT, (pen_index), 0, 0.0f, __LINE__)
+
+#define SIMPEN_EVENT_MOTION_SUPPRESSED(pen_index) \
+    STEP _simpen_event(SIMPEN_ACTION_MOTION_EVENT_S, (pen_index), 0, 0.0f, __LINE__)
+
+#define SIMPEN_EVENT_BUTTON(pen_index, push, button) \
+    STEP _simpen_event((push) ? SIMPEN_ACTION_PRESS : SIMPEN_ACTION_RELEASE, (pen_index), (button), 0.0f, __LINE__)
+
+#define SIMPEN_EVENT_TIP(pen_index, touch, tip)				\
+    STEP _simpen_event((touch) ? SIMPEN_ACTION_DOWN : SIMPEN_ACTION_UP, (pen_index), tip, 0.0f, __LINE__)
+
+#define SIMPEN_SET_ERASER(pen_index, eraser_mode) \
+    STEP _simpen_event(SIMPEN_ACTION_ERASER_MODE, (pen_index), eraser_mode, 0.0f, __LINE__)
+
+static void
+_pen_dump(const char *prefix, SDL_Pen *pen)
+{
+    int i;
+    char *axes_str;
+
+    if (!pen) {
+        SDL_Log("(NULL pen)");
+        return;
+    }
+
+    axes_str = SDL_strdup("");
+    for (i = 0; i < SDL_PEN_NUM_AXES; ++i) {
+        char *old_axes_str = axes_str;
+        SDL_asprintf(&axes_str, "%s\t%f", old_axes_str, pen->last.axes[i]);
+        SDL_free(old_axes_str);
+    }
+    SDL_Log("%s: pen %lu (%s): status=%04lx, flags=%lx, x,y=(%f, %f) axes = %s",
+            prefix,
+            (unsigned long) pen->header.id,
+            pen->name,
+            (unsigned long) pen->last.buttons,
+            (unsigned long) pen->header.flags,
+            pen->last.x, pen->last.y,
+            axes_str);
+    SDL_free(axes_str);
+}
+
+/* Runs until the next event has been issued or we are done and returns pointer to it.
+   Returns NULL once we hit SIMPEN_ACTION_DONE.
+   Updates simulated_pens accordingly.  There must be as many simulated_pens as the highest pen_index used in
+   any of the "steps".
+   Also validates the internal state with expectations (via SDL_GetPenStatus()) and updates the, but does not poll SDL events. */
+static simulated_pen_action *
+_pen_simulate(simulated_pen_action *steps, int *step_counter, SDL_Pen *simulated_pens, int num_pens)
+{
+    SDL_bool done = SDL_FALSE;
+    SDL_bool dump_pens = SDL_FALSE;
+    unsigned int mask;
+    int pen_nr;
+
+    do {
+        simulated_pen_action step = steps[*step_counter];
+        SDL_Pen *simpen = &simulated_pens[step.pen_index];
+
+        if (step.pen_index >= num_pens) {
+            SDLTest_AssertCheck(0,
+                                "Unexpected pen index %d at step %d, action %d", step.pen_index, *step_counter, step.type);
+            return NULL;
+        }
+
+        switch (step.type) {
+        case SIMPEN_ACTION_DONE:
+            SDLTest_AssertPass("SIMPEN_ACTION_DONE");
+            return NULL;
+
+        case SIMPEN_ACTION_MOVE_X:
+            SDLTest_AssertPass("SIMPEN_ACTION_MOVE_X [pen %d] : y <- %f", step.pen_index, step.update);
+            simpen->last.x = step.update;
+            break;
+
+        case SIMPEN_ACTION_MOVE_Y:
+            SDLTest_AssertPass("SIMPEN_ACTION_MOVE_Y [pen %d] : x <- %f", step.pen_index, step.update);
+            simpen->last.y = step.update;
+            break;
+
+        case SIMPEN_ACTION_AXIS:
+            SDLTest_AssertPass("SIMPEN_ACTION_AXIS [pen %d] : axis[%d] <- %f", step.pen_index, step.index, step.update);
+            simpen->last.axes[step.index] = step.update;
+            break;
+
+        case SIMPEN_ACTION_MOTION_EVENT:
+            done = SDL_TRUE;
+            SDLTest_AssertCheck(SDL_SendPenMotion(0, simpen->header.id, SDL_TRUE,
+                                                  &simpen->last),
+                                "SIMPEN_ACTION_MOTION_EVENT [pen %d]", step.pen_index);
+            break;
+
+        case SIMPEN_ACTION_MOTION_EVENT_S:
+            SDLTest_AssertCheck(!SDL_SendPenMotion(0, simpen->header.id, SDL_TRUE,
+                                                   &simpen->last),
+                                "SIMPEN_ACTION_MOTION_EVENT_SUPPRESSED [pen %d]", step.pen_index);
+            break;
+
+        case SIMPEN_ACTION_PRESS:
+            mask = (1 << (step.index - 1));
+            simpen->last.buttons |= mask;
+            SDLTest_AssertCheck(SDL_SendPenButton(0, simpen->header.id, SDL_PRESSED, step.index),
+                                "SIMPEN_ACTION_PRESS [pen %d]: button %d (mask %x)", step.pen_index, step.index, mask);
+            done = SDL_TRUE;
+            break;
+
+        case SIMPEN_ACTION_RELEASE:
+            mask = ~(1 << (step.index - 1));
+            simpen->last.buttons &= mask;
+            SDLTest_AssertCheck(SDL_SendPenButton(0, simpen->header.id, SDL_RELEASED, step.index),
+                                "SIMPEN_ACTION_RELEASE [pen %d]: button %d (mask %x)", step.pen_index, step.index, mask);
+            done = SDL_TRUE;
+            break;
+
+        case SIMPEN_ACTION_DOWN:
+            simpen->last.buttons |= SDL_PEN_DOWN_MASK;
+            SDLTest_AssertCheck(SDL_SendPenTipEvent(0, simpen->header.id, SDL_PRESSED),
+                                "SIMPEN_ACTION_DOWN [pen %d]: (mask %lx)", step.pen_index, SDL_PEN_DOWN_MASK);
+            done = SDL_TRUE;
+            break;
+
+        case SIMPEN_ACTION_UP:
+            simpen->last.buttons &= ~SDL_PEN_DOWN_MASK;
+            SDLTest_AssertCheck(SDL_SendPenTipEvent(0, simpen->header.id, SDL_RELEASED),
+                                "SIMPEN_ACTION_UP [pen %d]: (mask %lx)", step.pen_index, ~SDL_PEN_DOWN_MASK);
+            done = SDL_TRUE;
+            break;
+
+        case SIMPEN_ACTION_ERASER_MODE: {
+	    Uint32 pmask;
+	    SDL_Pen *pen = SDL_PenModifyBegin(simpen->header.id);
+
+	    if (step.index) {
+		pmask = SDL_PEN_ERASER_MASK;
+	    } else {
+		pmask = SDL_PEN_INK_MASK;
+	    }
+
+	    SDL_PenModifyAddCapabilities(pen, pmask);
+	    SDL_PenModifyEnd(pen, SDL_TRUE);
+
+	    simpen->header.flags &= ~(SDL_PEN_INK_MASK | SDL_PEN_ERASER_MASK);
+	    simpen->header.flags |= pmask;
+	    break;
+	}
+
+        default:
+            SDLTest_AssertCheck(0,
+                                "Unexpected pen simulation action %d", step.type);
+            return NULL;
+        }
+        ++(*step_counter);
+    } while (!done);
+
+    for (pen_nr = 0; pen_nr < num_pens; ++pen_nr) {
+        SDL_Pen *simpen = &simulated_pens[pen_nr];
+        float x = -1.0f, y = -1.0f;
+        float axes[SDL_PEN_NUM_AXES];
+        Uint32 actual_flags = SDL_GetPenStatus(simpen->header.id, &x, &y, axes, SDL_PEN_NUM_AXES);
+        int i;
+
+        if (simpen->last.x != x || simpen->last.y != y) {
+            SDLTest_AssertCheck(0, "Coordinate mismatch in pen %d", pen_nr);
+            dump_pens = SDL_TRUE;
+        }
+        if ((actual_flags & ~(SDL_PEN_INK_MASK | SDL_PEN_ERASER_MASK)) != (simpen->last.buttons & ~(SDL_PEN_INK_MASK | SDL_PEN_ERASER_MASK))) {
+            SDLTest_AssertCheck(0, "Status mismatch in pen %d (reported: %08x)", pen_nr, (unsigned int)actual_flags);
+            dump_pens = SDL_TRUE;
+        }
+        if ((actual_flags & (SDL_PEN_INK_MASK | SDL_PEN_ERASER_MASK)) != (simpen->header.flags & (SDL_PEN_INK_MASK | SDL_PEN_ERASER_MASK))) {
+            SDLTest_AssertCheck(0, "Flags mismatch in pen %d (reported: %08x)", pen_nr, (unsigned int)actual_flags);
+            dump_pens = SDL_TRUE;
+        }
+        for (i = 0; i < SDL_PEN_NUM_AXES; ++i) {
+            if (axes[i] != simpen->last.axes[i]) {
+                SDLTest_AssertCheck(0, "Axis %d mismatch in pen %d", pen_nr, i);
+                dump_pens = SDL_TRUE;
+            }
+        }
+    }
+
+    if (dump_pens) {
+        int i;
+        for (i = 0; i < num_pens; ++i) {
+            SDL_Log("==== pen #%d", i);
+            _pen_dump("expect", simulated_pens + i);
+            _pen_dump("actual", SDL_GetPenPtr(simulated_pens[i].header.id));
+        }
+    }
+
+    return &steps[(*step_counter) - 1];
+}
+
+/* Init simulated_pens with suitable initial state */
+static void
+_pen_simulate_init(pen_testdata *ptest, SDL_Pen *simulated_pens, int num_pens)
+{
+    int i;
+    for (i = 0; i < num_pens; ++i) {
+        simulated_pens[i] = *SDL_GetPenPtr(ptest->ids[i]);
+    }
+}
+
+/* ---------------------------------------- */
+/* Other helper functions                   */
+
+/* "standard" pen registration process */
+static SDL_Pen *
+_pen_register(SDL_PenID penid, SDL_GUID guid, char *name, Uint32 flags)
+{
+    SDL_Pen *pen = SDL_PenModifyBegin(penid);
+    pen->guid = guid;
+    SDL_strlcpy(pen->name, name, SDL_PEN_MAX_NAME);
+    SDL_PenModifyAddCapabilities(pen, flags);
+    return pen;
+}
+
+/* Test whether EXPECTED and ACTUAL of type TY agree.  Their C format string must be FMT.
+   MESSAGE is a string with one format string, passed as ARG0. */
+#define SDLTest_AssertEq1(TY, FMT, EXPECTED, ACTUAL, MESSAGE, ARG0)                                                                                               \
+    {                                                                                                                                                             \
+        TY _t_expect = (EXPECTED);                                                                                                                                \
+        TY _t_actual = (ACTUAL);                                                                                                                                  \
+        SDLTest_AssertCheck(_t_expect == _t_actual, "L%d: " MESSAGE ": expected " #EXPECTED " = " FMT ", actual = " FMT, __LINE__, (ARG0), _t_expect, _t_actual); \
+    }
+
+/* ================= Test Case Implementation ================== */
+
+/**
+ * @brief Check basic pen device introduction and iteration, as well as basic queries
+ *
+ * @sa SDL_GetPens, SDL_GetPenName, SDL_GetPenCapabilities
+ */
+static int
+pen_iteration(void *arg)
+{
+    pen_testdata ptest;
+    int i;
+    char long_pen_name[SDL_PEN_MAX_NAME + 10];
+    const char *name;
+    deviceinfo_backup *backup;
+
+    /* Check initial pens */
+    SDL_PumpEvents();
+    SDLTest_AssertPass("SDL_GetPens() => count = %d", _num_pens());
+
+    /* Grab unused pen IDs for testing */
+    backup = _setup_test(&ptest, 3); /* validates that we have zero pens */
+
+    /* Re-run GC, track deallocations */
+    SDL_PenGCMark();
+    _pen_trackGCSweep(&ptest);
+    _AssertCheck_num_pens(0, "after second GC pass");
+    SDLTest_AssertCheck(ptest.deallocated_id_flags == 0, "No unexpected device deallocations");
+    SDLTest_AssertCheck(ptest.deallocated_deviceinfo_flags == 0, "No unexpected deviceinfo deallocations");
+    SDLTest_AssertPass("Validated that GC on empty pen set is idempotent");
+
+    /* Add three pens, validate */
+    SDL_PenGCMark();
+
+    SDL_memset(long_pen_name, 'x', sizeof(long_pen_name)); /* Include pen name that is too long */
+    long_pen_name[sizeof(long_pen_name) - 1] = 0;
+
+    _pen_setDeviceinfo(_pen_register(ptest.ids[0], ptest.guids[0], "pen 0",
+                                     SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK),
+                       16);
+    _pen_setDeviceinfo(_pen_register(ptest.ids[2], ptest.guids[2], long_pen_name,
+                                     SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK),
+                       20);
+    _pen_setDeviceinfo(_pen_register(ptest.ids[1], ptest.guids[1], "pen 1",
+                                     SDL_PEN_ERASER_MASK | SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_YTILT_MASK),
+                       24);
+    _pen_trackGCSweep(&ptest);
+
+    _AssertCheck_num_pens(3, "after allocating three pens");
+
+    SDLTest_AssertCheck(ptest.deallocated_id_flags == 0, "No unexpected device deallocations");
+    SDLTest_AssertCheck(ptest.deallocated_deviceinfo_flags == 0, "No unexpected deviceinfo deallocations");
+
+    for (i = 0; i < 3; ++i) {
+        /* Check that all pens are accounted for */
+        int index = _pen_iterationFindsPenIDAt(ptest.ids[i]);
+        SDLTest_AssertCheck(-1 != index, "Found PenID(%lu)", (unsigned long) ptest.ids[i]);
+    }
+    SDLTest_AssertPass("Validated that all three pens are indexable");
+
+    /* Check pen properties */
+    SDLTest_AssertCheck(0 == SDL_strcmp("pen 0", SDL_GetPenName(ptest.ids[0])),
+                        "Pen #0 name");
+    SDLTest_AssertCheck((SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK) == SDL_GetPenCapabilities(ptest.ids[0], NULL),
+                        "Pen #0 capabilities");
+
+    SDLTest_AssertCheck(0 == SDL_strcmp("pen 1", SDL_GetPenName(ptest.ids[1])),
+                        "Pen #1 name");
+    SDLTest_AssertCheck((SDL_PEN_ERASER_MASK | SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_YTILT_MASK) == SDL_GetPenCapabilities(ptest.ids[1], NULL),
+                        "Pen #1 capabilities");
+
+    name = SDL_GetPenName(ptest.ids[2]);
+    SDLTest_AssertCheck(SDL_PEN_MAX_NAME - 1 == SDL_strlen(name),
+                        "Pen #2 name length");
+    SDLTest_AssertCheck(0 == SDL_memcmp(name, long_pen_name, SDL_PEN_MAX_NAME - 1),
+                        "Pen #2 name contents");
+    SDLTest_AssertCheck((SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK) == SDL_GetPenCapabilities(ptest.ids[2], NULL),
+                        "Pen #2 capabilities");
+    SDLTest_AssertPass("Pen registration and basic queries");
+
+    /* Re-run GC, track deallocations */
+    SDL_PenGCMark();
+    _pen_trackGCSweep(&ptest);
+    _AssertCheck_num_pens(0, "after third GC pass");
+    SDLTest_AssertCheck(ptest.deallocated_id_flags == 0x07,
+                        "No unexpected device deallocation : %08x", ptest.deallocated_id_flags);
+    SDLTest_AssertCheck(ptest.deallocated_deviceinfo_flags == 0x01110000,
+                        "No unexpected deviceinfo deallocation : %08x ", ptest.deallocated_deviceinfo_flags);
+    SDLTest_AssertPass("Validated that GC on empty pen set is idempotent");
+
+    /* tear down and finish */
+    _teardown_test(&ptest, backup);
+    return TEST_COMPLETED;
+}
+
+static void
+_expect_pen_attached(SDL_PenID penid)
+{
+    SDLTest_AssertCheck(-1 != _pen_iterationFindsPenIDAt(penid),
+                        "Found PenID(%lu)", (unsigned long) penid);
+    SDLTest_AssertCheck(SDL_PenConnected(penid),
+                        "Pen %lu was attached, as expected", (unsigned long) penid);
+}
+
+static void
+_expect_pen_detached(SDL_PenID penid)
+{
+    SDLTest_AssertCheck(-1 == _pen_iterationFindsPenIDAt(penid),
+                        "Did not find PenID(%lu), as expected", (unsigned long) penid);
+    SDLTest_AssertCheck(!SDL_PenConnected(penid),
+                        "Pen %lu was detached, as expected", (unsigned long) penid);
+}
+
+#define ATTACHED(i) (1 << (i))
+
+static void
+_expect_pens_attached_or_detached(SDL_PenID *pen_ids, int ids, Uint32 mask)
+{
+    int i;
+    int attached_count = 0;
+    for (i = 0; i < ids; ++i) {
+        if (mask & (1 << i)) {
+            ++attached_count;
+            _expect_pen_attached(pen_ids[i]);
+        } else {
+            _expect_pen_detached(pen_ids[i]);
+        }
+    }
+    _AssertCheck_num_pens(attached_count, "While checking attached/detached status");
+}
+
+/**
+ * @brief Check pen device hotplugging
+ *
+ * @sa SDL_GetPens, SDL_GetPenName, SDL_GetPenCapabilities, SDL_PenConnected
+ */
+static int
+pen_hotplugging(void *arg)
+{
+    pen_testdata ptest;
+    deviceinfo_backup *backup = _setup_test(&ptest, 3);
+    SDL_GUID checkguid;
+
+    /* Add two pens */
+    SDL_PenGCMark();
+
+    _pen_setDeviceinfo(_pen_register(ptest.ids[0], ptest.guids[0], "pen 0", SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK),
+                       16);
+    _pen_setDeviceinfo(_pen_register(ptest.ids[2], ptest.guids[2], "pen 2", SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK),
+                       24);
+    _pen_trackGCSweep(&ptest);
+
+    _AssertCheck_num_pens(2, "after allocating two pens (pass 1)");
+    SDLTest_AssertCheck(ptest.deallocated_id_flags == 0, "No unexpected device deallocation (pass 1)");
+    SDLTest_AssertCheck(ptest.deallocated_deviceinfo_flags == 0, "No unexpected deviceinfo deallocation (pass 1)");
+
+    _expect_pens_attached_or_detached(ptest.ids, 3, ATTACHED(0) | ATTACHED(2));
+    SDLTest_AssertPass("Validated hotplugging (pass 1): attachmend of two pens");
+
+    /* Introduce pen #1, remove pen #2 */
+    SDL_PenGCMark();
+    _pen_setDeviceinfo(_pen_register(ptest.ids[0], ptest.guids[0], "pen 0", SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK),
+                       DEVICEINFO_UNCHANGED);
+    _pen_setDeviceinfo(_pen_register(ptest.ids[1], ptest.guids[1], "pen 1", SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK),
+                       20);
+    _pen_trackGCSweep(&ptest);
+
+    _AssertCheck_num_pens(2, "after allocating two pens (pass 2)");
+    SDLTest_AssertCheck(ptest.deallocated_id_flags == 0x04, "No unexpected device deallocation (pass 2): %x", ptest.deallocated_id_flags);
+    SDLTest_AssertCheck(ptest.deallocated_deviceinfo_flags == 0x01000000, "No unexpected deviceinfo deallocation (pass 2): %x", ptest.deallocated_deviceinfo_flags);
+
+    _expect_pens_attached_or_detached(ptest.ids, 3, ATTACHED(0) | ATTACHED(1));
+    SDLTest_AssertPass("Validated hotplugging (pass 2): unplug one, attach another");
+
+    /* Return to previous state (#0 and #2 attached) */
+    SDL_PenGCMark();
+
+    _pen_setDeviceinfo(_pen_register(ptest.ids[0], ptest.guids[0], "pen 0", SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_YTILT),
+                       DEVICEINFO_UNCHANGED);
+    _pen_setDeviceinfo(_pen_register(ptest.ids[2], ptest.guids[2], "pen 2", SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK),
+                       24);
+    _pen_trackGCSweep(&ptest);
+
+    _AssertCheck_num_pens(2, "after allocating two pens (pass 3)");
+    SDLTest_AssertCheck(ptest.deallocated_id_flags == 0x02, "No unexpected device deallocation (pass 3)");
+    SDLTest_AssertCheck(ptest.deallocated_deviceinfo_flags == 0x00100000, "No unexpected deviceinfo deallocation (pass 3)");
+
+    _expect_pens_attached_or_detached(ptest.ids, 3, ATTACHED(0) | ATTACHED(2));
+    SDLTest_AssertPass("Validated hotplugging (pass 3): return to state of pass 1");
+
+    /* Introduce pen #1, remove pen #0 */
+    SDL_PenGCMark();
+    _pen_setDeviceinfo(_pen_register(ptest.ids[1], ptest.guids[1], "pen 1", SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK),
+                       20);
+    _pen_setDeviceinfo(_pen_register(ptest.ids[2], ptest.guids[2], "pen 2", SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK),
+                       DEVICEINFO_UNCHANGED);
+    _pen_trackGCSweep(&ptest);
+
+    _AssertCheck_num_pens(2, "after allocating two pens (pass 4)");
+    SDLTest_AssertCheck(ptest.deallocated_id_flags == 0x01, "No unexpected device deallocation (pass 4): %x", ptest.deallocated_id_flags);
+    SDLTest_AssertCheck(ptest.deallocated_deviceinfo_flags == 0x00010000, "No unexpected deviceinfo deallocation (pass 4): %x", ptest.deallocated_deviceinfo_flags);
+
+    _expect_pens_attached_or_detached(ptest.ids, 3, ATTACHED(1) | ATTACHED(2));
+    SDLTest_AssertPass("Validated hotplugging (pass 5)");
+
+    /* Check detached pen */
+    SDLTest_AssertCheck(0 == SDL_strcmp("pen 0", SDL_GetPenName(ptest.ids[0])),
+                        "Pen #0 name");
+    checkguid = SDL_GetPenGUID(ptest.ids[0]);
+    SDLTest_AssertCheck(0 == SDL_memcmp(ptest.guids[0].data, checkguid.data, sizeof(ptest.guids[0].data)),
+                        "Pen #0 guid");
+    SDLTest_AssertCheck((SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_YTILT) == SDL_GetPenCapabilities(ptest.ids[0], NULL),
+                        "Pen #0 capabilities");
+    SDLTest_AssertPass("Validated that detached pens retained name, GUID, axis info after pass 5");
+
+    /* Individually detach #1 dn #2 */
+    _expect_pens_attached_or_detached(ptest.ids, 3, ATTACHED(1) | ATTACHED(2));
+    SDL_PenModifyEnd(SDL_PenModifyBegin(ptest.ids[1]), SDL_FALSE);
+    _expect_pens_attached_or_detached(ptest.ids, 3, ATTACHED(2));
+
+    SDL_PenModifyEnd(SDL_PenModifyBegin(ptest.ids[2]), SDL_FALSE);
+    _expect_pens_attached_or_detached(ptest.ids, 3, 0);
+
+    SDLTest_AssertPass("Validated individual hotplugging (pass 6)");
+
+    /* Individually attach all */
+    SDL_PenModifyEnd(SDL_PenModifyBegin(ptest.ids[2]), SDL_TRUE);
+    _expect_pens_attached_or_detached(ptest.ids, 3, ATTACHED(2));
+
+    SDL_PenModifyEnd(SDL_PenModifyBegin(ptest.ids[0]), SDL_TRUE);
+    _expect_pens_attached_or_detached(ptest.ids, 3, ATTACHED(0) | ATTACHED(2));
+
+    SDL_PenModifyEnd(SDL_PenModifyBegin(ptest.ids[1]), SDL_TRUE);
+    _expect_pens_attached_or_detached(ptest.ids, 3, ATTACHED(0) | ATTACHED(1) | ATTACHED(2));
+    SDLTest_AssertPass("Validated individual hotplugging (pass 7)");
+
+    SDL_PenGCMark();
+    _pen_trackGCSweep(&ptest);
+    _AssertCheck_num_pens(0, "after hotplugging test (cleanup)");
+    SDLTest_AssertCheck(ptest.deallocated_id_flags == 0x06, "No unexpected device deallocation (cleanup): %x", ptest.deallocated_id_flags);
+    SDLTest_AssertCheck(ptest.deallocated_deviceinfo_flags == 0x01100000, "No unexpected deviceinfo deallocation (pass 4): %x", ptest.deallocated_deviceinfo_flags);
+
+    _teardown_test_with_gc(&ptest, backup);
+
+    return TEST_COMPLETED;
+}
+
+/**
+ * @brief Check pen device GUID handling
+ *
+ * @sa SDL_GetPenGUID
+ */
+static int
+pen_GUIDs(void *arg)
+{
+    int i;
+    char *names[4] = { "pen 0", "pen 1", "pen 2", "pen 3" };
+    pen_testdata ptest;
+    deviceinfo_backup *backup;
+
+    backup = _setup_test(&ptest, 4);
+
+    /* Define four pens */
+    SDL_PenGCMark();
+    for (i = 0; i < 4; ++i) {
+        _pen_setDeviceinfo(_pen_register(ptest.ids[i], ptest.guids[i], names[i], SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK),
+                           20);
+    }
+    _pen_trackGCSweep(&ptest);
+
+    /* Detach pens 0 and 2 */
+    SDL_PenGCMark();
+    for (i = 1; i < 4; i += 2) {
+        _pen_setDeviceinfo(_pen_register(ptest.ids[i], ptest.guids[i], names[i], SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK),
+                           DEVICEINFO_UNCHANGED);
+    }
+    _pen_trackGCSweep(&ptest);
+
+    for (i = 0; i < 4; ++i) {
+        SDLTest_AssertCheck(ptest.ids[i] == SDL_GetPenFromGUID(ptest.guids[i]),
+                            "GUID search succeeded for %d", i);
+    }
+
+    /* detach all */
+    SDL_PenGCMark();
+    _pen_trackGCSweep(&ptest);
+
+    _teardown_test(&ptest, backup);
+    SDLTest_AssertPass("Pen ID lookup by GUID");
+
+    return TEST_COMPLETED;
+}
+
+/**
+ * @brief Check pen device button reporting
+ *
+ */
+static int
+pen_buttonReporting(void *arg)
+{
+    int i;
+    int button_nr, pen_nr;
+    pen_testdata ptest;
+    SDL_Event event;
+    SDL_PenStatusInfo update;
+    float axes[SDL_PEN_NUM_AXES + 1];
+    const float expected_x[2] = { 10.0f, 20.0f };
+    const float expected_y[2] = { 11.0f, 21.0f };
+    const Uint32 all_axes = SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK | SDL_PEN_AXIS_ROTATION_MASK | SDL_PEN_AXIS_SLIDER_MASK;
+
+    /* Register pen */
+    deviceinfo_backup *backup = _setup_test(&ptest, 2);
+    SDL_PenGCMark();
+    _pen_setDeviceinfo(_pen_register(ptest.ids[0], ptest.guids[0], "test pen",
+                                     SDL_PEN_INK_MASK | all_axes),
+                       20);
+    _pen_setDeviceinfo(_pen_register(ptest.ids[1], ptest.guids[1], "test eraser",
+                                     SDL_PEN_ERASER_MASK | all_axes),
+                       24);
+    _pen_trackGCSweep(&ptest);
+
+    /* Position mouse suitably before we start */
+    for (i = 0; i <= SDL_PEN_NUM_AXES; ++i) {
+        axes[i] = 0.0625f * i; /* initialise with numbers that can be represented precisely in IEEE 754 and
+                                  are > 0.0f and <= 1.0f */
+    }
+
+    /* Let pens enter the test window */
+    SDL_SendPenWindowEvent(0, ptest.ids[0], ptest.window);
+    SDL_SendPenWindowEvent(0, ptest.ids[1], ptest.window);
+
+    update.x = expected_x[0];
+    update.y = expected_y[0];
+    SDL_memcpy(update.axes, axes, sizeof(float) * SDL_PEN_NUM_AXES);
+    SDL_SendPenMotion(0, ptest.ids[0], SDL_TRUE, &update);
+    update.x = expected_x[1];
+    update.y = expected_y[1];
+    SDL_memcpy(update.axes, axes + 1, sizeof(float) * SDL_PEN_NUM_AXES);
+    SDL_SendPenMotion(0, ptest.ids[1], SDL_TRUE, &update);
+
+    while (SDL_PollEvent(&event))
+        ; /* Flush event queue */
+
+    /* Trigger pen tip events for PEN_DOWN */
+    SDLTest_AssertPass("Touch pens to surface");
+
+    for (pen_nr = 0; pen_nr < 2; ++pen_nr) {
+        float *expected_axes = axes + pen_nr;
+	SDL_bool found_event = SDL_FALSE;
+        Uint16 pen_state = 0x0000 | SDL_PEN_DOWN_MASK;
+	Uint8 tip = SDL_PEN_TIP_INK;
+
+        if (pen_nr == 1) {
+            pen_state |= SDL_PEN_ERASER_MASK;
+	    tip = SDL_PEN_TIP_ERASER;
+        }
+
+	SDL_SendPenTipEvent(0, ptest.ids[pen_nr], SDL_PRESSED);
+
+	while (SDL_PollEvent(&event)) {
+	    if (event.type == SDL_EVENT_PEN_DOWN) {
+		SDLTest_AssertCheck(event.ptip.which == ptest.ids[pen_nr],
+			    "Received SDL_EVENT_PEN_DOWN from correct pen");
+		SDLTest_AssertCheck(event.ptip.tip == (pen_nr == 0)? SDL_PEN_TIP_INK : SDL_PEN_TIP_ERASER,
+				    "Received SDL_EVENT_PEN_DOWN for correct tip");
+		SDLTest_AssertCheck(event.ptip.state == SDL_PRESSED,
+				    "Received SDL_EVENT_PEN_DOWN but and marked SDL_PRESSED");
+		SDLTest_AssertCheck(event.ptip.tip == tip,
+				    "Received tip %x but expected %x", event.ptip.tip, tip);
+		SDLTest_AssertCheck(event.ptip.pen_state == pen_state,
+				    "Received SDL_EVENT_PEN_DOWN, and state %04x == %04x (expected)",
+				    event.pbutton.pen_state, pen_state);
+		SDLTest_AssertCheck((event.ptip.x == expected_x[pen_nr]) && (event.ptip.y == expected_y[pen_nr]),
+				    "Received SDL_EVENT_PEN_DOWN event at correct coordinates: (%f, %f) vs (%f, %f) (expected)",
+				    event.pbutton.x, event.pbutton.y, expected_x[pen_nr], expected_y[pen_nr]);
+		SDLTest_AssertCheck(0 == SDL_memcmp(expected_axes, event.pbutton.axes, sizeof(float) * SDL_PEN_NUM_AXES),
+				    "Received SDL_EVENT_PEN_DOWN event with correct axis values");
+		found_event = SDL_TRUE;
+	    }
+            SDLTest_AssertCheck(found_event,
+                                "Received the expected SDL_EVENT_PEN_DOWN event");
+	}
+    }
+
+    SDLTest_AssertPass("Pen and eraser set up for button testing");
+
+    /* Actual tests start: pen, then eraser */
+    for (pen_nr = 0; pen_nr < 2; ++pen_nr) {
+        Uint16 pen_state = 0x0000 | SDL_PEN_DOWN_MASK;
+        float *expected_axes = axes + pen_nr;
+
+        if (pen_nr == 1) {
+            pen_state |= SDL_PEN_ERASER_MASK;
+        }
+        for (button_nr = 1; button_nr <= 8; ++button_nr) {
+            SDL_bool found_event = SDL_FALSE;
+            pen_state |= (1 << (button_nr - 1));
+
+            SDL_SendPenButton(0, ptest.ids[pen_nr], SDL_PRESSED, button_nr);
+            while (SDL_PollEvent(&event)) {
+                if (event.type == SDL_EVENT_PEN_BUTTON_DOWN) {
+                    SDLTest_AssertCheck(event.pbutton.which == ptest.ids[pen_nr],
+                                        "Received SDL_EVENT_PEN_BUTTON_DOWN from correct pen");
+                    SDLTest_AssertCheck(event.pbutton.button == button_nr,
+                                        "Received SDL_EVENT_PEN_BUTTON_DOWN from correct button");
+                    SDLTest_AssertCheck(event.pbutton.state == SDL_PRESSED,
+                                        "Received SDL_EVENT_PEN_BUTTON_DOWN but and marked SDL_PRESSED");
+                    SDLTest_AssertCheck(event.pbutton.pen_state == pen_state,
+                                        "Received SDL_EVENT_PEN_BUTTON_DOWN, and state %04x == %04x (expected)",
+                                        event.pbutton.pen_state, pen_state);
+                    SDLTest_AssertCheck((event.pbutton.x == expected_x[pen_nr]) && (event.pbutton.y == expected_y[pen_nr]),
+                                        "Received SDL_EVENT_PEN_BUTTON_DOWN event at correct coordinates: (%f, %f) vs (%f, %f) (expected)",
+                                        event.pbutton.x, event.pbutton.y, expected_x[pen_nr], expected_y[pen_nr]);
+                    SDLTest_AssertCheck(0 == SDL_memcmp(expected_axes, event.pbutton.axes, sizeof(float) * SDL_PEN_NUM_AXES),
+                                        "Received SDL_EVENT_PEN_BUTTON_DOWN event with correct axis values");
+                    if (0 != SDL_memcmp(expected_axes, event.pbutton.axes, sizeof(float) * SDL_PEN_NUM_AXES)) {
+                        int ax;
+                        for (ax = 0; ax < SDL_PEN_NUM_AXES; ++ax) {
+                            SDL_Log("\tax %d\t%.5f\t%.5f expected (equal=%d)",
+                                    ax,
+                                    event.pbutton.axes[ax], expected_axes[ax],
+                                    event.pbutton.axes[ax] == expected_axes[ax]);
+                        }
+                    }
+                    found_event = SDL_TRUE;
+                }
+            }
+            SDLTest_AssertCheck(found_event,
+                                "Received the expected SDL_EVENT_PEN_BUTTON_DOWN event");
+        }
+    }
+    SDLTest_AssertPass("Pressed all buttons");
+
+    /* Release every other button */
+    for (pen_nr = 0; pen_nr < 2; ++pen_nr) {
+        Uint16 pen_state = 0x00ff | SDL_PEN_DOWN_MASK; /* 8 buttons pressed */
+        float *expected_axes = axes + pen_nr;
+
+        if (pen_nr == 1) {
+            pen_state |= SDL_PEN_ERASER_MASK;
+        }
+        for (button_nr = pen_nr + 1; button_nr <= 8; button_nr += 2) {
+            SDL_bool found_event = SDL_FALSE;
+            pen_state &= ~(1 << (button_nr - 1));
+
+            SDL_SendPenButton(0, ptest.ids[pen_nr], SDL_RELEASED, button_nr);
+            while (SDL_PollEvent(&event)) {
+                if (event.type == SDL_EVENT_PEN_BUTTON_UP) {
+                    SDLTest_AssertCheck(event.pbutton.which == ptest.ids[pen_nr],
+                                        "Received SDL_EVENT_PEN_BUTTON_UP from correct pen");
+                    SDLTest_AssertCheck(event.pbutton.button == button_nr,
+                                        "Received SDL_EVENT_PEN_BUTTON_UP from correct button");
+                    SDLTest_AssertCheck(event.pbutton.state == SDL_RELEASED,
+                                        "Received SDL_EVENT_PEN_BUTTON_UP and is marked SDL_RELEASED");
+                    SDLTest_AssertCheck(event.pbutton.pen_state == pen_state,
+                                        "Received SDL_EVENT_PEN_BUTTON_UP, and state %04x == %04x (expected)",
+                                        event.pbutton.pen_state, pen_state);
+                    SDLTest_AssertCheck((event.pbutton.x == expected_x[pen_nr]) && (event.pbutton.y == expected_y[pen_nr]),
+                                        "Received SDL_EVENT_PEN_BUTTON_UP event at correct coordinates");
+                    SDLTest_AssertCheck(0 == SDL_memcmp(expected_axes, event.pbutton.axes, sizeof(float) * SDL_PEN_NUM_AXES),
+                                        "Received SDL_EVENT_PEN_BUTTON_UP event with correct axis values");
+                    found_event = SDL_TRUE;
+                }
+            }
+            SDLTest_AssertCheck(found_event,
+                                "Received the expected SDL_EVENT_PEN_BUTTON_UP event");
+        }
+    }
+    SDLTest_AssertPass("Released every other button");
+
+    /* Trigger pen tip events for PEN_UP */
+    SDLTest_AssertPass("Remove pens from surface");
+
+    for (pen_nr = 0; pen_nr < 2; ++pen_nr) {
+        float *expected_axes = axes + pen_nr;
+	SDL_bool found_event = SDL_FALSE;
+        Uint16 pen_state = 0x0000;
+	Uint8 tip = SDL_PEN_TIP_INK;
+
+        if (pen_nr == 1) {
+            pen_state |= SDL_PEN_ERASER_MASK;
+	    tip = SDL_PEN_TIP_ERASER;
+        }
+
+	SDL_SendPenTipEvent(0, ptest.ids[pen_nr], SDL_RELEASED);
+
+	while (SDL_PollEvent(&event)) {
+	    if (event.type == SDL_EVENT_PEN_UP) {
+		SDLTest_AssertCheck(event.ptip.which == ptest.ids[pen_nr],
+			    "Received SDL_EVENT_PEN_UP from correct pen");
+		SDLTest_AssertCheck(event.ptip.tip == (pen_nr == 0)? SDL_PEN_TIP_INK : SDL_PEN_TIP_ERASER,
+				    "Received SDL_EVENT_PEN_UP for correct tip");
+		SDLTest_AssertCheck(event.ptip.state == SDL_RELEASED,
+				    "Received SDL_EVENT_PEN_UP but and marked SDL_RELEASED");
+		SDLTest_AssertCheck(event.ptip.tip == tip,
+				    "Received tip %x but expected %x", event.ptip.tip, tip);
+		SDLTest_AssertCheck((event.ptip.pen_state & 0xff00) == (pen_state & 0xff00),
+				    "Received SDL_EVENT_PEN_UP, and state %04x == %04x (expected)",
+				    event.pbutton.pen_state, pen_state);
+		SDLTest_AssertCheck((event.ptip.x == expected_x[pen_nr]) && (event.ptip.y == expected_y[pen_nr]),
+				    "Received SDL_EVENT_PEN_UP event at correct coordinates: (%f, %f) vs (%f, %f) (expected)",
+				    event.pbutton.x, event.pbutton.y, expected_x[pen_nr], expected_y[pen_nr]);
+		SDLTest_AssertCheck(0 == SDL_memcmp(expected_axes, event.pbutton.axes, sizeof(float) * SDL_PEN_NUM_AXES),
+				    "Received SDL_EVENT_PEN_UP event with correct axis values");
+		found_event = SDL_TRUE;
+	    }
+            SDLTest_AssertCheck(found_event,
+                                "Received the expected SDL_EVENT_PEN_UP event");
+	}
+    }
+
+    /* Cleanup */
+    SDL_PenGCMark();
+    _pen_trackGCSweep(&ptest);
+    _teardown_test(&ptest, backup);
+
+    return TEST_COMPLETED;
+}
+
+/**
+ * @brief Check pen device movement and axis update reporting
+ *
+ * Also tests SDL_GetPenStatus for agreement with the most recently reported events
+ *
+ * @sa SDL_GetPenStatus
+ */
+static int
+pen_movementAndAxes(void *arg)
+{
+    pen_testdata ptest;
+    SDL_Event event;
+#define MAX_STEPS 80
+    /* Pen simulation */
+    simulated_pen_action steps[MAX_STEPS];
+    size_t num_steps = 0;
+
+    SDL_Pen simulated_pens[2];
+    int sim_pc = 0;
+    simulated_pen_action *last_action;
+
+    /* Register pen */
+    deviceinfo_backup *backup = _setup_test(&ptest, 2);
+
+    /* Pen simulation program */
+#define STEP steps[num_steps++] =
+
+    /* #1: Check basic reporting */
+    /* Hover eraser, tilt axes */
+    SIMPEN_MOVE(0, 30.0f, 31.0f);
+    SIMPEN_AXIS(0, SDL_PEN_AXIS_PRESSURE, 0.0f);
+    SIMPEN_AXIS(0, SDL_PEN_AXIS_XTILT, 22.5f);
+    SIMPEN_AXIS(0, SDL_PEN_AXIS_YTILT, 45.0f);
+    SIMPEN_EVENT_MOTION(0);
+
+    /* #2: Check that motion events without motion aren't reported */
+    SIMPEN_EVENT_MOTION_SUPPRESSED(0);
+    SIMPEN_EVENT_MOTION_SUPPRESSED(0);
+
+    /* #3: Check multiple pens being reported */
+    /* Move pen and touch surface, don't tilt */
+    SIMPEN_MOVE(1, 40.0f, 41.0f);
+    SIMPEN_AXIS(1, SDL_PEN_AXIS_PRESSURE, 0.25f);
+    SIMPEN_EVENT_MOTION(1);
+
+    /* $4: Multi-buttons */
+    /* Press eraser buttons */
+    SIMPEN_EVENT_TIP(0, "down", SDL_PEN_TIP_ERASER);
+    SIMPEN_EVENT_BUTTON(0, "push", 2);
+    SIMPEN_EVENT_BUTTON(0, "push", 1);
+    SIMPEN_EVENT_BUTTON(0, 0, 2); /* release again */
+    SIMPEN_EVENT_BUTTON(0, "push", 3);
+
+    /* #5: Check move + button actions connecting */
+    /* Move and tilt pen, press some pen buttons */
+    SIMPEN_MOVE(1, 3.0f, 8.0f);
+    SIMPEN_AXIS(1, SDL_PEN_AXIS_PRESSURE, 0.5f);
+    SIMPEN_AXIS(1, SDL_PEN_AXIS_XTILT, -21.0f);
+    SIMPEN_AXIS(1, SDL_PEN_AXIS_YTILT, -25.0f);
+    SIMPEN_EVENT_MOTION(1);
+    SIMPEN_EVENT_BUTTON(1, "push", 2);
+    SIMPEN_EVENT_TIP(1, "down", SDL_PEN_TIP_INK);
+
+    /* #6: Check nonterference between pens */
+    /* Eraser releases buttons */
+    SIMPEN_EVENT_BUTTON(0, 0, 1);
+    SIMPEN_EVENT_TIP(0, 0, SDL_PEN_TIP_ERASER);
+
+    /* #7: Press-move-release action */
+    /* Eraser press-move-release */
+    SIMPEN_EVENT_BUTTON(0, "push", 1);
+    SIMPEN_MOVE(0, 99.0f, 88.0f);
+    SIMPEN_AXIS(0, SDL_PEN_AXIS_PRESSURE, 0.625f);
+    SIMPEN_EVENT_MOTION(0);
+    SIMPEN_MOVE(0, 44.5f, 42.25f);
+    SIMPEN_EVENT_MOTION(0);
+    SIMPEN_EVENT_BUTTON(0, 0, 1);
+
+    /* #8: Intertwining button release actions some more */
+    /* Pen releases button */
+    SIMPEN_EVENT_BUTTON(1, 0, 2);
+    SIMPEN_EVENT_TIP(1, 0, SDL_PEN_TIP_INK);
+
+    /* Push one more pen button, then release all ereaser buttons */
+    SIMPEN_EVENT_TIP(1, "down", SDL_PEN_TIP_INK);
+    SIMPEN_EVENT_BUTTON(0, 0, 2);
+    SIMPEN_EVENT_BUTTON(0, 0, 3);
+
+    /* Lift up pen, flip it so it becomes an eraser, and touch it again  */
+    SIMPEN_EVENT_TIP(1, 0, SDL_PEN_TIP_INK);
+    SIMPEN_SET_ERASER(1, 1);
+    SIMPEN_EVENT_TIP(1, "push", SDL_PEN_TIP_ERASER);
+
+    /* And back again */
+    SIMPEN_EVENT_TIP(1, 0, SDL_PEN_TIP_ERASER);
+    SIMPEN_SET_ERASER(1, 0);
+    SIMPEN_EVENT_TIP(1, "push", SDL_PEN_TIP_INK);
+
+    /* #9: Suppress move on unsupported axis */
+    SIMPEN_AXIS(1, SDL_PEN_AXIS_DISTANCE, 0.25f);
+    SIMPEN_EVENT_MOTION_SUPPRESSED(0);
+
+    SIMPEN_DONE();
+#undef STEP
+    /* End of pen simulation program */
+
+    SDLTest_AssertCheck(num_steps < MAX_STEPS, "Pen simulation program does not exceed buffer size");
+#undef MAX_STEPS
+
+    SDL_PenGCMark();
+    _pen_setDeviceinfo(_pen_register(ptest.ids[0], ptest.guids[0], "test eraser",
+                                     SDL_PEN_ERASER_MASK | SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK),
+                       20);
+    _pen_setDeviceinfo(_pen_register(ptest.ids[1], ptest.guids[1], "test pen",
+                                     SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK),
+                       24);
+    _pen_trackGCSweep(&ptest);
+    SDL_SendPenWindowEvent(0, ptest.ids[0], ptest.window);
+    SDL_SendPenWindowEvent(0, ptest.ids[1], ptest.window);
+    while (SDL_PollEvent(&event))
+        ; /* Flush event queue */
+    SDLTest_AssertPass("Pen and eraser set up for testing");
+
+    _pen_simulate_init(&ptest, simulated_pens, 2);
+    /* Simulate pen movements */
+    while ((last_action = _pen_simulate(steps, &sim_pc, &simulated_pens[0], 2))) {
+        int attempts = 0;
+        SDL_Pen *simpen = &simulated_pens[last_action->pen_index];
+        SDL_PenID reported_which = -1;
+        float reported_x = -1.0f, reported_y = -1.0f;
+        float *reported_axes = NULL;
+        Uint32 reported_pen_state = 0;
+        Uint32 expected_pen_state = simpen->header.flags & SDL_PEN_ERASER_MASK;
+        SDL_bool dump_pens = SDL_FALSE;
+
+        do {
+            SDL_PumpEvents();
+            SDL_PollEvent(&event);
+            if (++attempts > 10000) {
+                SDLTest_AssertCheck(0, "Never got the anticipated event");
+                return TEST_ABORTED;
+            }
+        } while (event.type != SDL_EVENT_PEN_DOWN
+		 && event.type != SDL_EVENT_PEN_UP
+		 && event.type != SDL_EVENT_PEN_MOTION
+                 && event.type != SDL_EVENT_PEN_BUTTON_UP
+                 && event.type != SDL_EVENT_PEN_BUTTON_DOWN); /* skip boring events */
+
+        expected_pen_state |= simpen->last.buttons;
+
+        SDLTest_AssertCheck(0 != event.type,
+                            "Received the anticipated event");
+
+        switch (last_action->type) {
+        case SIMPEN_ACTION_MOTION_EVENT:
+            SDLTest_AssertCheck(event.type == SDL_EVENT_PEN_MOTION, "Expected pen motion event (but got 0x%lx)", (unsigned long) event.type);
+            reported_which = event.pmotion.which;
+            reported_x = event.pmotion.x;
+            reported_y = event.pmotion.y;
+            reported_pen_state = event.pmotion.pen_state;
+            reported_axes = &event.pmotion.axes[0];
+            break;
+
+        case SIMPEN_ACTION_PRESS:
+            SDLTest_AssertCheck(event.type == SDL_EVENT_PEN_BUTTON_DOWN, "Expected PENBUTTONDOWN event (but got 0x%lx)", (unsigned long) event.type);
+            SDLTest_AssertCheck(event.pbutton.state == SDL_PRESSED, "Expected PRESSED button");
+            /* Fall through */
+        case SIMPEN_ACTION_RELEASE:
+            if (last_action->type == SIMPEN_ACTION_RELEASE) {
+                SDLTest_AssertCheck(event.type == SDL_EVENT_PEN_BUTTON_UP, "Expected PENBUTTONUP event (but got 0x%lx)", (unsigned long) event.type);
+                SDLTest_AssertCheck(event.pbutton.state == SDL_RELEASED, "Expected RELEASED button");
+            }
+            SDLTest_AssertCheck(event.pbutton.button == last_action->index, "Expected button %d, but got %d",
+                                last_action->index, event.pbutton.button);
+            reported_which = event.pbutton.which;
+            reported_x = event.pbutton.x;
+            reported_y = event.pbutton.y;
+            reported_pen_state = event.pbutton.pen_state;
+            reported_axes = &event.pbutton.axes[0];
+            break;
+
+        case SIMPEN_ACTION_DOWN:
+            SDLTest_AssertCheck(event.type == SDL_EVENT_PEN_DOWN, "Expected PENBUTTONDOWN event (but got 0x%lx)", (unsigned long) event.type);
+            SDLTest_AssertCheck(event.ptip.state == SDL_PRESSED, "Expected PRESSED button");
+            /* Fall through */
+        case SIMPEN_ACTION_UP:
+            if (last_action->type == SIMPEN_ACTION_UP) {
+                SDLTest_AssertCheck(event.type == SDL_EVENT_PEN_UP, "Expected PENBUTTONUP event (but got 0x%lx)", (unsigned long) event.type);
+                SDLTest_AssertCheck(event.ptip.state == SDL_RELEASED, "Expected RELEASED button");
+            }
+            SDLTest_AssertCheck(event.ptip.tip == last_action->index, "Expected tip %d, but got %d",
+                                last_action->index, event.ptip.tip);
+            reported_which = event.ptip.which;
+            reported_x = event.ptip.x;
+            reported_y = event.ptip.y;
+            reported_pen_state = event.ptip.pen_state;
+            reported_axes = &event.ptip.axes[0];
+            break;
+
+        case SIMPEN_ACTION_ERASER_MODE:
+	    break;
+
+        default:
+            SDLTest_AssertCheck(0, "Error in pen simulator: unexpected action %d", last_action->type);
+            return TEST_ABORTED;
+        }
+
+        if (reported_which != simpen->header.id) {
+            dump_pens = SDL_TRUE;
+            SDLTest_AssertCheck(0, "Expected report for pen %lu but got report for pen %lu",
+                                (unsigned long) simpen->header.id,
+                                (unsigned long) reported_which);
+        }
+        if (reported_x != simpen->last.x || reported_y != simpen->last.y) {
+            dump_pens = SDL_TRUE;
+            SDLTest_AssertCheck(0, "Mismatch in pen coordinates");
+        }
+        if (reported_x != simpen->last.x || reported_y != simpen->last.y) {
+            dump_pens = SDL_TRUE;
+            SDLTest_AssertCheck(0, "Mismatch in pen coordinates");
+        }
+        if (reported_pen_state != expected_pen_state) {
+            dump_pens = SDL_TRUE;
+            SDLTest_AssertCheck(0, "Mismatch in pen state: %lx vs %lx (expected)",
+                                (unsigned long) reported_pen_state,
+                                (unsigned long) expected_pen_state);
+        }
+        if (0 != SDL_memcmp(reported_axes, simpen->last.axes, sizeof(float) * SDL_PEN_NUM_AXES)) {
+            dump_pens = SDL_TRUE;
+            SDLTest_AssertCheck(0, "Mismatch in axes");
+        }
+
+        if (dump_pens) {
+            SDL_Log("----- Pen #%d:", last_action->pen_index);
+            _pen_dump("expect", simpen);
+            _pen_dump("actual", SDL_GetPenPtr(simpen->header.id));
+        }
+    }
+    SDLTest_AssertPass("Pen and eraser move and report events correctly and independently");
+
+    /* Cleanup */
+    SDL_PenGCMark();
+    _pen_trackGCSweep(&ptest);
+    _teardown_test(&ptest, backup);
+    return TEST_COMPLETED;
+}
+
+static void
+_expect_pen_config(SDL_PenID penid,
+                   SDL_GUID expected_guid,
+                   SDL_bool expected_attached,
+                   char *expected_name,
+                   int expected_type,
+                   int expected_num_buttons,
+                   float expected_max_tilt,
+                   int expected_axes)
+{
+    SDL_PenCapabilityInfo actual_info = { 0 };
+    const char *actual_name = SDL_GetPenName(penid);
+
+    if (penid == SDL_PEN_INVALID) {
+        SDLTest_Assert(0, "Invalid pen ID");
+        return;
+    }
+
+    SDLTest_AssertEq1(int, "%d", 0, SDL_GUIDCompare(expected_guid, SDL_GetPenGUID(penid)),
+                      "Pen %lu guid equality", (unsigned long) penid);
+
+    SDLTest_AssertCheck(0 == SDL_strcmp(expected_name, actual_name),
+                        "Expected name='%s' vs actual='%s'", expected_name, actual_name);
+
+    SDLTest_AssertEq1(int, "%d", expected_attached, SDL_PenConnected(penid),
+                      "Pen %lu is attached", (unsigned long) penid);
+    SDLTest_AssertEq1(int, "%d", expected_type, SDL_GetPenType(penid),
+                      "Pen %lu type", (unsigned long) penid);
+    SDLTest_AssertEq1(int, "%x", expected_axes, SDL_GetPenCapabilities(penid, &actual_info),
+                      "Pen %lu axis flags", (unsigned long) penid);
+    SDLTest_AssertEq1(int, "%d", expected_num_buttons, actual_info.num_buttons,
+                      "Pen %lu number of buttons", (unsigned long) penid);
+    SDLTest_AssertEq1(float, "%f", expected_max_tilt, actual_info.max_tilt,
+                      "Pen %lu max tilt", (unsigned long) penid);
+}
+
+/**
+ * @brief Check backend pen iniitalisation and pen meta-information
+ *
+ * @sa SDL_GetPenCapabilities, SDL_PenAxisInfo
+ */
+static int
+pen_initAndInfo(void *arg)
+{
+    pen_testdata ptest;
+    SDL_Pen *pen;
+    Uint32 mask;
+    char strbuf[SDL_PEN_MAX_NAME];
+
+    /* Init */
+    deviceinfo_backup *backup = _setup_test(&ptest, 7);
+
+    /* Register default pen */
+    _expect_pens_attached_or_detached(ptest.ids, 7, 0);
+
+    /* Register completely default pen */
+    pen = SDL_PenModifyBegin(ptest.ids[0]);
+    SDL_memcpy(pen->guid.data, ptest.guids[0].data, sizeof(ptest.guids[0].data));
+    SDL_PenModifyEnd(pen, SDL_TRUE);
+
+    SDL_snprintf(strbuf, sizeof(strbuf),
+                 "Pen %lu", (unsigned long) ptest.ids[0]);
+    _expect_pen_config(ptest.ids[0], ptest.guids[0], SDL_TRUE,
+                       strbuf, SDL_PEN_TYPE_PEN, SDL_PEN_INFO_UNKNOWN, 0.0f,
+                       SDL_PEN_INK_MASK);
+    _expect_pens_attached_or_detached(ptest.ids, 7, ATTACHED(0));
+    SDLTest_AssertPass("Pass #1: default pen");
+
+    /* Register mostly-default pen with buttons and custom name */
+    pen = SDL_PenModifyBegin(ptest.ids[1]);
+    SDL_PenModifyAddCapabilities(pen, SDL_PEN_AXIS_PRESSURE_MASK);
+    SDL_memcpy(pen->guid.data, ptest.guids[1].data, sizeof(ptest.guids[1].data));
+    SDL_strlcpy(strbuf, "My special test pen", SDL_PEN_MAX_NAME);
+    SDL_strlcpy(pen->name, strbuf, SDL_PEN_MAX_NAME);
+    pen->info.num_buttons = 7;
+    SDL_PenModifyEnd(pen, SDL_TRUE);
+
+    _expect_pen_config(ptest.ids[1], ptest.guids[1], SDL_TRUE,
+                       strbuf, SDL_PEN_TYPE_PEN, 7, 0.0f,
+                       SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK);
+    _expect_pens_attached_or_detached(ptest.ids, 7, ATTACHED(0) | ATTACHED(1));
+    SDLTest_AssertPass("Pass #2: default pen with button and name info");
+
+    /* Register eraser with default name, but keep initially detached */
+    pen = SDL_PenModifyBegin(ptest.ids[2]);
+    SDL_memcpy(pen->guid.data, ptest.guids[2].data, sizeof(ptest.guids[2].data));
+    pen->type = SDL_PEN_TYPE_ERASER;
+    SDL_PenModifyAddCapabilities(pen, SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK);
+    SDL_PenModifyEnd(pen, SDL_FALSE);
+
+    SDL_snprintf(strbuf, sizeof(strbuf),
+                 "Eraser %lu", (unsigned long) ptest.ids[2]);
+    _expect_pen_config(ptest.ids[2], ptest.guids[2], SDL_FALSE,
+                       strbuf, SDL_PEN_TYPE_ERASER, SDL_PEN_INFO_UNKNOWN, SDL_PEN_INFO_UNKNOWN,
+                       SDL_PEN_ERASER_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK);
+    _expect_pens_attached_or_detached(ptest.ids, 7, ATTACHED(0) | ATTACHED(1));
+    /* now make available */
+    SDL_PenModifyEnd(SDL_PenModifyBegin(ptest.ids[2]), SDL_TRUE);
+    _expect_pen_config(ptest.ids[2], ptest.guids[2], SDL_TRUE,
+                       strbuf, SDL_PEN_TYPE_ERASER, SDL_PEN_INFO_UNKNOWN, SDL_PEN_INFO_UNKNOWN,
+                       SDL_PEN_ERASER_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK);
+    _expect_pens_attached_or_detached(ptest.ids, 7, ATTACHED(0) | ATTACHED(1) | ATTACHED(2));
+    SDLTest_AssertPass("Pass #3: eraser-type pen initially detached, then attached");
+
+    /* Abort pen registration */
+    pen = SDL_PenModifyBegin(ptest.ids[3]);
+    SDL_memcpy(pen->guid.data, ptest.guids[3].data, sizeof(ptest.guids[3].data));
+    SDL_PenModifyAddCapabilities(pen, SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK);
+    pen->type = SDL_PEN_TYPE_NONE;
+    SDL_PenModifyEnd(pen, SDL_TRUE);
+    _expect_pens_attached_or_detached(ptest.ids, 7, ATTACHED(0) | ATTACHED(1) | ATTACHED(2));
+    SDLTest_AssertCheck(NULL == SDL_GetPenName(ptest.ids[3]), "Pen with aborted registration remains unknown");
+    SDLTest_AssertPass("Pass #4: aborted pen registration");
+
+    /* Brush with custom axes */
+    pen = SDL_PenModifyBegin(ptest.ids[4]);
+    SDL_memcpy(pen->guid.data, ptest.guids[4].data, sizeof(ptest.guids[4].data));
+    SDL_strlcpy(pen->name, "Testish Brush", SDL_PEN_MAX_NAME);
+    pen->type = SDL_PEN_TYPE_BRUSH;
+    pen->info.num_buttons = 1;
+    SDL_PenModifyAddCapabilities(pen, SDL_PEN_AXIS_ROTATION_MASK);
+    pen->info.max_tilt = 72.5f;
+    SDL_PenModifyAddCapabilities(pen, SDL_PEN_AXIS_XTILT_MASK);
+    SDL_PenModifyAddCapabilities(pen, SDL_PEN_AXIS_PRESSURE_MASK);
+    SDL_PenModifyEnd(pen, SDL_TRUE);
+    _expect_pen_config(ptest.ids[4], ptest.guids[4], SDL_TRUE,
+                       "Testish Brush", SDL_PEN_TYPE_BRUSH, 1, 72.5f,
+                       SDL_PEN_INK_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_ROTATION_MASK | SDL_PEN_AXIS_PRESSURE_MASK);
+    _expect_pens_attached_or_detached(ptest.ids, 7, ATTACHED(0) | ATTACHED(1) | ATTACHED(2) | ATTACHED(4));
+    SDLTest_AssertPass("Pass #5: brush-type pen with unusual axis layout");
+
+    /* Wacom airbrush pen */
+    {
+        const Uint32 wacom_type_id = 0x0912;
+        const Uint32 wacom_serial_id = 0xa0b1c2d3;
+        SDL_GUID guid = {
+            { 0, 0, 0, 0,
+              0, 0, 0, 0,
+              0, 0, 0, 0,
+              0, 0, 0, 0 }
+        };
+        guid.data[0] = (wacom_serial_id >> 0) & 0xff;
+        guid.data[1] = (wacom_serial_id >> 8) & 0xff;
+        guid.data[2] = (wacom_serial_id >> 16) & 0xff;
+        guid.data[3] = (wacom_serial_id >> 24) & 0xff;
+        guid.data[4] = (wacom_type_id >> 0) & 0xff;
+        guid.data[5] = (wacom_type_id >> 8) & 0xff;
+        guid.data[6] = (wacom_type_id >> 16) & 0xff;
+        guid.data[7] = (wacom_type_id >> 24) & 0xff;
+
+        pen = SDL_PenModifyBegin(ptest.ids[5]);
+        SDL_PenModifyForWacomID(pen, wacom_type_id, &mask);
+        SDL_PenUpdateGUIDForWacom(&pen->guid, wacom_type_id, wacom_serial_id);
+        SDL_PenModifyAddCapabilities(pen, mask);
+        SDL_PenModifyEnd(pen, SDL_TRUE);
+        _expect_pen_config(ptest.ids[5], guid, SDL_TRUE,
+                           "Wacom Airbrush Pen", SDL_PEN_TYPE_AIRBRUSH, 1, 64.0f, /* Max tilt angle */
+                           SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK | SDL_PEN_AXIS_SLIDER_MASK);
+        _expect_pens_attached_or_detached(ptest.ids, 7, ATTACHED(0) | ATTACHED(1) | ATTACHED(2) | ATTACHED(4) | ATTACHED(5));
+    }
+    SDLTest_AssertPass("Pass #6: wacom airbrush pen");
+
+    /* Cleanup */
+    SDL_PenGCMark();
+    _pen_trackGCSweep(&ptest);
+    _teardown_test(&ptest, backup);
+    return TEST_COMPLETED;
+}
+
+#define SET_POS(update, xpos, ypos) \
+    (update).x = (xpos);            \
+    (update).y = (ypos);
+
+static void
+_penmouse_expect_button(int type, int button)
+{
+    SDL_bool press = type == SDL_PRESSED;
+    SDLTest_AssertCheck((press ? SDL_EVENT_MOUSE_BUTTON_DOWN : SDL_EVENT_MOUSE_BUTTON_UP) == _mouseemu_last_event,
+                        "Mouse button %s: %x",
+                        (press ? "press" : "release"), _mouseemu_last_event);
+    SDLTest_AssertCheck(button == _mouseemu_last_button,
+                        "Observed the expected simulated button: %d", _mouseemu_last_button);
+    SDLTest_AssertCheck(SDL_PEN_MOUSEID == _mouseemu_last_mouseid,
+                        "Observed the expected mouse ID: 0x%x", _mouseemu_last_mouseid);
+
+    _mouseemu_last_event = 0;
+}
+
+/**
+ * @brief Check pen device mouse emulation and event suppression without SDL_HINT_PEN_DELAY_MOUSE_BUTTON
+ *
+ * Since we include SDL_pen.c, we link it against our own mock implementations of SDL_PSendMouseButton
+ * and SDL_SendMouseMotion; see tehere for details.
+ */
+static int
+pen_mouseEmulation(void *arg)
+{
+    pen_testdata ptest;
+    SDL_Event event;
+    int i;
+    SDL_PenStatusInfo update;
+    deviceinfo_backup *backup;
+
+    pen_delay_mouse_button_mode = 0;
+    pen_mouse_emulation_mode = PEN_MOUSE_EMULATE; /* to trigger our own SDL_SendMouseButton */
+
+    /* Register pen */
+    backup = _setup_test(&ptest, 1);
+    SDL_PenGCMark();
+    _pen_setDeviceinfo(_pen_register(ptest.ids[0], ptest.guids[0], "testpen",
+                                     SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT | SDL_PEN_AXIS_YTILT),
+                       20);
+    _pen_trackGCSweep(&ptest);
+
+    /* Move pen into window */
+    SDL_SendPenWindowEvent(0, ptest.ids[0], ptest.window);
+
+    /* Initialise pen location */
+    SDL_memset(update.axes, 0, sizeof(update.axes));
+    SET_POS(update, 100.0f, 100.0f);
+    SDL_SendPenMotion(0, ptest.ids[0], SDL_TRUE, &update);
+    while (SDL_PollEvent(&event))
+        ; /* Flush event queue */
+
+    /* Test motion forwarding */
+    _mouseemu_last_event = 0;
+    SET_POS(update, 121.25f, 110.75f);
+    SDL_SendPenMotion(0, ptest.ids[0], SDL_TRUE, &update);
+    SDLTest_AssertCheck(SDL_EVENT_MOUSE_MOTION == _mouseemu_last_event,
+                        "Mouse motion event: %d", _mouseemu_last_event);
+    SDLTest_AssertCheck(121.25f == _mouseemu_last_x && 110.75f == _mouseemu_last_y,
+                        "Motion to correct position: %f,%f", _mouseemu_last_x, _mouseemu_last_y);
+    SDLTest_AssertCheck(SDL_PEN_MOUSEID == _mouseemu_last_mouseid,
+                        "Observed the expected mouse ID: 0x%x", _mouseemu_last_mouseid);
+    SDLTest_AssertCheck(0 == _mouseemu_last_relative,
+                        "Absolute motion event");
+    SDLTest_AssertPass("Motion emulation");
+
+    /* Test redundant motion report suppression */
+    _mouseemu_last_event = 0;
+
+    SET_POS(update, 121.25f, 110.75f);
+    SDL_SendPenMotion(0, ptest.ids[0], SDL_TRUE, &update);
+
+    SET_POS(update, 121.25f, 110.75f);
+    SDL_SendPenMotion(0, ptest.ids[0], SDL_TRUE, &update);
+
+    update.axes[0] = 1.0f;
+    SDL_SendPenMotion(0, ptest.ids[0], SDL_TRUE, &update);
+
+    SET_POS(update, 121.25f, 110.75f);
+    update.axes[0] = 0.0f;
+    update.axes[1] = 0.75f;
+    SDL_SendPenMotion(0, ptest.ids[0], SDL_TRUE, &update);
+
+    SDLTest_AssertCheck(0 == _mouseemu_last_event,
+                        "Redundant mouse motion suppressed: %d", _mouseemu_last_event);
+    SDLTest_AssertPass("Redundant motion suppression");
+
+    /* Test button press reporting */
+    SDL_SendPenTipEvent(0, ptest.ids[0], SDL_PRESSED);
+    _penmouse_expect_button(SDL_PRESSED, 1);
+
+    for (i = 1; i <= 3; ++i) {
+        SDL_SendPenButton(0, ptest.ids[0], SDL_PRESSED, i);
+        _penmouse_expect_button(SDL_PRESSED, i + 1);
+    }
+    SDLTest_AssertPass("Button press mouse emulation");
+
+    /* Test button release reporting */
+    SDL_SendPenTipEvent(0, ptest.ids[0], SDL_RELEASED);
+    _penmouse_expect_button(SDL_RELEASED, 1);
+
+    for (i = 1; i <= 3; ++i) {
+        SDL_SendPenButton(0, ptest.ids[0], SDL_RELEASED, i);
+        _penmouse_expect_button(SDL_RELEASED, i + 1);
+    }
+    SDLTest_AssertPass("Button release mouse emulation");
+
+    /* Cleanup */
+    SDL_PenGCMark();
+    _pen_trackGCSweep(&ptest);
+    _teardown_test(&ptest, backup);
+    return TEST_COMPLETED;
+}
+
+/**
+ * @brief Check pen device mouse emulation when SDL_HINT_PEN_DELAY_MOUSE_BUTTON is enabled (default)
+ */
+static int
+pen_mouseEmulationDelayed(void *arg)
+{
+    pen_testdata ptest;
+    SDL_Event event;
+    int i;
+    SDL_PenStatusInfo update;
+    deviceinfo_backup *backup;
+
+    pen_delay_mouse_button_mode = 1;
+    pen_mouse_emulation_mode = PEN_MOUSE_EMULATE; /* to trigger our own SDL_SendMouseButton */
+
+    /* Register pen */
+    backup = _setup_test(&ptest, 1);
+    SDL_PenGCMark();
+    _pen_setDeviceinfo(_pen_register(ptest.ids[0], ptest.guids[0], "testpen",
+                                     SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT | SDL_PEN_AXIS_YTILT),
+                       20);
+    _pen_trackGCSweep(&ptest);
+
+    /* Move pen into window */
+    SDL_SendPenWindowEvent(0, ptest.ids[0], ptest.window);
+
+    /* Initialise pen location */
+    SDL_memset(update.axes, 0, sizeof(update.axes));
+    SET_POS(update, 100.0f, 100.0f);
+    SDL_SendPenMotion(0, ptest.ids[0], SDL_TRUE, &update);
+    while (SDL_PollEvent(&event))
+        ; /* Flush event queue */
+
+    /* Test motion forwarding */
+    _mouseemu_last_event = 0;
+    SET_POS(update, 121.25f, 110.75f);
+    SDL_SendPenMotion(0, ptest.ids[0], SDL_TRUE, &update);
+    SDLTest_AssertCheck(SDL_EVENT_MOUSE_MOTION == _mouseemu_last_event,
+                        "Mouse motion event: %d", _mouseemu_last_event);
+    SDLTest_AssertCheck(121.25f == _mouseemu_last_x && 110.75f == _mouseemu_last_y,
+                        "Motion to correct position: %f,%f", _mouseemu_last_x, _mouseemu_last_y);
+    SDLTest_AssertCheck(SDL_PEN_MOUSEID == _mouseemu_last_mouseid,
+                        "Observed the expected mouse ID: 0x%x", _mouseemu_last_mouseid);
+    SDLTest_AssertCheck(0 == _mouseemu_last_relative,
+                        "Absolute motion event");
+    SDLTest_AssertPass("Motion emulation");
+    _mouseemu_last_event = 0;
+
+    /* Test button press reporting */
+    for (i = 1; i <= 2; ++i) {
+        SDL_SendPenButton(0, ptest.ids[0], SDL_PRESSED, i);
+        SDLTest_AssertCheck(0 == _mouseemu_last_event,
+                            "Non-touching button press suppressed: %d", _mouseemu_last_event);
+        SDL_SendPenButton(0, ptest.ids[0], SDL_RELEASED, i);
+        SDLTest_AssertCheck(0 == _mouseemu_last_event,
+                            "Non-touching button release suppressed: %d", _mouseemu_last_event);
+    }
+
+    /* Touch surface */
+    SDL_SendPenTipEvent(0, ptest.ids[0], SDL_PRESSED);
+    _penmouse_expect_button(SDL_PRESSED, 1);
+    SDL_SendPenTipEvent(0, ptest.ids[0], SDL_RELEASED);
+    _penmouse_expect_button(SDL_RELEASED, 1);
+
+    /* Test button press reporting, releasing extra button AFTER lifting pen */
+    for (i = 1; i <= 2; ++i) {
+        SDL_SendPenButton(0, ptest.ids[0], SDL_PRESSED, i);
+        SDLTest_AssertCheck(0 == _mouseemu_last_event,
+                            "Non-touching button press suppressed (A.1): %d", _mouseemu_last_event);
+	SDL_SendPenTipEvent(0, ptest.ids[0], SDL_PRESSED);
+        _penmouse_expect_button(SDL_PRESSED, i + 1);
+
+	SDL_SendPenTipEvent(0, ptest.ids[0], SDL_RELEASED);
+        _penmouse_expect_button(SDL_RELEASED, i + 1);
+
+        SDL_SendPenButton(0, ptest.ids[0], SDL_RELEASED, i);
+        SDLTest_AssertCheck(0 == _mouseemu_last_event,
+                            "Non-touching button press suppressed (A.2): %d", _mouseemu_last_event);
+    }
+    SDLTest_AssertPass("Delayed button press mouse emulation, touching without releasing button");
+
+    /* Test button press reporting, releasing extra button BEFORE lifting pen */
+    for (i = 1; i <= 2; ++i) {
+        SDL_SendPenButton(0, ptest.ids[0], SDL_PRESSED, i);
+        SDLTest_AssertCheck(0 == _mouseemu_last_event,
+                            "Non-touching button press suppressed (B.1): %d", _mouseemu_last_event);
+	SDL_SendPenTipEvent(0, ptest.ids[0], SDL_PRESSED);
+        _penmouse_expect_button(SDL_PRESSED, i + 1);
+
+        SDL_SendPenButton(0, ptest.ids[0], SDL_RELEASED, i);
+        SDLTest_AssertCheck(0 == _mouseemu_last_event,
+                            "Non-touching button press suppressed (B.2): %d", _mouseemu_last_event);
+	SDL_SendPenTipEvent(0, ptest.ids[0], SDL_RELEASED);
+        _penmouse_expect_button(SDL_RELEASED, i + 1);
+    }
+    SDLTest_AssertPass("Delayed button press mouse emulation, touching and then releasing button");
+
+    /* Cleanup */
+    SDL_PenGCMark();
+    _pen_trackGCSweep(&ptest);
+    _teardown_test(&ptest, backup);
+    return TEST_COMPLETED;
+}
+
+/**
+ * @brief Ensure that all SDL_Pen*Event structures have compatible memory layout, as expected by SDL_pen.c
+ */
+static int
+pen_memoryLayout(void *arg)
+{
+#define LAYOUT_COMPATIBLE(field)					\
+    SDLTest_AssertCheck(offsetof(SDL_PenTipEvent, field) == offsetof(SDL_PenMotionEvent, field), \
+			"Memory layout SDL_PenTipEvent and SDL_PenMotionEvent compatibility: '" #field "'"); \
+    SDLTest_AssertCheck(offsetof(SDL_PenTipEvent, field) == offsetof(SDL_PenButtonEvent, field), \
+			"Memory layout SDL_PenTipEvent and SDL_PenBUttonEvent compatibility: '" #field "'");
+
+    LAYOUT_COMPATIBLE(which);
+    LAYOUT_COMPATIBLE(x);
+    LAYOUT_COMPATIBLE(y);
+    LAYOUT_COMPATIBLE(axes);
+
+    return TEST_COMPLETED;
+}
+
+/* ================= Test References ================== */
+
+/* Mouse test cases */
+static const SDLTest_TestCaseReference penTest1 = { (SDLTest_TestCaseFp)pen_iteration, "pen_iteration", "Iterate over all pens with SDL_PenIDForIndex", TEST_ENABLED };
+
+static const SDLTest_TestCaseReference penTest2 = { (SDLTest_TestCaseFp)pen_hotplugging, "pen_hotplugging", "Hotplug pens and validate their status, including SDL_PenConnected", TEST_ENABLED };
+
+static const SDLTest_TestCaseReference penTest3 = { (SDLTest_TestCaseFp)pen_GUIDs, "pen_GUIDs", "Check Pen SDL_GUID operations", TEST_ENABLED };
+
+static const SDLTest_TestCaseReference penTest4 = { (SDLTest_TestCaseFp)pen_buttonReporting, "pen_buttonReporting", "Check pen button presses", TEST_ENABLED };
+
+static const SDLTest_TestCaseReference penTest5 = { (SDLTest_TestCaseFp)pen_movementAndAxes, "pen_movementAndAxes", "Check pen movement and axis update reporting", TEST_ENABLED };
+
+static const SDLTest_TestCaseReference penTest6 = { (SDLTest_TestCaseFp)pen_initAndInfo, "pen_info", "Check pen self-description and initialisation", TEST_ENABLED };
+
+static const SDLTest_TestCaseReference penTest7 = { (SDLTest_TestCaseFp)pen_mouseEmulation, "pen_mouseEmulation", "Check pen-as-mouse event forwarding (direct)", TEST_ENABLED };
+
+static const SDLTest_TestCaseReference penTest8 = { (SDLTest_TestCaseFp)pen_mouseEmulationDelayed, "pen_mouseEmulationDelayed", "Check pen-as-mouse event forwarding (delayed)", TEST_ENABLED };
+
+static const SDLTest_TestCaseReference penTest9 = { (SDLTest_TestCaseFp)pen_memoryLayout, "pen_memoryLayout", "Check that all pen events have compatible layout (required by SDL_pen.c)", TEST_ENABLED };
+
+/* Sequence of Mouse test cases */
+static const SDLTest_TestCaseReference *penTests[] = {
+    &penTest1, &penTest2, &penTest3, &penTest4, &penTest5, &penTest6, &penTest7, &penTest8, &penTest9, NULL
+};
+
+/* Mouse test suite (global) */
+SDLTest_TestSuiteReference penTestSuite = {
+    "Pen",
+    NULL,
+    penTests,
+    NULL
+};

+ 1 - 0
test/testautomation_suites.h

@@ -20,6 +20,7 @@ extern SDLTest_TestSuiteReference keyboardTestSuite;
 extern SDLTest_TestSuiteReference mainTestSuite;
 extern SDLTest_TestSuiteReference mathTestSuite;
 extern SDLTest_TestSuiteReference mouseTestSuite;
+extern SDLTest_TestSuiteReference penTestSuite;
 extern SDLTest_TestSuiteReference pixelsTestSuite;
 extern SDLTest_TestSuiteReference platformTestSuite;
 extern SDLTest_TestSuiteReference propertiesTestSuite;

+ 496 - 0
test/testpen.c

@@ -0,0 +1,496 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2023 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+
+#include <math.h>
+#include <SDL3/SDL.h>
+#include <SDL3/SDL_main.h>
+#include <SDL3/SDL_test.h>
+#include <SDL3/SDL_test_common.h>
+
+#define WIDTH  1600
+#define HEIGHT 1200
+
+#define VERBOSE 0
+
+#define ALWAYS_SHOW_PRESSURE_BOX 1
+
+static SDLTest_CommonState *state;
+static int quitting = 0;
+
+static float last_x, last_y;
+static float last_xtilt, last_ytilt, last_pressure, last_distance, last_rotation;
+static int last_button;
+static int last_touching; /* tip touches surface */
+static int last_was_eraser;
+
+static SDL_Texture *offscreen_texture = NULL;
+
+static void DrawScreen(SDL_Renderer *renderer)
+{
+    float xdelta, ydelta, endx, endy;
+    /* off-screen texture to render into */
+    SDL_Texture *window_texture;
+    const float X = 128.0f, Y = 128.0f; /* mid-point in the off-screen texture */
+    SDL_FRect dest_rect;
+    float tilt_vec_x = SDL_sinf(last_xtilt * SDL_PI_F / 180.0f);
+    float tilt_vec_y = SDL_sinf(last_ytilt * SDL_PI_F / 180.0f);
+    int color = last_button + 1;
+
+    if (!renderer) {
+        return;
+    }
+
+    SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
+    SDL_SetRenderDrawColor(renderer, 0x40, 0x40, 0x40, 0xff);
+    SDL_RenderClear(renderer);
+
+    if (offscreen_texture == NULL) {
+        offscreen_texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, (int)(X * 2.0f), (int)(Y * 2.0f));
+    }
+
+    /* Render into off-screen texture so we can do pixel-precise rendering later */
+    window_texture = SDL_GetRenderTarget(renderer);
+    SDL_SetRenderTarget(renderer, offscreen_texture);
+
+    /* Rendering starts here */
+    SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
+    SDL_SetRenderDrawColor(renderer, 0x40, 0x40, 0x40, 0xff);
+    SDL_RenderClear(renderer);
+
+    SDL_SetRenderDrawColor(renderer, 0xa0, 0xa0, 0xa0, 0xff);
+    if (last_touching) {
+	SDL_FRect rect;
+
+        rect.x = 0;
+        rect.y = 0;
+        rect.w = 2.0f * X - 1.0f;
+        rect.h = 2.0f * Y - 1.0f;
+
+	SDL_RenderRect(renderer, &rect);
+    } else {
+	/* Show where the pen is rotating when it isn't touching the surface.
+	   Otherwise we draw the rotation angle below together with pressure information. */
+	float rot_vecx =  SDL_sinf(last_rotation / 180.0f * SDL_PI_F);
+	float rot_vecy = -SDL_cosf(last_rotation / 180.0f * SDL_PI_F);
+	float px = X + rot_vecx * 100.0f;
+	float py = Y + rot_vecy * 100.0f;
+	float px2 = X + rot_vecx * 80.0f;
+	float py2 = Y + rot_vecy * 80.0f;
+
+	SDL_RenderLine(renderer,
+		       px, py,
+		       px2 + rot_vecy * 20.0f,
+		       py2 - rot_vecx * 20.0f);
+	SDL_RenderLine(renderer,
+		       px, py,
+		       px2 - rot_vecy * 20.0f,
+		       py2 + rot_vecx * 20.0f);
+    }
+
+    if (last_was_eraser) {
+        SDL_FRect rect;
+
+        rect.x = X - 10.0f;
+        rect.y = Y - 10.0f;
+        rect.w = 21.0f;
+        rect.h = 21.0f;
+
+        SDL_SetRenderDrawColor(renderer, 0x00, 0xff, 0xff, 0xff);
+        SDL_RenderFillRect(renderer, &rect);
+    } else {
+        float distance = last_distance * 50.0f;
+
+        SDL_SetRenderDrawColor(renderer, 0xff, 0, 0, 0xff);
+        SDL_RenderLine(renderer,
+                       X - 10.0f - distance, Y,
+                       X - distance, Y);
+        SDL_RenderLine(renderer,
+                       X + 10.0f + distance, Y,
+                       X + distance, Y);
+        SDL_RenderLine(renderer,
+                       X, Y - 10.0f - distance,
+                       X, Y - distance);
+        SDL_RenderLine(renderer,
+                       X, Y + 10.0f + distance,
+                       X, Y + distance);
+
+    }
+
+    /* Draw a cone based on the direction the pen is leaning as if it were shining a light. */
+    /* Colour derived from pens, intensity based on pressure: */
+    SDL_SetRenderDrawColor(renderer,
+                           (color & 0x01) ? 0xff : 0,
+                           (color & 0x02) ? 0xff : 0,
+                           (color & 0x04) ? 0xff : 0,
+                           (int)(0xff));
+
+    xdelta = -tilt_vec_x * 100.0f;
+    ydelta = -tilt_vec_y * 100.0f;
+    endx = X + xdelta;
+    endy = Y + ydelta;
+    SDL_RenderLine(renderer, X, Y, endx, endy);
+
+    SDL_SetRenderDrawColor(renderer,
+                           (color & 0x01) ? 0xff : 0,
+                           (color & 0x02) ? 0xff : 0,
+                           (color & 0x04) ? 0xff : 0,
+                           (int)(0xff * last_pressure));
+    /* Cone base width based on pressure: */
+    SDL_RenderLine(renderer, X, Y, endx + (ydelta * last_pressure / 3.0f), endy - (xdelta * last_pressure / 3.0f));
+    SDL_RenderLine(renderer, X, Y, endx - (ydelta * last_pressure / 3.0f), endy + (xdelta * last_pressure / 3.0f));
+
+    /* If tilt is very small (or zero, for pens that don't have tilt), add some extra lines, rotated by the current rotation value */
+    if (ALWAYS_SHOW_PRESSURE_BOX || (fabs(tilt_vec_x) < 0.2f && fabs(tilt_vec_y) < 0.2f)) {
+        int rot;
+        float pressure = last_pressure * 80.0f;
+
+        /* Four times, rotated 90 degrees, so that we get a box */
+        for (rot = 0; rot < 4; ++rot) {
+
+            float vecx = SDL_cosf((last_rotation + (rot * 90.0f)) / 180.0f * SDL_PI_F);
+            float vecy = SDL_sinf((last_rotation + (rot * 90.0f)) / 180.0f * SDL_PI_F);
+
+            float px = X + vecx * pressure;
+            float py = Y + vecy * pressure;
+
+            SDL_RenderLine(renderer,
+                                px + vecy * 10.0f, py - vecx * 10.0f,
+                                px - vecy * 10.0f, py + vecx * 10.0f);
+
+            if (rot == 3) {
+                int r = 0;
+                for (; r >= 0; r -= 2) {
+                    float delta = 10.0f - ((float) r);
+
+                    SDL_RenderLine(renderer,
+                                   px + vecy * delta, py - vecx * delta,
+                                   px + (vecx * pressure * 0.4f),
+                                   py + (vecy * pressure * 0.4f));
+                    SDL_RenderLine(renderer,
+                                   px - vecy * delta, py + vecx * delta,
+                                   px + (vecx * pressure * 0.4f),
+                                   py + (vecy * pressure * 0.4f));
+                }
+            }
+        }
+    }
+
+    SDL_SetRenderTarget(renderer, window_texture);
+    /* Now render to pixel-precise position */
+    dest_rect.x = last_x - X;
+    dest_rect.y = last_y - Y;
+    dest_rect.w = X * 2.0f;
+    dest_rect.h = Y * 2.0f;
+    SDL_RenderTexture(renderer, offscreen_texture, NULL, &dest_rect);
+    SDL_RenderPresent(renderer);
+}
+
+static void dump_state(void)
+{
+    int i;
+    int pens_nr;
+
+    /* Make sure this also works with a NULL parameter */
+    SDL_PenID* pens = SDL_GetPens(NULL);
+    if (pens) {
+        SDL_free(pens);
+    }
+
+    pens = SDL_GetPens(&pens_nr);
+    if (!pens) {
+        SDL_Log("Couldn't get pens: %s\n", SDL_GetError());
+        return;
+    }
+    SDL_Log("Found %d pens (terminated by %u)\n", pens_nr, (unsigned) pens[pens_nr]);
+
+    for (i = 0; i < pens_nr; ++i) {
+        SDL_PenID penid = pens[i];
+        SDL_GUID guid = SDL_GetPenGUID(penid);
+        char guid_str[33];
+        float axes[SDL_PEN_NUM_AXES];
+        float x, y;
+        int k;
+        SDL_PenCapabilityInfo info;
+        Uint32 status = SDL_GetPenStatus(penid, &x, &y, axes, SDL_PEN_NUM_AXES);
+        Uint32 capabilities = SDL_GetPenCapabilities(penid, &info);
+        char *type;
+        char *buttons_str;
+
+        SDL_GUIDToString(guid, guid_str, 33);
+
+        switch (SDL_GetPenType(penid)) {
+        case SDL_PEN_TYPE_ERASER:
+            type = "Eraser";
+            break;
+        case SDL_PEN_TYPE_PEN:
+            type = "Pen";
+            break;
+        case SDL_PEN_TYPE_PENCIL:
+            type = "Pencil";
+            break;
+        case SDL_PEN_TYPE_BRUSH:
+            type = "Brush";
+            break;
+        case SDL_PEN_TYPE_AIRBRUSH:
+            type = "Airbrush";
+            break;
+        default:
+            type = "Unknown (bug?)";
+        }
+
+        switch (info.num_buttons) {
+        case SDL_PEN_INFO_UNKNOWN:
+            SDL_asprintf(&buttons_str, "? buttons");
+            break;
+        case 1:
+            SDL_asprintf(&buttons_str, "1 button");
+            break;
+        default:
+            SDL_asprintf(&buttons_str, "%d button", info.num_buttons);
+            break;
+        }
+
+        SDL_Log("%s %lu: [%s] attached=%d, %s [cap= %08lx:%08lx =status] '%s'\n",
+                type,
+                (unsigned long) penid, guid_str,
+                SDL_PenConnected(penid), /* should always be SDL_TRUE during iteration */
+                buttons_str,
+                (unsigned long) capabilities,
+                (unsigned long) status,
+                SDL_GetPenName(penid));
+        SDL_free(buttons_str);
+        SDL_Log("   pos=(%.2f, %.2f)", x, y);
+        for (k = 0; k < SDL_PEN_NUM_AXES; ++k) {
+            SDL_bool supported = capabilities & SDL_PEN_AXIS_CAPABILITY(k);
+            if (supported) {
+                if (k == SDL_PEN_AXIS_XTILT || k == SDL_PEN_AXIS_YTILT) {
+                    if (info.max_tilt == SDL_PEN_INFO_UNKNOWN) {
+                        SDL_Log("   axis %d:  %.3f (max tilt unknown)", k, axes[k]);
+                    } else {
+                        SDL_Log("   axis %d:  %.3f (tilt -%.1f..%.1f)", k, axes[k],
+                                info.max_tilt, info.max_tilt);
+                    }
+                } else {
+                    SDL_Log("   axis %d:  %.3f", k, axes[k]);
+                }
+            } else {
+                SDL_Log("   axis %d:  unsupported (%.3f)", k, axes[k]);
+            }
+        }
+    }
+    SDL_free(pens);
+}
+
+static void update_axes(float *axes)
+{
+    last_xtilt = axes[SDL_PEN_AXIS_XTILT];
+    last_ytilt = axes[SDL_PEN_AXIS_YTILT];
+    last_pressure = axes[SDL_PEN_AXIS_PRESSURE];
+    last_distance = axes[SDL_PEN_AXIS_DISTANCE];
+    last_rotation = axes[SDL_PEN_AXIS_ROTATION];
+}
+
+static void process_event(SDL_Event event)
+{
+    SDLTest_CommonEvent(state, &event, &quitting);
+
+    switch (event.type) {
+    case SDL_EVENT_KEY_DOWN:
+    {
+        dump_state();
+        break;
+    }
+    case SDL_EVENT_MOUSE_MOTION:
+    case SDL_EVENT_MOUSE_BUTTON_DOWN:
+    case SDL_EVENT_MOUSE_BUTTON_UP:
+#if VERBOSE
+    {
+        float x, y;
+        SDL_GetMouseState(&x, &y);
+        if (event.type == SDL_EVENT_MOUSE_MOTION) {
+            SDL_Log("[%lu] mouse motion: mouse ID %d is at (%.2f, %.2f)  (state: %.2f,%.2f) delta (%.2f, %.2f)\n",
+                    event.motion.timestamp,
+                    event.motion.which,
+                    event.motion.x, event.motion.y,
+                    event.motion.xrel, event.motion.yrel,
+                    x, y);
+        } else {
+            SDL_Log("[%lu] mouse button: mouse ID %d is at (%.2f, %.2f)  (state: %.2f,%.2f)\n",
+                    event.button.timestamp,
+                    event.button.which,
+                    event.button.x, event.button.y,
+                    x, y);
+        }
+    }
+#endif
+    if (event.motion.which != SDL_PEN_MOUSEID) {
+        SDL_ShowCursor();
+    } break;
+
+    case SDL_EVENT_PEN_MOTION:
+    {
+        SDL_PenMotionEvent *ev = &event.pmotion;
+
+        SDL_HideCursor();
+        last_x = ev->x;
+        last_y = ev->y;
+	update_axes(ev->axes);
+        last_was_eraser = ev->pen_state & SDL_PEN_ERASER_MASK;
+#if VERBOSE
+        SDL_Log("[%lu] pen motion: %s %u at (%.4f, %.4f); pressure=%.3f, tilt=%.3f/%.3f, dist=%.3f [buttons=%02x]\n",
+                (unsigned long) ev->timestamp,
+                last_was_eraser ? "eraser" : "pen",
+                (unsigned int)ev->which, ev->x, ev->y, last_pressure, last_xtilt, last_ytilt, last_distance,
+                ev->pen_state);
+#endif
+    } break;
+
+    case SDL_EVENT_PEN_UP:
+    case SDL_EVENT_PEN_DOWN: {
+        SDL_PenTipEvent *ev = &event.ptip;
+        last_x = ev->x;
+        last_y = ev->y;
+	update_axes(ev->axes);
+        last_was_eraser = ev->tip == SDL_PEN_TIP_ERASER;
+        last_button = ev->pen_state & 0xf; /* button mask */
+        last_touching = (event.type == SDL_EVENT_PEN_DOWN);
+    } break;
+
+    case SDL_EVENT_PEN_BUTTON_UP:
+    case SDL_EVENT_PEN_BUTTON_DOWN:
+    {
+        SDL_PenButtonEvent *ev = &event.pbutton;
+
+        SDL_HideCursor();
+        last_x = ev->x;
+        last_y = ev->y;
+	update_axes(ev->axes);
+        if (last_pressure > 0.0f && !last_touching) {
+            SDL_LogWarn(SDL_LOG_CATEGORY_TEST,
+                        "[%lu] : reported pressure %.5f even though pen is not touching surface",
+                        (unsigned long) ev->timestamp, last_pressure);
+
+        }
+        last_was_eraser = ev->pen_state & SDL_PEN_ERASER_MASK;
+        last_button = ev->pen_state & 0xf; /* button mask */
+        if ((ev->pen_state & SDL_PEN_DOWN_MASK) &&  !last_touching) {
+            SDL_LogWarn(SDL_LOG_CATEGORY_TEST,
+                        "[%lu] : reported flags %x (SDL_PEN_FLAG_DOWN_MASK) despite not receiving SDL_EVENT_PEN_DOWN",
+                        (unsigned long) ev->timestamp, ev->pen_state);
+
+        }
+        if (!(ev->pen_state & SDL_PEN_DOWN_MASK) &&  last_touching) {
+            SDL_LogWarn(SDL_LOG_CATEGORY_TEST,
+                        "[%lu] : reported flags %x (no SDL_PEN_FLAG_DOWN_MASK) despite receiving SDL_EVENT_PEN_DOWN without SDL_EVENT_PEN_UP afterwards",
+                        (unsigned long) ev->timestamp, ev->pen_state);
+
+        }
+#if VERBOSE
+        SDL_Log("[%lu] pen button: %s %u at (%.4f, %.4f); BUTTON %d reported %s with event %s [pressure=%.3f, tilt=%.3f/%.3f, dist=%.3f]\n",
+                (unsigned long) ev->timestamp,
+                last_was_eraser ? "eraser" : "pen",
+                (unsigned int)ev->which, ev->x, ev->y,
+                ev->button,
+                (ev->state == SDL_PRESSED) ? "PRESSED"
+                                           : ((ev->state == SDL_RELEASED) ? "RELEASED" : "--invalid--"),
+                event.type == SDL_EVENT_PEN_BUTTON_UP ? "PENBUTTONUP" : "PENBUTTONDOWN",
+                last_pressure, last_xtilt, last_ytilt, last_distance);
+#endif
+    } break;
+
+    case SDL_EVENT_WINDOW_PEN_ENTER:
+        SDL_Log("[%lu] Pen %lu entered window %lx",
+                (unsigned long) event.window.timestamp,
+                (unsigned long) event.window.data1,
+                (unsigned long) event.window.windowID);
+        break;
+
+    case SDL_EVENT_WINDOW_PEN_LEAVE:
+        SDL_Log("[%lu] Pen %lu left window %lx",
+                (unsigned long) event.window.timestamp,
+                (unsigned long) event.window.data1,
+                (unsigned long) event.window.windowID);
+        break;
+
+#if VERBOSE
+    case SDL_EVENT_WINDOW_MOUSE_ENTER:
+        SDL_Log("[%lu] Mouse entered window %lx",
+                (unsigned long) event.window.timestamp,
+                (unsigned long) event.window.windowID);
+        break;
+
+    case SDL_EVENT_WINDOW_MOUSE_LEAVE:
+        SDL_Log("[%lu] Mouse left window %lx",
+                (unsigned long) event.window.timestamp,
+                (unsigned long) event.window.windowID);
+        break;
+#endif
+
+    default:
+        break;
+    }
+}
+
+static void loop(void)
+{
+    SDL_Event event;
+    int i;
+
+    for (i = 0; i < state->num_windows; ++i) {
+        if (state->renderers[i]) {
+            DrawScreen(state->renderers[i]);
+        }
+    }
+
+    if (SDL_WaitEventTimeout(&event, 10)) {
+        process_event(event);
+    }
+    while (SDL_PollEvent(&event)) {
+        process_event(event);
+    }
+}
+
+int main(int argc, char *argv[])
+{
+    state = SDLTest_CommonCreateState(argv, SDL_INIT_VIDEO);
+    if (!state) {
+        return 1;
+    }
+
+    SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "2");
+
+    state->window_title = "Pressure-Sensitive Pen Test";
+    state->window_w = WIDTH;
+    state->window_h = HEIGHT;
+    state->skip_renderer = SDL_FALSE;
+
+    if (!SDLTest_CommonDefaultArgs(state, argc, argv) || !SDLTest_CommonInit(state)) {
+        SDLTest_CommonQuit(state);
+        return 1;
+    }
+
+    while (!quitting) {
+        loop();
+    }
+
+    SDLTest_CommonQuit(state);
+    return 0;
+}