Fix EVENT_FRAME/EVENT_RESIZE never firing: InitializeStage3Hooks was never called - #83
Conversation
…never called YkSetupFinalInitialization() resolves the live D3D11 swapchain (m_EngineSwapchain) via YkFetchD3D11Info specifically so it can be handed to Hooks::InitializeStage3Hooks, which installs the manual VMT hooks on IDXGISwapChain::Present/ResizeBuffers backing EVENT_FRAME/EVENT_RESIZE. That call was missing -- the swapchain got resolved and stored, but the hooks were never actually installed. CreateCallback(..., EVENT_FRAME, ...) has no way to detect this: it just registers into m_RegisteredCallbacks and reports AURIE_SUCCESS regardless of whether the underlying hook exists. So EVENT_FRAME/EVENT_RESIZE silently never fire -- no error, no crash, the callback is just unreachable, since nothing ever hooks Present()/ResizeBuffers() to dispatch to it in the first place. Confirmed by comparing against the stable branch, which has the identical VMT-hooking logic (there named Hooks::HkInitialize) and does call it, right after resolving the swapchain (Interface.cpp:914). This looks like a leftover from the v5 "Zeus" staged-init rewrite that split the old monolithic Interface.cpp into MI_Aurie.cpp/MI_Internal.cpp/ MI_Public.cpp -- InitializeStage3Hooks survived the move intact (it's byte-for-byte the same hooking code as stable's HkInitialize), but its one call site did not. This mirrors the exact call/log/error-check pattern already used two blocks above for InitializeStage1Hooks/InitializeStage2Hooks in this same function, using only members (m_WindowHandle, m_EngineSwapchain) already set earlier in the same init chain. Validated indirectly but concretely: an out-of-process plugin performing the equivalent operation (resolve the swapchain via the same os_get_info()-based technique YkFetchD3D11Info uses, then Aurie::MmCreateHook directly on vtable[8]) was confirmed live against a real GameMaker YYC game (GM 2026.0.0.23) -- hook installs successfully, fires on the very next Present() call, and sustained 115,000+ calls over 16 minutes of live gameplay with no gaps, crashes, or rendering glitches. Not build-tested against the full YYToolkit.dll (didn't set up the full build environment for this), but the change itself is minimal, in-style, and uses only symbols already exercised correctly elsewhere in this exact function.
|
Built this branch locally (plain MSBuild against Confirmed fixed, via the actual (The ~2s gap between stage 2 and stage 3 is the Every other Zeus subsystem in the log still reports Functional test: registered Updating the "not build-tested" caveat in the PR description above — this is now build-tested and live-confirmed, not just reasoned-through. |
The bug
EVENT_FRAME/EVENT_RESIZEregister successfully viaCreateCallbackbut the callback never actually fires, on any registration timing or calling-convention signature. Root cause:Hooks::InitializeStage3Hooks(the function that installs the manual VMT hooks onIDXGISwapChain::Present/ResizeBuffersthat these two events depend on) is defined inHooks.cppbut is never called anywhere in the codebase.YkSetupFinalInitialization()resolves the live D3D11 swapchain (m_EngineSwapchain) viaYkFetchD3D11Info, specifically so it can be handed toInitializeStage3Hooks— but then just setsm_ThirdInitComplete = trueand returns. The call that would actually install the hooks using that swapchain is missing.CreateCallbackhas no way to detect this — it just registers intom_RegisteredCallbacksand reportsAURIE_SUCCESSregardless of whether the underlying hook exists. SoEVENT_FRAME/EVENT_RESIZEfail completely silently: no error, no crash, the callback is just permanently unreachable.Confirmed via
stable, and it's a regression from the Zeus rewritestablehas the identical VMT-hooking code (byte-for-byte the samePresent/ResizeBuffershooking logic, just namedHooks::HkInitializethere instead ofInitializeStage3Hooks) — and it is called, right after the swapchain is resolved:Interface.cpp:914.So this isn't a gap that's always existed — it looks like a leftover from the v5 "Zeus" staged-init rewrite that split the old monolithic
Interface.cppintoMI_Aurie.cpp/MI_Internal.cpp/MI_Public.cpp.InitializeStage3Hookssurvived the move intact; its one call site did not.I checked all four hook-installing functions in
experimental'sHooks.cppfor call sites —InitializeStage3Hooksis the only one with zero:InitializeStage1Hooks(→HkExecuteIt)EVENT_OBJECT_CALLMI_Aurie.cpp:80✅InitializeStage2Hooks(→HkYYError,HkWndProc)EVENT_WNDPROCMI_Aurie.cpp:434✅InitializeStage3Hooks(→HkPresent,HkResizeBuffers)EVENT_FRAME,EVENT_RESIZEThe fix
Add the missing call in
YkSetupFinalInitialization(), mirroring the exact call/log/error-check pattern already used forInitializeStage1Hooks/InitializeStage2Hookstwo blocks above in the same function, and mirroringstable's working call site. Uses onlym_WindowHandle/m_EngineSwapchain, both already set earlier in the same init chain.Validation — build-tested and live-confirmed
Built this branch (plain MSBuild against
YYToolkit.sln, no changes needed) and ran it against the real game that surfaced this bug, with a test plugin that registersEVENT_FRAMEthe normal way (CreateCallback, no workaround). Confirmed viaaurie.log's own trace output that the new call actually runs and succeeds:(the ~2s gap is the existing
YkFetchD3D11Infopolling loop waiting for the swapchain — expected.) Every other Zeus subsystem in the log still reportsAURIE_SUCCESS— nothing else regressed.Functionally:
EVENT_FRAMEfired on the very first frame after registration and sustained continuous calls with zero gaps for the whole session (GM 2026.0.0.23, LTS, YYC x64 runner), running alongside an unrelatedEVENT_OBJECT_CALLsubscriber the whole time with no interference either direction. Full narrative in the PR comment below.Happy to answer questions or adjust the approach if there's a reason this call was left out that I'm not seeing.