From 7dbc379d807640a1b8e7b69da855aee68eff72d3 Mon Sep 17 00:00:00 2001 From: FakeMichau <49685661+FakeMichau@users.noreply.github.com> Date: Tue, 1 Sep 2026 03:40:48 +0200 Subject: [PATCH 1/8] Avoid a potential use after free with some shaders --- OptiScaler/shaders/hudless_compare/HC_Dx12.cpp | 18 +++++++++--------- OptiScaler/shaders/render_ui/RUI_Dx12.cpp | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/OptiScaler/shaders/hudless_compare/HC_Dx12.cpp b/OptiScaler/shaders/hudless_compare/HC_Dx12.cpp index f16396b1d..b26f45eef 100644 --- a/OptiScaler/shaders/hudless_compare/HC_Dx12.cpp +++ b/OptiScaler/shaders/hudless_compare/HC_Dx12.cpp @@ -7,6 +7,8 @@ #include +using Microsoft::WRL::ComPtr; + inline static int GetFormatGroup(DXGI_FORMAT format) { switch (format) @@ -228,7 +230,7 @@ bool HC_Dx12::Dispatch(IDXGISwapChain3* sc, ID3D12GraphicsCommandList* cmdList, } // Get SwapChain Buffer - ID3D12Resource* scBuffer = nullptr; + ComPtr scBuffer; auto scIndex = sc->GetCurrentBackBufferIndex(); auto result = sc->GetBuffer(scIndex, IID_PPV_ARGS(&scBuffer)); @@ -238,8 +240,6 @@ bool HC_Dx12::Dispatch(IDXGISwapChain3* sc, ID3D12GraphicsCommandList* cmdList, return false; } - scBuffer->Release(); - // Check Hudless Buffer D3D12_RESOURCE_DESC hudlessDesc = hudless->GetDesc(); @@ -252,7 +252,7 @@ bool HC_Dx12::Dispatch(IDXGISwapChain3* sc, ID3D12GraphicsCommandList* cmdList, _counter++; _counter = _counter % HC_NUM_OF_HEAPS; - if (!CreateBufferResource(_counter, _device, scBuffer, D3D12_RESOURCE_STATE_COPY_DEST)) + if (!CreateBufferResource(_counter, _device, scBuffer.Get(), D3D12_RESOURCE_STATE_COPY_DEST)) { LOG_ERROR("CreateBufferResource error!"); return false; @@ -260,12 +260,12 @@ bool HC_Dx12::Dispatch(IDXGISwapChain3* sc, ID3D12GraphicsCommandList* cmdList, // Copy Swapchain Buffer to read buffer SetBufferState(_counter, cmdList, D3D12_RESOURCE_STATE_COPY_DEST); - ResourceBarrier(cmdList, scBuffer, D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_COPY_SOURCE); + ResourceBarrier(cmdList, scBuffer.Get(), D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_COPY_SOURCE); if (_buffer[_counter] != nullptr) - cmdList->CopyResource(_buffer[_counter], scBuffer); + cmdList->CopyResource(_buffer[_counter], scBuffer.Get()); - ResourceBarrier(cmdList, scBuffer, D3D12_RESOURCE_STATE_COPY_SOURCE, D3D12_RESOURCE_STATE_RENDER_TARGET); + ResourceBarrier(cmdList, scBuffer.Get(), D3D12_RESOURCE_STATE_COPY_SOURCE, D3D12_RESOURCE_STATE_RENDER_TARGET); SetBufferState(_counter, cmdList, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); if (state != D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE) @@ -285,7 +285,7 @@ bool HC_Dx12::Dispatch(IDXGISwapChain3* sc, ID3D12GraphicsCommandList* cmdList, CreateShaderResourceView(_device, hudless, currentHeap.GetSrvCPU(0)); CreateShaderResourceView(_device, _buffer[_counter], currentHeap.GetSrvCPU(1)); - CreateRenderTargetView(_device, scBuffer, currentHeap.GetRtvCPU(0), 0); + CreateRenderTargetView(_device, scBuffer.Get(), currentHeap.GetRtvCPU(0), 0); InternalCompareParams constants {}; constants.DiffThreshold = 0.003f; @@ -325,7 +325,7 @@ bool HC_Dx12::Dispatch(IDXGISwapChain3* sc, ID3D12GraphicsCommandList* cmdList, cmdList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); cmdList->DrawInstanced(3, 1, 0, 0); - ResourceBarrier(cmdList, scBuffer, D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT); + ResourceBarrier(cmdList, scBuffer.Get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT); if (state != D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE) ResourceBarrier(cmdList, hudless, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, state); diff --git a/OptiScaler/shaders/render_ui/RUI_Dx12.cpp b/OptiScaler/shaders/render_ui/RUI_Dx12.cpp index eb4178dbe..6398aafc0 100644 --- a/OptiScaler/shaders/render_ui/RUI_Dx12.cpp +++ b/OptiScaler/shaders/render_ui/RUI_Dx12.cpp @@ -9,6 +9,8 @@ #include +using Microsoft::WRL::ComPtr; + bool RUI_Dx12::CreateBufferResource(UINT index, ID3D12Device* InDevice, ID3D12Resource* InSource, D3D12_RESOURCE_STATES InState) { @@ -171,7 +173,7 @@ bool RUI_Dx12::Dispatch(IDXGISwapChain3* sc, ID3D12GraphicsCommandList* cmdList, } // Get SwapChain Buffer - ID3D12Resource* scBuffer = nullptr; + ComPtr scBuffer; auto scIndex = sc->GetCurrentBackBufferIndex(); auto result = sc->GetBuffer(scIndex, IID_PPV_ARGS(&scBuffer)); @@ -181,8 +183,6 @@ bool RUI_Dx12::Dispatch(IDXGISwapChain3* sc, ID3D12GraphicsCommandList* cmdList, return false; } - scBuffer->Release(); - // Check Hudless Buffer D3D12_RESOURCE_DESC hudlessDesc = hudless->GetDesc(); @@ -195,7 +195,7 @@ bool RUI_Dx12::Dispatch(IDXGISwapChain3* sc, ID3D12GraphicsCommandList* cmdList, _counter++; _counter = _counter % HC_NUM_OF_HEAPS; - if (!CreateBufferResource(_counter, _device, scBuffer, D3D12_RESOURCE_STATE_COPY_DEST)) + if (!CreateBufferResource(_counter, _device, scBuffer.Get(), D3D12_RESOURCE_STATE_COPY_DEST)) { LOG_ERROR("CreateBufferResource error!"); return false; @@ -203,12 +203,12 @@ bool RUI_Dx12::Dispatch(IDXGISwapChain3* sc, ID3D12GraphicsCommandList* cmdList, // Copy Swapchain Buffer to read buffer SetBufferState(_counter, cmdList, D3D12_RESOURCE_STATE_COPY_DEST); - ResourceBarrier(cmdList, scBuffer, D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_COPY_SOURCE); + ResourceBarrier(cmdList, scBuffer.Get(), D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_COPY_SOURCE); if (_buffer[_counter] != nullptr) - cmdList->CopyResource(_buffer[_counter], scBuffer); + cmdList->CopyResource(_buffer[_counter], scBuffer.Get()); - ResourceBarrier(cmdList, scBuffer, D3D12_RESOURCE_STATE_COPY_SOURCE, D3D12_RESOURCE_STATE_RENDER_TARGET); + ResourceBarrier(cmdList, scBuffer.Get(), D3D12_RESOURCE_STATE_COPY_SOURCE, D3D12_RESOURCE_STATE_RENDER_TARGET); SetBufferState(_counter, cmdList, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); if (state != D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE) @@ -223,7 +223,7 @@ bool RUI_Dx12::Dispatch(IDXGISwapChain3* sc, ID3D12GraphicsCommandList* cmdList, // Create views CreateShaderResourceView(_device, hudless, currentHeap.GetSrvCPU(0)); CreateShaderResourceView(_device, _buffer[_counter], currentHeap.GetSrvCPU(1)); - CreateRenderTargetView(_device, scBuffer, currentHeap.GetRtvCPU(0), 0); + CreateRenderTargetView(_device, scBuffer.Get(), currentHeap.GetRtvCPU(0), 0); ID3D12DescriptorHeap* heaps[] = { currentHeap.GetHeapCSU() }; cmdList->SetDescriptorHeaps(_countof(heaps), heaps); @@ -253,7 +253,7 @@ bool RUI_Dx12::Dispatch(IDXGISwapChain3* sc, ID3D12GraphicsCommandList* cmdList, cmdList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); cmdList->DrawInstanced(3, 1, 0, 0); - ResourceBarrier(cmdList, scBuffer, D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT); + ResourceBarrier(cmdList, scBuffer.Get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT); if (state != D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE) ResourceBarrier(cmdList, hudless, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, state); From d7a4ecfae30067c702abaca58fdd4d3feeefc4b5 Mon Sep 17 00:00:00 2001 From: levzzz <86072227+levzzz5154@users.noreply.github.com> Date: Tue, 1 Sep 2026 20:38:23 +0500 Subject: [PATCH 2/8] Fix GPU probe recursion with dxvk-nvapi (issue #1101) queryNvapi() calls NvAPI_Initialize, which (via dxvk-nvapi) internally creates a DXGI factory and enumerates adapters. That enumeration hits OptiScaler's hooked EnumAdapters/EnumAdapters1, which calls getAllGpus() again while is_fetching is already set and returns an empty list. dxvk-nvapi then sees zero adapters, NvAPI_Initialize fails, and dlssCapable is never set - hiding DLSS in games like Death Stranding 2. Wrap queryNvapi in ScopedSkipDxgiLoadChecks so nvapi's internal DXGI enumeration passes through to the real adapters. --- OptiScaler/misc/IdentifyGpu.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/OptiScaler/misc/IdentifyGpu.cpp b/OptiScaler/misc/IdentifyGpu.cpp index d2c3f8106..da51cd833 100644 --- a/OptiScaler/misc/IdentifyGpu.cpp +++ b/OptiScaler/misc/IdentifyGpu.cpp @@ -194,6 +194,12 @@ std::vector IdentifyGpu::checkGpuInfo() void IdentifyGpu::queryNvapi(GpuInformation& gpuInfo) { + // Prevent recursion: NvAPI_Initialize (dxvk-nvapi) internally creates a DXGI factory + // and enumerates adapters, which re-enters our hooked EnumAdapters/EnumAdapters1 and + // calls getAllGpus() again while is_fetching is already set (returns empty list). + // Skip the DXGI load checks so nvapi sees the real adapters, allowing init to succeed. + ScopedSkipDxgiLoadChecks skipDxgiLoadChecks {}; + auto nvapiModule = NtdllProxy::LoadLibraryExW_Ldr(L"nvapi64.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32); // No nvapi, should not be nvidia, possibly external spoofing From c4c57a911982827fe2b24b75d98258d99dd4de3e Mon Sep 17 00:00:00 2001 From: cdozdil Date: Wed, 2 Sep 2026 10:25:08 +0300 Subject: [PATCH 3/8] More Dx11 resource management improvements --- OptiScaler/upscalers/IFeature_Dx11.cpp | 101 +++++++++----------- OptiScaler/upscalers/IFeature_Dx11wDx12.cpp | 63 +++++------- 2 files changed, 70 insertions(+), 94 deletions(-) diff --git a/OptiScaler/upscalers/IFeature_Dx11.cpp b/OptiScaler/upscalers/IFeature_Dx11.cpp index 6b6f6551e..90b90dab9 100644 --- a/OptiScaler/upscalers/IFeature_Dx11.cpp +++ b/OptiScaler/upscalers/IFeature_Dx11.cpp @@ -2,6 +2,8 @@ #include "IFeature_Dx11.h" #include +using Microsoft::WRL::ComPtr; + bool IFeature_Dx11::Init(ID3D11Device* InDevice, ID3D11DeviceContext* InContext, NVSDK_NGX_Parameter* InParameters) { if (InDevice == nullptr) @@ -37,66 +39,49 @@ bool IFeature_Dx11::Init(ID3D11Device* InDevice, ID3D11DeviceContext* InContext, return result; } -bool IFeature_Dx11::Evaluate(ID3D11DeviceContext* DeviceContext, NVSDK_NGX_Parameter* InParameters) +bool IFeature_Dx11::Evaluate(ID3D11DeviceContext* InDeviceContext, NVSDK_NGX_Parameter* InParameters) { - ID3D11ShaderResourceView* restoreSRVs[D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT] = {}; - ID3D11SamplerState* restoreSamplerStates[D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT] = {}; - ID3D11Buffer* restoreCBVs[D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT] = {}; - ID3D11UnorderedAccessView* restoreUAVs[D3D11_1_UAV_SLOT_COUNT] = {}; - ID3D11RenderTargetView* restoreRTVs[D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT] = {}; - ID3D11DepthStencilView* restoreDSV = nullptr; + auto result = true; + + ComPtr restoreSRVs[D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT] = {}; + ComPtr restoreSamplerStates[D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT] = {}; + ComPtr restoreCBVs[D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT] = {}; + ComPtr restoreUAVs[D3D11_1_UAV_SLOT_COUNT] = {}; + ComPtr restoreRTVs[D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT] = {}; + ID3D11RenderTargetView* rawRTVs[D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT] = {}; + ComPtr restoreDSV = nullptr; // backup compute shader resources for (UINT i = 0; i < D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT; i++) { - restoreSRVs[i] = nullptr; - DeviceContext->CSGetShaderResources(i, 1, &restoreSRVs[i]); - - if (restoreSRVs[i] != nullptr) - restoreSRVs[i]->Release(); + InDeviceContext->CSGetShaderResources(i, 1, restoreSRVs[i].GetAddressOf()); } for (UINT i = 0; i < D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT; i++) { - restoreSamplerStates[i] = nullptr; - DeviceContext->CSGetSamplers(i, 1, &restoreSamplerStates[i]); - - if (restoreSamplerStates[i] != nullptr) - restoreSamplerStates[i]->Release(); + InDeviceContext->CSGetSamplers(i, 1, restoreSamplerStates[i].GetAddressOf()); } for (UINT i = 0; i < D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT; i++) { - restoreCBVs[i] = nullptr; - DeviceContext->CSGetConstantBuffers(i, 1, &restoreCBVs[i]); - - if (restoreCBVs[i] != nullptr) - restoreCBVs[i]->Release(); + InDeviceContext->CSGetConstantBuffers(i, 1, restoreCBVs[i].GetAddressOf()); } for (UINT i = 0; i < D3D11_1_UAV_SLOT_COUNT; i++) { - restoreUAVs[i] = nullptr; - DeviceContext->CSGetUnorderedAccessViews(i, 1, &restoreUAVs[i]); - - if (restoreUAVs[i] != nullptr) - restoreUAVs[i]->Release(); + InDeviceContext->CSGetUnorderedAccessViews(i, 1, restoreUAVs[i].GetAddressOf()); } - DeviceContext->OMGetRenderTargets(D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT, restoreRTVs, &restoreDSV); + InDeviceContext->OMGetRenderTargets(D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT, rawRTVs, restoreDSV.GetAddressOf()); - for (UINT i = 0; i < D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT; i++) + for (UINT i = 0; i < D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT; ++i) { - if (restoreRTVs[i] != nullptr) - restoreRTVs[i]->Release(); + restoreRTVs[i].Attach(rawRTVs[i]); } - if (restoreDSV != nullptr) - restoreDSV->Release(); - // Unbind RenderTargets ID3D11RenderTargetView* nullRTVs[D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT] = {}; - DeviceContext->OMSetRenderTargets(D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT, nullRTVs, nullptr); + InDeviceContext->OMSetRenderTargets(D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT, nullRTVs, nullptr); if (Config::Instance()->OverrideSharpness.value_or_default()) _sharpness = Config::Instance()->Sharpness.value_or_default(); @@ -162,7 +147,7 @@ bool IFeature_Dx11::Evaluate(ID3D11DeviceContext* DeviceContext, NVSDK_NGX_Param { LOG_DEBUG("Scaling output..."); - if (!OutputScaler->Dispatch(Device, DeviceContext, (ID3D11Texture2D*) input, + if (!OutputScaler->Dispatch(Device, InDeviceContext, (ID3D11Texture2D*) input, (ID3D11Texture2D*) output)) { Config::Instance()->OutputScalingEnabled.set_volatile_value(false); @@ -227,7 +212,7 @@ bool IFeature_Dx11::Evaluate(ID3D11DeviceContext* DeviceContext, NVSDK_NGX_Param rcasConstants.CameraFar = Config::Instance()->FsrCameraFar.value_or_default(); } - if (!RCAS->Dispatch(Device, DeviceContext, (ID3D11Texture2D*) input, + if (!RCAS->Dispatch(Device, InDeviceContext, (ID3D11Texture2D*) input, (ID3D11Texture2D*) paramMotion, rcasConstants, (ID3D11Texture2D*) output, (ID3D11Texture2D*) paramDepth)) { @@ -255,7 +240,7 @@ bool IFeature_Dx11::Evaluate(ID3D11DeviceContext* DeviceContext, NVSDK_NGX_Param if (!Magnifier->CanRender() || !paramMotion || !paramOutput) return true; - return Magnifier->Dispatch(Device, DeviceContext, (ID3D11Texture2D*) input, + return Magnifier->Dispatch(Device, InDeviceContext, (ID3D11Texture2D*) input, (ID3D11Texture2D*) output); } }); } @@ -276,14 +261,16 @@ bool IFeature_Dx11::Evaluate(ID3D11DeviceContext* DeviceContext, NVSDK_NGX_Param // Upscaler will write to the first active shader, or just output InParameters->Set(NVSDK_NGX_Parameter_Output, currentTarget); - UpscalerTime->Start(DeviceContext); + UpscalerTime->Start(InDeviceContext); - auto evalResult = EvaluateInternal(DeviceContext, InParameters); + auto evalResult = EvaluateInternal(InDeviceContext, InParameters); - UpscalerTime->End(DeviceContext); + UpscalerTime->End(InDeviceContext); if (!evalResult) - return false; + result = false; + + bool pipelineFailed = false; // Iterate FORWARDS to execute the shaders in the defined order for (auto& pass : pipeline) @@ -292,13 +279,14 @@ bool IFeature_Dx11::Evaluate(ID3D11DeviceContext* DeviceContext, NVSDK_NGX_Param { if (!pass.Dispatch(pass.inputBuffer, pass.outputBuffer)) { - return true; + pipelineFailed = true; + break; } } } // imgui - if (!Config::Instance()->OverlayMenu.value_or_default() && _frameCount > 30) + if (!pipelineFailed && !Config::Instance()->OverlayMenu.value_or_default() && _frameCount > 30) { if (Imgui != nullptr && Imgui.get() != nullptr) { @@ -307,7 +295,7 @@ bool IFeature_Dx11::Evaluate(ID3D11DeviceContext* DeviceContext, NVSDK_NGX_Param Imgui.reset(); } else - Imgui->Render(DeviceContext, paramOutput); + Imgui->Render(InDeviceContext, paramOutput); } else { @@ -316,38 +304,39 @@ bool IFeature_Dx11::Evaluate(ID3D11DeviceContext* DeviceContext, NVSDK_NGX_Param } } - InParameters->Set(NVSDK_NGX_Parameter_Output, paramOutput); + if (evalResult && !pipelineFailed) + InParameters->Set(NVSDK_NGX_Parameter_Output, paramOutput); // restore compute shader resources for (UINT i = 0; i < D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT; i++) { - if (restoreSRVs[i] != nullptr) - DeviceContext->CSSetShaderResources(i, 1, &restoreSRVs[i]); + auto raw = restoreSRVs[i].Get(); + InDeviceContext->CSSetShaderResources(i, 1, &raw); } for (UINT i = 0; i < D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT; i++) { - if (restoreSamplerStates[i] != nullptr) - DeviceContext->CSSetSamplers(i, 1, &restoreSamplerStates[i]); + auto raw = restoreSamplerStates[i].Get(); + InDeviceContext->CSSetSamplers(i, 1, &raw); } for (UINT i = 0; i < D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT; i++) { - if (restoreCBVs[i] != nullptr) - DeviceContext->CSSetConstantBuffers(i, 1, &restoreCBVs[i]); + auto raw = restoreCBVs[i].Get(); + InDeviceContext->CSSetConstantBuffers(i, 1, &raw); } for (UINT i = 0; i < D3D11_1_UAV_SLOT_COUNT; i++) { - if (restoreUAVs[i] != nullptr) - DeviceContext->CSSetUnorderedAccessViews(i, 1, &restoreUAVs[i], 0); + auto raw = restoreUAVs[i].Get(); + InDeviceContext->CSSetUnorderedAccessViews(i, 1, &raw, 0); } - DeviceContext->OMSetRenderTargets(D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT, restoreRTVs, restoreDSV); + InDeviceContext->OMSetRenderTargets(D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT, rawRTVs, restoreDSV.Get()); _frameCount++; - return evalResult; + return result; } std::optional IFeature_Dx11::ReadUpscalerTime(void* deviceContextVoid) diff --git a/OptiScaler/upscalers/IFeature_Dx11wDx12.cpp b/OptiScaler/upscalers/IFeature_Dx11wDx12.cpp index c694ad7ba..42c1f5878 100644 --- a/OptiScaler/upscalers/IFeature_Dx11wDx12.cpp +++ b/OptiScaler/upscalers/IFeature_Dx11wDx12.cpp @@ -9,6 +9,8 @@ #include +using Microsoft::WRL::ComPtr; + void IFeature_Dx11wDx12::ResourceBarrier(ID3D12GraphicsCommandList* commandList, ID3D12Resource* resource, D3D12_RESOURCE_STATES beforeState, D3D12_RESOURCE_STATES afterState) { @@ -310,61 +312,42 @@ bool IFeature_Dx11wDx12::Evaluate(ID3D11DeviceContext* InDeviceContext, NVSDK_NG const bool hasRestoreParamReactive = getOriginalNgxResource( InParameters, NVSDK_NGX_Parameter_DLSS_Input_Bias_Current_Color_Mask, &restoreParamReactive); - ID3D11ShaderResourceView* restoreSRVs[D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT] = {}; - ID3D11SamplerState* restoreSamplerStates[D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT] = {}; - ID3D11Buffer* restoreCBVs[D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT] = {}; - ID3D11UnorderedAccessView* restoreUAVs[D3D11_1_UAV_SLOT_COUNT] = {}; - ID3D11RenderTargetView* restoreRTVs[D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT] = {}; - ID3D11DepthStencilView* restoreDSV = nullptr; + ComPtr restoreSRVs[D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT] = {}; + ComPtr restoreSamplerStates[D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT] = {}; + ComPtr restoreCBVs[D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT] = {}; + ComPtr restoreUAVs[D3D11_1_UAV_SLOT_COUNT] = {}; + ComPtr restoreRTVs[D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT] = {}; + ID3D11RenderTargetView* rawRTVs[D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT] = {}; + ComPtr restoreDSV = nullptr; // backup compute shader resources for (UINT i = 0; i < D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT; i++) { - restoreSRVs[i] = nullptr; - InDeviceContext->CSGetShaderResources(i, 1, &restoreSRVs[i]); - - if (restoreSRVs[i] != nullptr) - restoreSRVs[i]->Release(); + InDeviceContext->CSGetShaderResources(i, 1, restoreSRVs[i].GetAddressOf()); } for (UINT i = 0; i < D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT; i++) { - restoreSamplerStates[i] = nullptr; - InDeviceContext->CSGetSamplers(i, 1, &restoreSamplerStates[i]); - - if (restoreSamplerStates[i] != nullptr) - restoreSamplerStates[i]->Release(); + InDeviceContext->CSGetSamplers(i, 1, restoreSamplerStates[i].GetAddressOf()); } for (UINT i = 0; i < D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT; i++) { - restoreCBVs[i] = nullptr; - InDeviceContext->CSGetConstantBuffers(i, 1, &restoreCBVs[i]); - - if (restoreCBVs[i] != nullptr) - restoreCBVs[i]->Release(); + InDeviceContext->CSGetConstantBuffers(i, 1, restoreCBVs[i].GetAddressOf()); } for (UINT i = 0; i < D3D11_1_UAV_SLOT_COUNT; i++) { - restoreUAVs[i] = nullptr; - InDeviceContext->CSGetUnorderedAccessViews(i, 1, &restoreUAVs[i]); - - if (restoreUAVs[i] != nullptr) - restoreUAVs[i]->Release(); + InDeviceContext->CSGetUnorderedAccessViews(i, 1, restoreUAVs[i].GetAddressOf()); } - InDeviceContext->OMGetRenderTargets(D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT, restoreRTVs, &restoreDSV); + InDeviceContext->OMGetRenderTargets(D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT, rawRTVs, restoreDSV.GetAddressOf()); - for (UINT i = 0; i < D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT; i++) + for (UINT i = 0; i < D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT; ++i) { - if (restoreRTVs[i] != nullptr) - restoreRTVs[i]->Release(); + restoreRTVs[i].Attach(rawRTVs[i]); } - if (restoreDSV != nullptr) - restoreDSV->Release(); - // Unbind RenderTargets ID3D11RenderTargetView* nullRTVs[D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT] = {}; InDeviceContext->OMSetRenderTargets(D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT, nullRTVs, nullptr); @@ -476,25 +459,29 @@ bool IFeature_Dx11wDx12::Evaluate(ID3D11DeviceContext* InDeviceContext, NVSDK_NG // restore compute shader resources for (UINT i = 0; i < D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT; i++) { - InDeviceContext->CSSetShaderResources(i, 1, &restoreSRVs[i]); + auto raw = restoreSRVs[i].Get(); + InDeviceContext->CSSetShaderResources(i, 1, &raw); } for (UINT i = 0; i < D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT; i++) { - InDeviceContext->CSSetSamplers(i, 1, &restoreSamplerStates[i]); + auto raw = restoreSamplerStates[i].Get(); + InDeviceContext->CSSetSamplers(i, 1, &raw); } for (UINT i = 0; i < D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT; i++) { - InDeviceContext->CSSetConstantBuffers(i, 1, &restoreCBVs[i]); + auto raw = restoreCBVs[i].Get(); + InDeviceContext->CSSetConstantBuffers(i, 1, &raw); } for (UINT i = 0; i < D3D11_1_UAV_SLOT_COUNT; i++) { - InDeviceContext->CSSetUnorderedAccessViews(i, 1, &restoreUAVs[i], 0); + auto raw = restoreUAVs[i].Get(); + InDeviceContext->CSSetUnorderedAccessViews(i, 1, &raw, 0); } - InDeviceContext->OMSetRenderTargets(D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT, restoreRTVs, restoreDSV); + InDeviceContext->OMSetRenderTargets(D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT, rawRTVs, restoreDSV.Get()); return evalResult; } From 82eb45fb2ab5f33522ecc34342fa3b573559aa02 Mon Sep 17 00:00:00 2001 From: cdozdil Date: Wed, 2 Sep 2026 10:51:20 +0300 Subject: [PATCH 4/8] Input system fixes Disabled HID mouse hooks --- OptiScaler/menu/input/input_system.cpp | 59 ++- .../menu/input/input_system_detours.cpp | 186 ++++--- .../menu/input/input_system_directinput.cpp | 483 +++++++++++++----- .../menu/input/input_system_gameinput.cpp | 10 +- OptiScaler/menu/input/input_system_hid.cpp | 15 +- OptiScaler/menu/input/input_system_internal.h | 21 +- .../menu/input/input_system_messages.cpp | 29 +- OptiScaler/menu/input/input_system_raw.cpp | 32 +- OptiScaler/menu/input/input_system_window.cpp | 31 +- .../menu/input/input_system_windows_hooks.cpp | 86 +++- OptiScaler/menu/input/input_system_xinput.cpp | 96 +++- 11 files changed, 779 insertions(+), 269 deletions(-) diff --git a/OptiScaler/menu/input/input_system.cpp b/OptiScaler/menu/input/input_system.cpp index 624051726..f64ab2940 100644 --- a/OptiScaler/menu/input/input_system.cpp +++ b/OptiScaler/menu/input/input_system.cpp @@ -727,6 +727,12 @@ void ApplyMenuVisibilityChangeLocked(bool visible) { EndCursorClipBlockLocked(); + // Event-style controller APIs keep their own queues. Drain them before + // releasing the menu block so menu-time events cannot replay into the + // game on the first frame after closing. + DrainDirectInputBufferedDataLocked(); + DrainXInputKeystrokesLocked(); + _state.HasBlockedCursorScreenPos = false; _state.BlockedCursorScreenPos = {}; @@ -763,12 +769,27 @@ bool Initialize(const InitializeOptions& options) if (options.InputHwnd != nullptr && options.InputHwnd != _state.InputHwnd) SetInputWindow(options.InputHwnd, options.UseWndProcSubclass, true); + if (!_state.HooksInstalled) + { + LOG_WARN("Initialize re-entry retrying incomplete Win32 hook installation"); + _state.HooksInstalled = InstallHooks(); + + if (_state.HooksInstalled) + { + UpdateGameInputIntegrationLocked(); + UpdateXInputIntegrationLocked(); + UpdateDirectInputIntegrationLocked(); + } + } + LOG_DEBUG( - "Initialize re-entry state target:{} targetPid:{} input:{} inputPid:{} externalTarget:{} subclassed:{}", + "Initialize re-entry state target:{} targetPid:{} input:{} inputPid:{} externalTarget:{} subclassed:{} " + "hooksInstalled:{}", static_cast(_state.TargetHwnd), _state.TargetProcessId, static_cast(_state.InputHwnd), - _state.InputProcessId, _state.ExternalTargetProcess ? 1 : 0, _state.WndProcSubclassed ? 1 : 0); + _state.InputProcessId, _state.ExternalTargetProcess ? 1 : 0, _state.WndProcSubclassed ? 1 : 0, + _state.HooksInstalled ? 1 : 0); - return true; + return _state.HooksInstalled; } _state.Initialized = true; @@ -1050,14 +1071,32 @@ void Shutdown() { std::unique_lock lock(_state.Mutex); - RemoveWindowSubclass(); - ReleaseTrackedWindowsHooksLocked(); + // Restore cursor confinement and clear the blocking policy before any hook teardown + if (_state.MenuVisible) + ApplyMenuVisibilityChangeLocked(false); + else if (_state.CursorClipReleasedForMenu) + EndCursorClipBlockLocked(); + + const bool windowSubclassRemoved = RemoveWindowSubclass(true); + const bool trackedWindowsHooksRemoved = ReleaseTrackedWindowsHooksLocked(); RemoveExternalRawInputSinkLocked(); - RemoveExternalMouseHookLocked(); - RemoveDirectInputHooksLocked(); - RemoveXInputHooksLocked(); - RemoveGameInputHooksLocked(); - RemoveHooks(); + const bool externalMouseHookRemoved = RemoveExternalMouseHookLocked(); + + const bool directInputRemoved = RemoveDirectInputHooksLocked(); + const bool xInputRemoved = RemoveXInputHooksLocked(); + const bool gameInputRemoved = RemoveGameInputHooksLocked(); + const bool win32HooksRemoved = RemoveHooks(); + + if (!windowSubclassRemoved || !trackedWindowsHooksRemoved || !externalMouseHookRemoved || !directInputRemoved || + !xInputRemoved || !gameInputRemoved || !win32HooksRemoved) + { + LOG_ERROR("OptiInput shutdown incomplete; retaining state/trampolines for safety wndProc:{} trackedHooks:{} " + "externalMouse:{} dinput:{} xinput:{} gameInput:{} win32:{}", + windowSubclassRemoved ? 1 : 0, trackedWindowsHooksRemoved ? 1 : 0, externalMouseHookRemoved ? 1 : 0, + directInputRemoved ? 1 : 0, xInputRemoved ? 1 : 0, gameInputRemoved ? 1 : 0, + win32HooksRemoved ? 1 : 0); + return; + } ResetStateAfterShutdown(); } diff --git a/OptiScaler/menu/input/input_system_detours.cpp b/OptiScaler/menu/input/input_system_detours.cpp index 54277e520..ec92f604a 100644 --- a/OptiScaler/menu/input/input_system_detours.cpp +++ b/OptiScaler/menu/input/input_system_detours.cpp @@ -7,6 +7,8 @@ #include #include +// #define USE_HID_HOOKS + static bool messageHooks = false; static bool keyStateHooks = false; static bool getPosHooks = false; @@ -358,6 +360,7 @@ bool InstallHooks() LOG_ERROR("Win32 message hook installation failed result:{}", result); } +#ifdef USE_HID_HOOKS if (!State::Instance().isRunningOnLinux && !hidHooks) { DetourTransactionBegin(); @@ -377,6 +380,7 @@ bool InstallHooks() else LOG_ERROR("Win32 HID hook installation failed result:{}", result); } +#endif // USE_HID_HOOKS if (!rawHooks) { @@ -442,102 +446,128 @@ bool InstallHooks() if (!positionHooks && !positionIATHooks) positionIATHooks = InstallCursorIatHooks(); +#ifdef USE_HID_HOOKS + const bool hidReady = State::Instance().isRunningOnLinux || hidHooks; +#else + const bool hidReady = false; +#endif // USE_HID_HOOKS + _state.HooksInstalled = messageHooks && keyStateHooks && getPosHooks && clipCursorHooks && message2Hooks && - hidHooks && rawHooks && windowsHooks && (positionHooks || positionIATHooks); + hidReady && rawHooks && windowsHooks && (positionHooks || positionIATHooks); return _state.HooksInstalled; } -void RemoveHooks() +bool RemoveHooks() { - if (!_state.HooksInstalled) - return; + const bool hasDetourHooks = messageHooks || keyStateHooks || getPosHooks || clipCursorHooks || message2Hooks || + hidHooks || rawHooks || windowsHooks || positionHooks; + + if (!hasDetourHooks && !positionIATHooks) + { + _state.HooksInstalled = false; + return true; + } LOG_INFO("removing Win32 input hooks"); - DetourTransactionBegin(); - DetourUpdateThread(GetCurrentThread()); + LONG result = NO_ERROR; - if (messageHooks) + if (hasDetourHooks) { - DetourDetach(reinterpret_cast(&o_PeekMessageA), hkPeekMessageA); - DetourDetach(reinterpret_cast(&o_PeekMessageW), hkPeekMessageW); - DetourDetach(reinterpret_cast(&o_GetMessageA), hkGetMessageA); - DetourDetach(reinterpret_cast(&o_GetMessageW), hkGetMessageW); - messageHooks = false; - } + DetourTransactionBegin(); + DetourUpdateThread(GetCurrentThread()); - if (keyStateHooks) - { - DetourDetach(reinterpret_cast(&o_GetAsyncKeyState), hkGetAsyncKeyState); - DetourDetach(reinterpret_cast(&o_GetKeyState), hkGetKeyState); - DetourDetach(reinterpret_cast(&o_GetKeyboardState), hkGetKeyboardState); - keyStateHooks = false; - } + if (messageHooks) + { + DetourDetach(reinterpret_cast(&o_PeekMessageA), hkPeekMessageA); + DetourDetach(reinterpret_cast(&o_PeekMessageW), hkPeekMessageW); + DetourDetach(reinterpret_cast(&o_GetMessageA), hkGetMessageA); + DetourDetach(reinterpret_cast(&o_GetMessageW), hkGetMessageW); + } - if (getPosHooks) - { - DetourDetach(reinterpret_cast(&o_GetMessagePos), hkGetMessagePos); - DetourDetach(reinterpret_cast(&o_GetMouseMovePointsEx), hkGetMouseMovePointsEx); - getPosHooks = false; - } + if (keyStateHooks) + { + DetourDetach(reinterpret_cast(&o_GetAsyncKeyState), hkGetAsyncKeyState); + DetourDetach(reinterpret_cast(&o_GetKeyState), hkGetKeyState); + DetourDetach(reinterpret_cast(&o_GetKeyboardState), hkGetKeyboardState); + } - if (clipCursorHooks) - { - DetourDetach(reinterpret_cast(&o_ClipCursor), hkClipCursor); - DetourDetach(reinterpret_cast(&o_GetClipCursor), hkGetClipCursor); - clipCursorHooks = false; - } + if (getPosHooks) + { + DetourDetach(reinterpret_cast(&o_GetMessagePos), hkGetMessagePos); + DetourDetach(reinterpret_cast(&o_GetMouseMovePointsEx), hkGetMouseMovePointsEx); + } - if (message2Hooks) - { - DetourDetach(reinterpret_cast(&o_SendInput), hkSendInput); - DetourDetach(reinterpret_cast(&o_mouse_event), hkmouse_event); - DetourDetach(reinterpret_cast(&o_PostMessageA), hkPostMessageA); - DetourDetach(reinterpret_cast(&o_PostMessageW), hkPostMessageW); - DetourDetach(reinterpret_cast(&o_SendMessageA), hkSendMessageA); - DetourDetach(reinterpret_cast(&o_SendMessageW), hkSendMessageW); - message2Hooks = false; - } + if (clipCursorHooks) + { + DetourDetach(reinterpret_cast(&o_ClipCursor), hkClipCursor); + DetourDetach(reinterpret_cast(&o_GetClipCursor), hkGetClipCursor); + } - if (!State::Instance().isRunningOnLinux && hidHooks) - { - DetourDetach(reinterpret_cast(&o_CreateFileA), hkCreateFileA); - DetourDetach(reinterpret_cast(&o_CreateFileW), hkCreateFileW); - DetourDetach(reinterpret_cast(&o_ReadFile), hkReadFile); - DetourDetach(reinterpret_cast(&o_DeviceIoControl), hkDeviceIoControl); - DetourDetach(reinterpret_cast(&o_CloseHandle), hkCloseHandle); - hidHooks = false; - } + if (message2Hooks) + { + DetourDetach(reinterpret_cast(&o_SendInput), hkSendInput); + DetourDetach(reinterpret_cast(&o_mouse_event), hkmouse_event); + DetourDetach(reinterpret_cast(&o_PostMessageA), hkPostMessageA); + DetourDetach(reinterpret_cast(&o_PostMessageW), hkPostMessageW); + DetourDetach(reinterpret_cast(&o_SendMessageA), hkSendMessageA); + DetourDetach(reinterpret_cast(&o_SendMessageW), hkSendMessageW); + } - if (rawHooks) - { - DetourDetach(reinterpret_cast(&o_GetRawInputData), hkGetRawInputData); - DetourDetach(reinterpret_cast(&o_GetRawInputBuffer), hkGetRawInputBuffer); - DetourDetach(reinterpret_cast(&o_RegisterRawInputDevices), hkRegisterRawInputDevices); - rawHooks = false; - } + if (hidHooks) + { + DetourDetach(reinterpret_cast(&o_CreateFileA), hkCreateFileA); + DetourDetach(reinterpret_cast(&o_CreateFileW), hkCreateFileW); + DetourDetach(reinterpret_cast(&o_ReadFile), hkReadFile); + DetourDetach(reinterpret_cast(&o_DeviceIoControl), hkDeviceIoControl); + DetourDetach(reinterpret_cast(&o_CloseHandle), hkCloseHandle); + } - if (windowsHooks) - { - DetourDetach(reinterpret_cast(&o_SetWindowsHookExA), hkSetWindowsHookExA); - DetourDetach(reinterpret_cast(&o_SetWindowsHookExW), hkSetWindowsHookExW); - DetourDetach(reinterpret_cast(&o_UnhookWindowsHookEx), hkUnhookWindowsHookEx); - windowsHooks = false; - } + if (rawHooks) + { + DetourDetach(reinterpret_cast(&o_GetRawInputData), hkGetRawInputData); + DetourDetach(reinterpret_cast(&o_GetRawInputBuffer), hkGetRawInputBuffer); + DetourDetach(reinterpret_cast(&o_RegisterRawInputDevices), hkRegisterRawInputDevices); + } - if (positionHooks) - { - DetourDetach(reinterpret_cast(&o_GetCursorPos), hkGetCursorPos); - DetourDetach(reinterpret_cast(&o_SetCursorPos), hkSetCursorPos); + if (windowsHooks) + { + DetourDetach(reinterpret_cast(&o_SetWindowsHookExA), hkSetWindowsHookExA); + DetourDetach(reinterpret_cast(&o_SetWindowsHookExW), hkSetWindowsHookExW); + DetourDetach(reinterpret_cast(&o_UnhookWindowsHookEx), hkUnhookWindowsHookEx); + } - if (o_GetPhysicalCursorPos != nullptr) - DetourDetach(reinterpret_cast(&o_GetPhysicalCursorPos), hkGetPhysicalCursorPos); + if (positionHooks) + { + DetourDetach(reinterpret_cast(&o_GetCursorPos), hkGetCursorPos); + DetourDetach(reinterpret_cast(&o_SetCursorPos), hkSetCursorPos); - if (o_SetPhysicalCursorPos != nullptr) - DetourDetach(reinterpret_cast(&o_SetPhysicalCursorPos), hkSetPhysicalCursorPos); + if (o_GetPhysicalCursorPos != nullptr) + DetourDetach(reinterpret_cast(&o_GetPhysicalCursorPos), hkGetPhysicalCursorPos); + + if (o_SetPhysicalCursorPos != nullptr) + DetourDetach(reinterpret_cast(&o_SetPhysicalCursorPos), hkSetPhysicalCursorPos); + } - positionHooks = false; + result = DetourTransactionCommit(); + + if (result == NO_ERROR) + { + messageHooks = false; + keyStateHooks = false; + getPosHooks = false; + clipCursorHooks = false; + message2Hooks = false; + hidHooks = false; + rawHooks = false; + windowsHooks = false; + positionHooks = false; + } + else + { + LOG_WARN("Win32 input hook removal failed result:{}; retaining hook state for a safe retry", result); + } } if (positionIATHooks) @@ -546,11 +576,11 @@ void RemoveHooks() positionIATHooks = false; } - const LONG result = DetourTransactionCommit(); - if (result != NO_ERROR) - LOG_WARN("Win32 input hook removal completed with result:{}", result); + const bool hidReady = State::Instance().isRunningOnLinux || hidHooks; + _state.HooksInstalled = messageHooks && keyStateHooks && getPosHooks && clipCursorHooks && message2Hooks && + hidReady && rawHooks && windowsHooks && (positionHooks || positionIATHooks); - _state.HooksInstalled = false; + return result == NO_ERROR; } } // namespace OptiInput diff --git a/OptiScaler/menu/input/input_system_directinput.cpp b/OptiScaler/menu/input/input_system_directinput.cpp index d9ea42d8d..74fd117eb 100644 --- a/OptiScaler/menu/input/input_system_directinput.cpp +++ b/OptiScaler/menu/input/input_system_directinput.cpp @@ -24,6 +24,23 @@ constexpr GUID DirectInputSysMouseGuid = { 0x6f1d2b60, 0xd5a0, 0x11cf, { 0xbf, 0xc7, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00 } }; +constexpr std::size_t MaxDirectInputMethodHooks = 8; + +template struct DirectInputMethodHookSlot +{ + bool InUse = false; + T Target = nullptr; + T Trampoline = nullptr; +}; + +std::array, MaxDirectInputMethodHooks> + DirectInputCreateDeviceHooks {}; +std::array, MaxDirectInputMethodHooks> DirectInputReleaseHooks {}; +std::array, MaxDirectInputMethodHooks> + DirectInputGetDeviceStateHooks {}; +std::array, MaxDirectInputMethodHooks> + DirectInputGetDeviceDataHooks {}; + bool IsDirectInputKeyboardGuid(REFGUID guid) { return IsEqualGUID(guid, DirectInputSysKeyboardGuid) != FALSE; } bool IsDirectInputMouseGuid(REFGUID guid) { return IsEqualGUID(guid, DirectInputSysMouseGuid) != FALSE; } @@ -87,6 +104,127 @@ const char* DirectInputDeviceKindName(DirectInputDeviceKind kind) } } +template +DirectInputMethodHookSlot* +FindDirectInputMethodHookByTargetLocked(std::array, N>& hooks, T target) +{ + if (target == nullptr) + return nullptr; + + for (auto& slot : hooks) + { + if (slot.InUse && slot.Target == target) + return &slot; + } + + return nullptr; +} + +template +DirectInputMethodHookSlot* PrepareDirectInputMethodHookLocked(std::array, N>& hooks, + T target, bool* needsAttach) +{ + if (needsAttach != nullptr) + *needsAttach = false; + + if (target == nullptr) + return nullptr; + + if (auto* existing = FindDirectInputMethodHookByTargetLocked(hooks, target); existing != nullptr) + return existing; + + for (auto& slot : hooks) + { + if (slot.InUse) + continue; + + slot.InUse = true; + slot.Target = target; + slot.Trampoline = target; + + if (needsAttach != nullptr) + *needsAttach = true; + + return &slot; + } + + return nullptr; +} + +template +bool HasDirectInputMethodHooksLocked(const std::array, N>& hooks) +{ + for (const auto& slot : hooks) + { + if (slot.InUse) + return true; + } + + return false; +} + +template +T FirstDirectInputMethodTrampolineLocked(const std::array, N>& hooks) +{ + for (const auto& slot : hooks) + { + if (slot.InUse && slot.Trampoline != nullptr) + return slot.Trampoline; + } + + return nullptr; +} + +template +T ResolveDirectInputMethodTrampolineLocked(const std::array, N>& hooks, T target) +{ + if (target == nullptr) + return nullptr; + + for (const auto& slot : hooks) + { + if (slot.InUse && slot.Target == target) + return slot.Trampoline; + } + + return nullptr; +} + +void RefreshDirectInputDeviceHookStateLocked() +{ + _state.DirectInputDeviceReleaseHookInstalled = HasDirectInputMethodHooksLocked(DirectInputReleaseHooks); + _state.DirectInputGetDeviceStateHookInstalled = HasDirectInputMethodHooksLocked(DirectInputGetDeviceStateHooks); + _state.DirectInputGetDeviceDataHookInstalled = HasDirectInputMethodHooksLocked(DirectInputGetDeviceDataHooks); + + // Keep the legacy globals valid for diagnostics/compatibility, but do not use them to identify a target. + o_DirectInputCreateDeviceA = FirstDirectInputMethodTrampolineLocked(DirectInputCreateDeviceHooks); + o_DirectInputCreateDeviceW = o_DirectInputCreateDeviceA; + o_DirectInputDeviceRelease = FirstDirectInputMethodTrampolineLocked(DirectInputReleaseHooks); + o_DirectInputDeviceGetDeviceState = FirstDirectInputMethodTrampolineLocked(DirectInputGetDeviceStateHooks); + o_DirectInputDeviceGetDeviceData = FirstDirectInputMethodTrampolineLocked(DirectInputGetDeviceDataHooks); +} + +void ClearDirectInputMethodHooksLocked() +{ + DirectInputCreateDeviceHooks = {}; + _state.DirectInputCreateDeviceAHookInstalled = false; + _state.DirectInputCreateDeviceWHookInstalled = false; + DirectInputReleaseHooks = {}; + DirectInputGetDeviceStateHooks = {}; + DirectInputGetDeviceDataHooks = {}; + RefreshDirectInputDeviceHookStateLocked(); +} + +void MarkDirectInputDeviceKindSeenLocked(DirectInputDeviceKind kind) +{ + if (kind == DirectInputDeviceKind::Keyboard) + _state.DirectInputKeyboardDeviceSeen = true; + else if (kind == DirectInputDeviceKind::Mouse) + _state.DirectInputMouseDeviceSeen = true; + else + _state.DirectInputOtherDeviceSeen = true; +} + HMODULE FindLoadedDirectInput8Module() { return GetModuleHandleW(DirectInput8ModuleName); } HMODULE FindLoadedDirectInputLegacyModule() { return GetModuleHandleW(DirectInputLegacyModuleName); } @@ -99,9 +237,6 @@ void ClearDirectInputHookPointersLocked() o_DirectInputCreateEx = nullptr; o_DirectInputCreateDeviceA = nullptr; o_DirectInputCreateDeviceW = nullptr; - o_DirectInputDeviceGetDeviceState = nullptr; - o_DirectInputDeviceGetDeviceData = nullptr; - o_DirectInputDeviceRelease = nullptr; _state.DirectInput8CreateHookInstalled = false; _state.DirectInputCreateAHookInstalled = false; @@ -109,9 +244,8 @@ void ClearDirectInputHookPointersLocked() _state.DirectInputCreateExHookInstalled = false; _state.DirectInputCreateDeviceAHookInstalled = false; _state.DirectInputCreateDeviceWHookInstalled = false; - _state.DirectInputGetDeviceStateHookInstalled = false; - _state.DirectInputGetDeviceDataHookInstalled = false; - _state.DirectInputDeviceReleaseHookInstalled = false; + + ClearDirectInputMethodHooksLocked(); } std::size_t FindDirectInputDeviceSlotLocked(void* device) @@ -168,7 +302,21 @@ void TrackDirectInputDeviceLocked(void* device, DirectInputDeviceKind kind) if (slot.InUse && slot.Device == device) { - slot.Kind = kind; + // A later CreateDevice call may use an instance GUID that we cannot classify and + // therefore reports Other. Never downgrade a known keyboard/mouse classification. + if (slot.Kind == DirectInputDeviceKind::Other && kind != DirectInputDeviceKind::Other) + { + slot.Kind = kind; + MarkDirectInputDeviceKindSeenLocked(kind); + LOG_INFO("DirectInput device reclassified device:{} kind:{}", device, DirectInputDeviceKindName(kind)); + } + else if (slot.Kind != DirectInputDeviceKind::Other && kind != DirectInputDeviceKind::Other && + slot.Kind != kind) + { + LOG_WARN("DirectInput device kind mismatch device:{} existing:{} new:{}; preserving existing kind", + device, DirectInputDeviceKindName(slot.Kind), DirectInputDeviceKindName(kind)); + } + return; } @@ -190,12 +338,7 @@ void TrackDirectInputDeviceLocked(void* device, DirectInputDeviceKind kind) _state.DirectInputTrackedDeviceCount++; - if (kind == DirectInputDeviceKind::Keyboard) - _state.DirectInputKeyboardDeviceSeen = true; - else if (kind == DirectInputDeviceKind::Mouse) - _state.DirectInputMouseDeviceSeen = true; - else - _state.DirectInputOtherDeviceSeen = true; + MarkDirectInputDeviceKindSeenLocked(kind); LOG_INFO("DirectInput device captured device:{} kind:{}", device, DirectInputDeviceKindName(kind)); } @@ -215,53 +358,52 @@ bool HookDirectInputDeviceLocked(void* device, DirectInputDeviceKind kind) bool attachGetDeviceState = false; bool attachGetDeviceData = false; - if (o_DirectInputDeviceRelease == nullptr) - { - o_DirectInputDeviceRelease = release; - attachRelease = o_DirectInputDeviceRelease != nullptr; - } - else if (o_DirectInputDeviceRelease != release) - { - LOG_WARN("DirectInput device Release pointer differs, not detouring new pointer device:{}", device); - } + auto* releaseHook = PrepareDirectInputMethodHookLocked(DirectInputReleaseHooks, release, &attachRelease); + auto* getDeviceStateHook = + PrepareDirectInputMethodHookLocked(DirectInputGetDeviceStateHooks, getDeviceState, &attachGetDeviceState); + auto* getDeviceDataHook = + PrepareDirectInputMethodHookLocked(DirectInputGetDeviceDataHooks, getDeviceData, &attachGetDeviceData); - if (o_DirectInputDeviceGetDeviceState == nullptr) - { - o_DirectInputDeviceGetDeviceState = getDeviceState; - attachGetDeviceState = o_DirectInputDeviceGetDeviceState != nullptr; - } - else if (o_DirectInputDeviceGetDeviceState != getDeviceState) + bool completeCoverage = true; + + if (release != nullptr && releaseHook == nullptr) { - LOG_WARN("DirectInput GetDeviceState pointer differs, not detouring new pointer device:{}", device); + LOG_WARN("DirectInput Release hook table is full, device:{} target:{}", device, + reinterpret_cast(release)); + completeCoverage = false; } - if (o_DirectInputDeviceGetDeviceData == nullptr) + if (getDeviceState != nullptr && getDeviceStateHook == nullptr) { - o_DirectInputDeviceGetDeviceData = getDeviceData; - attachGetDeviceData = o_DirectInputDeviceGetDeviceData != nullptr; + LOG_WARN("DirectInput GetDeviceState hook table is full, device:{} target:{}", device, + reinterpret_cast(getDeviceState)); + completeCoverage = false; } - else if (o_DirectInputDeviceGetDeviceData != getDeviceData) + + if (getDeviceData != nullptr && getDeviceDataHook == nullptr) { - LOG_WARN("DirectInput GetDeviceData pointer differs, not detouring new pointer device:{}", device); + LOG_WARN("DirectInput GetDeviceData hook table is full, device:{} target:{}", device, + reinterpret_cast(getDeviceData)); + completeCoverage = false; } if (!attachRelease && !attachGetDeviceState && !attachGetDeviceData) { TrackDirectInputDeviceLocked(device, kind); - return true; + return completeCoverage; } DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); if (attachRelease) - DetourAttach(reinterpret_cast(&o_DirectInputDeviceRelease), hkDirectInputDeviceRelease); + DetourAttach(reinterpret_cast(&releaseHook->Trampoline), hkDirectInputDeviceRelease); if (attachGetDeviceState) - DetourAttach(reinterpret_cast(&o_DirectInputDeviceGetDeviceState), hkDirectInputGetDeviceState); + DetourAttach(reinterpret_cast(&getDeviceStateHook->Trampoline), hkDirectInputGetDeviceState); if (attachGetDeviceData) - DetourAttach(reinterpret_cast(&o_DirectInputDeviceGetDeviceData), hkDirectInputGetDeviceData); + DetourAttach(reinterpret_cast(&getDeviceDataHook->Trampoline), hkDirectInputGetDeviceData); const LONG result = DetourTransactionCommit(); @@ -271,28 +413,33 @@ bool HookDirectInputDeviceLocked(void* device, DirectInputDeviceKind kind) DirectInputDeviceKindName(kind)); if (attachRelease) - o_DirectInputDeviceRelease = nullptr; + *releaseHook = {}; if (attachGetDeviceState) - o_DirectInputDeviceGetDeviceState = nullptr; + *getDeviceStateHook = {}; if (attachGetDeviceData) - o_DirectInputDeviceGetDeviceData = nullptr; + *getDeviceDataHook = {}; + RefreshDirectInputDeviceHookStateLocked(); return false; } + RefreshDirectInputDeviceHookStateLocked(); + if (attachRelease) - _state.DirectInputDeviceReleaseHookInstalled = true; + LOG_INFO("DirectInput Release target detoured target:{} device:{}", reinterpret_cast(release), device); if (attachGetDeviceState) - _state.DirectInputGetDeviceStateHookInstalled = true; + LOG_INFO("DirectInput GetDeviceState target detoured target:{} device:{}", + reinterpret_cast(getDeviceState), device); if (attachGetDeviceData) - _state.DirectInputGetDeviceDataHookInstalled = true; + LOG_INFO("DirectInput GetDeviceData target detoured target:{} device:{}", + reinterpret_cast(getDeviceData), device); TrackDirectInputDeviceLocked(device, kind); - return true; + return completeCoverage; } bool HookDirectInputInterfaceLocked(void* directInput, bool wide) @@ -306,53 +453,38 @@ bool HookDirectInputInterfaceLocked(void* directInput, bool wide) if (createDevice == nullptr) return false; - if (wide) - { - if (_state.DirectInputCreateDeviceWHookInstalled) - { - if (o_DirectInputCreateDeviceW != createDevice) - LOG_WARN("DirectInput W CreateDevice pointer changed, existing hook remains active old:{} new:{}", - reinterpret_cast(o_DirectInputCreateDeviceW), reinterpret_cast(createDevice)); + bool needsAttach = false; + auto* slot = PrepareDirectInputMethodHookLocked(DirectInputCreateDeviceHooks, createDevice, &needsAttach); - return true; - } - - o_DirectInputCreateDeviceW = createDevice; - } - else + if (slot == nullptr) { - if (_state.DirectInputCreateDeviceAHookInstalled) - { - if (o_DirectInputCreateDeviceA != createDevice) - LOG_WARN("DirectInput A CreateDevice pointer changed, existing hook remains active old:{} new:{}", - reinterpret_cast(o_DirectInputCreateDeviceA), reinterpret_cast(createDevice)); - - return true; - } - - o_DirectInputCreateDeviceA = createDevice; + LOG_ERROR("DirectInput CreateDevice hook table full wide:{} target:{}", wide ? 1 : 0, + reinterpret_cast(createDevice)); + return false; } - DetourTransactionBegin(); - DetourUpdateThread(GetCurrentThread()); - - if (wide) - DetourAttach(reinterpret_cast(&o_DirectInputCreateDeviceW), hkDirectInputCreateDeviceW); - else - DetourAttach(reinterpret_cast(&o_DirectInputCreateDeviceA), hkDirectInputCreateDeviceA); - - const LONG result = DetourTransactionCommit(); - - if (result != NO_ERROR) + if (needsAttach) { - LOG_ERROR("DirectInput CreateDevice hook installation failed result:{} wide:{}", result, wide ? 1 : 0); + // ANSI and Unicode CreateDevice have the same ABI. Route every unique + // implementation through one detour so a shared A/W implementation is + // never attached twice. The interface's vtable identifies the correct + // per-target trampoline at call time. + DetourTransactionBegin(); + DetourUpdateThread(GetCurrentThread()); + DetourAttach(reinterpret_cast(&slot->Trampoline), hkDirectInputCreateDeviceA); - if (wide) - o_DirectInputCreateDeviceW = nullptr; - else - o_DirectInputCreateDeviceA = nullptr; + const LONG result = DetourTransactionCommit(); - return false; + if (result != NO_ERROR) + { + LOG_ERROR("DirectInput CreateDevice hook installation failed result:{} wide:{} target:{}", result, + wide ? 1 : 0, reinterpret_cast(createDevice)); + *slot = {}; + RefreshDirectInputDeviceHookStateLocked(); + return false; + } + + LOG_INFO("DirectInput CreateDevice target detoured target:{}", reinterpret_cast(createDevice)); } if (wide) @@ -360,7 +492,7 @@ bool HookDirectInputInterfaceLocked(void* directInput, bool wide) else _state.DirectInputCreateDeviceAHookInstalled = true; - LOG_INFO("DirectInput CreateDevice hook installed wide:{}", wide ? 1 : 0); + RefreshDirectInputDeviceHookStateLocked(); return true; } @@ -492,7 +624,7 @@ void UpdateDirectInputIntegrationLocked() } } -void RemoveDirectInputHooksLocked() +bool RemoveDirectInputHooksLocked() { if (!_state.DirectInput8CreateHookInstalled && !_state.DirectInputCreateAHookInstalled && !_state.DirectInputCreateWHookInstalled && !_state.DirectInputCreateExHookInstalled && @@ -502,7 +634,7 @@ void RemoveDirectInputHooksLocked() { ClearDirectInputHookPointersLocked(); ClearAllDirectInputDeviceSlotsLocked(); - return; + return true; } DetourTransactionBegin(); @@ -520,28 +652,65 @@ void RemoveDirectInputHooksLocked() if (_state.DirectInputCreateExHookInstalled && o_DirectInputCreateEx != nullptr) DetourDetach(reinterpret_cast(&o_DirectInputCreateEx), hkDirectInputCreateEx); - if (_state.DirectInputCreateDeviceAHookInstalled && o_DirectInputCreateDeviceA != nullptr) - DetourDetach(reinterpret_cast(&o_DirectInputCreateDeviceA), hkDirectInputCreateDeviceA); - - if (_state.DirectInputCreateDeviceWHookInstalled && o_DirectInputCreateDeviceW != nullptr) - DetourDetach(reinterpret_cast(&o_DirectInputCreateDeviceW), hkDirectInputCreateDeviceW); + for (auto& slot : DirectInputCreateDeviceHooks) + { + if (slot.InUse && slot.Trampoline != nullptr) + DetourDetach(reinterpret_cast(&slot.Trampoline), hkDirectInputCreateDeviceA); + } - if (_state.DirectInputGetDeviceStateHookInstalled && o_DirectInputDeviceGetDeviceState != nullptr) - DetourDetach(reinterpret_cast(&o_DirectInputDeviceGetDeviceState), hkDirectInputGetDeviceState); + for (auto& slot : DirectInputGetDeviceStateHooks) + { + if (slot.InUse && slot.Trampoline != nullptr) + DetourDetach(reinterpret_cast(&slot.Trampoline), hkDirectInputGetDeviceState); + } - if (_state.DirectInputGetDeviceDataHookInstalled && o_DirectInputDeviceGetDeviceData != nullptr) - DetourDetach(reinterpret_cast(&o_DirectInputDeviceGetDeviceData), hkDirectInputGetDeviceData); + for (auto& slot : DirectInputGetDeviceDataHooks) + { + if (slot.InUse && slot.Trampoline != nullptr) + DetourDetach(reinterpret_cast(&slot.Trampoline), hkDirectInputGetDeviceData); + } - if (_state.DirectInputDeviceReleaseHookInstalled && o_DirectInputDeviceRelease != nullptr) - DetourDetach(reinterpret_cast(&o_DirectInputDeviceRelease), hkDirectInputDeviceRelease); + for (auto& slot : DirectInputReleaseHooks) + { + if (slot.InUse && slot.Trampoline != nullptr) + DetourDetach(reinterpret_cast(&slot.Trampoline), hkDirectInputDeviceRelease); + } const LONG result = DetourTransactionCommit(); if (result != NO_ERROR) - LOG_WARN("DirectInput hook removal completed with result:{}", result); + { + LOG_WARN("DirectInput hook removal failed result:{}; retaining trampoline tables for a safe retry", result); + return false; + } ClearDirectInputHookPointersLocked(); ClearAllDirectInputDeviceSlotsLocked(); + return true; +} + +void DrainDirectInputBufferedDataLocked() +{ + for (DirectInputDeviceSlot& deviceSlot : _state.DirectInputDeviceSlots) + { + if (!deviceSlot.InUse || deviceSlot.Device == nullptr) + continue; + + PVOID* vtable = *reinterpret_cast(deviceSlot.Device); + auto target = reinterpret_cast(vtable[10]); + DirectInputGetDeviceData_t original = + ResolveDirectInputMethodTrampolineLocked(DirectInputGetDeviceDataHooks, target); + + if (original == nullptr) + continue; + + const DWORD objectDataSize = + deviceSlot.LastObjectDataSize != 0 ? deviceSlot.LastObjectDataSize : sizeof(DIDEVICEOBJECTDATA); + DWORD flushCount = INFINITE; + + ScopedHookBypass bypass; + original(deviceSlot.Device, objectDataSize, nullptr, &flushCount, 0); + } } HRESULT WINAPI hkDirectInput8Create(HINSTANCE instance, DWORD version, REFIID riid, LPVOID* out, LPUNKNOWN outer) @@ -658,7 +827,20 @@ HRESULT WINAPI hkDirectInputCreateEx(HINSTANCE instance, DWORD version, REFIID r HRESULT WINAPI hkDirectInputCreateDeviceA(void* directInput, REFGUID guid, void** device, LPUNKNOWN outer) { - HRESULT result = CallDirectInputCreateDeviceOriginal(o_DirectInputCreateDeviceA, directInput, guid, device, outer); + DirectInputCreateDevice_t original = nullptr; + + { + std::unique_lock lock(_state.Mutex); + + if (directInput != nullptr) + { + PVOID* vtable = *reinterpret_cast(directInput); + auto target = reinterpret_cast(vtable[3]); + original = ResolveDirectInputMethodTrampolineLocked(DirectInputCreateDeviceHooks, target); + } + } + + HRESULT result = CallDirectInputCreateDeviceOriginal(original, directInput, guid, device, outer); { std::unique_lock lock(_state.Mutex); @@ -680,7 +862,20 @@ HRESULT WINAPI hkDirectInputCreateDeviceA(void* directInput, REFGUID guid, void* HRESULT WINAPI hkDirectInputCreateDeviceW(void* directInput, REFGUID guid, void** device, LPUNKNOWN outer) { - HRESULT result = CallDirectInputCreateDeviceOriginal(o_DirectInputCreateDeviceW, directInput, guid, device, outer); + DirectInputCreateDevice_t original = nullptr; + + { + std::unique_lock lock(_state.Mutex); + + if (directInput != nullptr) + { + PVOID* vtable = *reinterpret_cast(directInput); + auto target = reinterpret_cast(vtable[3]); + original = ResolveDirectInputMethodTrampolineLocked(DirectInputCreateDeviceHooks, target); + } + } + + HRESULT result = CallDirectInputCreateDeviceOriginal(original, directInput, guid, device, outer); { std::unique_lock lock(_state.Mutex); @@ -702,6 +897,8 @@ HRESULT WINAPI hkDirectInputCreateDeviceW(void* directInput, REFGUID guid, void* HRESULT WINAPI hkDirectInputGetDeviceState(void* device, DWORD dataSize, LPVOID data) { + DirectInputGetDeviceState_t original = nullptr; + { std::unique_lock lock(_state.Mutex); const DirectInputDeviceKind kind = GetDirectInputDeviceKindLocked(device); @@ -719,52 +916,100 @@ HRESULT WINAPI hkDirectInputGetDeviceState(void* device, DWORD dataSize, LPVOID } _state.DirectInputGetDeviceStatePassedCount++; + + if (device != nullptr) + { + PVOID* vtable = *reinterpret_cast(device); + auto target = reinterpret_cast(vtable[9]); + original = ResolveDirectInputMethodTrampolineLocked(DirectInputGetDeviceStateHooks, target); + } } - if (o_DirectInputDeviceGetDeviceState == nullptr) + if (original == nullptr) return DIERR_GENERIC; ScopedHookBypass bypass; - return o_DirectInputDeviceGetDeviceState(device, dataSize, data); + return original(device, dataSize, data); } HRESULT WINAPI hkDirectInputGetDeviceData(void* device, DWORD objectDataSize, LPDIDEVICEOBJECTDATA data, LPDWORD inOut, DWORD flags) { + DirectInputGetDeviceData_t original = nullptr; + DirectInputDeviceKind kind = DirectInputDeviceKind::Other; + bool shouldBlock = false; + { std::unique_lock lock(_state.Mutex); - const DirectInputDeviceKind kind = GetDirectInputDeviceKindLocked(device); + kind = GetDirectInputDeviceKindLocked(device); _state.DirectInputGetDeviceDataCallCount++; + shouldBlock = ShouldBlockDirectInputDeviceLocked(kind); - if (ShouldBlockDirectInputDeviceLocked(kind)) + if (shouldBlock) + _state.DirectInputGetDeviceDataBlockedCount++; + else + _state.DirectInputGetDeviceDataPassedCount++; + + if (device != nullptr) { - if (inOut != nullptr) - *inOut = 0; + const std::size_t deviceSlotIndex = FindDirectInputDeviceSlotLocked(device); + if (deviceSlotIndex < MaxTrackedDirectInputDevices && objectDataSize != 0) + _state.DirectInputDeviceSlots[deviceSlotIndex].LastObjectDataSize = objectDataSize; - _state.DirectInputGetDeviceDataBlockedCount++; - OPTIINPUT_LOG_VERBOSE("blocking DirectInput GetDeviceData device:{} kind:{} flags:{}", device, - DirectInputDeviceKindName(kind), flags); - return DI_OK; + PVOID* vtable = *reinterpret_cast(device); + auto target = reinterpret_cast(vtable[10]); + original = ResolveDirectInputMethodTrampolineLocked(DirectInputGetDeviceDataHooks, target); } + } - _state.DirectInputGetDeviceDataPassedCount++; + if (!shouldBlock) + { + if (original == nullptr) + return DIERR_GENERIC; + + ScopedHookBypass bypass; + return original(device, objectDataSize, data, inOut, flags); } - if (o_DirectInputDeviceGetDeviceData == nullptr) - return DIERR_GENERIC; + // GetDeviceData is backed by a buffered event queue. Returning zero without + // touching the real queue lets menu-time events replay after closing the + // overlay. Flush the device buffer, then present an empty successful read. + if (original != nullptr) + { + DWORD flushCount = INFINITE; + ScopedHookBypass bypass; + original(device, objectDataSize, nullptr, &flushCount, 0); + } - ScopedHookBypass bypass; - return o_DirectInputDeviceGetDeviceData(device, objectDataSize, data, inOut, flags); + if (inOut != nullptr) + *inOut = 0; + + OPTIINPUT_LOG_VERBOSE("blocking DirectInput GetDeviceData device:{} kind:{} flags:{}", device, + DirectInputDeviceKindName(kind), flags); + return DI_OK; } ULONG WINAPI hkDirectInputDeviceRelease(void* device) { + DirectInputDeviceRelease_t original = nullptr; + + { + std::unique_lock lock(_state.Mutex); + + if (device != nullptr) + { + PVOID* vtable = *reinterpret_cast(device); + auto target = reinterpret_cast(vtable[2]); + original = ResolveDirectInputMethodTrampolineLocked(DirectInputReleaseHooks, target); + } + } + ULONG result = 0; - if (o_DirectInputDeviceRelease != nullptr) + if (original != nullptr) { ScopedHookBypass bypass; - result = o_DirectInputDeviceRelease(device); + result = original(device); } if (result == 0) diff --git a/OptiScaler/menu/input/input_system_gameinput.cpp b/OptiScaler/menu/input/input_system_gameinput.cpp index 92f69caeb..f0f96ab30 100644 --- a/OptiScaler/menu/input/input_system_gameinput.cpp +++ b/OptiScaler/menu/input/input_system_gameinput.cpp @@ -79,13 +79,13 @@ void UpdateGameInputIntegrationLocked() InstallGameInputCreateHookLocked(); } -void RemoveGameInputHooksLocked() +bool RemoveGameInputHooksLocked() { if (!_state.GameInputCreateHookInstalled || o_GameInputCreate == nullptr) { _state.GameInputCreateHookInstalled = false; o_GameInputCreate = nullptr; - return; + return true; } DetourTransactionBegin(); @@ -95,10 +95,14 @@ void RemoveGameInputHooksLocked() const LONG result = DetourTransactionCommit(); if (result != NO_ERROR) - LOG_WARN("GameInputCreate hook removal completed with result:{}", result); + { + LOG_WARN("GameInputCreate hook removal failed result:{}; retaining trampoline for a safe retry", result); + return false; + } _state.GameInputCreateHookInstalled = false; o_GameInputCreate = nullptr; + return true; } HRESULT WINAPI hkGameInputCreate(void** gameInput) diff --git a/OptiScaler/menu/input/input_system_hid.cpp b/OptiScaler/menu/input/input_system_hid.cpp index 0c214c9e3..347f6b7e3 100644 --- a/OptiScaler/menu/input/input_system_hid.cpp +++ b/OptiScaler/menu/input/input_system_hid.cpp @@ -72,8 +72,14 @@ std::wstring AnsiToWide(LPCSTR text) if (length <= 1) return {}; - std::wstring result(static_cast(length - 1), L'\0'); - MultiByteToWideChar(CP_ACP, 0, text, -1, &result[0], length); + // MultiByteToWideChar includes the terminating NUL when cbMultiByte == -1. + // Allocate room for it, then remove it from the returned std::wstring. + std::wstring result(static_cast(length), L'\0'); + + if (MultiByteToWideChar(CP_ACP, 0, text, -1, result.data(), length) != length) + return {}; + + result.resize(static_cast(length - 1)); return result; } @@ -392,12 +398,15 @@ BOOL WINAPI hkDeviceIoControl(HANDLE device, DWORD controlCode, LPVOID inBuffer, BOOL WINAPI hkCloseHandle(HANDLE handle) { + const BOOL result = o_CloseHandle(handle); + + if (result) { std::unique_lock lock(_state.Mutex); ClearHidHandleLocked(handle); } - return o_CloseHandle(handle); + return result; } } // namespace OptiInput diff --git a/OptiScaler/menu/input/input_system_internal.h b/OptiScaler/menu/input/input_system_internal.h index 86a497291..fb7091978 100644 --- a/OptiScaler/menu/input/input_system_internal.h +++ b/OptiScaler/menu/input/input_system_internal.h @@ -77,6 +77,7 @@ struct DirectInputDeviceSlot bool InUse = false; void* Device = nullptr; DirectInputDeviceKind Kind = DirectInputDeviceKind::Other; + DWORD LastObjectDataSize = 0; }; enum class HidDeviceKind @@ -478,17 +479,18 @@ class ScopedHookBypass // Lifecycle bool InstallHooks(); -void RemoveHooks(); -void ReleaseTrackedWindowsHooksLocked(); +bool RemoveHooks(); +bool ReleaseTrackedWindowsHooksLocked(); // GameInput / Windows.Gaming.Input void UpdateGameInputIntegrationLocked(); -void RemoveGameInputHooksLocked(); +bool RemoveGameInputHooksLocked(); HRESULT WINAPI hkGameInputCreate(void** gameInput); // XInput void UpdateXInputIntegrationLocked(); -void RemoveXInputHooksLocked(); +bool RemoveXInputHooksLocked(); +void DrainXInputKeystrokesLocked(); DWORD WINAPI hkXInputGetState(DWORD userIndex, XINPUT_STATE* state); DWORD WINAPI hkXInputGetStateEx(DWORD userIndex, XINPUT_STATE* state); DWORD WINAPI hkXInputGetKeystroke(DWORD userIndex, DWORD reserved, PXINPUT_KEYSTROKE keystroke); @@ -496,7 +498,8 @@ DWORD WINAPI hkXInputSetState(DWORD userIndex, XINPUT_VIBRATION* vibration); // DirectInput void UpdateDirectInputIntegrationLocked(); -void RemoveDirectInputHooksLocked(); +bool RemoveDirectInputHooksLocked(); +void DrainDirectInputBufferedDataLocked(); HRESULT WINAPI hkDirectInput8Create(HINSTANCE instance, DWORD version, REFIID riid, LPVOID* out, LPUNKNOWN outer); HRESULT WINAPI hkDirectInputCreateA(HINSTANCE instance, DWORD version, void** out, LPUNKNOWN outer); HRESULT WINAPI hkDirectInputCreateW(HINSTANCE instance, DWORD version, void** out, LPUNKNOWN outer); @@ -512,7 +515,7 @@ ULONG WINAPI hkDirectInputDeviceRelease(void* device); void SetTargetWindow(HWND hwnd, bool isUwp, bool useWndProcSubclass); void SetInputWindow(HWND hwnd, bool useWndProcSubclass, bool explicitInputHwnd); bool InstallWindowSubclass(HWND hwnd); -void RemoveWindowSubclass(); +bool RemoveWindowSubclass(bool preserveLostChain = false); bool TryGetWindowProc(HWND hwnd, WNDPROC* wndProc); void ClearTargetWindowLocked(); void ClearInputWindowLocked(); @@ -578,7 +581,9 @@ void SanitizeRawMouseAllLocked(RAWINPUT& input); void SanitizeRawMouseKeepAllowedButtonUpsLocked(RAWINPUT& input, USHORT allowedButtonUpFlags); void SanitizeRawKeyboardLocked(RAWINPUT& input); int NormalizeRawKeyboardVirtualKey(const RAWKEYBOARD& keyboard); -void HandleRawInputLocked(HRAWINPUT rawInputHandle); +// Returns true when this packet must still reach the game because it contains an input release +// the game is owed. The GetRawInputData hook sanitizes the packet before the game receives it. +bool HandleRawInputLocked(HRAWINPUT rawInputHandle); // Win32 hook tracking bool IsTrackedWindowsHookType(int hookType); @@ -595,7 +600,7 @@ int WindowsHookMouseMessageToButton(int hookType, WPARAM wParam, LPARAM lParam); HOOKPROC GetWindowsHookProxyProc(std::size_t slotIndex); LRESULT CALLBACK InvokeWindowsHookProxy(std::size_t slotIndex, int code, WPARAM wParam, LPARAM lParam); void UpdateExternalMouseHookLocked(); -void RemoveExternalMouseHookLocked(); +bool RemoveExternalMouseHookLocked(); void EnsureExternalRawInputSinkLocked(); void PumpExternalRawInputSinkLocked(); void RemoveExternalRawInputSinkLocked(); diff --git a/OptiScaler/menu/input/input_system_messages.cpp b/OptiScaler/menu/input/input_system_messages.cpp index e9b0214a5..d08d1b7a2 100644 --- a/OptiScaler/menu/input/input_system_messages.cpp +++ b/OptiScaler/menu/input/input_system_messages.cpp @@ -218,14 +218,16 @@ void SetKeyDown(int vk, DWORD messageTime, bool blocked) { key.Pressed = true; _state.LastPressedKey = vk; + + // Only the real down transition can be a blocked press. Auto-repeat while the menu opens + // mid-hold must not steal the matching key-up from the game. + if (blocked) + key.BlockedDown = true; } key.Down = true; key.LastMessageTime = messageTime; - if (blocked) - key.BlockedDown = true; - SyncAggregateModifierStateLocked(); } @@ -609,10 +611,14 @@ bool HandleWindowMessage(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, Inpu const bool shouldParseRawInput = source == InputMessageSource::WndProc || _state.BlockMouse || _state.BlockKeyboard; + bool mustReachGame = false; + if (shouldParseRawInput) - HandleRawInputLocked(reinterpret_cast(lParam)); + mustReachGame = HandleRawInputLocked(reinterpret_cast(lParam)); - shouldBlock = _state.BlockMouse || _state.BlockKeyboard; + // The GetRawInputData hook uses the cached sanitize decision made above. Keep WM_INPUT + // reachable only when it carries an owed release; all other blocked packets stay hidden. + shouldBlock = (_state.BlockMouse || _state.BlockKeyboard) && !mustReachGame; break; } @@ -726,7 +732,14 @@ LRESULT CALLBACK OptiInputWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lPa return TRUE; if (handled) + { + // Foreground raw-input messages require DefWindowProc cleanup even when + // the application intentionally consumes the WM_INPUT. + if (msg == WM_INPUT && GET_RAWINPUT_CODE_WPARAM(wParam) == RIM_INPUT) + DefWindowProcW(hwnd, msg, wParam, lParam); + return 0; + } if (originalWndProc != nullptr) return CallWindowProcW(originalWndProc, hwnd, msg, wParam, lParam); @@ -770,6 +783,12 @@ bool ProcessRemovedMessage(MSG* msg) if (!handled) return false; + // Foreground raw-input messages require DefWindowProc cleanup. Since this + // queue message is being consumed before DispatchMessage, perform that + // cleanup here while the original WM_INPUT parameters are still intact. + if (msg->message == WM_INPUT && GET_RAWINPUT_CODE_WPARAM(msg->wParam) == RIM_INPUT) + DefWindowProcW(msg->hwnd, msg->message, msg->wParam, msg->lParam); + // Important: // Let TranslateMessage generate WM_CHAR for ImGui text input before // neutralizing the original key message. diff --git a/OptiScaler/menu/input/input_system_raw.cpp b/OptiScaler/menu/input/input_system_raw.cpp index 7dff6273d..e83825039 100644 --- a/OptiScaler/menu/input/input_system_raw.cpp +++ b/OptiScaler/menu/input/input_system_raw.cpp @@ -269,7 +269,11 @@ RawSanitizeAction GetRawKeyboardSanitizeActionLocked(const RAWKEYBOARD& keyboard if (!released) { - _state.RawKeyboardBlockedDown[vk] = true; + // A repeat arriving after the menu opens is not a blocked press if the key was already held. + // In that case the game saw the original press and must still receive the matching release. + if (!_state.Keys[vk].Down) + _state.RawKeyboardBlockedDown[vk] = true; + return RawSanitizeAction::SanitizeAll; } @@ -652,10 +656,10 @@ void UpdateStateFromRawInputLocked(const RAWINPUT& input) } } -void HandleRawInputLocked(HRAWINPUT rawInputHandle) +bool HandleRawInputLocked(HRAWINPUT rawInputHandle) { if (rawInputHandle == nullptr) - return; + return false; UINT size = 0; @@ -665,11 +669,11 @@ void HandleRawInputLocked(HRAWINPUT rawInputHandle) const UINT queryResult = o_GetRawInputData(rawInputHandle, RID_INPUT, nullptr, &size, sizeof(RAWINPUTHEADER)); if (queryResult != 0) - return; + return false; } if (size == 0) - return; + return false; std::vector buffer(size); @@ -682,15 +686,29 @@ void HandleRawInputLocked(HRAWINPUT rawInputHandle) o_GetRawInputData(rawInputHandle, RID_INPUT, buffer.data(), &readSize, sizeof(RAWINPUTHEADER)); if (readResult == static_cast(-1)) - return; + return false; if (readResult != size) - return; + return false; } const RAWINPUT* input = reinterpret_cast(buffer.data()); + // Decide before updating aggregate state: the blocked-down bookkeeping describes the state + // before this packet. The same decision is cached by HRAWINPUT and reused by GetRawInputData. + const RawInputSanitizeDecision decision = GetRawInputSanitizeDecisionLocked(rawInputHandle, *input); + + // A fully-passed keyboard packet can contain an owed key-up. A partially-sanitized mouse packet + // can contain owed button-up(s) while movement/new presses are removed. In both cases WM_INPUT + // must reach the game so it gets a chance to call GetRawInputData and receive the sanitized data. + const bool mustReachGame = + (input->header.dwType == RIM_TYPEKEYBOARD && decision.Action == RawSanitizeAction::Pass) || + (input->header.dwType == RIM_TYPEMOUSE && + decision.Action == RawSanitizeAction::SanitizeMouseKeepAllowedButtonUps); + UpdateStateFromRawInputLocked(*input); + + return mustReachGame; } void ApplyRawInputSanitizeActionLocked(RAWINPUT& input, RawSanitizeAction action, USHORT allowedMouseButtonUpFlags) diff --git a/OptiScaler/menu/input/input_system_window.cpp b/OptiScaler/menu/input/input_system_window.cpp index e07a27262..9ddb07fca 100644 --- a/OptiScaler/menu/input/input_system_window.cpp +++ b/OptiScaler/menu/input/input_system_window.cpp @@ -460,10 +460,21 @@ void ValidateWindowSubclassLocked() } } -void RemoveWindowSubclass() +bool RemoveWindowSubclass(bool preserveLostChain) { if (!_state.WndProcSubclassed || _state.InputHwnd == nullptr || _state.OriginalWndProc == nullptr) { + const bool lostChainMayStillReferenceUs = + !_state.WndProcSubclassed && _state.InputHwnd != nullptr && _state.OriginalWndProc != nullptr; + + if (lostChainMayStillReferenceUs && preserveLostChain) + { + LOG_WARN("RemoveWindowSubclass preserving original WndProc because another WndProc may still chain " + "through OptiInput input:{} original:{}", + static_cast(_state.InputHwnd), reinterpret_cast(_state.OriginalWndProc)); + return false; + } + if (_state.WndProcSubclassed || _state.OriginalWndProc != nullptr) { LOG_DEBUG("RemoveWindowSubclass clearing stale state input:{} original:{} subclassed:{}", @@ -472,7 +483,7 @@ void RemoveWindowSubclass() } _state.WndProcSubclassed = false; _state.OriginalWndProc = nullptr; - return; + return true; } WNDPROC currentWndProc = nullptr; @@ -481,17 +492,31 @@ void RemoveWindowSubclass() { LOG_INFO("removing subclass input:{} restoringWndProc:{}", static_cast(_state.InputHwnd), reinterpret_cast(_state.OriginalWndProc)); - SetWindowLongPtrW(_state.InputHwnd, GWLP_WNDPROC, reinterpret_cast(_state.OriginalWndProc)); + + SetLastError(0); + const LONG_PTR previous = + SetWindowLongPtrW(_state.InputHwnd, GWLP_WNDPROC, reinterpret_cast(_state.OriginalWndProc)); + + if (previous == 0 && GetLastError() != 0) + { + LOG_WARN("RemoveWindowSubclass restore failed input:{} lastError:{}", static_cast(_state.InputHwnd), + GetLastError()); + return false; + } } else { LOG_WARN("RemoveWindowSubclass did not restore because current WndProc changed input:{} current:{} opti:{}", static_cast(_state.InputHwnd), reinterpret_cast(currentWndProc), reinterpret_cast(OptiInputWndProc)); + + if (preserveLostChain) + return false; } _state.WndProcSubclassed = false; _state.OriginalWndProc = nullptr; + return true; } bool IsInputWindow(HWND hwnd) diff --git a/OptiScaler/menu/input/input_system_windows_hooks.cpp b/OptiScaler/menu/input/input_system_windows_hooks.cpp index f5db878c0..5b7db1e4b 100644 --- a/OptiScaler/menu/input/input_system_windows_hooks.cpp +++ b/OptiScaler/menu/input/input_system_windows_hooks.cpp @@ -323,7 +323,11 @@ bool ShouldBlockWindowsKeyboardHookCallbackLocked(WindowsHookSlot& slot, int cod if (!released) { - _state.WindowsHookKeyboardBlockedDown[vk] = true; + // Do not turn an auto-repeat from a key held before menu-open into a blocked press. + // The game hook already saw that original down and is therefore owed the matching up. + if (!_state.Keys[vk].Down) + _state.WindowsHookKeyboardBlockedDown[vk] = true; + return true; } @@ -389,6 +393,7 @@ LRESULT CALLBACK InvokeWindowsHookProxy(std::size_t slotIndex, int code, WPARAM { HHOOK hook = nullptr; HOOKPROC originalProc = nullptr; + int hookType = 0; bool shouldBlock = false; bool keyboardHook = false; bool mouseHook = false; @@ -406,6 +411,7 @@ LRESULT CALLBACK InvokeWindowsHookProxy(std::size_t slotIndex, int code, WPARAM hook = slot.Hook; originalProc = slot.OriginalProc; + hookType = slot.HookType; keyboardHook = IsKeyboardWindowsHookType(slot.HookType); mouseHook = IsMouseWindowsHookType(slot.HookType); shouldBlock = ShouldBlockWindowsHookCallbackLocked(slot, code, wParam, lParam); @@ -433,7 +439,16 @@ LRESULT CALLBACK InvokeWindowsHookProxy(std::size_t slotIndex, int code, WPARAM } if (shouldBlock) + { + // Low-level hooks run before Windows processes the input. Returning non-zero from + // WH_MOUSE_LL/WH_KEYBOARD_LL would discard the event system-wide, including the + // physical cursor/keyboard state used by our own polling fallback. Keep the event + // moving through the system hook chain while still bypassing the intercepted proc. + if (hookType == WH_MOUSE_LL || hookType == WH_KEYBOARD_LL) + return CallNextHookEx(hook, code, wParam, lParam); + return 1; + } if (originalProc == nullptr) return CallNextHookEx(hook, code, wParam, lParam); @@ -582,31 +597,50 @@ void UpdateExternalMouseHookLocked() static_cast(_state.TargetHwnd), _state.TargetProcessId); } -void RemoveExternalMouseHookLocked() +bool RemoveExternalMouseHookLocked() { HHOOK hook = _state.ExternalLowLevelMouseHook; + + if (hook == nullptr) + { + _state.ExternalLowLevelMouseHookInstalled = false; + _state.ExternalLastMouseHookScreenValid = false; + _state.ExternalPendingMouseDeltaX = 0; + _state.ExternalPendingMouseDeltaY = 0; + return true; + } + + if (o_UnhookWindowsHookEx == nullptr) + { + LOG_WARN("external low-level mouse hook removal deferred because UnhookWindowsHookEx is unavailable hook:{}", + static_cast(hook)); + return false; + } + + { + ScopedHookBypass bypass; + if (!o_UnhookWindowsHookEx(hook)) + { + LOG_WARN("external low-level mouse hook removal failed hook:{} error:{}", static_cast(hook), + GetLastError()); + return false; + } + } + _state.ExternalLowLevelMouseHook = nullptr; _state.ExternalLowLevelMouseHookInstalled = false; _state.ExternalLastMouseHookScreenValid = false; _state.ExternalPendingMouseDeltaX = 0; _state.ExternalPendingMouseDeltaY = 0; - if (hook == nullptr || o_UnhookWindowsHookEx == nullptr) - return; - - ScopedHookBypass bypass; - if (!o_UnhookWindowsHookEx(hook)) - { - LOG_WARN("external low-level mouse hook removal failed hook:{} error:{}", static_cast(hook), - GetLastError()); - return; - } - LOG_INFO("external low-level mouse hook removed hook:{}", static_cast(hook)); + return true; } -void ReleaseTrackedWindowsHooksLocked() +bool ReleaseTrackedWindowsHooksLocked() { + bool allRemoved = true; + for (WindowsHookSlot& slot : _state.WindowsHookSlots) { if (!slot.InUse || slot.Hook == nullptr) @@ -615,18 +649,34 @@ void ReleaseTrackedWindowsHooksLocked() continue; } - HHOOK hook = slot.Hook; - slot = {}; + if (o_UnhookWindowsHookEx == nullptr) + { + allRemoved = false; + continue; + } + + const HHOOK hook = slot.Hook; + BOOL removed = FALSE; - if (o_UnhookWindowsHookEx != nullptr) { ScopedHookBypass bypass; - o_UnhookWindowsHookEx(hook); + removed = o_UnhookWindowsHookEx(hook); + } + + if (!removed) + { + LOG_WARN("tracked windows hook removal failed hook:{} type:{} error:{}", static_cast(hook), + slot.HookType, GetLastError()); + allRemoved = false; + continue; } + + slot = {}; } _state.WindowsHookKeyboardBlockedDown = {}; _state.WindowsHookMouseBlockedDown = {}; + return allRemoved; } HHOOK WINAPI hkSetWindowsHookExA(int hookType, HOOKPROC proc, HINSTANCE module, DWORD threadId) diff --git a/OptiScaler/menu/input/input_system_xinput.cpp b/OptiScaler/menu/input/input_system_xinput.cpp index 4b65eb662..da6b2c5a4 100644 --- a/OptiScaler/menu/input/input_system_xinput.cpp +++ b/OptiScaler/menu/input/input_system_xinput.cpp @@ -136,13 +136,13 @@ void UpdateXInputIntegrationLocked() _state.XInputSetStateHookInstalled ? 1 : 0); } -void RemoveXInputHooksLocked() +bool RemoveXInputHooksLocked() { if (!_state.XInputGetStateHookInstalled && !_state.XInputGetStateExHookInstalled && !_state.XInputGetKeystrokeHookInstalled && !_state.XInputSetStateHookInstalled) { ClearXInputHookPointersLocked(); - return; + return true; } DetourTransactionBegin(); @@ -163,9 +163,43 @@ void RemoveXInputHooksLocked() const LONG result = DetourTransactionCommit(); if (result != NO_ERROR) - LOG_WARN("XInput hook removal completed with result:{}", result); + { + LOG_WARN("XInput hook removal failed result:{}; retaining trampoline pointers for a safe retry", result); + return false; + } ClearXInputHookPointersLocked(); + return true; +} + +void DrainXInputKeystrokesLocked() +{ + if (!_state.XInputGetKeystrokeHookInstalled || o_XInputGetKeystroke == nullptr) + return; + + constexpr DWORD MaxDrainPerUser = 256; + + for (DWORD userIndex = 0; userIndex < XUSER_MAX_COUNT; ++userIndex) + { + DWORD drained = 0; + + for (; drained < MaxDrainPerUser; ++drained) + { + XINPUT_KEYSTROKE keystroke {}; + DWORD result = ERROR_EMPTY; + + { + ScopedHookBypass bypass; + result = o_XInputGetKeystroke(userIndex, 0, &keystroke); + } + + if (result != ERROR_SUCCESS) + break; + } + + if (drained == MaxDrainPerUser) + LOG_WARN("XInput keystroke drain reached safety limit userIndex:{}", userIndex); + } } DWORD WINAPI hkXInputGetState(DWORD userIndex, XINPUT_STATE* state) @@ -286,28 +320,60 @@ DWORD WINAPI hkXInputGetStateEx(DWORD userIndex, XINPUT_STATE* state) DWORD WINAPI hkXInputGetKeystroke(DWORD userIndex, DWORD reserved, PXINPUT_KEYSTROKE keystroke) { + bool shouldBlock = false; + { std::unique_lock lock(_state.Mutex); _state.XInputGetKeystrokeCallCount++; + shouldBlock = ShouldBlockXInputLocked(); - if (ShouldBlockXInputLocked()) - { - if (keystroke != nullptr) - *keystroke = {}; - + if (shouldBlock) _state.XInputGetKeystrokeBlockedCount++; - OPTIINPUT_LOG_VERBOSE("blocking XInputGetKeystroke userIndex:{}", userIndex); - return ERROR_EMPTY; - } - - _state.XInputGetKeystrokePassedCount++; + else + _state.XInputGetKeystrokePassedCount++; } if (o_XInputGetKeystroke == nullptr) + { + if (keystroke != nullptr) + *keystroke = {}; return ERROR_EMPTY; + } + + if (!shouldBlock) + { + ScopedHookBypass bypass; + return o_XInputGetKeystroke(userIndex, reserved, keystroke); + } + + // XInputGetKeystroke is a destructive queue read. Consume all pending + // events for the requested index while the overlay owns input so they + // cannot replay after the menu closes. + constexpr DWORD MaxDrain = 256; + DWORD drained = 0; + + for (; drained < MaxDrain; ++drained) + { + XINPUT_KEYSTROKE discarded {}; + DWORD result = ERROR_EMPTY; + + { + ScopedHookBypass bypass; + result = o_XInputGetKeystroke(userIndex, reserved, &discarded); + } + + if (result != ERROR_SUCCESS) + break; + } + + if (drained == MaxDrain) + LOG_WARN("XInputGetKeystroke drain reached safety limit userIndex:{}", userIndex); + + if (keystroke != nullptr) + *keystroke = {}; - ScopedHookBypass bypass; - return o_XInputGetKeystroke(userIndex, reserved, keystroke); + OPTIINPUT_LOG_VERBOSE("blocking XInputGetKeystroke userIndex:{} drained:{}", userIndex, drained); + return ERROR_EMPTY; } DWORD WINAPI hkXInputSetState(DWORD userIndex, XINPUT_VIBRATION* vibration) From 8030eccac8a2008887256d83c53f3808390f9039 Mon Sep 17 00:00:00 2001 From: cdozdil Date: Wed, 2 Sep 2026 11:18:22 +0300 Subject: [PATCH 5/8] Fix a small mistake at input hooks --- OptiScaler/menu/input/input_system_detours.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/OptiScaler/menu/input/input_system_detours.cpp b/OptiScaler/menu/input/input_system_detours.cpp index ec92f604a..f333cd2e2 100644 --- a/OptiScaler/menu/input/input_system_detours.cpp +++ b/OptiScaler/menu/input/input_system_detours.cpp @@ -448,12 +448,12 @@ bool InstallHooks() #ifdef USE_HID_HOOKS const bool hidReady = State::Instance().isRunningOnLinux || hidHooks; -#else - const bool hidReady = false; -#endif // USE_HID_HOOKS - _state.HooksInstalled = messageHooks && keyStateHooks && getPosHooks && clipCursorHooks && message2Hooks && hidReady && rawHooks && windowsHooks && (positionHooks || positionIATHooks); +#else + _state.HooksInstalled = messageHooks && keyStateHooks && getPosHooks && clipCursorHooks && message2Hooks && + rawHooks && windowsHooks && (positionHooks || positionIATHooks); +#endif // USE_HID_HOOKS return _state.HooksInstalled; } From b4924e30cf80b93ffc303429a132252ecb098401 Mon Sep 17 00:00:00 2001 From: cdozdil Date: Thu, 3 Sep 2026 00:17:55 +0300 Subject: [PATCH 6/8] Fix for D3D11 Debug Layers --- OptiScaler/Source.def | 3 +++ 1 file changed, 3 insertions(+) diff --git a/OptiScaler/Source.def b/OptiScaler/Source.def index e76a372f5..10605a759 100644 --- a/OptiScaler/Source.def +++ b/OptiScaler/Source.def @@ -23,6 +23,9 @@ CreateDXGIFactory1 = _CreateDXGIFactory1 CreateDXGIFactory2 = _CreateDXGIFactory2 DXGIDeclareAdapterRemovalSupport = _DXGIDeclareAdapterRemovalSupport DXGIGetDebugInterface1 = _DXGIGetDebugInterface1 +ApplyCompatResolutionQuirking = _ApplyCompatResolutionQuirking +CompatString = _CompatString +CompatValue = _CompatValue EXPORTS AppCacheCheckManifest = _AppCacheCheckManifest From f740a7633497bf57d58e0c4bba051b782b783fed Mon Sep 17 00:00:00 2001 From: cdozdil Date: Thu, 3 Sep 2026 00:18:09 +0300 Subject: [PATCH 7/8] Fix for possible nullptr --- OptiScaler/hooks/D3D11_Hooks.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/OptiScaler/hooks/D3D11_Hooks.cpp b/OptiScaler/hooks/D3D11_Hooks.cpp index e32dd4202..7df05ab7d 100644 --- a/OptiScaler/hooks/D3D11_Hooks.cpp +++ b/OptiScaler/hooks/D3D11_Hooks.cpp @@ -334,12 +334,10 @@ static HRESULT hkD3D11CreateDeviceAndSwapChain(IDXGIAdapter* pAdapter, D3D_DRIVE } } + static const D3D_FEATURE_LEVEL levels[] = { D3D_FEATURE_LEVEL_11_1 }; + if (!(State::Instance().gameQuirks & GameQuirk::SkipD3D11FeatureLevelElevation)) { - static const D3D_FEATURE_LEVEL levels[] = { - D3D_FEATURE_LEVEL_11_1, - }; - D3D_FEATURE_LEVEL maxLevel = D3D_FEATURE_LEVEL_1_0_CORE; for (UINT i = 0; i < FeatureLevels; ++i) From 65a5f6f9c5cec6e0a898bf7accc4bfcdfc807a02 Mon Sep 17 00:00:00 2001 From: cdozdil Date: Thu, 3 Sep 2026 00:18:48 +0300 Subject: [PATCH 8/8] Better HDR management --- OptiScaler/State.h | 11 +- OptiScaler/menu/menu_common.cpp | 199 ++++++++++++++++----- OptiScaler/with_dx12/dx11_with_dx12_sc.cpp | 5 - OptiScaler/with_dx12/dx11_with_dx12_sc.h | 2 + OptiScaler/wrapped/wrapped_swapchain.cpp | 34 +++- OptiScaler/wrapped/wrapped_swapchain.h | 23 ++- 6 files changed, 215 insertions(+), 59 deletions(-) diff --git a/OptiScaler/State.h b/OptiScaler/State.h index f746f80be..99219ba36 100644 --- a/OptiScaler/State.h +++ b/OptiScaler/State.h @@ -86,6 +86,14 @@ enum class SwapchainInteropApi : uint32_t Dx11wDx12, }; +enum class ColorEncoding : uint32_t +{ + SDR, + ScRGB, + PQ, + HLG +}; + typedef struct CapturedHudlessInfo { UINT64 usageCount = 1; @@ -303,7 +311,8 @@ class State // HDR std::vector scBuffers; - bool isHdrActive = false; + ColorEncoding swapchainEncoding = ColorEncoding::SDR; + bool hdrOutputActive = false; std::optional setInputApiName; ApiUpscalerInput currentInputApiName; diff --git a/OptiScaler/menu/menu_common.cpp b/OptiScaler/menu/menu_common.cpp index 58099a18f..01f64586e 100644 --- a/OptiScaler/menu/menu_common.cpp +++ b/OptiScaler/menu/menu_common.cpp @@ -35,6 +35,15 @@ #include #include +enum class UiTargetMode +{ + SDR, + LinearHDR, + ScRGB, + PQ, + HLG +}; + #define MARK_ALL_BACKENDS_CHANGED() \ for (auto& singleChangeBackend : State::Instance().changeBackend) \ singleChangeBackend.second = true; @@ -826,68 +835,162 @@ void MenuCommon::PopulateCombo(const std::string& name, TStorage& currentValue, } } -static ImVec4 toneMapColor(const ImVec4& color) +static UiTargetMode getUiTargetMode() { - if (State::Instance().isHdrActive || - (!Config::Instance()->OverlayMenu.value_or_default() && State::Instance().currentFeature != nullptr && - State::Instance().currentFeature->IsHdr())) - { - // Controls how strongly HDR/UI colors are pushed into the tone mapper before compression. - // Higher values make colors brighter before mapping; lower values make the result dimmer. - constexpr float exposure = 1.0f; + const auto& state = State::Instance(); - // Blends between original color and fully tone-mapped color. - // 0.0 = no tone mapping, 1.0 = full Reinhard compression. - constexpr float strength = 1.0f; + const bool fallback = !Config::Instance()->OverlayMenu.value_or_default(); - float peak = std::max(color.x, std::max(color.y, color.z)); + if (fallback) + { + // We have no swapchain information here. + // Only classify the upscaled working image. + if (state.currentFeature && state.currentFeature->IsHdr()) + return UiTargetMode::LinearHDR; - if (peak <= 0.0f) - return color; + return UiTargetMode::SDR; + } - float exposedPeak = peak * exposure; - float mappedPeak = exposedPeak / (1.0f + exposedPeak); + // Normal overlay path: actual swapchain encoding is known. + switch (state.swapchainEncoding) + { + case ColorEncoding::ScRGB: + return UiTargetMode::ScRGB; + + case ColorEncoding::PQ: + return UiTargetMode::PQ; - float reinhardScale = mappedPeak / peak; - float scale = 1.0f + (reinhardScale - 1.0f) * strength; + case ColorEncoding::HLG: + return UiTargetMode::HLG; - return ImVec4(color.x * scale, color.y * scale, color.z * scale, color.w); + case ColorEncoding::SDR: + default: + return UiTargetMode::SDR; } +} + +static float srgbToLinear(float x) +{ + x = std::clamp(x, 0.0f, 1.0f); + + if (x <= 0.04045f) + return x / 12.92f; - return color; + return std::pow((x + 0.055f) / 1.055f, 2.4f); } -static void MenuHdrCheck(ImGuiIO io) +static float linearToPQ(float nits) { - // If game is using HDR, apply tone mapping to the ImGui style - if (State::Instance().isHdrActive || - (!Config::Instance()->OverlayMenu.value_or_default() && State::Instance().currentFeature != nullptr && - State::Instance().currentFeature->IsHdr())) + // SMPTE ST.2084 + constexpr float m1 = 2610.0f / 16384.0f; + constexpr float m2 = 2523.0f / 32.0f; + constexpr float c1 = 3424.0f / 4096.0f; + constexpr float c2 = 2413.0f / 128.0f; + constexpr float c3 = 2392.0f / 128.0f; + + float y = std::clamp(nits / 10000.0f, 0.0f, 1.0f); + + float ym1 = std::pow(y, m1); + + return std::pow((c1 + c2 * ym1) / (1.0f + c3 * ym1), m2); +} + +static float linearToHLG(float x) +{ + // BT.2100 HLG OETF + constexpr float a = 0.17883277f; + constexpr float b = 0.28466892f; + constexpr float c = 0.55991073f; + + x = std::max(x, 0.0f); + + if (x <= (1.0f / 12.0f)) + return std::sqrt(3.0f * x); + + return a * std::log(12.0f * x - b) + c; +} + +static ImVec4 toneMapColor(const ImVec4& color) +{ + const auto mode = getUiTargetMode(); + + switch (mode) { - if (!_hdrTonemapApplied) - { - ImGuiStyle& style = ImGui::GetStyle(); + case UiTargetMode::SDR: + // Standard ImGui colors are already authored for SDR/sRGB. + return color; - CopyMemory(SdrColors, style.Colors, sizeof(style.Colors)); + case UiTargetMode::LinearHDR: + { + // Fallback mode: rendering directly into the upscaled HDR image. + // + // We don't know the final swapchain encoding here, so do NOT apply + // PQ/HLG encoding. Just convert ImGui's sRGB colors to linear. + // + // If we later determine that the upscaled image is pre-exposed, + // this is where the pre-exposure scale should be applied. + constexpr float workingSpaceScale = 1.0f; + + return ImVec4(srgbToLinear(color.x) * workingSpaceScale, srgbToLinear(color.y) * workingSpaceScale, + srgbToLinear(color.z) * workingSpaceScale, color.w); + } - // Apply tone mapping to the ImGui style - for (int i = 0; i < ImGuiCol_COUNT; ++i) - { - ImVec4 color = style.Colors[i]; - style.Colors[i] = toneMapColor(color); - } + case UiTargetMode::ScRGB: + { + // scRGB is linear and uses ~80 nits for value 1.0. + constexpr float scRgbReferenceWhiteNits = 80.0f; + constexpr float hdrUiWhiteNits = 203.0f; - _hdrTonemapApplied = true; - } + // On SDR output keep ordinary SDR white at scRGB 1.0. + // When HDR output is active, raise UI reference white. + const float uiWhiteNits = State::Instance().hdrOutputActive ? hdrUiWhiteNits : scRgbReferenceWhiteNits; + + const float scale = uiWhiteNits / scRgbReferenceWhiteNits; + + return ImVec4(srgbToLinear(color.x) * scale, srgbToLinear(color.y) * scale, srgbToLinear(color.z) * scale, + color.w); } - else + + case UiTargetMode::PQ: + { + // HDR10 / ST.2084. + // + // ImGui colors are interpreted as SDR-relative colors where + // 1.0 corresponds to our chosen HDR UI reference white. + constexpr float uiWhiteNits = 203.0f; + + return ImVec4(linearToPQ(srgbToLinear(color.x) * uiWhiteNits), linearToPQ(srgbToLinear(color.y) * uiWhiteNits), + linearToPQ(srgbToLinear(color.z) * uiWhiteNits), color.w); + } + + case UiTargetMode::HLG: { - if (_hdrTonemapApplied) + // HLG is relative rather than absolute-nits based. + return ImVec4(linearToHLG(srgbToLinear(color.x)), linearToHLG(srgbToLinear(color.y)), + linearToHLG(srgbToLinear(color.z)), color.w); + } + + default: + return color; + } +} + +static void MenuHdrCheck(ImGuiIO io) +{ + if (!_hdrTonemapApplied) + { + ImGuiStyle& style = ImGui::GetStyle(); + + CopyMemory(SdrColors, style.Colors, sizeof(style.Colors)); + + // Apply tone mapping to the ImGui style + for (int i = 0; i < ImGuiCol_COUNT; ++i) { - ImGuiStyle& style = ImGui::GetStyle(); - CopyMemory(style.Colors, SdrColors, sizeof(style.Colors)); - _hdrTonemapApplied = false; + ImVec4 color = style.Colors[i]; + style.Colors[i] = toneMapColor(color); } + + _hdrTonemapApplied = true; } } @@ -1552,9 +1655,10 @@ void MenuCommon::RenderNotifications(RenderMenuContext& ctx) auto& io = ctx.io; // Notifications - bool tonemapRequired = State::Instance().isHdrActive || - (!Config::Instance()->OverlayMenu.value_or_default() && - State::Instance().currentFeature != nullptr && State::Instance().currentFeature->IsHdr()); + bool tonemapRequired = + (State::Instance().hdrOutputActive && State::Instance().swapchainEncoding != ColorEncoding::SDR) || + (!Config::Instance()->OverlayMenu.value_or_default() && State::Instance().currentFeature != nullptr && + State::Instance().currentFeature->IsHdr()); float screenHeight = State::Instance().screenHeight; if (io.DisplaySize.y != 0) @@ -3812,7 +3916,7 @@ void MenuCommon::RenderFrameGenerationRuntimeSettings(RenderMenuContext& ctx) ImGui::TextColored(toneMapColor(ImVec4(1.f, 0.f, 0.f, 1.f)), "Borderless display mode required!"); } - if (!ignoreChecks && state.isHdrActive) + if (!ignoreChecks && (state.hdrOutputActive && state.swapchainEncoding != ColorEncoding::SDR)) { if (state.currentSwapchainDesc.BufferDesc.Format >= DXGI_FORMAT_R32G32B32A32_TYPELESS && state.currentSwapchainDesc.BufferDesc.Format <= DXGI_FORMAT_R16G16B16A16_SINT) @@ -3977,7 +4081,8 @@ void MenuCommon::RenderFrameGenerationRuntimeSettings(RenderMenuContext& ctx) { ImGui::SeparatorText("Frame Generation (DLSSG)"); - if (state.activeFgNvngx == FGNvngxReplacement::None && state.isHdrActive) + if (state.activeFgNvngx == FGNvngxReplacement::None && + (state.hdrOutputActive && state.swapchainEncoding != ColorEncoding::SDR)) { if (state.currentSwapchainDesc.BufferDesc.Format >= DXGI_FORMAT_R32G32B32A32_TYPELESS && state.currentSwapchainDesc.BufferDesc.Format <= DXGI_FORMAT_R16G16B16A16_SINT) diff --git a/OptiScaler/with_dx12/dx11_with_dx12_sc.cpp b/OptiScaler/with_dx12/dx11_with_dx12_sc.cpp index 1df3ce3d2..752c4c303 100644 --- a/OptiScaler/with_dx12/dx11_with_dx12_sc.cpp +++ b/OptiScaler/with_dx12/dx11_with_dx12_sc.cpp @@ -598,11 +598,6 @@ HRESULT STDMETHODCALLTYPE Dx11wDx12SC::CheckColorSpaceSupport(DXGI_COLOR_SPACE_T HRESULT STDMETHODCALLTYPE Dx11wDx12SC::SetColorSpace1(DXGI_COLOR_SPACE_TYPE ColorSpace) { - State::Instance().isHdrActive = ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020 || - ColorSpace == DXGI_COLOR_SPACE_YCBCR_FULL_GHLG_TOPLEFT_P2020 || - ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P2020 || - ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709; - if (_fgSwapChain != nullptr) return _fgSwapChain->SetColorSpace1(ColorSpace); diff --git a/OptiScaler/with_dx12/dx11_with_dx12_sc.h b/OptiScaler/with_dx12/dx11_with_dx12_sc.h index 55decf547..481ffc7d5 100644 --- a/OptiScaler/with_dx12/dx11_with_dx12_sc.h +++ b/OptiScaler/with_dx12/dx11_with_dx12_sc.h @@ -9,6 +9,8 @@ #include +using Microsoft::WRL::ComPtr; + class DECLSPEC_UUID("23b064bb-482d-416c-93b1-829acedfb3d0") Dx11wDx12SC final : public IDXGISwapChain4 { public: diff --git a/OptiScaler/wrapped/wrapped_swapchain.cpp b/OptiScaler/wrapped/wrapped_swapchain.cpp index e7c06e061..902493425 100644 --- a/OptiScaler/wrapped/wrapped_swapchain.cpp +++ b/OptiScaler/wrapped/wrapped_swapchain.cpp @@ -444,7 +444,7 @@ WrappedIDXGISwapChain4::WrappedIDXGISwapChain4(IDXGISwapChain* real, IUnknown* p _real->AddRef(); auto refCount = _real->Release(); - _device2 = _device; + CheckForHdrOutput(); LOG_INFO("{} created, real: {:X}, refCount: {}", _id, (UINT64) real, refCount); } @@ -952,6 +952,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDXGISwapChain4::ResizeBuffers(UINT BufferCount State::Instance().currentFG->Mutex.unlockThis(3); } + CheckForHdrOutput(); + return result; } @@ -1103,10 +1105,30 @@ HRESULT STDMETHODCALLTYPE WrappedIDXGISwapChain4::CheckColorSpaceSupport(DXGI_CO HRESULT STDMETHODCALLTYPE WrappedIDXGISwapChain4::SetColorSpace1(DXGI_COLOR_SPACE_TYPE ColorSpace) { - State::Instance().isHdrActive = ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020 || - ColorSpace == DXGI_COLOR_SPACE_YCBCR_FULL_GHLG_TOPLEFT_P2020 || - ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P2020 || - ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709; + if (ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020 || + ColorSpace == DXGI_COLOR_SPACE_RGB_STUDIO_G2084_NONE_P2020 || + ColorSpace == DXGI_COLOR_SPACE_YCBCR_STUDIO_G2084_LEFT_P2020 || + ColorSpace == DXGI_COLOR_SPACE_YCBCR_STUDIO_G2084_TOPLEFT_P2020) + { + State::Instance().swapchainEncoding = ColorEncoding::PQ; + } + else if (ColorSpace == DXGI_COLOR_SPACE_YCBCR_FULL_GHLG_TOPLEFT_P2020 || + ColorSpace == DXGI_COLOR_SPACE_YCBCR_STUDIO_GHLG_TOPLEFT_P2020) + { + State::Instance().swapchainEncoding = ColorEncoding::HLG; + } + else if (ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709) + { + State::Instance().swapchainEncoding = ColorEncoding::ScRGB; + } + else + { + State::Instance().swapchainEncoding = ColorEncoding::SDR; + } + + CheckForHdrOutput(); + + MenuOverlayDx::CleanupRenderTarget(true, _handle); return _real3->SetColorSpace1(ColorSpace); } @@ -1361,6 +1383,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDXGISwapChain4::ResizeBuffers1(UINT BufferCoun State::Instance().currentFG->Mutex.unlockThis(3); } + CheckForHdrOutput(); + return result; } diff --git a/OptiScaler/wrapped/wrapped_swapchain.h b/OptiScaler/wrapped/wrapped_swapchain.h index 98254c83e..dd2a9dda6 100644 --- a/OptiScaler/wrapped/wrapped_swapchain.h +++ b/OptiScaler/wrapped/wrapped_swapchain.h @@ -7,6 +7,8 @@ #include "dxgi1_6.h" #include "d3d12.h" +using Microsoft::WRL::ComPtr; + #define USE_LOCAL_MUTEX class DECLSPEC_UUID("3af622a3-82d0-49cd-994f-cce05122c222") WrappedIDXGISwapChain4 final : public IDXGISwapChain4 @@ -90,11 +92,30 @@ class DECLSPEC_UUID("3af622a3-82d0-49cd-994f-cce05122c222") WrappedIDXGISwapChai UINT _lastFlags = 0; IUnknown* _device = nullptr; - IUnknown* _device2 = nullptr; HWND _handle = nullptr; #ifdef USE_LOCAL_MUTEX OwnedMutex _localMutex; #endif + + void CheckForHdrOutput() + { + ComPtr output; + + HRESULT hr = GetContainingOutput(&output); + if (SUCCEEDED(hr) && output) + { + ComPtr output6; + + if (SUCCEEDED(output.As(&output6))) + { + DXGI_OUTPUT_DESC1 desc {}; + if (SUCCEEDED(output6->GetDesc1(&desc))) + { + State::Instance().hdrOutputActive = desc.ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020; + } + } + } + } };