touch: rawinput touch to compensate for letterboxing (#790)

## Link to GitHub Issue or related Pull Request, if one exists

Fixes #697

## Description of change

Adds aspect-ratio compensation for raw input touch handler.

This addresses the problem with playing rawinput touch games - namely
jubeat and reflec beat - on a display that is not 16:9, and user chose
to preserve the aspect ratio. This happens a lot on:

* 16:10 touch screens
* touch screen laptops (which range from 3:2, 16:10, and so on).

Root cause is a **Windows Raw Input limitation**. Unlike
`WM_POINTER`/`WM_TOUCH`, where the OS already maps digitizer coordinates
to screen space (applying the display's calibration/scaling), Raw Input
delivers the HID digitizer's *raw* logical coordinates, spanning the
whole physical panel, with **no calibration or display-mapping data
attached**. We have to reconstruct that mapping ourselves.

When a game runs at a display mode whose aspect ratio differs from the
panel's native resolution, Windows scales the image with
letterboxing/pillarboxing (black bars). That is standard OS/GPU
behavior. Because Raw Input gives us no mapping to account for those
bars, touches land off-target. This change detects the mismatch and
remaps each touch from panel-space into the displayed image region;
touches inside the black bars are treated as released.

Adds new launcher option **`-rawtouchaspect`**.

Notes:

- Native resolution is the largest-area mode from `EnumDisplaySettings`
on the **primary** display, consistent with the existing primary-only
touch pipeline.
- Math assumes centered, aspect-preserving scaling; only one axis is
ever inset.
- A small epsilon (0.01) makes near-matched panels (e.g. 1366×768 vs
true 16:9) a no-op.
- If the user forces a stretched image, then the touches will be off
again. In this case, the user needs to use the `force off` setting.

## Testing

On my tiny Surface Go tablet, 
* checked that jubeat squares align correctly
* good enough to full combo level 11 for reflec beat
This commit is contained in:
bicarus
2026-07-10 23:23:45 -07:00
committed by GitHub
parent 8d04eba21d
commit c3fcf2f417
7 changed files with 196 additions and 13 deletions
+4
View File
@@ -7,6 +7,7 @@
#include "cfg/configurator.h" #include "cfg/configurator.h"
#include "hooks/graphics/graphics.h" #include "hooks/graphics/graphics.h"
#include "touch/touch.h" #include "touch/touch.h"
#include "rawinput/touch.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/utils.h" #include "util/utils.h"
#include "util/detour.h" #include "util/detour.h"
@@ -57,6 +58,9 @@ namespace games::jb {
log_info("jubeat", "using window handle for touch: {}", fmt::ptr(wnd)); log_info("jubeat", "using window handle for touch: {}", fmt::ptr(wnd));
touch_create_wnd(wnd, true); touch_create_wnd(wnd, true);
// request automatic aspect ratio fixes
::rawinput::touch::ASPECT_COMPENSATION_GAME = true;
// show cursor // show cursor
if (GRAPHICS_SHOW_CURSOR) { if (GRAPHICS_SHOW_CURSOR) {
ShowCursor(TRUE); ShowCursor(TRUE);
+5
View File
@@ -4,6 +4,7 @@
#include "avs/game.h" #include "avs/game.h"
#include "hooks/graphics/graphics.h" #include "hooks/graphics/graphics.h"
#include "rawinput/touch.h"
#include "touch/touch.h" #include "touch/touch.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/time.h" #include "util/time.h"
@@ -80,6 +81,10 @@ bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
// show game window because it lost focus // show game window because it lost focus
ShowWindow(wnd, SW_SHOW); ShowWindow(wnd, SW_SHOW);
} }
// request automatic aspect ratio fixes
::rawinput::touch::ASPECT_COMPENSATION_GAME = true;
} else { } else {
// fallback to dx hook // fallback to dx hook
+9
View File
@@ -1064,6 +1064,15 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::InvertTouchCoordinates].value_bool()) { if (options[launcher::Options::InvertTouchCoordinates].value_bool()) {
rawinput::touch::INVERTED = true; rawinput::touch::INVERTED = true;
} }
if (options[launcher::Options::RawInputTouchAspectRatio].is_active()) {
auto mode = options[launcher::Options::RawInputTouchAspectRatio].value_text();
if (mode == "on") {
rawinput::touch::ASPECT_COMPENSATION_MODE = rawinput::touch::AspectMode::On;
} else if (mode == "off") {
rawinput::touch::ASPECT_COMPENSATION_MODE = rawinput::touch::AspectMode::Off;
}
// "auto" leaves the default (per-game)
}
// DisableTouchCardInsert is no longer honored in spice2x // DisableTouchCardInsert is no longer honored in spice2x
// if (options[launcher::Options::DisableTouchCardInsert].value_bool()) { // if (options[launcher::Options::DisableTouchCardInsert].value_bool()) {
// SPICETOUCH_CARD_DISABLE = true; // SPICETOUCH_CARD_DISABLE = true;
+18
View File
@@ -1801,6 +1801,24 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.type = OptionType::Bool, .type = OptionType::Bool,
.category = "Touch Parameters", .category = "Touch Parameters",
}, },
{
// RawInputTouchAspectRatio
.title = "Raw Input Touch Fix Aspect Ratio",
.name = "rawtouchaspect",
.desc = "Compensate for letterboxing/pillarboxing when the game runs at a display mode with a "
"different aspect ratio than the touch panel's native resolution. Only affects the default "
"raw input touch handler.\n\n"
"auto: enable on a per-game basis (jubeat, reflec).\n"
"on: forced on\n"
"off: forced off",
.type = OptionType::Enum,
.category = "Touch Parameters",
.elements = {
{"auto", ""},
{"on", ""},
{"off", ""},
},
},
{ {
// DisableTouchCardInsert // DisableTouchCardInsert
.title = "Disable Touch Card Insert (DEPRECATED - use -touchcard instead)", .title = "Disable Touch Card Insert (DEPRECATED - use -touchcard instead)",
+1
View File
@@ -179,6 +179,7 @@ namespace launcher {
ForceWinTouch, ForceWinTouch,
ForceTouchEmulation, ForceTouchEmulation,
InvertTouchCoordinates, InvertTouchCoordinates,
RawInputTouchAspectRatio,
DisableTouchCardInsert, DisableTouchCardInsert,
spice2x_TouchCardInsert, spice2x_TouchCardInsert,
ICCAReaderPort, ICCAReaderPort,
+140 -13
View File
@@ -24,13 +24,116 @@ namespace rawinput::touch {
// settings // settings
bool DISABLED = false; bool DISABLED = false;
bool INVERTED = false; bool INVERTED = false;
AspectMode ASPECT_COMPENSATION_MODE = AspectMode::Auto;
bool ASPECT_COMPENSATION_GAME = false;
// state // state
DWORD DISPLAY_ORIENTATION = DMDO_DEFAULT; DWORD DISPLAY_ORIENTATION = DMDO_DEFAULT;
long DISPLAY_SIZE_X = 1920L; long DISPLAY_SIZE_X = 1920L;
long DISPLAY_SIZE_Y = 1080L; long DISPLAY_SIZE_Y = 1080L;
long DISPLAY_NATIVE_X = 0L;
long DISPLAY_NATIVE_Y = 0L;
bool DISPLAY_INITIALIZED = false; bool DISPLAY_INITIALIZED = false;
bool aspect_compensation_enabled() {
switch (ASPECT_COMPENSATION_MODE) {
case AspectMode::On:
return true;
case AspectMode::Off:
return false;
default:
return ASPECT_COMPENSATION_GAME;
}
}
// compute the centered letterbox inset (fraction cropped from each side) that results
// from showing the current display mode on a panel with a different native aspect
// ratio. only one of inset_x / inset_y is ever non-zero.
static void compute_letterbox_inset(double &inset_x, double &inset_y) {
inset_x = 0.0;
inset_y = 0.0;
// native resolution in the current display orientation
long native_x = DISPLAY_NATIVE_X;
long native_y = DISPLAY_NATIVE_Y;
if ((DISPLAY_SIZE_Y >= DISPLAY_SIZE_X) != (native_y >= native_x)) {
long tmp = native_x;
native_x = native_y;
native_y = tmp;
}
// compare the displayed aspect ratio against the panel's native one; ignore
// tiny mismatches (e.g. 1366x768 vs true 16:9) that would only add a sub-pixel
// inset, so near-matched panels stay an exact no-op
double current_aspect = (double) DISPLAY_SIZE_X / (double) DISPLAY_SIZE_Y;
double native_aspect = (double) native_x / (double) native_y;
constexpr double aspect_epsilon = 0.01;
if (current_aspect < native_aspect - aspect_epsilon) {
// image is narrower than the panel -> bars on left/right
inset_x = (1.0 - current_aspect / native_aspect) / 2.0;
} else if (current_aspect > native_aspect + aspect_epsilon) {
// image is wider than the panel -> bars on top/bottom
inset_y = (1.0 - native_aspect / current_aspect) / 2.0;
}
}
// undo letterboxing: remap the normalized (0.0 - 1.0) digitizer coordinates, which span
// the whole physical panel, into the displayed image region. returns false when the
// touch lands in the black bars (outside the image) and should be ignored.
static bool remap_aspect_compensation(double &norm_x, double &norm_y) {
if (DISPLAY_NATIVE_X <= 0 || DISPLAY_NATIVE_Y <= 0) {
return true;
}
double inset_x, inset_y;
compute_letterbox_inset(inset_x, inset_y);
// log once the first time the compensation actually engages (non-zero inset)
static bool logged_active = false;
if (!logged_active && (inset_x > 0.0 || inset_y > 0.0)) {
logged_active = true;
log_info("rawinput", "aspect compensation active: display {}x{}, native {}x{}, inset x={:.4f} y={:.4f}",
(int) DISPLAY_SIZE_X, (int) DISPLAY_SIZE_Y,
(int) DISPLAY_NATIVE_X, (int) DISPLAY_NATIVE_Y,
inset_x, inset_y);
}
// ignore touches inside the black bars (outside the displayed image)
if ((inset_x > 0.0 && (norm_x < inset_x || norm_x > 1.0 - inset_x)) ||
(inset_y > 0.0 && (norm_y < inset_y || norm_y > 1.0 - inset_y))) {
return false;
}
// remap the displayed image region back to the full 0.0 - 1.0 range
if (inset_x > 0.0) {
norm_x = (norm_x - inset_x) / (1.0 - 2.0 * inset_x);
}
if (inset_y > 0.0) {
norm_y = (norm_y - inset_y) / (1.0 - 2.0 * inset_y);
}
return true;
}
// determine the native (maximum) resolution advertised by the primary display device.
// used to detect and undo letterboxing when a non-native display mode is in use.
// falls back to the current display size if enumeration yields nothing larger.
static void detect_native_resolution() {
DISPLAY_NATIVE_X = DISPLAY_SIZE_X;
DISPLAY_NATIVE_Y = DISPLAY_SIZE_Y;
DEVMODE native_mode{};
native_mode.dmSize = sizeof(native_mode);
for (int i = 0; EnumDisplaySettings(nullptr, i, &native_mode); i++) {
if ((long) native_mode.dmPelsWidth * (long) native_mode.dmPelsHeight >
DISPLAY_NATIVE_X * DISPLAY_NATIVE_Y) {
DISPLAY_NATIVE_X = (long) native_mode.dmPelsWidth;
DISPLAY_NATIVE_Y = (long) native_mode.dmPelsHeight;
}
}
}
bool is_touchscreen(Device *device) { bool is_touchscreen(Device *device) {
// check if disabled // check if disabled
@@ -322,6 +425,18 @@ namespace rawinput::touch {
std::vector<DWORD> touch_removes; std::vector<DWORD> touch_removes;
std::vector<TouchPoint> touch_writes; std::vector<TouchPoint> touch_writes;
std::vector<DWORD> touch_modifications; std::vector<DWORD> touch_modifications;
// drop every touch point with the given id (marking it as released)
auto remove_touch_point = [&] (DWORD id) {
touch_removes.push_back(id);
touch.touch_points.erase(std::remove_if(
touch.touch_points.begin(),
touch.touch_points.end(),
[id] (const HIDTouchPoint &x) {
return x.id == id;
}), touch.touch_points.end());
};
for (auto &hid_tp : touch_points) { for (auto &hid_tp : touch_points) {
// check if existing // check if existing
@@ -353,23 +468,29 @@ namespace rawinput::touch {
// only remove if it exists // only remove if it exists
if (existing != touch.touch_points.end()) { if (existing != touch.touch_points.end()) {
remove_touch_point(hid_tp.id);
// remove all touch points with this ID
touch_removes.push_back(hid_tp.id);
touch.touch_points.erase(std::remove_if(
touch.touch_points.begin(),
touch.touch_points.end(),
[hid_tp] (const HIDTouchPoint &x) {
return x.id == hid_tp.id;
}), touch.touch_points.end());
} }
} else { } else {
// write touch point // start from the normalized (0.0 - 1.0) digitizer coordinates
double norm_x = hid_tp.x;
double norm_y = hid_tp.y;
// undo letterboxing so raw input touches line up with the displayed
// image; touches that land in the black bars are treated as released
if (aspect_compensation_enabled() && !remap_aspect_compensation(norm_x, norm_y)) {
if (existing != touch.touch_points.end()) {
remove_touch_point(hid_tp.id);
}
continue;
}
// scale the normalized coordinates to the monitor to obtain the
// physical screen position of the touch
TouchPoint tp { TouchPoint tp {
.id = hid_tp.id, .id = hid_tp.id,
.x = (long) (hid_tp.x * DISPLAY_SIZE_X), .x = (long) (norm_x * DISPLAY_SIZE_X),
.y = (long) (hid_tp.y * DISPLAY_SIZE_Y), .y = (long) (norm_y * DISPLAY_SIZE_Y),
.mouse = false, .mouse = false,
}; };
touch_writes.push_back(tp); touch_writes.push_back(tp);
@@ -513,7 +634,13 @@ namespace rawinput::touch {
GetWindowRect(GetDesktopWindow(), &display_rect); GetWindowRect(GetDesktopWindow(), &display_rect);
DISPLAY_SIZE_X = display_rect.right - display_rect.left; DISPLAY_SIZE_X = display_rect.right - display_rect.left;
DISPLAY_SIZE_Y = display_rect.bottom - display_rect.top; DISPLAY_SIZE_Y = display_rect.bottom - display_rect.top;
log_info("rawinput", "display size: {}x{}", (int) DISPLAY_SIZE_X, (int) DISPLAY_SIZE_Y); log_info("rawinput", "primary display size: {}x{}", (int) DISPLAY_SIZE_X, (int) DISPLAY_SIZE_Y);
// determine the native (maximum) resolution of the primary display; used to
// detect and undo letterboxing when a non-native display mode is in use
detect_native_resolution();
log_info("rawinput", "primary display native size: {}x{}",
(int) DISPLAY_NATIVE_X, (int) DISPLAY_NATIVE_Y);
// determine monitor orientation // determine monitor orientation
DEVMODE display_mode{}; DEVMODE display_mode{};
+19
View File
@@ -9,6 +9,25 @@ namespace rawinput::touch {
extern bool DISABLED; extern bool DISABLED;
extern bool INVERTED; extern bool INVERTED;
// aspect-ratio compensation: when the current display mode has a different aspect
// ratio than the panel's native resolution, the image is letterboxed/pillarboxed
// on the physical panel while the digitizer still spans the whole panel. this
// remaps the touch into the displayed image region.
enum class AspectMode {
Auto, // decided per game via ASPECT_COMPENSATION_GAME
On, // always compensate
Off, // never compensate
};
// global override, driven by the -rawtouchaspect launcher option
extern AspectMode ASPECT_COMPENSATION_MODE;
// per-game request, honored only when ASPECT_COMPENSATION_MODE is Auto
extern bool ASPECT_COMPENSATION_GAME;
// effective decision combining the global mode and the per-game request
bool aspect_compensation_enabled();
// global state // global state
extern DWORD DISPLAY_ORIENTATION; extern DWORD DISPLAY_ORIENTATION;
extern long DISPLAY_SIZE_X; extern long DISPLAY_SIZE_X;