Skip to content

Commit 1c57db0

Browse files
committed
fix(compat): drain device-owned FG features before parent shutdown
1 parent 47f1b16 commit 1c57db0

16 files changed

Lines changed: 389 additions & 19 deletions

‎.github/workflows/just_build_no_signature.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ jobs:
133133
& $env:ComSpec /d /c $command
134134
if ($LASTEXITCODE) { throw "Compatibility guard test failed: $name" }
135135
}
136-
foreach ($script in @('test_shutdown_routing.py','test_provider_publication_routes.py','test_provider_dll_boundary.py','test_combo_release.py','test_combo_shutdown.py','test_native_operation_routes.py','test_native_module_publication.py','test_native_init_scopes.py','test_ngx_init_paths.py','test_ngx_init_metadata.py','test_provider_release_admission.py','test_provider_call_admission.py','test_provider_init_footprint.py','test_provider_handle_api.py','test_upstream_resource_guards.py','test_provider_settings.py')) {
136+
foreach ($script in @('test_shutdown_routing.py','test_provider_publication_routes.py','test_provider_dll_boundary.py','test_combo_release.py','test_combo_shutdown.py','test_native_operation_routes.py','test_native_module_publication.py','test_native_init_scopes.py','test_ngx_init_paths.py','test_ngx_init_metadata.py','test_provider_release_admission.py','test_provider_call_admission.py','test_provider_init_footprint.py','test_provider_handle_api.py','test_upstream_resource_guards.py','test_provider_settings.py','test_provider_shutdown_drain.py','test_ffx_release.py')) {
137137
$command = 'call "' + $vcvars + '" && python tests\' + $script + ' --compiler cl'
138138
& $env:ComSpec /d /c $command
139139
if ($LASTEXITCODE) { throw "Compatibility routing test failed: $script" }

‎OptiScaler/framegen/ProviderHandleRegistry.h‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include <optional>
66
#include <unordered_map>
77
#include <utility>
8+
#include <vector>
89

910
// Lookup retains the token. Admission is checked under a short per-handle lock;
1011
// provider callbacks never run under that lock and busy releases never wait.
@@ -32,6 +33,21 @@ template <typename Value> class ProviderHandleRegistry
3233
return key;
3334
}
3435

36+
// Only immutable identity fields may be inspected by the predicate. The
37+
// caller must exclude new publication/operations for a lifecycle snapshot.
38+
template <typename Predicate> std::vector<const void*> LiveKeys(Predicate&& predicate)
39+
{
40+
std::vector<const void*> keys;
41+
std::scoped_lock lock(_mutex);
42+
for (const auto& [key, entry] : _entries)
43+
{
44+
std::scoped_lock entryLock(entry->mutex);
45+
if (!entry->retired && predicate(static_cast<const Value&>(entry->value)))
46+
keys.push_back(key);
47+
}
48+
return keys;
49+
}
50+
3551
// Identity remains available after retirement. Retained tokens prevent their
3652
// addresses being recycled into unrelated handles while the DLL is loaded.
3753
template <typename Identity>

‎OptiScaler/framegen/nvngx/Nvngx_FFX.cpp‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,19 @@ NVSDK_NGX_Result Nvngx_FFX::D3D12_ReleaseFeature(NVSDK_NGX_Handle* InHandle)
102102
if (!InOurHandle)
103103
return NVSDK_NGX_Result_FAIL_InvalidParameter;
104104

105+
InOurHandle->releaseStarted = true;
106+
if (InOurHandle->fgContext && InOurHandle->fgContext->ctx)
107+
{
108+
const auto result = FfxApiProxy::D3D12_DestroyContext(&InOurHandle->fgContext->ctx, nullptr);
109+
if (result != FFX_API_RETURN_OK)
110+
return NVSDK_NGX_Result_Fail;
111+
InOurHandle->fgContext->ctx = nullptr;
112+
}
113+
// Destroy the context before dropping our device reference. On failure the
114+
// outer registry keeps this wrapper/device alive for an explicit retry.
115+
InOurHandle->fgContext.reset();
105116
InOurHandle->device->Release();
106-
delete InOurHandle; // destroys FFX context automatically
117+
delete InOurHandle;
107118

108119
return NVSDK_NGX_Result_Success;
109120
}
@@ -241,6 +252,9 @@ NVSDK_NGX_Result Nvngx_FFX::D3D12_EvaluateFeature(ID3D12GraphicsCommandList* InC
241252
if (!InParameters || !InOurHandle)
242253
return NVSDK_NGX_Result_FAIL_InvalidParameter;
243254

255+
if (InOurHandle->releaseStarted)
256+
return NVSDK_NGX_Result_FAIL_FeatureNotFound;
257+
244258
if (!Init())
245259
return NVSDK_NGX_Result_Fail;
246260

‎OptiScaler/framegen/nvngx/Nvngx_FFX.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ struct Nvngx_FFX_Handle
2929
uint32_t swapchainWidth = 0;
3030
uint32_t swapchainHeight = 0;
3131
uint64_t lastFrameId = 0;
32+
bool releaseStarted = false;
3233

3334
// for HDR
3435
bool hdrRangeSet = false;

‎OptiScaler/framegen/nvngx/Nvngx_FG.cpp‎

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -283,14 +283,53 @@ NVSDK_NGX_Result Nvngx_FG::D3D12_Init_Ext(unsigned long long InApplicationId, co
283283
return provider->D3D12_Init_Ext(InApplicationId, InApplicationDataPath, InDevice, InSDKVersion, InFeatureInfo);
284284
}
285285

286+
NVSDK_NGX_Result Nvngx_FG::DrainHandles(HandleApi api, const void* device)
287+
{
288+
// The caller holds exclusive transition admission. Snapshot before any
289+
// teardown, and never infer a legacy Vulkan command buffer's device from
290+
// mutable process-wide state.
291+
const auto keys =
292+
_handles.LiveKeys([&](const Nvngx_FG_Handle& handle)
293+
{ return handle.api == api && (!device || !handle.device || handle.device == device); });
294+
if (device)
295+
for (const auto* key : keys)
296+
if (_handles.GetIdentity(key, [](const Nvngx_FG_Handle& handle) { return handle.device; }) == nullptr)
297+
return NVSDK_NGX_Result_FAIL_NotInitialized;
298+
299+
if (keys.empty())
300+
return NVSDK_NGX_Result_Success;
301+
// DLL_PROCESS_DETACH may hold the loader lock. Do not start GPU/provider
302+
// destruction there; retain ownership rather than claiming a clean drain.
303+
if (State::Instance().isShuttingDown)
304+
return NVSDK_NGX_Result_FAIL_NotInitialized;
305+
auto* provider = _provider.Peek();
306+
if (!provider)
307+
return NVSDK_NGX_Result_FAIL_NotInitialized;
308+
309+
for (const auto* key : keys)
310+
{
311+
const auto result = _handles.Release(
312+
key, NVSDK_NGX_Result_FAIL_FeatureNotFound, NVSDK_NGX_Result_FAIL_NotInitialized,
313+
[&](const Nvngx_FG_Handle& handle)
314+
{
315+
return api == HandleApi::D3D12 ? provider->D3D12_ReleaseFeature(handle.nativeHandle)
316+
: provider->VULKAN_ReleaseFeature(handle.nativeHandle);
317+
},
318+
[](NVSDK_NGX_Result result) { return result == NVSDK_NGX_Result_Success; });
319+
if (result != NVSDK_NGX_Result_Success)
320+
return result;
321+
}
322+
return NVSDK_NGX_Result_Success;
323+
}
324+
286325
NVSDK_NGX_Result Nvngx_FG::D3D12_Shutdown()
287326
{
288327
return WithDx12Shutdown([&](auto closeProvider) { return closeProvider(nullptr); });
289328
}
290329

291330
NVSDK_NGX_Result Nvngx_FG::D3D12_Shutdown1(ID3D12Device* InDevice)
292331
{
293-
return WithDx12Shutdown([&](auto closeProvider) { return closeProvider(InDevice); });
332+
return WithDx12Shutdown([&](auto closeProvider) { return closeProvider(InDevice); }, InDevice);
294333
}
295334

296335
NVSDK_NGX_Result Nvngx_FG::D3D12_GetScratchBufferSize(NVSDK_NGX_Feature InFeatureId,
@@ -319,8 +358,13 @@ NVSDK_NGX_Result Nvngx_FG::D3D12_CreateFeature(ID3D12GraphicsCommandList* InCmdL
319358
if (!lease)
320359
return NVSDK_NGX_Result_FAIL_NotInitialized;
321360

361+
Microsoft::WRL::ComPtr<ID3D12Device> device;
362+
if (!InCmdList || FAILED(InCmdList->GetDevice(IID_PPV_ARGS(&device))) || !device)
363+
return NVSDK_NGX_Result_FAIL_InvalidParameter;
364+
322365
return CreateProviderHandle(
323-
_handles, OutHandle, Nvngx_FG_Handle { lastIdCreated++ + NVNGX_PROVIDER_ID_OFFSET, nullptr, HandleApi::D3D12 },
366+
_handles, OutHandle,
367+
Nvngx_FG_Handle { lastIdCreated++ + NVNGX_PROVIDER_ID_OFFSET, nullptr, HandleApi::D3D12, device.Get() },
324368
NVSDK_NGX_Result_Success, NVSDK_NGX_Result_FAIL_InvalidParameter, NVSDK_NGX_Result_Fail,
325369
[&](Nvngx_FG_Handle& handle)
326370
{
@@ -548,7 +592,7 @@ NVSDK_NGX_Result Nvngx_FG::VULKAN_Shutdown()
548592

549593
NVSDK_NGX_Result Nvngx_FG::VULKAN_Shutdown1(VkDevice InDevice)
550594
{
551-
return WithVulkanShutdown([&](auto closeProvider) { return closeProvider(InDevice); });
595+
return WithVulkanShutdown([&](auto closeProvider) { return closeProvider(InDevice); }, InDevice);
552596
}
553597

554598
NVSDK_NGX_Result Nvngx_FG::VULKAN_GetScratchBufferSize(NVSDK_NGX_Feature InFeatureId,
@@ -601,8 +645,12 @@ NVSDK_NGX_Result Nvngx_FG::VULKAN_CreateFeature1(VkDevice InDevice, VkCommandBuf
601645
if (!lease)
602646
return NVSDK_NGX_Result_FAIL_NotInitialized;
603647

648+
if (!InDevice)
649+
return NVSDK_NGX_Result_FAIL_InvalidParameter;
650+
604651
return CreateProviderHandle(
605-
_handles, OutHandle, Nvngx_FG_Handle { lastIdCreated++ + NVNGX_PROVIDER_ID_OFFSET, nullptr, HandleApi::Vulkan },
652+
_handles, OutHandle,
653+
Nvngx_FG_Handle { lastIdCreated++ + NVNGX_PROVIDER_ID_OFFSET, nullptr, HandleApi::Vulkan, InDevice },
606654
NVSDK_NGX_Result_Success, NVSDK_NGX_Result_FAIL_InvalidParameter, NVSDK_NGX_Result_Fail,
607655
[&](Nvngx_FG_Handle& handle)
608656
{

‎OptiScaler/framegen/nvngx/Nvngx_FG.h‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class Nvngx_FG
2727
unsigned int id;
2828
NVSDK_NGX_Handle* nativeHandle = nullptr;
2929
HandleApi api = HandleApi::D3D12;
30+
const void* device = nullptr; // Legacy Vulkan Create has no provable device identity.
3031
};
3132

3233
static inline std::atomic_uint32_t lastIdCreated = 0;
@@ -42,15 +43,20 @@ class Nvngx_FG
4243
static std::unique_ptr<IFGNvngx> createProvider();
4344
static ProviderLookup<IFGNvngx> lookupProvider();
4445
static IFGNvngx* getProvider();
46+
static NVSDK_NGX_Result DrainHandles(HandleApi api, const void* device);
4547

4648
public:
4749
// The exported API holds this admission through native shutdown, provider
4850
// shutdown and local cleanup. A rejected transition changes none of them.
49-
template <typename Callback> static NVSDK_NGX_Result WithDx12Shutdown(Callback&& callback)
51+
template <typename Callback>
52+
static NVSDK_NGX_Result WithDx12Shutdown(Callback&& callback, ID3D12Device* device = nullptr)
5053
{
5154
auto lease = _calls.TryTransition();
5255
if (!lease)
5356
return NVSDK_NGX_Result_FAIL_NotInitialized;
57+
const auto drained = DrainHandles(HandleApi::D3D12, device);
58+
if (drained != NVSDK_NGX_Result_Success)
59+
return drained;
5460
return std::forward<Callback>(callback)(
5561
[](ID3D12Device* device)
5662
{
@@ -71,11 +77,15 @@ class Nvngx_FG
7177
});
7278
}
7379

74-
template <typename Callback> static NVSDK_NGX_Result WithVulkanShutdown(Callback&& callback)
80+
template <typename Callback>
81+
static NVSDK_NGX_Result WithVulkanShutdown(Callback&& callback, VkDevice device = nullptr)
7582
{
7683
auto lease = _calls.TryTransition();
7784
if (!lease)
7885
return NVSDK_NGX_Result_FAIL_NotInitialized;
86+
const auto drained = DrainHandles(HandleApi::Vulkan, device);
87+
if (drained != NVSDK_NGX_Result_Success)
88+
return drained;
7989
return std::forward<Callback>(callback)(
8090
[](VkDevice device)
8191
{

‎OptiScaler/inputs/NVNGX_DLSS_Dx12.cpp‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,8 @@ static NVSDK_NGX_Result ShutdownD3D12(ID3D12Device* device)
412412
State::Instance().nvngxDx12Inited = false;
413413
shutdown = false;
414414
return NVSDK_NGX_Result_Success;
415-
});
415+
},
416+
device);
416417
}
417418

418419
NVSDK_NGX_API NVSDK_NGX_Result NVSDK_NGX_D3D12_Shutdown(void) { return ShutdownD3D12(nullptr); }

‎OptiScaler/inputs/NVNGX_DLSS_Vk.cpp‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1202,7 +1202,8 @@ static NVSDK_NGX_Result ShutdownVulkan(VkDevice device)
12021202
State::Instance().nvngxVkInited = false;
12031203
shutdown = false;
12041204
return NVSDK_NGX_Result_Success;
1205-
});
1205+
},
1206+
device);
12061207
}
12071208

12081209
NVSDK_NGX_API NVSDK_NGX_Result NVSDK_NGX_VULKAN_Shutdown(void) { return ShutdownVulkan(nullptr); }

‎docs/FEATURE_DRAIN_20260926.md‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Provider feature drain before parent shutdown — 2026-09-26
2+
3+
## Defects and policy
4+
5+
Parent Shutdown previously ignored the registry of outstanding replacement FG handles. DLL providers could destroy their contexts while a public token remained readable; FFX's parent Shutdown is a no-op, so merely retiring that token would leak its feature context and owned D3D12 device reference. Combo can retain one child after a failed Release, requiring retry before either parent is closed.
6+
7+
Normal shutdown now acquires the existing exclusive provider transition admission, selects live features for the requested API/device, calls their actual provider ReleaseFeature, and retires each public token only on success. Only after the drain succeeds does it enter the existing native/provider shutdown callback and local cleanup. No registry lock is held while calling provider/native code. Successful releases remain retired if a later release or parent shutdown fails; retry skips them. Failed releases and exceptions retain ownership and do not proceed to parent close. This is a partial-progress transaction, not a rollback that recreates released features.
8+
9+
Identity tokens remain retained until DLL unload, so successful Shutdown followed by Init cannot resurrect a previously drained token through address reuse. This closes that specific stale-handle path; repeated Init without a preceding successful close remains separate work.
10+
11+
## Device identity and conservative boundaries
12+
13+
- D3D12 Create obtains its device from the actual command list, with a temporary COM owner spanning creation/publication. Null command lists, failed GetDevice and null returned device are rejected with an empty output. Stored identity is immutable and non-owning; it is only compared, never dereferenced for cleanup.
14+
- Vulkan CreateFeature1 records its explicit device and rejects null. Legacy Vulkan CreateFeature has no device argument and remains explicitly unknown; mutable global vkDevice is not used to guess.
15+
- Device-specific shutdown with any matching-API unknown-device live token returns NotInitialized before releasing anything. The caller can release that token explicitly or request global shutdown. Global shutdown can drain unknown-device tokens of its API safely through their provider; it does not drain the other API.
16+
- Once DLL_PROCESS_DETACH has set isShuttingDown, live-feature drain is rejected before entering GPU/provider destruction under a possible loader lock. No successful cleanup is claimed. This does not redesign the DLL's other existing detach behavior.
17+
- ReleaseFeature's existing provider/driver contract still applies: CPU exclusion is not a GPU fence or proof that submitted work has finished. No real GPU tests were performed, per user instruction.
18+
19+
## FFX feature cleanup repair
20+
21+
FFX Release previously dropped its device reference before deleting the wrapper whose destructor destroys the context. It also discarded context destruction failures in the destructor while reporting successful Release. Release now explicitly destroys the context while the owned device reference is alive. Error/exception retains the wrapper/reference for retry; Evaluate rejects a wrapper whose release has started. Successful destruction clears the context before resetting the holder, preventing double destruction, then releases the device and deletes the wrapper. If the SDK clears the context even while returning failure, retry does not call DestroyContext on null.
22+
23+
This covers the normal explicit Release path and the shutdown drain that uses it. It does not assert that every separate context-holder destructor elsewhere can recover from a destruction failure.
24+
25+
## Tests and source scope
26+
27+
- 39 new `test_provider_shutdown_drain.py` checks compile the actual Create/Release/drain/coordinator methods with production registry/admission and fake SDK/COM calls. They cover invalid command lists/devices, balanced temporary COM reference, device/API isolation, active-call exclusion, detach rejection, worker-thread reentry, failures/exceptions, partial-progress retry, parent failure, stale tokens after reinitialization and unknown legacy Vulkan identity. D3D12 HUD work after admission remains a stand-in.
28+
- 22 new `test_ffx_release.py` checks compile the actual FFX holder/destructor, complete Release and Evaluate rejection prefix. Counted SDK/COM stand-ins check destruction order, absent contexts, error/exception retry and context cleared on error. No FFX GPU execution is simulated.
29+
- Actual exported shutdown tests expand from 52 to 56 checks: failed drain reaches neither native close nor local cleanup and receives the exact requested device. Existing fixtures for unrelated routes model an empty successful drain; the new suite tests the actual drain.
30+
- Affected 85 admission, 104 Init-footprint, 36 publication-routing, 42 handle-API, 20 Release-admission, 44 Combo Release and 132 Combo shutdown checks passed locally.
31+
- Three negative controls in temporary copied sources detect removal of device filtering, ignored drain failure, and premature device release. Production files are never mutated by those controls.
32+
- The source extraction helper now tolerates whitespace in declarations because clang-format wraps the longer coordinator signatures. It still fails if the production declaration is absent.
33+
34+
## Remaining work and synchronization gate
35+
36+
Repeated Init while old features remain, native Init/global configuration transaction ordering, partial Create rollback, and concurrent ordinary Evaluate access to shared HUD/depth resources are not solved by this batch. A legacy Vulkan token without explicit device ownership intentionally constrains device-specific close. Preserve these boundaries in the next continuation; do not invent a device association or label CPU tests as game compatibility proof.
37+
38+
The user on 2026-09-26 authorizes direct synchronization to aurora when the code-validation gate is actually met, without a separate confirmation or synchronization notification. Real-game tests remain deferred. No Release is authorized. This checkpoint alone does not close the remaining boundaries above. Full Windows validation will be appended after Actions completes.

0 commit comments

Comments
 (0)