mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-01 22:30:42 -07:00
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:
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)",
|
||||||
|
|||||||
@@ -179,6 +179,7 @@ namespace launcher {
|
|||||||
ForceWinTouch,
|
ForceWinTouch,
|
||||||
ForceTouchEmulation,
|
ForceTouchEmulation,
|
||||||
InvertTouchCoordinates,
|
InvertTouchCoordinates,
|
||||||
|
RawInputTouchAspectRatio,
|
||||||
DisableTouchCardInsert,
|
DisableTouchCardInsert,
|
||||||
spice2x_TouchCardInsert,
|
spice2x_TouchCardInsert,
|
||||||
ICCAReaderPort,
|
ICCAReaderPort,
|
||||||
|
|||||||
+140
-13
@@ -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{};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user