diff --git a/src/spice2x/games/jb/jb.cpp b/src/spice2x/games/jb/jb.cpp index c3bf1ca..4351f55 100644 --- a/src/spice2x/games/jb/jb.cpp +++ b/src/spice2x/games/jb/jb.cpp @@ -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); diff --git a/src/spice2x/games/rb/touch.cpp b/src/spice2x/games/rb/touch.cpp index be134d6..f14526c 100644 --- a/src/spice2x/games/rb/touch.cpp +++ b/src/spice2x/games/rb/touch.cpp @@ -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 diff --git a/src/spice2x/launcher/launcher.cpp b/src/spice2x/launcher/launcher.cpp index 5d3083f..2cadb04 100644 --- a/src/spice2x/launcher/launcher.cpp +++ b/src/spice2x/launcher/launcher.cpp @@ -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; diff --git a/src/spice2x/launcher/options.cpp b/src/spice2x/launcher/options.cpp index bf02992..f8e85b5 100644 --- a/src/spice2x/launcher/options.cpp +++ b/src/spice2x/launcher/options.cpp @@ -1801,6 +1801,24 @@ static const std::vector 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)", diff --git a/src/spice2x/launcher/options.h b/src/spice2x/launcher/options.h index 9e508a7..7e913eb 100644 --- a/src/spice2x/launcher/options.h +++ b/src/spice2x/launcher/options.h @@ -179,6 +179,7 @@ namespace launcher { ForceWinTouch, ForceTouchEmulation, InvertTouchCoordinates, + RawInputTouchAspectRatio, DisableTouchCardInsert, spice2x_TouchCardInsert, ICCAReaderPort, diff --git a/src/spice2x/rawinput/touch.cpp b/src/spice2x/rawinput/touch.cpp index 6a25d07..1859287 100644 --- a/src/spice2x/rawinput/touch.cpp +++ b/src/spice2x/rawinput/touch.cpp @@ -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 touch_removes; std::vector touch_writes; std::vector 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{}; diff --git a/src/spice2x/rawinput/touch.h b/src/spice2x/rawinput/touch.h index 2c527a5..a2f30e7 100644 --- a/src/spice2x/rawinput/touch.h +++ b/src/spice2x/rawinput/touch.h @@ -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;