From 6c6a9d10f1954eb5e2a26abe0cc02c980b19be39 Mon Sep 17 00:00:00 2001 From: y4my4my4m <8145020+y4my4my4m@users.noreply.github.com> Date: Fri, 4 Sep 2026 03:41:59 +0900 Subject: [PATCH 1/3] DLSS-NR: cover the creation frame, honour the guide and exposure barrier keys --- OptiScaler/shaders/dlssnr/DlssNr_Dx12.cpp | 119 ++++++++++++++++++---- 1 file changed, 99 insertions(+), 20 deletions(-) diff --git a/OptiScaler/shaders/dlssnr/DlssNr_Dx12.cpp b/OptiScaler/shaders/dlssnr/DlssNr_Dx12.cpp index f21fd42f6..5fc649425 100644 --- a/OptiScaler/shaders/dlssnr/DlssNr_Dx12.cpp +++ b/OptiScaler/shaders/dlssnr/DlssNr_Dx12.cpp @@ -1134,10 +1134,17 @@ ID3D12Resource* CreateGuideClone(ID3D12Device* device, ID3D12Resource* source) // ordinary depth that simply disagrees with the picture, and anything reading it has to notice. // Hands back something the model can actually read: the guide itself when it is typed, or a typed // copy of it when it is not. NGX requires its inputs in NON_PIXEL_SHADER_RESOURCE at evaluate time, -// which is a documented contract rather than a guess about any one game's frame graph, so that is -// the state transitioned away from and back to here. +// which is a documented contract rather than a guess about any one game's frame graph. +// +// arrival is the state the game actually left the guide in, from DepthResourceBarrier / +// MVResourceBarrier -- the same keys every upscaler in this tree honours for these two +// resources (FSR2Feature_Dx12.cpp:136). Unset means the contract holds and arrival is +// NON_PIXEL_SHADER_RESOURCE, which is what this transitioned from unconditionally before; a game that +// deviates was transitioned from a state it was not in, and under vkd3d-proton a wrong oldLayout in +// vkCmdPipelineBarrier is undefined rather than ignored. ID3D12Resource* ReadableGuide(ID3D12Device* device, ID3D12GraphicsCommandList* cmdList, - ID3D12Resource* source, ID3D12Resource** clone) + ID3D12Resource* source, ID3D12Resource** clone, + D3D12_RESOURCE_STATES arrival) { if (source == nullptr || !IsTypeless(source->GetDesc().Format)) return source; @@ -1170,11 +1177,9 @@ ID3D12Resource* ReadableGuide(ID3D12Device* device, ID3D12GraphicsCommandList* c (int) TypedGuideFormat(source->GetDesc().Format)); } - Barrier(cmdList, source, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, - D3D12_RESOURCE_STATE_COPY_SOURCE); + Barrier(cmdList, source, arrival, D3D12_RESOURCE_STATE_COPY_SOURCE); cmdList->CopyResource(*clone, source); - Barrier(cmdList, source, D3D12_RESOURCE_STATE_COPY_SOURCE, - D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE); + Barrier(cmdList, source, D3D12_RESOURCE_STATE_COPY_SOURCE, arrival); Barrier(cmdList, *clone, D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE); return *clone; @@ -1707,6 +1712,14 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c LOG_INFO("DLSS-NR: white point meter up, {}x{} tiles", kDlssNrMeterGrid, kDlssNrMeterGrid); } + // On an engine that needs its compute state put back -- the bindless quirks -- the envelope can + // only restore what was captured. If nothing was captured for this list, the upscaler decided + // touching state was unsafe this frame, and binding the pass now would leave state the envelope + // cannot clean up. So on those games, skip the frame rather than corrupt it. Ordinary games do + // not require restore, so they are unaffected and the pass runs as before. + const bool restoreRequired = cfg.RestoreComputeSignature.value_or_default() || + cfg.RestoreGraphicSignature.value_or_default(); + if (g_nr.feature == nullptr && g_nr.output != nullptr && g_nr.colorCopy != nullptr && g_nr.hdrCopy != nullptr) { @@ -1724,6 +1737,20 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c return; } + // Creation records onto the game's command list, so it binds NGX's own heaps, root signature + // and pipeline exactly as a dispatch would. The envelope further down covers every frame that + // reaches it; this one returns before it, and left those bindings live -- the state the + // envelope's own comment describes as removing the device on a bindless engine. Same guard, + // same envelope, applied to the frame the crash reports single out. + if (restoreRequired && !D3D12Hooks::CanRestoreRootSignature(cmdList)) + { + ReportSkipOnce("the upscaler could not restore state on the creation frame"); + device->Release(); + return; + } + + ScopedNrStateEnvelope creationEnvelope(cmdList); + SetExtras(cfg, nullptr, nullptr, 0, 0, 0, 0); g_nr.feature = g_nr.create(snippet->wstring().c_str(), State::Instance().NVNGX_ApplicationDataPath.c_str(), @@ -1833,14 +1860,6 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c // represent -- it exists precisely because the proxy is meant to clip. Normalising the highlights // away first leaves it nothing to give back. - // On an engine that needs its compute state put back -- the bindless quirks -- the envelope can - // only restore what was captured. If nothing was captured for this list, the upscaler decided - // touching state was unsafe this frame, and binding the pass now would leave state the envelope - // cannot clean up. So on those games, skip the frame rather than corrupt it. Ordinary games do - // not require restore, so they are unaffected and the pass runs as before. - const bool restoreRequired = cfg.RestoreComputeSignature.value_or_default() || - cfg.RestoreGraphicSignature.value_or_default(); - if (restoreRequired && !D3D12Hooks::CanRestoreRootSignature(cmdList)) { ReportSkipOnce("the upscaler could not restore state this frame"); @@ -1899,10 +1918,33 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c meterParams.Width = 1; meterParams.Height = 1; + // The game's exposure texture is read here as an SRV. D3D12 has no image layouts, so leaving + // it alone costs nothing there; Vulkan does, and a read still requires a shader-readable + // layout, so vkd3d-proton faults on what Windows ignores. ExposureResourceBarrier is the key + // every upscaler in this tree already honours for this resource + // (FSR2Feature_Dx12.cpp:164); unset means it arrives shader-readable and both of these are + // no-ops Barrier() skips. + auto* exposure = (ID3D12Resource*) frame.ExposureTexture; + + const D3D12_RESOURCE_STATES exposureArrival = + cfg.ExposureResourceBarrier.has_value() + ? (D3D12_RESOURCE_STATES) cfg.ExposureResourceBarrier.value() + : D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE; + Barrier(cmdList, target, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE); - DispatchPass(cmdList, meterParams, target, nullptr, nullptr, - (ID3D12Resource*) frame.ExposureTexture, nullptr, g_nr.meter, nullptr); + + if (exposure != nullptr) + Barrier(cmdList, exposure, exposureArrival, + D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE); + + DispatchPass(cmdList, meterParams, target, nullptr, nullptr, exposure, nullptr, g_nr.meter, + nullptr); + + if (exposure != nullptr) + Barrier(cmdList, exposure, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, + exposureArrival); + Barrier(cmdList, target, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); @@ -1962,8 +2004,21 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c // Read the exposure scan's candidates on the pass's own command list, once a frame. DlssNr::ExposureScan::Tick(device, cmdList); - ID3D12Resource* depthIn = ReadableGuide(device, cmdList, depth, &g_nr.depthClone); - ID3D12Resource* motionIn = ReadableGuide(device, cmdList, motion, &g_nr.motionClone); + // The state the game left its guides in, read the same way the output's is at the top of this + // function and the same way every upscaler in this tree reads it. Unset means the NGX contract + // holds and they arrive shader-readable, which is what this pass assumed unconditionally before. + const D3D12_RESOURCE_STATES depthArrival = + cfg.DepthResourceBarrier.has_value() + ? (D3D12_RESOURCE_STATES) cfg.DepthResourceBarrier.value() + : D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE; + + const D3D12_RESOURCE_STATES motionArrival = + cfg.MVResourceBarrier.has_value() + ? (D3D12_RESOURCE_STATES) cfg.MVResourceBarrier.value() + : D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE; + + ID3D12Resource* depthIn = ReadableGuide(device, cmdList, depth, &g_nr.depthClone, depthArrival); + ID3D12Resource* motionIn = ReadableGuide(device, cmdList, motion, &g_nr.motionClone, motionArrival); if (depthIn == nullptr || motionIn == nullptr) { @@ -1975,6 +2030,28 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c return; } + // A typed guide is handed to the model as it stands -- no clone, so ReadableGuide issued no + // barriers and it is still in the state the game left it. The model reads it, so it has to be + // shader-readable for that read, and is put back before this function returns. Both transitions + // are no-ops Barrier() skips when the arrival state already is NON_PIXEL_SHADER_RESOURCE. + const bool depthPassedThrough = depthIn == depth; + const bool motionPassedThrough = motionIn == motion; + + if (depthPassedThrough) + Barrier(cmdList, depth, depthArrival, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE); + + if (motionPassedThrough) + Barrier(cmdList, motion, motionArrival, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE); + + const auto restoreGuides = [&]() + { + if (depthPassedThrough) + Barrier(cmdList, depth, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, depthArrival); + + if (motionPassedThrough) + Barrier(cmdList, motion, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, motionArrival); + }; + // The vectors were scaled to full-frame pixels; the image the model reprojects is the working size. // The vectors were scaled to full-frame pixels; the image the model reprojects is the // working size. @@ -2004,6 +2081,7 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c proxyResult, NgxResultName(proxyResult)); } + restoreGuides(); Barrier(cmdList, target, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, outputArrival); device->Release(); return; @@ -2240,7 +2318,8 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c Barrier(cmdList, g_nr.colorCopy, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); - // Hand the output back in the state the upscaler and the game expect. + // Hand the guides and the output back in the states the upscaler and the game expect. + restoreGuides(); Barrier(cmdList, target, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, outputArrival); device->Release(); From e5c2fe63a6324e53c391e13eb9f54d7c60f8b75b Mon Sep 17 00:00:00 2001 From: y4my4my4m <8145020+y4my4my4m@users.noreply.github.com> Date: Fri, 4 Sep 2026 13:55:53 +0900 Subject: [PATCH 2/3] DLSS-NR: multipass, up to five chained model passes --- OptiScaler.ini | 6 + OptiScaler/Config.h | 19 +- OptiScaler/dlssnr/DlssNrFeature_Dx12.h | 4 + OptiScaler/dlssnr/DlssNr_Menu.cpp | 58 +++ OptiScaler/dlssnr/README.md | 23 + OptiScaler/shaders/dlssnr/DlssNr_Dx12.cpp | 513 ++++++++++++++++++++-- 6 files changed, 591 insertions(+), 32 deletions(-) diff --git a/OptiScaler.ini b/OptiScaler.ini index 58f889dd9..a7f009d19 100644 --- a/OptiScaler.ini +++ b/OptiScaler.ini @@ -1593,6 +1593,12 @@ MaxRatio=auto ; Default (auto) is 1.0 WorkingScale=auto +; How many times the model runs over the frame, each pass shown the last one's answer. Cost is exactly +; linear -- five passes is five model runs -- and each pass is its own model with its own memory, so +; VRAM grows with the count and raising it takes a couple of seconds to arrive. 1 to 5. +; Default (auto) is 1 +Passes=auto + ; 0 off, 1 the picture the model sees, 2 its raw answer, 3 what it changed amplified twenty times. ; A flat grey difference view means the model is doing nothing. ; Default (auto) is 0 diff --git a/OptiScaler/Config.h b/OptiScaler/Config.h index f4cfb8fc5..0abd0503e 100644 --- a/OptiScaler/Config.h +++ b/OptiScaler/Config.h @@ -445,16 +445,21 @@ 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 times to run the model over the same frame, each pass shown the last one's answer. // - // 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. + // A count of features, not a setting on one: every pass has its own NGX feature carrying its own + // temporal history, and each is built on a frame of its own before it is first evaluated. The + // proxy the composition differences against is written once, by the encode, and never by the + // chain, so what the composition receives is the whole chain's edit against the frame's own + // picture rather than the last pass's edit against the one before it. + // + // 1 is what the model was trained for. 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. The ceiling is DlssNr::kMaxPasses. // // 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 - // passes are sequential and each one needs the last one's output. + // again. The passes are sequential and each one needs the last one's output, so there is no + // amortisation. VRAM grows with the count as well: a feature's history is its own. CustomOptional DlssNrPasses { 1 }; // Which depth convention the model is told the guide uses. diff --git a/OptiScaler/dlssnr/DlssNrFeature_Dx12.h b/OptiScaler/dlssnr/DlssNrFeature_Dx12.h index 861d5e6d3..ac495cf9c 100644 --- a/OptiScaler/dlssnr/DlssNrFeature_Dx12.h +++ b/OptiScaler/dlssnr/DlssNrFeature_Dx12.h @@ -19,6 +19,10 @@ class Config; namespace DlssNr { +// The ceiling on how many times the model runs over one frame. The array of extra features, the +// pass-side clamp, the slider's bounds and the slider's own clamp all read this one number. +constexpr unsigned int kMaxPasses = 5; + // 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. diff --git a/OptiScaler/dlssnr/DlssNr_Menu.cpp b/OptiScaler/dlssnr/DlssNr_Menu.cpp index bbda2beb0..3ca676ca2 100644 --- a/OptiScaler/dlssnr/DlssNr_Menu.cpp +++ b/OptiScaler/dlssnr/DlssNr_Menu.cpp @@ -152,6 +152,64 @@ void RenderMenu(Config* config, float menuResScale) ImGui::Spacing(); ImGui::PushItemWidth(220.0f * menuResScale); + ImGui::SeparatorText("Cost"); + + { + // Coloured by what it costs, because the number alone does not say. The model is 98% of + // this pass's expense and every run pays it again, so the scale is linear and brutal: + // four passes is four times the model, not four percent more. + // + // Green at 1, what the model was trained for. Amber at 2 and 3, where it is being asked + // to enhance its own output. Red from 4, where it usually stops looking rendered. + // + // Applied when the handle is let go: every distinct value is a feature to build, and the + // build is spaced so the driver's latches survive it. + static int pendingPasses = -1; + + int passes = pendingPasses >= 0 ? pendingPasses + : (int) config->DlssNrPasses.value_or_default(); + + if (passes < 1) + passes = 1; + + const ImVec4 colour = passes <= 1 ? ImVec4(0.35f, 0.88f, 0.38f, 1.0f) + : passes <= 3 ? 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("Passes", &passes, 1, (int) DlssNr::kMaxPasses, + passes == 1 ? "%d (native)" : "%dx model cost")) + pendingPasses = passes; + + ImGui::PopStyleColor(2); + + if (ImGui::IsItemDeactivatedAfterEdit() && pendingPasses >= 0) + { + config->DlssNrPasses = + (uint32_t) std::clamp(pendingPasses, 1, (int) DlssNr::kMaxPasses); + pendingPasses = -1; + } + + HelpMarker("How many times the model runs over the frame, each pass shown the last one's" + "\nanswer." + "\n\nThe most expensive control here. Cost is exactly linear: five passes is" + "\nfive model runs, and the model is nearly all of what this pass costs." + "\n\nWhat it buys that nothing else can is the model re-deciding where detail" + "\ngoes, what hue it is, and how saturated. Detail strength amplifies the map" + "\nthe first pass drew; it cannot redraw it." + "\n\nWhat it does not buy is raw magnitude. Detail strength and Intensity are" + "\nfree and do that. Try both, and raise Model resolution, before this." + "\n\nPast 3, raise the ratio guard under Colour with it. The passes compound" + "\nthe luminance ratio and the guard clamps it, so beyond its limit the extra" + "\nruns are paid for and thrown away." + "\n\nEach pass is its own model, with its own memory, built on a frame of its" + "\nown -- so raising this takes a couple of seconds to arrive, and VRAM grows" + "\nwith it." + "\n\nNo effect on native Vulkan, or with the proxy path switched on."); + } + // 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/README.md b/OptiScaler/dlssnr/README.md index 72e8e745c..3d41e5326 100644 --- a/OptiScaler/dlssnr/README.md +++ b/OptiScaler/dlssnr/README.md @@ -99,6 +99,29 @@ part of the solution, and builds with everything else. the SDK header's vtable (floats sit at slot 6); the forwarder probes it. Rebuilding every frame exhausts the driver's latches and the feature stops responding until the process restarts, which is why the rebuild is debounced. +- **Multi-pass anchors on the proxy.** `Passes` runs the model N times over one frame, chained through + two work surfaces that alternate. The proxy the encode wrote is never written by the chain, so the + composition differences the last answer against the frame's own picture and receives the whole + chain's edit. Writing the proxy between passes makes the composition difference pass N against pass + N-1, and the effect then gets *weaker* with every pass past the first, with nothing to say so. + Debug view 3 is the test: the amplified edit at two passes must be visibly larger than at one. +- **A pass feature is built on a frame that evaluates nothing.** Each extra pass owns an NGX feature + with its own history, created under the same guard and envelope as the main feature and followed by + the same early return. Builds are spaced by `kSettleFrames`; back-to-back NGX creation exhausts the + driver's latches. The count is deliberately absent from `TuningMatchesFeature` — it counts features, + it is not a create argument of any of them. +- **A build is not a frame until something proves it.** `g_frames` counts Dispatch calls, and a title + that evaluates two upscaler features onto one open command list reaches the pass twice before any + submit. A pass feature stays out of the chain until either the present count or the command list + pointer has moved since its create (`PassWasSubmitted`), so the create and the first evaluate can + never share a list. The present count is the stronger of the two and is used wherever the swapchain + is wrapped; the pointer is the fallback, over-conservative by at most the frame a build already + costs. +- **Extra passes are priced before they are built.** A feature's history is driver-sized at the model's + working resolution, and five at 4K is not free. The cost is measured across the previous create from + `QueryVideoMemoryInfo`'s local segment, and the ramp waits rather than builds when the budget is + below it. Not every allocation failure comes back as a null handle — under vkd3d-proton a + VkDeviceMemory failure inside the NGX snippet surfaces as `VK_ERROR_DEVICE_LOST`. - **Never free under the GPU.** Every retired feature or surface is parked and freed 32 evaluates later; every internal feature is created on a private queue and fenced before use. Both rules were paid for with device hangs. diff --git a/OptiScaler/shaders/dlssnr/DlssNr_Dx12.cpp b/OptiScaler/shaders/dlssnr/DlssNr_Dx12.cpp index 5fc649425..4f2eb8c45 100644 --- a/OptiScaler/shaders/dlssnr/DlssNr_Dx12.cpp +++ b/OptiScaler/shaders/dlssnr/DlssNr_Dx12.cpp @@ -196,7 +196,30 @@ struct NrState // // 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] = {}; + void* passFeature[DlssNr::kMaxPasses] = {}; + + // Whether each extra feature still owes its first evaluate a history reset. Per index, not one + // flag: a feature built this frame has no history, while the ones beside it have a valid one that + // a blanket reset would throw away. + bool passReset[DlssNr::kMaxPasses] = {}; + + // The list a pass feature's creation was recorded on, and the present count at that moment. A + // feature is kept out of the chain until one of the two has moved, because neither an evaluate + // nor a Dispatch call is a submit. See PassWasSubmitted. + ID3D12GraphicsCommandList* passBuiltOn[DlssNr::kMaxPasses] = {}; + unsigned long long passBuiltAtPresent[DlssNr::kMaxPasses] = {}; + + // The highest pass count this session will attempt. Lowered, never raised, when a feature will not + // build or an extra pass refuses to run. + unsigned int passCeiling = DlssNr::kMaxPasses; + + // The frame after which the next extra feature may be built. NGX latches are exhausted by builds + // in quick succession, so the ramp is spaced by kSettleFrames. + unsigned long long passBuildAfter = 0; + + // Bytes of local video memory the last feature creation cost, measured across the create. Zero + // until one create has been measured, and the headroom check is skipped while it is. + unsigned long long featureBytes = 0; // The model cannot read and write one resource, so the frame is staged through these. ID3D12Resource* colorCopy = nullptr; @@ -210,6 +233,11 @@ struct NrState // The frame shrunk for the model, when it is working below full resolution. ID3D12Resource* colorSmall = nullptr; + // The chain's second work surface, the same size and format as output. Passes alternate between + // the two, so each pass reads what the last one wrote without a copy and without either surface + // being read and written by one evaluate. Allocated only above one pass. + ID3D12Resource* passScratch = nullptr; + unsigned int workWidth = 0; unsigned int workHeight = 0; @@ -375,6 +403,46 @@ void ClearCaptureDirectory() unsigned long long g_frames = 0; +// The present count as of the previous Dispatch, and whether it has ever moved. State::frameCount is +// written by the wrapped swapchain's Present; a session whose swapchain is not wrapped -- native +// Vulkan through the D3D12 bridge, for one -- leaves it at zero forever, and that has to be told +// apart from a game that has simply not presented yet. +unsigned long long g_lastPresent = 0; +bool g_presentMoves = false; + +void ObservePresent() +{ + const unsigned long long now = State::Instance().frameCount; + + if (now == g_lastPresent) + return; + + g_lastPresent = now; + g_presentMoves = true; +} + +// Whether the list a pass feature was created on has been submitted since. +// +// g_frames counts Dispatch calls, and a Dispatch call is not a frame: a title that evaluates two +// non-frame-generation upscaler features onto one open command list reaches here twice before +// anything is submitted. Creating and evaluating an NGX feature on one list is the hang multi-pass +// was removed for, so a build being "a frame ahead" has to be proved rather than assumed. +// +// The present count proves it wherever the swapchain is wrapped. Where it does not move, a changed +// command list pointer is the only evidence available; pointers are recycled, so that reading is +// over-conservative by at most one frame, which is what a build frame already costs. +// +// Both sides read g_lastPresent, sampled once per Dispatch, rather than State::frameCount directly: +// a present landing on another thread part-way through Dispatch is not evidence that this list went +// with it. +bool PassWasSubmitted(unsigned int pass, ID3D12GraphicsCommandList* cmdList) +{ + if (g_presentMoves) + return g_lastPresent != g_nr.passBuiltAtPresent[pass]; + + return cmdList != g_nr.passBuiltOn[pass]; +} + // A capture requested from outside the game: when the render path has no fence of its own, the write // waits until this frame count, by which point the GPU is certainly past the copies. unsigned long long g_captureWriteAtFrame = 0; @@ -672,6 +740,82 @@ void TickNrRetired() } } +// The adapter the pass's device sits on, for the memory budget. Found once and held for the session; +// Shutdown releases it. +IDXGIAdapter3* g_nrAdapter = nullptr; + +// One line per stall, not one per settle. +bool g_saidMemoryTight = false; + +// Local video memory left before the driver starts evicting, in bytes. +// +// An NGX feature's history is allocated inside the snippet, so a create that cannot be satisfied does +// not always come back as a null handle: under vkd3d-proton a VkDeviceMemory failure inside the +// snippet surfaces as VK_ERROR_DEVICE_LOST. The budget is the only thing that can be asked before +// the fact. Returns false when it cannot be read, and the caller then builds without the check. +bool LocalMemoryHeadroom(ID3D12Device* device, unsigned long long& headroom) +{ + if (g_nrAdapter == nullptr) + { + // Through the swapchain the game already has rather than a factory of this pass's own: + // CreateDXGIFactory is detoured, and the hook wraps what it returns and walks the module list. + // None of that belongs on the render thread mid-frame. Where there is no D3D12 swapchain -- + // native Vulkan over the bridge -- there is no budget to read and the check is skipped. + IDXGISwapChain* swapChain = State::Instance().currentRealSwapchain; + + if (swapChain == nullptr) + swapChain = State::Instance().currentSwapchain; + + if (swapChain == nullptr) + return false; + + IDXGIFactory4* factory = nullptr; + + if (FAILED(swapChain->GetParent(IID_PPV_ARGS(&factory))) || factory == nullptr) + return false; + + IDXGIAdapter* adapter = nullptr; + + if (SUCCEEDED(factory->EnumAdapterByLuid(device->GetAdapterLuid(), IID_PPV_ARGS(&adapter))) && + adapter != nullptr) + { + adapter->QueryInterface(IID_PPV_ARGS(&g_nrAdapter)); + adapter->Release(); + } + + factory->Release(); + + if (g_nrAdapter == nullptr) + return false; + } + + DXGI_QUERY_VIDEO_MEMORY_INFO info {}; + + if (FAILED(g_nrAdapter->QueryVideoMemoryInfo(0, DXGI_MEMORY_SEGMENT_GROUP_LOCAL, &info))) + return false; + + headroom = info.CurrentUsage < info.Budget ? info.Budget - info.CurrentUsage : 0ull; + return true; +} + +// What the local segment holds now, or zero when it cannot be read. Paired around a create to price +// one feature. +unsigned long long LocalMemoryUsed(ID3D12Device* device) +{ + unsigned long long headroom = 0; + + // For the adapter, which this establishes on first use. The headroom is not what is wanted here. + if (!LocalMemoryHeadroom(device, headroom)) + return 0; + + DXGI_QUERY_VIDEO_MEMORY_INFO info {}; + + if (FAILED(g_nrAdapter->QueryVideoMemoryInfo(0, DXGI_MEMORY_SEGMENT_GROUP_LOCAL, &info))) + return 0; + + return info.CurrentUsage; +} + // The inject point decides which buffer is being measured -- the upscaler's linear output or the // finished frame in swapchain format -- so a reading taken before a change describes a different // picture to one taken after. Everything else that depends on the format is invalidated here. @@ -684,6 +828,16 @@ void ForgetCalibration() g_nr.calibWhy = "measuring..."; } +// Every feature in the chain owns a history, and a cut invalidates all of them at once. Only the +// creation of a single extra feature resets one index on its own. +void ResetAllHistories() +{ + g_nr.reset = true; + + for (bool& r : g_nr.passReset) + r = true; +} + void ReleaseSurfacesIfFormatChanged(DXGI_FORMAT needed) { if (g_nr.output == nullptr || g_nr.output->GetDesc().Format == needed) @@ -701,10 +855,16 @@ void ReleaseSurfacesIfFormatChanged(DXGI_FORMAT needed) ParkNrFeature(f); for (ID3D12Resource** r : - { &g_nr.output, &g_nr.colorCopy, &g_nr.hdrCopy, &g_nr.colorSmall }) + { &g_nr.output, &g_nr.colorCopy, &g_nr.hdrCopy, &g_nr.colorSmall, &g_nr.passScratch }) ParkNrResource(*r); - g_nr.reset = true; + // A new format is a new allocation question, so a ceiling lowered by a failure under the old one + // is not an answer to it. + g_nr.passCeiling = DlssNr::kMaxPasses; + g_nr.passBuildAfter = 0; + g_nr.featureBytes = 0; + + ResetAllHistories(); } void Barrier(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* res, D3D12_RESOURCE_STATES from, @@ -1503,6 +1663,7 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c if (FAILED(target->GetDevice(IID_PPV_ARGS(&device))) || device == nullptr) { ReportSkipOnce("the output texture belongs to no D3D12 device"); + Barrier(cmdList, target, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, outputArrival); return; } @@ -1572,7 +1733,7 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c if (frame.Reset) { - g_nr.reset = true; + ResetAllHistories(); static unsigned long long resets = 0; ++resets; @@ -1624,6 +1785,7 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c { g_nr.failed = true; LOG_ERROR("DLSS-NR unavailable: {}", g_nr.reason); + Barrier(cmdList, target, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, outputArrival); device->Release(); return; } @@ -1636,6 +1798,18 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c const auto workHeight = (unsigned int) (height * workScale + 0.5f); const bool reduced = workWidth != width || workHeight != height; + // How many times the model is asked to run over this frame. A count of features, not a create + // argument of any one of them, so it is deliberately absent from TuningMatchesFeature: raising it + // must not tear down a feature that is still correct. + // + // Forced to one on the proxy path, which evaluates the main feature and returns several hundred + // lines below the ramp. Read there instead of here, the count would still cost a work-size surface + // and four features' worth of driver history for a chain that is never reached. + const unsigned int wantPasses = + cfg.DlssNrUseProxy.value_or_default() + ? 1u + : std::clamp(cfg.DlssNrPasses.value_or_default(), 1u, (uint32_t) DlssNr::kMaxPasses); + ReleaseSurfacesIfFormatChanged(desc.Format); const bool resolutionChanged = g_nr.width != width || g_nr.height != height || @@ -1664,6 +1838,13 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c ParkNrResource(g_nr.colorCopy); ParkNrResource(g_nr.hdrCopy); ParkNrResource(g_nr.colorSmall); + ParkNrResource(g_nr.passScratch); + + // A new resolution is a new allocation question. What failed at 4K is not what is being + // asked at 1080p, so the ceiling a failure there left behind does not carry over. + g_nr.passCeiling = DlssNr::kMaxPasses; + g_nr.passBuildAfter = 0; + g_nr.featureBytes = 0; } } @@ -1679,6 +1860,26 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c if (reduced && g_nr.colorSmall == nullptr) g_nr.colorSmall = CreateScratch(device, desc.Format, workWidth, workHeight); + // The chain's second surface. Kept once allocated even if the count returns to one: a resolution + // change parks it, and reallocating a work-size texture every time a slider crosses two is the + // churn the scratch set already refuses above. + if (wantPasses > 1 && g_nr.passCeiling > 1 && g_nr.passScratch == nullptr && g_nr.output != nullptr) + { + g_nr.passScratch = CreateScratch(device, desc.Format, workWidth, workHeight); + + if (g_nr.passScratch == nullptr) + { + // The ceiling carries the latch. Every term of the guard above stays true after a failure, + // so without it this is a committed-resource attempt and a warning every frame for the + // rest of the session, under exactly the memory pressure that caused the failure. The + // ceiling is restored wherever the surfaces are parked, which is where the question is + // worth asking again. + g_nr.passCeiling = 1; + LOG_WARN("DLSS-NR: the extra passes need a second work surface and it would not allocate; " + "running one pass"); + } + } + if (g_nr.meter == nullptr) { g_nr.meter = CreateScratch(device, DXGI_FORMAT_R32_FLOAT, kDlssNrMeterGrid, kDlssNrMeterGrid); @@ -1733,6 +1934,7 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c g_nr.failed = true; g_nr.reason = "nvngx_dlssnr.dll was not found beside OptiScaler or the game"; LOG_ERROR("DLSS-NR unavailable: {}", g_nr.reason); + Barrier(cmdList, target, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, outputArrival); device->Release(); return; } @@ -1745,10 +1947,17 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c if (restoreRequired && !D3D12Hooks::CanRestoreRootSignature(cmdList)) { ReportSkipOnce("the upscaler could not restore state on the creation frame"); + Barrier(cmdList, target, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, outputArrival); device->Release(); return; } + // Priced here so the first extra pass has a measurement to check the budget against rather + // than a guess. Every feature in the chain is built with the same arguments at the same + // working resolution, so one is worth what the next one costs. Only asked when extra passes + // are wanted: at one pass nothing reads it, and the query has an adapter to find first. + const unsigned long long usedBeforeCreate = wantPasses > 1 ? LocalMemoryUsed(device) : 0; + ScopedNrStateEnvelope creationEnvelope(cmdList); SetExtras(cfg, nullptr, nullptr, 0, 0, 0, 0); @@ -1775,26 +1984,38 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c // 0x-452FFFFF, which no one can decode back to 0xBAD00001. LOG_ERROR("DLSS-NR create failed: init 0x{:X} ({}), create 0x{:X} ({})", initResult, NgxResultName(initResult), createResult, NgxResultName(createResult)); + Barrier(cmdList, target, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, outputArrival); device->Release(); return; } g_nr.width = width; g_nr.height = height; - g_nr.reset = true; + ResetAllHistories(); RecordBuiltTuning(cfg); + + if (usedBeforeCreate != 0) + { + const unsigned long long usedAfterCreate = LocalMemoryUsed(device); + + if (usedAfterCreate > usedBeforeCreate) + g_nr.featureBytes = usedAfterCreate - usedBeforeCreate; + } + 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); // 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 // first; the first evaluate happens next frame. One frame without the model is invisible. + Barrier(cmdList, target, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, outputArrival); device->Release(); return; } if (g_nr.feature == nullptr) { + Barrier(cmdList, target, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, outputArrival); device->Release(); return; } @@ -1827,6 +2048,7 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c g_nr.failed = true; g_nr.reason = "the colour codec would not compile"; LOG_ERROR("DLSS-NR unavailable: {}", g_nr.reason); + Barrier(cmdList, target, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, outputArrival); device->Release(); return; } @@ -1836,6 +2058,7 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c // of the game's exposure rather than a number worth asking anyone to guess: measured means of 0.065, // 1.8 and 185 have all been seen in this one game. ++g_frames; + ObservePresent(); TickNrRetired(); CheckCaptureTrigger(); @@ -1849,6 +2072,139 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c LOG_INFO("DLSS-NR wrote matched before/after frames to {}", written); } + // The extra passes, one feature apiece, each built a frame before it is first evaluated. + // + // Creating and evaluating a feature on one command list is the dice-roll that hung the GPU, so a + // build is the last thing this frame records and the evaluate below is never reached on it. Sited + // under TickNrRetired so the retirement clock still runs on the frames it returns from. + const unsigned int livePasses = std::min(wantPasses, g_nr.passCeiling); + + // Retiring costs no frame, so it happens whether or not a build is due. + for (unsigned int i = livePasses; i < DlssNr::kMaxPasses; ++i) + ParkNrFeature(g_nr.passFeature[i]); + + // One build per settle, not one per frame. Rebuilding NGX features in quick succession exhausts + // the driver's latches and the model stops answering until the process restarts, so a 1 -> 5 + // change ramps over about 120 frames. + if (g_nr.passScratch != nullptr && g_frames >= g_nr.passBuildAfter) + { + for (unsigned int i = 1; i < livePasses; ++i) + { + if (g_nr.passFeature[i] != nullptr) + continue; + + auto passSnippet = Util::FindFilePath(g_dllDir, "nvngx_dlssnr.dll"); + + if (!passSnippet.has_value()) + passSnippet = Util::FindFilePath(Util::ExePath().remove_filename(), "nvngx_dlssnr.dll"); + + if (!passSnippet.has_value()) + { + g_nr.passCeiling = i; + LOG_WARN("DLSS-NR: nvngx_dlssnr.dll is no longer findable, so pass {} cannot be built; " + "running {}", + i + 1, i); + break; + } + + // Same guard and same envelope as the main feature's creation, for the same reason: the + // create records onto the game's list and binds NGX's heaps, root signature and pipeline. + if (restoreRequired && !D3D12Hooks::CanRestoreRootSignature(cmdList)) + { + ReportSkipOnce("the upscaler could not restore state on the creation frame"); + Barrier(cmdList, target, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, outputArrival); + device->Release(); + return; + } + + // A feature's history is sized by the driver at the model's working resolution and lives + // inside the snippet, so the count is priced against the last one that was measured. Not + // enough room is a wait, not a verdict: the ceiling stays where it is and the question is + // asked again after the next settle, because the game's own working set moves. + if (g_nr.featureBytes != 0) + { + unsigned long long headroom = 0; + + if (LocalMemoryHeadroom(device, headroom) && headroom < g_nr.featureBytes) + { + g_nr.passBuildAfter = g_frames + kSettleFrames; + + if (!g_saidMemoryTight) + { + g_saidMemoryTight = true; + LOG_WARN("DLSS-NR: pass {} is waiting on video memory ({} MB free, a feature " + "costs {} MB)", + i + 1, headroom >> 20, g_nr.featureBytes >> 20); + } + + break; + } + } + + const unsigned long long usedBeforeCreate = LocalMemoryUsed(device); + + { + ScopedNrStateEnvelope creationEnvelope(cmdList); + + SetExtras(cfg, nullptr, nullptr, 0, 0, 0, 0); + // Every argument the main feature was built with. The tuning is latched at creation, + // so a pass built with a different one would be a different model that nothing here + // records or could ever notice. + g_nr.passFeature[i] = g_nr.create( + passSnippet->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(), cfg.DlssNrLocalTone.value_or_default(), + cfg.DlssNrSkinStructure.value_or_default(), + cfg.DlssNrAutoMask.value_or_default() ? 1 : 0, 1); + } + + g_nr.passBuildAfter = g_frames + kSettleFrames; + + if (g_nr.passFeature[i] == nullptr) + { + // A missing extra pass is a weaker picture, not a broken session, so the failure + // latch is left alone. The ceiling drops instead: retrying a failed create every + // frame is what turns a failure into a crash. + g_nr.passCeiling = i; + const auto createResult = + (unsigned int) (g_nr.lastCreate != nullptr ? *g_nr.lastCreate : 0); + LOG_WARN("DLSS-NR: pass {} would not build (create 0x{:X}, {}); running {} for this " + "session", + i + 1, createResult, NgxResultName(createResult), i); + } + else + { + g_nr.passReset[i] = true; + g_nr.passBuiltOn[i] = cmdList; + g_nr.passBuiltAtPresent[i] = g_lastPresent; + g_saidMemoryTight = false; + + // What this one cost, for the next one's headroom check. Measured rather than + // guessed: the driver sizes the history and nothing here knows the model's shape. + if (usedBeforeCreate != 0) + { + const unsigned long long usedAfterCreate = LocalMemoryUsed(device); + + if (usedAfterCreate > usedBeforeCreate) + g_nr.featureBytes = usedAfterCreate - usedBeforeCreate; + } + + if (g_nr.featureBytes != 0) + LOG_INFO("DLSS-NR: pass {} built at {}x{}, {} MB", i + 1, workWidth, workHeight, + g_nr.featureBytes >> 20); + else + LOG_INFO("DLSS-NR: pass {} built at {}x{}", i + 1, workWidth, workHeight); + } + + Barrier(cmdList, target, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, outputArrival); + device->Release(); + return; + } + } + // Paper white, and nothing else. The frame is divided by this and encoded, and the soft knee // above 0.75 takes whatever is left over. // @@ -1867,6 +2223,9 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c // The device reference taken at the top of this function is released on every other path out. // It was not released here, and this is the one path a bindless game takes every single frame // -- so the game that most needed this skip was also leaking a device reference per frame. + // The output goes back the same way, for the same reason: this is a per-frame path, and a + // resource left in a state the game's tracking does not expect is a wrong barrier every frame. + Barrier(cmdList, target, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, outputArrival); device->Release(); return; } @@ -2087,19 +2446,88 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c return; } + // The chain. Pass p writes work[p & 1] and reads what pass p - 1 wrote; modelInput -- the proxy + // the encode built and the picture the resolve differences against -- is never written, so the + // edit the resolve receives is the whole chain's rather than the last pass's. + // + // Two surfaces alternating rather than a copy back over the input: no bandwidth, and no evaluate + // ever reads and writes one resource. Both rest in UNORDERED_ACCESS, the state CreateScratch + // leaves them in and the state every other transition in this function assumes. + ID3D12Resource* work[2] = { g_nr.output, g_nr.passScratch }; + D3D12_RESOURCE_STATES workState[2] = { D3D12_RESOURCE_STATE_UNORDERED_ACCESS, + D3D12_RESOURCE_STATE_UNORDERED_ACCESS }; + + const auto setWork = [&](unsigned int slot, D3D12_RESOURCE_STATES to) + { + if (work[slot] == nullptr) + return; + + Barrier(cmdList, work[slot], workState[slot], to); + workState[slot] = to; + }; + + // Counted from the features that exist and have been submitted, not from the setting: while the + // ramp is still building, a frame runs only the passes it holds, and a feature whose creation is + // still sitting in this open command list is not one of them. + unsigned int passes = 1; + + if (work[1] != nullptr) + { + while (passes < livePasses && g_nr.passFeature[passes] != nullptr && PassWasSubmitted(passes, cmdList)) + ++passes; + } + 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); + int result = NVSDK_NGX_Result_Success; + unsigned int answer = 0; + + for (unsigned int pass = 0; pass < passes; ++pass) + { + const unsigned int slot = pass & 1u; + + setWork(slot, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); + + const bool wantReset = pass == 0 ? g_nr.reset : g_nr.passReset[pass]; + + // Every feature in the chain sees one frame per frame, so each is handed the frame's own + // guides and the frame's own motion scale. Telling a later pass nothing moved would be a lie + // about a full frame of camera travel. + result = g_nr.evaluate( + cmdList, pass == 0 ? g_nr.feature : g_nr.passFeature[pass], g_nr.capabilityParams, + pass == 0 ? modelInput : work[(pass - 1) & 1u], depthIn, motionIn, work[slot], workWidth, + workHeight, guideWidth, guideHeight, g_nr.guideDepthInverted ? 1 : 0, wantReset ? 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); + + if (pass > 0) + g_nr.passReset[pass] = false; + + if (result != NVSDK_NGX_Result_Success) + { + if (pass == 0) + break; + + // A later pass refusing costs its own contribution, not the session. The picture is + // whatever the last pass that did run wrote, and the ceiling drops so this is not + // attempted again. + LOG_WARN("DLSS-NR: pass {} returned 0x{:X} ({}); running {} for this session", pass + 1, + (uint32_t) result, NgxResultName((unsigned int) result), pass); + ParkNrFeature(g_nr.passFeature[pass]); + g_nr.passCeiling = pass; + passes = pass; + result = NVSDK_NGX_Result_Success; + break; + } + + answer = slot; + + if (pass + 1 < passes) + setWork(slot, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE); + } if (g_ngxTime != nullptr) g_ngxTime->End(cmdList); @@ -2190,6 +2618,10 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c unsigned int residual; unsigned int workW; unsigned int workH; + + // The model timer brackets the whole chain, so its figure is the sum over this many + // evaluates and cannot be read without it. + unsigned int passes; }; static ComposeReport loggedCompose {}; @@ -2207,7 +2639,8 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c resolveParams.CompareMode, resolveParams.Transfer, g_nr.workWidth, - g_nr.workHeight }; + g_nr.workHeight, + passes }; if (!loggedCompose.valid || loggedCompose.whitePoint != composeNow.whitePoint || loggedCompose.transfer != composeNow.transfer || loggedCompose.colour != composeNow.colour || @@ -2216,23 +2649,22 @@ 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{} x{} pass(es), 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); - DispatchPass(cmdList, resolveParams, modelInput, g_nr.output, g_nr.hdrCopy, motionIn, + setWork(answer, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE); + DispatchPass(cmdList, resolveParams, modelInput, work[answer], g_nr.hdrCopy, motionIn, nullptr, target, nullptr); - Barrier(cmdList, g_nr.output, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, - D3D12_RESOURCE_STATE_UNORDERED_ACCESS); + setWork(answer, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); // On-demand capture works in this path too: the staging copy still holds the frame as the // upscaler produced it, and the edited frame is the output itself. The write happens a few @@ -2256,6 +2688,11 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c NgxResultName((unsigned int) result)); } + // Both work surfaces go back to the state the next frame assumes. Every way out of the chain + // arrives here, the mid-chain break included, and Barrier() skips the ones already there. + setWork(0, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); + setWork(1, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); + Barrier(cmdList, g_nr.hdrCopy, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); @@ -2331,7 +2768,7 @@ void RetryAfterFailure() { g_nr.failed = false; g_nr.reason = ""; - g_nr.reset = true; + ResetAllHistories(); } @@ -2738,6 +3175,26 @@ void Shutdown() f = nullptr; } + // The ceiling and the price are verdicts about a device and a resolution that are both going + // away. Carried across, a create that ran out of memory at 4K would still cap a session that has + // since resized down, and a device change would inherit the dead device's answer. + g_nr.passCeiling = DlssNr::kMaxPasses; + g_nr.passBuildAfter = 0; + g_nr.featureBytes = 0; + g_saidMemoryTight = false; + + for (auto& l : g_nr.passBuiltOn) + l = nullptr; + + for (auto& p : g_nr.passBuiltAtPresent) + p = 0; + + if (g_nrAdapter != nullptr) + { + g_nrAdapter->Release(); + g_nrAdapter = nullptr; + } + if (g_nr.output != nullptr) { g_nr.output->Release(); @@ -2762,6 +3219,12 @@ void Shutdown() g_nr.colorSmall = nullptr; } + if (g_nr.passScratch != nullptr) + { + g_nr.passScratch->Release(); + g_nr.passScratch = nullptr; + } + if (g_nr.meter != nullptr) { g_nr.meter->Release(); From 5a34e8d19d9468bc908a6f0b9167e74c2cb95466 Mon Sep 17 00:00:00 2001 From: y4my4my4m <8145020+y4my4my4m@users.noreply.github.com> Date: Fri, 4 Sep 2026 22:29:58 +0900 Subject: [PATCH 3/3] DLSS-NR: note the highlight guard tracks the pass count --- OptiScaler.ini | 5 +++++ OptiScaler/dlssnr/DlssNr_Menu.cpp | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/OptiScaler.ini b/OptiScaler.ini index a7f009d19..45410786d 100644 --- a/OptiScaler.ini +++ b/OptiScaler.ini @@ -1585,6 +1585,11 @@ WhitePointScale=auto ; The most the pass may brighten any pixel, as a multiple of what it already was. Darkening is not ; capped. Guards against the model turning a bright light into a string of coloured cells. +; +; The guard is applied once, to the finished composition, while Passes compounds the ratio it bounds. +; A value near the pass count keeps the headroom each pass gets roughly constant -- 1 pass at 1.0, +; 2 at 2.0, 3 at 3.0. Left at the default the third pass spends most of its contribution against the +; clamp. ; Default (auto) is 2.0 MaxRatio=auto diff --git a/OptiScaler/dlssnr/DlssNr_Menu.cpp b/OptiScaler/dlssnr/DlssNr_Menu.cpp index 3ca676ca2..c8a9b9c51 100644 --- a/OptiScaler/dlssnr/DlssNr_Menu.cpp +++ b/OptiScaler/dlssnr/DlssNr_Menu.cpp @@ -881,6 +881,10 @@ void RenderMenu(Config* config, float menuResScale) "\ninto the frame does the most damage: an early version turned every strip light" "\nin the scene into a string of coloured cells. 2x leaves detail intact while" "\nmaking that failure impossible. Raise it only if bright areas look clipped." + "\n\nThe guard is applied once, to the finished composition, while Passes" + "\ncompounds the ratio it bounds. A value near the pass count keeps the headroom" + "\neach pass gets roughly constant -- 1 pass at 1x, 2 at 2x, 3 at 3x. Left where" + "\nit is, the third pass spends most of its contribution against the clamp." "\n\nDarkening was once left uncapped, and the guard itself only bound the" "\ncolour-strength-zero end of the blend -- so at the default strength it bound" "\nnothing at all. Nioh 3 is why both are fixed: in a scene dark enough that the"