Add NVIDIA Smooth Motion Compatibility#2849
Open
ConfoundedHermit wants to merge 11 commits into
Open
Conversation
Author
|
There was an issue introduced in a crash fix (and carried over in subsequent commits) so a different method is being used now and has been tested successfully. |
…chieve intended result without issue
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Add NVIDIA Smooth Motion Compatibility
Related Issue
#2190
Summary
When NVIDIA Smooth Motion (frame generation) was enabled alongside Dalamud in Final Fantasy XIV, the visible game image froze while the game continued running behind the stale frame and the Dalamud overlay disappeared. This was caused by two issues:
IDXGISwapChainbehind an NVIDIANvPresentCOM wrapper, so Dalamud's hooks were attaching to / comparing against the wrong presentation object.Presentmultiple times per game frame and off the framework thread, so building a full ImGui frame inside thePresentdetour left the visible frame stale and out of sync with the display path.This PR fixes both by:
NvPresent(and stacked ReShade) COM wrappers so its hooks and swap-chain comparisons target the real game swap chain.Step()(build the ImGui frame) separately fromRender()(composite already-built draw data), plus a combinedRenderFrame()for the unchanged normal path. WhenNvPresenthas been unwrapped, the ImGui frame is built on the framework-update thread viaStep()(where running pluginDrawcallbacks and driving the D3D11 immediate context is safe), and thePresentdetour only callsRender()to re-composite the latest prepared draw data. Smooth Motion's extra / out-of-cadence Present calls therefore simply re-present the most recent prepared frame instead of leaving a stale image.Present(and thereforeRender()) can now run on the NVIDIA pacer thread rather than the game's main thread, Dalamud enables D3D11 multithread protection on the immediate context whenNvPresentis unwrapped, so concurrent immediate-context access between the game/framework thread and the presenting thread is serialized by the driver.imGuiResizeInProgressflag ensure a swap-chain resize (ResizeBuffers, including the ReShade addon resize path) can never run concurrently with present-time rendering or framework-thread frame preparation.For the normal (non-Smooth-Motion) path, behavior is effectively unchanged: the
Presentdetour still builds and renders the frame together via the newRenderFrame()(a single, atomicStep()+Render()).Credit
Based on
goaaatswork in https://github.com/goaaats/Dalamud/tree/fix/smooth_motionKnown Issues
NVIDIA Driver 610.47
Changes Made
Added
Dalamud/Interface/Internal/Unwrapper/ComHookUnwrapper.cs- New abstract base class that generalizes the old ReShade-specific unwrapping into a reusable COM-peeling strategy. It scans the early pointer-sized fields after a wrapper object's vtable, validates candidate objects with defensive readable/executable userspace memory checks (IsValidReadableMemoryAddress/IsValidExecutableMemoryAddress), andQueryInterface-validates before swapping theComPtrto the underlying object. Subclasses implementIsRelevantComObjectto decide whether a given wrapper applies. This is the old ReShade unwrap logic refactored so multiple injectors (ReShade, NvPresent) share one safe implementation. The peeling loop is hardened against infinite loops with aHashSet<nint>of visited objects (breaking on a revisit) and skips swapping when a candidateQueryInterfaceresolves back to the object currently being peeled, so it only reports success when it actually advances to a distinct underlying object. The memory-validation helpers reject negative sizes and compute the checked region from the realRegionSize(clamped to the remaining size) instead of a rounded-up size, fixing the per-region advance and avoiding over-reading.Dalamud/Interface/Internal/Unwrapper/NvPresentUnwrapper.cs- New (sealed)ComHookUnwrappersubclass that recognizes NVIDIA wrappers by checking whether the first few vtable function pointers belong to a loaded module whose name containsNvPresent. This is what lets Dalamud reach the real game swap chain when Smooth Motion has wrapped it.Dalamud/Interface/Internal/Unwrapper/ReShadeUnwrapper.cs- NewComHookUnwrappersubclass holding the ReShade-specific detection (checking forReShadeRegisterAddon/ReShadeUnregisterAddon/ReShadeRegisterEvent/ReShadeUnregisterEventexports). Behaviorally identical to the old static unwrapper, so existing ReShade users see no regression; it now shares the commonComHookUnwrapperpeeling implementation.Removed
Dalamud/Interface/Internal/ReShadeHandling/ReShadeUnwrapper.cs- Deleted the old staticReShadeUnwrapperclass. Its unwrap logic was refactored into the sharedComHookUnwrapperbase class (with the ReShade-specific detection moving to the newUnwrapper/ReShadeUnwrapper.cs), leaving the original file with no remaining callers. Removing it avoids leaving dead/duplicated COM-peeling code behind.Modified
Dalamud/Interface/ImGuiBackend/IImGuiBackend.cs- Extended the backend contract (and clarified the XML doc comments to match the new build/draw split):void Step()- "Builds an ImGui frame without drawing it."void RenderFrame()- "Builds and draws an ImGui frame atomically." (used by the normal, non-Smooth-Motion path).void Render()now means "Draws the most recently built ImGui frame." rather than "build and render".Dalamud/Interface/ImGuiBackend/Dx11Win32Backend.cs- Implemented the Step/Render split and immediate-context protection:Lock frameLock(renamed from the earlierstepLock) taken byStep(),Render(),RenderFrame(),OnPreResize(), andOnPostResize()so the frame being built is never observed mid-rebuild by a Present call arriving on the pacer thread, a resize can never run concurrently with frame construction or present-time rendering, and use of the non-thread-safe global ImGui state is serialized.Step()/StepInternal()performs all frame construction (OnNewFrame,NewRenderFrame/NewInputFrameevents,ImGui.NewFrame(),ImGuizmo.BeginFrame(),BuildUi,ImGui.Render()) and stores a snapshot of the resulting draw data in anImDrawData drawDatafield. A newbool hasDrawDataflag guards this snapshot: whenImGui.GetDrawData()returns null/empty the field is left default andhasDrawDatais cleared, so a missing draw-data handle is handled gracefully instead of dereferencing a null pointer.Step()callsImGui.UpdatePlatformWindows()afterStepInternal()(the call was moved out ofStepInternal()).Render()/RenderInternal()only renders the previously stored draw data: it early-returns whenhasDrawDatais false, draws the main viewport via the new privateRenderMainViewportInternal()(which pinsdrawDatavia afixedpointer and is itself guarded byhasDrawData), then callsImGui.RenderPlatformWindowsDefault(). It no longer builds a new ImGui frame. This is the core change that makes Smooth Motion's out-of-cadence Present calls safe (they just re-composite the latest prepared frame).RenderFrame()runsStepInternal()+RenderMainViewportInternal()+ImGui.UpdatePlatformWindows()+ImGui.RenderPlatformWindowsDefault()atomically underframeLockfor the unchanged normal path.OnPreResize()andOnPostResize()now takeframeLockand clear the cacheddrawData/hasDrawDatabefore delegating to the renderer, so a resize discards any stale prepared frame rather than re-presenting draw data sized for the old swap chain.SwapChainHelper.IsNvPresentUnwrappedis set, enables D3D11 multithread protection on the immediate context via a new privateEnableD3D11MultithreadProtectionhelper. SinceID3D11Multithreadis not exposed by TerraFX, it is obtained byQueryInterfaceon the immediate context and itsSetMultithreadProtected/GetMultithreadProtectedentries are invoked through the vtable. Logs whether protection was enabled.Dalamud/Interface/Internal/InterfaceManager.cs-Lock imGuiFrameLock, and theboolflagshooksInitialized,hasPreparedImGuiFrame, andimGuiResizeInProgress.FrameworkOnUpdatenow keeps its framework-update subscription instead of unsubscribing afterSetupHooks(). ThehooksInitializedflag gates the one-time hook setup (still delayed untilGetNetworkModuleProxy()is non-null). After hooks are set up, it returns early unless the backend exists, the atlas is built, andSwapChainHelper.IsNvPresentUnwrappedis true. In the NvPresent case it then, underimGuiFrameLock(and skipping ifimGuiResizeInProgress), invalidates any previously prepared frame via the newInvalidatePreparedImGuiFrame()helper, callsPrepareImGuiFrame(backend)+backend.Step()to build the next frame on the framework thread, and setshasPreparedImGuiFrame.InvalidatePreparedImGuiFrame()helper that, when a prepared frame is pending, clearshasPreparedImGuiFrameand callsPostImGuiRender()to release everything the frame referenced. This consolidates the previous inline "composite the prepared frame before replacing it" bookkeeping into one place so the framework-thread path and all resize paths invalidate the cached frame consistently.RenderDalamudDraw(the DXGIPresentdetour) is wrapped in atry/finallythat always resetsIsMainThreadInPresent. UnderimGuiFrameLockit skips whenimGuiResizeInProgress. WhenNvPresentis unwrapped it callsactiveBackend.Render()only (returning early if no frame has been prepared yet), re-compositing the cached draw data. Otherwise (normal path) it callsPrepareImGuiFrame(activeBackend)+activeBackend.RenderFrame()+PostImGuiRender().PrepareImGuiFrame(IImGuiBackend activeBackend)method holding the pre-render bookkeeping that used to be inline in the detour: draining therunBeforeImGuiRenderqueue and computing theImGuiConfigFlags.ViewportsEnableflag.DisposeServicenow unsubscribes fromframework.Updateto avoid a dangling handler after disposal.SwapChainHelper.UnwrapNvPresent()before installing hooks (loggingUnwrapped NvPresent/No NvPresent wrap detected), and again after a successful ReShade unwrap (loggingUnwrapped NvPresent after ReShade) to handle stacked wrapper orders.Dalamud/Interface/Internal/InterfaceManager.AsHook.cs- In theResizeBuffersdetour, takesimGuiFrameLock, setsimGuiResizeInProgress = true, and callsInvalidatePreparedImGuiFrame()to drop any cached frame before the swap chain changes, then wraps theResizeBuffersevent,OnPreResize, the originalResizeBufferscall, andOnPostResizein atry/finallythat clearsimGuiResizeInProgress. This prevents present-time rendering or framework-thread frame preparation from running during a resize.Dalamud/Interface/Internal/InterfaceManager.AsReShadeAddon.cs- The ReShade addon resize callbacks now participate in the same coordination: the pre-resize handler takesimGuiFrameLock, setsimGuiResizeInProgress = true, and callsInvalidatePreparedImGuiFrame()beforeOnPreResize; the post-resize handler callsInvalidatePreparedImGuiFrame()beforeOnPostResizeand clearsimGuiResizeInProgressunder the lock (including the early-out path whenGetDescfails).Dalamud/Interface/ImGuiBackend/Helpers/ReShadePeeler.cs- Applied the same robustness fixes as the newComHookUnwrapperto the existing helper: the peeling loop now tracks visited objects in aHashSet<nint>(breaking on a revisit) and skips a candidate whoseQueryInterfaceresolves back to the object currently being peeled, returning success only when it actually advances;IsValidReadableMemoryAddress/IsValidExecutableMemoryAddressreject negative sizes and compute the checked region from the realRegionSize(clamped to the remaining size) instead of a rounded-up size.Dalamud/Interface/Internal/SwapChainHelper.cs- Switched to the newUnwrappernamespace (and added ausingfor theReShadeHandlingnamespace), changedUnwrapReShade()to instantiatenew ReShadeUnwrapper(), added a newIsNvPresentUnwrappedproperty, and added a newUnwrapNvPresent()method that wraps the currentGameDeviceSwapChainin aComPtr<IDXGISwapChain>, runsNvPresentUnwrapper.Unwrap(), updates the cachedfoundGameDeviceSwapChainto the unwrapped object, and setsIsNvPresentUnwrappedso later hook/compare logic and the backend's multithread-protection step target / react to the real swap chain.