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
This commit is contained in:
bicarus
2026-07-13 02:06:54 -07:00
committed by GitHub
parent c255e3a1db
commit 5d25fdb035
9 changed files with 71 additions and 11 deletions
+36 -10
View File
@@ -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<TouchPoint> &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<TouchPoint> &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 < 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 };
}
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);
TOUCH_POINTS[i].store(point, std::memory_order_release);
}
}
@@ -207,7 +218,19 @@ namespace games::jb {
uint16_t next_state = 0;
std::vector<TouchPoint> 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;
+4
View File
@@ -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();
+5
View File
@@ -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();
+14
View File
@@ -2725,6 +2725,20 @@ static const std::vector<OptionDefinition> 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",
+1
View File
@@ -264,6 +264,7 @@ namespace launcher {
spice2x_JubeatLegacyTouch,
JubeatTouchAlgo,
JubeatTouchDebug,
JubeatTouchDebounce,
spice2x_RBTouchScale,
RBTouchSize,
RBTouchPollRate,
+5
View File
@@ -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<TouchPoint> *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);
+1
View File
@@ -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,
+2
View File
@@ -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);
+2
View File
@@ -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);