spice2x / spice2x.github.io

🌶️ spice2x is a spicier fork of SpiceTools with hundreds of bug fixes and new features 🌶️
https://spice2x.github.io/
GNU General Public License v3.0
116 stars 2 forks source link

TDJ Poke - Fixes and native touch experiment #196

Closed guardianblue closed 4 months ago

guardianblue commented 4 months ago

Description of change

  1. Allow poke to work when overlay subscreen is not active
  2. Fix a bug where touch points are incorrectly repeatedly sent
  3. Only send one touch when a keypad button is being hold
  4. Native touch implementation for win8+ (see testing section)

Compiling

Docker

Testing

  1. -w
  2. -w -iidxnosub
  3. -iidxnosub

Keypad press should register touches on the subscreen.

  1. -w -iidxnosub -iidxnativetouch (see below)

Since I don't have a touch device myself, I have to temporarily change iidx.cpp so that the following line is called even when native touch is used:

wintouchemu::hook_title_ends("beatmania IIDX", "main", avs::game::DLL_INSTANCE);

This seems to trick the game so that it doesn't ignore the native touch events. It would be nice if someone can test it on a device with real touch screen.

Patch file

poke-native2.patch

commit 4a885bb7eb8130af61b2bf83d8b4bdf4e91a070b
Author: guardianblue <a@example.com>
Date:   Fri Jul 12 18:24:33 2024 +0100

    TDJ | Poke fix

diff --git a/games/iidx/poke.cpp b/games/iidx/poke.cpp
index eb0ecbc..c4a942b 100644
--- a/games/iidx/poke.cpp
+++ b/games/iidx/poke.cpp
@@ -13,8 +13,15 @@
 #include "overlay/windows/generic_sub.h"
 #include "rawinput/rawinput.h"
 #include "touch/touch.h"
+#include "util/libutils.h"
 #include "util/logging.h"

+static HINSTANCE USER32_INSTANCE = nullptr;
+typedef BOOL (WINAPI *InitializeTouchInjection_t)(UINT32, DWORD);
+typedef BOOL (WINAPI *InjectTouchInput_t)(UINT32, POINTER_TOUCH_INFO*);
+static InitializeTouchInjection_t pInitializeTouchInjection = nullptr;
+static InjectTouchInput_t pInjectTouchInput = nullptr;
+
 namespace poke {

     static std::thread *THREAD = nullptr;
@@ -104,53 +111,79 @@ namespace poke {
         {"1/D", 50},
     };

-    void clear_touch_points() {
-        std::vector<DWORD> touch_ids;
-        std::vector<TouchPoint> touch_points;
-        touch_get_points(touch_points);
-        for (auto &tp : touch_points) {
-            touch_ids.emplace_back(tp.id);
+    void initialize_native_touch_library() {
+        if (USER32_INSTANCE == nullptr) {
+            USER32_INSTANCE = libutils::load_library("user32.dll");
         }
-        touch_remove_points(&touch_ids);
+
+        pInitializeTouchInjection = libutils::try_proc<InitializeTouchInjection_t>(
+                USER32_INSTANCE, "InitializeTouchInjection");
+        pInjectTouchInput = libutils::try_proc<InjectTouchInput_t>(
+                USER32_INSTANCE, "InjectTouchInput");
     }

-    // void emulate_native_touch(int touch_id, int x, int y) {
-    //     POINTER_TOUCH_INFO contact;
-    //     BOOL bRet = TRUE;
+    void emulate_native_touch(TouchPoint tp, bool is_down) {
+        if (pInjectTouchInput == nullptr) {
+            return;
+        }
+
+        POINTER_TOUCH_INFO contact;
+        BOOL bRet = TRUE;
+        const int contact_offset = 2;

-    //     memset(&contact, 0, sizeof(POINTER_TOUCH_INFO));
+        memset(&contact, 0, sizeof(POINTER_TOUCH_INFO));

-    //     contact.pointerInfo.pointerType = PT_TOUCH;
-    //     contact.pointerInfo.pointerId = touch_id;
-    //     contact.pointerInfo.ptPixelLocation.x = x;
-    //     contact.pointerInfo.ptPixelLocation.y = y;
-    //     contact.pointerInfo.pointerFlags = POINTER_FLAG_DOWN | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT;
-    //     contact.touchFlags = TOUCH_FLAG_NONE;
-    //     contact.touchMask = TOUCH_MASK_CONTACTAREA | TOUCH_MASK_ORIENTATION | TOUCH_MASK_PRESSURE;
-    //     contact.orientation = 90;
-    //     contact.pressure = 32000;
+        contact.pointerInfo.pointerType = PT_TOUCH;
+        contact.pointerInfo.pointerId = 0;
+        contact.pointerInfo.ptPixelLocation.x = tp.x;
+        contact.pointerInfo.ptPixelLocation.y = tp.y;
+        if (is_down) {
+            contact.pointerInfo.pointerFlags = POINTER_FLAG_DOWN | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT;
+        } else {
+            contact.pointerInfo.pointerFlags = POINTER_FLAG_UP;
+        }

-    //     contact.rcContact.top = y - 2;
-    //     contact.rcContact.bottom = y + 2;
-    //     contact.rcContact.left = x - 2;
-    //     contact.rcContact.right = x + 2;
+        contact.pointerInfo.dwTime = 0;
+        contact.pointerInfo.PerformanceCount = 0;

-    //     bRet = InjectTouchInput(1, &contact);
+        contact.touchFlags = TOUCH_FLAG_NONE;
+        contact.touchMask = TOUCH_MASK_CONTACTAREA | TOUCH_MASK_ORIENTATION | TOUCH_MASK_PRESSURE;
+        contact.orientation = 0;
+        contact.pressure = 1024;

-    //     if (bRet) {
-    //         contact.pointerInfo.pointerFlags = POINTER_FLAG_UP;
-    //         InjectTouchInput(1, &contact);
-    //     }
-    //     //log_warning("poke", "native touch: {} {}", to_string(x), to_string(y));
+        contact.rcContact.top = tp.y - contact_offset;
+        contact.rcContact.bottom = tp.y + contact_offset;
+        contact.rcContact.left = tp.x - contact_offset;
+        contact.rcContact.right = tp.x + contact_offset;

-    // }
+        bRet = InjectTouchInput(1, &contact);
+        if (!bRet) {
+            log_warning("poke", "error injecting native touch {}: ({}, {}) error: {}", is_down ? "down" : "up", tp.x, tp.y, GetLastError());
+        }
+    }

-    // void emulate_native_touch_points(std::vector<TouchPoint> *touch_points) {
-    //     InitializeTouchInjection(touch_points->size(), TOUCH_FEEDBACK_NONE);
-    //     for (auto &touch : *touch_points) {
-    //         emulate_native_touch(touch.id, touch.x, touch.y);
-    //     }
-    // }
+    void emulate_native_touch_points(std::vector<TouchPoint> *touch_points) {
+        int i = 0;
+        for (auto &touch : *touch_points) {
+            emulate_native_touch(touch, true);
+        }
+    }
+
+    void clear_native_touch_points(std::vector<TouchPoint> *touch_points) {
+        for (auto &touch : *touch_points) {
+            emulate_native_touch(touch, false);
+        }
+        touch_points->clear();
+    }
+    
+    void clear_touch_points(std::vector<TouchPoint> *touch_points) {
+        std::vector<DWORD> touch_ids;
+        for (auto &touch : *touch_points) {
+            touch_ids.emplace_back(touch.id);
+        }
+        touch_remove_points(&touch_ids);
+        touch_points->clear();
+    }

     void enable() {

@@ -165,79 +198,108 @@ namespace poke {
             // log
             log_info("poke", "enabled");

+            bool use_native = games::iidx::NATIVE_TOUCH;
+
+            std::vector<TouchPoint> touch_points;
+            std::vector<uint16_t> last_keypad_state = {0, 0};
+
+            if (use_native) {
+                initialize_native_touch_library();
+                
+                if (pInitializeTouchInjection != nullptr) {
+                    pInitializeTouchInjection(1, TOUCH_FEEDBACK_NONE);
+                }
+            }
+
             // set variable to false to stop
             while (THREAD_RUNNING) {

                 // clean up touch from last frame
-                clear_touch_points();
+                if (touch_points.size() > 0) {
+                    if (use_native) {
+                        clear_native_touch_points(&touch_points);
+                    } else {
+                        clear_touch_points(&touch_points);
+                    }
+                }

                 for (int unit = 0; unit < 2; unit++) {
                     // get keypad state
                     auto state = eamuse_get_keypad_state(unit);

-                    std::vector<TouchPoint> touch_points;
-
-                    // add keys
-                    for (auto &mapping : KEYPAD_MAPPINGS) {
-                        if (state & mapping.state) {
-                            std::string handle = fmt::format("{0}/{1}", unit, mapping.character);
-
-                            auto x_iter = IIDX_KEYPAD_POSITION_X.find(handle);
-                            auto y_iter = IIDX_KEYPAD_POSITION_Y.find(handle);
-
-                            if (x_iter != IIDX_KEYPAD_POSITION_X.end() && y_iter != IIDX_KEYPAD_POSITION_Y.end()) {
-                                DWORD touch_id = (DWORD)(0xFFFFFF * unit + mapping.character);
-
-                                float x = x_iter->second / 1920.0;
-                                float y = y_iter->second / 1080.0;
-
-                                if (GRAPHICS_IIDX_WSUB) {
-                                    x *= GRAPHICS_IIDX_WSUB_WIDTH;
-                                    y *= GRAPHICS_IIDX_WSUB_HEIGHT;
-                                } else if (GENERIC_SUB_WINDOW_FULLSIZE) {
-                                    if (GRAPHICS_WINDOWED) {
-                                        x *= SPICETOUCH_TOUCH_WIDTH;
-                                        y *= SPICETOUCH_TOUCH_HEIGHT;
+                    if (state != 0) {
+                        // add keys
+                        for (auto &mapping : KEYPAD_MAPPINGS) {
+                            if (state & mapping.state) {
+                                if (last_keypad_state[unit] & mapping.state) {
+                                    // log_warning("poke", "ignoring hold {} {}", unit, mapping.character);
+                                    continue;
+                                }
+                                std::string handle = fmt::format("{0}/{1}", unit, mapping.character);
+
+                                auto x_iter = IIDX_KEYPAD_POSITION_X.find(handle);
+                                auto y_iter = IIDX_KEYPAD_POSITION_Y.find(handle);
+
+                                if (x_iter != IIDX_KEYPAD_POSITION_X.end() && y_iter != IIDX_KEYPAD_POSITION_Y.end()) {
+                                    DWORD touch_id = (DWORD)(0xFFFFFF * unit + mapping.character);
+
+                                    float x = x_iter->second / 1920.0;
+                                    float y = y_iter->second / 1080.0;
+                                    if (use_native) {
+                                        x *= rawinput::TOUCHSCREEN_RANGE_X;
+                                        y *= rawinput::TOUCHSCREEN_RANGE_Y;
+                                    } else if (GRAPHICS_IIDX_WSUB) {
+                                        // Scale to windowed subscreen
+                                        x *= GRAPHICS_IIDX_WSUB_WIDTH;
+                                        y *= GRAPHICS_IIDX_WSUB_HEIGHT;
+                                    } else if (GENERIC_SUB_WINDOW_FULLSIZE || !overlay::OVERLAY->get_active()) {
+                                        // Overlay is not present, scale to main screen
+                                        if (GRAPHICS_WINDOWED) {
+                                            x *= SPICETOUCH_TOUCH_WIDTH;
+                                            y *= SPICETOUCH_TOUCH_HEIGHT;
+                                        } else {
+                                            x *= ImGui::GetIO().DisplaySize.x;
+                                            y *= ImGui::GetIO().DisplaySize.y;
+                                        }
                                     } else {
-                                        x *= ImGui::GetIO().DisplaySize.x;
-                                        y *= ImGui::GetIO().DisplaySize.y;
-                                    }
-                                } else {
-                                    // Overlay subscreen
-                                    x = (GENERIC_SUB_WINDOW_X + x * GENERIC_SUB_WINDOW_WIDTH);
-                                    y = (GENERIC_SUB_WINDOW_Y + y * GENERIC_SUB_WINDOW_HEIGHT);
-
-                                    // Scale to window size ratio
-                                    if (GRAPHICS_WINDOWED) {
-                                        x *= SPICETOUCH_TOUCH_WIDTH / ImGui::GetIO().DisplaySize.x;
-                                        y *= SPICETOUCH_TOUCH_HEIGHT / ImGui::GetIO().DisplaySize.y;
+                                        // Overlay subscreen
+                                        x = (GENERIC_SUB_WINDOW_X + x * GENERIC_SUB_WINDOW_WIDTH);
+                                        y = (GENERIC_SUB_WINDOW_Y + y * GENERIC_SUB_WINDOW_HEIGHT);
+
+                                        // Scale to window size ratio
+                                        if (GRAPHICS_WINDOWED) {
+                                            x *= SPICETOUCH_TOUCH_WIDTH / ImGui::GetIO().DisplaySize.x;
+                                            y *= SPICETOUCH_TOUCH_HEIGHT / ImGui::GetIO().DisplaySize.y;
+                                        }
                                     }
+
+                                    TouchPoint tp {
+                                        .id = touch_id,
+                                        .x = (LONG)x,
+                                        .y = (LONG)y,
+                                        .mouse = true,
+                                    };
+                                    touch_points.emplace_back(tp);
+                                    // log_warning("poke", "coords: {} {}", to_string(tp.x), to_string(tp.y));
                                 }
+                            }
+                        } // for all keys
+                    } // if state != 0

-                                TouchPoint tp {
-                                    .id = touch_id,
-                                    .x = (LONG)x,
-                                    .y = (LONG)y,
-                                    .mouse = true,
-                                };
-                                touch_points.emplace_back(tp);
+                    last_keypad_state[unit] = state;

-                                // log_warning("poke", "keypad: {} {}", to_string(tp.x), to_string(tp.y));
-                            }
-                        }
-
-                        if (touch_points.size() > 0) {
-                            // if (games::iidx::NATIVE_TOUCH) {
-                            //     emulate_native_touch_points(&touch_points);
-                            // } else {
-                                touch_write_points(&touch_points);
-                            // }
-                        }
+                } // for all units
+
+                if (touch_points.size() > 0) {
+                    if (use_native) {
+                        emulate_native_touch_points(&touch_points);
+                    } else {
+                        touch_write_points(&touch_points);
                     }
                 }

                 // slow down
-                Sleep(70);
+                Sleep(50);
             }

             return nullptr;
diff --git a/misc/wintouchemu.cpp b/misc/wintouchemu.cpp
index 35da2e7..e3bb23d 100644
--- a/misc/wintouchemu.cpp
+++ b/misc/wintouchemu.cpp
@@ -149,8 +149,6 @@ namespace wintouchemu {
                 auto y = touch_event->y;
                 auto valid = true;

-                // log_misc("wintouchemu", "touch event ({}, {})", to_string(x), to_string(y));
-
                 if (GRAPHICS_IIDX_WSUB) {
                     // touch was received on subscreen window.
                     RECT clientRect {};
@@ -164,6 +162,8 @@ namespace wintouchemu {
                     valid = false;
                 }

+                // log_misc("wintouchemu", "touch event {} ({}, {})", touch_event->type, x, y);
+
                 touch_input->x = x * 100;
                 touch_input->y = y * 100;
                 touch_input->hSource = hTouchInput;
@@ -206,14 +206,14 @@ namespace wintouchemu {
                         touch_input->y -= SPICETOUCH_TOUCH_Y;
                     }

-                    // log_misc("wintouchemu", "mouse state ({}, {})", to_string(touch_input->x), to_string(touch_input->y));
-
                     auto valid = true;
                     if (overlay::OVERLAY) {
                         valid = overlay::OVERLAY->transform_touch_point(
                             &touch_input->x, &touch_input->y);
                     }

+                    // log_misc("wintouchemu", "mouse state {} ({}, {})", valid, touch_input->x, touch_input->y);
+
                     // touch inputs require 100x precision per pixel
                     touch_input->x *= 100;
                     touch_input->y *= 100;
diff --git a/rawinput/rawinput.cpp b/rawinput/rawinput.cpp
index 7c4322a..cd3da0f 100644
--- a/rawinput/rawinput.cpp
+++ b/rawinput/rawinput.cpp
@@ -24,6 +24,8 @@ namespace rawinput {
     bool NOLEGACY = false;
     uint8_t HID_LIGHT_BRIGHTNESS = 100; // 100%
     bool ENABLE_SMX_STAGE = false;
+    int TOUCHSCREEN_RANGE_X = 0;
+    int TOUCHSCREEN_RANGE_Y = 0;
 }

 rawinput::RawInputManager::RawInputManager() {
@@ -2397,10 +2399,13 @@ void rawinput::RawInputManager::devices_flush_output(bool optimized) {

 void rawinput::RawInputManager::devices_print() {

+    bool touchscreen_found = false;
+
     // iterate devices
     log_info("rawinput", "printing list of detected devices");
     log_info("rawinput", "detected device count: {}", devices.size());
     for (auto &device : devices) {
+        bool is_touchscreen = false;

         // lock it
         device.mutex->lock();
@@ -2426,6 +2431,10 @@ void rawinput::RawInputManager::devices_print() {
                 // check touchscreen
                 if (device.hidInfo->touch.valid) {
                     log_info("rawinput", "device is marked as touchscreen");
+                    if (!touchscreen_found) {
+                        touchscreen_found = true;
+                        is_touchscreen = true;
+                    }
                 }

                 // button caps
@@ -2474,6 +2483,12 @@ void rawinput::RawInputManager::devices_print() {
                         LONG max = value_caps.LogicalMax;
                         log_misc("rawinput", "device value caps detected: {} ({} to {}, {}-bit)",
                                 name, min, max, value_caps.BitSize);
+
+                        if (name.compare("X") == 0 && is_touchscreen) {
+                            TOUCHSCREEN_RANGE_X = max;
+                        } else if (name.compare("Y") == 0 && is_touchscreen) {
+                            TOUCHSCREEN_RANGE_Y = max;
+                        }
                     }
                 }

diff --git a/rawinput/rawinput.h b/rawinput/rawinput.h
index 004a07b..1ddff4a 100644
--- a/rawinput/rawinput.h
+++ b/rawinput/rawinput.h
@@ -20,6 +20,9 @@ namespace rawinput {

     extern bool ENABLE_SMX_STAGE;

+    extern int TOUCHSCREEN_RANGE_X;
+    extern int TOUCHSCREEN_RANGE_Y;
+
     struct DeviceCallback {
         void *data;
         std::function<void(void*, Device*)> f;
diff --git a/rawinput/touch.cpp b/rawinput/touch.cpp
index 7b7ac70..68f2eb4 100644
--- a/rawinput/touch.cpp
+++ b/rawinput/touch.cpp
@@ -262,7 +262,7 @@ namespace rawinput::touch {
             hid_tp.id = (DWORD) hid->value_states_raw[touch.elements_contact_identifier[i]];
             hid_tp.id += (DWORD) (0xFFFFFF + device->id * 512);

-            //std::string src = "(none)";
+            // std::string src = "(none)";

             // check if tip switch is down
             size_t index_pressed = touch.elements_pressed[i];
@@ -270,10 +270,8 @@ namespace rawinput::touch {
                 if (index_pressed < button_states.size()) {
                     hid_tp.down = button_states[index_pressed];

-                    /*
-                    if (hid_tp.down)
-                        src = "pressed (index: " + to_string(touch.elements_pressed[i]) + ", state: " + to_string(button_states[index_pressed]) +")";
-                        */
+                    // if (hid_tp.down)
+                    //     src = "pressed (index: " + to_string(touch.elements_pressed[i]) + ", state: " + to_string(button_states[index_pressed]) +")";

                     break;
                 } else
@@ -287,10 +285,8 @@ namespace rawinput::touch {
                 height = hid->value_states[touch.elements_height[i]];
                 hid_tp.down = width > 0.f && height > 0.f;

-                /*
-                if (hid_tp.down)
-                    src = "width_height (width index: " + to_string(touch.elements_width[i]) + ", height index: " + to_string(touch.elements_height[i]) + ")";
-                    */
+                // if (hid_tp.down)
+                //     src = "width_height (width index: " + to_string(touch.elements_width[i]) + ", height index: " + to_string(touch.elements_height[i]) + ")";
             }

             // so last thing we can check is the pressure
@@ -298,24 +294,22 @@ namespace rawinput::touch {
                 auto pressure = hid->value_states[touch.elements_pressure[i]];
                 hid_tp.down = pressure > 0.f;

-                /*
-                if (hid_tp.down)
-                    src = "pressure (index: " + to_string(touch.elements_pressure[i]) + ")";
-                    */
+                // if (hid_tp.down)
+                //     src = "pressure (index: " + to_string(touch.elements_pressure[i]) + ")";
             }

-            /*
-            log_info("rawinput",
-                "touch i: " + to_string(i) +
-                " (id: " + to_string(hid_tp.id) +
-                "), ci: " + to_string(hid->value_states_raw[touch.elements_contact_identifier[i]]) +
-                ", x: " + to_string(pos_x) +
-                ", y: " + to_string(pos_y) +
-                ", width: " + to_string(width) +
-                ", height: " + to_string(height) +
-                ", down: " + to_string(hid_tp.down) +
-                ", src: " + src);
-                */
+            // log_info("rawinput",
+            //     "touch {} (id: {}), ci: {}, pos: ({}, {}) size: ({}, {}) down {} src: {}",
+            //     to_string(i),
+            //     to_string(hid_tp.id),
+            //     to_string(hid->value_states_raw[touch.elements_contact_identifier[i]]),
+            //     to_string(pos_x),
+            //     to_string(pos_y),
+            //     to_string(width),
+            //     to_string(height),
+            //     to_string(hid_tp.down),
+            //     src
+            // );

             // add to touch points
             touch_points.emplace_back(hid_tp);
guardianblue commented 4 months ago

@sp2xdev you might want to include this as the new release as the spamming of touch events from poke might also be a cause

guardianblue commented 4 months ago

nvm this can wait

sp2xdev commented 4 months ago

Not entirely sure if any of this will make a difference, but reading the MSDN docs:

If neither dwTime and PerformanceCount are specified, InjectTouchInput allocates the timestamp based on the timing of the call. If InjectTouchInput calls are repeatedly less than 0.1 millisecond apart, ERROR_NOT_READY might be returned. The error will not invalidate the input immediately, but the injection application needs to retry the same frame again for injection to succeed.

maybe the calls are 0.1 ms apart and therefore failing? I'm not sure.

Lastly - these APIs are Windows 8+ only, but spice needs to support Windows 7 (ideally XP as well although that doesn't quite work right now). Therefore, you need to use libutils::tryproc to obtain a function pointer instead of statically linking against the OS library (see win7.cpp / win8.cpp for examples) and check that the OS implements it (it might not).

guardianblue commented 4 months ago

Thanks for the info, will tinker further

guardianblue commented 4 months ago

It seems to work now (for win8+). Looks like touch width/height is needed after all.

I will put a new patch later to use tryproc

guardianblue commented 4 months ago

Updated with tryproc

sp2xdev commented 4 months ago

When in full screen and iidxnosub not checked, GRAPHICS_IIDX_WSUB is true (by default) for all games. There are a few places around the code (this change included) where GRAPHICS_IIDX_WSUB is checked but not GRAPHICS_WINDOWED && game is LDJ. I think this might potentially break quite a few things.

Let me see if I can clean this up first, and then merge your changes.

sp2xdev commented 4 months ago

Going through code now:

You have a lot of changes where comments are being moved around (touch.cpp, wintouchemu.cpp...)... please avoid submitting patches with these as it create extra work (I'll just revert them for now).

Will test with a touch screen momentarily.

sp2xdev commented 4 months ago

Poke in native touch mode does not work at all. I think the coordinates are completely off.... it's a bit tricky though, most people have touch monitors as a secondary monitor for playing TDJ.

I'm going to compile out the native stuff for now and integrate this, as I need to get a new build out to address some touch regressions in other games. If you (or anyone) wants to revive native touch poke, please do submit new code but only after it's been tested with a touch screen as primary or secondary monitor.

sp2xdev commented 4 months ago

One minor thing to point out for future reference - with -iidxnosub checked, in windowed or fullscreen mode, using poke (pressing numbers) result in a phantom touch being "stuck" at {0, 1080}. You can see this in the touch test menu. This is usually not a huge deal but can sometimes lead to dropped touches in menus.

sp2xdev commented 4 months ago

Merged & included in the beta release. Now we can log into TDJ with just a keyboard, without bringing up the subscreen! Thanks.

guardianblue commented 4 months ago

One minor thing to point out for future reference - with -iidxnosub checked, in windowed or fullscreen mode, using poke (pressing numbers) result in a phantom touch being "stuck" at {0, 1080}. You can see this in the touch test menu. This is usually not a huge deal but can sometimes lead to dropped touches in menus.

Thanks for catching this, I'll see how this can be prevented