diff --git a/OptiScaler/dlssnr/DlssNrFeature_Dx12.h b/OptiScaler/dlssnr/DlssNrFeature_Dx12.h index 51ce8dc7c..be8ee0744 100644 --- a/OptiScaler/dlssnr/DlssNrFeature_Dx12.h +++ b/OptiScaler/dlssnr/DlssNrFeature_Dx12.h @@ -178,5 +178,13 @@ std::optional LastGpuTime(); void RequestCapture(unsigned int frames); bool CaptureInProgress(); +// Session-only D3D12 inspection. Pending takes a matched snapshot on the next successful resolve; +// Held runs only that resolve with live Compare/Debug controls, never NGX with stale guides. +// Capture takes priority and resumes live rendering. Resume invalidates every model history. +enum class InspectionHoldState { Unavailable, Live, Pending, Held }; +InspectionHoldState GetInspectionHoldState(); +void RequestInspectionHold(); +void ReleaseInspectionHold(); + void Shutdown(); } // namespace DlssNr diff --git a/OptiScaler/dlssnr/DlssNrFeature_Vk.cpp b/OptiScaler/dlssnr/DlssNrFeature_Vk.cpp index fb913762d..3c4e930d7 100644 --- a/OptiScaler/dlssnr/DlssNrFeature_Vk.cpp +++ b/OptiScaler/dlssnr/DlssNrFeature_Vk.cpp @@ -627,7 +627,7 @@ bool EvaluateAtSeamVk(VkCommandBuffer cmdBuffer, NVSDK_NGX_Parameter* params, Vk // The model's working size. The slider is a fraction of the frame; at 1 it is the frame, and the // reduced path below never runs, so the default is byte-for-byte what it was. - const float workScale = std::clamp(cfg.DlssNrWorkingScale.value_or_default(), 0.25f, 1.0f); + const float workScale = std::clamp(cfg.DlssNrWorkingScale.value_or_default(), 0.25f, 2.0f); const uint32_t workWidth = (uint32_t) (width * workScale + 0.5f); const uint32_t workHeight = (uint32_t) (height * workScale + 0.5f); const bool reduced = workWidth != width || workHeight != height; diff --git a/OptiScaler/dlssnr/DlssNr_Menu.cpp b/OptiScaler/dlssnr/DlssNr_Menu.cpp index 95de73762..dcdb73f0b 100644 --- a/OptiScaler/dlssnr/DlssNr_Menu.cpp +++ b/OptiScaler/dlssnr/DlssNr_Menu.cpp @@ -376,24 +376,22 @@ void RenderMenu(Config* config, float menuResScale) ? pendingScale : (int) lroundf(config->DlssNrWorkingScale.value_or_default() * 100.0f); - if (ImGui::SliderInt("Model resolution", &scalePercent, 25, 100, "%d%%")) + if (ImGui::SliderInt("Model resolution", &scalePercent, 25, 200, "%d%%")) pendingScale = scalePercent; if (ImGui::IsItemDeactivatedAfterEdit() && pendingScale >= 0) { - config->DlssNrWorkingScale = std::clamp(pendingScale, 25, 100) / 100.0f; + config->DlssNrWorkingScale = std::clamp(pendingScale, 25, 200) / 100.0f; pendingScale = -1; } - HelpMarker("What fraction of the frame the model works at. Cost falls with the square of" - "\nthis, so half resolution is roughly a quarter of the time." - "\n\nThe frame is never reduced. Only the model's contribution is computed small" - "\nand enlarged, so the picture underneath is untouched whatever this says." - "\n\nWhat it trades: the shading the model adds is broad and survives enlargement;" - "\nthe fine structure it synthesises does not, and softens. Worth having when the" - "\npass costs more than you want to pay for the detail it returns." - "\n\nThe frame itself stays at full detail whatever this says -- only the" - "\nmodel's own work is done small."); + HelpMarker("The model's raster as a percentage of the frame, from 25% to 200%." + "\nThe original frame stays at full detail; only the model's work is resampled." + "\n\nBelow 100%, the model works on a smaller picture and its answer is enlarged." + "\nHalf resolution is a quarter of the model pixels, but fine detail softens." + "\n\nAbove 100%, the model works on a larger raster and its answer returns to" + "\nframe size. 200% is twice each axis, four times the model pixels and higher" + "\nVRAM cost; it does not add game geometry samples."); { bool dual = config->DlssNrDualFeature.value_or_default(); @@ -1152,7 +1150,29 @@ void RenderMenu(Config* config, float menuResScale) // while the exaggeration slider was still at 32x and deserves a clean re-run. - if (DlssNr::CaptureInProgress()) + const auto hold = DlssNr::GetInspectionHoldState(); + const bool holding = hold == DlssNr::InspectionHoldState::Held; + const bool holdPending = hold == DlssNr::InspectionHoldState::Pending; + const bool capturing = DlssNr::CaptureInProgress(); + ImGui::BeginDisabled(!holding && !holdPending && + (vulkan || capturing || hold == DlssNr::InspectionHoldState::Unavailable)); + if (ImGui::Button(holding ? "Resume" : holdPending ? "Cancel hold" : "Hold frame")) + { + if (holding || holdPending) + DlssNr::ReleaseInspectionHold(); + else + DlssNr::RequestInspectionHold(); + } + ImGui::EndDisabled(); + HelpMarker("D3D12 inspection only: keeps the next successful frame's proxy, model answer" + "\nand original together. Compare and Debug remain interactive; the model does" + "\nnot run again until Resume. This does not pause the game." + "\n\nModel and colour tuning apply after Resume. A resize releases the hold." + "\nCapture resumes live rendering first; Hold waits until capture has finished." + "\nUnavailable on native Vulkan or the experimental proxy path."); + ImGui::SameLine(); + + if (capturing) { ImGui::TextDisabled("Capturing..."); } diff --git a/OptiScaler/menu/menu_common.cpp b/OptiScaler/menu/menu_common.cpp index e7597895a..42b6239a0 100644 --- a/OptiScaler/menu/menu_common.cpp +++ b/OptiScaler/menu/menu_common.cpp @@ -3480,17 +3480,26 @@ void MenuCommon::RenderFrameGenerationSelection(RenderMenuContext& ctx) ImGui::EndDisabled(); - if (state.dlssgGameDMFGSupported && !dlssgInputOrOutput) + if (!dlssgInputOrOutput) { - ImGui::SameLine(0.0f, 16.0f); - - if (bool dynamicMFG = config->FGDLSSGOverrideForceDMFG.value_or_default(); - ImGui::Checkbox("Force Dynamic MFG", &dynamicMFG)) + if (state.dlssgGameDMFGSupported) { - config->FGDLSSGOverrideForceDMFG = dynamicMFG; - StreamlineHooks::updateDlssgOptions(); + ImGui::SameLine(0.0f, 16.0f); + + if (bool dynamicMFG = config->FGDLSSGOverrideForceDMFG.value_or_default(); + ImGui::Checkbox("Force Dynamic MFG", &dynamicMFG)) + { + config->FGDLSSGOverrideForceDMFG = dynamicMFG; + StreamlineHooks::updateDlssgOptions(); + } } + SeparatorWithHelpMarker("MFG Unlock (RTX 40)", + "Raises the generated frame maximum in nvngx_dlssg.dll and in the count " + "Streamline reports, so the ratio above offers up to 6X on pre-Blackwell " + "cards. Patched in memory; the file on disk is not touched. Takes effect " + "on the next game start. Undocumented and unsupported by NVIDIA."); + bool adaUnlock = config->FGDLSSGAdaMfgUnlock.value_or_default(); if (ImGui::Checkbox("Unlock MFG on RTX 40", &adaUnlock)) @@ -3539,14 +3548,10 @@ void MenuCommon::RenderFrameGenerationSelection(RenderMenuContext& ctx) ImGui::TextColored(good, "%u kernel containers run the Blackwell image.", mfg.KernelsRewritten); } } + } - ShowHelpMarker("Raises the generated frame maximum in nvngx_dlssg.dll and in the count Streamline " - "reports, so the ratio above offers up to 6X on pre-Blackwell cards. Patched in " - "memory; the file on disk is not touched. Takes effect on the next game start.\n\n" - "Pacing above 2X is uneven -- the module expects Blackwell's flip metering hardware. " - "Try DisableFlipMetering under [NvApi] alongside it.\n\n" - "Undocumented and unsupported by NVIDIA."); - + if (!dlssgInputOrOutput && state.dlssgGameDMFGSupported) + { ImGui::BeginDisabled(state.dlssgLastSetMode != sl::DLSSGMode::eDynamic); static float fpsTarget = config->FGDLSSGFramerateTargetDMFG.value_or_default(); ImGui::SliderFloat("DMFG FPS Target", &fpsTarget, 0, 200, "%.0f"); diff --git a/OptiScaler/shaders/dlssnr/DlssNr_Dx12.cpp b/OptiScaler/shaders/dlssnr/DlssNr_Dx12.cpp index 319450a2d..63d988d5f 100644 --- a/OptiScaler/shaders/dlssnr/DlssNr_Dx12.cpp +++ b/OptiScaler/shaders/dlssnr/DlssNr_Dx12.cpp @@ -397,6 +397,20 @@ std::optional g_lastGpuTime; // Writes matched before/after frames on request, so comparisons stop depending on video. capture::FrameCapture g_capture; +// All inspection state and capture requests are serialized by g_nrMutex. These are private GPU +// copies, not references to scratch surfaces that the next live frame will overwrite. +struct InspectionHold +{ + DlssNr::InspectionHoldState state = DlssNr::InspectionHoldState::Live; + std::array images {}; // actual model input, final answer, untouched original + DlssNrConstants constants {}; + ID3D12Device* device = nullptr; // borrowed from images, which keep the device alive + bool split = false; +}; +InspectionHold g_hold; + +void RequestCaptureLocked(unsigned int frames); + // One capture happens on its own each session, so there is always a fresh sample without anyone having // to remember to ask. Started after the scene has had a moment to settle: the first frames after a // feature is built carry its reset, and are not representative of anything. @@ -484,7 +498,7 @@ void CheckCaptureTrigger() if (std::filesystem::exists(trigger, ec)) { std::filesystem::remove(trigger, ec); - DlssNr::RequestCapture(capture::kMaxFrames); + RequestCaptureLocked(capture::kMaxFrames); LOG_INFO("DLSS-NR capture requested by trigger file"); } } @@ -862,6 +876,30 @@ void ResetAllHistories() r = true; } +// Never release a texture from the menu thread while the GPU may still read it. Use the same +// deferred retirement as the model's scratch set; Shutdown releases the remaining resources. +void ReleaseInspectionHoldLocked() +{ + if (g_hold.state == DlssNr::InspectionHoldState::Held) + ResetAllHistories(); + + for (auto& image : g_hold.images) + ParkNrResource(image); + + g_hold = {}; +} + +void RequestCaptureLocked(unsigned int frames) +{ + if (frames == 0 || g_capture.isActive()) + return; + + // Includes trigger-file requests: a held resolve must never strand the readback/write clock. + ReleaseInspectionHoldLocked(); + ClearCaptureDirectory(); + g_capture.request(frames); +} + void ReleaseSurfacesIfFormatChanged(DXGI_FORMAT needed) { if (g_nr.output == nullptr || g_nr.output->GetDesc().Format == needed) @@ -1259,6 +1297,62 @@ void Barrier(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* res, D3D12_RESO cmdList->ResourceBarrier(1, &b); } +void SnapshotInspectionHold(ID3D12Device* device, ID3D12GraphicsCommandList* cmdList, + ID3D12Resource* modelInput, ID3D12Resource* answer, + ID3D12Resource* original, const DlssNrConstants& constants, bool split) +{ + const std::array sources { modelInput, answer, original }; + std::array images {}; + for (size_t i = 0; i < images.size(); ++i) + { + const auto desc = sources[i]->GetDesc(); + images[i] = CreateScratch(device, desc.Format, (unsigned int) desc.Width, desc.Height); + if (images[i] == nullptr) + { + // No commands reference any of these yet, so partial allocation can be released now. + for (auto* image : images) + if (image != nullptr) + image->Release(); + g_hold.state = DlssNr::InspectionHoldState::Live; + LOG_WARN("DLSS-NR: Hold frame could not allocate its snapshot; continuing live"); + return; + } + } + + // All three sources are shader-readable here, after a successful model chain and resolve. + // Copies follow those writes on their command list; later resolves read only these snapshots. + for (size_t i = 0; i < images.size(); ++i) + { + Barrier(cmdList, sources[i], D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, + D3D12_RESOURCE_STATE_COPY_SOURCE); + Barrier(cmdList, images[i], D3D12_RESOURCE_STATE_UNORDERED_ACCESS, + D3D12_RESOURCE_STATE_COPY_DEST); + cmdList->CopyResource(images[i], sources[i]); + Barrier(cmdList, sources[i], D3D12_RESOURCE_STATE_COPY_SOURCE, + D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE); + Barrier(cmdList, images[i], D3D12_RESOURCE_STATE_COPY_DEST, + D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE); + } + g_hold.images = images; + g_hold.constants = constants; + g_hold.device = device; + g_hold.split = split; + g_hold.state = DlssNr::InspectionHoldState::Held; + LOG_INFO("DLSS-NR: inspection frame held"); +} + +// Only inspection settings remain live. In particular, the white point and transfer interpretation +// belong to the stored images and must not drift with the game's current exposure or model tuning. +void ApplyInspectionControls(DlssNrConstants& constants, const Config& cfg) +{ + constants.DebugView = cfg.DlssNrDebugView.value_or_default(); + constants.DebugScale = cfg.DlssNrWhitePointScale.value_or_default(); + constants.CompareMode = cfg.DlssNrCompare.value_or_default(); + constants.CompareSplit = cfg.DlssNrCompareSplit.value_or_default(); + constants.CompareZoom = std::max(1.0f, cfg.DlssNrCompareZoom.value_or_default()); + constants.CompareSwap = cfg.DlssNrCompareSwap.value_or_default() ? 1u : 0u; +} + // The surface the pre-upscale edit lands on, matched to the game's colour buffer. // // The buffer the game hands the upscaler is not necessarily a UAV, so the edit cannot be written back @@ -1787,6 +1881,7 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c { std::lock_guard nrLock(g_nrMutex); const Config& cfg = *Config::Instance(); + g_nr.wroteTarget = false; if (g_nr.failed || cmdList == nullptr || colour == nullptr || depth == nullptr || motion == nullptr || output == nullptr) @@ -1795,8 +1890,6 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c return; } - g_nr.wroteTarget = false; - ID3D12Resource* target = output; // The picture the model is shown, against the picture the edit lands on. Equal on the pass that @@ -1849,6 +1942,69 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c const auto width = (unsigned int) desc.Width; const auto height = desc.Height; + ++g_frames; + ObservePresent(); + TickNrRetired(); + CheckCaptureTrigger(); + + if (g_captureWriteAtFrame != 0 && g_frames >= g_captureWriteAtFrame) + { + g_captureWriteAtFrame = 0; + const auto captureDir = Util::DllPath().remove_filename() / "dlssnr-capture"; + const auto written = g_capture.write(captureDir); + + if (!written.empty()) + LOG_INFO("DLSS-NR wrote matched before/after frames to {}", written); + } + + if (g_hold.state == DlssNr::InspectionHoldState::Held) + { + const auto heldDesc = g_hold.images[2]->GetDesc(); + if (g_hold.device != device || heldDesc.Width != desc.Width || heldDesc.Height != desc.Height || + heldDesc.Format != desc.Format || g_hold.split != split || cfg.DlssNrUseProxy.value_or_default()) + ReleaseInspectionHoldLocked(); + } + + // A held frame bypasses encode, exposure/guide reads, feature rebuilds and NGX entirely. + // Keep the same state envelope and output-arrival contract as the live resolve. + if (g_hold.state == DlssNr::InspectionHoldState::Held) + { + 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 on the held frame"); + } + else + { + ScopedNrStateEnvelope envelope(cmdList); + auto constants = g_hold.constants; + ApplyInspectionControls(constants, cfg); + g_nr.wroteTarget = DispatchPass(cmdList, constants, g_hold.images[0], g_hold.images[1], + g_hold.images[2], nullptr, nullptr, target, nullptr); + if (!g_nr.wroteTarget) + { + LOG_WARN("DLSS-NR: held resolve failed; resuming live rendering"); + ReleaseInspectionHoldLocked(); + } + } + Barrier(cmdList, target, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, outputArrival); + device->Release(); + return; + } + + // Never reuse the compositor or a held snapshot on another device. Its owner must recreate it. + if (device != _device) + { + ReportSkipOnce("the D3D12 device changed; the pass needs recreation"); + Barrier(cmdList, target, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, outputArrival); + device->Release(); + return; + } + + if (cfg.DlssNrUseProxy.value_or_default()) + ReleaseInspectionHoldLocked(); + // 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 // needing them resampled, which is why nothing here rescales anything. @@ -1971,7 +2127,7 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c // What the model works at. The frame and its edit stay full resolution; only the model's input and // answer shrink, and the resolve enlarges the answer while compositing. float workScale = cfg.DlssNrWorkingScale.value_or_default(); - workScale = workScale < 0.25f ? 0.25f : (workScale > 1.0f ? 1.0f : workScale); + 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; @@ -2240,21 +2396,6 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c // finished, sRGB-encoded frames. The white point is what maps one to the other, and it is a property // 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(); - - if (g_captureWriteAtFrame != 0 && g_frames >= g_captureWriteAtFrame) - { - g_captureWriteAtFrame = 0; - const auto captureDir = Util::DllPath().remove_filename() / "dlssnr-capture"; - const auto written = g_capture.write(captureDir); - - if (!written.empty()) - 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 @@ -2765,15 +2906,10 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c resolveParams.Height = height; resolveParams.TransferStrength = cfg.DlssNrTransferStrength.value_or_default(); resolveParams.ColourStrength = cfg.DlssNrColourStrength.value_or_default(); - resolveParams.DebugView = cfg.DlssNrDebugView.value_or_default(); resolveParams.MaxRatio = cfg.DlssNrMaxRatio.value_or_default(); resolveParams.Transfer = cfg.DlssNrTransfer.value_or_default(); - resolveParams.DebugScale = cfg.DlssNrWhitePointScale.value_or_default(); resolveParams.Passthrough = isHdrBuffer ? 0u : 1u; - resolveParams.CompareMode = cfg.DlssNrCompare.value_or_default(); - resolveParams.CompareSplit = cfg.DlssNrCompareSplit.value_or_default(); - resolveParams.CompareZoom = std::max(1.0f, cfg.DlssNrCompareZoom.value_or_default()); - resolveParams.CompareSwap = cfg.DlssNrCompareSwap.value_or_default() ? 1u : 0u; + ApplyInspectionControls(resolveParams, cfg); // The numbers the composition actually ran with, logged when any of them changes. // @@ -2840,17 +2976,18 @@ void DlssNr_Dx12::Dispatch(ID3D12GraphicsCommandList* cmdList, ID3D12Resource* c } setWork(answer, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE); - DispatchPass(cmdList, resolveParams, modelInput, work[answer], g_nr.hdrCopy, motionIn, - nullptr, target, nullptr); + g_nr.wroteTarget = DispatchPass(cmdList, resolveParams, modelInput, work[answer], g_nr.hdrCopy, + motionIn, nullptr, target, nullptr); + if (g_nr.wroteTarget && g_hold.state == DlssNr::InspectionHoldState::Pending) + SnapshotInspectionHold(device, cmdList, modelInput, work[answer], g_nr.hdrCopy, + resolveParams, split); setWork(answer, D3D12_RESOURCE_STATE_UNORDERED_ACCESS); - g_nr.wroteTarget = true; - // 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 // frames later, once the GPU is certainly past these copies -- this path has no fence of its // own. - if (g_capture.isActive()) + if (g_nr.wroteTarget && g_capture.isActive()) { g_capture.record(cmdList, device, g_nr.colorCopy, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE, target, @@ -3485,15 +3622,48 @@ std::optional LastGpuTime() { return g_lastGpuTime; } void RequestCapture(unsigned int frames) { - ClearCaptureDirectory(); - g_capture.request(frames); + std::lock_guard nrLock(g_nrMutex); + RequestCaptureLocked(frames); } -bool CaptureInProgress() { return g_capture.isActive(); } +bool CaptureInProgress() +{ + std::lock_guard nrLock(g_nrMutex); + return g_capture.isActive(); +} + +InspectionHoldState GetInspectionHoldState() +{ + std::lock_guard nrLock(g_nrMutex); + if (g_hold.state != InspectionHoldState::Live) + return g_hold.state; + const auto& cfg = *Config::Instance(); + return g_nr.feature != nullptr && !g_nr.failed && cfg.DlssNrEnabled.value_or_default() && + !cfg.DlssNrUseProxy.value_or_default() + ? InspectionHoldState::Live : InspectionHoldState::Unavailable; +} + +void RequestInspectionHold() +{ + std::lock_guard nrLock(g_nrMutex); + const auto& cfg = *Config::Instance(); + if (g_hold.state == InspectionHoldState::Live && !g_capture.isActive() && + g_captureWriteAtFrame == 0 && g_nr.feature != nullptr && !g_nr.failed && + cfg.DlssNrEnabled.value_or_default() && !cfg.DlssNrUseProxy.value_or_default()) + g_hold.state = InspectionHoldState::Pending; +} + +void ReleaseInspectionHold() +{ + std::lock_guard nrLock(g_nrMutex); + ReleaseInspectionHoldLocked(); +} void Shutdown() { std::lock_guard nrLock(g_nrMutex); + ReleaseInspectionHoldLocked(); + g_captureWriteAtFrame = 0; for (auto& r : g_nrRetired) {