Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API for pressure-sensitive pens + XInput2/Wayland #8058

Merged
merged 1 commit into from
Nov 12, 2023

Conversation

creichen
Copy link
Contributor

@creichen creichen commented Jul 30, 2023

API for pressure-sensitive pens + XInput2 & Wayland implementations

This pull request adds support for pressure-sensitive pens to SDL. It is intended to maintain backward compatibility, and adds new events to provide more detailed information for pen movement.
It replaces #5481 (see heading "Changes since #5481" for a more detailed changelog).

TODO [NEW]

  • Porting API for pressure-sensitive pens + XInput2 & Wayland implementations #5481 to SDL3
  • Add separate events for PEN_UP and PEN_DOWN
  • Add logging support for the five new events (+ tests, if possible)
  • Update docs for SDL_GetPenType() to highlight that it is not reliable for classifying button events
  • Wayland: hit tests for window decoration elements if running in mouse emulation mode (SDL_HINT_PEN_NOT_MOUSE == 0)
  • X11: hit tests for window decoration elements if running in mouse emulation mode
  • Adjust pen API to report tilt and barrel rotation as degrees, following standard API convensions

Description

This pull request adds support for pressure-sensitive pens to SDL:

  • Extensions to the event API:
    • [EDIT] three new event types (PenTip, PenMotion, PenButton)
    • float- typed x, y positions
    • reports on pressure, tilt, and other pen axes
    • discriminate "pen" and "eraser" events, different pen IDs (if tracked by device-- probably Wacom-only for now)
  • API for querying pens SDL_pen.h)
  • Implementation for XInput2, incl. hotplugging support
  • Implementation for Wayland, incl. hotplugging support
  • Automatic test suite (testautomation_pen.c)
  • Demo program (testpen)
  • Hints to guide pen-as-mouse emulation:
    • SDL_HINT_PEN_NOT_MOUSE allows either entirely suppressing mouse events for pen events, or sending pen movement/button updates without moving the mouse position.
    • SDL_HINT_PEN_DELAY_MOUSE_BUTTON (default) reports mouse button presses when the pen touches/leaves the tablet, with the mouse button determined by which pen button is pressed when the pen first touches the surface. If disabled, pen buttons translate to mouse buttons even if the pen is just hovering, and the pen tip acts as left mouse button.
  • Support for multi-tablet setups is only experimental for this PR. In particular, pen GUIDs may change in the future if needed to improve cross-tablet tracking.

Design Rationale

Abstractly speaking, pens are similar to mice, except that they provide additional information (pressure, tilt). In practice, they are particularly useful for writing / drawing.

To mirror SDL conventions elsewhere, the API reports normalised data:

  • pressure: 0.0f .. 1.0f
  • [EDIT] tilt: -90.0f..90.0f, reporting horizontal and vertical tilt angles
  • [EDIT] rotation: -180.0f..179.0f, with 0.0f as centre, representing barrel rotation
  • distance: 0.0f .. 1.0f
  • slider: -1.0f .. 1.0f (0.0f as centre) or 0.0f .. 1.0f (untested)
  • PenMotion with pressure greater than 0 has button 1 pressed, which simulates current MouseMotion behaviour
  • Mouse events report which as SDL_PEN_MOUSEID (analogously to SDL_TOUCH_MOUSEID)

(See #5481 for more discussion of the design rationale)

Backward Compatibility

Pens still report mouse events via which = SDL_PEN_MOUSEID.
They should thus work the same as before to clients who are unaware of pen events.
Users can tweak this behaviour via the hint SDL_HINT_PEN_NOT_MOUSE.

No plans to fix in this PR:

  • XInput2: Eraser detection currently uses Wacom-specific information and otherwise checks for the device name, to see if it contains the string "eraser".
    • Technique adapted (and extended, for Wacom) from GDK
  • Pens and erasers aren't currently connected in the API (treated as separate devices or as one device that changes its type).
  • Pen pressure threshold for button 1 is 0, would be nice to default to higher value / configure via SDL hint
  • Support for additional platforms: I lack access to additional platforms and time/resources/strong interest to do this myself, but below are some resources and initial work by others:
  • Multi-tablet setups may work (as suggested by current testing) but isn't official yet.
  • No support for reporting on tablet devices (including which screen areas are mapped to which tablet), physical on-tablet buttons or paired devices ("Wacom ExpressKey").
  • [NEW] Wayland: replace linked lists by intrusive lists

Missing testing

Below are scenarios that are difficult for me to test:

  • Touch input unchanged?
  • Non-X11/Wayland systems unchanged?
  • X11 systems without XInput2 support unchanged?
  • Pens with wheels, three buttons (e.g., the latest Wacom pens)?

Existing Issue(s)

Changelog

  • 2023-08-19
    • Added PenTipEvent and SDL_EVENT_PEN_DOWN/SDL_EVENT_PEN_UP
    • The pen tip no longer counts as a button; the internal API has a new callback for this
    • The internal API now correctly handles state updates for pens that can switch between ink and eraser mode (updated in the documentation). The test suite also covers this case now.
    • Documentation updates
    • The three event types must now have compatible layout, due to a refactoring that removes some redundancy in event handling. The test suite checks for layout compatibility. Right now, the layout is identical, since all three structs use the same primitively typed fields in the same order.
    • Compiling on X11 without XInput2 should no longer produce warnings.
  • 2023-08-20
    • Should now correctly identify the Wacom Pro Pen 3 (2022) even if the backend cannot do so itself (untested)
    • Hit testing for XInput2 and Wayland (resize / move window), unless SDL_HINT_PEN_NOT_MOUSE is greater than 0
  • [NEW] 2023-08-26
    • Tilt and barrel rotation now report their results in degrees of tilt / degrees of rotation. The value 0 still stands for "pointing upwards".

Change relative to #5481

Since this PR is now scheduled for SDL3.2, I have updated it to fit the SDL3 coding style.
The changes compared to the SDL2 version of this PR (#5481) are:

  • Public API changes:
    • SDL_NumPens() and SDL_PenIDForIndex() have been replaced by
      SDL_GetPens(), mirroring the joystick API changes
    • SDL_PenStatus() -> SDL_GetPenStatus()
    • SDL_PenIDForGUID() -> SDL_GetPenFromGUID()
    • SDL_PenGUIDForPenID() -> SDL_GetPenGUID()
    • SDL_PenAttached() -> SDL_PenConnected()
    • SDL_PenCapabilities() -> SDL_GetPenCapabilities()
    • SDL_PenType() -> SDL_GetPenType()
    • SDL_PenName() -> SDL_GetPenName(); this function now guarantees that the caller can keep the string that it returns
    • All pen events now include a 64-bit nanosecond timestamp
    • Pen event names follow the new naming conventions:
      • SDL_EVENT_WINDOW_PEN_ENTER
      • SDL_EVENT_WINDOW_PEN_LEAVE
      • SDL_EVENT_PEN_MOTION
      • SDL_EVENT_PEN_BUTTON_DOWN
      • SDL_EVENT_PEN_BUTTON_UP
    • SDL_PENID_INVALID is now SDL_PEN_INVALID
  • Internal API changes of relevance to backend developers:
    • SDL_GetPen() -> SDL_GetPenPtr() (to avoid confusion with SDL_GetPens())
    • The following event reporting functions now take an additional Uint64 parameter to signal the event timestamp (or 0 if unknown, in which case the event handler fills in the time at which it was first notified of the event):
      • SDL_SendPenMotion()
      • SDL_SendPenButton()
      • SDL_SendPenWindowEvent()
    • SDL_PenModifyBegin() / SDL_PenModifyEnd() now acquire and release a mutex, respectively, which should allow backend developers to not have to worry about whether someone is accessing pen information in a parallel thread
  • clang-format everywhere, except where it reduced readability (the compressed Wacom table in SDL_pen.c) or needlessly re-shuffled header files in existing code (e.g., SDL_x11xinput2.c).
  • Uses a mutex to allow polling and pen API calls in separate threads (at least in principle; this is notoriously hard to verify.)

Acknowledgements and Thanks

  • Wacom generously donated an Art Pen to help with testing
  • @Pinglinux provided substantial and detailed help on how Wacom devices work on Linux and in XInput2, and some the information from their libwacom is now (in compressed form) part of the pull request.
  • @whot for technical advice on Wayland and X11 integration and libinput testing
  • @sp1ritCS testing and detailed feedback, esp. wrt Wayland
  • @sanette for testing Huion hardware and reporting bugs
  • @sezero for uncannily quick turnaround on portability and SDLishness fixes
  • @remjey for bug fixes for older and non-Wacom tablets, including XP-PEN

@creichen
Copy link
Contributor Author

I'd like to ask for help with the remaining build failure from someone with Visual C expertise:

  • Visual C builds don't work for this PR right now
  • The sub-build that is breaking is testautomation.vcproj
  • The underlying reason is that testautomation_pen.c includes SDL_mouse_c.h, which uses #include "SDL_internal.h", and SDL_internal.h is in /src/SDL_internal.h.
  • I've tried to make that header accessible to this build by adding a <ClInclude Include="..\..\..\src\SDL_internal.h" /> (also tried with only "..\..\src\SDL_internal.h") near the end of testautomation.vcproj, but that doesn't seem to help

Some of the possible (but sub-optimal) workarounds:

  • Creating a header file test/SDL_internal.h that forwards to the one in src, but that seems much clunkier than updating one existing build file
  • Disabling testautomation_pen.c on MSVC (via #ifdef), but that would create an obstacle towards implementing pen support on Windows
  • Modifying SDL_mouse_c.h and SDL_pen_c.h to not #include SDL_internal.h when used for testing, but that would make them more fragile

@slouken
Copy link
Collaborator

slouken commented Jul 30, 2023

Why does testautomation_pen.c need access to the internal SDL structures? We try to make sure that tests only cover the API surface.

@creichen
Copy link
Contributor Author

Why does testautomation_pen.c need access to the internal SDL structures?

testautomation_pen.c mocks the mouse API, and it calls the internal backend API for pen event reporting. Mocking the mouse API allows the tests to check that the pen API's mouse emulation works as intended, without having to run any code from SDL_mouse.c, and avoids initialising the video backend. Meanwhile, exposing the backend operations makes it possible to test the path from backend to frontend.
The practical advantages are:

  • The tests don't depend on the details of the SDL_mouse.c implementation (i.e., changes in SDL_mouse.c can't break these tests, only changes in SDL_mouse_c.h can)
  • The tests can run headless (no need to create a window for the MousePositionInWindow check)
  • Thanks to backend access, the tests can check that the surface API responds as intended when the backend API reports certain events

We try to make sure that tests only cover the API surface.

That does simplify the build setup, but it also puts a limit on the amount of test coverage that you can hope for. Given how useful these tests have been for me (especially when running with valgrind), I'd prefer to keep them around in order to help maintain the code.

@sezero
Copy link
Contributor

sezero commented Jul 30, 2023

Assuming it's decided that testautomation_pen.c will include SDL internal code, maybe something like the following? (Not even compile-tested.. Yes, it's hackity ugly..)

diff --git a/test/testautomation_pen.c b/test/testautomation_pen.c
index 8b3bc78..376125b 100644
--- a/test/testautomation_pen.c
+++ b/test/testautomation_pen.c
@@ -25,3 +25,3 @@
 
-#define SDL_internal_h_ /* Inhibit dynamic symbol redefinitions that clash with ours */
+#define testautomation_pen_c /* Inhibit dynamic symbol redefinitions that clash with ours */
 
diff --git a/src/events/SDL_mouse_c.h b/src/events/SDL_mouse_c.h
index cd08f16..c973aca 100644
--- a/src/events/SDL_mouse_c.h
+++ b/src/events/SDL_mouse_c.h
@@ -20,3 +20,5 @@
 */
+#ifndef testautomation_pen_c
 #include "SDL_internal.h"
+#endif
 
diff --git a/src/events/SDL_pen.c b/src/events/SDL_pen.c
index a87005e..4e29149 100644
--- a/src/events/SDL_pen.c
+++ b/src/events/SDL_pen.c
@@ -20,3 +20,5 @@
 */
+#ifndef testautomation_pen_c
 #include "SDL_internal.h"
+#endif
 
diff --git a/src/events/SDL_pen_c.h b/src/events/SDL_pen_c.h
index c4791c7..3c44029 100644
--- a/src/events/SDL_pen_c.h
+++ b/src/events/SDL_pen_c.h
@@ -20,4 +20,5 @@
 */
-
+#ifndef testautomation_pen_c
 #include "../SDL_internal.h"
+#endif
 

@creichen
Copy link
Contributor Author

@sezero , thank you-- I'd prefer to avoid this, but if there is no good way to fix it in the MSVC build files, this approach would be the least messy of the ones that I am currently aware of.

I can see if I can borrow a Windows machine next weekend; there seems to be a free trial version of MSVC.

@sezero
Copy link
Contributor

sezero commented Jul 31, 2023

The following works: (similar to what you last did, but additional include paths applied only to testautomation_pen.c and backslashes instead of forward ones): diff.txt

diff --git a/VisualC/tests/testautomation/testautomation.vcxproj b/VisualC/tests/testautomation/testautomation.vcxproj
index 2691b7d..057ae2e 100644
--- a/VisualC/tests/testautomation/testautomation.vcxproj
+++ b/VisualC/tests/testautomation/testautomation.vcxproj
@@ -83,18 +83,6 @@
     <CodeAnalysisRules Condition="'$(Configuration)|$(Platform)'=='Release|x64'" />
     <CodeAnalysisRuleAssemblies Condition="'$(Configuration)|$(Platform)'=='Release|x64'" />
   </PropertyGroup>
-  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
-    <IncludePath>$(ProjectDir)/../../src;$(IncludePath)</IncludePath>
-  </PropertyGroup>
-  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
-    <IncludePath>$(ProjectDir)/../../src;$(IncludePath)</IncludePath>
-  </PropertyGroup>
-  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
-    <IncludePath>$(ProjectDir)/../../src;$(IncludePath)</IncludePath>
-  </PropertyGroup>
-  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
-    <IncludePath>$(ProjectDir)/../../src;$(IncludePath)</IncludePath>
-  </PropertyGroup>
   <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
     <Midl>
       <PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
@@ -221,7 +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" />
+    <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_rect.c" />

P.S.: The MSVC project updates needs applying to VisualC-GDK too.

@creichen
Copy link
Contributor Author

@sezero Thank you! Let's see if I patched it correctly. I had a look at VisualC-GDK, but I don't see where it builds the testautomation bits; what should I look at?

@sezero
Copy link
Contributor

sezero commented Jul 31, 2023

I had a look at VisualC-GDK, but I don't see where it builds the testautomation

It doesn't. You only need to add the library-side new files to it, as far as I can see

@sezero
Copy link
Contributor

sezero commented Jul 31, 2023

Let's see if I patched it correctly.

All seems well at github actions front.

I had a look at VisualC-GDK, but

Just noticed that you already patched VisualC-GDK -- sorry for the noise.

@creichen
Copy link
Contributor Author

I patched it in reply to your comment, so it wasn't noise at all-- you checked the latest push faster than I could reply, though. :-)

@sezero
Copy link
Contributor

sezero commented Jul 31, 2023

OK then, everything is set. All this needs is maintainers' attention

Copy link
Contributor

@Kontrabant Kontrabant left a comment

Choose a reason for hiding this comment

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

Looks good at first glance, just a few minor things that I noticed in the main and Wayland code, which are pointed out below.

Two other general thoughts on the Wayland implementation:

  • Should a hit test be done on a tablet button event, so that applications using client-side decorations can have their windows moved via a tablet tool?
  • The tablet device linked-list code can be simplified and use the built-in Wayland intrusive-list functions, but that can come later.

Comment on lines 130 to 133
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_FIRST = SDL_EVENT_WINDOW_SHOWN,
SDL_EVENT_WINDOW_LAST = SDL_EVENT_WINDOW_DESTROYED,
SDL_EVENT_WINDOW_LAST = SDL_EVENT_WINDOW_PEN_LEAVE,
Copy link
Contributor

Choose a reason for hiding this comment

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

Can these events be moved above the destroyed event and leave destroyed as last? This also was requested when the occluded event was added.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'd ideally like them right after the MOUSE_ENTER events and will update the patch. The downside is that this will shift the meaning of later event IDs. As long as SDL3 isn't stable, that's not an issue, but if the patch doesn't make it for the first stable SDL3 release, we might need to reserve those two IDs for binary compatibility.
(Personally, I'd be delighted to have it included in the first stable SDL3 release, of course...)

Comment on lines 324 to 325
SDL_bool
SDL_MousePositionInWindow(SDL_Window *window, SDL_MouseID mouseID, float x, float y)
Copy link
Contributor

Choose a reason for hiding this comment

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

Return type should be on the same line in SDL3

Suggested change
SDL_bool
SDL_MousePositionInWindow(SDL_Window *window, SDL_MouseID mouseID, float x, float y)
SDL_bool SDL_MousePositionInWindow(SDL_Window *window, SDL_MouseID mouseID, float x, float y)

SDL_PenStatusInfo *status = &input->current_pen.update_status;
int button;
int button_mask;
Uint64 timestamp = Wayland_GetPointerTimestamp(input->sdlWaylandInput, time);
Copy link
Contributor

Choose a reason for hiding this comment

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

This interacts with the high-res timestamp protocol, and I can't find any documentation about how that protocol interacts with the tablet tool protocol. Let's just use

Suggested change
Uint64 timestamp = Wayland_GetPointerTimestamp(input->sdlWaylandInput, time);
Uint64 timestamp = Wayland_GetEventTimestamp(SDL_MS_TO_NS(time));

for now to be safe.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed; I get the same precision on Sway. I'll add a separate comment about this below.

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);
Copy link
Contributor

Choose a reason for hiding this comment

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

This should probably be moved to the frame done handler, since that's where the final state is committed. Just store the serial in the current pen struct.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I assume that your comment is specifically about the Wayland_UpdateImplicitGrabSerial bit?
My reading of the spec is that there is no guarantee that the serial numbers of different callbacks in the same frame will be identical (e.g., proximity_in and button both have a serial parameter, and proximity_in explicitly states that a button event may follow in the same frame, but there is no discussion of serial).
My understanding is that I should thus track the serial number across other callbacks (like tablet_tool_handle_down()), too. I will include this in my next push.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added to tablet_tool_handle_down() and tablet_tool_Handle_proximity_in(). Let me know if you disagree.

Copy link
Contributor

@Kontrabant Kontrabant Aug 1, 2023

Choose a reason for hiding this comment

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

The tool down event serial is good, but proximity in gives us the serial of an entry event, so we don't want it there. Our interest is the serials for events that can result in a 'grab' (button presses, touches, key events, etc…), as some compositors need the serial from a corresponding input event before they will allow, for example, a focus change, a system menu to be spawned, or data to be offered via the clipboard.

@creichen
Copy link
Contributor Author

creichen commented Aug 1, 2023

@Kontrabant : Regarding high-precision timestamps for tablet tools, I couldn't find mention of this in the Wayland input timestamps specs either. Weston (which is supposedly the only implementation that has high-precision input timestamps) doesn't seem to do tablet tools.
However, tablets seem to have temporal resolution in the sub-kHz range (e.g., the Cintiq 16 apparently has 266 "RPS" = "reports per second"), so millisecond precision is enough for accurately representing state-of-the-art tools, and we have three orders of magnitude to spare before nanoseconds become relevant.
In other words, I fully agree with the change you suggested and see no need to try to improve over it.

Regarding your other two points:

  • hit tests and client-side decorations on the Wayland side: I don't think I've heard of this before. So this is a Wayland-specific thing? Where could I read up on it? Is it related to libdecor.h (which I know nothing about)?

  • intrusive list functions: again not something I'm familiar with, but I'd be happy to look into it, if you can give me a pointer.

@sezero
Copy link
Contributor

sezero commented Aug 1, 2023

Here is a patch to xcode project, with no promises: patch.txt

@creichen
Copy link
Contributor Author

creichen commented Aug 1, 2023

Here is a patch to xcode project, with no promises: patch.txt

Thank you, but it doesn't seem to match (I re-force-pushed after rebasing in the hopes that it would work relative to the latest HEAD; you probably generated it for the version before that, and the file seems to have been shuffled around a lot.) Could you try it on the latest version, please? I suspect that Xcode randomly reshuffles these files when you save them, so sending the full files might be more robust than sending diffs...

struct SDL_WaylandTool *sdltool = data;
struct SDL_WaylandTabletInput *input = sdltool->tablet;
input->current_pen.update_status.axes[SDL_PEN_AXIS_XTILT] = (float)(SDL_sin(wl_fixed_to_double(xtilt) * M_PI / 180.0));
input->current_pen.update_status.axes[SDL_PEN_AXIS_YTILT] = (float)(SDL_sin(wl_fixed_to_double(ytilt) * M_PI / 180.0));
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you add a comment here explaining what you're doing? It's not immediately obvious to me why you would be taking the sine of the X and Y tilt angles here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is related to your later comment about whether to report angles or tilt vectors.
I will (for now) keep it as-is and add a comment, as you requested; if we switch to angles, this will go away.

SDL_PEN_AXIS_XTILT, /**< Pen horizontal tilt fraction. Bidirectional: -1.0..1.0 (left-to-right). This is NOT the angle, but the tilt vector x component. The physical max/min tilt may be smaller than -1.0 / 1.0, cf. \link SDL_PenCapabilityInfo \endlink . */
SDL_PEN_AXIS_YTILT, /**< Pen vertical tilt fraction. Bidirectional: -1.0..1.0 (top-to-bottom). This is NOT the angle, but the tilt vector y component. */
SDL_PEN_AXIS_DISTANCE, /**< Pen distance to drawing surface. Unidirectional: 0.0..1.0 */
SDL_PEN_AXIS_ROTATION, /**< Pen barrel rotation. Bidirectional: -1.0..1.0 (clockwise, 0 is facing up). Multiply with "2.0 * M_PI" to get radians, or "360.0" for degrees. */
Copy link
Collaborator

@cgutman cgutman Aug 4, 2023

Choose a reason for hiding this comment

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

This doesn't seem right. Multiplying -1.0 by 360 would give you a -360 degree rotation. Is this supposed to be 0.0..1.0?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for catching this! Will update the documentation (for now).
I went with -1.0..1.0 here, but this is not a straightforward call. My rationale is below:

  • Should we go from -x/2..x/2 or from 0..x?
  • Should x be 1.0, π, or 360?

Negative-Positive or 0-Positive?

  • negative-positive: Going from e.g. -1.0 to 1.0 with 0.0 at the centre means that "normal" pen usage will report 0, while barrel-rotating left will return negative values and right will return positive values. This symmetry makes it easy to e.g. set thresholds, but it means that rotating the pen by 180 degrees is ambiguous (is it -1.0 or 1.0?). Thus, the symmetry breaks at this point.
  • 0..positive: Going from e.g. 0.0 to 1.0 (without reaching 1.0) avoids the problem with 180 degree rotation, but setting a threshold for e.g. "less than 45 degrees left" now becomes "rotation > 0.875".

Range

  • Unit range (0..1) or -1..1: Homogenous wrt the other axis values, easy to convert to radians or degrees, but not a common format.
  • radians (-π..π or 0..2π): Probably the most common format, and likely the most directly useful; I expect that most uses of the angle will pass it directly into sin() and cos() anyway. However, IEEE-754 can't represent π and its multiples/fractions precisely, so anything that wants to look for exact right angles will be annoying.
  • degrees (0..359 or -180..179): This is what SDL_RenderTextureRotated() takes as parameters, and it is what Win32 and Wayland report (though not XInput2). May help port code, probably the most intuitive way for writing thresholds in code, but requires conversion to radians for most other use cases.

I'd personally find radians most natural, but nobody else uses them in SDL (or in other existing event APIs), so let's rule them out. The choice then is between being most like what people would expect for a feature that describes rotation (0..359, apparently?), and what people would most expect the range of values in the axis array to look like (0.0..1.0 or -1.0..1.0). Since the latter is also more straightforward to convert to radians, I went for that.

include/SDL3/SDL_pen.h Outdated Show resolved Hide resolved
@creichen creichen force-pushed the pen-api branch 7 times, most recently from 38e723f to eba23f8 Compare August 20, 2023 19:42
@creichen
Copy link
Contributor Author

Let's change tilt and rotation reporting to angles, which seems to be the consensus. Summary below:

@cgutman pointed out that angles (especially horizontal and vertical tilt angles) are "the" standard format for reporting pen tilt (with iOS being the only outlier); as @whot pointer out, this also de-facto applies to Xorg (though I owe him another check for the angle reporting discrepancy that I observed between Xorg and Wayland). @slouken and @jigpu noted that angles are intuitive; @jigpu further highlighted a case where tilt vectors would be more awkward to use for most developers.

My initial case for tilt vectors s is clearly weaker than I thought, and going by other APIs, horizontal/vertical tilt angles seem like the least surprising format to report overall. Thanks, everyone!

As far as I know, this is the first SDL device that will report angles. For rendering, SDL uses degrees (e.g., SDL_RenderTextureRotated()), which is also what the Win32 and Wayland APIs (and Xorg with Wacom) use. As mentioned in the discussion of rotational angles, this also has representational advantages over using radians, even if those are what we'll pass to sin() and cos() in the end, so degrees seem like the most natural format.

I'd propose going with "0 is up". For tilt vectors, this seems obvious; for barrel rotation, perhaps less so, but it is what at least Wayland does (I'll have to check the other APIs later).

@creichen creichen force-pushed the pen-api branch 3 times, most recently from 7afd735 to 7e2ddcc Compare August 26, 2023 19:59
test/testpen.c Outdated Show resolved Hide resolved
test/testpen.c Outdated Show resolved Hide resolved
src/events/SDL_pen.c Outdated Show resolved Hide resolved
@slouken
Copy link
Collaborator

slouken commented Nov 4, 2023

@creichen, can you update this for merging? I'd like to lock down the SDL3 ABI soon and this seems like a great addition.

@creichen
Copy link
Contributor Author

creichen commented Nov 5, 2023

Will do; not 100% sure I'll get to it this week, though.

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.
@sezero
Copy link
Contributor

sezero commented Nov 12, 2023

Is this ready for merging?

@creichen
Copy link
Contributor Author

creichen commented Nov 12, 2023

@sezero, @slouken: should be good to go, as far as I can tell.

@slouken slouken merged commit 7c80ac6 into libsdl-org:main Nov 12, 2023
35 checks passed
@sezero
Copy link
Contributor

sezero commented Nov 12, 2023

@slouken, @creichen: May I suggest the following patch for sdl2-compat compatibility?

EDIT: Created #8547 for it

diff --git a/include/SDL3/SDL_events.h b/include/SDL3/SDL_events.h
index 186f9f4..a49d30c 100644
--- a/include/SDL3/SDL_events.h
+++ b/include/SDL3/SDL_events.h
@@ -114,8 +114,6 @@ 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 */
@@ -129,8 +127,10 @@ typedef enum
                                              in an event watcher, the window handle is still valid and can still be used to retrieve any userdata
                                              associated with the window. Otherwise, the handle has already been destroyed and all resources
                                              associated with it are invalid */
+    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_FIRST = SDL_EVENT_WINDOW_SHOWN,
-    SDL_EVENT_WINDOW_LAST = SDL_EVENT_WINDOW_DESTROYED,
+    SDL_EVENT_WINDOW_LAST = SDL_EVENT_WINDOW_PEN_LEAVE,
 
     /* Keyboard events */
     SDL_EVENT_KEY_DOWN        = 0x300, /**< Key pressed */

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants