Compare commits

...

3 Commits

Author SHA1 Message Date
bicarus adf4cccd4a overlay: fix subscreen overlay rendering for popn (#869)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
For popn HC, some transparency effects, like the one for touch feedback,
rendered incorrectly; this is due to how alpha channel is handled for
subscreen overlay drawing on top of ImGui.

## Testing
Tested popn and iidx.
2026-08-16 04:24:14 -07:00
bicarus bf8e194685 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
2026-08-16 04:03:42 -07:00
bicarus a2e508208c gitadora: fix synthetic touch (mouse and api) (#867)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
Fix mouse and touch API handling for gitadora.

* hook native touch in every arena window mode (previously only using
wintouchemu for 1 windowed mode)
* publish the touch window (SMALL panel, or the main window drawing the
overlay) and only attach injection there
* route touch on the SMALL window straight through instead of the
overlay transform, and accept the mouse only there
* fix spiceapi by forcing 800x1280 touch canvas size
* run API capture before the subscreen present to fix api not showing
sub image


## Testing
Test:

gitadora 1/2/4 windowed mode, fullscreen

iidx /sdvx / popn windowed/fullscreen for regressions
2026-08-16 01:43:20 -07:00
12 changed files with 202 additions and 47 deletions
+41 -11
View File
@@ -12,7 +12,9 @@
#include "touch/touch.h" #include "touch/touch.h"
#include "touch/native/inject.h" #include "touch/native/inject.h"
#include "touch/native/nativetouchhook.h" #include "touch/native/nativetouchhook.h"
#include "touch/native/transform.h"
#include "util/utils.h" #include "util/utils.h"
#include "games/gitadora/gitadora.h"
#include "games/iidx/iidx.h" #include "games/iidx/iidx.h"
using namespace std::placeholders; using namespace std::placeholders;
@@ -40,6 +42,35 @@ namespace api::modules {
return nativetouch::inject::inject_synthetic_touch(position, true); 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") { Touch::Touch() : Module("touch") {
is_sdvx = avs::game::is_model("KFC"); is_sdvx = avs::game::is_model("KFC");
@@ -55,8 +86,8 @@ namespace api::modules {
native_canvas_w = 0; native_canvas_w = 0;
native_canvas_h = 0; native_canvas_h = 0;
if (is_sdvx) { if (is_sdvx) {
// windowed and landscape API coordinates already match the primary screen orientation; // windowed API coordinates land on the sub screen window as-is; fullscreen
// fullscreen portrait coordinates are rotated by apply_touch_errata // coordinates are rotated into the game's touch space by apply_touch_errata
const bool landscape_coordinates = const bool landscape_coordinates =
GRAPHICS_WINDOWED || GRAPHICS_FS_ORIENTATION_SWAP; GRAPHICS_WINDOWED || GRAPHICS_FS_ORIENTATION_SWAP;
native_canvas_w = landscape_coordinates ? 1920 : 1080; native_canvas_w = landscape_coordinates ? 1920 : 1080;
@@ -69,6 +100,10 @@ namespace api::modules {
// pop'n music API touch surface // pop'n music API touch surface
native_canvas_w = 1280; native_canvas_w = 1280;
native_canvas_h = 800; native_canvas_h = 800;
} else if (games::gitadora::is_arena_model()) {
// GITADORA arena SMALL subscreen, either in its own window or in the overlay
native_canvas_w = games::gitadora::ARENA_SUBSCREEN_WIDTH;
native_canvas_h = games::gitadora::ARENA_SUBSCREEN_HEIGHT;
} }
functions["read"] = std::bind(&Touch::read, this, _1, _2); functions["read"] = std::bind(&Touch::read, this, _1, _2);
@@ -212,19 +247,14 @@ namespace api::modules {
} }
void Touch::apply_touch_errata(int &x, int &y) { void Touch::apply_touch_errata(int &x, int &y) {
int x_raw = x;
int y_raw = y;
if (is_tdj_fhd) { if (is_tdj_fhd) {
// deal with TDJ FHD resolution mismatch (upgrade 720p to 1080p) // 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 // 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 // the target of the touch events so just assume it's the sub screen
x = x_raw * 1920 / 1280; x = x * 1920 / 1280;
y = y_raw * 1080 / 720; y = y * 1080 / 720;
} else if (is_sdvx && !GRAPHICS_WINDOWED && !GRAPHICS_FS_ORIENTATION_SWAP) { } else if (is_sdvx) {
// rotate API coordinates into SDVX's portrait touch space sdvx_touch_errata(x, y, use_native, native_canvas_w, native_canvas_h);
x = 1080 - y_raw;
y = x_raw;
} }
} }
} }
+11 -11
View File
@@ -41,6 +41,7 @@ namespace games::gitadora {
bool ARENA_TWO_HEAD_EXCLUSIVE = false; bool ARENA_TWO_HEAD_EXCLUSIVE = false;
std::optional<std::string> ASIO_DRIVER = std::nullopt; std::optional<std::string> ASIO_DRIVER = std::nullopt;
bool ALLOW_REALTEK_AUDIO = false; bool ALLOW_REALTEK_AUDIO = false;
bool NATIVE_TOUCH = false;
/* /*
* Prevent GitaDora from creating folders on F drive * Prevent GitaDora from creating folders on F drive
@@ -809,17 +810,16 @@ namespace games::gitadora {
hooks::audio::INJECT_FAKE_REALTEK_AUDIO = true; hooks::audio::INJECT_FAKE_REALTEK_AUDIO = true;
} }
// Single-window mode needs touch injection for its overlay. Two-head fullscreen // touch injection drives mouse-as-touch and API touch for the subscreen,
// mode uses the real SMALL output, so it only needs the display-topology shim. // no matter whether it is drawn by the overlay (single-window mode) or by
if (GRAPHICS_PREVENT_SECONDARY_WINDOWS) { // the dedicated SMALL window
// enable touch hook for subscreen overlay NATIVE_TOUCH = !wintouchemu::FORCE &&
const auto native_touch_ready = !wintouchemu::FORCE && nativetouch::hook(avs::game::DLL_INSTANCE);
nativetouch::hook(avs::game::DLL_INSTANCE); if (!NATIVE_TOUCH && GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
if (!native_touch_ready) { // the legacy fallback can only feed the subscreen overlay
wintouchemu::FORCE = true; wintouchemu::FORCE = true;
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true; wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
wintouchemu::hook("GITADORA", avs::game::DLL_INSTANCE); wintouchemu::hook("GITADORA", avs::game::DLL_INSTANCE);
}
} }
#if !SPICE_XP #if !SPICE_XP
+5
View File
@@ -23,6 +23,11 @@ namespace games::gitadora {
extern bool ARENA_TWO_HEAD_EXCLUSIVE; extern bool ARENA_TWO_HEAD_EXCLUSIVE;
extern std::optional<std::string> ASIO_DRIVER; extern std::optional<std::string> ASIO_DRIVER;
extern bool ALLOW_REALTEK_AUDIO; extern bool ALLOW_REALTEK_AUDIO;
extern bool NATIVE_TOUCH;
// arena SMALL subscreen (touch panel) resolution
static constexpr int ARENA_SUBSCREEN_WIDTH = 800;
static constexpr int ARENA_SUBSCREEN_HEIGHT = 1280;
class GitaDoraGame : public games::Game { class GitaDoraGame : public games::Game {
public: public:
@@ -1520,6 +1520,10 @@ void graphics_d3d9_on_present(
graphics_d3d9_process_screenshot(device, wrapped_device); graphics_d3d9_process_screenshot(device, wrapped_device);
} }
// API capture always includes the overlay; it must run before the subscreen present
// below, which leaves the arena SMALL back buffer black
graphics_d3d9_process_capture(device, wrapped_device);
// for IIDX TDJ / SDVX UFC, handle subscreen // for IIDX TDJ / SDVX UFC, handle subscreen
const bool is_vm = games::sdvx::is_valkyrie_model(); const bool is_vm = games::sdvx::is_valkyrie_model();
const bool is_tdj = avs::game::is_model("LDJ") && games::iidx::TDJ_MODE; const bool is_tdj = avs::game::is_model("LDJ") && games::iidx::TDJ_MODE;
@@ -1533,9 +1537,6 @@ void graphics_d3d9_on_present(
if (is_mfc) { if (is_mfc) {
wintouchemu::update(); wintouchemu::update();
} }
// API capture always includes the overlay
graphics_d3d9_process_capture(device, wrapped_device);
} }
void update_backbuffer_dimensions(D3DPRESENT_PARAMETERS *params) { void update_backbuffer_dimensions(D3DPRESENT_PARAMETERS *params) {
@@ -4,10 +4,12 @@
#include <d3d9.h> #include <d3d9.h>
#include "games/gitadora/gitadora.h"
inline constexpr UINT GFDM_SIDE_WIDTH = 1080; inline constexpr UINT GFDM_SIDE_WIDTH = 1080;
inline constexpr UINT GFDM_SIDE_HEIGHT = 1920; inline constexpr UINT GFDM_SIDE_HEIGHT = 1920;
inline constexpr UINT GFDM_SMALL_WIDTH = 800; inline constexpr UINT GFDM_SMALL_WIDTH = games::gitadora::ARENA_SUBSCREEN_WIDTH;
inline constexpr UINT GFDM_SMALL_HEIGHT = 1280; inline constexpr UINT GFDM_SMALL_HEIGHT = games::gitadora::ARENA_SUBSCREEN_HEIGHT;
inline constexpr UINT GFDM_LOGICAL_HEAD_COUNT = 4; inline constexpr UINT GFDM_LOGICAL_HEAD_COUNT = 4;
inline constexpr UINT GFDM_NATIVE_SMALL_SWAPCHAIN = 1; inline constexpr UINT GFDM_NATIVE_SMALL_SWAPCHAIN = 1;
+47 -2
View File
@@ -33,6 +33,7 @@
#include "util/utils.h" #include "util/utils.h"
#include "misc/wintouchemu.h" #include "misc/wintouchemu.h"
#include "touch/native/inject.h" #include "touch/native/inject.h"
#include "touch/native/nativetouchhook.h"
#include "util/time.h" #include "util/time.h"
#include "rawinput/rawinput.h" #include "rawinput/rawinput.h"
@@ -216,6 +217,18 @@ static void gitadora_remember_window(HWND hWnd, const std::string &window_name)
} else if (window_name == "SMALL") { } else if (window_name == "SMALL") {
GFDM_SUBSCREEN_WINDOW = hWnd; GFDM_SUBSCREEN_WINDOW = hWnd;
} }
// touch belongs to the SMALL panel when it exists, otherwise to the main window
// that draws the subscreen overlay
const bool hosts_touch = window_name == "SMALL" ||
(window_name == "GITADORA" && !graphics_gitadora_has_dedicated_subscreen());
if (nativetouch::is_hooked() && hWnd != nullptr && hosts_touch) {
nativetouch::inject::set_preferred_injection_window(hWnd);
}
}
bool graphics_gitadora_has_dedicated_subscreen() {
return GFDM_SUBSCREEN_WINDOW != nullptr;
} }
bool graphics_gitadora_prepare_two_head_device_window( bool graphics_gitadora_prepare_two_head_device_window(
@@ -616,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_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"); 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() const std::string gfdm_window_name = games::gitadora::is_arena_model()
? gitadora_canonical_window_name(effective_window_name) ? gitadora_canonical_window_name(effective_window_name)
@@ -720,6 +735,20 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
graphics_hook_subscreen_window(SDVX_SUBSCREEN_WINDOW); 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 // only hook touch window if multiple windows are allowed
if (gfdm_window_name == "LEFT" || gfdm_window_name == "RIGHT") { if (gfdm_window_name == "LEFT" || gfdm_window_name == "RIGHT") {
gitadora_remember_window(result, gfdm_window_name); gitadora_remember_window(result, gfdm_window_name);
@@ -731,6 +760,11 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
if (GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) { if (GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
graphics_hook_subscreen_window(GFDM_SUBSCREEN_WINDOW); graphics_hook_subscreen_window(GFDM_SUBSCREEN_WINDOW);
} }
// the dedicated SMALL window is the touch panel; mouse and API touch target it
if (nativetouch::is_hooked() && result != nullptr) {
nativetouch::inject::register_and_attach_window(result);
}
} }
if (is_gfdm_window && GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) { if (is_gfdm_window && GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
gitadora_force_window_style(result); gitadora_force_window_style(result);
@@ -1104,6 +1138,15 @@ static BOOL WINAPI ShowWindow_hook(HWND hWnd, int nCmdShow) {
return true; 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 // call original
return ShowWindow_orig(hWnd, nCmdShow); return ShowWindow_orig(hWnd, nCmdShow);
} }
@@ -1320,7 +1363,9 @@ void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParamet
const bool native_touch_overlay = const bool native_touch_overlay =
(games::iidx::NATIVE_TOUCH && games::iidx::TDJ_MODE && !GRAPHICS_IIDX_WSUB) || (games::iidx::NATIVE_TOUCH && games::iidx::TDJ_MODE && !GRAPHICS_IIDX_WSUB) ||
(games::popn::NATIVE_TOUCH && (games::popn::NATIVE_TOUCH &&
games::popn::is_pikapika_model() && GRAPHICS_PREVENT_SECONDARY_WINDOWS); games::popn::is_pikapika_model() && GRAPHICS_PREVENT_SECONDARY_WINDOWS) ||
(games::gitadora::NATIVE_TOUCH &&
games::gitadora::is_arena_model() && GRAPHICS_PREVENT_SECONDARY_WINDOWS);
if (native_touch_overlay) { if (native_touch_overlay) {
nativetouch::inject::register_and_attach_window(hWnd); nativetouch::inject::register_and_attach_window(hWnd);
} }
+1
View File
@@ -121,6 +121,7 @@ extern bool D3D9_DEVICE_HOOK_DISABLE;
void graphics_init(); void graphics_init();
void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParameters); void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParameters);
bool graphics_gitadora_has_dedicated_subscreen();
// The native GITADORA two-head D3D9 group uses the game's named SMALL // The native GITADORA two-head D3D9 group uses the game's named SMALL
// device window for the native physical SMALL head. The game requests // device window for the native physical SMALL head. The game requests
// D3DCREATE_NOWINDOWCHANGES, so this host must be made borderless and sized // D3DCREATE_NOWINDOWCHANGES, so this host must be made borderless and sized
@@ -25,6 +25,15 @@ namespace overlay::windows {
const ImVec4 YELLOW(1.f, 1.f, 0.f, 1.f); const ImVec4 YELLOW(1.f, 1.f, 0.f, 1.f);
const ImVec4 WHITE(1.f, 1.f, 1.f, 1.f); const ImVec4 WHITE(1.f, 1.f, 1.f, 1.f);
// a subscreen is an already-composited backbuffer, so its alpha bits are not
// meaningful for display. Use AddImage's diffuse alpha (opaque by default)
// instead of the texture alpha while preserving ImGui's normal blend state.
static void ignore_texture_alpha(const ImDrawList *, const ImDrawCmd *command) {
auto device = static_cast<IDirect3DDevice9 *>(command->UserCallbackData);
device->SetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_DIFFUSE);
device->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG2);
}
GenericSubScreen::GenericSubScreen(SpiceOverlay *overlay) : Window(overlay), device(overlay->get_device()) { GenericSubScreen::GenericSubScreen(SpiceOverlay *overlay) : Window(overlay), device(overlay->get_device()) {
this->remove_window_padding = true; this->remove_window_padding = true;
// ImGuiWindowFlags_NoBackground is needed as the background is drawn on top of the subscreen image // ImGuiWindowFlags_NoBackground is needed as the background is drawn on top of the subscreen image
@@ -239,10 +248,13 @@ namespace overlay::windows {
auto draw_list = this->draws_window auto draw_list = this->draws_window
? ImGui::GetWindowDrawList() ? ImGui::GetWindowDrawList()
: ImGui::GetBackgroundDrawList(); : ImGui::GetBackgroundDrawList();
draw_list->AddCallback(ignore_texture_alpha, this->device);
draw_list->AddImage( draw_list->AddImage(
reinterpret_cast<ImTextureID>(this->texture), reinterpret_cast<ImTextureID>(this->texture),
overlay_content_top_left, overlay_content_top_left,
bottom_right); bottom_right);
// following ImGui draws should resume normal texture-alpha modulation
draw_list->AddCallback(ImGui::GetPlatformIO().DrawCallback_ResetRenderState);
if (this->draws_window) { if (this->draws_window) {
// draw an invisible button so that it swallows mouse input // draw an invisible button so that it swallows mouse input
+38 -3
View File
@@ -123,6 +123,30 @@ namespace nativetouch::inject {
// asks the touch window thread to send an UPDATE when the game loop runs elsewhere // asks the touch window thread to send an UPDATE when the game loop runs elsewhere
static UINT contact_refresh_message; static UINT contact_refresh_message;
// the window that shows the touch surface; contacts can be registered from any thread,
// so this is published by the graphics window hooks rather than derived from the globals
static std::atomic<HWND> preferred_injection_window { nullptr };
void set_preferred_injection_window(HWND window) {
preferred_injection_window.store(window, std::memory_order_release);
}
// a game that publishes its touch surface needs injection only on that window
static bool is_published_touch_window(HWND window) {
const auto preferred = preferred_injection_window.load(std::memory_order_acquire);
return preferred == nullptr || window == preferred;
}
// games can register several windows for touch; keep synthetic contacts on the one
// that actually shows the touch surface
static bool is_preferred_injection_window(HWND window) {
if (GRAPHICS_IIDX_WSUB) {
return window == TDJ_SUBSCREEN_WINDOW;
}
return is_published_touch_window(window);
}
// submit one synthetic contact frame to Windows touch injection // submit one synthetic contact frame to Windows touch injection
static bool inject_touch_frame( static bool inject_touch_frame(
POINT position, POINTER_FLAGS pointer_flags, bool retry_if_not_ready = false) { POINT position, POINTER_FLAGS pointer_flags, bool retry_if_not_ready = false) {
@@ -325,11 +349,22 @@ namespace nativetouch::inject {
if (contact_state.input_window == window) { if (contact_state.input_window == window) {
release_active_contact(); release_active_contact();
} }
HWND expected_window = window; HWND expected_window = window;
injection_window.compare_exchange_strong( injection_window.compare_exchange_strong(
expected_window, nullptr, std::memory_order_acq_rel); expected_window, nullptr, std::memory_order_acq_rel);
HWND expected_preferred_window = window;
preferred_injection_window.compare_exchange_strong(
expected_preferred_window, nullptr, std::memory_order_acq_rel);
if (GRAPHICS_IIDX_WSUB) {
HWND expected_delivery_window = window;
touch_delivery_window.compare_exchange_strong(
expected_delivery_window, nullptr, std::memory_order_acq_rel);
}
contact_refresh_pending.store(false, std::memory_order_release); contact_refresh_pending.store(false, std::memory_order_release);
touch_delivery_window.store(nullptr, std::memory_order_release);
RemoveWindowSubclass(window, touch_window_subclass_proc, subclass_id); RemoveWindowSubclass(window, touch_window_subclass_proc, subclass_id);
} }
@@ -370,7 +405,7 @@ namespace nativetouch::inject {
} }
// publish the UI-thread target for synthetic touch requests // publish the UI-thread target for synthetic touch requests
if (!GRAPHICS_IIDX_WSUB || window == TDJ_SUBSCREEN_WINDOW) { if (is_preferred_injection_window(window)) {
injection_window.store(window, std::memory_order_release); injection_window.store(window, std::memory_order_release);
} }
log_misc( log_misc(
@@ -403,7 +438,7 @@ namespace nativetouch::inject {
// call original // call original
const auto result = RegisterTouchWindow_orig(window, flags); const auto result = RegisterTouchWindow_orig(window, flags);
if (result) { if (result && is_published_touch_window(window)) {
// attach but don't register for touch messages // attach but don't register for touch messages
// (we're already in the middle of RegisterTouchWindow as a result // (we're already in the middle of RegisterTouchWindow as a result
// of the game calling it) // of the game calling it)
+1
View File
@@ -7,6 +7,7 @@ struct tagTOUCHINPUT;
namespace nativetouch::inject { namespace nativetouch::inject {
void attach_window(HWND window); void attach_window(HWND window);
void register_and_attach_window(HWND window); void register_and_attach_window(HWND window);
void set_preferred_injection_window(HWND window);
bool hook_available(HMODULE module); bool hook_available(HMODULE module);
bool hook(HMODULE module); bool hook(HMODULE module);
bool inject_synthetic_touch(POINT position, bool down); bool inject_synthetic_touch(POINT position, bool down);
+37 -15
View File
@@ -38,6 +38,11 @@ namespace nativetouch::transform {
window == TDJ_SUBSCREEN_WINDOW; window == TDJ_SUBSCREEN_WINDOW;
} }
// mouse-as-touch only applies while the cursor is over the target window
static bool is_cursor_over_window(HWND window, POINT position) {
return screen_to_game_client(window, &position);
}
// convert game touch coordinates to Windows desktop coordinates // convert game touch coordinates to Windows desktop coordinates
bool game_to_screen(HWND window, POINT *position) { bool game_to_screen(HWND window, POINT *position) {
if (settings::SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES) { if (settings::SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES) {
@@ -66,7 +71,13 @@ namespace nativetouch::transform {
return ClientToScreen(window, position) != FALSE; return ClientToScreen(window, position) != FALSE;
} }
static bool has_active_overlay_transform() { static bool overlay_owns_touch_input() {
// the arena SMALL window is the touch surface whenever it exists, so the
// subscreen overlay must not claim touch input in those window modes
if (graphics_gitadora_has_dedicated_subscreen()) {
return false;
}
return overlay::OVERLAY != nullptr && return overlay::OVERLAY != nullptr &&
overlay::OVERLAY->get_active() && overlay::OVERLAY->get_active() &&
overlay::OVERLAY->has_subscreen_touch_transform(); overlay::OVERLAY->has_subscreen_touch_transform();
@@ -83,23 +94,28 @@ namespace nativetouch::transform {
return overlay::OVERLAY->transform_touch_point(&position->x, &position->y); return overlay::OVERLAY->transform_touch_point(&position->x, &position->y);
} }
// the digitizer is mapped to the zero-based primary display, while SDVX // SDVX still expects portrait coordinates when its image is rendered in landscape:
// still expects portrait coordinates when its image is rendered in landscape:
// (x, y) -> (width * (1 - y / height), height * x / width). // (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) { static bool transform_sdvx_landscape_touch_position(POINT *position) {
const auto landscape_width = static_cast<LONG>(GRAPHICS_FS_CUSTOM_RESOLUTION.has_value() ? const auto landscape_width = static_cast<LONG>(GRAPHICS_FS_CUSTOM_RESOLUTION.has_value() ?
GRAPHICS_FS_CUSTOM_RESOLUTION.value().first : GRAPHICS_FS_ORIGINAL_HEIGHT); GRAPHICS_FS_CUSTOM_RESOLUTION.value().first : GRAPHICS_FS_ORIGINAL_HEIGHT);
const auto landscape_height = static_cast<LONG>(GRAPHICS_FS_CUSTOM_RESOLUTION.has_value() ? const auto landscape_height = static_cast<LONG>(GRAPHICS_FS_CUSTOM_RESOLUTION.has_value() ?
GRAPHICS_FS_CUSTOM_RESOLUTION.value().second : GRAPHICS_FS_ORIGINAL_WIDTH); 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; return sdvx_landscape_rotate(position, landscape_width, landscape_height);
position->x = landscape_width -
MulDiv(position->y, landscape_width, landscape_height);
position->y = MulDiv(input_x, landscape_height, landscape_width);
return true;
} }
// convert physical screen coordinates to game touch coordinates for a known target // convert physical screen coordinates to game touch coordinates for a known target
@@ -132,7 +148,7 @@ namespace nativetouch::transform {
// check if subscreen overlay is active and can transform the touch point; // check if subscreen overlay is active and can transform the touch point;
// if not, the touch point is valid as-is // if not, the touch point is valid as-is
if (!has_active_overlay_transform()) { if (!overlay_owns_touch_input()) {
return true; return true;
} }
@@ -149,8 +165,14 @@ namespace nativetouch::transform {
// exception: sdvx windowed subscreen does not use the subscreen overlay transform // exception: sdvx windowed subscreen does not use the subscreen overlay transform
if (GRAPHICS_WINDOWED && window == SDVX_SUBSCREEN_WINDOW) { if (GRAPHICS_WINDOWED && window == SDVX_SUBSCREEN_WINDOW) {
POINT client_position = *position; return is_cursor_over_window(window, *position);
return screen_to_game_client(window, &client_position); }
// exception: the arena SMALL window is the touch panel, so accept the mouse there
// (and only there) with the coordinates a real contact on it would produce
if (graphics_gitadora_has_dedicated_subscreen()) {
return window == GFDM_SUBSCREEN_WINDOW &&
is_cursor_over_window(window, *position);
} }
// if this game has a subscreen overlay that can transform touch input // if this game has a subscreen overlay that can transform touch input
@@ -168,7 +190,7 @@ namespace nativetouch::transform {
// route hardware screen coordinates through dedicated or overlay mapping and report the result // route hardware screen coordinates through dedicated or overlay mapping and report the result
Result hardware_to_game(POINT *position) { Result hardware_to_game(POINT *position) {
const auto dedicated_subscreen = is_tdj_dedicated_subscreen(TDJ_SUBSCREEN_WINDOW); const auto dedicated_subscreen = is_tdj_dedicated_subscreen(TDJ_SUBSCREEN_WINDOW);
const auto active_overlay = has_active_overlay_transform(); const auto active_overlay = overlay_owns_touch_input();
// special case for SDVX landscape mode // special case for SDVX landscape mode
if (!dedicated_subscreen && !active_overlay && if (!dedicated_subscreen && !active_overlay &&
+1
View File
@@ -10,6 +10,7 @@ namespace nativetouch::transform {
}; };
bool is_tdj_dedicated_subscreen(HWND window); 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 game_to_screen(HWND window, POINT *position);
bool screen_to_game(HWND window, POINT *position); bool screen_to_game(HWND window, POINT *position);
bool mouse_to_game(HWND window, POINT *position); bool mouse_to_game(HWND window, POINT *position);