Skip to content

Commit bda5304

Browse files
committed
fix(compat): serialize Nukem configuration refresh with provider lifecycle
1 parent 644bd42 commit bda5304

8 files changed

Lines changed: 162 additions & 23 deletions

File tree

‎.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')) {
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')) {
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/Nvngx_FG.cpp‎

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -198,32 +198,43 @@ feature_version Nvngx_FG::extraVersion()
198198
return provider->extraVersion();
199199
}
200200

201-
void Nvngx_FG::setDebugView(bool enabled)
201+
bool Nvngx_FG::setDebugView(bool enabled)
202202
{
203-
auto* provider = getProvider();
203+
auto lease = _calls.TryTransition();
204+
if (!lease || (_dx12InitAttempts.empty() && _vulkanInitAttempts.empty()))
205+
return false;
206+
207+
// UI callbacks must neither construct a provider nor race Evaluate/Shutdown.
208+
auto* provider = _provider.Peek();
204209

205210
if (!provider)
206-
return;
211+
return false;
207212

208213
if (provider->getType() == FGNvngxReplacement::Nukems)
209214
{
210215
auto* nukemsProvider = static_cast<Nvngx_Nukems*>(provider);
211-
nukemsProvider->setDebugView(enabled);
216+
return nukemsProvider->setDebugView(enabled);
212217
}
218+
return false;
213219
}
214220

215-
void Nvngx_FG::setInterpolatedOnly(bool enabled)
221+
bool Nvngx_FG::setInterpolatedOnly(bool enabled)
216222
{
217-
auto* provider = getProvider();
223+
auto lease = _calls.TryTransition();
224+
if (!lease || (_dx12InitAttempts.empty() && _vulkanInitAttempts.empty()))
225+
return false;
226+
227+
auto* provider = _provider.Peek();
218228

219229
if (!provider)
220-
return;
230+
return false;
221231

222232
if (provider->getType() == FGNvngxReplacement::Nukems)
223233
{
224234
auto* nukemsProvider = static_cast<Nvngx_Nukems*>(provider);
225-
nukemsProvider->setInterpolatedOnly(enabled);
235+
return nukemsProvider->setInterpolatedOnly(enabled);
226236
}
237+
return false;
227238
}
228239

229240
NVSDK_NGX_Result Nvngx_FG::D3D12_Init(unsigned long long InApplicationId, const wchar_t* InApplicationDataPath,

‎OptiScaler/framegen/nvngx/Nvngx_FG.h‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,8 @@ class Nvngx_FG
110110
static feature_version extraVersion();
111111

112112
// TODO: nukem-specific, unify
113-
static void setDebugView(bool enabled);
114-
static void setInterpolatedOnly(bool enabled);
113+
static bool setDebugView(bool enabled);
114+
static bool setInterpolatedOnly(bool enabled);
115115

116116
static NVSDK_NGX_Result D3D12_Init(unsigned long long InApplicationId, const wchar_t* InApplicationDataPath,
117117
ID3D12Device* InDevice, const NVSDK_NGX_FeatureCommonInfo* InFeatureInfo,

‎OptiScaler/framegen/nvngx/Nvngx_Nukems.cpp‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,25 +57,26 @@ void Nvngx_Nukems::LoadLibraries()
5757
}
5858
}
5959

60-
void Nvngx_Nukems::setSetting(const wchar_t* setting, const wchar_t* value)
60+
bool Nvngx_Nukems::setSetting(const wchar_t* setting, const wchar_t* value)
6161
{
62-
if (is120orNewer())
62+
if (is120orNewer() && SetEnvironmentVariableW(setting, value))
6363
{
64-
SetEnvironmentVariable(setting, value);
6564
_refreshGlobalConfiguration();
65+
return true;
6666
}
67+
return false;
6768
}
6869

69-
void Nvngx_Nukems::setDebugView(bool enabled)
70+
bool Nvngx_Nukems::setDebugView(bool enabled)
7071
{
7172
auto setting = L"DLSSGTOFSR3_EnableDebugOverlay";
7273
auto value = enabled ? L"1" : L"";
73-
setSetting(setting, value);
74+
return setSetting(setting, value);
7475
}
7576

76-
void Nvngx_Nukems::setInterpolatedOnly(bool enabled)
77+
bool Nvngx_Nukems::setInterpolatedOnly(bool enabled)
7778
{
7879
auto setting = L"DLSSGTOFSR3_EnableInterpolatedFramesOnly";
7980
auto value = enabled ? L"1" : L"";
80-
setSetting(setting, value);
81+
return setSetting(setting, value);
8182
}

‎OptiScaler/framegen/nvngx/Nvngx_Nukems.h‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ class Nvngx_Nukems : public Nvngx_DllProxy
77
{
88
PFN_RefreshGlobalConfiguration _refreshGlobalConfiguration = nullptr;
99

10-
void setSetting(const wchar_t* setting, const wchar_t* value);
10+
bool setSetting(const wchar_t* setting, const wchar_t* value);
1111
bool is120orNewer() const { return _refreshGlobalConfiguration != nullptr; }
1212

1313
protected:
@@ -16,8 +16,8 @@ class Nvngx_Nukems : public Nvngx_DllProxy
1616
public:
1717
Nvngx_Nukems() { LoadLibraries(); }
1818

19-
void setDebugView(bool enabled);
20-
void setInterpolatedOnly(bool enabled);
19+
bool setDebugView(bool enabled);
20+
bool setInterpolatedOnly(bool enabled);
2121
int getMaxFakeFramesCount() override { return 1; }
2222
FGNvngxReplacement getType() override { return FGNvngxReplacement::Nukems; }
2323
};

‎OptiScaler/menu/menu_common.cpp‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4870,14 +4870,21 @@ void MenuCommon::RenderFrameGenerationRuntimeSettings(RenderMenuContext& ctx)
48704870

48714871
if (activeNvngxFg == FGNvngxReplacement::Nukems)
48724872
{
4873+
static bool settingRejected = false;
48734874
if (ImGui::Checkbox(AURORA_CN("启用调试视图"), &state.dlssgDebugView))
48744875
{
4875-
Nvngx_FG::setDebugView(state.dlssgDebugView);
4876+
settingRejected = !Nvngx_FG::setDebugView(state.dlssgDebugView);
4877+
if (settingRejected)
4878+
state.dlssgDebugView = !state.dlssgDebugView;
48764879
}
48774880
if (ImGui::Checkbox(AURORA_CN("仅显示插值帧"), &state.dlssgInterpolatedOnly))
48784881
{
4879-
Nvngx_FG::setInterpolatedOnly(state.dlssgInterpolatedOnly);
4882+
settingRejected = !Nvngx_FG::setInterpolatedOnly(state.dlssgInterpolatedOnly);
4883+
if (settingRejected)
4884+
state.dlssgInterpolatedOnly = !state.dlssgInterpolatedOnly;
48804885
}
4886+
if (settingRejected)
4887+
ImGui::TextWrapped(AURORA_CN("设置未应用:帧生成忙碌或当前运行库不支持,请稍后重试。"));
48814888
}
48824889

48834890
if (activeNvngxFg == FGNvngxReplacement::FFX || activeNvngxFg == FGNvngxReplacement::Combo)

‎docs/PROVIDER_TRANSITIONS_20260924.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,16 @@ Next work remains device/generation and outstanding-feature ownership, including
5555
- Combo may retain one child after a partial Release; existing releaseStarted blocks its Evaluate. Partial parent Shutdown is now accounted for at child level, but outstanding feature handles still need corresponding invalidation/drain handling before subsequent Evaluate/Release can be declared safe.
5656
- Nukem debug/interpolated-only setters invoke RefreshGlobalConfiguration outside the 22 SDK admission routes. They are a separate mutable callback surface to include in the remaining admission review. This batch does not silently claim they are covered.
5757

58+
## Nukem settings follow-up — 2026-09-25
59+
60+
The two mutable UI callbacks above now take the same exclusive transition admission as Init/Shutdown. This excludes them from ordinary SDK calls (including Evaluate) and other transitions, without holding the metadata mutex inside the DLL callback. They use only an already-published provider and require an existing attempted-Init cleanup footprint; they no longer lazily construct a provider to service a menu checkbox. A footprint is not proof that a failed Init became usable, and full device/generation readiness remains separate work.
61+
62+
Both routes return whether the setting was applied. Missing refresh export, absent/wrong provider, no footprint, busy admission and failed environment-variable update return false. On rejection the menu restores its checkbox and displays a Chinese retry explanation; it does not silently pretend success. No deferred request is queued across shutdown/reinitialization. The low-level setter does not invoke RefreshGlobalConfiguration after SetEnvironmentVariableW fails.
63+
64+
Thirty executable checks compile the complete production high-level and low-level setters with real ProviderCallAdmission and stand-ins for environment writes/DLL refresh. They cover both settings, enabled/disabled values, D3D12/Vulkan footprints, missing/busy/wrong provider, missing export, failed write, same-call and worker-thread callback rejection, and admission release on an exception. Replacing transition admission with ordinary operation admission in a temporary source copy fails check2. The affected 85 existing admission checks pass. These tests do not simulate actual Nukem DLL internals or promise exception recovery after a DLL partially changes its own configuration.
65+
66+
Device/generation identity, outstanding FFX/Combo feature cleanup and global native Init transactions remain open. This bounded callback fix does not complete those larger contracts.
67+
5868
## Final validation recorded 2026-09-25
5969

6070
Latest built code: `5ac7f631ccffda0177f47608f89a15c4b5ce2193`. Windows job `107467622284` completed full MSBuild, runtime inventory/copy, MSVC compatibility checks, package and artifact upload successfully. Its log confirms all four new suites (85 + 104 + 132 + 42 checks), plus affected 52 shutdown, 36 publication, 20 Release and 44 Combo Release checks passed. Local Zig checks and three negative controls are additional CPU evidence, not game verification.

‎tests/test_provider_settings.py‎

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
"""Execute complete production Nukem setters and provider UI routes.
2+
3+
The real transition admission is used; the OS environment and DLL callback are
4+
stand-ins. This tests CPU call ordering, not the DLL's configuration internals.
5+
"""
6+
import argparse
7+
from pathlib import Path
8+
import subprocess
9+
import tempfile
10+
from test_shutdown_routing import function
11+
12+
ROOT = Path(__file__).resolve().parents[1]
13+
PRELUDE = r'''
14+
#include "framegen/ProviderCallAdmission.h"
15+
#include <unordered_set>
16+
#include <functional>
17+
#include <future>
18+
#include <chrono>
19+
#include <string>
20+
#include <stdexcept>
21+
#include <iostream>
22+
#include <cstdlib>
23+
enum class FGNvngxReplacement { Nukems, Other };
24+
int writes=0,refreshes=0,checks=0;
25+
bool environmentOK=true;
26+
std::wstring key,value;
27+
std::function<void()> callback;
28+
bool SetEnvironmentVariableW(const wchar_t* k,const wchar_t* v) {
29+
++writes;key=k;value=v;return environmentOK;
30+
}
31+
void refresh(){++refreshes;if(callback)callback();}
32+
void check(bool ok){if(!ok){std::cerr<<"settings check "<<checks+1<<" failed\n";std::exit(2);}++checks;}
33+
struct Base { FGNvngxReplacement type=FGNvngxReplacement::Nukems; FGNvngxReplacement getType(){return type;} };
34+
struct Nvngx_Nukems:Base {
35+
void (*_refreshGlobalConfiguration)()=refresh;
36+
bool is120orNewer() const {return _refreshGlobalConfiguration!=nullptr;}
37+
bool setSetting(const wchar_t*,const wchar_t*);
38+
bool setDebugView(bool);bool setInterpolatedOnly(bool);
39+
} provider;
40+
struct Nvngx_FG {
41+
inline static ProviderCallAdmission _calls;
42+
inline static std::unordered_set<void*> _dx12InitAttempts,_vulkanInitAttempts;
43+
struct Publication {bool ready;int peeks;Base* Peek(){++peeks;return ready?&provider:nullptr;}};
44+
inline static Publication _provider{true,0};
45+
static bool setDebugView(bool);static bool setInterpolatedOnly(bool);
46+
};
47+
'''
48+
CHECKS = r'''
49+
int main(){
50+
for(auto setter:{Nvngx_FG::setDebugView,Nvngx_FG::setInterpolatedOnly}) {
51+
int before=writes;
52+
check(!setter(true)&&writes==before); // No Init footprint: no lazy load or environment write.
53+
Nvngx_FG::_dx12InitAttempts.insert(&provider);
54+
int peeks=Nvngx_FG::_provider.peeks;
55+
{auto busy=Nvngx_FG::_calls.TryOperation();check(!setter(true)&&writes==before&&Nvngx_FG::_provider.peeks==peeks);}
56+
{auto busy=Nvngx_FG::_calls.TryTransition();check(!setter(true)&&writes==before&&Nvngx_FG::_provider.peeks==peeks);}
57+
Nvngx_FG::_provider.ready=false;check(!setter(true)&&writes==before);Nvngx_FG::_provider.ready=true;
58+
provider.type=FGNvngxReplacement::Other;check(!setter(true)&&writes==before);provider.type=FGNvngxReplacement::Nukems;
59+
provider._refreshGlobalConfiguration=nullptr;check(!setter(true)&&writes==before);provider._refreshGlobalConfiguration=refresh;
60+
environmentOK=false;int calls=refreshes;
61+
check(!setter(true)&&writes==before+1&&refreshes==calls);environmentOK=true;
62+
callback=[setter]{
63+
auto work=std::async(std::launch::async,[setter]{
64+
return !setter(false)&&!Nvngx_FG::_calls.TryOperation()&&!Nvngx_FG::_calls.TryTransition();
65+
});
66+
check(work.wait_for(std::chrono::seconds(2))==std::future_status::ready&&work.get());
67+
};
68+
check(setter(true)&&value==L"1"&&refreshes==calls+1);
69+
check(key==(setter==Nvngx_FG::setDebugView?L"DLSSGTOFSR3_EnableDebugOverlay":L"DLSSGTOFSR3_EnableInterpolatedFramesOnly"));
70+
callback={};check(setter(false)&&value.empty()&&refreshes==calls+2);
71+
check(bool(Nvngx_FG::_calls.TryOperation())&&bool(Nvngx_FG::_calls.TryOperation()));
72+
callback=[]{throw std::runtime_error("DLL callback");};bool threw=false;
73+
try{setter(true);}catch(const std::runtime_error&){threw=true;}
74+
check(threw&&bool(Nvngx_FG::_calls.TryTransition()));callback={};
75+
Nvngx_FG::_dx12InitAttempts.clear();Nvngx_FG::_vulkanInitAttempts.insert(&provider);
76+
check(setter(true));Nvngx_FG::_vulkanInitAttempts.clear();
77+
before=writes;check(!setter(false)&&writes==before);
78+
}
79+
std::cout<<"PASS: "<<checks<<" provider settings checks\n";
80+
}
81+
'''
82+
83+
84+
def main():
85+
parser = argparse.ArgumentParser()
86+
parser.add_argument('--compiler', required=True)
87+
parser.add_argument('--driver')
88+
args = parser.parse_args()
89+
proxy = (ROOT/'OptiScaler/framegen/nvngx/Nvngx_FG.cpp').read_text(encoding='utf-8')
90+
nukem = (ROOT/'OptiScaler/framegen/nvngx/Nvngx_Nukems.cpp').read_text(encoding='utf-8')
91+
cpp = PRELUDE
92+
for name in ('setDebugView', 'setInterpolatedOnly'):
93+
cpp += function(proxy, 'bool Nvngx_FG::'+name+'(')
94+
cpp += function(nukem, 'bool Nvngx_Nukems::'+name+'(')
95+
cpp += function(nukem, 'bool Nvngx_Nukems::setSetting(') + CHECKS
96+
with tempfile.TemporaryDirectory(prefix='aurora-provider-settings-') as directory:
97+
path = Path(directory)
98+
test, exe = path/'test.cpp', path/'test.exe'
99+
test.write_text(cpp, encoding='utf-8')
100+
command = [args.compiler] + ([args.driver] if args.driver else [])
101+
if Path(args.compiler).stem.lower() == 'cl':
102+
command += ['/nologo', '/EHsc', '/std:c++20', '/I'+str(ROOT/'OptiScaler'), str(test), '/Fe:'+str(exe)]
103+
else:
104+
command += ['-std=c++20', '-pthread', '-I'+str(ROOT/'OptiScaler'), str(test), '-o', str(exe)]
105+
subprocess.run(command, cwd=path, check=True)
106+
subprocess.run([str(exe)], cwd=path, check=True, timeout=25)
107+
108+
109+
if __name__ == '__main__':
110+
main()

0 commit comments

Comments
 (0)