diff --git a/OptiScaler.ini b/OptiScaler.ini index 131e4197..27c4d52a 100644 --- a/OptiScaler.ini +++ b/OptiScaler.ini @@ -1339,6 +1339,12 @@ CheckForUpdate=auto ; true or false - Default (auto) is false, except for OptiFG when true DisableOverlays=auto +; Use manual input polling instead of hooking WndProc +; Might help games which does not capture inputs properly +; As a downside it will not be able to block inputs when menu is open +; true or false - Default (auto) is false +ManualInputPolling=auto + ; Simulate waitable object event signals for FG swapchain ; true or false - Default (auto) is false SimulateWaitableObject=auto diff --git a/OptiScaler/Config.cpp b/OptiScaler/Config.cpp index 5678d34a..9851a4aa 100644 --- a/OptiScaler/Config.cpp +++ b/OptiScaler/Config.cpp @@ -561,6 +561,7 @@ bool Config::Reload(std::filesystem::path iniPath) { CheckForUpdate.set_from_config(readBool("Hotfix", "CheckForUpdate")); DisableOverlays.set_from_config(readBool("Hotfix", "DisableOverlays")); + ManualInputPolling.set_from_config(readBool("Hotfix", "ManualInputPolling")); SimulateWaitableObject.set_from_config(readBool("Hotfix", "SimulateWaitableObject")); @@ -1199,6 +1200,8 @@ bool Config::SaveIni() ini.SetValue("Hotfix", "SimulateWaitableObject", GetBoolValue(Instance()->SimulateWaitableObject.value_for_config()).c_str()); ini.SetValue("Hotfix", "DisableOverlays", GetBoolValue(Instance()->DisableOverlays.value_for_config()).c_str()); + ini.SetValue("Hotfix", "ManualInputPolling", + GetBoolValue(Instance()->ManualInputPolling.value_for_config()).c_str()); ini.SetValue("Hotfix", "RoundInternalResolution", GetIntValue(Instance()->RoundInternalResolution.value_for_config()).c_str()); diff --git a/OptiScaler/Config.h b/OptiScaler/Config.h index 4d5ad60e..304973e0 100644 --- a/OptiScaler/Config.h +++ b/OptiScaler/Config.h @@ -350,6 +350,7 @@ class Config // Hotfixes CustomOptional CheckForUpdate { true }; CustomOptional DisableOverlays { false }; + CustomOptional ManualInputPolling { false }; CustomOptional SimulateWaitableObject { false }; diff --git a/OptiScaler/dllmain.cpp b/OptiScaler/dllmain.cpp index 0d78a2ac..55c78202 100644 --- a/OptiScaler/dllmain.cpp +++ b/OptiScaler/dllmain.cpp @@ -1247,6 +1247,9 @@ static void printQuirks(flag_set& quirks) if (quirks & GameQuirk::PregmataFixDLSSModes) stringQuirks.push_back("Fix DLSS quality selection in Pragmata"); + if (quirks & GameQuirk::UseManualInputs) + stringQuirks.push_back("Use manual input polling"); + state->detectedQuirks.append_range(stringQuirks); for (auto& stringQuirk : stringQuirks) spdlog::info("Quirk: {}", stringQuirk); @@ -1500,6 +1503,13 @@ static void CheckQuirks() else quirks.reset(GameQuirk::OldOverlayMenu); + if (quirks & GameQuirk::UseManualInputs && !Config::Instance()->ManualInputPolling.has_value()) + { + Config::Instance()->ManualInputPolling.set_volatile_value(true); + } + else + quirks.reset(GameQuirk::UseManualInputs); + // For Luma, we assume if Luma addon in game folder it's used const auto dir = Util::ExePath().parent_path(); bool lumaDetected = false; diff --git a/OptiScaler/menu/menu_common.cpp b/OptiScaler/menu/menu_common.cpp index a6342044..1f03b119 100644 --- a/OptiScaler/menu/menu_common.cpp +++ b/OptiScaler/menu/menu_common.cpp @@ -38,6 +38,7 @@ static bool inputMenu = false; static bool inputFG = false; static bool inputFps = false; static bool inputFpsCycle = false; +static bool inputManual = false; static bool hasGamepad = false; static bool fsr31InitTried = false; static bool xefgInitTried = false; @@ -133,6 +134,7 @@ static std::string updateNoticeTag; static std::string updateNoticeUrl; static float lastMenuScale = 0.0f; static CustomOptional comboPreset { 0 }; +static int lastKey = 0; template struct RingBuffer { @@ -208,6 +210,120 @@ inline std::string StrFmt(const char* fmt, ...) return out; } +bool IsKeyReleasedOnce(int vk) +{ + static bool previousDown[256] {}; + + if (vk <= 0 || vk >= 256) + return false; + + bool isDown = (GetAsyncKeyState(vk) & 0x8000) != 0; + bool released = previousDown[vk] && !isDown; + + previousDown[vk] = isDown; + + return released; +} + +void UpdateManualInput(HWND targetHwnd) +{ + ImGuiIO& io = ImGui::GetIO(); + + // Only capture input when target window is foreground + HWND foreground = GetForegroundWindow(); + bool focused = foreground == targetHwnd; + + io.AddFocusEvent(focused); + + if (!focused) + { + io.AddMousePosEvent(-FLT_MAX, -FLT_MAX); + return; + } + + const auto config = Config::Instance(); + + auto CheckShortcut = [&](int vk, bool& inputFlag, const char* logMessage) + { + if (inputFlag) + return; + + if (vk <= 0 || vk >= 256) + return; + + if (IsKeyReleasedOnce(vk)) + { + lastKey = vk; + receivingWmInputs = false; + inputFlag = true; + LOG_DEBUG("{}", logMessage); + } + }; + + CheckShortcut(config->ShortcutKey.value_or_default(), inputMenu, "Menu key pressed, will be switching menu"); + + CheckShortcut(config->FpsShortcutKey.value_or_default(), inputFps, "Menu key pressed, will be switching FPS"); + + CheckShortcut(config->FGShortcutKey.value_or_default(), inputFG, "Menu key pressed, will be switching FG mode"); + + CheckShortcut(config->FpsCycleShortcutKey.value_or_default(), inputFpsCycle, + "Menu key pressed, will be switching FPS mode"); + + // Mouse position + POINT cursorPos {}; + GetCursorPos(&cursorPos); + + POINT clientPos = cursorPos; + ScreenToClient(targetHwnd, &clientPos); + + io.AddMousePosEvent(static_cast(clientPos.x), static_cast(clientPos.y)); + + // Mouse buttons + io.AddMouseButtonEvent(0, (GetAsyncKeyState(VK_LBUTTON) & 0x8000) != 0); + io.AddMouseButtonEvent(1, (GetAsyncKeyState(VK_RBUTTON) & 0x8000) != 0); + io.AddMouseButtonEvent(2, (GetAsyncKeyState(VK_MBUTTON) & 0x8000) != 0); + io.AddMouseButtonEvent(3, (GetAsyncKeyState(VK_XBUTTON1) & 0x8000) != 0); + io.AddMouseButtonEvent(4, (GetAsyncKeyState(VK_XBUTTON2) & 0x8000) != 0); + + // Common keyboard keys + auto AddKey = [&](ImGuiKey key, int vk) { io.AddKeyEvent(key, (GetAsyncKeyState(vk) & 0x8000) != 0); }; + + AddKey(ImGuiKey_Tab, VK_TAB); + AddKey(ImGuiKey_LeftArrow, VK_LEFT); + AddKey(ImGuiKey_RightArrow, VK_RIGHT); + AddKey(ImGuiKey_UpArrow, VK_UP); + AddKey(ImGuiKey_DownArrow, VK_DOWN); + AddKey(ImGuiKey_PageUp, VK_PRIOR); + AddKey(ImGuiKey_PageDown, VK_NEXT); + AddKey(ImGuiKey_Home, VK_HOME); + AddKey(ImGuiKey_End, VK_END); + AddKey(ImGuiKey_Insert, VK_INSERT); + AddKey(ImGuiKey_Delete, VK_DELETE); + AddKey(ImGuiKey_Backspace, VK_BACK); + AddKey(ImGuiKey_Space, VK_SPACE); + AddKey(ImGuiKey_Enter, VK_RETURN); + AddKey(ImGuiKey_Escape, VK_ESCAPE); + + AddKey(ImGuiKey_LeftCtrl, VK_LCONTROL); + AddKey(ImGuiKey_LeftShift, VK_LSHIFT); + AddKey(ImGuiKey_LeftAlt, VK_LMENU); + AddKey(ImGuiKey_RightCtrl, VK_RCONTROL); + AddKey(ImGuiKey_RightShift, VK_RSHIFT); + AddKey(ImGuiKey_RightAlt, VK_RMENU); + + // Letters + for (int vk = 'A'; vk <= 'Z'; vk++) + { + io.AddKeyEvent(static_cast(ImGuiKey_A + (vk - 'A')), (GetAsyncKeyState(vk) & 0x8000) != 0); + } + + // Numbers + for (int vk = '0'; vk <= '9'; vk++) + { + io.AddKeyEvent(static_cast(ImGuiKey_0 + (vk - '0')), (GetAsyncKeyState(vk) & 0x8000) != 0); + } +} + void MenuCommon::ShowTooltip(const char* tip) { if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) @@ -637,8 +753,6 @@ ImGuiKey MenuCommon::ImGui_ImplWin32_VirtualKeyToImGuiKey(WPARAM wParam) } } -static int lastKey = 0; - class Keybind { std::string name; @@ -1874,6 +1988,9 @@ void MenuCommon::Present() lastFrameTime = now - lastTime; lastTime = now; + + if (inputManual && _handle != nullptr) + UpdateManualInput(_handle); } bool MenuCommon::RenderMenu() @@ -1900,6 +2017,9 @@ bool MenuCommon::RenderMenu() } lastTime = now; + + if (inputManual && _handle != nullptr) + UpdateManualInput(_handle); } else { @@ -7193,35 +7313,54 @@ void MenuCommon::Init(HWND InHwnd, bool isUWP) _hdrTonemapApplied = false; } - if ((_oWndProc == nullptr || oldHandle != _handle) && !isUWP) + DWORD hwndPid = 0; + DWORD hwndTid = GetWindowThreadProcessId(_handle, &hwndPid); + + LOG_DEBUG("HWND: {:X}, IsWindow: {}, HWND PID: {}, Current PID: {}, HWND TID: {}, Current TID: {}", + (ULONG64) _handle, IsWindow(_handle), hwndPid, GetCurrentProcessId(), hwndTid, GetCurrentThreadId()); + + if (hwndPid == GetCurrentProcessId() && !Config::Instance()->ManualInputPolling.value_or_default()) { - if (oldHandle != nullptr && _oWndProc != nullptr) + inputManual = false; + + if ((_oWndProc == nullptr || oldHandle != _handle) && !isUWP) { - LOG_DEBUG("Restoring old WndProc: {:X}", (ULONG64) _oWndProc); + if (oldHandle != nullptr && _oWndProc != nullptr) + { + LOG_DEBUG("Restoring old WndProc: {:X}", (ULONG64) _oWndProc); + + SetLastError(0); + auto restoreResult = SetWindowLongPtr(oldHandle, GWLP_WNDPROC, (LONG_PTR) _oWndProc); + auto error = GetLastError(); + + if (restoreResult == 0 && error != 0) + { + LOG_ERROR("Failed to restore old WndProc. Error: {:X}", error); + } + } SetLastError(0); - auto restoreResult = SetWindowLongPtr(oldHandle, GWLP_WNDPROC, (LONG_PTR) _oWndProc); + auto setResult = (WNDPROC) SetWindowLongPtr(_handle, GWLP_WNDPROC, (LONG_PTR) WndProc); auto error = GetLastError(); - if (restoreResult == 0 && error != 0) + if (setResult == nullptr && error != 0) { - LOG_ERROR("Failed to restore old WndProc. Error: {:X}", error); + LOG_ERROR("Failed to hook WndProc. Error: {:X}", error); + } + else + { + _oWndProc = setResult; + LOG_DEBUG("_oWndProc: {:X}", (ULONG64) _oWndProc); } } + } + else + { + LOG_WARN("HWND does not belong to current process," + " Manual input polling will be used. HWND PID: {}, Current PID: {}", + hwndPid, GetCurrentProcessId()); - SetLastError(0); - auto setResult = (WNDPROC) SetWindowLongPtr(_handle, GWLP_WNDPROC, (LONG_PTR) WndProc); - auto error = GetLastError(); - - if (setResult == nullptr && error != 0) - { - LOG_ERROR("Failed to hook WndProc. Error: {:X}", error); - } - else - { - _oWndProc = setResult; - LOG_DEBUG("_oWndProc: {:X}", (ULONG64) _oWndProc); - } + inputManual = true; } if (!pfn_SetCursorPos_hooked) diff --git a/OptiScaler/misc/Quirks.h b/OptiScaler/misc/Quirks.h index 60811de6..8ebcbb0c 100644 --- a/OptiScaler/misc/Quirks.h +++ b/OptiScaler/misc/Quirks.h @@ -42,6 +42,7 @@ enum class GameQuirk : uint64_t DoNotPreserveFGSwapChain, DoNotSkipResize, OldOverlayMenu, + UseManualInputs, // Quirks that are applied deeper in code CyberpunkHudlessState, @@ -280,7 +281,7 @@ static const QuirkEntry quirkTable[] = { // Metro Exodus Enhanced Edition // ForceBorderless required to avoid black screen with XeFG QUIRK_ENTRY("metroexodus.exe", GameQuirk::DisableDxgiSpoofing, GameQuirk::ForceBorderlessWhenUsingXeFG, - GameQuirk::ForceAutoExposure), + GameQuirk::ForceAutoExposure, GameQuirk::UseManualInputs), // Star Wars: Outlaws // SL spoof enough to unlock everything DLSS