From bf8e19468536ba84d1e37b4c1e17bd960a81a923 Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Sun, 16 Aug 2026 04:03:42 -0700 Subject: [PATCH] sdvx: fix api touch and -sdvxnosub sub window (#868) ## Link to GitHub Issue or related Pull Request, if one exists Related: #859 and #862. ## Description of change Three SDVX (Valkyrie model) fixes: * **API touch went to the wrong window.** SDVX registers touch on both its Main Screen and Sub Screen windows, and the native injector kept whichever attached last. The touch surface is now published explicitly: sub screen window when windowed, main window in fullscreen. * **Landscape never rotated API coordinates.** Synthetic contacts bypass `transform::hardware_to_game`, so they missed the rotation a real finger gets. Applied before injection now, as portrait already did. Extracted to `sdvx_landscape_rotate` so both paths share it; gated on the native path since `wintouchemu` rotates via the subscreen overlay instead. * **`-sdvxnosub` didn't hide the sub window in fullscreen.** `ShowWindow_hook` had branches for GITADORA, pop'n and IIDX but not SDVX. Added the missing one. ## Testing Tested Nabla --- src/spice2x/api/modules/touch.cpp | 47 +++++++++++++++++++------ src/spice2x/hooks/graphics/graphics.cpp | 27 +++++++++++++- src/spice2x/touch/native/transform.cpp | 25 +++++++------ src/spice2x/touch/native/transform.h | 1 + 4 files changed, 78 insertions(+), 22 deletions(-) diff --git a/src/spice2x/api/modules/touch.cpp b/src/spice2x/api/modules/touch.cpp index eb9d013..217d710 100644 --- a/src/spice2x/api/modules/touch.cpp +++ b/src/spice2x/api/modules/touch.cpp @@ -12,6 +12,7 @@ #include "touch/touch.h" #include "touch/native/inject.h" #include "touch/native/nativetouchhook.h" +#include "touch/native/transform.h" #include "util/utils.h" #include "games/gitadora/gitadora.h" #include "games/iidx/iidx.h" @@ -41,6 +42,35 @@ namespace api::modules { return nativetouch::inject::inject_synthetic_touch(position, true); } + // map API coordinates onto the touch space SDVX reads, which depends on how it is displayed + static void sdvx_touch_errata( + int &x, int &y, bool use_native, int canvas_w, int canvas_h) { + + // windowed coordinates already match the sub screen window they land on + if (GRAPHICS_WINDOWED) { + return; + } + + // landscape mode: native injection hands the game these coordinates + // unchanged, so apply the rotation the touchscreen gets, while wintouchemu instead + // rotates them later through the subscreen overlay + if (GRAPHICS_FS_ORIENTATION_SWAP) { + if (use_native) { + POINT position { x, y }; + if (nativetouch::transform::sdvx_landscape_rotate(&position, canvas_w, canvas_h)) { + x = position.x; + y = position.y; + } + } + return; + } + + // rotate into the portrait touch space + const int x_raw = x; + x = canvas_w - y; + y = x_raw; + } + Touch::Touch() : Module("touch") { is_sdvx = avs::game::is_model("KFC"); @@ -56,8 +86,8 @@ namespace api::modules { native_canvas_w = 0; native_canvas_h = 0; if (is_sdvx) { - // windowed and landscape API coordinates already match the primary screen orientation; - // fullscreen portrait coordinates are rotated by apply_touch_errata + // windowed API coordinates land on the sub screen window as-is; fullscreen + // coordinates are rotated into the game's touch space by apply_touch_errata const bool landscape_coordinates = GRAPHICS_WINDOWED || GRAPHICS_FS_ORIENTATION_SWAP; native_canvas_w = landscape_coordinates ? 1920 : 1080; @@ -217,19 +247,14 @@ namespace api::modules { } void Touch::apply_touch_errata(int &x, int &y) { - int x_raw = x; - int y_raw = y; - if (is_tdj_fhd) { // deal with TDJ FHD resolution mismatch (upgrade 720p to 1080p) // we don't know what screen is being shown on the companion and the API doesn't specify // the target of the touch events so just assume it's the sub screen - x = x_raw * 1920 / 1280; - y = y_raw * 1080 / 720; - } else if (is_sdvx && !GRAPHICS_WINDOWED && !GRAPHICS_FS_ORIENTATION_SWAP) { - // rotate API coordinates into SDVX's portrait touch space - x = 1080 - y_raw; - y = x_raw; + x = x * 1920 / 1280; + y = y * 1080 / 720; + } else if (is_sdvx) { + sdvx_touch_errata(x, y, use_native, native_canvas_w, native_canvas_h); } } } diff --git a/src/spice2x/hooks/graphics/graphics.cpp b/src/spice2x/hooks/graphics/graphics.cpp index 3ddba12..31f1db8 100644 --- a/src/spice2x/hooks/graphics/graphics.cpp +++ b/src/spice2x/hooks/graphics/graphics.cpp @@ -629,8 +629,10 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC } } + const bool is_sdvx = avs::game::is_model("KFC"); bool is_tdj_sub_window = avs::game::is_model("LDJ") && window_name.ends_with(" sub"); - bool is_sdvx_sub_window = avs::game::is_model("KFC") && window_name.ends_with(" Sub Screen"); + bool is_sdvx_sub_window = is_sdvx && window_name.ends_with(" Sub Screen"); + bool is_sdvx_main_window = is_sdvx && window_name.ends_with(" Main Screen"); bool is_popn_sub_window = avs::game::is_model("M39") && window_name.ends_with("Sub Screen"); const std::string gfdm_window_name = games::gitadora::is_arena_model() ? gitadora_canonical_window_name(effective_window_name) @@ -733,6 +735,20 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC graphics_hook_subscreen_window(SDVX_SUBSCREEN_WINDOW); } + // SDVX registers touch on both windows, so name the one synthetic touches must land on + // instead of letting window creation order decide: the sub screen window when windowed, + // the main window in fullscreen since the game reads it in primary-display coordinates + if (nativetouch::is_hooked() && + result != nullptr && + (GRAPHICS_WINDOWED ? is_sdvx_sub_window : is_sdvx_main_window)) { + log_misc( + "graphics", + "SDVX touch surface is {}, {}", + fmt::ptr(result), + window_name); + nativetouch::inject::set_preferred_injection_window(result); + } + // only hook touch window if multiple windows are allowed if (gfdm_window_name == "LEFT" || gfdm_window_name == "RIGHT") { gitadora_remember_window(result, gfdm_window_name); @@ -1122,6 +1138,15 @@ static BOOL WINAPI ShowWindow_hook(HWND hWnd, int nCmdShow) { return true; } + // fullscreen SDVX keeps two adapters so the subscreen overlay can draw, so the game still + // creates the sub window even when the user asked for it to be gone + if (avs::game::is_model("KFC") && + GRAPHICS_PREVENT_SECONDARY_WINDOWS && + hWnd == SDVX_SUBSCREEN_WINDOW) { + log_info("graphics", "ShowWindow_hook - hiding sub window {}", fmt::ptr(hWnd)); + return true; + } + // call original return ShowWindow_orig(hWnd, nCmdShow); } diff --git a/src/spice2x/touch/native/transform.cpp b/src/spice2x/touch/native/transform.cpp index dc2c67d..7dbb7e6 100644 --- a/src/spice2x/touch/native/transform.cpp +++ b/src/spice2x/touch/native/transform.cpp @@ -94,23 +94,28 @@ namespace nativetouch::transform { return overlay::OVERLAY->transform_touch_point(&position->x, &position->y); } - // the digitizer is mapped to the zero-based primary display, while SDVX - // still expects portrait coordinates when its image is rendered in landscape: + // SDVX still expects portrait coordinates when its image is rendered in landscape: // (x, y) -> (width * (1 - y / height), height * x / width). + bool sdvx_landscape_rotate(POINT *position, LONG width, LONG height) { + if (width <= 0 || height <= 0) { + return false; + } + + const auto input_x = position->x; + position->x = width - MulDiv(position->y, width, height); + position->y = MulDiv(input_x, height, width); + return true; + } + + // the digitizer is mapped to the zero-based primary display, so the contact is already + // in the effective landscape resolution the rotation is based on static bool transform_sdvx_landscape_touch_position(POINT *position) { const auto landscape_width = static_cast(GRAPHICS_FS_CUSTOM_RESOLUTION.has_value() ? GRAPHICS_FS_CUSTOM_RESOLUTION.value().first : GRAPHICS_FS_ORIGINAL_HEIGHT); const auto landscape_height = static_cast(GRAPHICS_FS_CUSTOM_RESOLUTION.has_value() ? GRAPHICS_FS_CUSTOM_RESOLUTION.value().second : GRAPHICS_FS_ORIGINAL_WIDTH); - if (landscape_width <= 0 || landscape_height <= 0) { - return false; - } - const auto input_x = position->x; - position->x = landscape_width - - MulDiv(position->y, landscape_width, landscape_height); - position->y = MulDiv(input_x, landscape_height, landscape_width); - return true; + return sdvx_landscape_rotate(position, landscape_width, landscape_height); } // convert physical screen coordinates to game touch coordinates for a known target diff --git a/src/spice2x/touch/native/transform.h b/src/spice2x/touch/native/transform.h index 77a71af..7bb594d 100644 --- a/src/spice2x/touch/native/transform.h +++ b/src/spice2x/touch/native/transform.h @@ -10,6 +10,7 @@ namespace nativetouch::transform { }; bool is_tdj_dedicated_subscreen(HWND window); + bool sdvx_landscape_rotate(POINT *position, LONG width, LONG height); bool game_to_screen(HWND window, POINT *position); bool screen_to_game(HWND window, POINT *position); bool mouse_to_game(HWND window, POINT *position);