Skip to content

Commit f2cd07b

Browse files
committed
fix: retain private Combo creation rollback ownership
1 parent af6ac02 commit f2cd07b

12 files changed

Lines changed: 545 additions & 42 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','test_provider_shutdown_drain.py','test_ffx_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_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','test_ffx_create.py','test_combo_create.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/nvngx/IFGNvngx.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ class IFGNvngx
3333
const NVSDK_NGX_FeatureCommonInfo* InFeatureInfo) = 0;
3434
virtual NVSDK_NGX_Result D3D12_Shutdown() = 0;
3535
virtual NVSDK_NGX_Result D3D12_Shutdown1(ID3D12Device* InDevice) = 0;
36+
// Internal pre-shutdown hook, under exclusive Nvngx_FG transition admission.
37+
// Drain owned objects that failed creation before a public token existed.
38+
virtual NVSDK_NGX_Result D3D12_DrainPending(ID3D12Device* InDevice) { return NVSDK_NGX_Result_Success; }
3639
virtual NVSDK_NGX_Result D3D12_GetScratchBufferSize(NVSDK_NGX_Feature InFeatureId,
3740
const NVSDK_NGX_Parameter* InParameters,
3841
size_t* OutSizeInBytes) = 0;

‎OptiScaler/framegen/nvngx/Nvngx_Combo.cpp‎

Lines changed: 87 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,47 @@ NVSDK_NGX_Result Nvngx_Combo::D3D12_Shutdown() { return D3D12_Shutdown1(nullptr)
4444

4545
NVSDK_NGX_Result Nvngx_Combo::D3D12_Shutdown1(ID3D12Device* InDevice)
4646
{
47+
const auto pending = D3D12_DrainPending(InDevice);
48+
if (pending != NVSDK_NGX_Result_Success)
49+
return pending;
4750
const auto resultArturs = ShutdownChild(_artursInitAttempts, *artursProvider, InDevice);
4851
const auto resultFfx = ShutdownChild(_ffxInitAttempts, *ffxProvider, InDevice);
4952
return resultArturs != NVSDK_NGX_Result_Success ? resultArturs : resultFfx;
5053
}
5154

55+
void Nvngx_Combo::ForgetPending(const std::shared_ptr<PendingCreate>& pending)
56+
{
57+
std::lock_guard lock(_pendingMutex);
58+
std::erase(_pendingCreates, pending);
59+
}
60+
61+
NVSDK_NGX_Result Nvngx_Combo::D3D12_DrainPending(ID3D12Device* InDevice)
62+
{
63+
std::vector<std::shared_ptr<PendingCreate>> pending;
64+
{
65+
std::lock_guard lock(_pendingMutex);
66+
for (const auto& entry : _pendingCreates)
67+
if (!InDevice || entry->device.Get() == InDevice)
68+
pending.push_back(entry);
69+
}
70+
// An interrupted third-party call or error with non-null output does not
71+
// prove ownership. Do not pass that pointer to Release or close its parent.
72+
for (const auto& entry : pending)
73+
if (entry->uncertain)
74+
{
75+
LOG_ERROR("Combo creation ownership is uncertain; shutdown retained until process restart");
76+
return NVSDK_NGX_Result_FAIL_NotInitialized;
77+
}
78+
for (const auto& entry : pending)
79+
{
80+
const auto result = ReleaseChildren(entry->handle.get());
81+
if (result != NVSDK_NGX_Result_Success)
82+
return result;
83+
ForgetPending(entry);
84+
}
85+
return NVSDK_NGX_Result_Success;
86+
}
87+
5288
NVSDK_NGX_Result Nvngx_Combo::D3D12_GetScratchBufferSize(NVSDK_NGX_Feature InFeatureId,
5389
const NVSDK_NGX_Parameter* InParameters,
5490
size_t* OutSizeInBytes)
@@ -64,42 +100,62 @@ NVSDK_NGX_Result Nvngx_Combo::D3D12_GetScratchBufferSize(NVSDK_NGX_Feature InFea
64100
NVSDK_NGX_Result Nvngx_Combo::D3D12_CreateFeature(ID3D12GraphicsCommandList* InCmdList, NVSDK_NGX_Feature InFeatureID,
65101
NVSDK_NGX_Parameter* InParameters, NVSDK_NGX_Handle** OutHandle)
66102
{
67-
Nvngx_Combo_Handle** OutOurHandle = (Nvngx_Combo_Handle**) OutHandle;
68-
69-
if (!OutOurHandle || !InParameters)
103+
if (!OutHandle)
104+
return NVSDK_NGX_Result_FAIL_InvalidParameter;
105+
*OutHandle = nullptr;
106+
if (!InParameters || !InCmdList)
70107
return NVSDK_NGX_Result_FAIL_InvalidParameter;
71108

72109
if (InFeatureID != NVSDK_NGX_Feature_FrameGeneration)
73110
return NVSDK_NGX_Result_FAIL_FeatureNotSupported;
74111

75-
*OutOurHandle = new Nvngx_Combo_Handle(lastIdCreated++, nullptr);
76-
77-
auto resultArturs =
78-
artursProvider->D3D12_CreateFeature(InCmdList, InFeatureID, InParameters, &(*OutOurHandle)->artursHandle);
79-
auto resultFfx =
80-
ffxProvider->D3D12_CreateFeature(InCmdList, InFeatureID, InParameters, &(*OutOurHandle)->ffxHandle);
112+
ComPtr<ID3D12Device> device;
113+
if (FAILED(InCmdList->GetDevice(IID_PPV_ARGS(&device))) || !device)
114+
return NVSDK_NGX_Result_FAIL_PlatformError;
81115

82-
if (resultArturs != NVSDK_NGX_Result_Success || resultFfx != NVSDK_NGX_Result_Success)
116+
auto pending = std::make_shared<PendingCreate>();
117+
pending->handle = std::make_unique<Nvngx_Combo_Handle>();
118+
pending->handle->Id = lastIdCreated++;
119+
// Retain identity too: a failed child must not let this address be reused
120+
// for an unrelated device while its private cleanup is still pending.
121+
pending->device.Attach(device.Detach());
83122
{
84-
if (resultArturs == NVSDK_NGX_Result_Success)
85-
artursProvider->D3D12_ReleaseFeature((*OutOurHandle)->artursHandle);
86-
87-
if (resultFfx == NVSDK_NGX_Result_Success)
88-
ffxProvider->D3D12_ReleaseFeature((*OutOurHandle)->ffxHandle);
89-
90-
delete *OutOurHandle;
91-
*OutOurHandle = nullptr;
92-
123+
std::lock_guard lock(_pendingMutex);
124+
_pendingCreates.push_back(pending);
125+
}
126+
// Register before the first external call. Exceptions retain the private
127+
// ledger, and no partially constructed handle is exposed to the caller.
128+
pending->uncertain = true;
129+
const auto resultArturs =
130+
artursProvider->D3D12_CreateFeature(InCmdList, InFeatureID, InParameters, &pending->uncertainHandle);
131+
if (resultArturs != NVSDK_NGX_Result_Success || !pending->uncertainHandle)
132+
{
133+
if (resultArturs != NVSDK_NGX_Result_Success && !pending->uncertainHandle)
134+
ForgetPending(pending);
135+
return NVSDK_NGX_Result_Fail;
136+
}
137+
pending->handle->artursHandle = pending->uncertainHandle;
138+
pending->uncertainHandle = nullptr;
139+
pending->uncertain = false;
140+
141+
// FFX Create has a strong output guarantee: failure/exception publishes no
142+
// object. Its successful output and the first child stay ledger-owned here.
143+
const auto resultFfx =
144+
ffxProvider->D3D12_CreateFeature(InCmdList, InFeatureID, InParameters, &pending->handle->ffxHandle);
145+
if (resultFfx != NVSDK_NGX_Result_Success)
146+
{
147+
if (ReleaseChildren(pending->handle.get()) == NVSDK_NGX_Result_Success)
148+
ForgetPending(pending);
93149
return NVSDK_NGX_Result_Fail;
94150
}
95151

152+
ForgetPending(pending);
153+
*OutHandle = reinterpret_cast<NVSDK_NGX_Handle*>(pending->handle.release());
96154
return NVSDK_NGX_Result_Success;
97155
}
98156

99-
NVSDK_NGX_Result Nvngx_Combo::D3D12_ReleaseFeature(NVSDK_NGX_Handle* InHandle)
157+
NVSDK_NGX_Result Nvngx_Combo::ReleaseChildren(Nvngx_Combo_Handle* InOurHandle)
100158
{
101-
Nvngx_Combo_Handle* InOurHandle = (Nvngx_Combo_Handle*) InHandle;
102-
103159
if (!InOurHandle)
104160
return NVSDK_NGX_Result_FAIL_InvalidParameter;
105161

@@ -115,6 +171,15 @@ NVSDK_NGX_Result Nvngx_Combo::D3D12_ReleaseFeature(NVSDK_NGX_Handle* InHandle)
115171
if (InOurHandle->artursHandle || InOurHandle->ffxHandle)
116172
return NVSDK_NGX_Result_Fail;
117173

174+
return NVSDK_NGX_Result_Success;
175+
}
176+
177+
NVSDK_NGX_Result Nvngx_Combo::D3D12_ReleaseFeature(NVSDK_NGX_Handle* InHandle)
178+
{
179+
auto* InOurHandle = reinterpret_cast<Nvngx_Combo_Handle*>(InHandle);
180+
const auto result = ReleaseChildren(InOurHandle);
181+
if (result != NVSDK_NGX_Result_Success)
182+
return result;
118183
delete InOurHandle;
119184
return NVSDK_NGX_Result_Success;
120185
}

‎OptiScaler/framegen/nvngx/Nvngx_Combo.h‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
#include "Nvngx_Arturs.h"
55
#include <d3d12.h>
66
#include <unordered_set>
7+
#include <mutex>
8+
#include <vector>
9+
#include <wrl/client.h>
710

811
struct Nvngx_Combo_Handle
912
{
@@ -22,6 +25,20 @@ class Nvngx_Combo : public IFGNvngx
2225
std::unique_ptr<Nvngx_Arturs> artursProvider = nullptr;
2326
std::unique_ptr<Nvngx_FFX> ffxProvider = nullptr;
2427

28+
struct PendingCreate
29+
{
30+
std::unique_ptr<Nvngx_Combo_Handle> handle;
31+
Microsoft::WRL::ComPtr<ID3D12Device> device;
32+
NVSDK_NGX_Handle* uncertainHandle = nullptr;
33+
bool uncertain = false;
34+
};
35+
// Create calls may run concurrently. Hold this mutex only for bookkeeping,
36+
// never across child callbacks; Shutdown is excluded by outer admission.
37+
std::mutex _pendingMutex;
38+
std::vector<std::shared_ptr<PendingCreate>> _pendingCreates;
39+
void ForgetPending(const std::shared_ptr<PendingCreate>& pending);
40+
NVSDK_NGX_Result ReleaseChildren(Nvngx_Combo_Handle* handle);
41+
2542
// The outer Nvngx_FG transition admission serializes Init/Shutdown.
2643
// Record each child separately so retry never closes a completed child twice.
2744
std::unordered_set<ID3D12Device*> _artursInitAttempts;
@@ -69,6 +86,7 @@ class Nvngx_Combo : public IFGNvngx
6986
NVSDK_NGX_Result D3D12_Shutdown() override;
7087

7188
NVSDK_NGX_Result D3D12_Shutdown1(ID3D12Device* InDevice) override;
89+
NVSDK_NGX_Result D3D12_DrainPending(ID3D12Device* InDevice) override;
7290

7391
NVSDK_NGX_Result D3D12_GetScratchBufferSize(NVSDK_NGX_Feature InFeatureId, const NVSDK_NGX_Parameter* InParameters,
7492
size_t* OutSizeInBytes) override;

‎OptiScaler/framegen/nvngx/Nvngx_FFX.cpp‎

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -70,27 +70,32 @@ NVSDK_NGX_Result Nvngx_FFX::D3D12_GetScratchBufferSize(NVSDK_NGX_Feature InFeatu
7070
NVSDK_NGX_Result Nvngx_FFX::D3D12_CreateFeature(ID3D12GraphicsCommandList* InCmdList, NVSDK_NGX_Feature InFeatureID,
7171
NVSDK_NGX_Parameter* InParameters, NVSDK_NGX_Handle** OutHandle)
7272
{
73-
Nvngx_FFX_Handle** OutOurHandle = (Nvngx_FFX_Handle**) OutHandle;
74-
75-
if (!Init())
76-
return NVSDK_NGX_Result_Fail;
77-
78-
if (!OutOurHandle || !InParameters)
73+
if (!OutHandle)
74+
return NVSDK_NGX_Result_FAIL_InvalidParameter;
75+
*OutHandle = nullptr;
76+
if (!InParameters || !InCmdList)
7977
return NVSDK_NGX_Result_FAIL_InvalidParameter;
8078

8179
if (InFeatureID != NVSDK_NGX_Feature_FrameGeneration)
8280
return NVSDK_NGX_Result_FAIL_FeatureNotSupported;
8381

84-
ID3D12Device* pDevice = nullptr;
82+
if (!Init())
83+
return NVSDK_NGX_Result_Fail;
8584

86-
if (InCmdList->GetDevice(IID_PPV_ARGS(&pDevice)) != S_OK)
85+
ComPtr<ID3D12Device> device;
86+
if (FAILED(InCmdList->GetDevice(IID_PPV_ARGS(&device))) || !device)
8787
return NVSDK_NGX_Result_FAIL_PlatformError;
8888

89-
// Can't create FFX context yet, missing data
90-
*OutOurHandle = new Nvngx_FFX_Handle(lastIdCreated++, nullptr, pDevice);
89+
// Keep creation private until every potentially throwing operation is done.
90+
// There is no FFX context yet; the temporary COM owner also covers allocation
91+
// or parameter-read exceptions, without publishing a half-created handle.
92+
auto handle = std::make_unique<Nvngx_FFX_Handle>();
93+
handle->Id = lastIdCreated++;
94+
InParameters->Get("Width", &handle->swapchainWidth);
95+
InParameters->Get("Height", &handle->swapchainHeight);
9196

92-
InParameters->Get("Width", &(*OutOurHandle)->swapchainWidth);
93-
InParameters->Get("Height", &(*OutOurHandle)->swapchainHeight);
97+
handle->device = device.Detach();
98+
*OutHandle = reinterpret_cast<NVSDK_NGX_Handle*>(handle.release());
9499

95100
return NVSDK_NGX_Result_Success;
96101
}

‎OptiScaler/framegen/nvngx/Nvngx_FG.cpp‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -296,15 +296,22 @@ NVSDK_NGX_Result Nvngx_FG::DrainHandles(HandleApi api, const void* device)
296296
if (_handles.GetIdentity(key, [](const Nvngx_FG_Handle& handle) { return handle.device; }) == nullptr)
297297
return NVSDK_NGX_Result_FAIL_NotInitialized;
298298

299-
if (keys.empty())
300-
return NVSDK_NGX_Result_Success;
299+
auto* provider = _provider.Peek();
300+
if (!provider)
301+
return keys.empty() ? NVSDK_NGX_Result_Success : NVSDK_NGX_Result_FAIL_NotInitialized;
301302
// DLL_PROCESS_DETACH may hold the loader lock. Do not start GPU/provider
302303
// destruction there; retain ownership rather than claiming a clean drain.
303304
if (State::Instance().isShuttingDown)
304305
return NVSDK_NGX_Result_FAIL_NotInitialized;
305-
auto* provider = _provider.Peek();
306-
if (!provider)
307-
return NVSDK_NGX_Result_FAIL_NotInitialized;
306+
307+
// Failed Combo creation can own children without any public token. Drain
308+
// those before native shutdown even when the registry is empty.
309+
if (api == HandleApi::D3D12)
310+
{
311+
const auto result = provider->D3D12_DrainPending(static_cast<ID3D12Device*>(const_cast<void*>(device)));
312+
if (result != NVSDK_NGX_Result_Success)
313+
return result;
314+
}
308315

309316
for (const auto* key : keys)
310317
_handles.SuspendReads(key);
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Private feature creation rollback — 2026-09-26
2+
3+
## Confirmed source defects
4+
5+
`Nvngx_Combo::D3D12_CreateFeature` published its wrapper before either child
6+
completed creation. If a child failed, it ignored the successful child's Release
7+
result and deleted the wrapper. In particular, the newly explicit FFX/Combo
8+
release failure handling could not recover ownership already lost during Create.
9+
An exception between child calls also left an unpublished child outside the
10+
public token registry. Draining only published tokens before parent shutdown was
11+
therefore incomplete.
12+
13+
`Nvngx_FFX::D3D12_CreateFeature` acquired a raw COM device reference before
14+
allocating its wrapper, published that wrapper before reading parameters, and
15+
did not reject a null command list or successful GetDevice with a null output.
16+
Allocation and parameter-read exceptions could leak ownership or expose partial
17+
output. These are code-level findings; no claim is made that they explain every
18+
reported Witcher/ZZZ crash.
19+
20+
## Changes
21+
22+
- FFX keeps its wrapper and device reference private with RAII. It publishes
23+
output only after parameter reads finish. Invalid arguments, initialization
24+
errors, device-query errors, allocation failures and exceptions leave null
25+
output. FFX context creation still occurs later in Evaluate.
26+
- Combo registers private ownership before calling either child. Successful
27+
children remain recorded when rollback fails or throws; the public output
28+
stays null. The pending record owns a COM reference so its device address cannot
29+
be reused before retry completes. Failed first-child creation no longer starts
30+
the second child needlessly.
31+
- Shared `ReleaseChildren` preserves the existing partial-release retry rules
32+
for both published and private wrappers. Successful cleanup is not repeated.
33+
- Internal `D3D12_DrainPending` runs before native shutdown even when there are no
34+
public tokens. Combo's direct Shutdown also checks its private ledger. Failed
35+
private cleanup blocks parent teardown and preserves Init obligations.
36+
- An exception inside the third-party Arturs Create, a failure with non-null
37+
output, or success with null output leaves uncertain ownership. No guessed
38+
pointer is sent to Release; shutdown of that device is refused and logged until
39+
process restart. This intentionally prioritizes retention over false success.
40+
- Only short metadata operations hold the private ledger mutex. Child calls run
41+
outside it. Exclusive outer transition admission excludes Shutdown from
42+
concurrent Create; ordinary Create calls can still execute concurrently.
43+
- Provider drain no longer treats an empty public registry as proof that no
44+
private objects remain. If a provider exists during process detach, it refuses
45+
to start the drain under the possible loader lock, including an empty registry.
46+
47+
## Validation
48+
49+
Local extracted-source C++ tests compiled with Zig's C++ driver:
50+
51+
| Suite | Checks | Scope |
52+
| --- | ---: | --- |
53+
| `test_ffx_create.py` | 15 | Entire production Create/Release and real wrapper types; counted COM, injected allocation and parameter exceptions |
54+
| `test_combo_create.py` | 56 | Complete Create/rollback/private drain/Shutdown, actual ledger members, SDK stand-ins, device isolation and concurrent Create |
55+
| `test_provider_shutdown_drain.py` | 50 | Actual outer drain/coordinators/registry; includes four new private-drain cases |
56+
| `test_combo_release.py` | 44 | Affected published partial-release behavior |
57+
| `test_combo_shutdown.py` | 132 | Affected child close footprints; private hook stubbed here and real in new suite |
58+
| `test_ffx_release.py` | 22 | Context destruction and device-reference ordering |
59+
| `test_shutdown_routing.py` | 56 | Exported shutdown routing |
60+
| `test_provider_handle_api.py` | 42 | Existing API-specific token routing |
61+
62+
Three in-memory negative controls were rejected by executable assertions:
63+
transferring the FFX device reference before parameter reads; treating live
64+
children as successful rollback; restoring the outer empty-registry early
65+
return. Production files were not mutated by these controls.
66+
67+
The two new suites are included in the Windows build workflow. Full DLL build,
68+
MSVC test results and artifact identity are recorded below once available.
69+
70+
## Remaining scope
71+
72+
This closes the known owned Combo partial-Create gap; it does not establish
73+
arbitrary third-party exception recovery. Repeated Init with old features,
74+
native Init transaction ordering, and concurrent ordinary Evaluate scratch-state
75+
access remain separate lifecycle work. No installer changes, game files, driver
76+
profiles, native SL1 policy or frame-multiplier algorithms were changed.
77+
Actual GPU/game behavior remains unverified at the user's request. No aurora
78+
synchronization or Release is implied by these focused checks.

0 commit comments

Comments
 (0)