#include #include "graphics.h" #include #include #include #include #include "avs/game.h" #include "cfg/icon.h" #include "cfg/screen_resize.h" #include "games/ddr/ddr.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 data; unsigned short width, height; uint64_t timestamp; }; HWND TDJ_SUBSCREEN_WINDOW = nullptr; HWND SDVX_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_CUSTOM {}; static bool GRAPHICS_SCREENSHOT_TRIGGER = false; static std::set GRAPHICS_SCREENS { 0 }; static std::mutex GRAPHICS_SCREENS_M {}; static std::vector 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 GRAPHICS_WINDOWS; UINT GRAPHICS_FORCE_REFRESH = 0; 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; // 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 if (avs::game::is_model({"J32", "J33", "K32", "K33", "L32", "L33", "M32"})) { // set window name if (!lpWindowName) { lpWindowName = "GITADORA"; } } 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"); // TDJ windowed mode with subscreen: hide maximize button if ((is_tdj_sub_window && GRAPHICS_IIDX_WSUB) || is_sdvx_sub_window) { dwStyle &= ~(WS_MAXIMIZEBOX); } // call original HWND result = CreateWindowExA_orig(dwExStyle, lpClassName, lpWindowName, dwStyle, x, y, nWidth, nHeight, hWndParent, hMenu, hInstance, lpParam); GRAPHICS_WINDOWS.push_back(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); } disable_touch_indicators(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); 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_IIDX_WSUB_WIDTH, GRAPHICS_IIDX_WSUB_HEIGHT); AdjustWindowRect(&rect, dwStyle, 0); X = GRAPHICS_IIDX_WSUB_X; Y = GRAPHICS_IIDX_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; } // 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(); } // 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(GetWindowLongPtrA(hWnd, GWLP_WNDPROC)); SetWindowLongPtrA(hWnd, GWLP_WNDPROC, reinterpret_cast(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(GetWindowLongPtrA(hWnd, GWLP_WNDPROC)); SetWindowLongPtrA(hWnd, GWLP_WNDPROC, reinterpret_cast(WsubWindowProc)); } } void graphics_screens_register(int screen) { std::lock_guard lock(GRAPHICS_SCREENS_M); GRAPHICS_SCREENS.insert(screen); } void graphics_screens_unregister(int screen) { std::lock_guard lock(GRAPHICS_SCREENS_M); GRAPHICS_SCREENS.erase(screen); } void graphics_screens_get(std::vector &screens) { std::lock_guard 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 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 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 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++; } }