forked from ImDreamt/MFGAdaUnlock-RenoDx
-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathaddon.cpp
More file actions
1532 lines (1375 loc) · 66 KB
/
Copy pathaddon.cpp
File metadata and controls
1532 lines (1375 loc) · 66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* RenoDX MFG Unlock
* SPDX-License-Identifier: MIT
*
* Makes DLSS multi-frame generation (3x and above) available on Ada (RTX 40),
* which NVIDIA ships gated to Blackwell (RTX 50) only -- and corrects the
* interpolation so the extra frames carry new motion instead of repeats.
*
* Nothing on disk is modified. Every patch is applied to the mapped image and
* reverted on unload.
*
* ---------------------------------------------------------------------------
* LAYOUT
*
* this file arch gates, flip metering, the plugin's frame ceiling,
* config, overlay, and the fallback parameter override
* midpoint.hpp the temporal fix -- fatbin/PTX rewrite
* framecount.hpp forcing numFramesToGenerate through slDLSSGSetOptions
* loadhook.hpp catching the snippet as it is mapped
* ngx_hook.hpp thread-safe Detours installer
*
* Each of those carries its own commentary. What follows is this file only.
*
* ---------------------------------------------------------------------------
* HOW THE CAPABILITY IS DECIDED -- three gates, outermost first
*
* 1. Which GPUs the snippet claims at all. nvngx_dlssg.dll exports
*
* NVSDK_NGX_GetGPUArchitecture: mov eax, 0x190 ; Ada
* ret
*
* a hardcoded minimum architecture that NGX reads before anything else. It
* matches each snippet's published hardware requirement exactly --
* nvngx_dlss 0x160 (Turing), nvngx_dlssg 0x190 (Ada), nvngx_dlssnr 0x1b0
* (Blackwell). A 40-series card already clears this one, so it is left
* alone; it is documented because it is the first thing to read when a
* feature is missing *entirely* rather than merely limited.
*
* 2. How many frames the snippet advertises, in
* DLSSGInstanceManager::PopulateParameters:
*
* cmp ebp, 0x1b0 ; NVAPI arch id, 0x1b0 == GB20x (RTX 50)
* jl <not supported> ; anything below -->
* mov edi, 5 ; Blackwell: max frame count 5
* ...
* <not supported>: mov edi, 1
* ... Set("DLSSG.MultiFrameCountMax", edi)
*
* 3. A second compare against the same constant, feeding a runtime capability
* flag that drives generation itself:
*
* cmp eax, 0x1b0
* setae al
* mov byte ptr [rdi+0x28], al
*
* Patching (2) without (3) is the worst of both: the options appear, the
* runtime accepts the request, and the game renders black.
*
* So this addon rewrites 0x1b0 -> 0x190 at every *compare*, in both encodings
* (3D imm32 and 81 /7 imm32), and deliberately leaves `mov r32, 0x1b0` alone --
* that is the arch-id lookup table, not a gate.
*
* ---------------------------------------------------------------------------
* WHY THE MAPPED IMAGE AND NEVER THE FILE
*
* NGX verifies the snippet's Authenticode signature when it LOADS it, so the
* same bytes changed on disk make frame generation disappear altogether. The
* mapped copy is never re-checked.
*
* ---------------------------------------------------------------------------
* THE PARAMETER OVERRIDE (kept, but not what does the work)
*
* NVSDK_NGX_*_GetParameters / GetCapabilityParameters are also hooked, and slot
* 11 of the returned object's vtable -- Get(const char*, unsigned int*) -- is
* replaced so "DLSSG.MultiFrameCountMax" can be answered directly.
* NVSDK_NGX_Parameter declares 8 Set overloads before its 8 Get overloads,
* which is where that slot number comes from.
*
* This was the original approach, and it does not work with Streamline:
* sl.dlss_g builds its own NVSDK_NGX_Parameter rather than passing NGX's along,
* so the patch arms and never fires. It is kept because it costs nothing and is
* the only lever for an NGX consumer that is not Streamline. The arch gates
* above are what actually does the job in every game tested.
*
* ---------------------------------------------------------------------------
* PACING
*
* Blackwell paces multi-frame output with hardware flip metering that Ada does
* not have; left enabled, 3x+ freezes the presented image while audio keeps
* running. Streamline already ships the software fallback, so the addon only
* has to force the plugin down it -- see TryPatchFlipMeteringInModule, which
* derives the field's offset AND its polarity at runtime because both move
* between plugin builds.
*
* None of this makes multi-frame generation correct by NVIDIA's standards on
* hardware they did not ship it for. It makes it run, and midpoint.hpp makes it
* look right; the rest is judged by eye.
*/
#define ImTextureID ImU64
#include <windows.h>
#include <algorithm>
#include <atomic>
#include <cwchar>
#include <cstring>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
#include <d3d11.h>
#include <d3d12.h>
#include <nvsdk_ngx.h>
#include <deps/imgui/imgui.h>
#include <include/reshade.hpp>
#include "./framecount.hpp"
#include "./loadhook.hpp"
#include "./midpoint.hpp"
#include "./ngx_hook.hpp"
namespace {
constexpr const char* kConfigSection = "RenoDX.MFGUnlock";
constexpr const char* kParamName = "DLSSG.MultiFrameCountMax";
// Slot 11 of NVSDK_NGX_Parameter == Get(const char*, unsigned int*).
constexpr size_t kGetUInt32Slot = 11;
constexpr unsigned int kMinCount = 2;
constexpr unsigned int kMaxCount = 5;
std::atomic_bool g_enabled{true};
std::atomic<unsigned int> g_max_count{4};
std::atomic_bool g_force_flip_meter_off{false};
std::atomic_bool g_temporal_fix{true};
// Raising the plugin's own clamp broke GTA V Enhanced -- its 2.9.1.0 plugin was
// only ever shipped bounded at 3, and lifting that is not the same as it being
// able to cope. Off by default; updating the plugin is the sound fix.
std::atomic_bool g_raise_ceiling{false};
enum class DetectedRenderApi : unsigned int {
kUnknown,
kD3D11,
kD3D12,
kVulkan,
kOther,
};
std::atomic<DetectedRenderApi> g_render_api{DetectedRenderApi::kUnknown};
const char* RenderApiName(DetectedRenderApi api) {
switch (api) {
case DetectedRenderApi::kD3D11:
return "Direct3D 11";
case DetectedRenderApi::kD3D12:
return "Direct3D 12";
case DetectedRenderApi::kVulkan:
return "Vulkan";
case DetectedRenderApi::kOther:
return "unsupported/other";
default:
return "not detected yet";
}
}
// Our own image. Both marker scans look for strings that are, necessarily,
// string literals inside this very DLL -- so without excluding ourselves the
// scan happily identifies the addon as the DLSS-G plugin and then fails to
// make sense of it. Harmless where the real plugin is enumerated first;
// fatal where it is not loaded at all.
HMODULE g_self_module = nullptr;
std::atomic_bool g_vtable_patched{false};
std::atomic_bool g_override_reported{false};
std::atomic<unsigned int> g_override_hits{0};
std::atomic<unsigned int> g_runtime_reported_value{0};
void** g_patched_slot = nullptr;
void* g_original_slot_value = nullptr;
using GetUInt32Fn = NVSDK_NGX_Result (*)(void* self, const char* name, unsigned int* out);
GetUInt32Fn g_real_get_uint32 = nullptr;
bool NgxFailed(NVSDK_NGX_Result result) {
return (static_cast<unsigned int>(result) & 0xfff00000u) == 0xbad00000u;
}
NVSDK_NGX_Result HookedGetUInt32(void* self, const char* name, unsigned int* out) {
NVSDK_NGX_Result result = g_real_get_uint32(self, name, out);
if (!g_enabled.load(std::memory_order_relaxed)) return result;
if (name == nullptr || out == nullptr) return result;
if (std::strcmp(name, kParamName) != 0) return result;
const bool failed = NgxFailed(result);
const unsigned int reported = failed ? 0u : *out;
const unsigned int want = g_max_count.load(std::memory_order_relaxed);
g_runtime_reported_value.store(reported, std::memory_order_relaxed);
// Never lower a value the runtime already offers.
if (!failed && reported >= want) return result;
*out = want;
g_override_hits.fetch_add(1, std::memory_order_relaxed);
if (!g_override_reported.exchange(true, std::memory_order_relaxed)) {
std::stringstream s;
s << "mfgunlock: " << kParamName << " came back as ";
if (failed) {
s << "a failure (0x" << std::hex << static_cast<unsigned int>(result) << std::dec << ")";
} else {
s << reported;
}
s << "; reporting " << want << " instead.";
reshade::log::message(reshade::log::level::info, s.str().c_str());
}
return NVSDK_NGX_Result_Success;
}
bool PatchParameterVTable(NVSDK_NGX_Parameter* params) {
if (params == nullptr) return false;
if (g_vtable_patched.load(std::memory_order_acquire)) return true;
auto** vtable = *reinterpret_cast<void***>(params);
if (vtable == nullptr) return false;
void** slot = &vtable[kGetUInt32Slot];
DWORD old_protect = 0;
if (VirtualProtect(slot, sizeof(void*), PAGE_READWRITE, &old_protect) == 0) {
reshade::log::message(reshade::log::level::error,
"mfgunlock: could not make the NGX parameter vtable writable.");
return false;
}
g_original_slot_value = *slot;
g_real_get_uint32 = reinterpret_cast<GetUInt32Fn>(g_original_slot_value);
*slot = reinterpret_cast<void*>(&HookedGetUInt32);
g_patched_slot = slot;
DWORD ignored = 0;
VirtualProtect(slot, sizeof(void*), old_protect, &ignored);
g_vtable_patched.store(true, std::memory_order_release);
reshade::log::message(
reshade::log::level::info,
"mfgunlock: NGX parameter vtable patched; multi-frame capability override armed.");
return true;
}
void RestoreParameterVTable() {
if (!g_vtable_patched.load(std::memory_order_acquire)) return;
if (g_patched_slot == nullptr || g_original_slot_value == nullptr) return;
DWORD old_protect = 0;
if (VirtualProtect(g_patched_slot, sizeof(void*), PAGE_READWRITE, &old_protect) != 0) {
*g_patched_slot = g_original_slot_value;
DWORD ignored = 0;
VirtualProtect(g_patched_slot, sizeof(void*), old_protect, &ignored);
}
g_vtable_patched.store(false, std::memory_order_release);
}
// ---------------------------------------------------------------- NGX entries
using NgxParamsOutFn = NVSDK_NGX_Result(NVSDK_CONV*)(NVSDK_NGX_Parameter**);
NgxParamsOutFn g_real_allocate_parameters = nullptr;
NgxParamsOutFn g_real_get_capability_parameters = nullptr;
NgxParamsOutFn g_real_get_device_capability_parameters = nullptr;
NgxParamsOutFn g_real_get_parameters = nullptr;
NVSDK_NGX_Result NVSDK_CONV HookedAllocateParameters(NVSDK_NGX_Parameter** out_parameters) {
NVSDK_NGX_Result result = g_real_allocate_parameters(out_parameters);
if (!NgxFailed(result) && out_parameters != nullptr) PatchParameterVTable(*out_parameters);
return result;
}
NVSDK_NGX_Result NVSDK_CONV HookedGetCapabilityParameters(NVSDK_NGX_Parameter** out_parameters) {
NVSDK_NGX_Result result = g_real_get_capability_parameters(out_parameters);
if (!NgxFailed(result) && out_parameters != nullptr) PatchParameterVTable(*out_parameters);
return result;
}
NVSDK_NGX_Result NVSDK_CONV HookedGetDeviceCapabilityParameters(
NVSDK_NGX_Parameter** out_parameters) {
NVSDK_NGX_Result result = g_real_get_device_capability_parameters(out_parameters);
if (!NgxFailed(result) && out_parameters != nullptr) PatchParameterVTable(*out_parameters);
return result;
}
NVSDK_NGX_Result NVSDK_CONV HookedGetParameters(NVSDK_NGX_Parameter** out_parameters) {
NVSDK_NGX_Result result = g_real_get_parameters(out_parameters);
if (!NgxFailed(result) && out_parameters != nullptr) PatchParameterVTable(*out_parameters);
return result;
}
// All four hand out a parameter block, and which one Streamline uses for the
// capability query is not something we can know from outside. They all live in
// the NGX loader, so hooking the set costs nothing extra -- and missing the one
// that is actually used would look exactly like the addon doing nothing.
const std::vector<mfgunlock::hook::HookItem> kNgxHooks = {
{"NVSDK_NGX_D3D12_AllocateParameters",
reinterpret_cast<void**>(&g_real_allocate_parameters),
reinterpret_cast<void*>(&HookedAllocateParameters)},
{"NVSDK_NGX_D3D12_GetCapabilityParameters",
reinterpret_cast<void**>(&g_real_get_capability_parameters),
reinterpret_cast<void*>(&HookedGetCapabilityParameters)},
{"NVSDK_NGX_D3D12_GetDeviceCapabilityParameters",
reinterpret_cast<void**>(&g_real_get_device_capability_parameters),
reinterpret_cast<void*>(&HookedGetDeviceCapabilityParameters)},
{"NVSDK_NGX_D3D12_GetParameters",
reinterpret_cast<void**>(&g_real_get_parameters),
reinterpret_cast<void*>(&HookedGetParameters)},
};
// Only the NGX loader hands out parameter blocks; the feature snippets do not
// export these.
constexpr const wchar_t* kNgxModules[] = {L"_nvngx.dll", L"nvngx.dll"};
std::atomic_bool g_hooked{false};
int g_hook_attempts = 0;
constexpr int kMaxHookAttempts = 8;
// Resolved, not hooked -- used only to hand back the block we allocate below.
NgxParamsOutFn g_real_destroy_parameters = nullptr;
void TryInstallHooks() {
if (g_hooked.load(std::memory_order_acquire)) return;
if (g_hook_attempts >= kMaxHookAttempts) return;
for (const auto* name : kNgxModules) {
HMODULE mod = GetModuleHandleW(name);
if (mod == nullptr) continue;
if (GetProcAddress(mod, "NVSDK_NGX_D3D12_AllocateParameters") == nullptr) continue;
++g_hook_attempts;
char narrow[64] = {};
WideCharToMultiByte(CP_UTF8, 0, name, -1, narrow, sizeof(narrow) - 1, nullptr, nullptr);
if (!mfgunlock::hook::Install(mod, kNgxHooks, narrow)) continue;
g_real_destroy_parameters = reinterpret_cast<NgxParamsOutFn>(
reinterpret_cast<void*>(GetProcAddress(mod, "NVSDK_NGX_D3D12_DestroyParameters")));
g_hooked.store(true, std::memory_order_release);
return;
}
}
// Every parameter object shares one vtable, so we do not have to wait for the
// game to hand us one: allocate a throwaway block ourselves, take the vtable
// from it, and give it straight back. Before NGX is initialised this just
// returns an error and we retry on the next present.
//
// Waiting passively would mean the override arms only once DLSS-G is already
// initialising, which is a race against the very query we want to answer.
int g_bootstrap_attempts = 0;
constexpr int kMaxBootstrapAttempts = 2000;
void TryBootstrapVTable() {
if (g_vtable_patched.load(std::memory_order_acquire)) return;
if (!g_hooked.load(std::memory_order_acquire)) return;
if (g_real_allocate_parameters == nullptr) return;
if (g_bootstrap_attempts >= kMaxBootstrapAttempts) return;
++g_bootstrap_attempts;
NVSDK_NGX_Parameter* params = nullptr;
NVSDK_NGX_Result result = g_real_allocate_parameters(¶ms);
if (NgxFailed(result) || params == nullptr) return;
PatchParameterVTable(params);
if (g_real_destroy_parameters != nullptr) {
reinterpret_cast<NVSDK_NGX_Result(NVSDK_CONV*)(NVSDK_NGX_Parameter*)>(
reinterpret_cast<void*>(g_real_destroy_parameters))(params);
}
}
// ------------------------------------------------------- in-memory arch gate
//
// The read-side override assumes Streamline reads the capability through the
// NGX loader's parameter object. It does not: the vtable patch arms fine and
// then never fires, because sl.* carries its own NVSDK_NGX_Parameter
// implementation and we never see that vtable.
//
// So patch the decision instead of the answer. NGX verifies the snippet's
// Authenticode signature when it LOADS the file -- which is why the on-disk
// byte patch made frame generation disappear entirely. Editing the same bytes
// in the mapped image afterwards is never re-checked, so the signed DLL loads
// and then behaves like the patched one.
//
// The instruction, in DLSSGInstanceManager::PopulateParameters:
// 81 FD B0 01 00 00 cmp ebp, 0x1b0 ; arch id, 0x1b0 == GB20x
// Exactly one occurrence in .text of 310.8, which is what makes this safe to
// find by pattern. 0x1b0 -> 0x190 lets AD10x take the Blackwell path.
// nvngx_dlssg.dll gates multi-frame on the NVAPI arch id in more than one
// place, and they do different jobs:
//
// DLSSGInstanceManager::PopulateParameters -- decides what to advertise
// 81 FD B0 01 00 00 cmp ebp, 0x1b0 ; 0x1b0 == GB20x (RTX 50)
// jl <report max = 1>
//
// ...and a separate runtime capability flag that drives generation itself:
// 3D B0 01 00 00 cmp eax, 0x1b0
// 0F 93 C0 setae al
// 88 47 28 mov byte ptr [rdi+0x28], al
//
// Patching only the first is what produced 3x/4x rendering black: the runtime
// advertised multi-frame, accepted the request, and then took the non-Blackwell
// path when actually generating, so the extra frames were presented empty.
//
// So rewrite every comparison against the Blackwell arch id, in both encodings.
// 0x1b0 is a specific NVAPI arch constant, and any compare against it in this
// DLL is an arch gate -- but a `mov r32, 0x1b0` is the arch-id lookup table
// returning Blackwell's own id, which must be left alone. Only `cmp` forms are
// rewritten.
//
// NGX verifies the snippet's Authenticode signature when it LOADS the file --
// which is why patching the same bytes on disk made frame generation vanish.
// The mapped image is never re-checked, so the signed DLL loads and then
// behaves as patched.
// ------------------------------------------------ locating the DLSS-G snippet
//
// Finding it as GetModuleHandleW(L"nvngx_dlssg.dll") is the same mistake that
// already cost us a silent no-op on the Streamline side: NGX can load the
// snippet from the driver's OTA store, and a game may stage it under another
// path. The name is a fast path, not a contract.
//
// The game-folder DLL and the driver's ...\models\dlssg\... OTA path are
// unambiguous. For renamed providers elsewhere, fall back to the NGX provider
// export plus the older "dlfg_kernel" descriptor. This matters in STALKER 2,
// whose active provider is an opaque .bin from the driver cache and whose
// current build no longer contains that descriptor.
constexpr char kDlssgMarker[] = "dlfg_kernel";
std::vector<HMODULE> g_inspected_modules;
std::vector<HMODULE> g_dlssg_modules;
SRWLOCK g_provider_maintenance_lock = SRWLOCK_INIT;
std::atomic_bool g_provider_rescan_requested{false};
bool ModuleContains(HMODULE mod, const char* needle, size_t needle_len) {
auto* base = reinterpret_cast<unsigned char*>(mod);
const auto* dos = reinterpret_cast<const IMAGE_DOS_HEADER*>(base);
if (IsBadReadPtr(base, sizeof(IMAGE_DOS_HEADER)) != 0) return false;
if (dos->e_magic != IMAGE_DOS_SIGNATURE) return false;
const auto* nt = reinterpret_cast<const IMAGE_NT_HEADERS64*>(base + dos->e_lfanew);
if (nt->Signature != IMAGE_NT_SIGNATURE) return false;
if (nt->OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC) return false;
const auto* section = IMAGE_FIRST_SECTION(nt);
for (WORD i = 0; i < nt->FileHeader.NumberOfSections; ++i, ++section) {
if ((section->Characteristics & IMAGE_SCN_MEM_READ) == 0) continue;
unsigned char* start = base + section->VirtualAddress;
const size_t size = section->Misc.VirtualSize;
if (size < needle_len) continue;
for (size_t off = 0; off + needle_len <= size; ++off) {
if (std::memcmp(start + off, needle, needle_len) == 0) return true;
}
}
return false;
}
void RememberDlssgModule(HMODULE mod) {
if (mod == nullptr || mod == g_self_module) return;
if (std::find(g_dlssg_modules.begin(), g_dlssg_modules.end(), mod) == g_dlssg_modules.end()) {
g_dlssg_modules.push_back(mod);
}
}
bool HasKnownDlssgPath(HMODULE mod) {
wchar_t module_path[32768] = {};
const DWORD length = GetModuleFileNameW(mod, module_path, ARRAYSIZE(module_path));
if (length == 0 || length >= ARRAYSIZE(module_path)) return false;
for (DWORD i = 0; i < length; ++i) {
if (module_path[i] >= L'A' && module_path[i] <= L'Z') {
module_path[i] = static_cast<wchar_t>(module_path[i] - L'A' + L'a');
}
}
return std::wcsstr(module_path, L"nvngx_dlssg") != nullptr ||
std::wcsstr(module_path, L"\\models\\dlssg\\") != nullptr;
}
bool IsDlssgProvider(HMODULE mod) {
// Keep the established D3D/OTA path as the fast path. Vulkan-specific export
// checks are only needed when a game has renamed or relocated the provider.
if (HasKnownDlssgPath(mod)) return true;
// A renamed provider may expose either graphics backend. Streamline's
// slDLSSGSetOptions/slDLSSGGetState interface is renderer-independent, so
// discovery must not discard Vulkan snippets before the shared patch path
// gets a chance to inspect them.
const bool has_d3d12_entry =
GetProcAddress(mod, "NVSDK_NGX_D3D12_PopulateDeviceParameters_Impl") != nullptr;
const bool has_vulkan_entry =
GetProcAddress(mod, "NVSDK_NGX_VULKAN_PopulateDeviceParameters_Impl") != nullptr;
// Retain content-based discovery for games that rename or relocate the
// snippet, but only scan modules exposing an NGX provider entry point.
if (!has_d3d12_entry && !has_vulkan_entry) return false;
return ModuleContains(mod, kDlssgMarker, sizeof(kDlssgMarker) - 1);
}
// Perform one shared bootstrap pass for providers that were mapped before this
// addon. Providers mapped later are handled directly by the loader hook, so
// module enumeration never has to run from the presentation thread.
const std::vector<HMODULE>& DiscoverDlssgModules() {
if (HMODULE fast = GetModuleHandleW(L"nvngx_dlssg.dll")) RememberDlssgModule(fast);
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, GetCurrentProcessId());
if (snap == INVALID_HANDLE_VALUE) return g_dlssg_modules;
MODULEENTRY32W me = {};
me.dwSize = sizeof(me);
if (Module32FirstW(snap, &me)) {
do {
if (me.hModule == g_self_module) continue;
if (std::find(g_inspected_modules.begin(), g_inspected_modules.end(), me.hModule) !=
g_inspected_modules.end()) {
continue;
}
g_inspected_modules.push_back(me.hModule);
if (IsDlssgProvider(me.hModule)) RememberDlssgModule(me.hModule);
} while (Module32NextW(snap, &me));
}
CloseHandle(snap);
return g_dlssg_modules;
}
constexpr unsigned char kArchOld = 0xB0; // 0x1b0 GB20x
constexpr unsigned char kArchNew = 0x90; // 0x190 AD10x
struct GateSite {
unsigned char* address; // the byte holding the arch id's low octet
unsigned char original;
};
std::atomic_bool g_gate_patched{false};
std::vector<GateSite> g_gate_sites;
std::vector<HMODULE> g_gate_modules;
std::vector<HMODULE> g_gate_rejected_modules;
int g_gate_attempts = 0;
// Split out so the load-time trigger can patch a module it already holds a
// handle to. That path runs under the loader lock, where CreateToolhelp32Snapshot
// (which FindDlssgModule uses) would deadlock -- so it must never scan.
void PatchArchGatesInModule(HMODULE mod) {
if (mod == nullptr) return;
if (std::find(g_gate_modules.begin(), g_gate_modules.end(), mod) != g_gate_modules.end()) return;
if (std::find(g_gate_rejected_modules.begin(), g_gate_rejected_modules.end(), mod) !=
g_gate_rejected_modules.end()) {
return;
}
auto* base = reinterpret_cast<unsigned char*>(mod);
const auto* dos = reinterpret_cast<const IMAGE_DOS_HEADER*>(base);
if (dos->e_magic != IMAGE_DOS_SIGNATURE) return;
const auto* nt = reinterpret_cast<const IMAGE_NT_HEADERS64*>(base + dos->e_lfanew);
if (nt->Signature != IMAGE_NT_SIGNATURE) return;
std::vector<unsigned char*> found;
const auto* section = IMAGE_FIRST_SECTION(nt);
for (WORD i = 0; i < nt->FileHeader.NumberOfSections; ++i, ++section) {
if ((section->Characteristics & IMAGE_SCN_MEM_EXECUTE) == 0) continue;
unsigned char* start = base + section->VirtualAddress;
const size_t size = section->Misc.VirtualSize;
if (size < 6) continue;
for (size_t off = 0; off + 6 <= size; ++off) {
// 3D id32 cmp eax, imm32
if (start[off] == 0x3D && start[off + 1] == kArchOld && start[off + 2] == 0x01 &&
start[off + 3] == 0x00 && start[off + 4] == 0x00) {
found.push_back(start + off + 1);
continue;
}
// 81 /7 id32 cmp r32, imm32
if (start[off] == 0x81 && start[off + 1] >= 0xF8 && start[off + 1] <= 0xFF &&
start[off + 2] == kArchOld && start[off + 3] == 0x01 && start[off + 4] == 0x00 &&
start[off + 5] == 0x00) {
found.push_back(start + off + 2);
}
}
}
if (found.empty() || found.size() > 4) {
char module_path[MAX_PATH] = {};
GetModuleFileNameA(mod, module_path, MAX_PATH);
std::stringstream s;
s << "mfgunlock: found " << found.size() << " arch-gate comparisons in " << module_path
<< " (expected 1-4); leaving this provider alone.";
reshade::log::message(reshade::log::level::warning, s.str().c_str());
g_gate_rejected_modules.push_back(mod);
return;
}
const size_t sites_before = g_gate_sites.size();
for (unsigned char* site : found) {
DWORD old_protect = 0;
if (VirtualProtect(site, 1, PAGE_EXECUTE_READWRITE, &old_protect) == 0) continue;
g_gate_sites.push_back({site, *site});
*site = kArchNew;
DWORD ignored = 0;
VirtualProtect(site, 1, old_protect, &ignored);
FlushInstructionCache(GetCurrentProcess(), site, 1);
}
const size_t sites_written = g_gate_sites.size() - sites_before;
if (sites_written == 0) {
reshade::log::message(reshade::log::level::error,
"mfgunlock: could not make the nvngx_dlssg.dll arch gates writable.");
g_gate_rejected_modules.push_back(mod);
return;
}
g_gate_modules.push_back(mod);
g_gate_patched.store(true, std::memory_order_release);
char module_path[MAX_PATH] = {};
GetModuleFileNameA(mod, module_path, MAX_PATH);
std::stringstream s;
s << "mfgunlock: rewrote " << sites_written << " arch gate(s) (0x1b0 -> 0x190) in "
<< module_path << "; multi-frame should report as supported AND generate.";
reshade::log::message(reshade::log::level::info, s.str().c_str());
}
void TryPatchDlssgArchGate() {
++g_gate_attempts;
for (HMODULE mod : g_dlssg_modules) PatchArchGatesInModule(mod);
}
void RestoreDlssgArchGate() {
if (!g_gate_patched.load(std::memory_order_acquire)) return;
for (const auto& site : g_gate_sites) {
DWORD old_protect = 0;
if (VirtualProtect(site.address, 1, PAGE_EXECUTE_READWRITE, &old_protect) != 0) {
*site.address = site.original;
DWORD ignored = 0;
VirtualProtect(site.address, 1, old_protect, &ignored);
FlushInstructionCache(GetCurrentProcess(), site.address, 1);
}
}
g_gate_sites.clear();
g_gate_modules.clear();
g_gate_rejected_modules.clear();
g_gate_patched.store(false, std::memory_order_release);
}
// ------------------------------------------------------ temporal (midpoint)
//
// Unlocking the multipliers gets the right NUMBER of generated frames; this
// gets the right CONTENT. Without it every generated frame is the same 0.5
// blend, so 4x shows three identical half-way frames and the motion is no
// smoother than 2x despite double the counter. See midpoint.hpp.
std::atomic_bool g_midpoint_patched{false};
struct MidpointModulePatch {
HMODULE module;
std::vector<mfgunlock::midpoint::Patch> patches;
void* allocation;
};
std::vector<MidpointModulePatch> g_midpoint_modules;
std::vector<HMODULE> g_midpoint_rejected_modules;
std::string g_midpoint_detail;
int g_midpoint_attempts = 0;
void PatchMidpointInModule(HMODULE mod) {
if (mod == nullptr) return;
const auto already_patched = std::find_if(
g_midpoint_modules.begin(), g_midpoint_modules.end(),
[mod](const MidpointModulePatch& patch) { return patch.module == mod; });
if (already_patched != g_midpoint_modules.end()) return;
if (std::find(g_midpoint_rejected_modules.begin(), g_midpoint_rejected_modules.end(), mod) !=
g_midpoint_rejected_modules.end()) {
return;
}
std::vector<mfgunlock::midpoint::Patch> patches;
void* allocation = nullptr;
std::string detail;
if (!mfgunlock::midpoint::Apply(mod, patches, allocation, detail)) {
char module_path[MAX_PATH] = {};
GetModuleFileNameA(mod, module_path, MAX_PATH);
std::stringstream s;
s << "mfgunlock: temporal fix not applied to " << module_path << " -- " << detail << ".";
reshade::log::message(reshade::log::level::warning, s.str().c_str());
g_midpoint_rejected_modules.push_back(mod);
return;
}
g_midpoint_detail = detail;
g_midpoint_modules.push_back({mod, std::move(patches), allocation});
g_midpoint_patched.store(true, std::memory_order_release);
char module_path[MAX_PATH] = {};
GetModuleFileNameA(mod, module_path, MAX_PATH);
std::stringstream s;
s << "mfgunlock: temporal fix applied to " << module_path << " -- " << detail
<< "; generated frames should now land at their own time, not all at the midpoint.";
reshade::log::message(reshade::log::level::info, s.str().c_str());
}
void TryPatchMidpoint() {
++g_midpoint_attempts;
for (HMODULE mod : g_dlssg_modules) PatchMidpointInModule(mod);
}
// Provider state is normally updated synchronously by the loader hook. The
// bounded fallback worker below may inspect the process at the same time, so
// serialize vector updates and patch bookkeeping. A loader callback must not
// wait here: if maintenance is already in progress it requests another pass
// and returns, avoiding a lock-order inversion with the Windows loader lock.
void ProcessLoadedDlssgModule(HMODULE mod) {
if (!TryAcquireSRWLockExclusive(&g_provider_maintenance_lock)) {
g_provider_rescan_requested.store(true, std::memory_order_release);
return;
}
RememberDlssgModule(mod);
if (g_enabled.load(std::memory_order_relaxed)) PatchArchGatesInModule(mod);
if (g_temporal_fix.load(std::memory_order_relaxed)) PatchMidpointInModule(mod);
ReleaseSRWLockExclusive(&g_provider_maintenance_lock);
}
void RunProviderMaintenance() {
AcquireSRWLockExclusive(&g_provider_maintenance_lock);
DiscoverDlssgModules();
if (g_enabled.load(std::memory_order_relaxed)) TryPatchDlssgArchGate();
if (g_temporal_fix.load(std::memory_order_relaxed)) TryPatchMidpoint();
ReleaseSRWLockExclusive(&g_provider_maintenance_lock);
}
void RestoreMidpoint() {
if (!g_midpoint_patched.load(std::memory_order_acquire)) return;
for (auto& module : g_midpoint_modules) {
mfgunlock::midpoint::Restore(module.patches, module.allocation);
}
g_midpoint_modules.clear();
g_midpoint_rejected_modules.clear();
g_midpoint_patched.store(false, std::memory_order_release);
}
// ------------------------------------------------- flip metering (sl.dlss_g)
//
// With the gate open, 2x works but 3x/4x freeze the display while audio keeps
// running -- frames are generated and never reach the screen. Blackwell paces
// multi-frame output with hardware flip metering; Ada has none, so the present
// queue waits on something that never happens.
//
// Streamline already ships the fallback. sl.dlss_g/ngx.cpp logs
// "FG1 DLL has been detected: forcing flip-metering off." and writes a flag on
// the DLSS-G context, dropping it onto the software RSYNC pacer in rsync.cpp.
// That is the path Ada needs.
//
// Two things make this impossible to hardcode, both learned the hard way:
//
// 1. The plugin in bin/x64 is usually NOT the one running. Streamline
// OTA-updates its plugins into
// C:\ProgramData\NVIDIA\NGX\models\sl_dlss_g_0\versions\<n>\files\<hash>.dll
// so GetModuleHandleW(L"sl.dlss_g.dll") finds nothing and a name-based patch
// silently does nothing at all -- no error, no log line, no effect.
//
// 2. The flag's offset AND ITS POLARITY differ between builds. The game-folder
// build clears [ctx+0x38bc] to mean "flip metering off"; the OTA build sets
// [ctx+0x44f8] to 1 to mean the same thing. A hardcoded value is a coin flip
// that silently does the opposite half the time.
//
// So derive everything from the binary: find the module carrying the marker
// string, find the code that logs it, and read the (offset, value) the fallback
// itself writes. That pair IS the wanted state, whatever its polarity. Then flip
// every other site writing that offset to match.
constexpr char kFlipMarker[] = "FG1 DLL has been detected";
struct FlipSite {
unsigned char* address;
unsigned char original[7];
unsigned char length;
};
std::atomic_bool g_flip_meter_patched{false};
std::vector<FlipSite> g_flip_meter_sites;
std::atomic<unsigned int> g_flip_meter_offset{0};
std::atomic<unsigned int> g_flip_meter_value{0};
int g_flip_meter_attempts = 0;
constexpr int kMaxFlipMeterAttempts = 4000;
// Records the original bytes before writing, so the instruction can be put back
// exactly as it was. Patches here are either one byte (an immediate flipped in
// place) or seven (a whole store rewritten), never anything else.
bool WriteFlipSite(unsigned char* at, const unsigned char* bytes, size_t length) {
if (length == 0 || length > sizeof(FlipSite::original)) return false;
DWORD old_protect = 0;
if (VirtualProtect(at, length, PAGE_EXECUTE_READWRITE, &old_protect) == 0) return false;
FlipSite site = {};
site.address = at;
site.length = static_cast<unsigned char>(length);
std::memcpy(site.original, at, length);
g_flip_meter_sites.push_back(site);
std::memcpy(at, bytes, length);
DWORD ignored = 0;
VirtualProtect(at, length, old_protect, &ignored);
FlushInstructionCache(GetCurrentProcess(), at, length);
return true;
}
bool ModuleImage(HMODULE mod, unsigned char** out_base, const IMAGE_NT_HEADERS64** out_nt) {
if (mod == nullptr) return false;
auto* base = reinterpret_cast<unsigned char*>(mod);
const auto* dos = reinterpret_cast<const IMAGE_DOS_HEADER*>(base);
if (dos->e_magic != IMAGE_DOS_SIGNATURE) return false;
const auto* nt = reinterpret_cast<const IMAGE_NT_HEADERS64*>(base + dos->e_lfanew);
if (nt->Signature != IMAGE_NT_SIGNATURE) return false;
if (nt->OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC) return false;
*out_base = base;
*out_nt = nt;
return true;
}
// ------------------------------------------- Streamline's own frame ceiling
//
// The plugin starts with its own compiled maximum, then lowers it to the value
// reported by NGX:
//
// BA 03 00 00 00 mov edx, 3
// 3B CA cmp ecx, edx
// 0F 42 D1 cmovb edx, ecx ; edx = min(count, 3)
//
// `ecx` is the device maximum cached by the Streamline wrapper, not the game's
// current request. Most games observe the rewritten NGX gates early enough for
// it to be 5. STALKER 2 does not: its wrapper caches 1, so this CMOV reduces the
// otherwise-valid compiled maximum back to one generated frame. The native UI
// can then expose 3x/4x, but slDLSSGSetOptions rejects either with
// eErrorInvalidState (38).
//
// Turn the conditional move into `cmovb edx, edx` by changing only its ModRM
// byte (D1 -> D2). This is an atomic one-byte code patch and leaves the
// instruction boundary intact. The immediate stays in place as a hard bound:
// old plugins remain capped at their compiled 3 generated frames (4x), while
// newer plugins keep their compiled 5 (6x). The
// opt-in RaiseFrameCeiling setting may still raise an old plugin's immediate,
// but is deliberately separate because doing so is not safe in every game.
constexpr unsigned char kCeilingTarget = 5; // generated frames == 6x
std::atomic_bool g_ceiling_patched{false};
unsigned char* g_ceiling_site = nullptr;
unsigned char g_ceiling_original = 0;
unsigned char g_ceiling_cmov_original = 0;
unsigned int g_ceiling_compiled = 0;
unsigned int g_ceiling_effective = 0;
void PatchFrameCountCeiling(HMODULE mod) {
if (g_ceiling_patched.load(std::memory_order_acquire)) return;
unsigned char* base = nullptr;
const IMAGE_NT_HEADERS64* nt = nullptr;
if (!ModuleImage(mod, &base, &nt)) return;
const unsigned char tail[] = {0x3B, 0xCA, 0x0F, 0x42, 0xD1};
unsigned char* found = nullptr;
size_t hits = 0;
const auto* section = IMAGE_FIRST_SECTION(nt);
for (WORD i = 0; i < nt->FileHeader.NumberOfSections; ++i, ++section) {
if ((section->Characteristics & IMAGE_SCN_MEM_EXECUTE) == 0) continue;
unsigned char* start = base + section->VirtualAddress;
const size_t size = section->Misc.VirtualSize;
if (size < 10) continue;
for (size_t off = 0; off + 10 <= size; ++off) {
if (start[off] != 0xBA) continue;
if (start[off + 2] != 0 || start[off + 3] != 0 || start[off + 4] != 0) continue;
if (std::memcmp(start + off + 5, tail, sizeof(tail)) != 0) continue;
const unsigned char ceiling = start[off + 1];
if (ceiling == 0 || ceiling > 8) continue;
if (found == nullptr) found = start + off;
++hits;
}
}
if (hits != 1 || found == nullptr) {
std::stringstream s;
s << "mfgunlock: found " << hits
<< " frame-count clamps in the DLSS-G plugin (expected 1); leaving them alone.";
reshade::log::message(reshade::log::level::warning, s.str().c_str());
return;
}
DWORD old_protect = 0;
if (VirtualProtect(found, 10, PAGE_EXECUTE_READWRITE, &old_protect) == 0) return;
g_ceiling_site = found;
g_ceiling_original = found[1];
g_ceiling_cmov_original = found[9];
g_ceiling_compiled = found[1];
g_ceiling_effective = g_ceiling_compiled;
if (g_raise_ceiling.load(std::memory_order_relaxed) && found[1] < kCeilingTarget) {
found[1] = kCeilingTarget;
g_ceiling_effective = kCeilingTarget;
}
// cmovb edx, ecx -> cmovb edx, edx: same three-byte instruction, no lowering.
found[9] = 0xD2;
DWORD ignored = 0;
VirtualProtect(found, 10, old_protect, &ignored);
FlushInstructionCache(GetCurrentProcess(), found, 10);
g_ceiling_patched.store(true, std::memory_order_release);
mfgunlock::framecount::g_advertised_max_generated.store(g_ceiling_effective,
std::memory_order_release);
std::stringstream s;
s << "mfgunlock: stopped the DLSS-G plugin from lowering its compiled ceiling of "
<< g_ceiling_compiled << " generated frame(s) to the stale NGX device value";
if (g_ceiling_effective != g_ceiling_compiled) {
s << "; RaiseFrameCeiling also changed the hard bound to " << g_ceiling_effective;
}
s << " (effective maximum " << (g_ceiling_effective + 1) << "x).";
reshade::log::message(reshade::log::level::info, s.str().c_str());
}
void RestoreFrameCountCeiling() {
if (!g_ceiling_patched.load(std::memory_order_acquire)) return;
if (g_ceiling_site == nullptr) return;
DWORD old_protect = 0;
if (VirtualProtect(g_ceiling_site, 10, PAGE_EXECUTE_READWRITE, &old_protect) != 0) {
g_ceiling_site[9] = g_ceiling_cmov_original;
g_ceiling_site[1] = g_ceiling_original;
DWORD ignored = 0;
VirtualProtect(g_ceiling_site, 10, old_protect, &ignored);
FlushInstructionCache(GetCurrentProcess(), g_ceiling_site, 10);
}
g_ceiling_site = nullptr;
g_ceiling_original = 0;
g_ceiling_cmov_original = 0;
g_ceiling_compiled = 0;
g_ceiling_effective = 0;
mfgunlock::framecount::g_advertised_max_generated.store(0, std::memory_order_release);
g_ceiling_patched.store(false, std::memory_order_release);
}
// Handles the DLSS-G Streamline plugin wherever it was loaded from. Returns true
// once a module has been dealt with, so the caller stops scanning.
bool TryPatchFlipMeteringInModule(HMODULE mod) {
unsigned char* base = nullptr;
const IMAGE_NT_HEADERS64* nt = nullptr;
if (!ModuleImage(mod, &base, &nt)) return false;
// 1. Is this the DLSS-G plugin? The marker string identifies it regardless of
// what the OTA layer decided to call the file.
const size_t marker_len = sizeof(kFlipMarker) - 1;
const unsigned char* marker = nullptr;
const auto* section = IMAGE_FIRST_SECTION(nt);
for (WORD i = 0; i < nt->FileHeader.NumberOfSections && marker == nullptr; ++i, ++section) {
if ((section->Characteristics & IMAGE_SCN_MEM_READ) == 0) continue;
unsigned char* start = base + section->VirtualAddress;
const size_t size = section->Misc.VirtualSize;
if (size < marker_len) continue;
for (size_t off = 0; off + marker_len <= size; ++off) {
if (std::memcmp(start + off, kFlipMarker, marker_len) == 0) {
marker = start + off;
break;
}
}
}
if (marker == nullptr) return false;
++g_flip_meter_attempts;
// 2. Find the code referencing it, then read the (offset, value) the fallback
// writes: C6 /r disp32 imm8 == mov byte ptr [reg+disp32], imm8.
unsigned int want_offset = 0;
int want_value = -1;
section = IMAGE_FIRST_SECTION(nt);
for (WORD i = 0; i < nt->FileHeader.NumberOfSections && want_value < 0; ++i, ++section) {
if ((section->Characteristics & IMAGE_SCN_MEM_EXECUTE) == 0) continue;
unsigned char* start = base + section->VirtualAddress;
const size_t size = section->Misc.VirtualSize;
if (size < 8) continue;
for (size_t off = 0; off + 8 <= size && want_value < 0; ++off) {
// lea reg, [rip+disp32] pointing at the marker string
if (!(start[off] == 0x48 || start[off] == 0x4C)) continue;
if (start[off + 1] != 0x8D) continue;
if ((start[off + 2] & 0xC7) != 0x05) continue;
int disp = 0;
std::memcpy(&disp, start + off + 3, sizeof(disp));
if (start + off + 7 + disp != marker) continue;
const size_t window = 0x200;
const size_t limit = (off + window < size) ? (off + window) : size;
for (size_t w = off; w + 7 <= limit; ++w) {
if (start[w] != 0xC6) continue;
if (start[w + 1] < 0x80 || start[w + 1] > 0xBF) continue; // mod=10, disp32
unsigned int field = 0;
std::memcpy(&field, start + w + 2, sizeof(field));
const unsigned char imm = start[w + 6];
if (field <= 0x100 || field >= 0x20000) continue;
if (imm > 1) continue;
want_offset = field;
want_value = imm;
break;
}
}