From 1a1be7a3938ea8a95abe6908ab1828809c08a36b Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Mon, 13 Jul 2026 23:30:41 -0700 Subject: [PATCH] rb: touch debug overlay, tweak scaling (#801) ## Link to GitHub Issue or related Pull Request, if one exists n/a ## Description of change Similar to #797, draw an overlay to debug touch issues for reflec beat. For rb, we draw on the ImGui surface. Draw horizontal and vertical line for triggered sensors, and also a bounding box if scaling is in use. Make small tweaks to how scaling works. ## Testing Verified volzza 2 / reflesia / 1st game (landscape KBR) --- src/spice2x/CMakeLists.txt | 1 + src/spice2x/games/rb/touch.cpp | 89 ++++++++++------- src/spice2x/games/rb/touch_debug.cpp | 139 +++++++++++++++++++++++++++ src/spice2x/games/rb/touch_debug.h | 14 +++ src/spice2x/games/rb/touch_defs.h | 17 ++++ src/spice2x/launcher/launcher.cpp | 2 + src/spice2x/launcher/options.cpp | 12 +++ src/spice2x/launcher/options.h | 1 + src/spice2x/overlay/overlay.cpp | 12 ++- 9 files changed, 252 insertions(+), 35 deletions(-) create mode 100644 src/spice2x/games/rb/touch_debug.cpp create mode 100644 src/spice2x/games/rb/touch_debug.h create mode 100644 src/spice2x/games/rb/touch_defs.h diff --git a/src/spice2x/CMakeLists.txt b/src/spice2x/CMakeLists.txt index 66db3e8..dca99bd 100644 --- a/src/spice2x/CMakeLists.txt +++ b/src/spice2x/CMakeLists.txt @@ -432,6 +432,7 @@ set(SOURCE_FILES ${SOURCE_FILES} games/rb/rb.cpp games/rb/io.cpp games/rb/touch.cpp + games/rb/touch_debug.cpp games/bs/bs.cpp games/bs/io.cpp games/rf3d/rf3d.cpp diff --git a/src/spice2x/games/rb/touch.cpp b/src/spice2x/games/rb/touch.cpp index f2d967a..50108ca 100644 --- a/src/spice2x/games/rb/touch.cpp +++ b/src/spice2x/games/rb/touch.cpp @@ -6,6 +6,8 @@ #include "hooks/graphics/graphics.h" #include "rawinput/touch.h" #include "touch/touch.h" +#include "touch_debug.h" +#include "touch_defs.h" #include "util/logging.h" #include "util/time.h" #include "util/utils.h" @@ -13,27 +15,38 @@ static std::string WINDOW_TITLE = "REFLEC BEAT"; namespace games::rb { - uint16_t TOUCH_SCALING = 1000; + static constexpr double TOUCH_POINT_OFFSET_DIVISOR = 3.0; + static constexpr double X_INPUT_COORDINATE_COUNT = 54.0; + + uint16_t TOUCH_SCALING = TOUCH_SCALE_DEFAULT; + + static void packet_set_bit(unsigned char *packet, int bit) { + packet[TOUCH_PACKET_DATA_OFFSET + bit / 8] |= + static_cast(1u << (bit % 8)); + } } -games::rb::ReflecBeatTouchDeviceHandle::ReflecBeatTouchDeviceHandle(bool log_fps) { - this->log_fps = log_fps; +games::rb::ReflecBeatTouchDeviceHandle::ReflecBeatTouchDeviceHandle(bool log_fps) : log_fps(log_fps) { } void games::rb::ReflecBeatTouchDeviceHandle::grid_insert(unsigned char *data, int cursor_x, int cursor_y) { - // scale to grid position - there are 48 columns and 76 rows of IR sensors. - // for whatever reason, the last y row (#75) results in weird input few rows above; just drop it - int grid_x = CLAMP((cursor_x * 48) / window_width, 0, 47); - int grid_y = CLAMP((cursor_y * 76) / window_height, 0, 74); + // read() contracts X uniformly so screen edges land within usable sensors 2..45 + int grid_x = CLAMP( + (cursor_x * X_SENSOR_COUNT) / window_width, + 0, + X_SENSOR_COUNT - 1); + int grid_y = CLAMP( + (cursor_y * Y_SENSOR_COUNT) / window_height, + 0, + Y_SENSOR_COUNT - 1); // get bit positions - int bit_x = 88 + grid_x; - int bit_y = 74 - grid_y; + int bit_x = X_SENSOR_FIRST_BIT + grid_x; + int bit_y = Y_SENSOR_FIRST_BIT - grid_y; - // insert bits - data[3 + bit_x / 8] |= (char) 1 << (bit_x % 8); - data[3 + bit_y / 8] |= (char) 1 << (bit_y % 8); + packet_set_bit(data, bit_x); + packet_set_bit(data, bit_y); } bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) { @@ -71,16 +84,13 @@ bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) { // update window SetWindowPos(wnd, nullptr, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER); + } - // create touch window - touch_create_wnd(wnd); + // create touch window + touch_create_wnd(wnd); - } else { - - // create touch window - touch_create_wnd(wnd); - - // show game window because it lost focus + // show fullscreen game window because it lost focus + if (!GRAPHICS_WINDOWED) { ShowWindow(wnd, SW_SHOW); } @@ -98,13 +108,15 @@ bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) { ShowCursor(TRUE); } + touch_debug_attach(); + return true; } int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) { // check buffer size - if (nNumberOfBytesToRead < 20) { + if (nNumberOfBytesToRead < TOUCH_PACKET_SIZE) { return 0; } @@ -130,17 +142,17 @@ int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberO } // create data - unsigned char data[20] {}; + unsigned char data[TOUCH_PACKET_SIZE] {}; // data header data[0] = 0x55; data[2] = 0x4C; - const auto SCALE_FACTOR = games::rb::TOUCH_SCALING / 1000.f; + const float scale_factor = games::rb::TOUCH_SCALING / (float) games::rb::TOUCH_SCALE_DEFAULT; // iterate all touch points - auto offset_x = (int) (window_width / 48.0 / 3.0); - auto offset_y = (int) (window_height / 76.0 / 3.0); + int offset_x = (int) (window_width / (double) X_SENSOR_COUNT / TOUCH_POINT_OFFSET_DIVISOR); + int offset_y = (int) (window_height / (double) Y_SENSOR_COUNT / TOUCH_POINT_OFFSET_DIVISOR); std::vector touch_points; touch_get_points(touch_points); for (auto &point : touch_points) { @@ -156,18 +168,23 @@ int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberO } // apply scaling - x = x - (window_width * (1.f - SCALE_FACTOR) / 2); - x = x / SCALE_FACTOR; - y = y - (window_height * (1.f - SCALE_FACTOR) / 2); - y = y / SCALE_FACTOR; + x = x - (window_width * (1.f - scale_factor) / 2); + x = x / scale_factor; + y = y - (window_height * (1.f - scale_factor) / 2); + y = y / scale_factor; if (x < 0 || window_width <= x || y < 0 || window_height <= y) { continue; } - // point scaling - const auto point_x = (int) ((x - window_width / 2.0) * 48.0 / 54.0 + window_width / 2.0); - const auto point_y = (int) (y - window_height / 76); + // point coordinates + // keep the verified left edge fixed while adding up to one-third of a beam + // at the right edge, preventing the finger footprint from including sensor 44 + const auto point_x = (int) ( + (x - window_width / 2.0) * X_SENSOR_COUNT / X_INPUT_COORDINATE_COUNT + + window_width / 2.0 + + x / (X_SENSOR_COUNT * TOUCH_POINT_OFFSET_DIVISOR)); + const auto point_y = (int) y; // model a finger as a 3x3 block of IR sensors around the touch point // this gives better accuracy (than just 1x1) since the logic below @@ -185,8 +202,10 @@ int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberO grid_insert(data, point_x, point_y + offset_y); // south } + touch_debug_publish(data, is_landscape); + // copy data to buffer - memcpy(lpBuffer, data, 20); + memcpy(lpBuffer, data, TOUCH_PACKET_SIZE); // update frame logging if (log_fps) { @@ -201,7 +220,7 @@ int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberO } // return amount of bytes written - return 20; + return TOUCH_PACKET_SIZE; } int games::rb::ReflecBeatTouchDeviceHandle::write(LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite) { @@ -215,6 +234,8 @@ int games::rb::ReflecBeatTouchDeviceHandle::device_io(DWORD dwIoControlCode, LPV bool games::rb::ReflecBeatTouchDeviceHandle::close() { + touch_debug_detach(); + // detach touch module touch_detach(); diff --git a/src/spice2x/games/rb/touch_debug.cpp b/src/spice2x/games/rb/touch_debug.cpp new file mode 100644 index 0000000..90d49e4 --- /dev/null +++ b/src/spice2x/games/rb/touch_debug.cpp @@ -0,0 +1,139 @@ +#include "touch_debug.h" + +#include +#include +#include +#include + +#include "external/imgui/imgui.h" +#include "games/rb/rb.h" +#include "games/rb/touch_defs.h" + +namespace games::rb { + + struct TouchDebugState { + std::array packet {}; + bool is_landscape = false; + }; + + std::atomic_bool TOUCH_DEBUG_OVERLAY = false; + static std::atomic_bool TOUCH_ACTIVE = false; + static std::mutex TOUCH_DEBUG_STATE_M; + static TouchDebugState TOUCH_DEBUG_STATE; + + static float touch_scale_factor() { + return TOUCH_SCALING / (float) TOUCH_SCALE_DEFAULT; + } + + static void clear_touch_debug_state() { + std::lock_guard lock(TOUCH_DEBUG_STATE_M); + TOUCH_DEBUG_STATE = {}; + } + + static TouchDebugState get_touch_debug_state() { + std::lock_guard lock(TOUCH_DEBUG_STATE_M); + return TOUCH_DEBUG_STATE; + } + + static bool packet_bit_active( + const std::array &packet, int bit) { + return (packet[TOUCH_PACKET_DATA_OFFSET + bit / 8] & (1u << (bit % 8))) != 0; + } + + static int sensor_center(int sensor, int sensor_count, int extent) { + return ((sensor * 2 + 1) * extent) / (sensor_count * 2); + } + + bool touch_debug_overlay_enabled() { + return TOUCH_DEBUG_OVERLAY && TOUCH_ACTIVE.load(std::memory_order_acquire); + } + + void touch_draw_debug_overlay() { + if (!touch_debug_overlay_enabled()) { + return; + } + + const auto &io = ImGui::GetIO(); + int width = static_cast(io.DisplaySize.x); + int height = static_cast(io.DisplaySize.y); + if (width <= 0 || height <= 0) { + return; + } + + const float scale_factor = touch_scale_factor(); + const float left = width * (1.f - scale_factor) / 2.f; + const float top = height * (1.f - scale_factor) / 2.f; + const float right = width - left; + const float bottom = height - top; + + TouchDebugState state = get_touch_debug_state(); + ImDrawList *draw_list = ImGui::GetBackgroundDrawList(); + auto draw_line = [&](float x1, float y1, float x2, float y2) { + draw_list->AddLine( + ImVec2(x1, y1), ImVec2(x2, y2), + IM_COL32(0, 255, 64, 255), 2.f); + }; + + // show the valid input area when touch scaling restricts it + if (TOUCH_SCALING != TOUCH_SCALE_DEFAULT) { + draw_list->AddRect( + ImVec2(left, top), ImVec2(right, bottom), + IM_COL32(255, 255, 255, 255), 0.f, 0, 2.f); + } + + // spread the usable X sensor centers 2..45 across the full touch area + for (int sensor = X_SENSOR_FIRST_ACTIVE; sensor <= X_SENSOR_LAST_ACTIVE; sensor++) { + if (!packet_bit_active(state.packet, X_SENSOR_FIRST_BIT + sensor)) { + continue; + } + + int position = sensor_center( + sensor - X_SENSOR_FIRST_ACTIVE, X_SENSOR_ACTIVE_COUNT, + state.is_landscape ? height : width); + if (state.is_landscape) { + float y = top + position * scale_factor; + draw_line(left, y, right, y); + } else { + float x = left + position * scale_factor; + draw_line(x, top, x, bottom); + } + } + + for (int sensor = 0; sensor < Y_SENSOR_COUNT; sensor++) { + if (!packet_bit_active(state.packet, Y_SENSOR_FIRST_BIT - sensor)) { + continue; + } + + int position = sensor_center( + sensor, Y_SENSOR_COUNT, + state.is_landscape ? width : height); + if (state.is_landscape) { + float x = right - position * scale_factor; + draw_line(x, top, x, bottom); + } else { + float y = top + position * scale_factor; + draw_line(left, y, right, y); + } + } + } + + void touch_debug_attach() { + clear_touch_debug_state(); + TOUCH_ACTIVE.store(true, std::memory_order_release); + } + + void touch_debug_detach() { + TOUCH_ACTIVE.store(false, std::memory_order_release); + clear_touch_debug_state(); + } + + void touch_debug_publish(const unsigned char *data, bool is_landscape) { + if (!TOUCH_DEBUG_OVERLAY) { + return; + } + + std::lock_guard lock(TOUCH_DEBUG_STATE_M); + memcpy(TOUCH_DEBUG_STATE.packet.data(), data, TOUCH_PACKET_SIZE); + TOUCH_DEBUG_STATE.is_landscape = is_landscape; + } +} diff --git a/src/spice2x/games/rb/touch_debug.h b/src/spice2x/games/rb/touch_debug.h new file mode 100644 index 0000000..1abd14d --- /dev/null +++ b/src/spice2x/games/rb/touch_debug.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +namespace games::rb { + extern std::atomic_bool TOUCH_DEBUG_OVERLAY; + + bool touch_debug_overlay_enabled(); + void touch_draw_debug_overlay(); + + void touch_debug_attach(); + void touch_debug_detach(); + void touch_debug_publish(const unsigned char *data, bool is_landscape); +} diff --git a/src/spice2x/games/rb/touch_defs.h b/src/spice2x/games/rb/touch_defs.h new file mode 100644 index 0000000..f0c2474 --- /dev/null +++ b/src/spice2x/games/rb/touch_defs.h @@ -0,0 +1,17 @@ +#pragma once + +namespace games::rb { + inline constexpr int TOUCH_SCALE_DEFAULT = 1000; + inline constexpr int TOUCH_PACKET_SIZE = 20; + inline constexpr int TOUCH_PACKET_DATA_OFFSET = 3; + + inline constexpr int X_SENSOR_COUNT = 48; + inline constexpr int X_SENSOR_FIRST_ACTIVE = 2; + inline constexpr int X_SENSOR_LAST_ACTIVE = 45; + inline constexpr int X_SENSOR_ACTIVE_COUNT = + X_SENSOR_LAST_ACTIVE - X_SENSOR_FIRST_ACTIVE + 1; + inline constexpr int X_SENSOR_FIRST_BIT = 88; + + inline constexpr int Y_SENSOR_COUNT = 76; + inline constexpr int Y_SENSOR_FIRST_BIT = 75; +} diff --git a/src/spice2x/launcher/launcher.cpp b/src/spice2x/launcher/launcher.cpp index 4bd03e3..678d2e7 100644 --- a/src/spice2x/launcher/launcher.cpp +++ b/src/spice2x/launcher/launcher.cpp @@ -49,6 +49,7 @@ #include "games/popn/popn.h" #include "games/qma/qma.h" #include "games/rb/rb.h" +#include "games/rb/touch_debug.h" #include "games/rf3d/rf3d.h" #include "games/sc/sc.h" #include "games/scotto/scotto.h" @@ -1339,6 +1340,7 @@ int main_implementation(int argc, char *argv[]) { } // reflec beat touch emulation + games::rb::TOUCH_DEBUG_OVERLAY = options[launcher::Options::RBTouchDebug].value_bool(); 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 d4d3414..cbc7572 100644 --- a/src/spice2x/launcher/options.cpp +++ b/src/spice2x/launcher/options.cpp @@ -2739,6 +2739,18 @@ static const std::vector OPTION_DEFINITIONS = { .category = "Game Options", .quick_setting_category = "Game", }, + { + // RBTouchDebug + .title = "RB Touch Debug Overlay", + .name = "rbtouchdebug", + .desc = "Draw lines to show IR sensor emulation state.\n\n" + "Note: lines will not perfectly align with touches; this is by design, it's showing the " + "IR sensor state, before the game applies its own touch detection algorithm.", + .type = OptionType::Bool, + .game_name = "Reflec Beat", + .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 67504e6..a9f51aa 100644 --- a/src/spice2x/launcher/options.h +++ b/src/spice2x/launcher/options.h @@ -265,6 +265,7 @@ namespace launcher { JubeatTouchAlgo, JubeatTouchDebug, JubeatTouchDebounce, + RBTouchDebug, spice2x_RBTouchScale, RBTouchSize, RBTouchPollRate, diff --git a/src/spice2x/overlay/overlay.cpp b/src/spice2x/overlay/overlay.cpp index 6ffd097..e994713 100644 --- a/src/spice2x/overlay/overlay.cpp +++ b/src/spice2x/overlay/overlay.cpp @@ -6,6 +6,7 @@ #include "games/gitadora/gitadora.h" #include "games/iidx/iidx.h" #include "games/popn/popn.h" +#include "games/rb/touch_debug.h" #include "hooks/graphics/graphics.h" #include "misc/eamuse.h" #include "touch/touch.h" @@ -551,9 +552,13 @@ void overlay::SpiceOverlay::new_frame() { const bool draw_fps_persistent = this->renderer != OverlayRenderer::SOFTWARE && this->window_fps->get_active(); + // draw RB touch diagnostics + const bool draw_rb_touch_debug = this->renderer != OverlayRenderer::SOFTWARE + && games::rb::touch_debug_overlay_enabled(); + // check if there is nothing to draw this->has_pending_frame = false; - if (!this->active && !draw_notifications && !draw_fps_persistent) { + if (!this->active && !draw_notifications && !draw_fps_persistent && !draw_rb_touch_debug) { return; } @@ -578,6 +583,10 @@ void overlay::SpiceOverlay::new_frame() { ImGui::NewFrame(); this->has_pending_frame = true; + if (draw_rb_touch_debug) { + games::rb::touch_draw_debug_overlay(); + } + // build windows only when the overlay itself is active if (this->active) { for (auto &window : this->windows) { @@ -595,6 +604,7 @@ void overlay::SpiceOverlay::new_frame() { if (draw_fps_persistent) { this->window_fps->build(); } + // draw notifications last so they paint on top of any overlay windows if (draw_notifications) { overlay::notifications::draw();