#1124·OptiScaler

Overlay input blocking: desktop-wide WH_MOUSE_LL event deletion, and a DirectInput re-hook check that can never succeed

Author: UminoryCreated Sep 1, 2026Updated Sep 16, 2026

This is not a crash report and I am not asking for support for my installation. Two defects inmenu/input/ are reported below — both read off master's source and verified against my logs — anda third, in an unrelated area, is noted at the end only because the fix is one line. The environmentis filled in because the template asks for it.

Defects 1 and 2 are in the same subsystem and both concern the input blocking added for #668, whichis why they are in one report; each has its own evidence and its own suggested fix, so they can besplit or tracked separately.

Generalised trigger condition for defect 1: any code inside the game's process callsSetWindowsHookEx(WH_MOUSE_LL, …) while OptiScaler's SetWindowsHookEx proxy is active. The proxyonly intercepts calls made inside the game process, so this cannot be reproduced with an externaltest program — the low-level hook has to be installed by the game or by something loaded into it.That is why my observations of it cannot be separated from my environment even though the defectitself can be read straight off the source. Defect 2 needs no special environment at all.


Environment

Game name and version:

Genshin Impact 7.0 (D3D11). Kernel-level anti-cheat; OptiScaler is loaded by a third-party launcherrather than the usual dxgi.dll rename, so setup_windows.bat was not used.

Mods and mod versions used

OptiScaler v10.0.0-dev (792f2f1) (20260830_121248) — the binary I measured is a build ofOptiScaler_DLSSNR, a downstream fork of thatcommit, with the input code unmodified in it.

All line numbers in this report come from a master source snapshot I downloaded on 2026-08-31, notfrom 792f2f1 itself — if the two have drifted, trust the quoted code over the line numbers.

GPU

RTX 4060 Laptop

OS

Windows 10 22H2

Used automated or manual install?

  • Automated
  • Manual

Strictly neither: because of the anti-cheat the DLL is loaded by a third-party launcher, so noinstaller script was involved. Manual is the closer of the two.

If on AMD/Intel and Automated, used DLSS inputs?

  • Yes
  • No

N/A — NVIDIA GPU, and not an automated install.

Did you check the Wiki and Compatibility List?

  • Yes
  • No

Please describe the issue and steps to reproduce it

Upscaler inputs: this game has no upscaler selector of its own — the inputs are supplied by athird-party FSR bridge, and OptiScaler's selected output upscaler is DLSS (Dx12Upscaler = dlss).None of that matters for defects 1 and 2: that code lives in the input/overlay layer and runsregardless of which upscaler is active.

Defect 1, as observed:

  1. Run the game with OptiScaler loaded, with any in-process component installing a WH_MOUSE_LLhook (here: the anti-cheat, which re-arms one every ~5 s).
  2. Press Insert to open the OptiScaler overlay.
  3. The physical mouse pointer stops moving across the whole desktop, not just inside the game.Keyboard navigation of the overlay still works normally.
  4. Close the overlay — the pointer recovers immediately.

Defect 2, as observed: 27 DirectInput … pointer differs, not detouring new pointer warnings in a61 s session, including one where a device object warns about itself 50 µs after being hookedsuccessfully. No reproduction conditions beyond "a game that creates more than one DirectInputdevice, or calls CreateDevice more than once".


Defect 1 — InvokeWindowsHookProxy returns 1 from low-level hooks, deleting the event system-wide

OptiScaler/menu/input/input_system_windows_hooks.cpp:435

if (shouldBlock)
    return 1;

if (originalProc == nullptr)
    return CallNextHookEx(hook, code, wParam, lParam);

return originalProc(code, wParam, lParam);

For a thread hook (WH_MOUSE, WH_KEYBOARD) that is correct: it only stops delivery to thetarget window. For a low-level hook (WH_MOUSE_LL, WH_KEYBOARD_LL) the hook runs before thesystem processes the raw event, so a nonzero return makes the OS discard the event outright. Whilethe overlay is open the physical mouse pointer therefore stops moving for the whole desktop, notjust in the game; closing the overlay restores it. With WH_KEYBOARD_LL the same code would swallowthe desktop's keyboard.

IsKeyboardWindowsHookType (:67) and IsMouseWindowsHookType (:69) deliberately merge the LLand thread variants, so nothing downstream of ShouldBlockWindowsHookCallbackLocked can tell themapart at the return site.

Why this is self-defeating rather than just heavy-handed

The blocking OptiScaler actually needs is already implemented elsewhere, and it depends on the realcursor continuing to move:

  • ApplyMenuVisibilityChangeLocked snapshots BlockedCursorScreenPos and callsBeginCursorClipBlockLocked() on menu open (input_system.cpp:711-724).
  • hkGetCursorPos then returns that pinned point to every caller outside OptiScaler.dll, and thereal point to OptiScaler itself — the module of the return address decides(input_system_cursor.cpp:148, :176, :190-206).

So the game already sees a frozen cursor position by design. Meanwhile the overlay's own pointercomes from GetCursorPos in PollInputFallbackLocked, and in my session the acquisition mode waspolled-absolute on 1566 of 2375 menu-visible frames. Deleting the events that move the OS cursorremoves the overlay's own input source and makes the virtualization pointless, while costing everyother application on the desktop its mouse.

Suggested fix

Forward blocked LL events down the chain instead of deleting them, while still not calling thegame's originalProc — that keeps #668 fixed, because from the game's point of view the event isstill gone:

if (shouldBlock)
{
    // Low-level hooks: a nonzero return makes the OS drop the event system-wide.
    // Pass it on, but never to the game's own proc.
    if (hookType == WH_MOUSE_LL || hookType == WH_KEYBOARD_LL)
        return CallNextHookEx(hook, code, wParam, lParam);

    return 1;
}

slot.HookType needs to be copied out alongside hook / originalProc inside the existing lockscope (:407-410).

Evidence

I verified the mechanism and the fix on a release build by binary-patching the three return 1tails that MSVC emitted for that statement, redirecting each into the function's ownCallNextHookEx(hook, code, wParam, lParam) path. Result: the cursor behaves normally with theoverlay open, and the overlay still blocks input into the game — i.e. the OS-level discard was theonly thing lost. That is the answer to the obvious question about the fix above: removing thediscard does not reopen input to the game.

Supporting numbers from the attached log (61 s, 68k lines, overlay open 26.7 s / 2375 frames):

  • 39 intercepted SetWindowsHookExW calls across all my logs are type:14 slot:0 — the gameinstalls one global WH_MOUSE_LL hook, re-armed every ~5 s, and no keyboard hook at all. So everyexecution of return 1 in this game lands on the one type where it means "delete".
  • recvWnd:yes on 1 of 2375 frames; the window-message route is unavailable here (the subclass istaken over by another WndProc ~6 s after install and ValidateWindowSubclassLocked deliberatelydoes not reinstall it), which is why the overlay falls back to polling GetCursorPos.

Empirically this is a regression relative to 0.9.4, which I ran for weeks in the same setup with aworking mouse in the overlay; 0.9.4 has no SetWindowsHookEx proxy.


Defect 2 — the DirectInput re-hook check compares a Detours trampoline with the vtable entry

HookDirectInputDeviceLocked (OptiScaler/menu/input/input_system_directinput.cpp:203) keeps oneglobal original pointer per method and decides whether to attach by comparing it with the candidatedevice's vtable entry:

PVOID* vtable = *reinterpret_cast<PVOID**>(device);
auto getDeviceState = reinterpret_cast<DirectInputGetDeviceState_t>(vtable[9]);
...
if (o_DirectInputDeviceGetDeviceState == nullptr)
{
    o_DirectInputDeviceGetDeviceState = getDeviceState;
    attachGetDeviceState = o_DirectInputDeviceGetDeviceState != nullptr;
}
else if (o_DirectInputDeviceGetDeviceState != getDeviceState)
{
    LOG_WARN("DirectInput GetDeviceState pointer differs, not detouring new pointer device:{}", device);
}

After a successful DetourAttach(&o_DirectInputDeviceGetDeviceState, hkDirectInputGetDeviceState)(:261), o_DirectInputDeviceGetDeviceState holds Detours' trampoline, not the originalfunction. The vtable entry still holds the original address. So from the second call onward thecomparison can never succeed — not even for the same device object.

Two consequences

1. The warning is unconditional and misleading. In my log the same device pointer warns aboutitself 50 µs after it was hooked successfully:

13:26:50.032693 [I] TrackDirectInputDeviceLocked  DirectInput device captured device:0x16704627048 kind:mouse
13:26:50.032743 [W] HookDirectInputDeviceLocked   DirectInput Release pointer differs ... device:0x16704627048
13:26:50.032747 [W] HookDirectInputDeviceLocked   DirectInput GetDeviceState pointer differs ... device:0x16704627048
13:26:50.032751 [W] HookDirectInputDeviceLocked   DirectInput GetDeviceData pointer differs ... device:0x16704627048

Its vtable cannot have changed in 50 µs. 27 of these warnings in a 61 s session, all [W], readingas "DirectInput blocking is not installed" when it is. To be clear about what I am not claiming:DirectInput blocking worked in this session — the mouse device was detoured cleanly. The cost wasdiagnostic. I spent a long time misdiagnosing my own overlay-input problem from these warningsbefore reading the source, and they would mislead anyone triaging a #668-style report the same way.

2. A device with a genuinely different implementation is silently never hooked. Because Detourspatches the target function, every device whose vtable[9] is that same function is coveredautomatically — which is why blocking works at all. But a device with a different implementation(dinput8's keyboard device class vs its mouse device class, a game-side wrapper, or dinput.dlllegacy devices alongside dinput8.dll) needs its own detour, and this branch is the only place thatwould attach one. Since the check cannot distinguish "same implementation, already covered" from"different implementation, needs attaching", the second implementation is dropped without anydiagnosable signal. Whichever device type is created first wins; if that is the keyboard, mousedeltas keep reaching the game while the overlay is open — the #668 symptom.

Suggested fix

Track the original target addresses, not the live o_ pointers:

  • keep a small set of already-detoured original function addresses (recorded before DetourAttach),and test candidates against that set — this alone silences the false warnings;
  • when a candidate's implementation is not in the set, attach a detour for it too, keyed by originaladdress (a small table of {original, trampoline} per method rather than one global o_), insteadof bailing out.

Minor, same file

TrackDirectInputDeviceLocked overwrites slot.Kind for an already-tracked device (:169-173), andhkDirectInputCreateDevice{A,W} derives the kind from the GUID of that call (:670, :692), whereanything other than GUID_SysMouse / GUID_SysKeyboard maps to Other (:31-40) — including adevice instance GUID from EnumDevices. In my log the mouse device is re-labelled kind:other 5times and the keyboard device 3 times after their correct first classification. This is currentlyharmless because ShouldBlockDirectInputOtherLocked() (:52) also blocks while the menu is visible,but slot.Kind is what hkDirectInputGetDeviceState uses to choose a policy (:707), so any futuredivergence (e.g. letting gamepads through) would silently mis-handle a mouse. Suggest neverdowngrading a known Keyboard/Mouse to Other.


Appendix — unrelated area, low priority: ReadUpscalerTime(void*) has no type discipline

Not reproducible on a stock install, and not part of the above. Mentioning it only because the guardis one line.

wrapped_swapchain.cpp:252 checks the feature type before handing over a command queue, but theelse branch has no symmetric check before handing over a device context:

if (cq != nullptr && currentFeature->Api() == API::DX12 && !currentFeature->IsWithDx12())
    ... currentFeature->ReadUpscalerTime(cq)
else if (device != nullptr)                              // no type check
{
    ID3D11DeviceContext* context = nullptr;
    device->GetImmediateContext(&context);
    ... currentFeature->ReadUpscalerTime(context)        // <-- DX11 context
}

ReadUpscalerTime is virtual std::optional<double> ReadUpscalerTime(void*) (IFeature.h:121), sothe argument type is entirely the caller's responsibility, and IFeature_Dx12::ReadUpscalerTime(IFeature_Dx12.cpp:287) trusts it absolutely: (ID3D12CommandQueue*) commandQueueVoid. A plainIFeature_Dx12 under a DX11 swapchain therefore receives an ID3D11DeviceContext* and callsGetTimestampFrequency on it — vtable slot 16, which on a DX11 context is PSSetConstantBuffers.wrapped_swapchain.cpp:267-277 and hooks/FG_Hooks.cpp:1148-1156 have the same shape.

Suggested guard:

else if (device != nullptr && (currentFeature->Api() != API::DX12 || currentFeature->IsWithDx12()))

Why it does not affect your users: a stock DX11 game gets IFeature_Dx11 (correctly typed) orIFeature_Dx11wDx12, whose override ignores the passed context and substitutes its own queue(IFeature_Dx11wDx12.h:74-79). I reach it (0xC0000005 in d3d11.dll, same PC every launch, oneframe after the first upscaled frame) only because a third-party DX11→DX12 bridge dispatches to areal DX12 feature underneath the game's DX11 swapchain, producing exactlyApi() == DX12 && !IsWithDx12() with cq == nullptr — a configuration your code has no reason toexpect. Worked around locally by returning std::nullopt, which costs only the overlay's upscalerGPU-time readout. Close this part as won't-fix without hesitation if the one-line guard is not worthit; it is a void* interface hardening remark, not a user-facing bug.


Related: #668, which is what this input-blocking system implements. Defect 1's suggested fix does notregress it, and defect 2 is a plausible cause of #668-style reports on games whose keyboardDirectInput device is created before the mouse one.

I have attached

  • OptiScaler.log (set LogLevel=1 and LogToFile=true in OptiScaler.ini, zip it if too big)

OptiScaler.log

  • Screenshot of game folder (where you placed Opti)
  • Screenshot of Opti overlay in-game (opens with shortcut, default Insert)

One deviation to declare: the attached log is LogLevel=1 (Debug), not 0 (Trace). Every linereferenced above is present at Debug; Trace writes ~10 MB per session on this setup and instruments alot of per-frame paths, which I wanted to keep out of the timing numbers. Say the word if you want aTrace-level log as well.