Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions OptiScaler.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -1593,6 +1598,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
Expand Down
19 changes: 12 additions & 7 deletions OptiScaler/Config.h
Original file line number Diff line number Diff line change
Expand Up @@ -445,16 +445,21 @@ class Config
// picture that had been tuned came back wrong for a reason nothing on screen explained.
CustomOptional<float> 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<uint32_t> DlssNrPasses { 1 };

// Which depth convention the model is told the guide uses.
Expand Down
4 changes: 4 additions & 0 deletions OptiScaler/dlssnr/DlssNrFeature_Dx12.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
62 changes: 62 additions & 0 deletions OptiScaler/dlssnr/DlssNr_Menu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -823,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"
Expand Down
23 changes: 23 additions & 0 deletions OptiScaler/dlssnr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading