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)
This commit is contained in:
bicarus
2026-07-13 23:30:41 -07:00
committed by GitHub
parent a2d8f60d12
commit 1a1be7a393
9 changed files with 252 additions and 35 deletions
+1
View File
@@ -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
+55 -34
View File
@@ -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<unsigned char>(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<TouchPoint> 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();
+139
View File
@@ -0,0 +1,139 @@
#include "touch_debug.h"
#include <array>
#include <atomic>
#include <cstring>
#include <mutex>
#include "external/imgui/imgui.h"
#include "games/rb/rb.h"
#include "games/rb/touch_defs.h"
namespace games::rb {
struct TouchDebugState {
std::array<unsigned char, TOUCH_PACKET_SIZE> 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<std::mutex> lock(TOUCH_DEBUG_STATE_M);
TOUCH_DEBUG_STATE = {};
}
static TouchDebugState get_touch_debug_state() {
std::lock_guard<std::mutex> lock(TOUCH_DEBUG_STATE_M);
return TOUCH_DEBUG_STATE;
}
static bool packet_bit_active(
const std::array<unsigned char, TOUCH_PACKET_SIZE> &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<int>(io.DisplaySize.x);
int height = static_cast<int>(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<std::mutex> lock(TOUCH_DEBUG_STATE_M);
memcpy(TOUCH_DEBUG_STATE.packet.data(), data, TOUCH_PACKET_SIZE);
TOUCH_DEBUG_STATE.is_landscape = is_landscape;
}
}
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include <atomic>
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);
}
+17
View File
@@ -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;
}
+2
View File
@@ -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();
}
+12
View File
@@ -2739,6 +2739,18 @@ static const std::vector<OptionDefinition> 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",
+1
View File
@@ -265,6 +265,7 @@ namespace launcher {
JubeatTouchAlgo,
JubeatTouchDebug,
JubeatTouchDebounce,
RBTouchDebug,
spice2x_RBTouchScale,
RBTouchSize,
RBTouchPollRate,
+11 -1
View File
@@ -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();