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/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"
using namespace std::placeholders;
@@ -40,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");
@@ -55,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;
@@ -69,6 +100,10 @@ namespace api::modules {
// pop'n music API touch surface
native_canvas_w = 1280;
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);
@@ -212,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);
}
}
}
+11 -11
View File
@@ -41,6 +41,7 @@ namespace games::gitadora {
bool ARENA_TWO_HEAD_EXCLUSIVE = false;
std::optional<std::string> ASIO_DRIVER = std::nullopt;
bool ALLOW_REALTEK_AUDIO = false;
bool NATIVE_TOUCH = false;
/*
* Prevent GitaDora from creating folders on F drive
@@ -809,17 +810,16 @@ namespace games::gitadora {
hooks::audio::INJECT_FAKE_REALTEK_AUDIO = true;
}
// Single-window mode needs touch injection for its overlay. Two-head fullscreen
// mode uses the real SMALL output, so it only needs the display-topology shim.
if (GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
// enable touch hook for subscreen overlay
const auto native_touch_ready = !wintouchemu::FORCE &&
nativetouch::hook(avs::game::DLL_INSTANCE);
if (!native_touch_ready) {
wintouchemu::FORCE = true;
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
wintouchemu::hook("GITADORA", avs::game::DLL_INSTANCE);
}
// touch injection drives mouse-as-touch and API touch for the subscreen,
// no matter whether it is drawn by the overlay (single-window mode) or by
// the dedicated SMALL window
NATIVE_TOUCH = !wintouchemu::FORCE &&
nativetouch::hook(avs::game::DLL_INSTANCE);
if (!NATIVE_TOUCH && GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
// the legacy fallback can only feed the subscreen overlay
wintouchemu::FORCE = true;
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
wintouchemu::hook("GITADORA", avs::game::DLL_INSTANCE);
}
#if !SPICE_XP
+5
View File
@@ -23,6 +23,11 @@ namespace games::gitadora {
extern bool ARENA_TWO_HEAD_EXCLUSIVE;
extern std::optional<std::string> ASIO_DRIVER;
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 {
public:
@@ -1520,6 +1520,10 @@ void graphics_d3d9_on_present(
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
const bool is_vm = games::sdvx::is_valkyrie_model();
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) {
wintouchemu::update();
}
// API capture always includes the overlay
graphics_d3d9_process_capture(device, wrapped_device);
}
void update_backbuffer_dimensions(D3DPRESENT_PARAMETERS *params) {
@@ -4,10 +4,12 @@
#include <d3d9.h>
#include "games/gitadora/gitadora.h"
inline constexpr UINT GFDM_SIDE_WIDTH = 1080;
inline constexpr UINT GFDM_SIDE_HEIGHT = 1920;
inline constexpr UINT GFDM_SMALL_WIDTH = 800;
inline constexpr UINT GFDM_SMALL_HEIGHT = 1280;
inline constexpr UINT GFDM_SMALL_WIDTH = games::gitadora::ARENA_SUBSCREEN_WIDTH;
inline constexpr UINT GFDM_SMALL_HEIGHT = games::gitadora::ARENA_SUBSCREEN_HEIGHT;
inline constexpr UINT GFDM_LOGICAL_HEAD_COUNT = 4;
inline constexpr UINT GFDM_NATIVE_SMALL_SWAPCHAIN = 1;
+47 -2
View File
@@ -33,6 +33,7 @@
#include "util/utils.h"
#include "misc/wintouchemu.h"
#include "touch/native/inject.h"
#include "touch/native/nativetouchhook.h"
#include "util/time.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") {
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(
@@ -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_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)
@@ -720,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);
@@ -731,6 +760,11 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
if (GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
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) {
gitadora_force_window_style(result);
@@ -1104,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);
}
@@ -1320,7 +1363,9 @@ void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParamet
const bool native_touch_overlay =
(games::iidx::NATIVE_TOUCH && games::iidx::TDJ_MODE && !GRAPHICS_IIDX_WSUB) ||
(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) {
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_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
// device window for the native physical SMALL head. The game requests
// 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 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()) {
this->remove_window_padding = true;
// 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
? ImGui::GetWindowDrawList()
: ImGui::GetBackgroundDrawList();
draw_list->AddCallback(ignore_texture_alpha, this->device);
draw_list->AddImage(
reinterpret_cast<ImTextureID>(this->texture),
overlay_content_top_left,
bottom_right);
// following ImGui draws should resume normal texture-alpha modulation
draw_list->AddCallback(ImGui::GetPlatformIO().DrawCallback_ResetRenderState);
if (this->draws_window) {
// 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
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
static bool inject_touch_frame(
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) {
release_active_contact();
}
HWND expected_window = window;
injection_window.compare_exchange_strong(
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);
touch_delivery_window.store(nullptr, std::memory_order_release);
RemoveWindowSubclass(window, touch_window_subclass_proc, subclass_id);
}
@@ -370,7 +405,7 @@ namespace nativetouch::inject {
}
// 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);
}
log_misc(
@@ -403,7 +438,7 @@ namespace nativetouch::inject {
// call original
const auto result = RegisterTouchWindow_orig(window, flags);
if (result) {
if (result && is_published_touch_window(window)) {
// attach but don't register for touch messages
// (we're already in the middle of RegisterTouchWindow as a result
// of the game calling it)
+1
View File
@@ -7,6 +7,7 @@ struct tagTOUCHINPUT;
namespace nativetouch::inject {
void 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(HMODULE module);
bool inject_synthetic_touch(POINT position, bool down);
+37 -15
View File
@@ -38,6 +38,11 @@ namespace nativetouch::transform {
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
bool game_to_screen(HWND window, POINT *position) {
if (settings::SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES) {
@@ -66,7 +71,13 @@ namespace nativetouch::transform {
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 &&
overlay::OVERLAY->get_active() &&
overlay::OVERLAY->has_subscreen_touch_transform();
@@ -83,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<LONG>(GRAPHICS_FS_CUSTOM_RESOLUTION.has_value() ?
GRAPHICS_FS_CUSTOM_RESOLUTION.value().first : GRAPHICS_FS_ORIGINAL_HEIGHT);
const auto landscape_height = static_cast<LONG>(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
@@ -132,7 +148,7 @@ namespace nativetouch::transform {
// check if subscreen overlay is active and can transform the touch point;
// if not, the touch point is valid as-is
if (!has_active_overlay_transform()) {
if (!overlay_owns_touch_input()) {
return true;
}
@@ -149,8 +165,14 @@ namespace nativetouch::transform {
// exception: sdvx windowed subscreen does not use the subscreen overlay transform
if (GRAPHICS_WINDOWED && window == SDVX_SUBSCREEN_WINDOW) {
POINT client_position = *position;
return screen_to_game_client(window, &client_position);
return is_cursor_over_window(window, *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
@@ -168,7 +190,7 @@ namespace nativetouch::transform {
// route hardware screen coordinates through dedicated or overlay mapping and report the result
Result hardware_to_game(POINT *position) {
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
if (!dedicated_subscreen && !active_overlay &&
+1
View File
@@ -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);