Files
spice2x-r3d/src/spice2x/hooks/graphics/graphics.cpp
T
bicarusandGitHub 7a1f46d1be gitadora: (arena model) fix window settings not working (#494)
## Link to GitHub Issue, if one exists
#477 

## Description of change
F11 menu (window settings) did not work for arena model because the game
engine races to create many windows, so our logic of grabbing the first
window created doesn't always get the main window.

## Testing
2026-01-02 15:07:32 -08:00

1093 lines
36 KiB
C++

#include <initguid.h>
#include "graphics.h"
#include <set>
#include <vector>
#include <mutex>
#include <condition_variable>
#include "avs/game.h"
#include "cfg/icon.h"
#include "cfg/screen_resize.h"
#include "games/ddr/ddr.h"
#include "games/gitadora/gitadora.h"
#include "games/iidx/iidx.h"
#include "hooks/graphics/backends/d3d9/d3d9_backend.h"
#include "launcher/shutdown.h"
#include "overlay/overlay.h"
#include "touch/touch.h"
#include "touch/touch_indicators.h"
#include "util/detour.h"
#include "util/logging.h"
#include "util/fileutils.h"
#include "util/utils.h"
#include "misc/wintouchemu.h"
#include "util/time.h"
#include "rawinput/rawinput.h"
struct CaptureData {
std::shared_ptr<uint8_t[]> data;
unsigned short width, height;
uint64_t timestamp;
};
HWND TDJ_SUBSCREEN_WINDOW = nullptr;
HWND SDVX_SUBSCREEN_WINDOW = nullptr;
HWND GFDM_SUBSCREEN_WINDOW = nullptr;
// icon
static HICON WINDOW_ICON = LoadIcon(GetModuleHandle(nullptr), MAKEINTRESOURCE(MAINICON));
// state
static WNDPROC WNDPROC_ORIG = nullptr;
static WNDPROC WSUB_WNDPROC_ORIG = nullptr;
static std::vector<WNDPROC> WNDPROC_CUSTOM {};
static bool GRAPHICS_SCREENSHOT_TRIGGER = false;
static std::set<int> GRAPHICS_SCREENS { 0 };
static std::mutex GRAPHICS_SCREENS_M {};
static std::vector<int> GRAPHICS_CAPTURE_SCREENS;
static const size_t GRAPHICS_CAPTURE_SCREEN_NO = 4;
static std::mutex GRAPHICS_CAPTURE_SCREENS_M {};
static CaptureData GRAPHICS_CAPTURE_BUFFER[GRAPHICS_CAPTURE_SCREEN_NO] {};
static std::mutex GRAPHICS_CAPTURE_BUFFER_M[GRAPHICS_CAPTURE_SCREEN_NO] {};
static std::condition_variable GRAPHICS_CAPTURE_CV[GRAPHICS_CAPTURE_SCREEN_NO] {};
// flag settings
bool GRAPHICS_CAPTURE_CURSOR = false;
bool GRAPHICS_LOG_HRESULT = false;
bool GRAPHICS_SDVX_FORCE_720 = false;
bool GRAPHICS_SHOW_CURSOR = false;
graphics_orientation GRAPHICS_ADJUST_ORIENTATION = ORIENTATION_NORMAL;
bool GRAPHICS_WINDOWED = false;
std::vector<HWND> GRAPHICS_WINDOWS;
std::optional<HWND> GRAPHICS_WINDOW_MAIN;
UINT GRAPHICS_FORCE_REFRESH = 0;
std::optional<int> GRAPHICS_FORCE_VSYNC_BUFFER;
bool GRAPHICS_FORCE_SINGLE_ADAPTER = false;
bool GRAPHICS_PREVENT_SECONDARY_WINDOW = false;
graphics_dx9on12_state GRAPHICS_9_ON_12_STATE = DX9ON12_AUTO;
bool GRAPHICS_9_ON_12_REQUESTED_BY_GAME = false;
bool SUBSCREEN_FORCE_REDRAW = false;
bool D3D9_DEVICE_HOOK_DISABLE = false;
std::optional<std::pair<uint32_t, uint32_t>> GRAPHICS_FS_CUSTOM_RESOLUTION;
bool GRAPHICS_FS_ORIENTATION_SWAP = false;
uint32_t GRAPHICS_FS_ORIGINAL_WIDTH = 0;
uint32_t GRAPHICS_FS_ORIGINAL_HEIGHT = 0;
// settings
std::string GRAPHICS_DEVICEID = "PCI\\VEN_1002&DEV_7146";
std::string GRAPHICS_SCREENSHOT_DIR = ".\\screenshots";
static decltype(ChangeDisplaySettingsA) *ChangeDisplaySettingsA_orig = nullptr;
static decltype(ChangeDisplaySettingsExA) *ChangeDisplaySettingsExA_orig = nullptr;
static decltype(ClipCursor) *ClipCursor_orig = nullptr;
static decltype(CreateWindowExA) *CreateWindowExA_orig = nullptr;
static decltype(CreateWindowExW) *CreateWindowExW_orig = nullptr;
static decltype(EnableWindow) *EnableWindow_orig = nullptr;
static decltype(EnumDisplayDevicesA) *EnumDisplayDevicesA_orig = nullptr;
static decltype(MoveWindow) *MoveWindow_orig = nullptr;
static decltype(PeekMessageA) *PeekMessageA_orig = nullptr;
static decltype(RegisterClassA) *RegisterClassA_orig = nullptr;
static decltype(RegisterClassExA) *RegisterClassExA_orig = nullptr;
static decltype(RegisterClassW) *RegisterClassW_orig = nullptr;
static decltype(RegisterClassExW) *RegisterClassExW_orig = nullptr;
static decltype(ShowCursor) *ShowCursor_orig = nullptr;
static decltype(SetCursor) *SetCursor_orig = nullptr;
static decltype(SetWindowLongA) *SetWindowLongA_orig = nullptr;
static decltype(SetWindowLongW) *SetWindowLongW_orig = nullptr;
static decltype(SetWindowPos) *SetWindowPos_orig = nullptr;
static void reset_window_hook(HWND hWnd) {
overlay::destroy(hWnd);
if (WNDPROC_ORIG) {
SetWindowLongPtrA(hWnd, GWLP_WNDPROC, (LONG_PTR) WNDPROC_ORIG);
WNDPROC_ORIG = nullptr;
}
}
// window procedure
static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
// terminate
if (uMsg == WM_CLOSE) {
log_info("graphics", "detected WM_CLOSE, terminating...");
launcher::shutdown(0);
return false;
}
// overlay specific
if (overlay::OVERLAY) {
switch (uMsg) {
case WM_CHAR: {
// input characters if overlay is active
if (overlay::OVERLAY->has_focus()) {
overlay::OVERLAY->input_char((unsigned int) wParam);
return true;
}
break;
}
case WM_DESTROY: {
auto wndproc = WNDPROC_ORIG;
reset_window_hook(hWnd);
return CallWindowProcA(wndproc, hWnd, uMsg, wParam, lParam);
}
case WM_SETCURSOR: {
// set cursor back to the overlay one
if (LOWORD(lParam) == HTCLIENT && overlay::OVERLAY->update_cursor()) {
return true;
}
break;
}
default:
break;
}
}
if (wintouchemu::INJECT_MOUSE_AS_WM_TOUCH) {
// drop mouse inputs since only wintouches should be used
switch (uMsg) {
case WM_LBUTTONDOWN:
case WM_LBUTTONUP:
case WM_MBUTTONDOWN:
case WM_MBUTTONUP:
case WM_RBUTTONDOWN:
case WM_RBUTTONUP:
case WM_XBUTTONDOWN:
case WM_XBUTTONUP:
return true;
}
}
// window resize
graphics_windowed_wndproc(hWnd, uMsg, wParam, lParam);
// call custom procedures
for (WNDPROC wndProc : WNDPROC_CUSTOM) {
wndProc(hWnd, uMsg, wParam, lParam);
}
// capture mouse
if (GRAPHICS_CAPTURE_CURSOR) {
bool free_cursor = false;
bool capture_cursor = false;
bool early_return = false;
switch (uMsg) {
case WM_SETFOCUS:
capture_cursor = true;
early_return = true;
break;
case WM_KILLFOCUS:
free_cursor = true;
early_return = true;
break;
case WM_WINDOWPOSCHANGED:
// known issue: dragging with the title bar results in WM_WINDOWPOSCHANGED
// getting called, which calls ClipCursor successfully, but doesn't actually
// confine the cursor for some reason; seems like odd Windows behavior
// (can be fixed if focus is shifted to another window and then back to the game
// window)
if (hWnd == GetActiveWindow()) {
capture_cursor = true;
} else {
free_cursor = true;
}
// do not return early; may result in WM_SIZE / WM_MOVE no longer being called
default:
break;
}
if (free_cursor) {
ClipCursor(nullptr);
} else if (capture_cursor) {
RECT WINDOW_RECT;
GetWindowRect(hWnd, &WINDOW_RECT);
ClipCursor(&WINDOW_RECT);
}
if (early_return) {
return true;
}
}
// drop keydown messages
switch (uMsg) {
case WM_SYSKEYDOWN:
case WM_SYSKEYUP:
case WM_KEYDOWN:
case WM_KEYUP:
return true;
default:
break;
}
switch (uMsg) {
case WM_MOVE:
case WM_SIZE: {
// Update SPICETOUCH space when the main window changes size or moves.
// The update happens regardless of whether the "fake" spicetouch window is present or not.
// This allows touches received on subscreen window to be translated correctly.
update_spicetouch_window_dimensions(hWnd);
// log_misc(
// "graphics", "detected window change ({}x{} @ {}, {}), updating touch coord-space to match",
// SPICETOUCH_TOUCH_WIDTH, SPICETOUCH_TOUCH_HEIGHT, SPICETOUCH_TOUCH_X, SPICETOUCH_TOUCH_Y);
// Update SPICETOUCH window if present
if (SPICETOUCH_TOUCH_HWND) {
SetWindowPos(
SPICETOUCH_TOUCH_HWND, HWND_TOP,
SPICETOUCH_TOUCH_X, SPICETOUCH_TOUCH_Y,
SPICETOUCH_TOUCH_WIDTH, SPICETOUCH_TOUCH_HEIGHT,
SWP_NOZORDER | SWP_NOREDRAW | SWP_NOREPOSITION | SWP_NOACTIVATE);
}
}
default:
break;
}
// call default
return CallWindowProcA(WNDPROC_ORIG, hWnd, uMsg, wParam, lParam);
}
// window procedure for subscreen
// this might be replaced by spicetouch hook later
static LRESULT CALLBACK WsubWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
if (uMsg == WM_CLOSE) {
log_misc("graphics", "ignore WM_CLOSE for subscreen window");
return false;
}
return CallWindowProcA(WSUB_WNDPROC_ORIG, hWnd, uMsg, wParam, lParam);
}
static LONG WINAPI ChangeDisplaySettingsA_hook(DEVMODEA *lpDevMode, DWORD dwflags) {
log_misc("graphics", "ChangeDisplaySettingsA hook hit");
// ignore display settings changes when running windowed
if (GRAPHICS_WINDOWED) {
return DISP_CHANGE_SUCCESSFUL;
}
// call original
return ChangeDisplaySettingsA_orig(lpDevMode, dwflags);
}
static LONG WINAPI ChangeDisplaySettingsExA_hook(LPCSTR lpszDeviceName, DEVMODEA *lpDevMode, HWND hwnd,
DWORD dwflags, LPVOID lParam)
{
log_misc("graphics", "ChangeDisplaySettingsExA hook hit");
// ignore display settings changes when running windowed
if (GRAPHICS_WINDOWED) {
return DISP_CHANGE_SUCCESSFUL;
}
// call original
return ChangeDisplaySettingsExA_orig(lpszDeviceName, lpDevMode, hwnd, dwflags, lParam);
}
static BOOL WINAPI ClipCursor_hook(const RECT *lpRect) {
log_misc("graphics", "ClipCursor hook hit");
// ignore cursor confine when having no explicit cursor confine
if (!GRAPHICS_CAPTURE_CURSOR) {
return TRUE;
}
// call original
return ClipCursor_orig(lpRect);
}
static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPCSTR lpWindowName,
DWORD dwStyle, int x, int y, int nWidth, int nHeight,
HWND hWndParent, HMENU hMenu, HINSTANCE hInstance,
LPVOID lpParam)
{
const std::string window_name(lpWindowName != nullptr ? lpWindowName : "(null)");
log_misc("graphics", "CreateWindowExA hook hit (0x{:08x}, {}, {}, 0x{:08x}, {}, {}, {}, {}, {}, {}, {}, {})",
dwExStyle,
fmt::ptr(lpClassName),
window_name,
dwStyle,
x,
y,
nWidth,
nHeight,
fmt::ptr(hWndParent),
fmt::ptr(hMenu),
fmt::ptr(hInstance),
fmt::ptr(lpParam));
// set display orientation and/or refresh rate
// only set orientation when the target window is portrait
// (avoid doing this for SDVX subscreen which is in landscape, for example)
const auto adjust_orientation =
(GRAPHICS_ADJUST_ORIENTATION == ORIENTATION_CW ||
GRAPHICS_ADJUST_ORIENTATION == ORIENTATION_CCW);
if ((nHeight > nWidth && adjust_orientation) || GRAPHICS_FORCE_REFRESH > 0) {
DEVMODE mode {};
// get display settings
if (EnumDisplaySettings(nullptr, ENUM_CURRENT_SETTINGS, &mode)) {
if (adjust_orientation) {
DWORD orientation = GRAPHICS_ADJUST_ORIENTATION == ORIENTATION_CW ? DMDO_90 : DMDO_270;
log_misc(
"graphics",
"auto-rotate: call ChangeDisplaySettings and rotate display to DMDO_xx mode {}",
orientation);
mode.dmPelsWidth = nWidth;
mode.dmPelsHeight = nHeight;
mode.dmDisplayOrientation = orientation;
}
if (GRAPHICS_FORCE_REFRESH > 0) {
log_info(
"graphics",
"call ChangeDisplaySettings to force refresh rate: {} => {} Hz (-graphics-force-refresh)",
mode.dmDisplayFrequency,
GRAPHICS_FORCE_REFRESH);
mode.dmDisplayFrequency = GRAPHICS_FORCE_REFRESH;
}
const auto disp_res = ChangeDisplaySettings(&mode, CDS_FULLSCREEN);
if (disp_res != DISP_CHANGE_SUCCESSFUL) {
log_warning("graphics", "failed to change display settings: {}", disp_res);
}
} else {
log_warning("graphics", "failed to get display settings");
}
}
// gfdm
bool is_gitadora_main_window = false;
if (avs::game::is_model({"J32", "J33", "K32", "K33", "L32", "L33", "M32"})) {
// set window name
if (!lpWindowName) {
lpWindowName = "GITADORA";
is_gitadora_main_window = true;
}
}
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_gfdm_sub_window = games::gitadora::is_arena_model() && window_name.ends_with("SMALL");
// TDJ windowed mode with subscreen: hide maximize button
if ((is_tdj_sub_window && GRAPHICS_IIDX_WSUB) || is_sdvx_sub_window || is_gfdm_sub_window) {
dwStyle &= ~(WS_MAXIMIZEBOX);
}
// mouse clicks become misaligned when resized
if (is_gfdm_sub_window) {
dwStyle &= ~(WS_SIZEBOX);
}
if ((is_tdj_sub_window || is_sdvx_sub_window || is_gfdm_sub_window) && GRAPHICS_WSUB_BORDERLESS) {
dwStyle &= ~(WS_OVERLAPPEDWINDOW);
}
if (is_sdvx_sub_window) {
graphics_load_windowed_subscreen_parameters();
if (GRAPHICS_WSUB_SIZE.has_value()) {
nWidth = GRAPHICS_WSUB_WIDTH;
nHeight = GRAPHICS_WSUB_HEIGHT;
} else {
GRAPHICS_WSUB_WIDTH = nWidth;
GRAPHICS_WSUB_HEIGHT = nHeight;
}
if (GRAPHICS_WSUB_POS.has_value()) {
x = GRAPHICS_WSUB_X;
y = GRAPHICS_WSUB_Y;
} else {
GRAPHICS_WSUB_X = x;
GRAPHICS_WSUB_Y = y;
}
}
// call original
HWND result = CreateWindowExA_orig(dwExStyle, lpClassName, lpWindowName, dwStyle, x, y, nWidth, nHeight,
hWndParent, hMenu, hInstance, lpParam);
GRAPHICS_WINDOWS.push_back(result);
// this is only really needed for arena model, but pre-arena model was single window anyway
if (is_gitadora_main_window) {
GRAPHICS_WINDOW_MAIN = result;
}
if (is_tdj_sub_window) {
// TDJ windowed mode: remember the subscreen window handle for later
TDJ_SUBSCREEN_WINDOW = result;
// hook for preventing the closing of subscreen window
if (GRAPHICS_IIDX_WSUB) {
graphics_hook_subscreen_window(TDJ_SUBSCREEN_WINDOW);
}
}
// hook for preventing the closing of subscreen window
if (is_sdvx_sub_window) {
SDVX_SUBSCREEN_WINDOW = result;
graphics_hook_subscreen_window(SDVX_SUBSCREEN_WINDOW);
}
if (is_gfdm_sub_window) {
GFDM_SUBSCREEN_WINDOW = result;
graphics_hook_subscreen_window(GFDM_SUBSCREEN_WINDOW);
}
disable_touch_indicators(result);
log_misc("graphics", "CreateWindowExA returned {}", fmt::ptr(result));
return result;
}
static HWND WINAPI CreateWindowExW_hook(DWORD dwExStyle, LPCWSTR lpClassName, LPCWSTR lpWindowName,
DWORD dwStyle, int x, int y, int nWidth, int nHeight,
HWND hWndParent, HMENU hMenu, HINSTANCE hInstance,
LPVOID lpParam)
{
log_misc("graphics", "CreateWindowExW hook hit ({:x}, {}, {}, {:x}, {}, {}, {}, {}, {}, {}, {}, {})",
dwExStyle,
fmt::ptr(lpClassName),
lpWindowName != nullptr ? ws2s(lpWindowName) : "(null)",
dwStyle,
x,
y,
nWidth,
nHeight,
fmt::ptr(hWndParent),
fmt::ptr(hMenu),
fmt::ptr(hInstance),
fmt::ptr(lpParam));
// DDR specific stuff
if (avs::game::is_model("MDX")) {
// set window name
if (!lpWindowName) {
lpWindowName = L"Dance Dance Revolution";
}
// windowed mode adjustments
if (GRAPHICS_WINDOWED) {
// change window style
dwExStyle = 0;
dwStyle |= WS_OVERLAPPEDWINDOW;
// adjust window size to include window decoration
RECT rect {};
if (games::ddr::SDMODE) {
SetRect(&rect, 0, 0, 800, 600);
} else {
SetRect(&rect, 0, 0, 1280, 720);
}
AdjustWindowRect(&rect, dwStyle, (hMenu != nullptr));
nWidth = rect.right - rect.left;
nHeight = rect.bottom - rect.top;
}
}
// DanEvo specific stuff
if (avs::game::is_model("KDM")) {
// set window name
if (!lpWindowName) {
lpWindowName = L"Dance Evolution";
}
}
// call original
HWND result = CreateWindowExW_orig(
dwExStyle, lpClassName, lpWindowName, dwStyle, x, y, nWidth, nHeight,
hWndParent, hMenu, hInstance, lpParam);
GRAPHICS_WINDOWS.push_back(result);
log_misc("graphics", "CreateWindowExW returned {}", fmt::ptr(result));
disable_touch_indicators(result);
return result;
}
static BOOL WINAPI EnableWindow_hook(HWND hWnd, BOOL bEnable) {
return TRUE;
}
static BOOL WINAPI EnumDisplayDevicesA_hook(LPCTSTR lpDevice, DWORD iDevNum,
PDISPLAY_DEVICE lpDisplayDevice, DWORD dwFlags) {
// call original
BOOL value = EnumDisplayDevicesA_orig(lpDevice, iDevNum, lpDisplayDevice, dwFlags);
#ifndef SPICE64
// older IIDX games check for hardcoded PCI vendor/device ID pair of GPU
if ((avs::game::is_model("JDZ") || avs::game::is_model("KDZ")) && value) {
log_info(
"graphics",
"EnumDisplayDevicesA_hook: swap DeviceID {} with {} (for IIDX 18/19)",
lpDisplayDevice->DeviceID,
GRAPHICS_DEVICEID.c_str());
memcpy(&lpDisplayDevice->DeviceID, GRAPHICS_DEVICEID.c_str(), GRAPHICS_DEVICEID.size() + 1);
}
#endif
// return original result
return value;
}
static BOOL WINAPI MoveWindow_hook(HWND hWnd, int X, int Y, int nWidth, int nHeight, BOOL bRepaint) {
log_misc("graphics", "MoveWindow hook hit ({}, {}, {}, {}, {}, {})",
fmt::ptr(hWnd),
X,
Y,
nWidth,
nHeight,
bRepaint);
// sound voltex windowed mode adjustments
if (GRAPHICS_WINDOWED && GRAPHICS_SDVX_FORCE_720 && avs::game::is_model("KFC")) {
RECT rect {};
DWORD dwStyle;
dwStyle = GetWindowLongA(hWnd, GWL_STYLE);
// luckily, SDVX does not draw a menu. So we can leave the last
// argument to `AdjustWindowRect` as `0`.
SetRect(&rect, 0, 0, 720, 1280);
AdjustWindowRect(&rect, dwStyle, 0);
nWidth = rect.right - rect.left;
nHeight = rect.bottom - rect.top;
}
// iidx windowed TDJ mode
if (GRAPHICS_WINDOWED && TDJ_SUBSCREEN_WINDOW && hWnd == TDJ_SUBSCREEN_WINDOW) {
if (GRAPHICS_IIDX_WSUB) {
// (Experimental) Show subscreen in windowed mode
graphics_load_windowed_subscreen_parameters();
RECT rect {};
DWORD dwStyle;
dwStyle = GetWindowLongA(hWnd, GWL_STYLE);
SetRect(&rect, 0, 0, GRAPHICS_WSUB_WIDTH, GRAPHICS_WSUB_HEIGHT);
AdjustWindowRect(&rect, dwStyle, 0);
X = GRAPHICS_WSUB_X;
Y = GRAPHICS_WSUB_Y;
nWidth = rect.right - rect.left;
nHeight = rect.bottom - rect.top;
touch_attach_wnd(TDJ_SUBSCREEN_WINDOW);
} else {
// Existing behaviour: suppress subscreen window and prompt user to use overlay instead
log_info(
"graphics",
"MoveWindow hook - hiding TDJ subscreen window {}; please use subscreen overlay instead (-iidxtdjw)",
fmt::ptr(hWnd));
SendMessage(hWnd, WM_CLOSE, 0, 0);
TDJ_SUBSCREEN_WINDOW = nullptr;
return TRUE;
}
}
// call original
return MoveWindow_orig(hWnd, X, Y, nWidth, nHeight, bRepaint);
}
static BOOL WINAPI PeekMessageA_hook(
LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT wMsgFilterMax, UINT wRemoveMsg) {
// SDVX polls for messages too slowly
if (avs::game::is_model("KFC")) {
// process remaining messages
BOOL ret;
while ((ret = PeekMessageA_orig(lpMsg, hWnd, wMsgFilterMin, wMsgFilterMax, PM_REMOVE)) != 0) {
if (ret == -1) {
return ret;
} else {
TranslateMessage(lpMsg);
DispatchMessageA(lpMsg);
}
}
// return no message
return FALSE;
}
return PeekMessageA_orig(lpMsg, hWnd, wMsgFilterMin, wMsgFilterMax, wRemoveMsg);
}
static int WINAPI ShowCursor_hook(BOOL bShow) {
// prevent game from hiding cursor when option is enabled
if (GRAPHICS_SHOW_CURSOR && !bShow) {
return 1;
}
// call original
return ShowCursor_orig(bShow);
}
static HCURSOR WINAPI SetCursor_hook(HCURSOR hCursor) {
if (GRAPHICS_SHOW_CURSOR && hCursor == NULL) {
return GetCursor();
}
return SetCursor_orig(hCursor);
}
static LONG WINAPI SetWindowLongA_hook(HWND hWnd, int nIndex, LONG dwNewLong) {
// DDR window style fix
if (nIndex == GWL_STYLE && avs::game::is_model("MDX")) {
dwNewLong |= WS_OVERLAPPEDWINDOW;
}
// call original
return SetWindowLongA_orig(hWnd, nIndex, dwNewLong);
}
static LONG WINAPI SetWindowLongW_hook(HWND hWnd, int nIndex, LONG dwNewLong) {
// DDR overlapped window fix
if (nIndex == GWL_STYLE && avs::game::is_model("MDX")) {
dwNewLong |= WS_OVERLAPPEDWINDOW;
}
// call original
return SetWindowLongW_orig(hWnd, nIndex, dwNewLong);
}
static BOOL WINAPI SetWindowPos_hook(HWND hWnd, HWND hWndInsertAfter,
int X, int Y, int cx, int cy, UINT uFlags) {
// windowed mode adjustments
if (GRAPHICS_WINDOWED && (avs::game::is_model("LMA") || avs::game::is_model("MDX"))) {
return TRUE;
}
// prevent gitadora arena model from shifting windows around if the user has preferences
if (GRAPHICS_WINDOWED && games::gitadora::is_arena_model() &&
GRAPHICS_WINDOW_MAIN.has_value() && hWnd == GRAPHICS_WINDOW_MAIN.value() &&
cfg::SCREENRESIZE->enable_window_resize) {
return TRUE;
}
// call original
return SetWindowPos_orig(hWnd, hWndInsertAfter, X, Y, cx, cy, uFlags);
}
static ATOM WINAPI RegisterClassA_hook(const WNDCLASSA *lpWndClass) {
// check for null
if (!lpWndClass) {
return RegisterClassA_orig(lpWndClass);
}
// copy struct and use own icon
WNDCLASSA wnd = *lpWndClass;
wnd.hIcon = WINDOW_ICON;
// call original
return RegisterClassA_orig(&wnd);
}
static ATOM WINAPI RegisterClassExA_hook(const WNDCLASSEXA *Arg1) {
// check for null
if (!Arg1) {
return RegisterClassExA_orig(Arg1);
}
// copy struct and use own icon
WNDCLASSEXA wnd = *Arg1;
wnd.hIcon = WINDOW_ICON;
wnd.hIconSm = WINDOW_ICON;
// call original
return RegisterClassExA_orig(&wnd);
}
static ATOM WINAPI RegisterClassW_hook(const WNDCLASSW *lpWndClass) {
// check for null
if (!lpWndClass) {
return RegisterClassW_orig(lpWndClass);
}
// copy struct and use own icon
WNDCLASSW wnd = *lpWndClass;
wnd.hIcon = WINDOW_ICON;
// call original
return RegisterClassW_orig(&wnd);
}
static ATOM WINAPI RegisterClassExW_hook(const WNDCLASSEXW *Arg1) {
// check for null
if (!Arg1) {
return RegisterClassExW_orig(Arg1);
}
// copy struct and use own icon
WNDCLASSEXW wnd = *Arg1;
wnd.hIcon = WINDOW_ICON;
wnd.hIconSm = WINDOW_ICON;
// call original
return RegisterClassExW_orig(&wnd);
}
static HHOOK WINAPI SetWindowsHookExA_hook(int, HOOKPROC, HINSTANCE, DWORD) {
log_misc("graphics", "SetWindowsHookExA hook hit");
// we don't do hooks
return nullptr;
}
static BOOL WINAPI SetCursorPos_hook(int, int) {
// prevent games from messing with the cursor position themselves
return TRUE;
}
static int WINAPI MessageBoxA_hook(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) {
auto text = lpText != nullptr ? lpText : "(null)";
auto title = lpCaption != nullptr ? lpCaption : "(null)";
log_info("graphics", "MessageBoxA: {} - {}", title, text);
return IDOK;
}
static int WINAPI MessageBoxExA_hook(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType, WORD wLanguageId) {
auto text = lpText != nullptr ? lpText : "(null)";
auto title = lpCaption != nullptr ? lpCaption : "(null)";
log_info("graphics", "MessageBoxExA: {} - {}", title, text);
return IDOK;
}
static int WINAPI MessageBoxW_hook(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType) {
auto text = lpText != nullptr ? lpText : L"(null)";
auto title = lpCaption != nullptr ? lpCaption : L"(null)";
log_info("graphics", "MessageBoxW: {} - {}", ws2s(title), ws2s(text));
return IDOK;
}
static int WINAPI MessageBoxExW_hook(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType, WORD wLanguageId) {
auto text = lpText != nullptr ? lpText : L"(null)";
auto title = lpCaption != nullptr ? lpCaption : L"(null)";
log_info("graphics", "MessageBoxExW: {} - {}", ws2s(title), ws2s(text));
return IDOK;
}
void graphics_init() {
log_info("graphics", "initializing");
// init screen resize
log_info("ScreenResize", "initializing");
if(cfg::SCREENRESIZE == nullptr){
cfg::SCREENRESIZE = std::make_unique<cfg::ScreenResize>();
}
// init backends
graphics_d3d9_init();
// general hooks
ChangeDisplaySettingsA_orig = detour::iat_try("ChangeDisplaySettingsA", ChangeDisplaySettingsA_hook);
ChangeDisplaySettingsExA_orig = detour::iat_try("ChangeDisplaySettingsExA", ChangeDisplaySettingsExA_hook);
ClipCursor_orig = detour::iat_try("ClipCursor", ClipCursor_hook);
CreateWindowExA_orig = detour::iat_try("CreateWindowExA", CreateWindowExA_hook);
CreateWindowExW_orig = detour::iat_try("CreateWindowExW", CreateWindowExW_hook);
EnableWindow_orig = detour::iat_try("EnableWindow", EnableWindow_hook);
EnumDisplayDevicesA_orig = detour::iat_try("EnumDisplayDevicesA", EnumDisplayDevicesA_hook);
MoveWindow_orig = detour::iat_try("MoveWindow", MoveWindow_hook);
PeekMessageA_orig = detour::iat_try("PeekMessageA", PeekMessageA_hook);
RegisterClassA_orig = detour::iat_try("RegisterClassA", RegisterClassA_hook);
RegisterClassExA_orig = detour::iat_try("RegisterClassExA", RegisterClassExA_hook);
RegisterClassW_orig = detour::iat_try("RegisterClassW", RegisterClassW_hook);
RegisterClassExW_orig = detour::iat_try("RegisterClassExW", RegisterClassExW_hook);
ShowCursor_orig = detour::iat_try("ShowCursor", ShowCursor_hook);
SetCursor_orig = detour::iat_try("SetCursor", SetCursor_hook);
SetWindowLongA_orig = detour::iat_try("SetWindowLongA", SetWindowLongA_hook);
SetWindowLongW_orig = detour::iat_try("SetWindowLongW", SetWindowLongW_hook);
SetWindowPos_orig = detour::iat_try("SetWindowPos", SetWindowPos_hook);
detour::iat_try("MessageBoxA", MessageBoxA_hook);
detour::iat_try("MessageBoxExA", MessageBoxExA_hook);
detour::iat_try("MessageBoxW", MessageBoxW_hook);
detour::iat_try("MessageBoxExW", MessageBoxExW_hook);
detour::iat_try("SetWindowsHookExA", SetWindowsHookExA_hook);
detour::iat_try("SetCursorPos", SetCursorPos_hook);
}
void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParameters) {
// update window size for a few games
// TODO: make this work on everything
if (pPresentationParameters != nullptr && GRAPHICS_WINDOWED
&& (avs::game::is_model({ "K39", "L39", "M39", "JMP", "LDJ" }))) {
// check dimensions
auto new_width = pPresentationParameters->BackBufferWidth;
auto new_height = pPresentationParameters->BackBufferHeight;
if (new_width != 0 && new_height != 0) {
RECT rect {};
GetWindowRect(hWnd, &rect);
auto width = rect.right - rect.left;
auto height = rect.bottom - rect.top;
log_info("graphics", "resized window: {}x{} -> {}x{}", width, height, new_width, new_height);
DWORD dwStyle = GetWindowLongA(hWnd, GWL_STYLE);
DWORD dwExStyle = GetWindowLongA(hWnd, GWL_EXSTYLE);
HMENU menu = GetMenu(hWnd);
SetRect(&rect, 0, 0, new_width, new_height);
AdjustWindowRectEx(&rect, dwStyle, (menu != nullptr), dwExStyle);
// make sure the window does not go off the screen
if (rect.top < 0) {
rect.bottom += -rect.top;
rect.top = 0;
}
width = rect.right - rect.left;
height = rect.bottom - rect.top;
SetWindowPos(hWnd, HWND_TOP, rect.left, rect.top, width, height, 0);
}
}
// show cursor
if (GRAPHICS_SHOW_CURSOR) {
ShowCursor(TRUE);
}
// capture mouse
if (GRAPHICS_CAPTURE_CURSOR) {
RECT rect {};
GetWindowRect(hWnd, &rect);
ClipCursor(&rect);
}
// hook window procedure
if (WNDPROC_ORIG == nullptr) {
WNDPROC_ORIG = reinterpret_cast<WNDPROC>(GetWindowLongPtrA(hWnd, GWLP_WNDPROC));
SetWindowLongPtrA(hWnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(WindowProc));
// NOLEGACY causes WM_CHAR to be not received
// reflec beat game engine does not pass WM_CHAR through for some reason (unrelated to SpiceTouch)
if (!rawinput::NOLEGACY && !(avs::game::is_model({"KBR", "LBR", "MBR"}))) {
overlay::USE_WM_CHAR_FOR_IMGUI_CHAR_INPUT = true;
}
graphics_capture_initial_window(hWnd);
}
}
void graphics_add_wnd_proc(WNDPROC wnd_proc) {
WNDPROC_CUSTOM.push_back(wnd_proc);
}
void graphics_remove_wnd_proc(WNDPROC wndProc) {
for (size_t x = 0; x < WNDPROC_CUSTOM.size(); x++) {
if (WNDPROC_CUSTOM[x] == wndProc) {
WNDPROC_CUSTOM.erase(WNDPROC_CUSTOM.begin() + x);
}
}
}
void graphics_hook_subscreen_window(HWND hWnd) {
// hook window procedure
if (WSUB_WNDPROC_ORIG == nullptr) {
WSUB_WNDPROC_ORIG = reinterpret_cast<WNDPROC>(GetWindowLongPtrA(hWnd, GWLP_WNDPROC));
SetWindowLongPtrA(hWnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(WsubWindowProc));
}
if (GRAPHICS_WSUB_ALWAYS_ON_TOP) {
graphics_update_z_order(hWnd, true);
}
}
void graphics_screens_register(int screen) {
std::lock_guard<std::mutex> lock(GRAPHICS_SCREENS_M);
GRAPHICS_SCREENS.insert(screen);
}
void graphics_screens_unregister(int screen) {
std::lock_guard<std::mutex> lock(GRAPHICS_SCREENS_M);
GRAPHICS_SCREENS.erase(screen);
}
void graphics_screens_get(std::vector<int> &screens) {
std::lock_guard<std::mutex> lock(GRAPHICS_SCREENS_M);
screens.insert(screens.end(), GRAPHICS_SCREENS.begin(), GRAPHICS_SCREENS.end());
}
void graphics_screenshot_trigger() {
GRAPHICS_SCREENSHOT_TRIGGER = true;
}
bool graphics_screenshot_consume() {
auto flag = GRAPHICS_SCREENSHOT_TRIGGER;
GRAPHICS_SCREENSHOT_TRIGGER = false;
return flag;
}
void graphics_capture_trigger(int screen) {
std::lock_guard<std::mutex> lock(GRAPHICS_CAPTURE_SCREENS_M);
GRAPHICS_CAPTURE_SCREENS.push_back(screen);
}
bool graphics_capture_consume(int *screen) {
auto flag = !GRAPHICS_CAPTURE_SCREENS.empty();
if (flag) {
std::lock_guard<std::mutex> lock(GRAPHICS_CAPTURE_SCREENS_M);
*screen = GRAPHICS_CAPTURE_SCREENS.back();
GRAPHICS_CAPTURE_SCREENS.pop_back();
}
return flag;
}
void graphics_capture_enqueue(int screen, uint8_t *data, size_t width, size_t height) {
GRAPHICS_CAPTURE_BUFFER_M[screen].lock();
auto &capture = GRAPHICS_CAPTURE_BUFFER[screen];
capture.data.reset(data);
capture.width = width;
capture.height = height;
capture.timestamp = get_performance_milliseconds();
GRAPHICS_CAPTURE_BUFFER_M[screen].unlock();
GRAPHICS_CAPTURE_CV[screen].notify_one();
}
void graphics_capture_skip(int screen) {
GRAPHICS_CAPTURE_CV[screen].notify_one();
}
bool graphics_capture_receive_jpeg(int screen, TooJpeg::WRITE_ONE_BYTE receiver,
bool rgb, int quality, bool downsample, int divide, uint64_t *timestamp,
int *width, int *height) {
// wait for capture event
std::unique_lock<std::mutex> lock(GRAPHICS_CAPTURE_BUFFER_M[screen]);
GRAPHICS_CAPTURE_CV[screen].wait(lock, [screen] {
return GRAPHICS_CAPTURE_BUFFER[screen].data != nullptr;
});
auto &capture = GRAPHICS_CAPTURE_BUFFER[screen];
auto capture_data = capture.data;
auto capture_width = capture.width;
auto capture_height = capture.height;
auto capture_timestamp = capture.timestamp;
lock.unlock();
// validate data
if (!capture_data || !capture_width || !capture_height) {
return false;
}
// divide image size
if (divide > 1) {
// get new resolution (round up)
int width_new = (capture_width + divide - 1) / divide;
int height_new = (capture_height + divide - 1) / divide;
// allocate new data
auto data_old = capture_data.get();
auto data_new = new uint8_t[width_new * height_new * 3];
// copy pixel data
int data_y = 0;
for (int y = 0; y < capture_height; y += divide) {
int data_y_offset_old = y * capture_width;
int data_y_offset = data_y * width_new;
int data_x = 0;
for (int x = 0; x < capture_width; x += divide) {
auto pixel_new = &data_new[(data_x + data_y_offset) * 3];
auto pixel_old = &data_old[(data_y_offset_old + x) * 3];
memcpy(pixel_new, pixel_old, 3);
data_x++;
}
data_y++;
}
// update capture data
capture_data.reset(data_new);
capture_width = width_new;
capture_height = height_new;
}
// compress
auto success = TooJpeg::writeJpeg(
receiver, capture_data.get(),
capture_width, capture_height,
rgb, quality, downsample);
// status
if (timestamp) {
*timestamp = capture_timestamp;
}
if (width) {
*width = capture_width;
}
if (height) {
*height = capture_height;
}
// clean up
return success;
}
std::string graphics_screenshot_genpath() {
// verify dir path
if (GRAPHICS_SCREENSHOT_DIR.empty()) {
return "";
} else {
auto last_char = GRAPHICS_SCREENSHOT_DIR.back();
if (last_char == '\\' || last_char == '/') {
GRAPHICS_SCREENSHOT_DIR.pop_back();
}
}
// ensure the output directory exists
if (!fileutils::dir_exists(GRAPHICS_SCREENSHOT_DIR)) {
if (!fileutils::dir_create_recursive(GRAPHICS_SCREENSHOT_DIR)) {
log_warning("graphics", "could not create screenshot dir: {}", GRAPHICS_SCREENSHOT_DIR);
return "";
}
}
// generate date prefix
auto t_now = std::time(nullptr);
auto tm_now = *std::gmtime(&t_now);
auto prefix = to_string(std::put_time(&tm_now, "%Y%m%d"));
// find next filename
size_t id = 0;
while (true) {
auto filepath = fmt::format("{}\\{}_{}.png", GRAPHICS_SCREENSHOT_DIR, prefix, id);
if (!fileutils::file_exists(filepath)) {
return filepath;
}
id++;
}
}