mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-01 14:20: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 "hooks/graphics/graphics.h"
|
||||
#include "touch/touch.h"
|
||||
#include "rawinput/touch.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/utils.h"
|
||||
#include "util/detour.h"
|
||||
@@ -57,6 +58,9 @@ namespace games::jb {
|
||||
log_info("jubeat", "using window handle for touch: {}", fmt::ptr(wnd));
|
||||
touch_create_wnd(wnd, true);
|
||||
|
||||
// request automatic aspect ratio fixes
|
||||
::rawinput::touch::ASPECT_COMPENSATION_GAME = true;
|
||||
|
||||
// show cursor
|
||||
if (GRAPHICS_SHOW_CURSOR) {
|
||||
ShowCursor(TRUE);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "rawinput/touch.h"
|
||||
#include "touch/touch.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/time.h"
|
||||
@@ -80,6 +81,10 @@ bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
|
||||
// show game window because it lost focus
|
||||
ShowWindow(wnd, SW_SHOW);
|
||||
}
|
||||
|
||||
// request automatic aspect ratio fixes
|
||||
::rawinput::touch::ASPECT_COMPENSATION_GAME = true;
|
||||
|
||||
} else {
|
||||
|
||||
// fallback to dx hook
|
||||
|
||||
@@ -1064,6 +1064,15 @@ int main_implementation(int argc, char *argv[]) {
|
||||
if (options[launcher::Options::InvertTouchCoordinates].value_bool()) {
|
||||
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
|
||||
// if (options[launcher::Options::DisableTouchCardInsert].value_bool()) {
|
||||
// SPICETOUCH_CARD_DISABLE = true;
|
||||
|
||||
@@ -1801,6 +1801,24 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
.type = OptionType::Bool,
|
||||
.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
|
||||
.title = "Disable Touch Card Insert (DEPRECATED - use -touchcard instead)",
|
||||
|
||||
@@ -179,6 +179,7 @@ namespace launcher {
|
||||
ForceWinTouch,
|
||||
ForceTouchEmulation,
|
||||
InvertTouchCoordinates,
|
||||
RawInputTouchAspectRatio,
|
||||
DisableTouchCardInsert,
|
||||
spice2x_TouchCardInsert,
|
||||
ICCAReaderPort,
|
||||
|
||||
+140
-13
@@ -24,13 +24,116 @@ namespace rawinput::touch {
|
||||
// settings
|
||||
bool DISABLED = false;
|
||||
bool INVERTED = false;
|
||||
AspectMode ASPECT_COMPENSATION_MODE = AspectMode::Auto;
|
||||
bool ASPECT_COMPENSATION_GAME = false;
|
||||
|
||||
// state
|
||||
DWORD DISPLAY_ORIENTATION = DMDO_DEFAULT;
|
||||
long DISPLAY_SIZE_X = 1920L;
|
||||
long DISPLAY_SIZE_Y = 1080L;
|
||||
long DISPLAY_NATIVE_X = 0L;
|
||||
long DISPLAY_NATIVE_Y = 0L;
|
||||
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) {
|
||||
|
||||
// check if disabled
|
||||
@@ -322,6 +425,18 @@ namespace rawinput::touch {
|
||||
std::vector<DWORD> touch_removes;
|
||||
std::vector<TouchPoint> touch_writes;
|
||||
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) {
|
||||
|
||||
// check if existing
|
||||
@@ -353,23 +468,29 @@ namespace rawinput::touch {
|
||||
|
||||
// only remove if it exists
|
||||
if (existing != touch.touch_points.end()) {
|
||||
|
||||
// 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());
|
||||
remove_touch_point(hid_tp.id);
|
||||
}
|
||||
} 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 {
|
||||
.id = hid_tp.id,
|
||||
.x = (long) (hid_tp.x * DISPLAY_SIZE_X),
|
||||
.y = (long) (hid_tp.y * DISPLAY_SIZE_Y),
|
||||
.x = (long) (norm_x * DISPLAY_SIZE_X),
|
||||
.y = (long) (norm_y * DISPLAY_SIZE_Y),
|
||||
.mouse = false,
|
||||
};
|
||||
touch_writes.push_back(tp);
|
||||
@@ -513,7 +634,13 @@ namespace rawinput::touch {
|
||||
GetWindowRect(GetDesktopWindow(), &display_rect);
|
||||
DISPLAY_SIZE_X = display_rect.right - display_rect.left;
|
||||
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
|
||||
DEVMODE display_mode{};
|
||||
|
||||
@@ -9,6 +9,25 @@ namespace rawinput::touch {
|
||||
extern bool DISABLED;
|
||||
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
|
||||
extern DWORD DISPLAY_ORIENTATION;
|
||||
extern long DISPLAY_SIZE_X;
|
||||
|
||||
Reference in New Issue
Block a user