Skip to content

Commit 914c251

Browse files
committed
fix(ngx): gate native operations against concurrent shutdown
1 parent 3df8499 commit 914c251

8 files changed

Lines changed: 335 additions & 61 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')) {
136+
foreach ($script in @('test_shutdown_routing.py','test_provider_publication_routes.py','test_provider_dll_boundary.py','test_combo_release.py','test_native_operation_routes.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/NVNGX_Parameter.h‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ bool TryDestroyNGXParameters(NVSDK_NGX_Parameter* InParameters, PFN_DestroyNGXPa
275275
LOG_INFO("Calling NVFree");
276276
result = NVFree(InParameters);
277277
LOG_INFO("Calling NVFree result: {0:X}", (UINT) result);
278-
return true;
278+
return result == NVSDK_NGX_Result_Success;
279279
}
280280
else
281281
return false;

‎OptiScaler/proxies/NVNGX_Proxy.h‎

Lines changed: 86 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -847,51 +847,61 @@ class NVNGXProxy
847847

848848
static PFN_D3D12_DestroyParameters D3D12_DestroyParameters()
849849
{
850-
if (!IsDx12Inited())
851-
return nullptr;
852-
853-
return _module.D3D12_DestroyParameters;
850+
// Availability is stable; admission is checked when the pointer is called.
851+
// This also protects pointers cached before a concurrent shutdown.
852+
return _module.D3D12_DestroyParameters ? +[](NVSDK_NGX_Parameter* InParameters) -> NVSDK_NGX_Result
853+
{
854+
return _dx12Devices.RunOperation(NVSDK_NGX_Result_FAIL_NotInitialized,
855+
[&] { return _module.D3D12_DestroyParameters(InParameters); });
856+
} : nullptr;
854857
}
855858

856859
static PFN_D3D12_CreateFeature D3D12_CreateFeature()
857860
{
858-
if (!IsDx12Inited())
859-
return nullptr;
860-
861-
return _module.D3D12_CreateFeature;
861+
// Availability is stable; admission is checked when the pointer is called.
862+
// This also protects pointers cached before a concurrent shutdown.
863+
return _module.D3D12_CreateFeature ? +[](ID3D12GraphicsCommandList* InCmdList, NVSDK_NGX_Feature InFeatureID,
864+
NVSDK_NGX_Parameter* InParameters, NVSDK_NGX_Handle** OutHandle) -> NVSDK_NGX_Result
865+
{
866+
if (!OutHandle)
867+
return NVSDK_NGX_Result_FAIL_InvalidParameter;
868+
*OutHandle = nullptr;
869+
return _dx12Devices.RunOperation(NVSDK_NGX_Result_FAIL_NotInitialized,
870+
[&] { return _module.D3D12_CreateFeature(InCmdList, InFeatureID, InParameters, OutHandle); });
871+
} : nullptr;
862872
}
863873

864874
static PFN_D3D12_EvaluateFeature D3D12_EvaluateFeature()
865875
{
866-
if (!IsDx12Inited())
867-
return nullptr;
868-
869-
return _module.D3D12_EvaluateFeature;
876+
// Availability is stable; admission is checked when the pointer is called.
877+
// This also protects pointers cached before a concurrent shutdown.
878+
return _module.D3D12_EvaluateFeature ? +[](ID3D12GraphicsCommandList* InCmdList,
879+
const NVSDK_NGX_Handle* InFeatureHandle,
880+
const NVSDK_NGX_Parameter* InParameters,
881+
PFN_NVSDK_NGX_ProgressCallback InCallback) -> NVSDK_NGX_Result
882+
{
883+
return _dx12Devices.RunOperation(NVSDK_NGX_Result_FAIL_NotInitialized,
884+
[&] { return _module.D3D12_EvaluateFeature(InCmdList, InFeatureHandle, InParameters, InCallback); });
885+
} : nullptr;
870886
}
871887

872888
static PFN_D3D12_ReleaseFeature D3D12_ReleaseFeature()
873889
{
874-
if (!IsDx12Inited())
875-
return nullptr;
876-
877-
return _module.D3D12_ReleaseFeature;
890+
// Availability is stable; admission is checked when the pointer is called.
891+
// This also protects pointers cached before a concurrent shutdown.
892+
return _module.D3D12_ReleaseFeature ? +[](NVSDK_NGX_Handle* InHandle) -> NVSDK_NGX_Result
893+
{
894+
return _dx12Devices.RunOperation(NVSDK_NGX_Result_FAIL_NotInitialized,
895+
[&] { return _module.D3D12_ReleaseFeature(InHandle); });
896+
} : nullptr;
878897
}
879898

880899
static PFN_D3D12_Shutdown D3D12_Shutdown()
881900
{
882-
if (!IsDx12Inited())
883-
return nullptr;
884-
885-
return _module.D3D12_Shutdown;
901+
return _module.D3D12_Shutdown ? +[]() { return ShutdownDx12(nullptr); } : nullptr;
886902
}
887903

888-
static PFN_D3D12_Shutdown1 D3D12_Shutdown1()
889-
{
890-
if (!IsDx12Inited())
891-
return nullptr;
892-
893-
return _module.D3D12_Shutdown1;
894-
}
904+
static PFN_D3D12_Shutdown1 D3D12_Shutdown1() { return _module.D3D12_Shutdown1 ? &ShutdownDx12 : nullptr; }
895905

896906
// Vulkan
897907
static bool InitVulkan(VkInstance InInstance, VkPhysicalDevice InPD, VkDevice InDevice,
@@ -994,59 +1004,77 @@ class NVNGXProxy
9941004

9951005
static PFN_VULKAN_DestroyParameters VULKAN_DestroyParameters()
9961006
{
997-
if (!IsVulkanInited())
998-
return nullptr;
999-
1000-
return _module.VULKAN_DestroyParameters;
1007+
// Availability is stable; admission is checked when the pointer is called.
1008+
// This also protects pointers cached before a concurrent shutdown.
1009+
return _module.VULKAN_DestroyParameters ? +[](NVSDK_NGX_Parameter* InParameters) -> NVSDK_NGX_Result
1010+
{
1011+
return _vulkanDevices.RunOperation(NVSDK_NGX_Result_FAIL_NotInitialized,
1012+
[&] { return _module.VULKAN_DestroyParameters(InParameters); });
1013+
} : nullptr;
10011014
}
10021015

10031016
static PFN_VULKAN_CreateFeature VULKAN_CreateFeature()
10041017
{
1005-
if (!IsVulkanInited())
1006-
return nullptr;
1007-
1008-
return _module.VULKAN_CreateFeature;
1018+
// Availability is stable; admission is checked when the pointer is called.
1019+
// This also protects pointers cached before a concurrent shutdown.
1020+
return _module.VULKAN_CreateFeature ? +[](VkCommandBuffer InCmdBuffer, NVSDK_NGX_Feature InFeatureID,
1021+
NVSDK_NGX_Parameter* InParameters, NVSDK_NGX_Handle** OutHandle) -> NVSDK_NGX_Result
1022+
{
1023+
if (!OutHandle)
1024+
return NVSDK_NGX_Result_FAIL_InvalidParameter;
1025+
*OutHandle = nullptr;
1026+
return _vulkanDevices.RunOperation(NVSDK_NGX_Result_FAIL_NotInitialized,
1027+
[&] { return _module.VULKAN_CreateFeature(InCmdBuffer, InFeatureID, InParameters, OutHandle); });
1028+
} : nullptr;
10091029
}
10101030

10111031
static PFN_VULKAN_CreateFeature1 VULKAN_CreateFeature1()
10121032
{
1013-
if (!IsVulkanInited())
1014-
return nullptr;
1015-
1016-
return _module.VULKAN_CreateFeature1;
1033+
// Availability is stable; admission is checked when the pointer is called.
1034+
// This also protects pointers cached before a concurrent shutdown.
1035+
return _module.VULKAN_CreateFeature1 ? +[](VkDevice InDevice, VkCommandBuffer InCmdList,
1036+
NVSDK_NGX_Feature InFeatureID, NVSDK_NGX_Parameter* InParameters,
1037+
NVSDK_NGX_Handle** OutHandle) -> NVSDK_NGX_Result
1038+
{
1039+
if (!OutHandle)
1040+
return NVSDK_NGX_Result_FAIL_InvalidParameter;
1041+
*OutHandle = nullptr;
1042+
return _vulkanDevices.RunOperation(NVSDK_NGX_Result_FAIL_NotInitialized,
1043+
[&] { return _module.VULKAN_CreateFeature1(InDevice, InCmdList, InFeatureID, InParameters, OutHandle); }, InDevice);
1044+
} : nullptr;
10171045
}
10181046

10191047
static PFN_VULKAN_EvaluateFeature VULKAN_EvaluateFeature()
10201048
{
1021-
if (!IsVulkanInited())
1022-
return nullptr;
1023-
1024-
return _module.VULKAN_EvaluateFeature;
1049+
// Availability is stable; admission is checked when the pointer is called.
1050+
// This also protects pointers cached before a concurrent shutdown.
1051+
return _module.VULKAN_EvaluateFeature ? +[](VkCommandBuffer InCmdList,
1052+
const NVSDK_NGX_Handle* InFeatureHandle,
1053+
const NVSDK_NGX_Parameter* InParameters,
1054+
PFN_NVSDK_NGX_ProgressCallback InCallback) -> NVSDK_NGX_Result
1055+
{
1056+
return _vulkanDevices.RunOperation(NVSDK_NGX_Result_FAIL_NotInitialized,
1057+
[&] { return _module.VULKAN_EvaluateFeature(InCmdList, InFeatureHandle, InParameters, InCallback); });
1058+
} : nullptr;
10251059
}
10261060

10271061
static PFN_VULKAN_ReleaseFeature VULKAN_ReleaseFeature()
10281062
{
1029-
if (!IsVulkanInited())
1030-
return nullptr;
1031-
1032-
return _module.VULKAN_ReleaseFeature;
1063+
// Availability is stable; admission is checked when the pointer is called.
1064+
// This also protects pointers cached before a concurrent shutdown.
1065+
return _module.VULKAN_ReleaseFeature ? +[](NVSDK_NGX_Handle* InHandle) -> NVSDK_NGX_Result
1066+
{
1067+
return _vulkanDevices.RunOperation(NVSDK_NGX_Result_FAIL_NotInitialized,
1068+
[&] { return _module.VULKAN_ReleaseFeature(InHandle); });
1069+
} : nullptr;
10331070
}
10341071

10351072
static PFN_VULKAN_Shutdown VULKAN_Shutdown()
10361073
{
1037-
if (!IsVulkanInited())
1038-
return nullptr;
1039-
1040-
return _module.VULKAN_Shutdown;
1074+
return _module.VULKAN_Shutdown ? +[]() { return ShutdownVulkan(nullptr); } : nullptr;
10411075
}
10421076

1043-
static PFN_VULKAN_Shutdown1 VULKAN_Shutdown1()
1044-
{
1045-
if (!IsVulkanInited())
1046-
return nullptr;
1047-
1048-
return _module.VULKAN_Shutdown1;
1049-
}
1077+
static PFN_VULKAN_Shutdown1 VULKAN_Shutdown1() { return _module.VULKAN_Shutdown1 ? &ShutdownVulkan : nullptr; }
10501078

10511079
static PFN_UpdateFeature UpdateFeature()
10521080
{

‎OptiScaler/proxies/NativeDeviceLifecycle.h‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
77

88
// Tracks native Init/Shutdown ownership per API. Callbacks run without the
99
// metadata lock. Overlapping/reentrant transitions fail rather than waiting on
10-
// a native DLL or loader callback. This does not yet lease Evaluate/Create calls.
10+
// a native DLL or loader callback. Operation admission covers the API as a whole
11+
// because legacy native handles do not all expose a reliable device association.
1112
class NativeDeviceLifecycle
1213
{
1314
std::mutex _mutex;
1415
std::unordered_map<const void*, bool> _devices;
1516
bool _transition = false;
17+
size_t _operations = 0;
1618
const void* _target = nullptr;
1719
std::atomic_size_t _usable { 0 };
1820

@@ -57,6 +59,28 @@ class NativeDeviceLifecycle
5759
return entry != _devices.end() && entry->second && (!_transition || (_target && device != _target));
5860
}
5961

62+
template <typename Result, typename Callback>
63+
Result RunOperation(Result unavailable, Callback&& callback, const void* device = nullptr)
64+
{
65+
{
66+
std::scoped_lock lock(_mutex);
67+
if (_transition || _devices.empty() || (device && !_devices.contains(device)))
68+
return unavailable;
69+
++_operations;
70+
}
71+
// Native code runs without the metadata mutex, including callbacks.
72+
struct Completion
73+
{
74+
NativeDeviceLifecycle& owner;
75+
~Completion()
76+
{
77+
std::scoped_lock lock(owner._mutex);
78+
--owner._operations;
79+
}
80+
} completion { *this };
81+
return std::forward<Callback>(callback)();
82+
}
83+
6084
template <typename Result, typename Callback>
6185
Result Initialize(const void* device, Result success, Result invalid, Result busy, Callback&& callback)
6286
{
@@ -68,6 +92,8 @@ class NativeDeviceLifecycle
6892
return busy;
6993
if (_devices.contains(device))
7094
return success;
95+
if (_operations)
96+
return busy;
7197
// Allocate bookkeeping before the native callback succeeds.
7298
_devices.emplace(device, false);
7399
_transition = true;
@@ -96,6 +122,8 @@ class NativeDeviceLifecycle
96122
return busy;
97123
if (device ? !_devices.contains(device) : _devices.empty())
98124
return success; // Nothing owned; do not call a native shutdown.
125+
if (_operations)
126+
return busy; // No waiting or deferred native teardown.
99127
_transition = true;
100128
_target = device; // nullptr means all devices in this API.
101129
UpdateUsable();

‎docs/CODEX_HANDOFF.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,5 @@ The 2026-09-17 continuation fixes private provider construction/publication and
3636
The same continuation also initializes DLL-provider depth pointers, guards missing per-operation exports (69 extracted checks), and fixes Combo partial-release ownership/retry (44 checks). See the same publication audit for limitations; this does not establish native API/device shutdown safety or game compatibility.
3737

3838
Latest validated code checkpoint: e737b087 (Windows build 35129887577 and format 35129887470 succeeded; 171 new publication/export/Combo checks passed). The publication audit records remaining native shutdown/device ownership findings, including the separate DLSSFeatureDx12 nested native-shutdown route; continue there rather than repeating these completed fixes.
39+
40+
The next checkpoint `3df84993` fixes native D3D12/Vulkan device-specific Init/Shutdown and removes the nested DLSSFeatureDx12 native shutdown. Windows build `35188944901` and format `35188944895` succeeded; 37 device and 43 extracted shutdown checks passed. See [native lifecycle audit](NATIVE_DEVICE_LIFECYCLE_20260917.md), including the operation-admission follow-up, test counts and exact remaining boundaries. Do not restart this ownership work or rerun unrelated installer suites. The latest user explicitly permits aurora synchronization after code/review/build completion without waiting for game tests; remaining lifecycle boundaries must still be resolved or explicitly scoped before that gate is met.

‎docs/NATIVE_DEVICE_LIFECYCLE_20260917.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,19 @@ This is an Init/Shutdown ownership checkpoint, not full concurrent operation adm
2424
Replacement FG providers have separate device/handle ownership and shutdown semantics, not covered by this native registry. Outer adapters still ignore their shutdown result. Global module loading, mutable shared depth/HUD resources and general State/Config ownership are not solved here. Aurora still has a single current-device cache; native multidevice bookkeeping does not make every renderer feature multidevice-safe. The process-exit skip intentionally leaves native bookkeeping until DLL teardown.
2525

2626
The user has deferred real-game validation and authorized synchronization to aurora after code work/review/build completion. Remaining lifecycle work means that gate has not yet been reached. No main-branch merge or Release is performed by this checkpoint.
27+
28+
## Init/Shutdown checkpoint validation
29+
30+
Commit `3df849934112a115ef09fa280ffe810337b4042c` was pushed to Compatibility-fixes. [Windows DLL build, CPU checks and package](https://github.com/abc354402600/OptiScaler-Aurora/actions/runs/35188944901) and [incremental format](https://github.com/abc354402600/OptiScaler-Aurora/actions/runs/35188944895) succeeded. Job `105097010598` confirms 37 device checks and 43 shutdown checks passed. Artifact `OptiScaler_Aurora_v1.0_20260917_compat_3df84993.7z` was uploaded (ID `10484080454`, 234774490 bytes).
31+
32+
## Native operation admission follow-up
33+
34+
Nine D3D12/Vulkan Create/Evaluate/Release/DestroyParameters getters now return same-signature guarded entry points rather than raw native pointers. Export availability is stable after module initialization; readiness is checked at invocation, including when a caller cached a pointer before shutdown. Create outputs are cleared on rejected admission, and Vulkan CreateFeature1 additionally verifies its explicitly supplied device. Shutdown getters also route through the ownership helper so callers cannot bypass it.
35+
36+
An operation briefly increments an API-local active count and releases the metadata mutex before foreign code. Init of a new device and owned-device/global Shutdown reject while that API has active guarded calls. During a transition new calls are rejected. Exceptions unwind the active count. There is no wait across a native callback and no deferred teardown. Calls on another API remain independent. This is conservatively API-wide because legacy native handle and command-buffer routes do not expose complete device/generation ownership; it does not pretend those associations have been solved.
37+
38+
The changed Busy return exposed an existing bug in TryDestroyNGXParameters: it returned true even if native DestroyParameters failed. It now reports success only for native Success, preserving the existing internal-table deletion and unknown-table policy.
39+
40+
Focused tests now contain **55 device/admission checks** (18 added) and **236 new extracted-operation/parameter-destruction checks** covering all nine actual getters, cached pointers before/during/after shutdown, callback reentry, output clearing, absent exports, explicit unknown Vulkan device, retries, both shutdown getters and native destroy failure. The existing 43 shutdown routing checks were rerun because the shared lifecycle header changed; they passed. Local checks use counted native stand-ins. Cumulative MSVC validation will be recorded after this follow-up is pushed.
41+
42+
Still open: calls bypassing these getters, parameter acquisition/query routes, old native handles after a device generation closes/reopens, D3D11, replacement-provider lifecycle, and global module publication. Existing higher-level owners may discard resources after a failed release; no general ownership redesign is claimed. These checks do not establish in-game behavior or complete the main-branch merge gate.

0 commit comments

Comments
 (0)