From 926cee08936ede0a194b0534559a309279bf64d9 Mon Sep 17 00:00:00 2001 From: wilsjo2 <97138003+wilsjo2@users.noreply.github.com> Date: Sat, 5 Sep 2026 20:18:34 +1000 Subject: [PATCH 1/7] DLSS-NR: add guarded pre-SR multipass --- OptiScaler.ini | 14 + OptiScaler/Config.cpp | 3 + OptiScaler/Config.h | 13 +- OptiScaler/dlssnr/DlssNrFeature_Dx12.h | 11 +- OptiScaler/dlssnr/DlssNr_Menu.cpp | 50 +- OptiScaler/dlssnr/design/pre-sr-multipass.md | 49 ++ OptiScaler/inputs/NVNGX_DLSS_Dx12.cpp | 14 +- OptiScaler/shaders/dlssnr/DlssNr_Common.h | 10 + OptiScaler/shaders/dlssnr/DlssNr_Dx12.cpp | 554 ++++++++++++++++--- OptiScaler/shaders/dlssnr/DlssNr_Dx12.h | 16 +- OptiScaler/upscalers/IFeature_Dx11wDx12.cpp | 6 +- OptiScaler/upscalers/IFeature_VkwDx12.cpp | 6 +- 12 files changed, 639 insertions(+), 107 deletions(-) create mode 100644 OptiScaler/dlssnr/design/pre-sr-multipass.md diff --git a/OptiScaler.ini b/OptiScaler.ini index 56e4cd193..06b00e860 100644 --- a/OptiScaler.ini +++ b/OptiScaler.ini @@ -1561,6 +1561,20 @@ ToggleKey=auto ; true or false - Default (auto) is false Enabled=auto +; Run Neural Rendering over the colour input immediately before Super Resolution, at render +; resolution. Ray Reconstruction remains on the supported post-upscale path. false preserves the +; normal v0.2.0 placement. Padded/offset dynamic-resolution inputs safely fall back post-upscale. +; true or false - Default (auto) is false +RunBeforeSR=auto + +; Number of sequential model layers between one encode and one final composition. Each extra layer +; consumes the preceding model output and owns a separate persistent feature/history. 1 is normal; +; 2 and 3 are deliberately over-processed and cost almost exactly 2x and 3x the model time. The final +; answer is composed once against the immutable base proxy, so colour and transfer do not compound. +; Values are clamped to 1..3. +; Default (auto) is 1 +Passes=auto + ; How far the frame moves toward the model's picture. Its answer is not added to the frame: it is a ; complete picture, rescaled so its luminance sits where the original says it should, and this blends ; between the two. 0 gives back exactly what the upscaler produced, 1 is the model's picture, and above diff --git a/OptiScaler/Config.cpp b/OptiScaler/Config.cpp index a16587a47..b9e4ef751 100644 --- a/OptiScaler/Config.cpp +++ b/OptiScaler/Config.cpp @@ -317,6 +317,7 @@ bool Config::Reload(std::filesystem::path iniPath) // --- DLSS 5 Neural Rendering (OptiScaler/dlssnr) --- DlssNrEnabled.set_from_config(readBool("DlssNr", "Enabled")); + DlssNrRunBeforeSr.set_from_config(readBool("DlssNr", "RunBeforeSR")); DlssNrToggleKey.set_from_config(readInt("DlssNr", "ToggleKey")); DlssNrTransferStrength.set_from_config(readFloat("DlssNr", "TransferStrength")); DlssNrColourStrength.set_from_config(readFloat("DlssNr", "ColourStrength")); @@ -1198,6 +1199,8 @@ bool Config::SaveIni() // --- DLSS 5 Neural Rendering (OptiScaler/dlssnr) --- ini.SetValue("DlssNr", "Enabled", GetBoolValue(Instance()->DlssNrEnabled.value_for_config()).c_str()); + ini.SetValue("DlssNr", "RunBeforeSR", + GetBoolValue(Instance()->DlssNrRunBeforeSr.value_for_config()).c_str()); { auto toggle = Instance()->DlssNrToggleKey.value_for_config(); ini.SetValue("DlssNr", "ToggleKey", GetIntValue(toggle, toggle > 0).c_str()); diff --git a/OptiScaler/Config.h b/OptiScaler/Config.h index 8f4329234..1a12f5cc1 100644 --- a/OptiScaler/Config.h +++ b/OptiScaler/Config.h @@ -257,6 +257,9 @@ class Config // DLSS Neural Rendering: a detail-synthesis pass over the upscaler's output. Off by default -- it is // an undocumented feature driven directly through its snippet, not something NVIDIA exposes. CustomOptional DlssNrEnabled { false }; + // Run the NR pass on the upscaler's colour input, at render resolution, immediately before SR. + // Off preserves the v0.2.0 post-upscale placement. + CustomOptional DlssNrRunBeforeSr { false }; // Toggles the pass in game. Unbound by default -- a key that does something unexpected is worse // than one that does nothing. CustomOptional DlssNrToggleKey { UnboundKey }; @@ -464,15 +467,17 @@ class Config // picture that had been tuned came back wrong for a reason nothing on screen explained. CustomOptional DlssNrScanTrim { 1.0f }; - // How many times to run the model over the same frame, each pass fed the previous one's answer. + // How many sequential model layers to run between one encode and one final composition. Each extra + // layer consumes the preceding model output and owns a persistent feature/history. The implementation + // deliberately caps this at three and never evaluates a feature on the command list that created it. // // 1 is what the model was trained for and what every published number describes. Above that it // is being asked to enhance its own output, which is outside its training distribution: detail - // compounds, and so does anything it got wrong. Two often looks richer. Four usually looks - // synthetic. Eight is there because somebody will want to see it. + // compounds, and so does anything it got wrong. Two often looks richer; three is the guarded + // ceiling because further layers converge while still paying the full cost. // // The cost is exactly linear -- the model is 98% of the frame's expense and every pass pays it - // again -- so 8 costs eight times, near enough. There is no shortcut and no amortisation: the + // again -- so 3 costs three times, near enough. There is no shortcut and no amortisation: the // passes are sequential and each one needs the last one's output. CustomOptional DlssNrPasses { 1 }; diff --git a/OptiScaler/dlssnr/DlssNrFeature_Dx12.h b/OptiScaler/dlssnr/DlssNrFeature_Dx12.h index 861d5e6d3..6dfcecab1 100644 --- a/OptiScaler/dlssnr/DlssNrFeature_Dx12.h +++ b/OptiScaler/dlssnr/DlssNrFeature_Dx12.h @@ -19,6 +19,8 @@ class Config; namespace DlssNr { +inline constexpr unsigned int MaxPassCount = 3; + // The model runs immediately after the game's upscaler, before the interface is drawn. It is shown a // display-referred proxy of that frame -- the sort of picture it was trained on -- and its answer is // composed back over the untouched original. @@ -31,7 +33,14 @@ namespace DlssNr // State::currentCommandQueue only exists once a D3D12 swapchain has been created, which a Vulkan // game never does -- so without this the pass runs and never reports what it cost. void EvaluateAfterUpscale(ID3D12GraphicsCommandList* cmdList, NVSDK_NGX_Parameter* params, - ID3D12CommandQueue* timingQueue = nullptr); + ID3D12CommandQueue* timingQueue = nullptr, bool forcePost = false, + unsigned long long submissionEpoch = 0); + +// Runs the same pass over Color immediately before Super Resolution consumes it. The call is a no-op +// unless RunBeforeSR is enabled. Color is returned in its original readable state. +void EvaluateBeforeUpscale(ID3D12GraphicsCommandList* cmdList, NVSDK_NGX_Parameter* params, + ID3D12CommandQueue* timingQueue = nullptr, + unsigned long long submissionEpoch = 0); diff --git a/OptiScaler/dlssnr/DlssNr_Menu.cpp b/OptiScaler/dlssnr/DlssNr_Menu.cpp index 54434ddfb..aca9eab5e 100644 --- a/OptiScaler/dlssnr/DlssNr_Menu.cpp +++ b/OptiScaler/dlssnr/DlssNr_Menu.cpp @@ -94,11 +94,23 @@ void RenderMenu(Config* config, float menuResScale) if (ImGui::Checkbox("Enable Neural Rendering", &enabled)) config->DlssNrEnabled = enabled; - HelpMarker("Synthesises detail in the upscaler's output, before frame generation sees it." - "\n\nNeeds two similarly named files beside OptiScaler, one character apart:" - "\n nvngx_dlssnr.dll NVIDIA's model (~165 MB) -- you supply it" - "\n nvngx.dll_dlssnr.dll the forwarder (~13 KB) -- ships in this package" - "\nUndocumented and driven directly, so none of this is officially supported."); + HelpMarker("Synthesises detail in the upscaler's frame, before frame generation sees it." + "\n\nNeeds two similarly named files beside OptiScaler, one character apart:" + "\n nvngx_dlssnr.dll NVIDIA's model (~165 MB) -- you supply it" + "\n nvngx.dll_dlssnr.dll the forwarder (~13 KB) -- ships in this package" + "\nUndocumented and driven directly, so none of this is officially supported."); + + bool beforeSr = config->DlssNrRunBeforeSr.value_or_default(); + if (ImGui::Checkbox("Apply before Super Resolution", &beforeSr)) + config->DlssNrRunBeforeSr = beforeSr; + + HelpMarker("Runs Neural Rendering on the render-resolution colour input immediately before" + "\nSuper Resolution, so SR temporally accumulates and upscales the enhanced frame." + "\n\nRay Reconstruction is deliberately excluded: its input contract differs and" + "\ncontinues to use the post-upscale Neural Rendering path. Padded or offset" + "\ndynamic-resolution inputs also fall back post-upscale for safety." + "\n\nThis placement control currently applies to the Direct3D 12 path and its" + "\nDirect3D 11/Vulkan bridges; native Vulkan keeps the post-upscale path."); // The toggle can be bound to a key, and nobody would think to look for it under Keybinds // unless told. Dimmed, because it is a note rather than a setting. @@ -178,6 +190,34 @@ void RenderMenu(Config* config, float menuResScale) ImGui::Spacing(); ImGui::PushItemWidth(220.0f * menuResScale); + ImGui::SeparatorText("Cost"); + + { + int passes = (int) std::clamp(config->DlssNrPasses.value_or_default(), 1u, + DlssNr::MaxPassCount); + const ImVec4 colour = passes <= 1 ? ImVec4(0.35f, 0.88f, 0.38f, 1.0f) + : passes == 2 ? ImVec4(0.95f, 0.70f, 0.20f, 1.0f) + : ImVec4(0.92f, 0.30f, 0.25f, 1.0f); + + ImGui::PushStyleColor(ImGuiCol_Text, colour); + ImGui::PushStyleColor(ImGuiCol_SliderGrab, colour); + + if (ImGui::SliderInt("Model passes", &passes, 1, (int) DlssNr::MaxPassCount, + passes == 1 ? "%d (normal)" : "%dx model cost")) + config->DlssNrPasses = (uint32_t) std::clamp(passes, 1, (int) DlssNr::MaxPassCount); + + ImGui::PopStyleColor(2); + + HelpMarker("Runs sequential model layers between one encode and one final composition." + "\nEach additional layer consumes the previous layer's model output and owns" + "\na separate persistent feature and temporal history." + "\n\nThe base proxy stays immutable and the final answer is composed against it" + "\nonce, so colour and transfer strength do not compound. Local tone is applied" + "\nonly by the first layer." + "\n\nCost scales almost linearly. Two is the common 'deep fried' look; three is" + "\nthe guarded ceiling because later layers converge while cost and artifacts grow."); + } + // Any percentage, rather than a handful of steps somebody chose in advance. The lower bound // is 25%: below that the model is working on so little of the picture that its answer no // longer survives being enlarged onto it. diff --git a/OptiScaler/dlssnr/design/pre-sr-multipass.md b/OptiScaler/dlssnr/design/pre-sr-multipass.md new file mode 100644 index 000000000..600f06368 --- /dev/null +++ b/OptiScaler/dlssnr/design/pre-sr-multipass.md @@ -0,0 +1,49 @@ +# Pre-SR placement and persistent multipass + +This experimental branch adds two opt-in controls to the `[DlssNr]` section: + +- `RunBeforeSR=true` runs Neural Rendering on the colour input immediately before Super Resolution. + The default is `false`, preserving the v0.2.0 post-upscale seam. Ray Reconstruction/DLSSD is + deliberately forced to remain post-upscale because its input contract is not compatible with the + PR #6 pre-SR path. +- `Passes=N` selects one to three sequential model layers. The default is `1`. + +## Multipass lifetime and data flow + +Every layer owns a persistent NGX feature and temporal history. A missing feature is created in one +submission epoch and remains pending until that epoch changes; no Neural Rendering feature is +evaluated on the command-list recording that created it. Native DX12 uses the wrapped Present count, +while the DX11/Vulkan bridges supply their post-submit frame counter. At most one extra feature is +created per submitted frame. A failed extra creation is latched and the ready contiguous prefix +remains active, rather than reusing the main feature or retrying every frame. + +The frame is encoded once. Its base proxy remains immutable while model answers ping-pong through two +same-format, same-size resources: + +`base -> A -> B -> A` (as needed for one, two, or three layers) + +The final answer is composed once against the immutable base. This keeps matched-residual transfer +cumulative (`final - base`) without compounding colour/transfer settings. Local tone is applied by the +first model layer only. A camera cut resets every active layer; a newly created extra layer is also +reset on its first evaluation. + +Features and scratch resources are parked for deferred release on tuning, raster, format, placement, +or pass-count changes so in-flight frame-generation work cannot retain freed objects. + +## Resource-state rules + +Post-SR output uses the existing output-arrival state. Pre-SR colour arrives and is returned as a +non-pixel shader resource. If Color has no UAV flag, composition writes to an owned scratch texture and +copies back instead of binding an illegal UAV. + +## Guardrails + +- Pass count is clamped to `1..3`; historical testing found later layers converged while cost and + artifacts continued to grow. +- The driver-proxy backend remains single-pass and logs the effective fallback. +- A padded, offset, or max-sized dynamic-resolution Color allocation falls back to post-SR until the + colour codec supports subrect origins; this avoids processing stale pixels or reporting a false model + resolution. +- Placement is part of the rebuild key even when pre/post surfaces happen to share dimensions and + format (for example DLAA). +- Working scales from 25% through 200% remain supported; the ping-pong resources use model-work size. diff --git a/OptiScaler/inputs/NVNGX_DLSS_Dx12.cpp b/OptiScaler/inputs/NVNGX_DLSS_Dx12.cpp index cb41b9f79..0ea3042ae 100644 --- a/OptiScaler/inputs/NVNGX_DLSS_Dx12.cpp +++ b/OptiScaler/inputs/NVNGX_DLSS_Dx12.cpp @@ -1157,6 +1157,11 @@ NVSDK_NGX_API NVSDK_NGX_Result NVSDK_NGX_D3D12_EvaluateFeature(ID3D12GraphicsCom { LOG_DEBUG("Passthrough to native DLSS EvaluateFeature for handle {}", handleId); + // Pre-SR placement is valid only for Super Resolution. Ray Reconstruction carries a + // different set of inputs and stays on the post-upscale path. + if (feature == NVSDK_NGX_Feature_SuperSampling) + DlssNr::EvaluateBeforeUpscale(InCmdList, InParameters); + NVSDK_NGX_Result result = NVNGXProxy::D3D12_EvaluateFeature()(InCmdList, InFeatureHandle, InParameters, InCallback); LOG_DEBUG("Native DLSS EvaluateFeature result: 0x{:X}", (uint32_t) result); @@ -1167,7 +1172,8 @@ NVSDK_NGX_API NVSDK_NGX_Result NVSDK_NGX_D3D12_EvaluateFeature(ID3D12GraphicsCom // motion vectors too, and its handle can reach here because the branch above does not // return, so filtering on the parameter block alone would run the model twice a frame. if (result == NVSDK_NGX_Result_Success && feature != NVSDK_NGX_Feature_FrameGeneration) - DlssNr::EvaluateAfterUpscale(InCmdList, InParameters); + DlssNr::EvaluateAfterUpscale(InCmdList, InParameters, nullptr, + feature == NVSDK_NGX_Feature_RayReconstruction); return result; } @@ -1189,12 +1195,16 @@ NVSDK_NGX_API NVSDK_NGX_Result NVSDK_NGX_D3D12_EvaluateFeature(ID3D12GraphicsCom if (lastDlssgCameraFar.has_value()) InParameters->Set("DLSSG.CameraFar", lastDlssgCameraFar.value()); + if (feature == NVSDK_NGX_Feature_SuperSampling) + DlssNr::EvaluateBeforeUpscale(InCmdList, InParameters); + // OptiScaler internal handling const NVSDK_NGX_Result optiResult = TryEvaluateOptiFeature(InCmdList, InFeatureHandle, InParameters, InCallback); // Same pass, for OptiScaler's own upscalers rather than native DLSS. if (optiResult == NVSDK_NGX_Result_Success && feature != NVSDK_NGX_Feature_FrameGeneration) - DlssNr::EvaluateAfterUpscale(InCmdList, InParameters); + DlssNr::EvaluateAfterUpscale(InCmdList, InParameters, nullptr, + feature == NVSDK_NGX_Feature_RayReconstruction); return optiResult; } diff --git a/OptiScaler/shaders/dlssnr/DlssNr_Common.h b/OptiScaler/shaders/dlssnr/DlssNr_Common.h index 72f8e48eb..61e6ac53f 100644 --- a/OptiScaler/shaders/dlssnr/DlssNr_Common.h +++ b/OptiScaler/shaders/dlssnr/DlssNr_Common.h @@ -70,6 +70,16 @@ struct DlssNrFrameInfo // washed out and banded. bool ColourIsLinearHdr = true; + // The SR colour input arrives readable, whereas a completed upscaler output normally arrives as + // a UAV. The DX12 pass uses this to preserve the caller's state and to fall back through a copy + // when a pre-SR colour resource was not created with UAV support. + bool BeforeUpscale = false; + + // Submission epoch supplied by the caller. Native DX12 uses the wrapped swapchain Present count; + // the DX11/Vulkan bridges use their successfully submitted frame counter. A feature created in an + // epoch is never evaluated until this value changes. + unsigned long long SubmissionEpoch = 0; + // The game's own exposure: a 1x1 texture holding, in the SDK's words, "the final exposure scale". // // This is the number that makes a cave and a field comparable, and it is the reason a fixed paper diff --git a/OptiScaler/shaders/dlssnr/DlssNr_Dx12.cpp b/OptiScaler/shaders/dlssnr/DlssNr_Dx12.cpp index bb74ef57a..64ca1a0e3 100644 --- a/OptiScaler/shaders/dlssnr/DlssNr_Dx12.cpp +++ b/OptiScaler/shaders/dlssnr/DlssNr_Dx12.cpp @@ -183,6 +183,8 @@ struct NrState NVSDK_NGX_Parameter* capabilityParams = nullptr; void* feature = nullptr; + bool featurePendingSubmission = false; + unsigned long long featureCreateEpoch = 0; // A feature per extra pass, each with its own temporal history. // @@ -196,13 +198,23 @@ struct NrState // that. A feature apiece can, because each carries its own history. // // Indexed by pass, so [0] is unused and the first extra pass is [1]. Wasting one pointer keeps - // every index here equal to the pass number it belongs to. - void* passFeature[4] = {}; + // every index here equal to the pass number it belongs to. Extra features are created on a + // build-only invocation and first evaluated on a later command list. + void* passFeature[DlssNr::MaxPassCount] = {}; + bool passNeedsReset[DlssNr::MaxPassCount] = {}; + bool passCreateFailed[DlssNr::MaxPassCount] = {}; + bool passPendingSubmission[DlssNr::MaxPassCount] = {}; + unsigned long long passCreateEpoch[DlssNr::MaxPassCount] = {}; // The model cannot read and write one resource, so the frame is staged through these. ID3D12Resource* colorCopy = nullptr; ID3D12Resource* output = nullptr; + // The second half of the model-output ping-pong. The base proxy stays immutable: pass 0 writes + // output (A), pass 1 writes this (B), and pass 2 writes A again. Only the final answer is composed. + ID3D12Resource* passScratch = nullptr; + bool passScratchFailed = false; + // The frame as the upscaler wrote it. The resolve adds the model's edit to this rather than // reconstructing it by inverting the tone curve, which is what turned every light in the frame into // a string of coloured cells. @@ -317,6 +329,7 @@ struct NrState unsigned int width = 0; unsigned int height = 0; + bool beforeUpscale = false; bool reset = true; // Dimensions of the guides as the upscaler handed them over, kept for the present path, which runs @@ -720,15 +733,24 @@ void ReleaseSurfacesIfFormatChanged(DXGI_FORMAT needed) ForgetCalibration(); ParkNrFeature(g_nr.feature); + g_nr.featurePendingSubmission = false; // The extras go with it: they were built for this raster and this tuning too. - for (void*& f : g_nr.passFeature) - ParkNrFeature(f); + for (unsigned int i = 1; i < DlssNr::MaxPassCount; ++i) + { + ParkNrFeature(g_nr.passFeature[i]); + g_nr.passNeedsReset[i] = false; + g_nr.passCreateFailed[i] = false; + g_nr.passPendingSubmission[i] = false; + } for (ID3D12Resource** r : - { &g_nr.output, &g_nr.colorCopy, &g_nr.hdrCopy, &g_nr.colorSmall }) + { &g_nr.output, &g_nr.passScratch, &g_nr.colorCopy, &g_nr.hdrCopy, &g_nr.colorSmall, + &g_nr.outputNative }) ParkNrResource(*r); + g_nr.passScratchFailed = false; + g_nr.reset = true; } @@ -1505,18 +1527,23 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c ID3D12Resource* target = output; - // The state the upscaler left the output in. Every upscaler in this tree ends Evaluate by moving - // the output to OutputResourceBarrier when the user set it (FFXFeature_Dx12.cpp:606 and the FSR2 / - // XeSS equivalents), and leaves it in UNORDERED_ACCESS -- what its own compute wrote -- when they - // did not. This pass then reads and writes the output as a UAV, so it normalises to that here and - // restores the arrival state before every exit. When the config is unset the two states are equal - // and Barrier() skips the no-op, so the default path is byte-identical. + // A completed upscaler output normally arrives as a UAV. The pre-SR colour input instead arrives + // readable. Track every transition so both paths return the resource exactly as their caller gave + // it to us; a pre-SR resource without UAV support is written through a scratch-and-copy fallback. const D3D12_RESOURCE_STATES outputArrival = - Config::Instance()->OutputResourceBarrier.has_value() + frame.BeforeUpscale + ? (Config::Instance()->ColorResourceBarrier.has_value() + ? (D3D12_RESOURCE_STATES) Config::Instance()->ColorResourceBarrier.value() + : D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE) + : Config::Instance()->OutputResourceBarrier.has_value() ? (D3D12_RESOURCE_STATES) Config::Instance()->OutputResourceBarrier.value() : D3D12_RESOURCE_STATE_UNORDERED_ACCESS; - - Barrier(cmdList, target, outputArrival, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); + D3D12_RESOURCE_STATES targetState = outputArrival; + const auto TransitionTarget = [&](D3D12_RESOURCE_STATES to) + { + Barrier(cmdList, target, targetState, to); + targetState = to; + }; ID3D12Device* device = nullptr; @@ -1529,6 +1556,8 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c const D3D12_RESOURCE_DESC desc = target->GetDesc(); const auto width = (unsigned int) desc.Width; const auto height = desc.Height; + const bool targetSupportsUav = + (desc.Flags & D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS) != 0; // Depth and motion vectors are the upscaler's inputs and so are at render resolution, while colour // and output are at display resolution. The model takes that as a subrect per resource rather than @@ -1658,11 +1687,27 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c const auto workWidth = (unsigned int) (width * workScale + 0.5f); const auto workHeight = (unsigned int) (height * workScale + 0.5f); const bool reduced = workWidth != width || workHeight != height; + const unsigned int configuredPasses = + std::clamp(cfg.DlssNrPasses.value_or_default(), 1u, DlssNr::MaxPassCount); + const bool proxyBackend = cfg.DlssNrUseProxy.value_or_default(); + const unsigned int requestedPasses = proxyBackend ? 1u : configuredPasses; + + if (proxyBackend && configuredPasses > 1) + { + static bool warnedProxyPasses = false; + if (!warnedProxyPasses) + { + warnedProxyPasses = true; + LOG_WARN("DLSS-NR: the driver-proxy backend supports one pass; Passes={} is using 1", + configuredPasses); + } + } ReleaseSurfacesIfFormatChanged(desc.Format); const bool resolutionChanged = g_nr.width != width || g_nr.height != height || g_nr.workWidth != workWidth || g_nr.workHeight != workHeight; + const bool placementChanged = g_nr.feature != nullptr && g_nr.beforeUpscale != frame.BeforeUpscale; // The model reads its tuning once, while the feature is built, so a changed setting only takes // effect when the feature is rebuilt. TuningMatchesFeature was written to notice that and then @@ -1670,24 +1715,35 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c // else -- a resolution change -- happened to force a rebuild by accident. const bool tuningChanged = !TuningMatchesFeature(cfg); - if (g_nr.feature != nullptr && (resolutionChanged || tuningChanged)) + if (g_nr.feature != nullptr && (resolutionChanged || tuningChanged || placementChanged)) { // Parked rather than released: with frame generation the GPU can still be several frames // deep in work that references all of it. ParkNrFeature(g_nr.feature); + g_nr.featurePendingSubmission = false; - for (void*& f : g_nr.passFeature) - ParkNrFeature(f); + for (unsigned int i = 1; i < DlssNr::MaxPassCount; ++i) + { + ParkNrFeature(g_nr.passFeature[i]); + g_nr.passNeedsReset[i] = false; + g_nr.passCreateFailed[i] = false; + g_nr.passPendingSubmission[i] = false; + } - // Only a resolution change invalidates the scratch textures. Tuning does not, and throwing - // them away for it would mean a reallocation every time a slider moves. - if (resolutionChanged) + // Resolution and seam changes invalidate the scratch state. Tuning does not, and throwing + // resources away for it would mean a reallocation every time a slider moves. + if (resolutionChanged || placementChanged) { + if (placementChanged) + ForgetCalibration(); + ParkNrResource(g_nr.output); + ParkNrResource(g_nr.passScratch); ParkNrResource(g_nr.colorCopy); ParkNrResource(g_nr.hdrCopy); ParkNrResource(g_nr.colorSmall); ParkNrResource(g_nr.outputNative); + g_nr.passScratchFailed = false; } } @@ -1700,6 +1756,22 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c g_nr.workHeight = workHeight; } + if (requestedPasses == 1) + { + // Reclaim the extra raster and clear its failure latch. Raising the count later gets one fresh + // allocation attempt; holding a failing allocation at two must not retry it every frame. + ParkNrResource(g_nr.passScratch); + g_nr.passScratchFailed = false; + } + else if (g_nr.passScratch == nullptr && !g_nr.passScratchFailed) + { + g_nr.passScratch = CreateScratch(device, desc.Format, workWidth, workHeight); + g_nr.passScratchFailed = g_nr.passScratch == nullptr; + + if (g_nr.passScratchFailed) + LOG_ERROR("DLSS-NR: could not allocate the model-output ping-pong; extra passes are disabled"); + } + if (reduced && g_nr.colorSmall == nullptr) g_nr.colorSmall = CreateScratch(device, desc.Format, workWidth, workHeight); @@ -1772,6 +1844,7 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c if (g_nr.feature == nullptr) { + g_nr.featurePendingSubmission = false; g_nr.failed = true; g_nr.reason = "the model would not initialise"; const auto initResult = (unsigned int) (g_nr.lastInit != nullptr ? *g_nr.lastInit : 0); @@ -1787,10 +1860,16 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c g_nr.width = width; g_nr.height = height; + g_nr.beforeUpscale = frame.BeforeUpscale; g_nr.reset = true; + g_nr.featurePendingSubmission = true; + g_nr.featureCreateEpoch = frame.SubmissionEpoch; RecordBuiltTuning(cfg); - LOG_INFO("DLSS-NR running at {}x{}, guides {}x{} (preset {}, intensity {}, style {})", width, - height, guideWidth, guideHeight, g_nr.builtPreset, g_nr.builtIntensity, g_nr.builtStyle); + LOG_INFO("DLSS-NR running {} SR: target {}x{}, model {}x{}, guides {}x{} " + "(preset {}, intensity {}, style {}, build epoch {})", + frame.BeforeUpscale ? "before" : "after", width, height, workWidth, workHeight, + guideWidth, guideHeight, g_nr.builtPreset, g_nr.builtIntensity, g_nr.builtStyle, + frame.SubmissionEpoch); // Creating and evaluating a feature in the same command list is the dice-roll that hung the // GPU (every crash died on a creation frame). The creation goes through the game's own submit @@ -1805,23 +1884,134 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c return; } + // A later function call is not proof that the command list containing CreateFeature was + // submitted: some engines record more than one upscale on the same list. Native DX12 supplies + // the wrapped Present count and the bridges supply their post-Execute frame counter, so an epoch + // change is the first point at which evaluating the feature is safe. + if (g_nr.featurePendingSubmission) + { + if (frame.SubmissionEpoch == g_nr.featureCreateEpoch) + { + device->Release(); + return; + } + + g_nr.featurePendingSubmission = false; + LOG_INFO("DLSS-NR: primary feature ready after submitted epoch {}", g_nr.featureCreateEpoch); + } + + // Park no-longer-requested feature histories immediately (their actual release remains deferred), + // and clear their failure latch so a later 1 -> N change is a deliberate retry. + for (unsigned int pass = 1; pass < DlssNr::MaxPassCount; ++pass) + { + if (pass >= requestedPasses) + { + ParkNrFeature(g_nr.passFeature[pass]); + g_nr.passNeedsReset[pass] = false; + g_nr.passCreateFailed[pass] = false; + g_nr.passPendingSubmission[pass] = false; + } + } + + // Do not create another feature, and do not evaluate any feature, while a requested layer still + // belongs to the current submission epoch. This keeps multiple upscaler evaluations recorded on + // one command list from recreating the historical create/evaluate GPU hang. + for (unsigned int pass = 1; pass < requestedPasses; ++pass) + { + if (!g_nr.passPendingSubmission[pass]) + continue; + + if (frame.SubmissionEpoch == g_nr.passCreateEpoch[pass]) + { + device->Release(); + return; + } + + g_nr.passPendingSubmission[pass] = false; + LOG_INFO("DLSS-NR: feature for pass {} ready after submitted epoch {}", pass + 1, + g_nr.passCreateEpoch[pass]); + } + + // Build at most one missing extra feature on this invocation and evaluate nothing afterwards. + // NGX feature creation records work on the supplied command list; evaluating that feature before + // the list has been submitted is the creation-frame GPU hang that caused the old multi-pass path + // to be removed. A new feature therefore gets an entire build-only frame and starts next time. + if (g_nr.passScratch != nullptr) + { + for (unsigned int pass = 1; pass < requestedPasses; ++pass) + { + if (g_nr.passFeature[pass] != nullptr) + continue; + + if (g_nr.passCreateFailed[pass]) + break; + + auto snippet = Util::FindFilePath(g_dllDir, "nvngx_dlssnr.dll"); + if (!snippet.has_value()) + snippet = Util::FindFilePath(Util::ExePath().remove_filename(), "nvngx_dlssnr.dll"); + + if (!snippet.has_value()) + { + g_nr.passCreateFailed[pass] = true; + LOG_ERROR("DLSS-NR: pass {} feature not built because nvngx_dlssnr.dll disappeared", + pass + 1); + } + else + { + SetExtras(cfg, nullptr, nullptr, 0, 0, 0, 0); + g_nr.passFeature[pass] = g_nr.create( + snippet->wstring().c_str(), State::Instance().NVNGX_ApplicationDataPath.c_str(), + device, cmdList, g_nr.capabilityParams, workWidth, workHeight, + (int) cfg.DlssNrPreset.value_or_default(), cfg.DlssNrIntensity.value_or_default(), + (int) cfg.DlssNrStyle.value_or_default(), + cfg.DlssNrLocalStructure.value_or_default(), + // Local tone belongs to the frame and is applied by pass zero only. + 0.0f, cfg.DlssNrSkinStructure.value_or_default(), + cfg.DlssNrAutoMask.value_or_default() ? 1 : 0, 1); + + if (g_nr.passFeature[pass] != nullptr) + { + g_nr.passNeedsReset[pass] = true; + g_nr.passPendingSubmission[pass] = true; + g_nr.passCreateEpoch[pass] = frame.SubmissionEpoch; + LOG_INFO("DLSS-NR: feature for pass {} built at epoch {}; waiting for submission", + pass + 1, frame.SubmissionEpoch); + } + else + { + g_nr.passPendingSubmission[pass] = false; + g_nr.passCreateFailed[pass] = true; + LOG_ERROR("DLSS-NR: feature for pass {} failed to build; using {} ready pass(es)", + pass + 1, pass); + } + } + + device->Release(); + return; + } + } + // The upscaler has just written this, so it is a UAV. The model needs it readable. // Whether the buffer the upscaler just wrote is linear HDR or an already tone-mapped picture is not // something to assume: the game says so, in the flags it created its own DLSS feature with. Running // the colour transform over a frame that has already been through a tonemapper is pure damage, and // skipping it on one that has not leaves the model reading ordinary values as enormously bright. - // Both have to agree: the caller says what the game intends, the format says what the surface can - // actually hold. A game that claims HDR while rendering into eight bits gets its frame encoded - // twice otherwise. - const bool gameSaysHdr = frame.ColourIsLinearHdr; - const bool isHdrBuffer = gameSaysHdr && FormatCanHoldLinearHdr(desc.Format); + // EvaluateInternal has already combined the game's HDR flag with the authoritative output format. + // That authority matters before SR: Color and Output may use different surface formats while still + // representing the same frame colour space. + const bool isHdrBuffer = frame.ColourIsLinearHdr; static bool reportedHdr = false; + static bool reportedHdrValue = false; + static bool reportedBefore = false; - if (!reportedHdr) + if (!reportedHdr || reportedHdrValue != isHdrBuffer || reportedBefore != frame.BeforeUpscale) { reportedHdr = true; - LOG_INFO("DLSS-NR: the game's DLSS buffer is {} so the colour transform is {}", + reportedHdrValue = isHdrBuffer; + reportedBefore = frame.BeforeUpscale; + LOG_INFO("DLSS-NR {} SR: the game's DLSS colour space is {} so the colour transform is {}", + frame.BeforeUpscale ? "before" : "after", isHdrBuffer ? "linear HDR" : "already tone-mapped", isHdrBuffer ? "on" : "off"); } @@ -1932,12 +2122,11 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c meterParams.Width = 1; meterParams.Height = 1; - Barrier(cmdList, target, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, - D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE); + const D3D12_RESOURCE_STATES priorTargetState = targetState; + TransitionTarget(D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE); DispatchPass(cmdList, meterParams, target, nullptr, nullptr, (ID3D12Resource*) frame.ExposureTexture, nullptr, g_nr.meter, nullptr); - Barrier(cmdList, target, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, - D3D12_RESOURCE_STATE_UNORDERED_ACCESS); + TransitionTarget(priorTargetState); CopyMeterToReadback(cmdList, device, true); ConsumeMeterReadback(); @@ -1991,15 +2180,14 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c if (g_nr.heldColor != nullptr) { - Barrier(cmdList, target, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, - D3D12_RESOURCE_STATE_COPY_SOURCE); + const D3D12_RESOURCE_STATES priorTargetState = targetState; + TransitionTarget(D3D12_RESOURCE_STATE_COPY_SOURCE); Barrier(cmdList, g_nr.heldColor, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_COPY_DEST); cmdList->CopyResource(g_nr.heldColor, target); Barrier(cmdList, g_nr.heldColor, D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_COPY_SOURCE); - Barrier(cmdList, target, D3D12_RESOURCE_STATE_COPY_SOURCE, - D3D12_RESOURCE_STATE_UNORDERED_ACCESS); + TransitionTarget(priorTargetState); g_nr.heldActive = true; g_nr.heldWidth = (unsigned int) td.Width; @@ -2011,11 +2199,10 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c else { // Held: restore the frozen frame onto the live output before the encode reads it. - Barrier(cmdList, target, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, - D3D12_RESOURCE_STATE_COPY_DEST); + const D3D12_RESOURCE_STATES priorTargetState = targetState; + TransitionTarget(D3D12_RESOURCE_STATE_COPY_DEST); cmdList->CopyResource(target, g_nr.heldColor); - Barrier(cmdList, target, D3D12_RESOURCE_STATE_COPY_DEST, - D3D12_RESOURCE_STATE_UNORDERED_ACCESS); + TransitionTarget(priorTargetState); } // Suspend white-point measurement while held: use the snapshot so it cannot drift and @@ -2046,13 +2233,12 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c encodeParams.Width = width; encodeParams.Height = height; - Barrier(cmdList, target, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, - D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE); + TransitionTarget(D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE); DispatchPass(cmdList, encodeParams, target, nullptr, nullptr, nullptr, exposureTex, g_nr.colorCopy, g_nr.hdrCopy); - Barrier(cmdList, target, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, - D3D12_RESOURCE_STATE_UNORDERED_ACCESS); + if (targetSupportsUav) + TransitionTarget(D3D12_RESOURCE_STATE_UNORDERED_ACCESS); // The transitions double as the wait for the encode's writes. Barrier(cmdList, g_nr.colorCopy, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE); @@ -2141,7 +2327,7 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c g_nr.failed = true; g_nr.reason = "the game's depth or motion vectors could not be made readable"; LOG_ERROR("DLSS-NR unavailable: {}", g_nr.reason); - Barrier(cmdList, target, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, outputArrival); + TransitionTarget(outputArrival); device->Release(); return; } @@ -2175,7 +2361,7 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c proxyResult, NgxResultName(proxyResult)); } - Barrier(cmdList, target, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, outputArrival); + TransitionTarget(outputArrival); device->Release(); return; } @@ -2183,16 +2369,98 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c if (g_ngxTime != nullptr) g_ngxTime->Start(cmdList); - // Multi-pass was removed: re-feeding the model its own output re-opened the same-command-list - // feature-creation hang, and the colour core is not settled enough to build on. One evaluate. - const int result = g_nr.evaluate( - cmdList, g_nr.feature, g_nr.capabilityParams, modelInput, depthIn, motionIn, g_nr.output, - workWidth, workHeight, guideWidth, guideHeight, g_nr.guideDepthInverted ? 1 : 0, - g_nr.reset ? 1 : 0, cfg.DlssNrIntensity.value_or_default(), - (int) cfg.DlssNrStyle.value_or_default(), cfg.DlssNrLocalStructure.value_or_default(), - cfg.DlssNrLocalTone.value_or_default(), cfg.DlssNrSkinStructure.value_or_default(), - cfg.DlssNrAutoMask.value_or_default() ? 1 : 0, g_nr.guideMvScaleX * mvToWork, - g_nr.guideMvScaleY * mvToWork); + // Count only a contiguous set of ready, separate feature histories. A failed extra creation never + // falls back to reusing the main feature: that tells one temporal model several frames elapsed in + // one game frame and makes its history fight the later layers. + unsigned int effectivePasses = 1; + if (g_nr.passScratch != nullptr) + { + for (unsigned int pass = 1; pass < requestedPasses; ++pass) + { + if (g_nr.passFeature[pass] == nullptr || g_nr.passPendingSubmission[pass]) + break; + ++effectivePasses; + } + } + + { + static unsigned int loggedConfigured = 0; + static unsigned int loggedEffective = 0; + if (loggedConfigured != configuredPasses || loggedEffective != effectivePasses) + { + loggedConfigured = configuredPasses; + loggedEffective = effectivePasses; + LOG_INFO("DLSS-NR model passes: configured {}, effective {}", configuredPasses, + effectivePasses); + } + } + + // Encode happened once above. Keep that base proxy immutable and ping-pong only model answers: + // pass 0: base -> A, pass 1: A -> B, pass 2: B -> A. + // The final answer is resolved once against the original base, so matched-residual transfer is the + // cumulative final-minus-base edit and colour/transfer controls are not compounded. + ID3D12Resource* passInput = modelInput; + ID3D12Resource* passOutput = g_nr.output; + ID3D12Resource* finalAnswer = nullptr; + bool outputReadable = false; + bool scratchReadable = false; + + const auto MakeModelReadable = [&](ID3D12Resource* resource) + { + bool& readable = resource == g_nr.output ? outputReadable : scratchReadable; + if (!readable) + { + Barrier(cmdList, resource, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, + D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE); + readable = true; + } + }; + + const auto MakeModelWritable = [&](ID3D12Resource* resource) + { + bool& readable = resource == g_nr.output ? outputReadable : scratchReadable; + if (readable) + { + Barrier(cmdList, resource, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, + D3D12_RESOURCE_STATE_UNORDERED_ACCESS); + readable = false; + } + }; + + int result = NVSDK_NGX_Result_Success; + + for (unsigned int pass = 0; pass < effectivePasses && result == NVSDK_NGX_Result_Success; + ++pass) + { + void* const passFeature = pass == 0 ? g_nr.feature : g_nr.passFeature[pass]; + const bool passReset = g_nr.reset || (pass > 0 && g_nr.passNeedsReset[pass]); + const float passTone = pass == 0 ? cfg.DlssNrLocalTone.value_or_default() : 0.0f; + + MakeModelWritable(passOutput); + result = g_nr.evaluate( + cmdList, passFeature, g_nr.capabilityParams, passInput, depthIn, motionIn, passOutput, + workWidth, workHeight, guideWidth, guideHeight, g_nr.guideDepthInverted ? 1 : 0, + passReset ? 1 : 0, cfg.DlssNrIntensity.value_or_default(), + (int) cfg.DlssNrStyle.value_or_default(), cfg.DlssNrLocalStructure.value_or_default(), + passTone, cfg.DlssNrSkinStructure.value_or_default(), + cfg.DlssNrAutoMask.value_or_default() ? 1 : 0, g_nr.guideMvScaleX * mvToWork, + g_nr.guideMvScaleY * mvToWork); + + if (result != NVSDK_NGX_Result_Success) + break; + + if (pass > 0) + g_nr.passNeedsReset[pass] = false; + + finalAnswer = passOutput; + MakeModelReadable(finalAnswer); + + if (pass + 1 < effectivePasses) + { + passInput = finalAnswer; + passOutput = passOutput == g_nr.output ? g_nr.passScratch : g_nr.output; + } + } if (g_ngxTime != nullptr) g_ngxTime->End(cmdList); @@ -2301,6 +2569,7 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c unsigned int residual; unsigned int workW; unsigned int workH; + unsigned int passes; }; static ComposeReport loggedCompose {}; @@ -2318,7 +2587,8 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c resolveParams.CompareMode, resolveParams.Transfer, g_nr.workWidth, - g_nr.workHeight }; + g_nr.workHeight, + effectivePasses }; if (!loggedCompose.valid || loggedCompose.whitePoint != composeNow.whitePoint || loggedCompose.transfer != composeNow.transfer || loggedCompose.colour != composeNow.colour || @@ -2327,29 +2597,26 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c loggedCompose.debugView != composeNow.debugView || loggedCompose.compareMode != composeNow.compareMode || loggedCompose.residual != composeNow.residual || loggedCompose.workW != composeNow.workW || - loggedCompose.workH != composeNow.workH) + loggedCompose.workH != composeNow.workH || loggedCompose.passes != composeNow.passes) { loggedCompose = composeNow; LOG_INFO("DLSS-NR composition: paper white {:.2f}x, detail {:.2f}, colour {:.2f}, guard " - "{:.1f}x, colour transform {}, transfer {}, model {}x{}, debug view {}, compare {}", + "{:.1f}x, colour transform {}, transfer {}, model {}x{}, passes {}, debug view {}, compare {}", composeNow.whitePoint, composeNow.transfer, composeNow.colour, composeNow.maxRatio, composeNow.passthrough != 0 ? "off (frame already tone mapped)" : "on (linear HDR)", composeNow.residual == 1 ? "matched residual" : "classic", composeNow.workW, - composeNow.workH, composeNow.debugView, composeNow.compareMode); + composeNow.workH, composeNow.passes, composeNow.debugView, composeNow.compareMode); } - Barrier(cmdList, g_nr.output, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, - D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE); - // Supersampling down-leg. Average the Nx model answer back to native with the chosen filter, so // the resolve composites a native answer against the native proxy 1:1 -- a real area resample, // not the single bilinear tap the Nx answer would otherwise get in the resolve (which aliases // the model's detail into noise, the "noisier above 100%" the probe showed). On success the // resolve reads the native proxy (colorCopy) and native answer (outputNative); on failure it - // falls back to the Nx pair. g_nr.output is NPSR here; outputNative is UAV from last frame. + // falls back to the Nx pair. finalAnswer is NPSR here; outputNative is UAV from last frame. bool superDownOk = false; if (workScale > 1.0f && g_nr.superDown != nullptr && g_nr.outputNative != nullptr && - g_nr.superDown->Dispatch(cmdList, g_nr.output, g_nr.outputNative)) + g_nr.superDown->Dispatch(cmdList, finalAnswer, g_nr.outputNative)) { Barrier(cmdList, g_nr.outputNative, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE); @@ -2357,12 +2624,41 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c } ID3D12Resource* resolveProxy = superDownOk ? g_nr.colorCopy : modelInput; - ID3D12Resource* resolveAnswer = superDownOk ? g_nr.outputNative : g_nr.output; + ID3D12Resource* resolveAnswer = superDownOk ? g_nr.outputNative : finalAnswer; - DispatchPass(cmdList, resolveParams, resolveProxy, resolveAnswer, g_nr.hdrCopy, motionIn, - exposureTex, target, nullptr); - Barrier(cmdList, g_nr.output, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, - D3D12_RESOURCE_STATE_UNORDERED_ACCESS); + // Pre-SR Color is not guaranteed to have UAV support. Write directly when legal; otherwise + // resolve into hdrCopy while the original Color remains readable, then copy the result back. + ID3D12Resource* resolveOriginal = targetSupportsUav ? g_nr.hdrCopy : target; + ID3D12Resource* resolveTarget = targetSupportsUav ? target : g_nr.hdrCopy; + + if (targetSupportsUav) + { + TransitionTarget(D3D12_RESOURCE_STATE_UNORDERED_ACCESS); + } + else + { + Barrier(cmdList, g_nr.hdrCopy, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, + D3D12_RESOURCE_STATE_UNORDERED_ACCESS); + } + + DispatchPass(cmdList, resolveParams, resolveProxy, resolveAnswer, resolveOriginal, motionIn, + exposureTex, resolveTarget, nullptr); + + if (!targetSupportsUav) + { + Barrier(cmdList, g_nr.hdrCopy, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, + D3D12_RESOURCE_STATE_COPY_SOURCE); + const D3D12_RESOURCE_STATES priorTargetState = targetState; + TransitionTarget(D3D12_RESOURCE_STATE_COPY_DEST); + cmdList->CopyResource(target, g_nr.hdrCopy); + TransitionTarget(priorTargetState); + Barrier(cmdList, g_nr.hdrCopy, D3D12_RESOURCE_STATE_COPY_SOURCE, + D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE); + } + + MakeModelWritable(g_nr.output); + if (g_nr.passScratch != nullptr) + MakeModelWritable(g_nr.passScratch); if (superDownOk) Barrier(cmdList, g_nr.outputNative, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, @@ -2376,7 +2672,7 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c { g_capture.record(cmdList, device, g_nr.colorCopy, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, target, - D3D12_RESOURCE_STATE_UNORDERED_ACCESS); + targetState); if (g_capture.readyToWrite() && g_captureWriteAtFrame == 0) g_captureWriteAtFrame = g_frames + 8; @@ -2390,6 +2686,12 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c NgxResultName((unsigned int) result)); } + // On an evaluation failure, intermediate A/B inputs may still be readable. Restore both persistent + // ping-pong surfaces to the UAV state the next frame starts from. + MakeModelWritable(g_nr.output); + if (g_nr.passScratch != nullptr) + MakeModelWritable(g_nr.passScratch); + Barrier(cmdList, g_nr.hdrCopy, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); @@ -2453,7 +2755,7 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c D3D12_RESOURCE_STATE_UNORDERED_ACCESS); // Hand the output back in the state the upscaler and the game expect. - Barrier(cmdList, target, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, outputArrival); + TransitionTarget(outputArrival); device->Release(); } @@ -2473,10 +2775,13 @@ void RetryAfterFailure() // This is the call site's job, not the pass's. A caller that has the resources in hand -- a // reprojection stage, a frame generation path, anything that is not the upscaler seam -- calls // RunPass directly and never touches an NGX parameter block. -void EvaluateAfterUpscale(ID3D12GraphicsCommandList* cmdList, NVSDK_NGX_Parameter* params, - ID3D12CommandQueue* timingQueue) +void EvaluateInternal(ID3D12GraphicsCommandList* cmdList, NVSDK_NGX_Parameter* params, + bool beforeUpscale, ID3D12CommandQueue* timingQueue, bool forcePost, + unsigned long long submissionEpoch) { - if (!Config::Instance()->DlssNrEnabled.value_or_default()) + const Config& cfg = *Config::Instance(); + + if (!cfg.DlssNrEnabled.value_or_default()) { ReportSkipOnce("it is switched off"); return; @@ -2488,6 +2793,57 @@ void EvaluateAfterUpscale(ID3D12GraphicsCommandList* cmdList, NVSDK_NGX_Paramete return; } + // Ray Reconstruction is explicitly forced post: PR #6 reports that pre-SR placement does not work + // with DLSSD's input contract. Padded/offset Color inputs also stay post until the colour codec can + // address subrect origins: processing their whole allocation would run the wrong raster and touch + // stale pixels. Rechecking on the post call makes this a real fallback rather than dropping NR. + bool preSrCompatible = true; + if (cfg.DlssNrRunBeforeSr.value_or_default() && !forcePost) + { + ID3D12Resource* preColor = GetResource(params, NVSDK_NGX_Parameter_Color, "DLSSD.Color"); + unsigned int renderWidth = 0, renderHeight = 0, colorBaseX = 0, colorBaseY = 0; + params->Get(NVSDK_NGX_Parameter_DLSS_Render_Subrect_Dimensions_Width, &renderWidth); + params->Get(NVSDK_NGX_Parameter_DLSS_Render_Subrect_Dimensions_Height, &renderHeight); + params->Get(NVSDK_NGX_Parameter_DLSS_Input_Color_Subrect_Base_X, &colorBaseX); + params->Get(NVSDK_NGX_Parameter_DLSS_Input_Color_Subrect_Base_Y, &colorBaseY); + + if (preColor == nullptr) + { + preSrCompatible = false; + } + else + { + const D3D12_RESOURCE_DESC colorDesc = preColor->GetDesc(); + const unsigned int allocationWidth = (unsigned int) colorDesc.Width; + const unsigned int allocationHeight = colorDesc.Height; + const bool hasAnyActiveSize = renderWidth != 0 || renderHeight != 0; + const bool hasActiveSize = renderWidth != 0 && renderHeight != 0; + preSrCompatible = colorBaseX == 0 && colorBaseY == 0 && + (!hasAnyActiveSize || + (hasActiveSize && renderWidth == allocationWidth && + renderHeight == allocationHeight)); + + if (!preSrCompatible) + { + static bool warnedSubrect = false; + if (!warnedSubrect) + { + warnedSubrect = true; + LOG_WARN("DLSS-NR before SR requires an origin-zero Color allocation matching the " + "active render size; got allocation {}x{}, active {}x{} at {},{}. " + "Falling back after SR.", + allocationWidth, allocationHeight, renderWidth, renderHeight, colorBaseX, + colorBaseY); + } + } + } + } + + const bool configuredBefore = cfg.DlssNrRunBeforeSr.value_or_default() && !forcePost && + preSrCompatible; + if (configuredBefore != beforeUpscale) + return; + // Which of the game's APIs this evaluate arrived through. // // Says out loud what was previously only reasoned about: an FSR or XeSS title reaches this pass @@ -2505,7 +2861,10 @@ void EvaluateAfterUpscale(ID3D12GraphicsCommandList* cmdList, NVSDK_NGX_Paramete } } - ID3D12Resource* target = GetResource(params, NVSDK_NGX_Parameter_Output, "DLSSD.Output"); + ID3D12Resource* output = GetResource(params, NVSDK_NGX_Parameter_Output, "DLSSD.Output"); + ID3D12Resource* target = beforeUpscale + ? GetResource(params, NVSDK_NGX_Parameter_Color, "DLSSD.Color") + : output; ID3D12Resource* depth = GetResource(params, NVSDK_NGX_Parameter_Depth, "DLSSD.Depth"); ID3D12Resource* motion = GetResource(params, NVSDK_NGX_Parameter_MotionVectors, "DLSSD.MotionVectors"); @@ -2513,7 +2872,8 @@ void EvaluateAfterUpscale(ID3D12GraphicsCommandList* cmdList, NVSDK_NGX_Paramete // carry none of it -- so it stays quiet and tries again next frame. if (target == nullptr || depth == nullptr || motion == nullptr) { - ReportSkipOnce(target == nullptr ? "the parameters carried no output texture" + ReportSkipOnce(target == nullptr ? (beforeUpscale ? "the parameters carried no color texture" + : "the parameters carried no output texture") : depth == nullptr ? "the parameters carried no depth" : "the parameters carried no motion vectors"); return; @@ -2524,7 +2884,16 @@ void EvaluateAfterUpscale(ID3D12GraphicsCommandList* cmdList, NVSDK_NGX_Paramete DlssNrFrameInfo frame {}; frame.DepthInverted = (createFlags & NVSDK_NGX_DLSS_Feature_Flags_DepthInverted) != 0; - frame.ColourIsLinearHdr = (createFlags & NVSDK_NGX_DLSS_Feature_Flags_IsHDR) != 0; + frame.BeforeUpscale = beforeUpscale; + frame.SubmissionEpoch = timingQueue != nullptr ? submissionEpoch : State::Instance().frameCount; + + // Color and Output may use different formats even though DLSS treats them as the same frame colour + // space. Output is the stable authority across injection points; target is only a fallback for a + // malformed parameter block. + ID3D12Resource* colourAuthority = output != nullptr ? output : target; + frame.ColourIsLinearHdr = + (createFlags & NVSDK_NGX_DLSS_Feature_Flags_IsHDR) != 0 && + colourAuthority != nullptr && FormatCanHoldLinearHdr(colourAuthority->GetDesc().Format); // The game telling the upscaler to forget everything it has accumulated: a cut, a teleport, a // load. Every upscaler in this tree reads it and this pass did not, so the model's history was @@ -2677,6 +3046,19 @@ void EvaluateAfterUpscale(ID3D12GraphicsCommandList* cmdList, NVSDK_NGX_Paramete g_compose->Dispatch(cmdList, target, depth, motion, target, frame, timingQueue); } +void EvaluateAfterUpscale(ID3D12GraphicsCommandList* cmdList, NVSDK_NGX_Parameter* params, + ID3D12CommandQueue* timingQueue, bool forcePost, + unsigned long long submissionEpoch) +{ + EvaluateInternal(cmdList, params, false, timingQueue, forcePost, submissionEpoch); +} + +void EvaluateBeforeUpscale(ID3D12GraphicsCommandList* cmdList, NVSDK_NGX_Parameter* params, + ID3D12CommandQueue* timingQueue, unsigned long long submissionEpoch) +{ + EvaluateInternal(cmdList, params, true, timingQueue, false, submissionEpoch); +} + // The pass. Resources in, nothing read from anywhere the caller cannot see. void ProbeD3D11(void* d3d11Device) @@ -2862,13 +3244,18 @@ void Shutdown() g_nr.release(g_nr.feature); g_nr.feature = nullptr; + g_nr.featurePendingSubmission = false; - for (void*& f : g_nr.passFeature) + for (unsigned int pass = 1; pass < DlssNr::MaxPassCount; ++pass) { + void*& f = g_nr.passFeature[pass]; if (f != nullptr && g_nr.release != nullptr) g_nr.release(f); f = nullptr; + g_nr.passNeedsReset[pass] = false; + g_nr.passCreateFailed[pass] = false; + g_nr.passPendingSubmission[pass] = false; } if (g_nr.output != nullptr) @@ -2877,6 +3264,13 @@ void Shutdown() g_nr.output = nullptr; } + if (g_nr.passScratch != nullptr) + { + g_nr.passScratch->Release(); + g_nr.passScratch = nullptr; + } + g_nr.passScratchFailed = false; + if (g_nr.colorCopy != nullptr) { g_nr.colorCopy->Release(); diff --git a/OptiScaler/shaders/dlssnr/DlssNr_Dx12.h b/OptiScaler/shaders/dlssnr/DlssNr_Dx12.h index 56fdff758..acf6df215 100644 --- a/OptiScaler/shaders/dlssnr/DlssNr_Dx12.h +++ b/OptiScaler/shaders/dlssnr/DlssNr_Dx12.h @@ -29,19 +29,9 @@ // there has to be enough for three passes times the deepest pipeline we might sit behind. // Descriptor and constant slots, consumed one per dispatch and reused round-robin with no fence. // -// The pass records four dispatches per frame -- meter, encode, downsample, resolve -- so sixteen slots -// is four frames of coverage before a slot is rewritten. The comment this replaces said "three passes -// times the deepest pipeline we might sit behind", and the pass count has since grown to four while -// the ring did not. -// -// Four frames is not enough. Frame generation deliberately runs the GPU several frames behind the CPU, -// and the constants live in an UPLOAD heap written at record time -- so a wrap while the GPU is still -// reading a slot rewrites descriptors and constants underneath it. -// -// A fifth dispatch has since been added -- the calibration grid -- which at thirty-two slots would -// have left six frames, spending exactly the headroom the previous note set aside. Forty-eight -// restores eight frames at five dispatches. If a sixth is ever added, raise this with it rather than -// spending the margin again. +// The shader still records at most meter + encode + downsample + resolve per frame. Extra model layers +// are NGX evaluates and do not consume this ring; their A/B resources and feature histories are +// persistent. Forty-eight slots leave twelve fully populated frames before descriptor/constant reuse. #define DLSSNR_NUM_OF_HEAPS 48 class DlssNr_Dx12 : public Shader_Dx12, public DlssNr_Common diff --git a/OptiScaler/upscalers/IFeature_Dx11wDx12.cpp b/OptiScaler/upscalers/IFeature_Dx11wDx12.cpp index 7f2476579..c0502954c 100644 --- a/OptiScaler/upscalers/IFeature_Dx11wDx12.cpp +++ b/OptiScaler/upscalers/IFeature_Dx11wDx12.cpp @@ -457,6 +457,8 @@ bool IFeature_Dx11wDx12::Evaluate(ID3D11DeviceContext* InDeviceContext, NVSDK_NG (void*) dx11Reactive.Dx12Resource); LOG_DEBUG("Dispatch!!"); + if (dx12Feature->GetUpscalerType() != Upscaler::DLSSD) + DlssNr::EvaluateBeforeUpscale(cmdList, InParameters, Dx12CommandQueue, _frameCount); dx12EvalResult = dx12Feature->Evaluate(cmdList, InParameters); // DLSS 5 Neural Rendering rides the bridge: at this moment the block carries the D3D12 copies @@ -475,7 +477,9 @@ bool IFeature_Dx11wDx12::Evaluate(ID3D11DeviceContext* InDeviceContext, NVSDK_NG if (dx12EvalResult && Config::Instance()->DlssNrEnabled.value_or_default()) { - DlssNr::EvaluateAfterUpscale(cmdList, InParameters, Dx12CommandQueue); + DlssNr::EvaluateAfterUpscale(cmdList, InParameters, Dx12CommandQueue, + dx12Feature->GetUpscalerType() == Upscaler::DLSSD, + _frameCount); // Asked only after the D3D12 path has had its turn. Probing first would have made a D3D11 // init the very first thing to ever touch the snippet, and if that had left its core diff --git a/OptiScaler/upscalers/IFeature_VkwDx12.cpp b/OptiScaler/upscalers/IFeature_VkwDx12.cpp index edf392702..341541563 100644 --- a/OptiScaler/upscalers/IFeature_VkwDx12.cpp +++ b/OptiScaler/upscalers/IFeature_VkwDx12.cpp @@ -2156,6 +2156,8 @@ bool IFeature_VkwDx12::Evaluate(VkCommandBuffer InCmdBuffer, NVSDK_NGX_Parameter InParameters->Set(NVSDK_NGX_Parameter_DLSS_Input_Bias_Current_Color_Mask, (void*) vkReactive.Dx12Resource); LOG_DEBUG("Dispatch!!"); + if (dx12Feature->GetUpscalerType() != Upscaler::DLSSD) + DlssNr::EvaluateBeforeUpscale(cmdList, InParameters, Dx12CommandQueue, _frameCount); dx12EvalResult = dx12Feature->Evaluate(cmdList, InParameters); // The parameter block still holds the D3D12 resources written above -- the Vulkan handles are @@ -2170,7 +2172,9 @@ bool IFeature_VkwDx12::Evaluate(VkCommandBuffer InCmdBuffer, NVSDK_NGX_Parameter } if (dx12EvalResult && Config::Instance()->DlssNrEnabled.value_or_default()) - DlssNr::EvaluateAfterUpscale(cmdList, InParameters, Dx12CommandQueue); + DlssNr::EvaluateAfterUpscale(cmdList, InParameters, Dx12CommandQueue, + dx12Feature->GetUpscalerType() == Upscaler::DLSSD, + _frameCount); } while (false); From 0820fd557017562be9e051f084bc67494587a6a5 Mon Sep 17 00:00:00 2001 From: wilsjo2 <97138003+wilsjo2@users.noreply.github.com> Date: Sun, 6 Sep 2026 09:41:59 +1000 Subject: [PATCH 2/7] docs: explain fork scope and additions --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index f185e6dce..69447e432 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,22 @@ +# OptiScaler DLSS-NR pre-SR multipass fork + +> [!IMPORTANT] +> This is an experimental fork of [Dagherbou/OptiScaler_DLSSNR](https://github.com/Dagherbou/OptiScaler_DLSSNR), based on commit [`97376162`](https://github.com/Dagherbou/OptiScaler_DLSSNR/commit/973761621353b99bee3dc7d4bb27b117fef2644f) (`v0.2.0-dlssnr` / `v0.2.0-patch1`). It is not the main OptiScaler project and is not supported by NVIDIA or game developers. + +The upstream fork already provided experimental direct access to NVIDIA DLSS Neural Rendering. This fork adds: + +- **Optional Neural Rendering before DLSS Super Resolution.** The model can process the DLSS input image—such as 1920x1080 in 4K Performance mode—before DLSS upscales it to the display resolution. +- **Configurable multipass processing.** `[DlssNr] Passes=1..3` runs one, two, or three sequential neural evaluations. Each pass has independent persistent history; the final result is composed once against the original base image. +- **Guarded fallbacks.** Ray Reconstruction remains post-SR, and padded or offset dynamic-resolution inputs fall back to the existing post-SR path instead of using unsafe dimensions. +- **Matching overlay and INI controls.** `RunBeforeSR` and `Passes` are exposed in both configuration and the OptiScaler overlay. +- **Verified BG3 path.** Baldur's Gate 3 was tested with two neural passes at 1920x1080 followed by DLSS Super Resolution to 3840x2160. + +Download the exact tested BG3 package from the [BG3 pre-SR multipass release](https://github.com/wilsjo2/OptiScaler-DLSSNR-PreSR-Multipass/releases/tag/bg3-presr-multipass-e16d5866). NVIDIA's proprietary `nvngx_dlssnr.dll` is required but is **not redistributed** here. + +Implementation details and safety invariants are documented in [the pre-SR multipass design note](OptiScaler/dlssnr/design/pre-sr-multipass.md). The remainder of this README is the upstream OptiScaler documentation. + +--- +
![Logo](https://github.com/user-attachments/assets/c7dad5da-0b29-4710-8a57-b58e4e407abd) From facc24f61899d68f9271e4f36f375d1186bbbb00 Mon Sep 17 00:00:00 2001 From: wilsjo2 <97138003+wilsjo2@users.noreply.github.com> Date: Sun, 6 Sep 2026 10:13:17 +1000 Subject: [PATCH 3/7] DLSS-NR: add per-pass model profiles --- OptiScaler.ini | 15 ++- OptiScaler/Config.cpp | 12 +++ OptiScaler/Config.h | 7 ++ OptiScaler/dlssnr/DlssNr_Menu.cpp | 49 +++++++++- OptiScaler/dlssnr/design/pre-sr-multipass.md | 9 ++ OptiScaler/shaders/dlssnr/DlssNr_Dx12.cpp | 96 ++++++++++++++------ README.md | 7 ++ 7 files changed, 164 insertions(+), 31 deletions(-) diff --git a/OptiScaler.ini b/OptiScaler.ini index 06b00e860..177db3b18 100644 --- a/OptiScaler.ini +++ b/OptiScaler.ini @@ -1624,12 +1624,21 @@ DebugView=auto ; true or false - Default (auto) is true AutoCapture=auto -; Model controls. The preset is baked in when the model is created, so it needs a restart; the rest are -; live. Presets and styles are undocumented, and the preset scale is not the same one super resolution -; or ray reconstruction use. +; Model controls. Preset and Style are baked into each feature, so changing them causes a guarded +; feature rebuild after the current GPU work retires. The values are undocumented, and the preset +; scale is not the same one Super Resolution or Ray Reconstruction uses. ; Default (auto) is 0 Preset=auto Style=auto + +; Passes 2 and 3 inherit the pass 1 Preset/Style above when left on auto. Set an override to use a +; different built-in model profile for that layer: preset 0..3; style 0 standard, 1 natural, +; 2 cinematic. These select profiles inside the same NVIDIA model DLL, not different model files. +Pass2Preset=auto +Pass2Style=auto +Pass3Preset=auto +Pass3Style=auto + ; Default (auto) is 1.0 Intensity=auto LocalStructure=auto diff --git a/OptiScaler/Config.cpp b/OptiScaler/Config.cpp index b9e4ef751..0d9ee0664 100644 --- a/OptiScaler/Config.cpp +++ b/OptiScaler/Config.cpp @@ -362,6 +362,10 @@ bool Config::Reload(std::filesystem::path iniPath) DlssNrPreset.set_from_config(readUInt("DlssNr", "Preset")); DlssNrIntensity.set_from_config(readFloat("DlssNr", "Intensity")); DlssNrStyle.set_from_config(readUInt("DlssNr", "Style")); + DlssNrPass2Preset.set_from_config(readUInt("DlssNr", "Pass2Preset")); + DlssNrPass2Style.set_from_config(readUInt("DlssNr", "Pass2Style")); + DlssNrPass3Preset.set_from_config(readUInt("DlssNr", "Pass3Preset")); + DlssNrPass3Style.set_from_config(readUInt("DlssNr", "Pass3Style")); DlssNrLocalStructure.set_from_config(readFloat("DlssNr", "LocalStructure")); DlssNrLocalTone.set_from_config(readFloat("DlssNr", "LocalTone")); DlssNrSkinStructure.set_from_config(readFloat("DlssNr", "SkinStructure")); @@ -1253,6 +1257,14 @@ bool Config::SaveIni() ini.SetValue("DlssNr", "Preset", GetIntValue(Instance()->DlssNrPreset.value_for_config()).c_str()); ini.SetValue("DlssNr", "Intensity", GetFloatValue(Instance()->DlssNrIntensity.value_for_config()).c_str()); ini.SetValue("DlssNr", "Style", GetIntValue(Instance()->DlssNrStyle.value_for_config()).c_str()); + ini.SetValue("DlssNr", "Pass2Preset", + GetIntValue(Instance()->DlssNrPass2Preset.value_for_config()).c_str()); + ini.SetValue("DlssNr", "Pass2Style", + GetIntValue(Instance()->DlssNrPass2Style.value_for_config()).c_str()); + ini.SetValue("DlssNr", "Pass3Preset", + GetIntValue(Instance()->DlssNrPass3Preset.value_for_config()).c_str()); + ini.SetValue("DlssNr", "Pass3Style", + GetIntValue(Instance()->DlssNrPass3Style.value_for_config()).c_str()); ini.SetValue("DlssNr", "LocalStructure", GetFloatValue(Instance()->DlssNrLocalStructure.value_for_config()).c_str()); ini.SetValue("DlssNr", "LocalTone", GetFloatValue(Instance()->DlssNrLocalTone.value_for_config()).c_str()); diff --git a/OptiScaler/Config.h b/OptiScaler/Config.h index 1a12f5cc1..fd45b3a67 100644 --- a/OptiScaler/Config.h +++ b/OptiScaler/Config.h @@ -267,6 +267,13 @@ class Config CustomOptional DlssNrIntensity { 1.0f }; // 0 default (standard), 1 natural, 2 cinematic -- the model's own processing profiles. CustomOptional DlssNrStyle { 0 }; + // Optional per-pass model profiles. Pass 1 uses Preset/Style above; an absent override inherits + // pass 1. Keeping inheritance explicit preserves every existing configuration and lets changing + // the base profile update the whole stack unless a later pass was deliberately specialised. + CustomOptional DlssNrPass2Preset; + CustomOptional DlssNrPass2Style; + CustomOptional DlssNrPass3Preset; + CustomOptional DlssNrPass3Style; CustomOptional DlssNrLocalStructure { 1.0f }; CustomOptional DlssNrLocalTone { 1.0f }; // -1 means follow local structure, which is the model's own default. It is not a strength of zero. diff --git a/OptiScaler/dlssnr/DlssNr_Menu.cpp b/OptiScaler/dlssnr/DlssNr_Menu.cpp index aca9eab5e..c0353ee1c 100644 --- a/OptiScaler/dlssnr/DlssNr_Menu.cpp +++ b/OptiScaler/dlssnr/DlssNr_Menu.cpp @@ -80,6 +80,27 @@ static bool DeferredSlider(const char* label, CustomOptional* opt, float return changed; } +// An absent later-pass setting inherits pass 1. The first combo item represents that absence; the +// remaining items map directly to the model's zero-based profile values. +static bool InheritedProfileCombo(const char* label, CustomOptional* opt, + const char* const* names, int nameCount) +{ + int selected = 0; + + if (opt->has_value()) + selected = std::clamp((int) opt->value(), 0, nameCount - 2) + 1; + + if (!ImGui::Combo(label, &selected, names, nameCount)) + return false; + + if (selected == 0) + *opt = std::optional {}; + else + *opt = (uint32_t) (selected - 1); + + return true; +} + void RenderMenu(Config* config, float menuResScale) { @@ -386,7 +407,7 @@ void RenderMenu(Config* config, float menuResScale) static const char* nrPresetNames[] = { "Default", "Preset 1", "Preset 2", "Preset 3" }; int preset = (int) config->DlssNrPreset.value_or_default(); - if (ImGui::Combo("Model preset", &preset, nrPresetNames, IM_ARRAYSIZE(nrPresetNames))) + if (ImGui::Combo("Pass 1 model preset", &preset, nrPresetNames, IM_ARRAYSIZE(nrPresetNames))) config->DlssNrPreset = (uint32_t) preset; HelpMarker("Default leaves the choice to the model." @@ -399,7 +420,7 @@ void RenderMenu(Config* config, float menuResScale) if (style > 2) style = 2; - if (ImGui::Combo("Style", &style, nrStyleNames, IM_ARRAYSIZE(nrStyleNames))) + if (ImGui::Combo("Pass 1 style", &style, nrStyleNames, IM_ARRAYSIZE(nrStyleNames))) config->DlssNrStyle = (uint32_t) style; HelpMarker("The model's own processing profiles." @@ -412,6 +433,30 @@ void RenderMenu(Config* config, float menuResScale) "\n\nRead when the model is built, so a change rebuilds it after a moment. The" "\nnames come from community testing; NVIDIA ships no names in the binaries."); + ImGui::SeparatorText("Later-pass model profiles"); + ImGui::TextDisabled("Auto inherits pass 1. Overrides rebuild only while that pass is active."); + + static const char* inheritedPresetNames[] = { + "Auto (inherit pass 1)", "Default", "Preset 1", "Preset 2", "Preset 3" + }; + static const char* inheritedStyleNames[] = { + "Auto (inherit pass 1)", "Default (standard)", "Natural", "Cinematic" + }; + + InheritedProfileCombo("Pass 2 preset", &config->DlssNrPass2Preset, + inheritedPresetNames, IM_ARRAYSIZE(inheritedPresetNames)); + InheritedProfileCombo("Pass 2 style", &config->DlssNrPass2Style, + inheritedStyleNames, IM_ARRAYSIZE(inheritedStyleNames)); + InheritedProfileCombo("Pass 3 preset", &config->DlssNrPass3Preset, + inheritedPresetNames, IM_ARRAYSIZE(inheritedPresetNames)); + InheritedProfileCombo("Pass 3 style", &config->DlssNrPass3Style, + inheritedStyleNames, IM_ARRAYSIZE(inheritedStyleNames)); + + HelpMarker("These select different built-in profiles inside the same NVIDIA model DLL." + "\nThey do not load a different model file per pass. Preset values are 0..3;" + "\nstyles are 0 standard, 1 natural, and 2 cinematic. The names are based on" + "\ncommunity testing because NVIDIA has not published this integration API."); + DeferredSlider("Intensity", &config->DlssNrIntensity, 0.0f, 2.0f, 1.0f); HelpMarker("The model's own strength control, applied inside it. Distinct from detail" diff --git a/OptiScaler/dlssnr/design/pre-sr-multipass.md b/OptiScaler/dlssnr/design/pre-sr-multipass.md index 600f06368..d33e5871b 100644 --- a/OptiScaler/dlssnr/design/pre-sr-multipass.md +++ b/OptiScaler/dlssnr/design/pre-sr-multipass.md @@ -7,6 +7,9 @@ This experimental branch adds two opt-in controls to the `[DlssNr]` section: deliberately forced to remain post-upscale because its input contract is not compatible with the PR #6 pre-SR path. - `Passes=N` selects one to three sequential model layers. The default is `1`. +- `Pass2Preset`, `Pass2Style`, `Pass3Preset`, and `Pass3Style` optionally select a different built-in + profile for later layers. `auto` inherits pass 1. These are profiles inside one model runtime, not + separate model DLLs. ## Multipass lifetime and data flow @@ -17,6 +20,12 @@ while the DX11/Vulkan bridges supply their post-submit frame counter. At most on created per submitted frame. A failed extra creation is latched and the ready contiguous prefix remains active, rather than reusing the main feature or retrying every frame. +Preset and style are read when a layer's feature is created. Pass 1 uses `Preset` and `Style`; later +passes inherit those values unless their override is set. Changing an active layer's profile parks the +whole generation for deferred release and rebuilds it through the same one-feature-per-submission +sequence. Changing an inactive layer does not disturb pass 1; its profile is read when that layer is +later enabled. + The frame is encoded once. Its base proxy remains immutable while model answers ping-pong through two same-format, same-size resources: diff --git a/OptiScaler/shaders/dlssnr/DlssNr_Dx12.cpp b/OptiScaler/shaders/dlssnr/DlssNr_Dx12.cpp index 64ca1a0e3..1386cc3c8 100644 --- a/OptiScaler/shaders/dlssnr/DlssNr_Dx12.cpp +++ b/OptiScaler/shaders/dlssnr/DlssNr_Dx12.cpp @@ -343,10 +343,11 @@ struct NrState float guideMvScaleX = 1.0f; float guideMvScaleY = 1.0f; - // The values the live feature was created with, and when a difference from them was first seen. - unsigned int builtPreset = 0; + // The values each live feature was created with. Preset and style may differ per layer; the + // remaining strengths are intentionally shared by the stack. + unsigned int builtPreset[DlssNr::MaxPassCount] = {}; float builtIntensity = 0.0f; - unsigned int builtStyle = 0; + unsigned int builtStyle[DlssNr::MaxPassCount] = {}; float builtLocalStructure = 0.0f; float builtLocalTone = 0.0f; float builtSkinStructure = 0.0f; @@ -1308,22 +1309,61 @@ void SetExtras(const Config& cfg, ID3D12Resource* ui, ID3D12Resource* backbuffer uiWidth, uiHeight, bbWidth, bbHeight); } -bool TuningMatchesFeature(const Config& cfg) +unsigned int PassPreset(const Config& cfg, unsigned int pass) { - return g_nr.builtPreset == cfg.DlssNrPreset.value_or_default() && - g_nr.builtIntensity == cfg.DlssNrIntensity.value_or_default() && - g_nr.builtStyle == cfg.DlssNrStyle.value_or_default() && - g_nr.builtLocalStructure == cfg.DlssNrLocalStructure.value_or_default() && - g_nr.builtLocalTone == cfg.DlssNrLocalTone.value_or_default() && - g_nr.builtSkinStructure == cfg.DlssNrSkinStructure.value_or_default() && - g_nr.builtAutoMask == cfg.DlssNrAutoMask.value_or_default(); + const unsigned int base = std::min(cfg.DlssNrPreset.value_or_default(), 3u); + + if (pass == 1 && cfg.DlssNrPass2Preset.has_value()) + return std::min(cfg.DlssNrPass2Preset.value(), 3u); + + if (pass == 2 && cfg.DlssNrPass3Preset.has_value()) + return std::min(cfg.DlssNrPass3Preset.value(), 3u); + + return base; +} + +unsigned int PassStyle(const Config& cfg, unsigned int pass) +{ + const unsigned int base = std::min(cfg.DlssNrStyle.value_or_default(), 2u); + + if (pass == 1 && cfg.DlssNrPass2Style.has_value()) + return std::min(cfg.DlssNrPass2Style.value(), 2u); + + if (pass == 2 && cfg.DlssNrPass3Style.has_value()) + return std::min(cfg.DlssNrPass3Style.value(), 2u); + + return base; +} + +bool TuningMatchesFeature(const Config& cfg, unsigned int requestedPasses) +{ + if (g_nr.builtIntensity != cfg.DlssNrIntensity.value_or_default() || + g_nr.builtLocalStructure != cfg.DlssNrLocalStructure.value_or_default() || + g_nr.builtLocalTone != cfg.DlssNrLocalTone.value_or_default() || + g_nr.builtSkinStructure != cfg.DlssNrSkinStructure.value_or_default() || + g_nr.builtAutoMask != cfg.DlssNrAutoMask.value_or_default()) + return false; + + for (unsigned int pass = 0; pass < requestedPasses; ++pass) + { + // A profile cannot be stale until its feature exists. This lets a user prepare pass 2 or 3 + // while running fewer layers without needlessly rebuilding pass 1. + if (pass > 0 && g_nr.passFeature[pass] == nullptr) + continue; + + if (g_nr.builtPreset[pass] != PassPreset(cfg, pass) || + g_nr.builtStyle[pass] != PassStyle(cfg, pass)) + return false; + } + + return true; } -void RecordBuiltTuning(const Config& cfg) +void RecordBuiltPrimaryTuning(const Config& cfg) { - g_nr.builtPreset = cfg.DlssNrPreset.value_or_default(); + g_nr.builtPreset[0] = PassPreset(cfg, 0); g_nr.builtIntensity = cfg.DlssNrIntensity.value_or_default(); - g_nr.builtStyle = cfg.DlssNrStyle.value_or_default(); + g_nr.builtStyle[0] = PassStyle(cfg, 0); g_nr.builtLocalStructure = cfg.DlssNrLocalStructure.value_or_default(); g_nr.builtLocalTone = cfg.DlssNrLocalTone.value_or_default(); g_nr.builtSkinStructure = cfg.DlssNrSkinStructure.value_or_default(); @@ -1713,7 +1753,7 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c // effect when the feature is rebuilt. TuningMatchesFeature was written to notice that and then // never called, which is why every one of these controls appeared to do nothing until something // else -- a resolution change -- happened to force a rebuild by accident. - const bool tuningChanged = !TuningMatchesFeature(cfg); + const bool tuningChanged = !TuningMatchesFeature(cfg, requestedPasses); if (g_nr.feature != nullptr && (resolutionChanged || tuningChanged || placementChanged)) { @@ -1833,8 +1873,8 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c g_nr.feature = g_nr.create(snippet->wstring().c_str(), State::Instance().NVNGX_ApplicationDataPath.c_str(), device, cmdList, g_nr.capabilityParams, workWidth, workHeight, - (int) cfg.DlssNrPreset.value_or_default(), - cfg.DlssNrIntensity.value_or_default(), (int) cfg.DlssNrStyle.value_or_default(), + (int) PassPreset(cfg, 0), + cfg.DlssNrIntensity.value_or_default(), (int) PassStyle(cfg, 0), cfg.DlssNrLocalStructure.value_or_default(), cfg.DlssNrLocalTone.value_or_default(), cfg.DlssNrSkinStructure.value_or_default(), cfg.DlssNrAutoMask.value_or_default() ? 1 : 0, @@ -1864,11 +1904,11 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c g_nr.reset = true; g_nr.featurePendingSubmission = true; g_nr.featureCreateEpoch = frame.SubmissionEpoch; - RecordBuiltTuning(cfg); + RecordBuiltPrimaryTuning(cfg); LOG_INFO("DLSS-NR running {} SR: target {}x{}, model {}x{}, guides {}x{} " "(preset {}, intensity {}, style {}, build epoch {})", frame.BeforeUpscale ? "before" : "after", width, height, workWidth, workHeight, - guideWidth, guideHeight, g_nr.builtPreset, g_nr.builtIntensity, g_nr.builtStyle, + guideWidth, guideHeight, g_nr.builtPreset[0], g_nr.builtIntensity, g_nr.builtStyle[0], frame.SubmissionEpoch); // Creating and evaluating a feature in the same command list is the dice-roll that hung the @@ -1962,8 +2002,8 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c g_nr.passFeature[pass] = g_nr.create( snippet->wstring().c_str(), State::Instance().NVNGX_ApplicationDataPath.c_str(), device, cmdList, g_nr.capabilityParams, workWidth, workHeight, - (int) cfg.DlssNrPreset.value_or_default(), cfg.DlssNrIntensity.value_or_default(), - (int) cfg.DlssNrStyle.value_or_default(), + (int) PassPreset(cfg, pass), cfg.DlssNrIntensity.value_or_default(), + (int) PassStyle(cfg, pass), cfg.DlssNrLocalStructure.value_or_default(), // Local tone belongs to the frame and is applied by pass zero only. 0.0f, cfg.DlssNrSkinStructure.value_or_default(), @@ -1971,11 +2011,15 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c if (g_nr.passFeature[pass] != nullptr) { + g_nr.builtPreset[pass] = PassPreset(cfg, pass); + g_nr.builtStyle[pass] = PassStyle(cfg, pass); g_nr.passNeedsReset[pass] = true; g_nr.passPendingSubmission[pass] = true; g_nr.passCreateEpoch[pass] = frame.SubmissionEpoch; - LOG_INFO("DLSS-NR: feature for pass {} built at epoch {}; waiting for submission", - pass + 1, frame.SubmissionEpoch); + LOG_INFO("DLSS-NR: feature for pass {} built with preset {}, style {} at epoch {}; " + "waiting for submission", + pass + 1, g_nr.builtPreset[pass], g_nr.builtStyle[pass], + frame.SubmissionEpoch); } else { @@ -2441,7 +2485,7 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c cmdList, passFeature, g_nr.capabilityParams, passInput, depthIn, motionIn, passOutput, workWidth, workHeight, guideWidth, guideHeight, g_nr.guideDepthInverted ? 1 : 0, passReset ? 1 : 0, cfg.DlssNrIntensity.value_or_default(), - (int) cfg.DlssNrStyle.value_or_default(), cfg.DlssNrLocalStructure.value_or_default(), + (int) PassStyle(cfg, pass), cfg.DlssNrLocalStructure.value_or_default(), passTone, cfg.DlssNrSkinStructure.value_or_default(), cfg.DlssNrAutoMask.value_or_default() ? 1 : 0, g_nr.guideMvScaleX * mvToWork, g_nr.guideMvScaleY * mvToWork); @@ -2514,12 +2558,12 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c const NVSDK_NGX_Result presetResult = g_nr.capabilityParams->Get("DLSSNR.Hint.Render.Preset", &preset); LOG_DEBUG("DLSS-NR readback DLSSNR.Hint.Render.Preset -> {} (result 0x{:X}, we wrote {})", preset, - (uint32_t) presetResult, cfg.DlssNrPreset.value_or_default()); + (uint32_t) presetResult, PassPreset(cfg, 0)); LOG_DEBUG("DLSS-NR wrote intensity {}, local structure {}, local tone {}, skin {}, style {}", cfg.DlssNrIntensity.value_or_default(), cfg.DlssNrLocalStructure.value_or_default(), cfg.DlssNrLocalTone.value_or_default(), cfg.DlssNrSkinStructure.value_or_default(), - cfg.DlssNrStyle.value_or_default()); + PassStyle(cfg, 0)); } if (result == NVSDK_NGX_Result_Success) diff --git a/README.md b/README.md index 69447e432..f8cb511dd 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ The upstream fork already provided experimental direct access to NVIDIA DLSS Neu - **Optional Neural Rendering before DLSS Super Resolution.** The model can process the DLSS input image—such as 1920x1080 in 4K Performance mode—before DLSS upscales it to the display resolution. - **Configurable multipass processing.** `[DlssNr] Passes=1..3` runs one, two, or three sequential neural evaluations. Each pass has independent persistent history; the final result is composed once against the original base image. +- **Per-pass model profiles.** Passes 2 and 3 can inherit pass 1 or select their own built-in preset and style (`standard`, `natural`, or `cinematic`) without loading competing model DLLs. - **Guarded fallbacks.** Ray Reconstruction remains post-SR, and padded or offset dynamic-resolution inputs fall back to the existing post-SR path instead of using unsafe dimensions. - **Matching overlay and INI controls.** `RunBeforeSR` and `Passes` are exposed in both configuration and the OptiScaler overlay. - **Verified BG3 path.** Baldur's Gate 3 was tested with two neural passes at 1920x1080 followed by DLSS Super Resolution to 3840x2160. @@ -15,6 +16,12 @@ Download the exact tested BG3 package from the [BG3 pre-SR multipass release](ht Implementation details and safety invariants are documented in [the pre-SR multipass design note](OptiScaler/dlssnr/design/pre-sr-multipass.md). The remainder of this README is the upstream OptiScaler documentation. +### Compatibility scope + +The implementation contains no BG3-specific executable names, offsets, or shaders. It is designed for 64-bit games whose DLSS Super Resolution call reaches OptiScaler's Direct3D 12 path, including its Direct3D 11/Vulkan-to-DX12 bridges. It has also run in Hogwarts Legacy and Cyberpunk 2077. Compatibility still depends on the game exposing valid colour, depth, motion-vector, resolution, and command-submission data through its upscaler integration. + +Ray Reconstruction deliberately uses the post-SR path. Native Vulkan currently retains the upstream post-SR implementation. Games with unusual loaders, multiple swapchains, offset/padded dynamic-resolution textures, anti-cheat, or another `dxgi.dll` mod may need a different OptiScaler proxy name or will use the guarded post-SR fallback. + ---
From 218f365bcf8f716d24a493a42480151137bdfe7e Mon Sep 17 00:00:00 2001 From: wilsjo2 <97138003+wilsjo2@users.noreply.github.com> Date: Sun, 6 Sep 2026 10:17:12 +1000 Subject: [PATCH 4/7] docs: add general package download --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f8cb511dd..5f12b91bc 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,12 @@ The upstream fork already provided experimental direct access to NVIDIA DLSS Neu - **Matching overlay and INI controls.** `RunBeforeSR` and `Passes` are exposed in both configuration and the OptiScaler overlay. - **Verified BG3 path.** Baldur's Gate 3 was tested with two neural passes at 1920x1080 followed by DLSS Super Resolution to 3840x2160. -Download the exact tested BG3 package from the [BG3 pre-SR multipass release](https://github.com/wilsjo2/OptiScaler-DLSSNR-PreSR-Multipass/releases/tag/bg3-presr-multipass-e16d5866). NVIDIA's proprietary `nvngx_dlssnr.dll` is required but is **not redistributed** here. +Downloads: + +- [General experimental package with per-pass profiles](https://github.com/wilsjo2/OptiScaler-DLSSNR-PreSR-Multipass/releases/tag/general-per-pass-profiles-facc24f6) — game-neutral defaults; build-tested, published as a prerelease while broader runtime reports come in. +- [Exact original BG3-tested package](https://github.com/wilsjo2/OptiScaler-DLSSNR-PreSR-Multipass/releases/tag/bg3-presr-multipass-e16d5866) — the earlier build used for the 1920x1080 to 3840x2160 validation. + +NVIDIA's proprietary `nvngx_dlssnr.dll` is required but is **not redistributed** here. Implementation details and safety invariants are documented in [the pre-SR multipass design note](OptiScaler/dlssnr/design/pre-sr-multipass.md). The remainder of this README is the upstream OptiScaler documentation. From c10f96f6bd3037d4770e1ff8a0956f8d00cfe94c Mon Sep 17 00:00:00 2001 From: wilsjo2 <97138003+wilsjo2@users.noreply.github.com> Date: Sun, 6 Sep 2026 10:55:03 +1000 Subject: [PATCH 5/7] packaging: support cross-generation DLSS-NR installs --- INSTALL-DLSSNR.md | 105 ++++++++++++++++++++++++++++++++++++ OptiScaler.ini | 8 +-- OptiScaler/dlssnr/README.md | 5 +- README.md | 45 ++++++++++++++-- package_release.ps1 | 84 +++++++++++++++++++++++++---- setup_windows.bat | 62 +++++++++++++-------- 6 files changed, 271 insertions(+), 38 deletions(-) create mode 100644 INSTALL-DLSSNR.md diff --git a/INSTALL-DLSSNR.md b/INSTALL-DLSSNR.md new file mode 100644 index 000000000..31c82e4f4 --- /dev/null +++ b/INSTALL-DLSSNR.md @@ -0,0 +1,105 @@ +# Install DLSS Neural Rendering + +This fork is experimental. Do not use injection mods in anti-cheat-protected multiplayer games. + +## Requirements + +- A 64-bit game whose temporal upscaler reaches an OptiScaler D3D12 path. Native D3D12 is preferred; + supported D3D11 and Vulkan games can use OptiScaler's D3D12 bridges. +- NVIDIA driver 616.56 or newer. +- The complete release archive from this repository. It includes `setup_windows.bat`, the + `OptiScaler` backend folder, `OptiScaler.dll`, `OptiScaler.ini`, and `nvngx.dll_dlssnr.dll`. +- A separately obtained `nvngx_dlssnr.dll` 310.8 runtime appropriate for the GPU. + +The two similarly named files are different and both are required: + +| File | Purpose | +|---|---| +| `nvngx.dll_dlssnr.dll` | Open-source forwarder supplied by this project | +| `nvngx_dlssnr.dll` | NVIDIA-derived Neural Rendering runtime supplied separately by the user | + +## Choose the correct runtime + +| GPU | Runtime | SHA-256 | +|---|---|---| +| RTX 50 | Original NVIDIA-signed 310.8 | `E16BCF15E16E13F527491CDF7845B2FE6521A738D8F7C9C721866A8496E1FC8E` | +| RTX 20 / 30 / 40 | ShortFuse cross-generation 310.8 | `E67DEE209320CDAFE0E93E45675D7AA34323A53ACC57A72B2E40A181581C989A` | + +For RTX 20/30/40, obtain the compatibility runtime from +[ShortFuse's pinned RenoDX thread](https://discord.com/channels/1408098019194310818/1543976771920330884). +The compatibility DLL automatically selects an FP16-oriented path on RTX 20/30, an Ada-compatible +path on RTX 40, and leaves the RTX 50 path unchanged. + +The compatibility runtime is modified, so Windows reports the original NVIDIA signature as invalid. +That is expected for this exact hash, but it removes the assurance provided by Authenticode. Keep +security protection enabled, use only the pinned developer attachment, and verify the SHA-256 value: + +```powershell +Get-FileHash .\nvngx_dlssnr.dll -Algorithm SHA256 +``` + +## Install + +1. Close the game and its launcher. +2. Find the directory containing the real game executable, which is often below the game's root. +3. Back up any existing proxy DLL, `OptiScaler.ini`, and OptiScaler installation. +4. Extract the entire release archive into that executable directory. Do not copy only the two DLLs. +5. Put the correct `nvngx_dlssnr.dll` from the table above in the same directory. +6. Run `setup_windows.bat`. It renames `OptiScaler.dll` to a proxy filename the game will load and + creates an uninstaller. `dxgi.dll` is the usual first choice. The validated Cyberpunk 2077 setup + used `dbghelp.dll` to coexist with its existing loaders. +7. Enable Neural Rendering in the `Insert` overlay, or edit `OptiScaler.ini`: + +```ini +[DlssNr] +Enabled=true +RunBeforeSR=true +Passes=1 +WorkingScale=1.0 +``` + +Begin with one pass. RTX 20/30 use a much heavier FP16 path, so reduced model resolution may be +necessary. With `RunBeforeSR=true`, DLSS Performance at 3840x2160 gives the model a 1920x1080 input +before Super Resolution. `WorkingScale=0.5` lowers only the model's work resolution further. + +For a portable setup, leave the process filter disabled: + +```ini +[ProcessFilter] +TargetProcessName=auto +``` + +Do not copy an INI containing another game's executable name. A mismatch intentionally puts +OptiScaler into pass-through mode, which means no menu and no Neural Rendering. + +## Game notes + +- **Baldur's Gate 3:** install beside `bg3.exe` / `bg3_dx11.exe` in `Baldurs Gate 3\bin`. + Use `Dx12Upscaler=dlss` for `bg3.exe`, or `Dx11Upscaler=dlss_12` for `bg3_dx11.exe`. +- **Hogwarts Legacy:** install in `Phoenix\Binaries\Win64`; `dxgi.dll` was validated. +- **Cyberpunk 2077:** install in `bin\x64`; `dbghelp.dll` was validated on the development machine. + Existing CET/RED4ext/ReShade loaders can require a different proxy or correct chaining. + +Do not install the RenoDX DLSS add-on merely to obtain its compatibility runtime. This OptiScaler +fork drives `nvngx_dlssnr.dll` itself, and two Neural Rendering injectors can conflict. + +## Diagnose a missing menu + +Set: + +```ini +[Log] +LogToFile=true +LogLevel=2 +``` + +Then launch into a rendered scene and press `Insert` (`Alt+Insert` can help on some keyboard layouts). + +- No `OptiScaler.log` beside the executable: the proxy was not loaded. Check the directory, proxy + filename, antivirus quarantine, and conflicts with another DLL using the same proxy name. +- The log says `OptiScaler ... loaded` and `working as ...`: injection succeeded. A remaining problem + belongs to the overlay input or Neural Rendering initialization, not the loader. +- `the model would not initialise`: on RTX 20/30/40, first check that the runtime hash is the + compatibility `E67DEE...` build rather than the original `E16BC...` build. +- The menu toggles but does not accept input: try `[Hotfix] ManualInputPolling=true` and test without + conflicting overlays. diff --git a/OptiScaler.ini b/OptiScaler.ini index 177db3b18..e05158881 100644 --- a/OptiScaler.ini +++ b/OptiScaler.ini @@ -1553,9 +1553,11 @@ OutputResourceBarrier=auto ToggleKey=auto ; DLSS 5 Neural Rendering. Synthesises detail in the upscaler's output, before frame generation sees it. ; -; Needs nvngx_dlssnr.dll from a driver that ships it, placed beside OptiScaler or the game executable, -; plus nvngx.dll_dlssnr.dll from this package. The snippet refuses callers whose module path does not -; contain "nvngx.dll", which is the only reason that second file exists. +; Needs nvngx_dlssnr.dll 310.8 placed beside OptiScaler or the game executable, plus +; nvngx.dll_dlssnr.dll from this package. RTX 50 uses the original NVIDIA runtime. RTX 20/30/40 needs +; ShortFuse's cross-generation compatibility runtime from the pinned RenoDX thread; see +; INSTALL-DLSSNR.md for its SHA-256. The snippet refuses callers whose module path does not contain +; "nvngx.dll", which is the only reason that second file exists. ; ; Undocumented and driven directly, so none of it is officially supported. ; true or false - Default (auto) is false diff --git a/OptiScaler/dlssnr/README.md b/OptiScaler/dlssnr/README.md index 72e8e745c..ea6722a54 100644 --- a/OptiScaler/dlssnr/README.md +++ b/OptiScaler/dlssnr/README.md @@ -2,7 +2,10 @@ A self-contained module that drives NVIDIA's DLSS Neural Rendering model (`nvngx_dlssnr.dll`, NGX feature 18) over the frames OptiScaler already handles. Nothing in it is officially supported by -NVIDIA; the model ships in driver packages and is not redistributed here. +NVIDIA, and the proprietary runtime is not redistributed here. RTX 50 uses the original 310.8 +runtime; RTX 20/30/40 requires ShortFuse's cross-generation 310.8 compatibility runtime. The latter +preserves the model while supplying architecture-compatible GPU programs. See +[`INSTALL-DLSSNR.md`](../../INSTALL-DLSSNR.md) for the pinned source, hashes, and security caveat. ## For maintainers: how to remove it diff --git a/README.md b/README.md index 5f12b91bc..ba7f32255 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,54 @@ The upstream fork already provided experimental direct access to NVIDIA DLSS Neu - **Per-pass model profiles.** Passes 2 and 3 can inherit pass 1 or select their own built-in preset and style (`standard`, `natural`, or `cinematic`) without loading competing model DLLs. - **Guarded fallbacks.** Ray Reconstruction remains post-SR, and padded or offset dynamic-resolution inputs fall back to the existing post-SR path instead of using unsafe dimensions. - **Matching overlay and INI controls.** `RunBeforeSR` and `Passes` are exposed in both configuration and the OptiScaler overlay. -- **Verified BG3 path.** Baldur's Gate 3 was tested with two neural passes at 1920x1080 followed by DLSS Super Resolution to 3840x2160. +- **Verified BG3 path.** Baldur's Gate 3 was tested through the `bg3_dx11.exe` D3D11-to-D3D12 bridge with two neural passes at 1920x1080 followed by DLSS Super Resolution to 3840x2160. Downloads: -- [General experimental package with per-pass profiles](https://github.com/wilsjo2/OptiScaler-DLSSNR-PreSR-Multipass/releases/tag/general-per-pass-profiles-facc24f6) — game-neutral defaults; build-tested, published as a prerelease while broader runtime reports come in. -- [Exact original BG3-tested package](https://github.com/wilsjo2/OptiScaler-DLSSNR-PreSR-Multipass/releases/tag/bg3-presr-multipass-e16d5866) — the earlier build used for the 1920x1080 to 3840x2160 validation. +- [Portable cross-generation package](https://github.com/wilsjo2/OptiScaler-DLSSNR-PreSR-Multipass/releases/tag/v0.3.0-crossgen-portable) — the complete installer and backend layout, with game-neutral defaults and RTX 20/30/40/50 runtime guidance. +- The earlier `general-per-pass-profiles-facc24f6` and `bg3-presr-multipass-e16d5866` packages are retained only as historical validation artifacts. They are incomplete for a clean installation and should not be redistributed. NVIDIA's proprietary `nvngx_dlssnr.dll` is required but is **not redistributed** here. +### GPU and runtime compatibility + +The Neural Rendering network can run on RTX 20, 30, 40, and 50-series GPUs, but not with the same +runtime binary on every architecture: + +| GPU | Required `nvngx_dlssnr.dll` 310.8 runtime | SHA-256 | +|---|---|---| +| RTX 50 | Original NVIDIA-signed runtime | `E16BCF15E16E13F527491CDF7845B2FE6521A738D8F7C9C721866A8496E1FC8E` | +| RTX 20 / 30 / 40 | ShortFuse cross-generation compatibility runtime from the pinned RenoDX thread | `E67DEE209320CDAFE0E93E45675D7AA34323A53ACC57A72B2E40A181581C989A` | + +The compatibility runtime preserves the model but replaces or backports GPU programs that the older +architectures cannot execute: the RTX 20/30 path is predominantly FP16, while the RTX 40 path +backports Blackwell-only operations. It is a modified NVIDIA-derived binary, so its original NVIDIA +Authenticode signature no longer validates. Obtain it only from +[ShortFuse's pinned RenoDX thread](https://discord.com/channels/1408098019194310818/1543976771920330884) +and verify the hash above. Do not use random DLL mirrors. Driver 616.56 or newer is required. + +### Quick install + +1. Close the game, then back up any existing OptiScaler/ReShade proxy DLL and INI files. +2. Extract the **entire** release archive beside the game's real 64-bit executable. Keep the + `OptiScaler` and `Licenses` directories with the DLLs; copying `OptiScaler.dll` alone will not work. +3. Add the GPU-appropriate `nvngx_dlssnr.dll` from the table above to that same directory and verify + its SHA-256. Both it and this package's differently named `nvngx.dll_dlssnr.dll` must be present. +4. Run `setup_windows.bat` from that directory. Choose `dxgi.dll` first unless the game or another + loader already uses that name, and answer **NVIDIA** when prompted. The script renames + `OptiScaler.dll` to the selected loadable proxy and confirms which Neural Rendering runtime it found. +5. Leave `[ProcessFilter] TargetProcessName=auto` for a portable installation. Start the game, enter a + rendered scene, press `Insert`, and enable **DLSS Neural Rendering**. Start with one pass. +6. For pre-upscale operation, set `RunBeforeSR=true` and select DLSS in the game. At 3840x2160 output, + DLSS Performance supplies a 1920x1080 input to the model before Super Resolution. + +If no menu or `OptiScaler.log` appears, the proxy did not load: re-check the executable directory, +proxy filename, antivirus quarantine, and conflicts with an existing loader. Do not copy an INI whose +`TargetProcessName` names a different game; that deliberately activates pass-through mode. + +Read [INSTALL-DLSSNR.md](INSTALL-DLSSNR.md) for the full instructions, per-game paths, loader notes, +configuration example, and diagnostics. + Implementation details and safety invariants are documented in [the pre-SR multipass design note](OptiScaler/dlssnr/design/pre-sr-multipass.md). The remainder of this README is the upstream OptiScaler documentation. ### Compatibility scope diff --git a/package_release.ps1 b/package_release.ps1 index 2ee0a5916..51b2b84ee 100644 --- a/package_release.ps1 +++ b/package_release.ps1 @@ -19,11 +19,23 @@ $ErrorActionPreference = "Stop" # than one now -- the experiment runs in a git worktree beside the main tree, and a hardcoded root # silently packages the other one's build output while reporting success. $root = Split-Path -Parent $PSCommandPath -$msb = "C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe" $stage = "$root\release\$Version" $zip = "$root\release\OptiScaler-DLSSNR-$Version.zip" if (-not $SkipBuild) { + $msb = (Get-Command MSBuild.exe -ErrorAction SilentlyContinue).Source + if (-not $msb) { + $msb = @( + "C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe", + "C:\Program Files\Microsoft Visual Studio\2022\BuildTools\MSBuild\Current\Bin\MSBuild.exe", + "C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe", + "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin\MSBuild.exe" + ) | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1 + } + if (-not $msb) { + throw 'MSBuild.exe was not found. Install Visual Studio C++ build tools or use -SkipBuild with a verified existing build.' + } + foreach ($proj in @("$root\OptiScaler\dlssnr\forwarder\dlssnr_forwarder.vcxproj", "$root\OptiScaler.sln")) { $out = & $msb $proj /p:Configuration=Release /p:Platform=x64 /v:minimal /m 2>&1 $err = $out | Select-String "error " @@ -59,23 +71,45 @@ if ($missing.Count -gt 0) { if (Test-Path $stage) { Remove-Item $stage -Recurse -Force } New-Item -ItemType Directory -Force -Path $stage | Out-Null -# Files, then folders. Anything not named here does not ship. -$files = @( +# Files, then folders. Anything not named here does not ship. Runtime files and user-facing +# instructions come from the checkout rather than the build directory so a stale post-build copy +# cannot put old GPU guidance or an old INI into a fresh package. +$buildFiles = @( "OptiScaler.dll", + "!! EXTRACT ALL FILES TO GAME FOLDER !!" +) + +$sourceFiles = @( "OptiScaler.ini", "setup_windows.bat", "setup_linux.sh", - "!! EXTRACT ALL FILES TO GAME FOLDER !!", - "READ ME - DLSS Neural Rendering.txt" + "README.md", + "INSTALL-DLSSNR.md", + "LICENSE" ) -foreach ($f in $files) { - if (Test-Path "$src\$f") { Copy-Item "$src\$f" "$stage\$f" -Force } - else { Write-Host "missing from build output: $f" } +foreach ($f in $buildFiles) { + $source = "$src\$f" + if (-not (Test-Path -LiteralPath $source)) { + throw "Required build output is missing: $source" + } + Copy-Item -LiteralPath $source -Destination "$stage\$f" -Force +} + +foreach ($f in $sourceFiles) { + $source = "$root\$f" + if (-not (Test-Path -LiteralPath $source)) { + throw "Required release file is missing: $source" + } + Copy-Item -LiteralPath $source -Destination "$stage\$f" -Force } foreach ($d in @("Licenses", "OptiScaler")) { - Copy-Item "$src\$d" "$stage\$d" -Recurse -Force + $source = "$src\$d" + if (-not (Test-Path -LiteralPath $source -PathType Container)) { + throw "Required dependency directory is missing: $source" + } + Copy-Item -LiteralPath $source -Destination "$stage\$d" -Recurse -Force } Copy-Item $forwarder "$stage\nvngx.dll_dlssnr.dll" -Force @@ -99,6 +133,12 @@ Set-Content $iniPath $ini -Encoding utf8 -NoNewline $check = Select-String -Path $iniPath -Pattern '^LogToFile=|^LogLevel=' | ForEach-Object { $_.Line } Write-Host "log settings: $($check -join ', ')" +$targetProcess = Select-String -Path $iniPath -Pattern '^TargetProcessName=' | Select-Object -First 1 +if ($targetProcess.Line -ne 'TargetProcessName=auto') { + throw "REFUSING: portable package has a game-specific process filter: $($targetProcess.Line)" +} +Write-Host "process filter: portable (TargetProcessName=auto)" + # Belt and braces: nothing that is a build artifact, and nothing from the abandoned warp work, may # survive into the zip regardless of how it got into the staging folder. Get-ChildItem $stage -Recurse -Include *.exp, *.lib, *.pdb, *.ilk, *latewarp* | Remove-Item -Force @@ -119,6 +159,32 @@ if ($on) { Write-Host "ini verified: nothing switched on by default" +# The proprietary runtime must never slip into a public artifact. Its two approved hashes are +# documentation/diagnostic inputs only; users obtain the GPU-appropriate file themselves. +if (Get-ChildItem -LiteralPath $stage -Recurse -File | Where-Object { $_.Name -ieq 'nvngx_dlssnr.dll' }) { + throw 'REFUSING: proprietary nvngx_dlssnr.dll is present in the staging directory' +} + +$crossGenHash = 'E67DEE209320CDAFE0E93E45675D7AA34323A53ACC57A72B2E40A181581C989A' +foreach ($requiredTextFile in @("$stage\README.md", "$stage\INSTALL-DLSSNR.md", "$stage\setup_windows.bat")) { + if ((Get-Content -LiteralPath $requiredTextFile -Raw).IndexOf($crossGenHash, [StringComparison]::OrdinalIgnoreCase) -lt 0) { + throw "REFUSING: cross-generation runtime hash is missing from $requiredTextFile" + } +} +Write-Host "cross-generation guidance: present and hash-pinned" + +# Hash every shipped file after the staging tree is final. Use forward slashes so the list is easy +# to verify from PowerShell, 7-Zip, Linux, or Wine. +$checksumLines = Get-ChildItem -LiteralPath $stage -Recurse -File | + Where-Object { $_.Name -ne 'SHA256SUMS.txt' } | + Sort-Object FullName | + ForEach-Object { + $relative = [IO.Path]::GetRelativePath($stage, $_.FullName).Replace('\', '/') + "{0} *{1}" -f (Get-FileHash -Algorithm SHA256 -LiteralPath $_.FullName).Hash, $relative + } +[IO.File]::WriteAllLines("$stage\SHA256SUMS.txt", $checksumLines, [Text.UTF8Encoding]::new($false)) +Write-Host "checksums: $($checksumLines.Count) files" + if (Test-Path $zip) { Remove-Item $zip -Force } Compress-Archive -Path "$stage\*" -DestinationPath $zip -CompressionLevel Optimal diff --git a/setup_windows.bat b/setup_windows.bat index bc8a87773..905e323ec 100644 --- a/setup_windows.bat +++ b/setup_windows.bat @@ -407,23 +407,40 @@ echo. set setupSuccess=true REM --- DLSS 5 Neural Rendering --------------------------------------------------------------- -REM The model ships in an NVIDIA driver package and cannot be redistributed here, so the user has -REM to supply it. Saying where it goes, and whether it is already there, heads off the single most -REM common reason for the feature to sit silently disabled. +REM The proprietary model/runtime cannot be redistributed here, so the user has to supply either +REM NVIDIA's original build or the GPU-compatible community build. Hashing it here catches the +REM Blackwell-only runtime on an older card before the game fails without an obvious explanation. echo. echo ------------------------------------------------------------------ echo DLSS 5 Neural Rendering echo ------------------------------------------------------------------ echo. +set "dlssnrStockHash=E16BCF15E16E13F527491CDF7845B2FE6521A738D8F7C9C721866A8496E1FC8E" +set "dlssnrCrossGenHash=E67DEE209320CDAFE0E93E45675D7AA34323A53ACC57A72B2E40A181581C989A" +set "dlssnrHash=" +if exist "nvngx_dlssnr.dll" for /f "skip=1 tokens=*" %%H in ('certutil -hashfile "nvngx_dlssnr.dll" SHA256') do if not defined dlssnrHash set "dlssnrHash=%%H" +set "dlssnrHash=!dlssnrHash: =!" if exist "nvngx_dlssnr.dll" ( - echo nvngx_dlssnr.dll found here. Neural Rendering can run. + echo nvngx_dlssnr.dll found here. + echo SHA-256: !dlssnrHash! + echo. + if /i "!dlssnrHash!"=="!dlssnrCrossGenHash!" ( + echo ShortFuse cross-generation runtime detected. + echo This build supports RTX 20, 30, 40, and 50 series GPUs. + ) else if /i "!dlssnrHash!"=="!dlssnrStockHash!" ( + echo Original NVIDIA 310.8 runtime detected. + echo This file is for RTX 50. RTX 20, 30, and 40 users must replace + echo it with ShortFuse's pinned cross-generation compatibility runtime. + ) else ( + echo WARNING: This runtime hash is not one documented by this release. + echo Do not assume that it supports your GPU or came from a trusted source. + ) ) else ( echo nvngx_dlssnr.dll was NOT found in this folder. echo. - echo Neural Rendering needs it. It cannot ship with OptiScaler because - echo it comes from an NVIDIA driver package, so copy it into THIS - echo folder - the same one holding the game executable and the file - echo OptiScaler was just renamed to. + echo Neural Rendering needs it, but it cannot be redistributed here. + echo Copy the GPU-appropriate 310.8 runtime into THIS folder - the + echo same one holding the game executable and the renamed OptiScaler. echo. echo One copy per game. There is no shared or system-wide location. ) @@ -433,29 +450,30 @@ echo. echo nvngx.dll_dlssnr.dll ships in this package ^(about 13 KB^) echo nvngx_dlssnr.dll you supply it ^(about 165 MB^) echo. -echo To check you have the right file: Properties ^> Details should -echo read "NVIDIA DLSSNR" at about 165 MB. A file that size named -echo nvngx_dlssd.dll is this model misnamed, not Ray Reconstruction - -echo installing it as Ray Reconstruction breaks that instead. +echo Runtime required by GPU: +echo RTX 50 original NVIDIA 310.8 +echo !dlssnrStockHash! +echo RTX 20/30/40 ShortFuse cross-generation 310.8 +echo !dlssnrCrossGenHash! +echo. +echo Obtain the compatibility runtime only from ShortFuse's pinned thread +echo in the RenoDX Discord: https://discord.com/invite/renodx +echo Channel: dlss5-forum ^> Patched DLSS-NR for RTX20, RTX30, and RTX40 +echo. +echo The compatibility DLL is modified, so its NVIDIA signature does not +echo validate. Keep security protection on and verify the exact hash above. echo. echo Neural Rendering is OFF by default. Turn it on in the OptiScaler echo overlay under "DLSS Neural Rendering", or set Enabled=true under echo the DlssNr section of OptiScaler.ini. echo. -echo Needs an RTX 50 series card and a driver new enough to ship the -echo model. If it cannot run, the overlay says why rather than failing -echo quietly. +echo RTX 20, 30, 40, and 50 are supported with the correct runtime. +echo Driver 616.56 or newer is required. Start with one pass on RTX 20/30. echo. :end pause - -if "%setupSuccess%"=="true" ( - del "setup_linux.sh" - del "%~nx0" -) - -exit /b +exit /b 0 :create_uninstaller setlocal DisableDelayedExpansion From 8a3805e4decf5fff128c2bc9bec439c69146a573 Mon Sep 17 00:00:00 2001 From: wilsjo2 <97138003+wilsjo2@users.noreply.github.com> Date: Sun, 6 Sep 2026 14:33:28 +1000 Subject: [PATCH 6/7] DLSS-NR: add independent controls and history for native RR --- INSTALL-DLSSNR.md | 28 +++++++++++++++++++++ OptiScaler.ini | 8 ++++++ OptiScaler/Config.cpp | 9 +++++++ OptiScaler/Config.h | 3 +++ OptiScaler/dlssnr/DlssNrFeature_Dx12.h | 2 ++ OptiScaler/dlssnr/DlssNr_Menu.cpp | 20 ++++++++++++++- OptiScaler/shaders/dlssnr/DlssNr_Common.h | 2 ++ OptiScaler/shaders/dlssnr/DlssNr_Dx12.cpp | 30 +++++++++++++++++++---- README.md | 4 +++ setup_windows.bat | 6 +++++ 10 files changed, 106 insertions(+), 6 deletions(-) diff --git a/INSTALL-DLSSNR.md b/INSTALL-DLSSNR.md index 31c82e4f4..5bb3194f5 100644 --- a/INSTALL-DLSSNR.md +++ b/INSTALL-DLSSNR.md @@ -83,6 +83,34 @@ OptiScaler into pass-through mode, which means no menu and no Neural Rendering. Do not install the RenoDX DLSS add-on merely to obtain its compatibility runtime. This OptiScaler fork drives `nvngx_dlssnr.dll` itself, and two Neural Rendering injectors can conflict. +## Neural Rendering with native Ray Reconstruction + +In a game that already supports RR, enable RR in the game's settings and enable +**Apply after Ray Reconstruction (DX12)** in OptiScaler's Neural Rendering menu. The master +**Enable Neural Rendering** switch must also be on. Equivalent INI settings: + +```ini +[DlssNr] +Enabled=true +ApplyAfterRR=true +RRPasses=1 +RRWorkingScale=0.5 +``` + +RR reconstructs and upscales first. NR then processes that output before frame generation. +`RunBeforeSR` does not override this order. At 4K output, `RRWorkingScale=0.5` runs NR at +1920x1080 and resizes its edit for composition; it does not reduce RR's own resolution. +`RRPasses=1..3` is independent of ordinary `Passes`, while per-pass model profiles are shared. +Switching between SR and RR rebuilds NR history even when their dimensions match. +These controls apply to D3D12 and its bridges, not the upstream native Vulkan NR path. + +If Cyberpunk's RR option is greyed out with this fork, avoid the `d3d12.dll` proxy: an +[upstream report](https://github.com/Dagherbou/OptiScaler_DLSSNR/issues/8) confirmed that using +`dxgi.dll` resolved a Streamline conflict. Back up existing loaders before changing the proxy. +Keep the game's genuine `nvngx_dlssd.dll` (RR) separate from `nvngx_dlssnr.dll` (NR). +For an RR-only comparison, disable the master NR switch; “Apply the model” merely hides the edit +and still incurs NR's GPU cost. Successful RR initialization alone does not prove image quality. + ## Diagnose a missing menu Set: diff --git a/OptiScaler.ini b/OptiScaler.ini index e05158881..45132c95f 100644 --- a/OptiScaler.ini +++ b/OptiScaler.ini @@ -1569,6 +1569,14 @@ Enabled=auto ; true or false - Default (auto) is false RunBeforeSR=auto +; D3D12 and its bridges: opt in to NR after the game's native Ray Reconstruction. +; RR performs denoising/upscaling first. This never enables RR in unsupported games. +; Defaults: false, one pass, half of RR output width/height (1080p NR at 4K output). +; RRPasses is clamped to 1..3; RRWorkingScale to 0.25..2.0. +ApplyAfterRR=auto +RRPasses=auto +RRWorkingScale=auto + ; Number of sequential model layers between one encode and one final composition. Each extra layer ; consumes the preceding model output and owns a separate persistent feature/history. 1 is normal; ; 2 and 3 are deliberately over-processed and cost almost exactly 2x and 3x the model time. The final diff --git a/OptiScaler/Config.cpp b/OptiScaler/Config.cpp index 0d9ee0664..da4ddb225 100644 --- a/OptiScaler/Config.cpp +++ b/OptiScaler/Config.cpp @@ -318,6 +318,9 @@ bool Config::Reload(std::filesystem::path iniPath) // --- DLSS 5 Neural Rendering (OptiScaler/dlssnr) --- DlssNrEnabled.set_from_config(readBool("DlssNr", "Enabled")); DlssNrRunBeforeSr.set_from_config(readBool("DlssNr", "RunBeforeSR")); + DlssNrApplyAfterRR.set_from_config(readBool("DlssNr", "ApplyAfterRR")); + DlssNrRRPasses.set_from_config(readUInt("DlssNr", "RRPasses")); + DlssNrRRWorkingScale.set_from_config(readFloat("DlssNr", "RRWorkingScale")); DlssNrToggleKey.set_from_config(readInt("DlssNr", "ToggleKey")); DlssNrTransferStrength.set_from_config(readFloat("DlssNr", "TransferStrength")); DlssNrColourStrength.set_from_config(readFloat("DlssNr", "ColourStrength")); @@ -1205,6 +1208,12 @@ bool Config::SaveIni() ini.SetValue("DlssNr", "Enabled", GetBoolValue(Instance()->DlssNrEnabled.value_for_config()).c_str()); ini.SetValue("DlssNr", "RunBeforeSR", GetBoolValue(Instance()->DlssNrRunBeforeSr.value_for_config()).c_str()); + ini.SetValue("DlssNr", "ApplyAfterRR", + GetBoolValue(Instance()->DlssNrApplyAfterRR.value_for_config()).c_str()); + ini.SetValue("DlssNr", "RRPasses", + GetIntValue(Instance()->DlssNrRRPasses.value_for_config()).c_str()); + ini.SetValue("DlssNr", "RRWorkingScale", + GetFloatValue(Instance()->DlssNrRRWorkingScale.value_for_config()).c_str()); { auto toggle = Instance()->DlssNrToggleKey.value_for_config(); ini.SetValue("DlssNr", "ToggleKey", GetIntValue(toggle, toggle > 0).c_str()); diff --git a/OptiScaler/Config.h b/OptiScaler/Config.h index fd45b3a67..d6d267c40 100644 --- a/OptiScaler/Config.h +++ b/OptiScaler/Config.h @@ -260,6 +260,9 @@ class Config // Run the NR pass on the upscaler's colour input, at render resolution, immediately before SR. // Off preserves the v0.2.0 post-upscale placement. CustomOptional DlssNrRunBeforeSr { false }; + CustomOptional DlssNrApplyAfterRR { false }; + CustomOptional DlssNrRRPasses { 1 }; + CustomOptional DlssNrRRWorkingScale { 0.5f }; // Toggles the pass in game. Unbound by default -- a key that does something unexpected is worse // than one that does nothing. CustomOptional DlssNrToggleKey { UnboundKey }; diff --git a/OptiScaler/dlssnr/DlssNrFeature_Dx12.h b/OptiScaler/dlssnr/DlssNrFeature_Dx12.h index 6dfcecab1..69d510845 100644 --- a/OptiScaler/dlssnr/DlssNrFeature_Dx12.h +++ b/OptiScaler/dlssnr/DlssNrFeature_Dx12.h @@ -32,6 +32,8 @@ inline constexpr unsigned int MaxPassCount = 3; // timingQueue is the queue this command list will be executed on, when the caller knows it. // State::currentCommandQueue only exists once a D3D12 swapchain has been created, which a Vulkan // game never does -- so without this the pass runs and never reports what it cost. +// forcePost identifies an RR feature: selects ApplyAfterRR, RRPasses and RRWorkingScale. +// Do not set it for ordinary SR fallback; that decision is made from Color's active subrect. void EvaluateAfterUpscale(ID3D12GraphicsCommandList* cmdList, NVSDK_NGX_Parameter* params, ID3D12CommandQueue* timingQueue = nullptr, bool forcePost = false, unsigned long long submissionEpoch = 0); diff --git a/OptiScaler/dlssnr/DlssNr_Menu.cpp b/OptiScaler/dlssnr/DlssNr_Menu.cpp index c0353ee1c..fb7b83249 100644 --- a/OptiScaler/dlssnr/DlssNr_Menu.cpp +++ b/OptiScaler/dlssnr/DlssNr_Menu.cpp @@ -128,11 +128,29 @@ void RenderMenu(Config* config, float menuResScale) HelpMarker("Runs Neural Rendering on the render-resolution colour input immediately before" "\nSuper Resolution, so SR temporally accumulates and upscales the enhanced frame." "\n\nRay Reconstruction is deliberately excluded: its input contract differs and" - "\ncontinues to use the post-upscale Neural Rendering path. Padded or offset" + "\nuses the separate Apply after Ray Reconstruction option. Padded or offset" "\ndynamic-resolution inputs also fall back post-upscale for safety." "\n\nThis placement control currently applies to the Direct3D 12 path and its" "\nDirect3D 11/Vulkan bridges; native Vulkan keeps the post-upscale path."); + bool afterRR = config->DlssNrApplyAfterRR.value_or_default(); + if (ImGui::Checkbox("Apply after Ray Reconstruction (DX12)", &afterRR)) + config->DlssNrApplyAfterRR = afterRR; + HelpMarker("Requires the game's native Ray Reconstruction option. RR denoises and upscales" + "\nfirst; NR then processes its output before frame generation." + "\nThis does not add RR to games without the required rendering buffers." + "\nIndependent controls below prevent inheriting the cost of the SR configuration." + "\nNative Vulkan does not use these DX12 controls."); + int rrPasses = (int) config->DlssNrRRPasses.value_or_default(); + if (ImGui::SliderInt("NR passes after RR", &rrPasses, 1, (int) MaxPassCount)) + config->DlssNrRRPasses = (unsigned int) rrPasses; + float rrScale = config->DlssNrRRWorkingScale.value_or_default(); + if (ImGui::SliderFloat("NR model scale after RR", &rrScale, 0.25f, 2.0f, "%.2fx")) + config->DlssNrRRWorkingScale = rrScale; + HelpMarker("Relative to RR's OUTPUT resolution: 0.50x at 4K runs NR at 1920x1080." + "\nRR itself remains full quality. The NR edit is resized for final composition." + "\nPasses 2 and 3 use the same per-pass model profiles as the SR path."); + // The toggle can be bound to a key, and nobody would think to look for it under Keybinds // unless told. Dimmed, because it is a note rather than a setting. ImGui::TextDisabled("Can be toggled with a key -- bind it under Keybinds, \"Neural Rendering\"."); diff --git a/OptiScaler/shaders/dlssnr/DlssNr_Common.h b/OptiScaler/shaders/dlssnr/DlssNr_Common.h index 61e6ac53f..2c64042d1 100644 --- a/OptiScaler/shaders/dlssnr/DlssNr_Common.h +++ b/OptiScaler/shaders/dlssnr/DlssNr_Common.h @@ -74,6 +74,8 @@ struct DlssNrFrameInfo // a UAV. The DX12 pass uses this to preserve the caller's state and to fall back through a copy // when a pre-SR colour resource was not created with UAV support. bool BeforeUpscale = false; + // A native RR result selects independent NR cost controls and a separate history lifecycle. + bool AfterRayReconstruction = false; // Submission epoch supplied by the caller. Native DX12 uses the wrapped swapchain Present count; // the DX11/Vulkan bridges use their successfully submitted frame counter. A feature created in an diff --git a/OptiScaler/shaders/dlssnr/DlssNr_Dx12.cpp b/OptiScaler/shaders/dlssnr/DlssNr_Dx12.cpp index 1386cc3c8..b6c784a60 100644 --- a/OptiScaler/shaders/dlssnr/DlssNr_Dx12.cpp +++ b/OptiScaler/shaders/dlssnr/DlssNr_Dx12.cpp @@ -330,6 +330,7 @@ struct NrState unsigned int width = 0; unsigned int height = 0; bool beforeUpscale = false; + bool afterRayReconstruction = false; bool reset = true; // Dimensions of the guides as the upscaler handed them over, kept for the present path, which runs @@ -1722,13 +1723,18 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c // the model runs reduced and cheaper; above 1 it SUPERSAMPLES -- the proxy is upscaled to a larger // working size so the model denoises a super-native input, which the resolve then samples back down. // Capped at 2x: cost grows with the area and NGX acceptance above native is what this probe tests. - float workScale = cfg.DlssNrWorkingScale.value_or_default(); + float workScale = frame.AfterRayReconstruction ? cfg.DlssNrRRWorkingScale.value_or_default() + : cfg.DlssNrWorkingScale.value_or_default(); + if (!std::isfinite(workScale)) + workScale = frame.AfterRayReconstruction ? 0.5f : 1.0f; workScale = workScale < 0.25f ? 0.25f : (workScale > 2.0f ? 2.0f : workScale); const auto workWidth = (unsigned int) (width * workScale + 0.5f); const auto workHeight = (unsigned int) (height * workScale + 0.5f); const bool reduced = workWidth != width || workHeight != height; const unsigned int configuredPasses = - std::clamp(cfg.DlssNrPasses.value_or_default(), 1u, DlssNr::MaxPassCount); + std::clamp(frame.AfterRayReconstruction ? cfg.DlssNrRRPasses.value_or_default() + : cfg.DlssNrPasses.value_or_default(), + 1u, DlssNr::MaxPassCount); const bool proxyBackend = cfg.DlssNrUseProxy.value_or_default(); const unsigned int requestedPasses = proxyBackend ? 1u : configuredPasses; @@ -1747,7 +1753,9 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c const bool resolutionChanged = g_nr.width != width || g_nr.height != height || g_nr.workWidth != workWidth || g_nr.workHeight != workHeight; - const bool placementChanged = g_nr.feature != nullptr && g_nr.beforeUpscale != frame.BeforeUpscale; + const bool placementChanged = g_nr.feature != nullptr && + (g_nr.beforeUpscale != frame.BeforeUpscale || + g_nr.afterRayReconstruction != frame.AfterRayReconstruction); // The model reads its tuning once, while the feature is built, so a changed setting only takes // effect when the feature is rebuilt. TuningMatchesFeature was written to notice that and then @@ -1901,13 +1909,16 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c g_nr.width = width; g_nr.height = height; g_nr.beforeUpscale = frame.BeforeUpscale; + g_nr.afterRayReconstruction = frame.AfterRayReconstruction; g_nr.reset = true; g_nr.featurePendingSubmission = true; g_nr.featureCreateEpoch = frame.SubmissionEpoch; RecordBuiltPrimaryTuning(cfg); - LOG_INFO("DLSS-NR running {} SR: target {}x{}, model {}x{}, guides {}x{} " + LOG_INFO("DLSS-NR running {}: target {}x{}, model {}x{}, guides {}x{} " "(preset {}, intensity {}, style {}, build epoch {})", - frame.BeforeUpscale ? "before" : "after", width, height, workWidth, workHeight, + frame.AfterRayReconstruction ? "after Ray Reconstruction" : + (frame.BeforeUpscale ? "before SR" : "after SR"), + width, height, workWidth, workHeight, guideWidth, guideHeight, g_nr.builtPreset[0], g_nr.builtIntensity, g_nr.builtStyle[0], frame.SubmissionEpoch); @@ -2837,6 +2848,14 @@ void EvaluateInternal(ID3D12GraphicsCommandList* cmdList, NVSDK_NGX_Parameter* p return; } + // forcePost is supplied only for a native RR feature by the NGX and bridge callers. + // RR already reconstructs and upscales; never edit its noisy input or inherit SR's multipass cost. + if (forcePost && !cfg.DlssNrApplyAfterRR.value_or_default()) + { + ReportSkipOnce("Ray Reconstruction is active; enable ApplyAfterRR to process its output"); + return; + } + // Ray Reconstruction is explicitly forced post: PR #6 reports that pre-SR placement does not work // with DLSSD's input contract. Padded/offset Color inputs also stay post until the colour codec can // address subrect origins: processing their whole allocation would run the wrong raster and touch @@ -2929,6 +2948,7 @@ void EvaluateInternal(ID3D12GraphicsCommandList* cmdList, NVSDK_NGX_Parameter* p DlssNrFrameInfo frame {}; frame.DepthInverted = (createFlags & NVSDK_NGX_DLSS_Feature_Flags_DepthInverted) != 0; frame.BeforeUpscale = beforeUpscale; + frame.AfterRayReconstruction = forcePost; frame.SubmissionEpoch = timingQueue != nullptr ? submissionEpoch : State::Instance().frameCount; // Color and Output may use different formats even though DLSS treats them as the same frame colour diff --git a/README.md b/README.md index ba7f32255..37cbcfce3 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,14 @@ The upstream fork already provided experimental direct access to NVIDIA DLSS Neu - **Per-pass model profiles.** Passes 2 and 3 can inherit pass 1 or select their own built-in preset and style (`standard`, `natural`, or `cinematic`) without loading competing model DLLs. - **Guarded fallbacks.** Ray Reconstruction remains post-SR, and padded or offset dynamic-resolution inputs fall back to the existing post-SR path instead of using unsafe dimensions. - **Matching overlay and INI controls.** `RunBeforeSR` and `Passes` are exposed in both configuration and the OptiScaler overlay. +- **Optional NR after native Ray Reconstruction (DX12).** Enable `ApplyAfterRR` separately; + `RRPasses` defaults to 1 and `RRWorkingScale` to 0.5 of RR's output dimensions. RR keeps its + original noisy inputs, and NR history is rebuilt when switching between SR and RR. - **Verified BG3 path.** Baldur's Gate 3 was tested through the `bg3_dx11.exe` D3D11-to-D3D12 bridge with two neural passes at 1920x1080 followed by DLSS Super Resolution to 3840x2160. Downloads: +- [Native RR controls preview](https://github.com/wilsjo2/OptiScaler-DLSSNR-PreSR-Multipass/releases/tag/v0.4.0-rr-preview) — compiled experimental build with independent NR-after-RR controls. In-game RR/NR validation is pending. - [Portable cross-generation package](https://github.com/wilsjo2/OptiScaler-DLSSNR-PreSR-Multipass/releases/tag/v0.3.0-crossgen-portable) — the complete installer and backend layout, with game-neutral defaults and RTX 20/30/40/50 runtime guidance. - The earlier `general-per-pass-profiles-facc24f6` and `bg3-presr-multipass-e16d5866` packages are retained only as historical validation artifacts. They are incomplete for a clean installation and should not be redistributed. diff --git a/setup_windows.bat b/setup_windows.bat index 905e323ec..485e3b8df 100644 --- a/setup_windows.bat +++ b/setup_windows.bat @@ -207,6 +207,12 @@ if exist %selectedFilename% ( REM Wine doesn't support powershell :checkWine +if /i %selectedFilename%=="d3d12.dll" ( + echo WARNING: This proxy has a reported Streamline conflict that can grey out + echo Cyberpunk's Ray Reconstruction option. Try dxgi.dll or another compatible + echo proxy if that happens. Back up existing ReShade or other loaders first. + echo See INSTALL-DLSSNR.md for the confirmed upstream report. +) reg query HKEY_CURRENT_USER\Software\Wine\DllOverrides >nul 2>&1 if %errorlevel%==0 ( echo. From 1366c6fabd536193e3b237759ca97821ff2fe36d Mon Sep 17 00:00:00 2001 From: wilsjo2 <97138003+wilsjo2@users.noreply.github.com> Date: Sun, 6 Sep 2026 14:47:52 +1000 Subject: [PATCH 7/7] DLSS-NR: expose independent model strengths for each pass --- INSTALL-DLSSNR.md | 19 +++ OptiScaler.ini | 14 ++ OptiScaler/Config.cpp | 20 +++ OptiScaler/Config.h | 10 ++ OptiScaler/dlssnr/DlssNr_Menu.cpp | 159 +++++++++++----------- OptiScaler/shaders/dlssnr/DlssNr_Dx12.cpp | 93 ++++++++++--- README.md | 4 + 7 files changed, 216 insertions(+), 103 deletions(-) diff --git a/INSTALL-DLSSNR.md b/INSTALL-DLSSNR.md index 5bb3194f5..d233272ba 100644 --- a/INSTALL-DLSSNR.md +++ b/INSTALL-DLSSNR.md @@ -83,6 +83,25 @@ OptiScaler into pass-through mode, which means no menu and no Neural Rendering. Do not install the RenoDX DLSS add-on merely to obtain its compatibility runtime. This OptiScaler fork drives `nvngx_dlssnr.dll` itself, and two Neural Rendering injectors can conflict. +## Individual pass controls + +Under **DLSS Neural Rendering → Model passes**, expand Pass 1, Pass 2, or Pass 3. Each contains +Style, Intensity, Local structure, Local tone, Skin structure, and Auto skin mask. Sliders commit +when released to avoid rebuilding the model on every movement. Set the model pass count to 2 or 3 +to activate later passes; editing inactive passes prepares their settings without running them. + +Later passes inherit pass 1 unless overridden, except Local tone, which defaults to 0 to preserve +the earlier build's appearance. Reset on a later-pass slider clears its override. Intensity, +local structure, and local tone range from 0 to 2; skin structure ranges from -1 to 2, with -1 +following local structure. The corresponding INI keys are `Pass2Intensity`, `Pass2LocalStructure`, +`Pass2LocalTone`, `Pass2SkinStructure`, and `Pass2AutoMask`, with matching `Pass3...` keys. +Use `auto` for the default behavior. Styles retain `Pass2Style` / `Pass3Style`. + +These controls apply to D3D12 multipass and its bridges, both before/after SR and after native RR. +Native Vulkan and the driver-proxy backend remain single-pass. Preset hints are still transmitted +at model creation, but a changed hint is not proof of a changed model. They are preserved under +**Advanced preset hints (effect unverified)** and in the INI for compatibility. + ## Neural Rendering with native Ray Reconstruction In a game that already supports RR, enable RR in the game's settings and enable diff --git a/OptiScaler.ini b/OptiScaler.ini index 45132c95f..e4a78b287 100644 --- a/OptiScaler.ini +++ b/OptiScaler.ini @@ -1649,6 +1649,20 @@ Pass2Style=auto Pass3Preset=auto Pass3Style=auto +; Per-pass model strengths (DX12 multipass, including NR after RR). +; Auto inherits pass 1, except LocalTone: auto keeps the legacy later-pass value 0. +; Intensity/LocalStructure/LocalTone: 0..2; SkinStructure: -1..2 (-1 follows structure). +Pass2Intensity=auto +Pass2LocalStructure=auto +Pass2LocalTone=auto +Pass2SkinStructure=auto +Pass2AutoMask=auto +Pass3Intensity=auto +Pass3LocalStructure=auto +Pass3LocalTone=auto +Pass3SkinStructure=auto +Pass3AutoMask=auto + ; Default (auto) is 1.0 Intensity=auto LocalStructure=auto diff --git a/OptiScaler/Config.cpp b/OptiScaler/Config.cpp index da4ddb225..81b2cbb1f 100644 --- a/OptiScaler/Config.cpp +++ b/OptiScaler/Config.cpp @@ -373,6 +373,16 @@ bool Config::Reload(std::filesystem::path iniPath) DlssNrLocalTone.set_from_config(readFloat("DlssNr", "LocalTone")); DlssNrSkinStructure.set_from_config(readFloat("DlssNr", "SkinStructure")); DlssNrAutoMask.set_from_config(readBool("DlssNr", "AutoMask")); + DlssNrPass2Intensity.set_from_config(readFloat("DlssNr", "Pass2Intensity")); + DlssNrPass2LocalStructure.set_from_config(readFloat("DlssNr", "Pass2LocalStructure")); + DlssNrPass2LocalTone.set_from_config(readFloat("DlssNr", "Pass2LocalTone")); + DlssNrPass2SkinStructure.set_from_config(readFloat("DlssNr", "Pass2SkinStructure")); + DlssNrPass2AutoMask.set_from_config(readBool("DlssNr", "Pass2AutoMask")); + DlssNrPass3Intensity.set_from_config(readFloat("DlssNr", "Pass3Intensity")); + DlssNrPass3LocalStructure.set_from_config(readFloat("DlssNr", "Pass3LocalStructure")); + DlssNrPass3LocalTone.set_from_config(readFloat("DlssNr", "Pass3LocalTone")); + DlssNrPass3SkinStructure.set_from_config(readFloat("DlssNr", "Pass3SkinStructure")); + DlssNrPass3AutoMask.set_from_config(readBool("DlssNr", "Pass3AutoMask")); DlssNrReversibleMode.set_from_config(readUInt("DlssNr", "ReversibleMode")); DlssNrApplyModel.set_from_config(readBool("DlssNr", "ApplyModel")); DlssNrHoldFrame.set_from_config(readBool("DlssNr", "HoldFrame")); @@ -1280,6 +1290,16 @@ bool Config::SaveIni() ini.SetValue("DlssNr", "SkinStructure", GetFloatValue(Instance()->DlssNrSkinStructure.value_for_config()).c_str()); ini.SetValue("DlssNr", "AutoMask", GetBoolValue(Instance()->DlssNrAutoMask.value_for_config()).c_str()); + ini.SetValue("DlssNr", "Pass2Intensity", GetFloatValue(Instance()->DlssNrPass2Intensity.value_for_config()).c_str()); + ini.SetValue("DlssNr", "Pass2LocalStructure", GetFloatValue(Instance()->DlssNrPass2LocalStructure.value_for_config()).c_str()); + ini.SetValue("DlssNr", "Pass2LocalTone", GetFloatValue(Instance()->DlssNrPass2LocalTone.value_for_config()).c_str()); + ini.SetValue("DlssNr", "Pass2SkinStructure", GetFloatValue(Instance()->DlssNrPass2SkinStructure.value_for_config()).c_str()); + ini.SetValue("DlssNr", "Pass2AutoMask", GetBoolValue(Instance()->DlssNrPass2AutoMask.value_for_config()).c_str()); + ini.SetValue("DlssNr", "Pass3Intensity", GetFloatValue(Instance()->DlssNrPass3Intensity.value_for_config()).c_str()); + ini.SetValue("DlssNr", "Pass3LocalStructure", GetFloatValue(Instance()->DlssNrPass3LocalStructure.value_for_config()).c_str()); + ini.SetValue("DlssNr", "Pass3LocalTone", GetFloatValue(Instance()->DlssNrPass3LocalTone.value_for_config()).c_str()); + ini.SetValue("DlssNr", "Pass3SkinStructure", GetFloatValue(Instance()->DlssNrPass3SkinStructure.value_for_config()).c_str()); + ini.SetValue("DlssNr", "Pass3AutoMask", GetBoolValue(Instance()->DlssNrPass3AutoMask.value_for_config()).c_str()); ini.SetValue("DlssNr", "ReversibleMode", GetIntValue(Instance()->DlssNrReversibleMode.value_for_config()).c_str()); ini.SetValue("DlssNr", "ApplyModel", GetBoolValue(Instance()->DlssNrApplyModel.value_for_config()).c_str()); ini.SetValue("DlssNr", "HoldFrame", GetBoolValue(Instance()->DlssNrHoldFrame.value_for_config()).c_str()); diff --git a/OptiScaler/Config.h b/OptiScaler/Config.h index d6d267c40..2820be2f7 100644 --- a/OptiScaler/Config.h +++ b/OptiScaler/Config.h @@ -282,6 +282,16 @@ class Config // -1 means follow local structure, which is the model's own default. It is not a strength of zero. CustomOptional DlssNrSkinStructure { -1.0f }; CustomOptional DlssNrAutoMask { true }; + CustomOptional DlssNrPass2Intensity; + CustomOptional DlssNrPass2LocalStructure; + CustomOptional DlssNrPass2LocalTone; + CustomOptional DlssNrPass2SkinStructure; + CustomOptional DlssNrPass2AutoMask; + CustomOptional DlssNrPass3Intensity; + CustomOptional DlssNrPass3LocalStructure; + CustomOptional DlssNrPass3LocalTone; + CustomOptional DlssNrPass3SkinStructure; + CustomOptional DlssNrPass3AutoMask; // How much of the model's edit reaches the frame. Separated because detail synthesis is a luminance // edit and any colour shift is usually the part you do not want, and allowed past 1.0 because diff --git a/OptiScaler/dlssnr/DlssNr_Menu.cpp b/OptiScaler/dlssnr/DlssNr_Menu.cpp index fb7b83249..03f2b1105 100644 --- a/OptiScaler/dlssnr/DlssNr_Menu.cpp +++ b/OptiScaler/dlssnr/DlssNr_Menu.cpp @@ -43,21 +43,23 @@ static void HelpMarker(const char* tip) // live under the cursor; only the commit that triggers the rebuild waits for release. Cheap controls // that are just shader constants (detail, colour, paper white) do not use this -- they can afford to // apply live. -static bool DeferredSlider(const char* label, CustomOptional* opt, float mn, float mx, - float def, const char* fmt = "%.2f") +template +static bool DeferredSlider(const char* label, Option* opt, float mn, float mx, + float def, const char* fmt = "%.2f", bool inheritReset = false) { - static std::unordered_map pending; + static std::unordered_map pending; + const ImGuiID id = ImGui::GetID(label); - auto it = pending.find(label); - float value = it != pending.end() ? it->second : opt->value_or_default(); + auto it = pending.find(id); + float value = it != pending.end() ? it->second : (opt->has_value() ? opt->value() : def); bool changed = false; if (ImGui::SliderFloat(label, &value, mn, mx, fmt)) - pending[label] = value; + pending[id] = value; if (ImGui::IsItemDeactivatedAfterEdit()) { - auto committed = pending.find(label); + auto committed = pending.find(id); if (committed != pending.end()) { @@ -72,8 +74,11 @@ static bool DeferredSlider(const char* label, CustomOptional* opt, float const std::string resetId = std::string("Reset##") + label; if (ImGui::SmallButton(resetId.c_str())) { - *opt = def; - pending.erase(std::string(label)); // drop any in-flight drag so the reset actually sticks + if (inheritReset) + *opt = std::optional {}; + else + *opt = def; + pending.erase(id); changed = true; } @@ -419,82 +424,74 @@ void RenderMenu(Config* config, float menuResScale) "\nstable. If you love the Replace look but the flicker bothers you, use this." "\n\nOff is byte-identical to before."); - ImGui::SeparatorText("Model"); - - ImGui::TextUnformatted("Read when the model is built, so a change rebuilds it after a moment."); - - static const char* nrPresetNames[] = { "Default", "Preset 1", "Preset 2", "Preset 3" }; - int preset = (int) config->DlssNrPreset.value_or_default(); - if (ImGui::Combo("Pass 1 model preset", &preset, nrPresetNames, IM_ARRAYSIZE(nrPresetNames))) - config->DlssNrPreset = (uint32_t) preset; - - HelpMarker("Default leaves the choice to the model." - "\n\nNot the same scale as the super resolution or ray reconstruction presets --" - "\nthe same number means something different here."); - - static const char* nrStyleNames[] = { "Default (standard)", "Natural", "Cinematic" }; - int style = (int) config->DlssNrStyle.value_or_default(); - - if (style > 2) - style = 2; - - if (ImGui::Combo("Pass 1 style", &style, nrStyleNames, IM_ARRAYSIZE(nrStyleNames))) - config->DlssNrStyle = (uint32_t) style; - - HelpMarker("The model's own processing profiles." - "\n\nDefault (standard): the strongest. Boosts local contrast and deepens" - "\nlighting, and can oversaturate or look stylised -- most of what reads as" - "\n'the model changed my game's look' is this profile." - "\n\nNatural: the same detail work with a gentler hand. Keeps skin tones and" - "\ntonal balance closer to what the game rendered." - "\n\nCinematic: tones down the shine and over-processing for a film-like look." - "\n\nRead when the model is built, so a change rebuilds it after a moment. The" - "\nnames come from community testing; NVIDIA ships no names in the binaries."); - - ImGui::SeparatorText("Later-pass model profiles"); - ImGui::TextDisabled("Auto inherits pass 1. Overrides rebuild only while that pass is active."); - - static const char* inheritedPresetNames[] = { - "Auto (inherit pass 1)", "Default", "Preset 1", "Preset 2", "Preset 3" - }; - static const char* inheritedStyleNames[] = { - "Auto (inherit pass 1)", "Default (standard)", "Natural", "Cinematic" - }; - - InheritedProfileCombo("Pass 2 preset", &config->DlssNrPass2Preset, - inheritedPresetNames, IM_ARRAYSIZE(inheritedPresetNames)); - InheritedProfileCombo("Pass 2 style", &config->DlssNrPass2Style, - inheritedStyleNames, IM_ARRAYSIZE(inheritedStyleNames)); - InheritedProfileCombo("Pass 3 preset", &config->DlssNrPass3Preset, - inheritedPresetNames, IM_ARRAYSIZE(inheritedPresetNames)); - InheritedProfileCombo("Pass 3 style", &config->DlssNrPass3Style, - inheritedStyleNames, IM_ARRAYSIZE(inheritedStyleNames)); - - HelpMarker("These select different built-in profiles inside the same NVIDIA model DLL." - "\nThey do not load a different model file per pass. Preset values are 0..3;" - "\nstyles are 0 standard, 1 natural, and 2 cinematic. The names are based on" - "\ncommunity testing because NVIDIA has not published this integration API."); - - DeferredSlider("Intensity", &config->DlssNrIntensity, 0.0f, 2.0f, 1.0f); - - HelpMarker("The model's own strength control, applied inside it. Distinct from detail" - "\nstrength above, which scales the result afterwards."); + ImGui::SeparatorText("Model passes"); + ImGui::TextWrapped("Each pass has its own style and model strengths. Changes apply when you release a slider."); + static const char* styles[] = { "Standard", "Natural", "Cinematic" }; + static const char* inheritedStyles[] = { "Auto (inherit pass 1)", "Standard", "Natural", "Cinematic" }; - DeferredSlider("Local structure", &config->DlssNrLocalStructure, 0.0f, 2.0f, 1.0f); - - DeferredSlider("Local tone", &config->DlssNrLocalTone, 0.0f, 2.0f, 1.0f); - - - DeferredSlider("Skin structure", &config->DlssNrSkinStructure, -1.0f, 2.0f, -1.0f); + if (ImGui::TreeNodeEx("Pass 1", ImGuiTreeNodeFlags_DefaultOpen)) + { + int style = (int) std::min(config->DlssNrStyle.value_or_default(), 2u); + if (ImGui::Combo("Style", &style, styles, IM_ARRAYSIZE(styles))) + config->DlssNrStyle = (uint32_t) style; + DeferredSlider("Intensity", &config->DlssNrIntensity, 0.0f, 2.0f, 1.0f); + DeferredSlider("Local structure", &config->DlssNrLocalStructure, 0.0f, 2.0f, 1.0f); + DeferredSlider("Local tone", &config->DlssNrLocalTone, 0.0f, 2.0f, 1.0f); + DeferredSlider("Skin structure", &config->DlssNrSkinStructure, -1.0f, 2.0f, -1.0f); + HelpMarker("-1 follows local structure. Higher values control skin independently."); + bool mask = config->DlssNrAutoMask.value_or_default(); + if (ImGui::Checkbox("Auto skin mask", &mask)) + config->DlssNrAutoMask = mask; + ImGui::TreePop(); + } - HelpMarker("-1 means follow local structure, and is the model's own default -- it is not a" - "\nstrength of zero. 0 and above set skin independently of the rest of the frame."); + if (ImGui::TreeNodeEx("Pass 2", ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::TextWrapped("Unset controls inherit pass 1, except local tone which defaults to 0. Reset restores this behavior. Only active passes run."); + InheritedProfileCombo("Style", &config->DlssNrPass2Style, inheritedStyles, IM_ARRAYSIZE(inheritedStyles)); + DeferredSlider("Intensity", &config->DlssNrPass2Intensity, 0.0f, 2.0f, config->DlssNrIntensity.value_or_default(), "%.2f", true); + DeferredSlider("Local structure", &config->DlssNrPass2LocalStructure, 0.0f, 2.0f, config->DlssNrLocalStructure.value_or_default(), "%.2f", true); + DeferredSlider("Local tone", &config->DlssNrPass2LocalTone, 0.0f, 2.0f, 0.0f, "%.2f", true); + DeferredSlider("Skin structure", &config->DlssNrPass2SkinStructure, -1.0f, 2.0f, config->DlssNrSkinStructure.value_or_default(), "%.2f", true); + bool mask = config->DlssNrPass2AutoMask.has_value() ? config->DlssNrPass2AutoMask.value() : config->DlssNrAutoMask.value_or_default(); + if (ImGui::Checkbox("Auto skin mask", &mask)) + config->DlssNrPass2AutoMask = mask; + ImGui::SameLine(); + if (ImGui::SmallButton("Reset##mask")) + config->DlssNrPass2AutoMask = std::optional {}; + ImGui::TreePop(); + } - bool autoMask = config->DlssNrAutoMask.value_or_default(); - if (ImGui::Checkbox("Auto skin mask", &autoMask)) - config->DlssNrAutoMask = autoMask; + if (ImGui::TreeNodeEx("Pass 3", ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::TextWrapped("Unset controls inherit pass 1, except local tone which defaults to 0. Reset restores this behavior. Only active passes run."); + InheritedProfileCombo("Style", &config->DlssNrPass3Style, inheritedStyles, IM_ARRAYSIZE(inheritedStyles)); + DeferredSlider("Intensity", &config->DlssNrPass3Intensity, 0.0f, 2.0f, config->DlssNrIntensity.value_or_default(), "%.2f", true); + DeferredSlider("Local structure", &config->DlssNrPass3LocalStructure, 0.0f, 2.0f, config->DlssNrLocalStructure.value_or_default(), "%.2f", true); + DeferredSlider("Local tone", &config->DlssNrPass3LocalTone, 0.0f, 2.0f, 0.0f, "%.2f", true); + DeferredSlider("Skin structure", &config->DlssNrPass3SkinStructure, -1.0f, 2.0f, config->DlssNrSkinStructure.value_or_default(), "%.2f", true); + bool mask = config->DlssNrPass3AutoMask.has_value() ? config->DlssNrPass3AutoMask.value() : config->DlssNrAutoMask.value_or_default(); + if (ImGui::Checkbox("Auto skin mask", &mask)) + config->DlssNrPass3AutoMask = mask; + ImGui::SameLine(); + if (ImGui::SmallButton("Reset##mask")) + config->DlssNrPass3AutoMask = std::optional {}; + ImGui::TreePop(); + } - HelpMarker("Lets the model find skin itself rather than treating the frame uniformly."); + if (ImGui::TreeNode("Advanced preset hints (effect unverified)")) + { + ImGui::TextWrapped("These hints are passed to NVIDIA at creation, but their visual effect is unverified. Use Style for model profile selection. Existing INI hints are preserved."); + static const char* presets[] = { "Default", "Preset 1", "Preset 2", "Preset 3" }; + static const char* inheritedPresets[] = { "Auto (inherit pass 1)", "Default", "Preset 1", "Preset 2", "Preset 3" }; + int preset = (int) std::min(config->DlssNrPreset.value_or_default(), 3u); + if (ImGui::Combo("Pass 1 preset hint", &preset, presets, IM_ARRAYSIZE(presets))) + config->DlssNrPreset = (uint32_t) preset; + InheritedProfileCombo("Pass 2 preset hint", &config->DlssNrPass2Preset, inheritedPresets, IM_ARRAYSIZE(inheritedPresets)); + InheritedProfileCombo("Pass 3 preset hint", &config->DlssNrPass3Preset, inheritedPresets, IM_ARRAYSIZE(inheritedPresets)); + ImGui::TreePop(); + } + ImGui::TextWrapped("Per-pass overrides apply to the DX12 multipass path, including NR after RR. Native Vulkan and the driver-proxy backend remain single-pass."); ImGui::SeparatorText("Colour"); diff --git a/OptiScaler/shaders/dlssnr/DlssNr_Dx12.cpp b/OptiScaler/shaders/dlssnr/DlssNr_Dx12.cpp index b6c784a60..557dabba6 100644 --- a/OptiScaler/shaders/dlssnr/DlssNr_Dx12.cpp +++ b/OptiScaler/shaders/dlssnr/DlssNr_Dx12.cpp @@ -164,6 +164,16 @@ using PFN_NrProbeFloat = void(__cdecl*) (void*, const char*, float, int); // One per back buffer, so an allocator is never reset while its frame is still in flight. +struct NrPassTuning +{ + float intensity = 1.0f; + float structure = 1.0f; + float tone = 0.0f; + float skin = -1.0f; + bool autoMask = true; + bool operator==(const NrPassTuning&) const = default; +}; + struct NrState { HMODULE forwarder = nullptr; @@ -348,6 +358,7 @@ struct NrState // remaining strengths are intentionally shared by the stack. unsigned int builtPreset[DlssNr::MaxPassCount] = {}; float builtIntensity = 0.0f; + NrPassTuning builtPassTuning[DlssNr::MaxPassCount] {}; unsigned int builtStyle[DlssNr::MaxPassCount] = {}; float builtLocalStructure = 0.0f; float builtLocalTone = 0.0f; @@ -1336,15 +1347,51 @@ unsigned int PassStyle(const Config& cfg, unsigned int pass) return base; } -bool TuningMatchesFeature(const Config& cfg, unsigned int requestedPasses) +NrPassTuning PassTuning(const Config& cfg, unsigned int pass) { - if (g_nr.builtIntensity != cfg.DlssNrIntensity.value_or_default() || - g_nr.builtLocalStructure != cfg.DlssNrLocalStructure.value_or_default() || - g_nr.builtLocalTone != cfg.DlssNrLocalTone.value_or_default() || - g_nr.builtSkinStructure != cfg.DlssNrSkinStructure.value_or_default() || - g_nr.builtAutoMask != cfg.DlssNrAutoMask.value_or_default()) - return false; + NrPassTuning result { cfg.DlssNrIntensity.value_or_default(), + cfg.DlssNrLocalStructure.value_or_default(), + pass == 0 ? cfg.DlssNrLocalTone.value_or_default() : 0.0f, + cfg.DlssNrSkinStructure.value_or_default(), + cfg.DlssNrAutoMask.value_or_default() }; + if (pass == 1) + { + if (cfg.DlssNrPass2Intensity.has_value()) + result.intensity = cfg.DlssNrPass2Intensity.value(); + if (cfg.DlssNrPass2LocalStructure.has_value()) + result.structure = cfg.DlssNrPass2LocalStructure.value(); + if (cfg.DlssNrPass2LocalTone.has_value()) + result.tone = cfg.DlssNrPass2LocalTone.value(); + if (cfg.DlssNrPass2SkinStructure.has_value()) + result.skin = cfg.DlssNrPass2SkinStructure.value(); + if (cfg.DlssNrPass2AutoMask.has_value()) + result.autoMask = cfg.DlssNrPass2AutoMask.value(); + } + if (pass == 2) + { + if (cfg.DlssNrPass3Intensity.has_value()) + result.intensity = cfg.DlssNrPass3Intensity.value(); + if (cfg.DlssNrPass3LocalStructure.has_value()) + result.structure = cfg.DlssNrPass3LocalStructure.value(); + if (cfg.DlssNrPass3LocalTone.has_value()) + result.tone = cfg.DlssNrPass3LocalTone.value(); + if (cfg.DlssNrPass3SkinStructure.has_value()) + result.skin = cfg.DlssNrPass3SkinStructure.value(); + if (cfg.DlssNrPass3AutoMask.has_value()) + result.autoMask = cfg.DlssNrPass3AutoMask.value(); + } + const auto bounded = [](float value, float fallback, float minimum) { + return std::isfinite(value) ? std::clamp(value, minimum, 2.0f) : fallback; + }; + result.intensity = bounded(result.intensity, 1.0f, 0.0f); + result.structure = bounded(result.structure, 1.0f, 0.0f); + result.tone = bounded(result.tone, pass == 0 ? 1.0f : 0.0f, 0.0f); + result.skin = bounded(result.skin, -1.0f, -1.0f); + return result; +} +bool TuningMatchesFeature(const Config& cfg, unsigned int requestedPasses) +{ for (unsigned int pass = 0; pass < requestedPasses; ++pass) { // A profile cannot be stale until its feature exists. This lets a user prepare pass 2 or 3 @@ -1352,7 +1399,8 @@ bool TuningMatchesFeature(const Config& cfg, unsigned int requestedPasses) if (pass > 0 && g_nr.passFeature[pass] == nullptr) continue; - if (g_nr.builtPreset[pass] != PassPreset(cfg, pass) || + if (g_nr.builtPassTuning[pass] != PassTuning(cfg, pass) || + g_nr.builtPreset[pass] != PassPreset(cfg, pass) || g_nr.builtStyle[pass] != PassStyle(cfg, pass)) return false; } @@ -1362,6 +1410,7 @@ bool TuningMatchesFeature(const Config& cfg, unsigned int requestedPasses) void RecordBuiltPrimaryTuning(const Config& cfg) { + g_nr.builtPassTuning[0] = PassTuning(cfg, 0); g_nr.builtPreset[0] = PassPreset(cfg, 0); g_nr.builtIntensity = cfg.DlssNrIntensity.value_or_default(); g_nr.builtStyle[0] = PassStyle(cfg, 0); @@ -1878,14 +1927,14 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c } SetExtras(cfg, nullptr, nullptr, 0, 0, 0, 0); + const auto tuning = PassTuning(cfg, 0); g_nr.feature = g_nr.create(snippet->wstring().c_str(), State::Instance().NVNGX_ApplicationDataPath.c_str(), device, cmdList, g_nr.capabilityParams, workWidth, workHeight, (int) PassPreset(cfg, 0), - cfg.DlssNrIntensity.value_or_default(), (int) PassStyle(cfg, 0), - cfg.DlssNrLocalStructure.value_or_default(), cfg.DlssNrLocalTone.value_or_default(), - cfg.DlssNrSkinStructure.value_or_default(), - cfg.DlssNrAutoMask.value_or_default() ? 1 : 0, + tuning.intensity, (int) PassStyle(cfg, 0), + tuning.structure, tuning.tone, tuning.skin, + tuning.autoMask ? 1 : 0, // UI correction at the model's own default: with no UI layer fed to it there // is nothing for it to correct. 1); @@ -2010,19 +2059,19 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c else { SetExtras(cfg, nullptr, nullptr, 0, 0, 0, 0); + const auto tuning = PassTuning(cfg, pass); g_nr.passFeature[pass] = g_nr.create( snippet->wstring().c_str(), State::Instance().NVNGX_ApplicationDataPath.c_str(), device, cmdList, g_nr.capabilityParams, workWidth, workHeight, - (int) PassPreset(cfg, pass), cfg.DlssNrIntensity.value_or_default(), + (int) PassPreset(cfg, pass), tuning.intensity, (int) PassStyle(cfg, pass), - cfg.DlssNrLocalStructure.value_or_default(), - // Local tone belongs to the frame and is applied by pass zero only. - 0.0f, cfg.DlssNrSkinStructure.value_or_default(), - cfg.DlssNrAutoMask.value_or_default() ? 1 : 0, 1); + tuning.structure, tuning.tone, tuning.skin, + tuning.autoMask ? 1 : 0, 1); if (g_nr.passFeature[pass] != nullptr) { g_nr.builtPreset[pass] = PassPreset(cfg, pass); + g_nr.builtPassTuning[pass] = tuning; g_nr.builtStyle[pass] = PassStyle(cfg, pass); g_nr.passNeedsReset[pass] = true; g_nr.passPendingSubmission[pass] = true; @@ -2489,16 +2538,16 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c { void* const passFeature = pass == 0 ? g_nr.feature : g_nr.passFeature[pass]; const bool passReset = g_nr.reset || (pass > 0 && g_nr.passNeedsReset[pass]); - const float passTone = pass == 0 ? cfg.DlssNrLocalTone.value_or_default() : 0.0f; + const auto tuning = PassTuning(cfg, pass); MakeModelWritable(passOutput); result = g_nr.evaluate( cmdList, passFeature, g_nr.capabilityParams, passInput, depthIn, motionIn, passOutput, workWidth, workHeight, guideWidth, guideHeight, g_nr.guideDepthInverted ? 1 : 0, - passReset ? 1 : 0, cfg.DlssNrIntensity.value_or_default(), - (int) PassStyle(cfg, pass), cfg.DlssNrLocalStructure.value_or_default(), - passTone, cfg.DlssNrSkinStructure.value_or_default(), - cfg.DlssNrAutoMask.value_or_default() ? 1 : 0, g_nr.guideMvScaleX * mvToWork, + passReset ? 1 : 0, tuning.intensity, + (int) PassStyle(cfg, pass), tuning.structure, + tuning.tone, tuning.skin, + tuning.autoMask ? 1 : 0, g_nr.guideMvScaleX * mvToWork, g_nr.guideMvScaleY * mvToWork); if (result != NVSDK_NGX_Result_Success) diff --git a/README.md b/README.md index 37cbcfce3..b899b1fd2 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,9 @@ The upstream fork already provided experimental direct access to NVIDIA DLSS Neu - **Optional Neural Rendering before DLSS Super Resolution.** The model can process the DLSS input image—such as 1920x1080 in 4K Performance mode—before DLSS upscales it to the display resolution. - **Configurable multipass processing.** `[DlssNr] Passes=1..3` runs one, two, or three sequential neural evaluations. Each pass has independent persistent history; the final result is composed once against the original base image. - **Per-pass model profiles.** Passes 2 and 3 can inherit pass 1 or select their own built-in preset and style (`standard`, `natural`, or `cinematic`) without loading competing model DLLs. +- **Independent model strengths per pass.** Each pass has intensity, local structure, local tone, + skin structure, and auto skin mask controls. Preset hints are grouped under a collapsed advanced + section because their visual effect is unverified; style is the primary profile selector. - **Guarded fallbacks.** Ray Reconstruction remains post-SR, and padded or offset dynamic-resolution inputs fall back to the existing post-SR path instead of using unsafe dimensions. - **Matching overlay and INI controls.** `RunBeforeSR` and `Passes` are exposed in both configuration and the OptiScaler overlay. - **Optional NR after native Ray Reconstruction (DX12).** Enable `ApplyAfterRR` separately; @@ -17,6 +20,7 @@ The upstream fork already provided experimental direct access to NVIDIA DLSS Neu Downloads: +- [Per-pass controls preview](https://github.com/wilsjo2/OptiScaler-DLSSNR-PreSR-Multipass/releases/tag/v0.5.0-pass-controls-preview) — reorganized pass sections and independent model strengths, including the RR controls. Runtime validation is pending. - [Native RR controls preview](https://github.com/wilsjo2/OptiScaler-DLSSNR-PreSR-Multipass/releases/tag/v0.4.0-rr-preview) — compiled experimental build with independent NR-after-RR controls. In-game RR/NR validation is pending. - [Portable cross-generation package](https://github.com/wilsjo2/OptiScaler-DLSSNR-PreSR-Multipass/releases/tag/v0.3.0-crossgen-portable) — the complete installer and backend layout, with game-neutral defaults and RTX 20/30/40/50 runtime guidance. - The earlier `general-per-pass-profiles-facc24f6` and `bg3-presr-multipass-e16d5866` packages are retained only as historical validation artifacts. They are incomplete for a clean installation and should not be redistributed.