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
63 changes: 19 additions & 44 deletions OptiScaler/dlssnr/DlssNrFeature_Dx12.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include <d3d12.h>
#include <optional>

#include <shaders/dlssnr/DlssNr_Common.h>
#include <nvsdk_ngx.h>
Expand Down Expand Up @@ -33,21 +34,15 @@ namespace DlssNr
void EvaluateAfterUpscale(ID3D12GraphicsCommandList* cmdList, NVSDK_NGX_Parameter* params,
ID3D12CommandQueue* timingQueue = nullptr);



// Frame generation titles tag their UI layer through Streamline; a copy of it makes the HUD mask
// exact at the finished frame. Called at tag time.




// The settings panel, drawn inside OptiScaler's menu.
void RenderMenu(::Config* config, float menuResScale);

// Clears the session failure latch, so a failure caused by transient thrash does not cost a restart.
void RetryAfterFailure();


// Asks the model whether it will work on Direct3D 11 at all, once, and logs the answer.
//
// The bridge exists because of a claim nobody tested: "the model refuses on DX11, it answers
Expand All @@ -57,51 +52,32 @@ void RetryAfterFailure();
// entry points and initialises on the game's own device, which is where a refusal would appear.
void ProbeD3D11(void* d3d11Device);

// What scale this game's buffer is on, measured from the untouched copy of each frame.
//
// A suggestion only. Nothing applies it: the menu shows it and the user takes it or does not, which
// keeps the number visible and adjustable rather than a value that moved on its own. Confidence is
// how settled recent readings are -- 1 means they agree, 0 means the scene is changing under the
// measurement and no single value would serve.
struct CalibrationReading
{
float suggestion = 0.0f;

// How much recent readings agree. This is steadiness, not correctness: a frozen frame agrees with
// itself perfectly, so a loading screen scores full marks for a number that means nothing. Read it
// together with usable.
float steadiness = 0.0f;

unsigned long long samples = 0;

// Whether the scene is worth measuring at all. False when the frame is already tone mapped -- the
// divisor does nothing there and the reading would be a meaningless 0.9 -- or when too little of
// the picture is lit to say where the top of the range is. A dark cave gives a small number very
// steadily, which is the trap this exists to close.
bool usable = false;
const char* why = "";
};

CalibrationReading Calibration();

// Whether the model is loaded and running, for the overlay.
bool IsRunning();

// Why it is not, if it is not. Empty while it is running or has not been tried yet.
const char* FailureReason();

// What the game offers by way of exposure. Observed every frame whether or not the setting is on, so
// the menu can say whether turning it on would do anything here.
struct ExposureStatus
// Menu queries. Read atomics only; never take g_nrMutex.
bool GameExposureCanEnable();
bool GameExposureEffective();
enum class GameExposureWait
{
unsigned long long seenFrames = 0; // evaluates observed; 0 means nothing has run yet
bool offeredNow = false; // a texture on the most recent frame
bool everOffered = false; // a texture on any frame so far
float exposure = 0.0f; // last value read back, 0 if none
float preExposure = 1.0f;
NativeVulkan,
Waiting,
Passthrough,
Absent,
Available
};

ExposureStatus GameExposureStatus();
GameExposureWait GameExposureUiState();
struct GameExposureReadout
{
bool haveNumbers;
float e, p, s;
float gameW;
float slider;
};
GameExposureReadout GameExposureMenuReadout();

// The white point the exposure meter has settled on, or 0 if it has not taken a reading yet. For the
// overlay, so the number in use is visible rather than inferred.
Expand All @@ -111,7 +87,6 @@ std::optional<double> LastGpuTime();

// What the white point meter last settled on, or 0 when it is not running. For the menu.


// Writes a run of consecutive frames, each as the upscaler produced it and again after the model's edit.
// The pair is a control: same frames, same run, one variable.
void RequestCapture(unsigned int frames);
Expand Down
60 changes: 26 additions & 34 deletions OptiScaler/dlssnr/DlssNrFeature_Vk.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ namespace
// it, whichever API is being used, so these calls go through the same shim the D3D12 path does.
using PFN_VkProbe = int(__cdecl*)(const wchar_t*);
using PFN_VkInit = int(__cdecl*)(const wchar_t*, const wchar_t*, void*, void*, void*, int);
using PFN_VkCreate = void*(__cdecl*)(void*, void*, unsigned int, unsigned int, int, float, int, float, float, float,
int, int);
using PFN_VkCreate = void*(__cdecl*) (void*, void*, unsigned int, unsigned int, int, float, int, float, float, float,
int, int);
using PFN_VkEvaluate = int(__cdecl*)(void*, void*, void*, void*, void*, void*, void*, unsigned int, unsigned int,
unsigned int, unsigned int, int, int, float, int, float, float, float, int, float,
float);
Expand Down Expand Up @@ -281,8 +281,8 @@ bool CreateMeterReadback()
VkMemoryAllocateInfo alloc {};
alloc.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
alloc.allocationSize = req.size;
alloc.memoryTypeIndex = FindMemoryTypeIndex(
req.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
alloc.memoryTypeIndex = FindMemoryTypeIndex(req.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);

if (alloc.memoryTypeIndex == UINT32_MAX ||
vkAllocateMemory(g_vk.device, &alloc, nullptr, &g_vk.meterReadbackMemory[i]) != VK_SUCCESS ||
Expand Down Expand Up @@ -511,8 +511,7 @@ void EvaluateAfterUpscaleVk(VkCommandBuffer cmdBuffer, NVSDK_NGX_Parameter* para
// number, not a lost device, and the gate on the readback throws a wrong number away.
NVSDK_NGX_Resource_VK* exposure = nullptr;
float preExposure = 1.0f;
const bool havePre =
params->Get(NVSDK_NGX_Parameter_DLSS_Pre_Exposure, &preExposure) == NVSDK_NGX_Result_Success;
const bool havePre = params->Get(NVSDK_NGX_Parameter_DLSS_Pre_Exposure, &preExposure) == NVSDK_NGX_Result_Success;

params->Get(NVSDK_NGX_Parameter_ExposureTexture, (void**) &exposure);

Expand Down Expand Up @@ -558,8 +557,8 @@ void EvaluateAfterUpscaleVk(VkCommandBuffer cmdBuffer, NVSDK_NGX_Parameter* para
std::abs(loggedExposure - g_vk.gameExposure) > std::max(0.02f * g_vk.gameExposure, 1e-5f))
{
loggedExposure = g_vk.gameExposure;
LOG_INFO("DLSS-NR Vulkan: the game's exposure is {}, pre-exposure {}, so white point {}",
g_vk.gameExposure, g_vk.gamePreExposure, g_vk.gamePreExposure / g_vk.gameExposure);
LOG_INFO("DLSS-NR Vulkan: the game's exposure is {}, pre-exposure {}, so white point {}", g_vk.gameExposure,
g_vk.gamePreExposure, g_vk.gamePreExposure / g_vk.gameExposure);
}

if (colour == nullptr || depth == nullptr || motion == nullptr)
Expand Down Expand Up @@ -631,9 +630,8 @@ void EvaluateAfterUpscaleVk(VkCommandBuffer cmdBuffer, NVSDK_NGX_Parameter* para
return;
}

const int result =
g_vk.init(snippet->wstring().c_str(), State::Instance().NVNGX_ApplicationDataPath.c_str(),
(void*) instance, (void*) physicalDevice, (void*) device, 0x0000015);
const int result = g_vk.init(snippet->wstring().c_str(), State::Instance().NVNGX_ApplicationDataPath.c_str(),
(void*) instance, (void*) physicalDevice, (void*) device, 0x0000015);

if (result != 1)
{
Expand Down Expand Up @@ -695,8 +693,7 @@ void EvaluateAfterUpscaleVk(VkCommandBuffer cmdBuffer, NVSDK_NGX_Parameter* para

// Resize. The feature is built for a size and has to be rebuilt when the frame OR the working
// size changes -- moving the slider is a rebuild, which is why it is compared here.
if (g_vk.width != width || g_vk.height != height || g_vk.workWidth != workWidth ||
g_vk.workHeight != workHeight)
if (g_vk.width != width || g_vk.height != height || g_vk.workWidth != workWidth || g_vk.workHeight != workHeight)
{
// This block releases the feature and frees the surfaces below IMMEDIATELY. A frame-size
// change is already fenced by the game -- it recreates the swapchain around it -- but moving
Expand All @@ -718,9 +715,9 @@ void EvaluateAfterUpscaleVk(VkCommandBuffer cmdBuffer, NVSDK_NGX_Parameter* para

// The meter is a fixed 8x8 whatever the frame is, so it is only built the once -- but it is
// built alongside the rest so that a failure here is caught by the same check.
const bool meterReady = (g_vk.meter.Valid() || CreateImage(g_vk.meter, kMeterSide, kMeterSide,
VK_FORMAT_R32_SFLOAT, true)) &&
CreateMeterReadback();
const bool meterReady =
(g_vk.meter.Valid() || CreateImage(g_vk.meter, kMeterSide, kMeterSide, VK_FORMAT_R32_SFLOAT, true)) &&
CreateMeterReadback();

if (!meterReady)
LOG_WARN("DLSS-NR Vulkan: no exposure meter; the white point stays on the slider");
Expand Down Expand Up @@ -779,8 +776,7 @@ void EvaluateAfterUpscaleVk(VkCommandBuffer cmdBuffer, NVSDK_NGX_Parameter* para
{
unsigned int gameReset = 0;

if (params->Get(NVSDK_NGX_Parameter_Reset, &gameReset) == NVSDK_NGX_Result_Success &&
gameReset != 0)
if (params->Get(NVSDK_NGX_Parameter_Reset, &gameReset) == NVSDK_NGX_Result_Success && gameReset != 0)
{
g_vk.reset = true;

Expand All @@ -796,14 +792,8 @@ void EvaluateAfterUpscaleVk(VkCommandBuffer cmdBuffer, NVSDK_NGX_Parameter* para
// and encoding an already tone-mapped frame a second time looks washed out and banded.
const bool linearHdr = gameSaysHdr && FormatCanHoldLinearHdr(colour->Resource.ImageViewInfo.Format);

// The same rule as the D3D12 path, deliberately spelled the same way: the game divides its frame
// by preExposure and multiplies by exposure, so undoing that is the divisor this pass wants, and
// the slider becomes a trim on top rather than the answer.
//
// The trim is bounded here, at the point of use, rather than at the slider. Someone who found 64
// by hand on the manual path and then switches the exposure source on keeps that 64 in their ini;
// bounding it in the menu would leave the picture wrong for a reason the menu no longer showed.
// Their value stays in the config untouched, so switching back to manual restores it.
// Native Vulkan source 1 stays on the delayed courier. Do not call DecideWhitePoint; do not
// set UseGameExposure. P / E * trim, origin clamp, no S.
float whitePoint = cfg.DlssNrWhitePointScale.value_or_default();

if (cfg.DlssNrWhitePointSource.value_or_default() == 1 && g_vk.gameExposure > 1e-6f)
Expand All @@ -827,6 +817,7 @@ void EvaluateAfterUpscaleVk(VkCommandBuffer cmdBuffer, NVSDK_NGX_Parameter* para
encode.Width = width;
encode.Height = height;
encode.WhitePoint = whitePoint;
encode.UseGameExposure = 0;
encode.Passthrough = linearHdr ? 0u : 1u;
encode.TransferStrength = cfg.DlssNrTransferStrength.value_or_default();
encode.ColourStrength = cfg.DlssNrColourStrength.value_or_default();
Expand Down Expand Up @@ -860,9 +851,8 @@ void EvaluateAfterUpscaleVk(VkCommandBuffer cmdBuffer, NVSDK_NGX_Parameter* para
// storage image, which is only legal in GENERAL, and nothing transitions it in between. It is the
// upscaler's output, a storage image the upscaler has just written, so GENERAL is what it is.
// Inert on the only hardware this model runs on, wrong everywhere it is read.
if (!g_vk.pass->Dispatch(cmdBuffer, encode, width, height, colour->Resource.ImageViewInfo.ImageView,
VK_NULL_HANDLE, VK_NULL_HANDLE, VK_NULL_HANDLE, g_vk.proxy.view, g_vk.keep.view,
VK_IMAGE_LAYOUT_GENERAL))
if (!g_vk.pass->Dispatch(cmdBuffer, encode, width, height, colour->Resource.ImageViewInfo.ImageView, VK_NULL_HANDLE,
VK_NULL_HANDLE, VK_NULL_HANDLE, g_vk.proxy.view, g_vk.keep.view, VK_IMAGE_LAYOUT_GENERAL))
{
Fail("the encode dispatch failed");
return;
Expand Down Expand Up @@ -898,8 +888,8 @@ void EvaluateAfterUpscaleVk(VkCommandBuffer cmdBuffer, NVSDK_NGX_Parameter* para
// The meter: the game's 1x1 exposure -> texel (0,0) of the grid -> a buffer the CPU can read
// -----------------------------------------------------------------------------------------

// The motion slot carries it, because the meter has no use for motion vectors and the shader is
// one shader with a fixed set of bindings. The source slot is left empty and gets the dummy.
// Mode 3 binds the game's exposure on binding 8. Motion (binding 4) and the other unused reads
// get the dummy. The shader is one program with a fixed set of bindings.
//
// Gated on the setting that consumes the answer, which is not merely tidy. This is the only place
// the pass binds a resource it does not own on a guess about its layout, and the guess is good
Expand All @@ -922,8 +912,9 @@ void EvaluateAfterUpscaleVk(VkCommandBuffer cmdBuffer, NVSDK_NGX_Parameter* para
Transition(cmdBuffer, g_vk.meter, VK_IMAGE_LAYOUT_GENERAL);

if (g_vk.pass->Dispatch(cmdBuffer, meter, kMeterSide, kMeterSide, VK_NULL_HANDLE, VK_NULL_HANDLE,
VK_NULL_HANDLE, exposure->Resource.ImageViewInfo.ImageView, g_vk.meter.view,
VK_NULL_HANDLE, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
VK_NULL_HANDLE, VK_NULL_HANDLE, g_vk.meter.view, VK_NULL_HANDLE,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
exposure->Resource.ImageViewInfo.ImageView,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL))
{
Transition(cmdBuffer, g_vk.meter, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
Expand Down Expand Up @@ -993,9 +984,10 @@ void EvaluateAfterUpscaleVk(VkCommandBuffer cmdBuffer, NVSDK_NGX_Parameter* para
Transition(cmdBuffer, g_vk.keep, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);

Transition(cmdBuffer, *modelInput, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
Transition(cmdBuffer, g_vk.proxy, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);

if (!g_vk.pass->Dispatch(cmdBuffer, resolve, width, height, modelInput->view, g_vk.output.view, g_vk.keep.view,
VK_NULL_HANDLE, colour->Resource.ImageViewInfo.ImageView, VK_NULL_HANDLE,
g_vk.proxy.view, colour->Resource.ImageViewInfo.ImageView, VK_NULL_HANDLE,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL))
{
Fail("the resolve dispatch failed");
Expand Down
5 changes: 2 additions & 3 deletions OptiScaler/dlssnr/DlssNrFeature_Vk.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,8 @@ unsigned long long FramesVk();
// A timestamp pair either side of the whole pass, read three frames later so the query is retired.
std::optional<double> LastGpuTimeVk();

// Whether the game offers an exposure texture on this path. Observed only: it is not read, because
// binding the game's image means naming a layout this side cannot know. For the menu, and to settle
// whether reading it is worth the risk on any real Vulkan game.
// Whether the game offers an exposure texture on this path. The picture never sets UseGameExposure.
// Mode 3 may bind that texture on binding 8 when WhitePointSource is 1, as a courier only.
bool ExposureOfferedVk();

void ShutdownVk(bool deviceAlive = true);
Expand Down
66 changes: 66 additions & 0 deletions OptiScaler/dlssnr/DlssNr_Exposure.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#pragma once
#include <algorithm>
#include <cmath>
#include <cstdint>

inline float FoldZeroToOne(float v) { return (v > 1e-6f) ? v : 1.0f; }

inline float SanitizeExposure(float e)
{
if (!(e > 1e-6f && e < 1e6f))
return 1.0f;
return e;
}

inline float GameWhite(float e, float p, float s)
{
e = SanitizeExposure(e);
p = (std::max) (FoldZeroToOne(p), 1e-4f);
s = (std::max) (FoldZeroToOne(s), 1e-4f);
const float gameW = p / (std::max) (e * s, 1e-4f);
// T0 invert: return e * s / p;
return (std::max) (gameW, 1e-4f);
}

struct WhitePointDecision
{
float whitePoint; // written to encode/resolve gWhitePoint
uint32_t useGameExposure; // 1 only for D3D12 Approach B this frame
};

inline WhitePointDecision DecideWhitePoint(uint32_t source, // WhitePointSource 0/1/2
bool isHdrBuffer, //
bool usable, // ExposureUsable(gameRes)
bool heldE, // finite gameExposure > 1e-6
float scale, // WhitePointScale
float trim, // clamp(WhitePointTrim, 0.25, 4)
float heldExposure, // last courier E
float pre, // frame.PreExposure
float exposureScale, // frame.ExposureScale
float anchoredOrZero) // ExposureScan::AnchoredWhitePoint(...) or 0
{
WhitePointDecision d { scale, 0 };
if (!isHdrBuffer)
return d;
if (source != 1 && source != 2)
return d;
if (source == 2)
{
if (anchoredOrZero > 0.0f)
d.whitePoint = anchoredOrZero;
return d;
}
// source == 1
if (usable)
{
d.useGameExposure = 1;
d.whitePoint = trim; // PaperWhite() multiplies by GameW
return d;
}
if (heldE)
{
d.whitePoint = trim * GameWhite(heldExposure, pre, exposureScale);
return d;
}
return d; // hole without held E: Scale
}
Loading