From 5d25fdb0358157302fea95a03d06d2cfc0eff25b Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Mon, 13 Jul 2026 02:06:54 -0700 Subject: [PATCH] jb: touch debounce (#798) ## Link to GitHub Issue or related Pull Request, if one exists n/a ## Description of change Debouce option for touch input. Applies to all touch handlers (rawinput, win8, win7) since it sits at the touch layer, and then consumed by jb touch logic. ## Testing --- src/spice2x/games/jb/jb_touch.cpp | 48 ++++++++++++++++++++++++------- src/spice2x/games/jb/jb_touch.h | 4 +++ src/spice2x/launcher/launcher.cpp | 5 ++++ src/spice2x/launcher/options.cpp | 14 +++++++++ src/spice2x/launcher/options.h | 1 + src/spice2x/touch/touch.cpp | 5 ++++ src/spice2x/touch/touch.h | 1 + src/spice2x/touch/win7.cpp | 2 ++ src/spice2x/touch/win8.cpp | 2 ++ 9 files changed, 71 insertions(+), 11 deletions(-) diff --git a/src/spice2x/games/jb/jb_touch.cpp b/src/spice2x/games/jb/jb_touch.cpp index 990c4c3..dd7ba9c 100644 --- a/src/spice2x/games/jb/jb_touch.cpp +++ b/src/spice2x/games/jb/jb_touch.cpp @@ -12,6 +12,7 @@ #include "touch/touch.h" #include "rawinput/touch.h" #include "util/logging.h" +#include "util/time.h" #include "util/utils.h" // touch layout: a 4x4 grid of 160px buttons separated by 37 / 38 / 37 px gaps @@ -36,6 +37,7 @@ namespace games::jb { // touch state JubeatTouchAlgorithm TOUCH_ALGORITHM = Improved; JubeatTouchDebugMode TOUCH_DEBUG_OVERLAY = JbTouchDebugAuto; + uint32_t TOUCH_DEBOUNCE_MS = 0; static std::atomic_bool TOUCH_ENABLE = false; static bool TOUCH_ATTACHED = false; static bool IS_PORTRAIT = true; @@ -52,22 +54,31 @@ namespace games::jb { static void clear_touch_points() { for (auto &point : TOUCH_POINTS) { - point.store(JB_INVALID_TOUCH_POINT, std::memory_order_relaxed); + point.store(JB_INVALID_TOUCH_POINT, std::memory_order_release); } } - static void publish_touch_points(const std::vector &touch_points) { + // false when a contact is younger than the debounce window and should be ignored + static bool touch_matured(const TouchPoint &tp, double now_ms, double threshold_ms) { + return now_ms - tp.down_ms >= threshold_ms; + } + + // snapshot for the debug overlay: each slot atomically holds one matured contact or + // the invalid sentinel. immature and absent contacts are published as invalid, so the + // overlay never shows a suppressed tap and a single atomic per slot cannot tear + static void publish_touch_points(const std::vector &touch_points, + double now_ms, double threshold_ms) { size_t count = touch_points.size(); if (count > JB_MAX_TOUCH_POINTS) { count = JB_MAX_TOUCH_POINTS; } - for (size_t i = count; i < JB_MAX_TOUCH_POINTS; i++) { - TOUCH_POINTS[i].store(JB_INVALID_TOUCH_POINT, std::memory_order_relaxed); - } - for (size_t i = 0; i < count; i++) { - POINT point { touch_points[i].x, touch_points[i].y }; - TOUCH_POINTS[i].store(point, std::memory_order_relaxed); + for (size_t i = 0; i < JB_MAX_TOUCH_POINTS; i++) { + POINT point = JB_INVALID_TOUCH_POINT; + if (i < count && touch_matured(touch_points[i], now_ms, threshold_ms)) { + point = { touch_points[i].x, touch_points[i].y }; + } + TOUCH_POINTS[i].store(point, std::memory_order_release); } } @@ -207,7 +218,19 @@ namespace games::jb { uint16_t next_state = 0; std::vector touch_points; touch_get_points(touch_points); - publish_touch_points(touch_points); + + // debounce: the landing time is stamped by the touch layer, so this is + // independent of how often the game polls for input (0 = off) + double now_ms = get_performance_milliseconds(); + double threshold_ms = TOUCH_DEBOUNCE_MS; + + // publish every raw contact (with its maturity) for the debug overlay, then drop + // the ones still inside the debounce window so no algorithm sees them + publish_touch_points(touch_points, now_ms, threshold_ms); + std::erase_if(touch_points, [&](const TouchPoint &tp) { + return !touch_matured(tp, now_ms, threshold_ms); + }); + if (TOUCH_ALGORITHM == Legacy) { // legacy: evenly divide the play area into a 4x4 grid @@ -355,18 +378,21 @@ namespace games::jb { int radius = touch_radius(); int draw_radius = radius > 0 ? radius : JB_TOUCH_RADIUS; + // each slot holds a matured contact or the invalid sentinel; immature and absent + // contacts were already filtered out by publish_touch_points for (auto &touch_point : TOUCH_POINTS) { - POINT point = touch_point.load(std::memory_order_relaxed); + POINT point = touch_point.load(std::memory_order_acquire); if (point.x == JB_INVALID_TOUCH_COORD) { continue; } + bool pressed = touch_presses_button(point.x, point.y, gx, gy, legacy, radius); SelectObject(hdc, pressed ? pen_white : pen_gray); Ellipse(hdc, point.x - draw_radius, point.y - draw_radius, point.x + draw_radius, point.y + draw_radius); - // the arc would point at a single snapped-to button; plus can trigger several at + // the arc points at a single snapped-to button; plus can trigger several at // once, so skip it there if (pressed || TOUCH_ALGORITHM == Plus) { continue; diff --git a/src/spice2x/games/jb/jb_touch.h b/src/spice2x/games/jb/jb_touch.h index dcf0461..2cc92c2 100644 --- a/src/spice2x/games/jb/jb_touch.h +++ b/src/spice2x/games/jb/jb_touch.h @@ -26,6 +26,10 @@ namespace games::jb { extern JubeatTouchAlgorithm TOUCH_ALGORITHM; extern JubeatTouchDebugMode TOUCH_DEBUG_OVERLAY; + // minimum contact age in milliseconds before a touch registers (0 = off); + // filters momentary phantom touches from noisy panels + extern uint32_t TOUCH_DEBOUNCE_MS; + // atomically published panel state, read by the I/O layer std::bitset<16> touch_state(); diff --git a/src/spice2x/launcher/launcher.cpp b/src/spice2x/launcher/launcher.cpp index b833e36..4bd03e3 100644 --- a/src/spice2x/launcher/launcher.cpp +++ b/src/spice2x/launcher/launcher.cpp @@ -1333,6 +1333,11 @@ int main_implementation(int argc, char *argv[]) { } } + if (options[launcher::Options::JubeatTouchDebounce].is_active()) { + games::jb::TOUCH_DEBOUNCE_MS = + options[launcher::Options::JubeatTouchDebounce].value_uint32(); + } + // reflec beat touch emulation if (options[launcher::Options::spice2x_RBTouchScale].is_active()) { games::rb::TOUCH_SCALING = options[launcher::Options::spice2x_RBTouchScale].value_uint32(); diff --git a/src/spice2x/launcher/options.cpp b/src/spice2x/launcher/options.cpp index e4e2818..d4d3414 100644 --- a/src/spice2x/launcher/options.cpp +++ b/src/spice2x/launcher/options.cpp @@ -2725,6 +2725,20 @@ static const std::vector OPTION_DEFINITIONS = { }, .quick_setting_category = "Game", }, + { + // JubeatTouchDebounce + .title = "JB Touch Debounce", + .name = "jubeattouchdebounce", + .desc = "For touch screen players: ignore extremely quick touches by requiring a " + "touch to be held for at least this many milliseconds before it registers.\n\n" + "Useful for filtering phantom touches on a noisy touch screen. A higher value " + "adds input latency, so keep it small. Default: off (0).", + .type = OptionType::Integer, + .setting_name = "8", + .game_name = "Jubeat", + .category = "Game Options", + .quick_setting_category = "Game", + }, { // spice2x_RBTouchScale .title = "RB Touch Emulation Scale", diff --git a/src/spice2x/launcher/options.h b/src/spice2x/launcher/options.h index d9e6b2d..67504e6 100644 --- a/src/spice2x/launcher/options.h +++ b/src/spice2x/launcher/options.h @@ -264,6 +264,7 @@ namespace launcher { spice2x_JubeatLegacyTouch, JubeatTouchAlgo, JubeatTouchDebug, + JubeatTouchDebounce, spice2x_RBTouchScale, RBTouchSize, RBTouchPollRate, diff --git a/src/spice2x/touch/touch.cpp b/src/spice2x/touch/touch.cpp index f9853d4..cee87ab 100644 --- a/src/spice2x/touch/touch.cpp +++ b/src/spice2x/touch/touch.cpp @@ -20,6 +20,7 @@ #include "util/detour.h" #include "util/libutils.h" #include "util/logging.h" +#include "util/time.h" #include "util/utils.h" #include "handler.h" @@ -538,6 +539,7 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP .x = GET_X_LPARAM(lParam), .y = GET_Y_LPARAM(lParam), .mouse = true, + .down_ms = get_performance_milliseconds(), }; TOUCH_POINTS.push_back(tp); @@ -851,6 +853,9 @@ void touch_write_points(std::vector *touch_points) { // create new touch point when not found if (!found) { + // stamp the landing time so debounce can measure the contact's age + tp.down_ms = get_performance_milliseconds(); + // add touch point TOUCH_POINTS.push_back(tp); diff --git a/src/spice2x/touch/touch.h b/src/spice2x/touch/touch.h index ec041b3..c1abb20 100644 --- a/src/spice2x/touch/touch.h +++ b/src/spice2x/touch/touch.h @@ -7,6 +7,7 @@ struct TouchPoint { DWORD id; LONG x, y; bool mouse; + double down_ms = 0.0; // performance-counter milliseconds when the contact first landed }; enum TouchEventType { TOUCH_DOWN, diff --git a/src/spice2x/touch/win7.cpp b/src/spice2x/touch/win7.cpp index f444a39..72204e6 100644 --- a/src/spice2x/touch/win7.cpp +++ b/src/spice2x/touch/win7.cpp @@ -11,6 +11,7 @@ #include "util/libutils.h" #include "util/logging.h" +#include "util/time.h" #include "rawinput/touch.h" // mingw issue #2205 workaround @@ -177,6 +178,7 @@ void Win7Handler::handle_message(msg_handler_result &result, HWND hWnd, UINT msg .x = point.x, .y = point.y, .mouse = false, + .down_ms = get_performance_milliseconds(), }; TOUCH_POINTS.push_back(tp); diff --git a/src/spice2x/touch/win8.cpp b/src/spice2x/touch/win8.cpp index 11d528d..4aadc57 100644 --- a/src/spice2x/touch/win8.cpp +++ b/src/spice2x/touch/win8.cpp @@ -12,6 +12,7 @@ #include "util/libutils.h" #include "util/logging.h" +#include "util/time.h" #include "rawinput/touch.h" // mingw does not seem to have this either @@ -220,6 +221,7 @@ void Win8Handler::handle_message(msg_handler_result &result, HWND hWnd, UINT msg .x = point.x, .y = point.y, .mouse = false, + .down_ms = get_performance_milliseconds(), }; TOUCH_POINTS.push_back(tp);