Fix DLSS quality-change corruption and frame completion ordering
- Reprepare enc3 weights each launch to avoid stale caches after feature recreation - Require per-slot input markers before evaluating recorded frames - Advance split-frame timelines only through completed frames - Add cancellation and readiness tests plus recreation consistency checks - Update architecture and native kernel documentation ref #8
This commit is contained in:
1 parent
1620c36e15
commit
6684888c0b
9 files changed
+302
-33
No files matched your search
@@ -24,13 +24,13 @@ With VRAM interop and split frames (the defaults when the patched vkd3d-proton i
|
||||
- A frame marker.
|
||||
- A split. vkd3d-proton submits everything the game records after the DLSS call separately, gated on a timeline semaphore reaching this frame's number.
|
||||
- At the start of that second part, a copy of the DLSS result into the game's output texture. Linear R11G11B10, RGB10A2, and RGBA8/BGRA8 outputs are converted from RGBA16F on the GPU. sRGB formats continue through host staging to preserve their existing byte interpretation.
|
||||
2. **The CUDA worker queues DLSS.** The prep thread checks the GPU-written input marker with short CPU sleeps. It drops recorded-but-unsubmitted frames when they time out or their feature retires, before any DLSS work is queued. The CUDA worker then queues:
|
||||
2. **The CUDA worker queues DLSS.** The prep thread checks the GPU-written marker for that input slot with short CPU sleeps. A later frame's marker cannot make a discarded frame's stale inputs appear ready. It drops recorded-but-unsubmitted frames when the queue skips them, they time out or their feature retires, before any DLSS work is queued. The CUDA worker then queues:
|
||||
- the input copies into CUDA arrays, or none when NGX samples the buffers in place (`LinearInputs`);
|
||||
- NGX's evaluation (NGX's own CPU synchronisations are elided with `ElideNgxSync`);
|
||||
- the result, written straight into the output buffer by the native output kernel (`DirectOutput`, preset K) or copied there.
|
||||
3. **Release.** When the GPU finishes, the worker signals the timeline semaphore and the rest of the game's frame runs, now with this frame's result.
|
||||
|
||||
The watchdog reports delayed output but never signals an unfinished producer as ready. Retiring features remain registered until their CPU/CUDA jobs drain. The lifetime-aware vkd3d extension retains their staging buffers, imported buffers, conversion images and split semaphore through command-allocator reset or destruction after GPU completion. Discarded recordings are covered too; cleanup no longer relies on a 50 ms delay.
|
||||
The watchdog reports delayed output but never signals an unfinished producer as ready. Cancellation and completion can occur out of order across the prep and CUDA threads, so the output timeline advances only through a completed prefix of admitted frames. Cancelling a later frame cannot release a game copy while an earlier producer is still writing. Retiring features remain registered until their CPU/CUDA jobs drain. The lifetime-aware vkd3d extension retains their staging buffers, imported buffers, conversion images and split semaphore through command-allocator reset or destruction after GPU completion. Discarded recordings are covered too; cleanup no longer relies on a 50 ms delay.
|
||||
|
||||
The shim requires the matching lifetime-aware d3d12.dll and d3d12core.dll, even for host staging. It rejects older runtimes rather than recording references whose lifetime it cannot protect. FrameAge > 0 shows the newest finished result; without VRAM interop, inputs and output are staged through host memory.
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ With `D4R_ZLUDA_NATIVE_DIR=DIR`, whenever NGX loads a PTX module and asks for ke
|
||||
| `u32 d4r_prep_blocks` | grid size of the prep kernel (128 threads per block) |
|
||||
| `u32 d4r_prep_key_offset` | byte offset of a u64 in the parameter block; the prep is skipped while that value (the weights pointer) repeats (0 = no key) |
|
||||
| `u32 d4r_prep_key_at` | the same as offset + 1, so a key at offset 0 can be named (M's layers keep their weights pointer there) |
|
||||
| `u32 d4r_prep_key_slots` | N > 0: the kernel keeps one prepared image per key for up to N keys, so the prep runs once per weight set (the six enc3 tube blocks); keys beyond N share an overflow image and are prepared on every launch |
|
||||
| `u32 d4r_prep_key_slots` | N > 0: the kernel keeps one prepared image per key for up to N keys; keys beyond N share an overflow image. This requires stable weight-address identity. Enc3 disables key caching and prepares every launch instead. |
|
||||
| `u32 d4r_block_z` | replaces the launch's block z dimension (more waves per window) |
|
||||
| `u32 d4r_grid_x` | replaces the grid (persistent kernels) |
|
||||
|
||||
@@ -36,7 +36,7 @@ The release is built this way, with hashes from DLSS 310.7.0 and 310.9.1. The PT
|
||||
- **Weights:** prep kernels expand the weights into WMMA operand images once.
|
||||
|
||||
**DLSS 4.5, presets L/M (`kernels/m`, `rrlite_*`).**
|
||||
- **Network:** the Swin blocks of DLSS 4.5, whose weights are FP8. Their prep kernels expand the weights to f16 WMMA operands, once per weight set.
|
||||
- **Network:** the Swin blocks of DLSS 4.5, whose weights are FP8. Their prep kernels expand the weights to f16 WMMA operands. The enc3 tube layer prepares on every launch: NGX can recycle a weights address for a different tube block after feature recreation, so persistent address-only reuse can return stale weights and corrupt the image after quality changes. Other layers retain their existing preparation policy.
|
||||
- **Template:** `swin_block.h` covers encoders, the tube-shaped enc3 and decoders.
|
||||
- **Output encoding:** the FP8 output is encoded two values at a time with packed 16-bit operations (`enc8x2`, exhaustively equal to the scalar encoder), and the 2×2 patch merge reads the rounded f16 values the codes decode to, from a row layout without LDS bank conflicts, instead of decoding the bytes again in every wave.
|
||||
- **Weight loads:** enc1 forms each weight tile's address in scalar registers (`SWIN_SCALAR_BLOAD`); the 8-wave layers are faster without it.
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
#define SWIN_F32ACC
|
||||
#define SWIN_MAX_VGPR 192
|
||||
#define SWIN_MLP_R(NW) 2
|
||||
#define SWIN_PREP_SLOTS 8 // one weight image per tube block (six per frame)
|
||||
// NGX can recycle a weights address for another tube block after feature
|
||||
// recreation. Address-only caching then serves stale packed weights. Prepare
|
||||
// each launch; the default single image is sufficient on the launch stream.
|
||||
#define SWIN_PREP_KEY_AT 0
|
||||
#include "swin_block.h"
|
||||
SWIN_MODULE_W(rrlite_enc3_tube_4x4, 128, 4, 8, 0, true, TubeParams, 0)
|
||||
@@ -747,9 +747,13 @@ __device__ __forceinline__ void swin_block(const CommonParams& p, const TubePara
|
||||
#ifndef SWIN_PREP_SLOTS
|
||||
#define SWIN_PREP_SLOTS 0 // weight images kept per weights pointer (0: one image, re-prepared when the pointer changes)
|
||||
#endif
|
||||
#ifndef SWIN_PREP_KEY_AT
|
||||
#define SWIN_PREP_KEY_AT 1 // weights pointer at offset 0; 0 forces preparation on every launch
|
||||
#endif
|
||||
|
||||
// Weight-image slots for kernels launched with several weight sets (the six enc3 tube blocks). The ZLUDA
|
||||
// hook runs the prep only for a weights pointer it has not seen (d4r_prep_key_at / d4r_prep_key_slots);
|
||||
// Optional weight-image slots for several weight sets with stable address identity. The ZLUDA
|
||||
// hook can run prep only for a weights pointer it has not seen (d4r_prep_key_at / d4r_prep_key_slots);
|
||||
// enc3 disables this policy because NGX recycles weight addresses across feature lifetimes.
|
||||
// the prep claims the first free slot for that pointer, and pointers beyond S share the overflow slot S,
|
||||
// which the hook re-prepares on every launch. Keys are written by one thread of the prep; any prep
|
||||
// thread that reads before that write finds the same (first free) slot.
|
||||
@@ -790,8 +794,8 @@ template <int S> __device__ __forceinline__ int prep_slot_claim(uint64_t* keys,
|
||||
__constant__ SwinDescs<C, NH, NPM, CIN> g_descs = make_descs<C, NH, NPM, CIN>(); \
|
||||
extern "C" __device__ __attribute__((used)) uint32_t d4r_block_z = NWAVES; \
|
||||
extern "C" __device__ __attribute__((used)) uint32_t d4r_prep_blocks = (NAME##_L::TOTAL + 127) / 128; \
|
||||
/* the prep reads only p.w (offset 0): skip it while the image for that pointer is resident */ \
|
||||
extern "C" __device__ __attribute__((used)) uint32_t d4r_prep_key_at = 0 + 1; \
|
||||
/* enc3 disables address-only reuse: a recreated feature may recycle a weights address */ \
|
||||
extern "C" __device__ __attribute__((used)) uint32_t d4r_prep_key_at = SWIN_PREP_KEY_AT; \
|
||||
extern "C" __device__ __attribute__((used)) uint32_t d4r_prep_key_slots = NAME##_S; \
|
||||
extern "C" __global__ void __launch_bounds__(128) NAME##_prep(PARAMS p) \
|
||||
{ \
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
"""Timeline safety when prep cancellation overtakes a CUDA producer."""
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class FrameCompletionTests(unittest.TestCase):
|
||||
def test_shim_does_not_signal_past_running_producer(self):
|
||||
shim = (ROOT / 'tools/d4r_nvngx_shim.cpp').read_text()
|
||||
start = shim.index('static void release_split_frame(Feature* feature, uint32_t frame)\n{')
|
||||
end = shim.index('\nstatic void split_watchdog()', start)
|
||||
source = r'''
|
||||
#include "d4r_frame_completion.h"
|
||||
#include <cassert>
|
||||
#include <mutex>
|
||||
#include <cstdarg>
|
||||
using VkResult = int;
|
||||
constexpr int VK_SUCCESS = 0, VK_STRUCTURE_TYPE_SEMAPHORE_SIGNAL_INFO = 1;
|
||||
struct VkSemaphoreSignalInfo { int type; int semaphore = 0; uint64_t value = 0; };
|
||||
struct Feature {
|
||||
std::mutex splitMutex;
|
||||
D4rFrameCompletion splitCompletion;
|
||||
uint64_t splitSignalled = 0;
|
||||
int splitSemaphore = 1;
|
||||
};
|
||||
static uint64_t gpuTimeline = 0;
|
||||
static bool failSignal = false;
|
||||
static int signal(int, const VkSemaphoreSignalInfo* info) {
|
||||
if (failSignal) return -1;
|
||||
assert(info->value > gpuTimeline);
|
||||
gpuTimeline = info->value;
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
static struct { int device = 1; decltype(&signal) signalSemaphore = signal; } g_vk;
|
||||
static void logf(const char*, ...) {}
|
||||
''' + shim[start:end] + r'''
|
||||
int main() {
|
||||
Feature feature;
|
||||
feature.splitCompletion.admit(1); // still running on the CUDA worker
|
||||
feature.splitCompletion.admit(2); // cancelled by the prep worker
|
||||
release_split_frame(&feature, 2);
|
||||
assert(gpuTimeline == 0 && feature.splitSignalled == 0);
|
||||
release_split_frame(&feature, 1);
|
||||
assert(gpuTimeline == 2 && feature.splitSignalled == 2);
|
||||
feature.splitCompletion.admit(4); // 3 was rejected before admission
|
||||
failSignal = true;
|
||||
release_split_frame(&feature, 4);
|
||||
assert(gpuTimeline == 2 && feature.splitSignalled == 2);
|
||||
failSignal = false;
|
||||
feature.splitCompletion.admit(5);
|
||||
release_split_frame(&feature, 5);
|
||||
assert(gpuTimeline == 5 && feature.splitSignalled == 5);
|
||||
}
|
||||
'''
|
||||
self.run_cpp(source)
|
||||
|
||||
def test_out_of_order_retirement(self):
|
||||
source = r'''
|
||||
#include "d4r_frame_completion.h"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
|
||||
int main() {
|
||||
// Exhaust every completion/cancellation order of six admitted frames.
|
||||
// Gaps represent rejected evaluations, which never have a producer.
|
||||
const std::array<unsigned, 6> frames = {1, 2, 4, 7, 8, 12};
|
||||
std::array<unsigned, 6> order = {0, 1, 2, 3, 4, 5};
|
||||
do {
|
||||
D4rFrameCompletion completion;
|
||||
for (auto frame : frames) completion.admit(frame);
|
||||
std::array<bool, 6> done = {};
|
||||
for (auto index : order) {
|
||||
done[index] = true;
|
||||
unsigned expected = 0;
|
||||
for (unsigned i = 0; i < frames.size() && done[i]; ++i)
|
||||
expected = frames[i];
|
||||
assert(completion.retire(frames[index]) == expected);
|
||||
}
|
||||
assert(completion.retire(12) == 12);
|
||||
completion.admit(15);
|
||||
completion.admit(16);
|
||||
assert(completion.retire(16) == 12);
|
||||
assert(completion.retire(999) == 12);
|
||||
assert(completion.retire(15) == 16);
|
||||
} while (std::next_permutation(order.begin(), order.end()));
|
||||
// A retiring prep job must not release a still-running earlier writer.
|
||||
D4rFrameCompletion completion;
|
||||
completion.admit(1345);
|
||||
completion.admit(1346);
|
||||
assert(completion.retire(1346) == 0);
|
||||
assert(completion.retire(1345) == 1346);
|
||||
}
|
||||
'''
|
||||
self.run_cpp(source)
|
||||
|
||||
def run_cpp(self, source):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
runner = Path(temporary) / 'frame-completion'
|
||||
subprocess.run(['c++', '-std=c++17', '-Wall', '-Wextra', '-Werror',
|
||||
'-I', str(ROOT / 'tools'), '-x', 'c++', '-', '-o', str(runner)],
|
||||
input=source, text=True, capture_output=True, check=True)
|
||||
subprocess.run([str(runner)], check=True, timeout=5)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Exercise the shim's marker loop with discarded and delayed recordings."""
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class InputReadinessTests(unittest.TestCase):
|
||||
def test_exact_input_slot_before_evaluation(self):
|
||||
source = (ROOT / 'tools/d4r_nvngx_shim.cpp').read_text()
|
||||
start = source.index(' // A later input marker')
|
||||
end = source.index(' const auto ready = ProfileClock::now();', start)
|
||||
runner_source = r'''
|
||||
#include <atomic>
|
||||
#include <cassert>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <thread>
|
||||
using ProfileClock = std::chrono::steady_clock;
|
||||
struct Feature { uint32_t* markerValue; std::atomic<bool> retiring{false}; };
|
||||
struct InputSlot { bool busy = true; };
|
||||
static unsigned retired = 0, evaluated = 0, sleeps = 0;
|
||||
static uint32_t* arrival = nullptr;
|
||||
static uint32_t arrivalFrame = 0;
|
||||
static void frame_retired(Feature*, uint32_t) { ++retired; }
|
||||
static void logf(const char*, ...) {}
|
||||
static unsigned env_uint(const char*, unsigned fallback) { return fallback; }
|
||||
static void d4r_sleep_us(unsigned) {
|
||||
assert(++sleeps <= 2);
|
||||
assert(arrival != nullptr);
|
||||
*arrival = arrivalFrame;
|
||||
}
|
||||
static void prepare(Feature* feature, int slotIndex, uint32_t frame, InputSlot& slot) {
|
||||
const auto start = ProfileClock::now();
|
||||
''' + source[start:end] + r'''
|
||||
++evaluated;
|
||||
}
|
||||
int main() {
|
||||
// A later submission advances the aggregate marker, but not frame 1's
|
||||
// slot. Old aggregate-only logic would evaluate unsubmitted inputs here.
|
||||
uint32_t markers[] = {2, 0, 0, 2};
|
||||
Feature feature{markers};
|
||||
InputSlot skipped;
|
||||
prepare(&feature, 1, 1, skipped);
|
||||
assert(retired == 1 && evaluated == 0 && sleeps == 0 && !skipped.busy);
|
||||
InputSlot ready;
|
||||
prepare(&feature, 2, 2, ready);
|
||||
assert(retired == 1 && evaluated == 1 && sleeps == 0);
|
||||
// Delayed input stays pending until that slot's exact tag arrives.
|
||||
markers[0] = 3;
|
||||
arrival = &markers[1];
|
||||
arrivalFrame = 3;
|
||||
InputSlot delayed;
|
||||
prepare(&feature, 0, 3, delayed);
|
||||
assert(evaluated == 2 && sleeps == 1);
|
||||
feature.retiring = true;
|
||||
InputSlot cancelled;
|
||||
prepare(&feature, 1, 4, cancelled);
|
||||
assert(retired == 2 && evaluated == 2 && !cancelled.busy);
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
runner = Path(temporary) / 'input-readiness'
|
||||
subprocess.run(['c++', '-std=c++17', '-Wall', '-Wextra', '-Werror',
|
||||
'-x', 'c++', '-', '-o', str(runner)], input=runner_source,
|
||||
text=True, capture_output=True, check=True)
|
||||
subprocess.run([str(runner)], check=True, timeout=5)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -754,6 +754,18 @@ int main(int argc, char** argv)
|
||||
const float scaleX = static_cast<float>(outWidth) / inWidth, scaleY = static_cast<float>(outHeight) / inHeight;
|
||||
const uint32_t phases = static_cast<uint32_t>(std::lround(8.0f * scaleX * scaleY));
|
||||
|
||||
// A discarded recording followed by a submitted frame must not make the
|
||||
// discarded inputs appear ready, or contaminate the new feature's history.
|
||||
if (std::getenv("D4R_HARNESS_SKIP_RECORDED_INPUT") != nullptr)
|
||||
{
|
||||
if (evaluate(g_list, feature, parameters, nullptr) != NGX_SUCCESS ||
|
||||
!check(g_list->Close(), "skip input list close") ||
|
||||
!check(g_allocator->Reset(), "skip input allocator reset") ||
|
||||
!check(g_list->Reset(g_allocator, nullptr), "skip input list reset"))
|
||||
return 1;
|
||||
lifecycle_report("SKIP input: discarded recording before submitting the next frame");
|
||||
}
|
||||
|
||||
for (int frame = 1; frame <= frames; ++frame)
|
||||
{
|
||||
if (qualityScene)
|
||||
@@ -998,9 +1010,11 @@ int main(int argc, char** argv)
|
||||
// D4R_HARNESS_RECREATE=N: N more release/create cycles at alternating render sizes (as when a game's DLSS
|
||||
// quality setting changes), a few evaluations each, logging this process's VRAM to find leaks per cycle.
|
||||
const int recreateCycles = std::getenv("D4R_HARNESS_RECREATE") != nullptr ? std::atoi(std::getenv("D4R_HARNESS_RECREATE")) : 0;
|
||||
const bool verifyRecreation = std::getenv("D4R_HARNESS_VERIFY_RECREATION") != nullptr;
|
||||
std::vector<uint8_t> recreationReference[2];
|
||||
for (int cycle = 1; cycle <= recreateCycles; ++cycle)
|
||||
{
|
||||
release(feature);
|
||||
if (release(feature) != NGX_SUCCESS) return 1;
|
||||
feature = nullptr;
|
||||
const UINT width = cycle % 2 != 0 ? inWidth * 10 / 13 : inWidth;
|
||||
const UINT height = cycle % 2 != 0 ? inHeight * 10 / 13 : inHeight;
|
||||
@@ -1017,12 +1031,23 @@ int main(int argc, char** argv)
|
||||
for (int frame = 0; frame < 4; ++frame)
|
||||
{
|
||||
d4r_ngx_set_int(parameters, "Reset", frame == 0 ? 1 : 0);
|
||||
evaluate(g_list, feature, parameters, nullptr);
|
||||
if (evaluate(g_list, feature, parameters, nullptr) != NGX_SUCCESS) return 1;
|
||||
submit_and_wait();
|
||||
Sleep(frameWaitMs);
|
||||
}
|
||||
const std::vector<uint8_t> cycleOutput = read_back(outputTexture, D3D12_RESOURCE_STATE_UNORDERED_ACCESS,
|
||||
outWidth * (rgba8 ? 4 : 8));
|
||||
if (verifyRecreation)
|
||||
{
|
||||
auto& reference = recreationReference[cycle % 2];
|
||||
if (reference.empty())
|
||||
reference = cycleOutput;
|
||||
else if (reference != cycleOutput)
|
||||
{
|
||||
lifecycle_report("RECREATE coherence FAILED: identical inputs and render size changed output");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
if (FILE* cycleFile = std::fopen((std::string(argv[2]) + ".cycle" + std::to_string(cycle)).c_str(), "wb"))
|
||||
{
|
||||
std::fwrite(cycleOutput.data(), 1, cycleOutput.size(), cycleFile);
|
||||
@@ -1038,6 +1063,8 @@ int main(int argc, char** argv)
|
||||
if (report != nullptr)
|
||||
std::fclose(report);
|
||||
}
|
||||
if (verifyRecreation && recreateCycles >= 4)
|
||||
lifecycle_report("RECREATE coherence: repeated render sizes match byte-for-byte");
|
||||
|
||||
release(feature);
|
||||
shutdown();
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
|
||||
// A timeline signal releases every lower value too. Prep can cancel a later
|
||||
// frame while the CUDA worker is still producing an earlier one, so only the
|
||||
// completed prefix of admitted frames may be signalled. The caller serializes
|
||||
// admission and retirement with the same mutex used for the timeline signal.
|
||||
class D4rFrameCompletion
|
||||
{
|
||||
public:
|
||||
void admit(uint32_t frame) { pending_.push_back({frame, false}); }
|
||||
|
||||
uint64_t retire(uint32_t frame)
|
||||
{
|
||||
for (auto& pending : pending_)
|
||||
if (pending.frame == frame)
|
||||
{
|
||||
pending.done = true;
|
||||
break;
|
||||
}
|
||||
while (!pending_.empty() && pending_.front().done)
|
||||
{
|
||||
completedThrough_ = pending_.front().frame;
|
||||
pending_.pop_front();
|
||||
}
|
||||
return completedThrough_;
|
||||
}
|
||||
|
||||
private:
|
||||
struct Pending { uint32_t frame; bool done; };
|
||||
std::deque<Pending> pending_;
|
||||
uint64_t completedThrough_ = 0;
|
||||
};
|
||||
+40
-22
@@ -59,6 +59,7 @@
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include "d4r_event_wait.h"
|
||||
#include "d4r_frame_completion.h"
|
||||
#include "d4r_win32_wait.h"
|
||||
#include "d4r_vkd3d_interop.h"
|
||||
|
||||
@@ -1635,11 +1636,12 @@ struct Feature
|
||||
VramImage exposureConversion;
|
||||
VramImage outputConversion;
|
||||
// Split frames: the rest of frame N's command list is submitted only once
|
||||
// splitSemaphore reaches N (signalled when N retires, or by the watchdog).
|
||||
// splitSemaphore reaches N (all admitted frames through N have retired).
|
||||
std::atomic<bool> split{false};
|
||||
VkSemaphore splitSemaphore = VK_NULL_HANDLE;
|
||||
std::mutex splitMutex;
|
||||
uint64_t splitSignalled = 0; // under splitMutex
|
||||
D4rFrameCompletion splitCompletion; // under splitMutex; cancellation can finish out of order
|
||||
// Inputs become ready on the CPU before CUDA work is queued. GPU-side waits
|
||||
// for recorded-but-discarded command lists can otherwise wedge the device.
|
||||
// D4R_SHIM_OUTPUT_DIRECT: this frame's result is stored by the native output kernel straight into its
|
||||
@@ -1653,7 +1655,7 @@ struct Feature
|
||||
std::vector<VramBuffer> retiredBuffers; // may still be read by queued command lists
|
||||
std::vector<VramImage> retiredImages;
|
||||
ID3D12Resource* marker = nullptr;
|
||||
volatile uint32_t* markerValue = nullptr;
|
||||
volatile uint32_t* markerValue = nullptr; // [0] latest input; [1 + slot] exact input frame
|
||||
uint32_t frame = 0;
|
||||
uint32_t rejectedFormatCount = 0; // game thread; rate-limits format diagnostics
|
||||
std::atomic<int> latestOutput{-1};
|
||||
@@ -2606,13 +2608,14 @@ static bool record_vram_output(ID3D12GraphicsCommandList* list, const VramBuffer
|
||||
// the game's command list right after the input copies and the frame marker;
|
||||
// vkd3d-proton submits the second half separately, waiting (at queue submission
|
||||
// level, so no GPU ring is ever blocked) for splitSemaphore to reach N. The
|
||||
// worker writes N's result into output slot N % kOutputSlots and signals N when
|
||||
// the frame retires; a dropped frame signals too, and a watchdog signals any
|
||||
// frame the GPU has reached but nobody released within the timeout.
|
||||
// worker writes N's result into output slot N % kOutputSlots. Completed and
|
||||
// dropped frames advance the signal only past earlier retired frames. The
|
||||
// watchdog reports delays without releasing an unfinished producer.
|
||||
|
||||
static void signal_split(Feature* feature, uint64_t value, const char* reason)
|
||||
static void release_split_frame(Feature* feature, uint32_t frame)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(feature->splitMutex);
|
||||
const uint64_t value = feature->splitCompletion.retire(frame);
|
||||
if (value <= feature->splitSignalled)
|
||||
return;
|
||||
VkSemaphoreSignalInfo info = {VK_STRUCTURE_TYPE_SEMAPHORE_SIGNAL_INFO};
|
||||
@@ -2621,14 +2624,8 @@ static void signal_split(Feature* feature, uint64_t value, const char* reason)
|
||||
const VkResult result = g_vk.signalSemaphore(g_vk.device, &info);
|
||||
if (result == VK_SUCCESS)
|
||||
feature->splitSignalled = value;
|
||||
if (result != VK_SUCCESS || reason != nullptr)
|
||||
logf("split frame %llu released%s%s (VkResult %d)", static_cast<unsigned long long>(value),
|
||||
reason != nullptr ? " by " : "", reason != nullptr ? reason : "", result);
|
||||
}
|
||||
|
||||
static void release_split_frame(Feature* feature, uint32_t frame)
|
||||
{
|
||||
signal_split(feature, frame, nullptr);
|
||||
if (result != VK_SUCCESS)
|
||||
logf("split frame %llu signal failed (VkResult %d)", static_cast<unsigned long long>(value), result);
|
||||
}
|
||||
|
||||
static void split_watchdog()
|
||||
@@ -4003,14 +4000,23 @@ static void prepare_inputs(Feature* feature, int slotIndex, uint32_t frame, Fram
|
||||
{
|
||||
InputSlot& slot = feature->inputs[slotIndex];
|
||||
const auto start = ProfileClock::now();
|
||||
// Wait for actual input readiness without queueing an unbounded GPU-side wait.
|
||||
while (static_cast<int32_t>(*feature->markerValue - frame) < 0)
|
||||
// A later input marker does not prove this slot's copies were submitted:
|
||||
// the game may have discarded this frame's command list. Require its own
|
||||
// tag, otherwise stale input data can enter DLSS's temporal history.
|
||||
const volatile uint32_t* inputMarker = feature->markerValue + 1 + slotIndex;
|
||||
while (*inputMarker != frame)
|
||||
{
|
||||
std::atomic_thread_fence(std::memory_order_acquire);
|
||||
if (feature->retiring.load() || std::chrono::steady_clock::now() - start > std::chrono::seconds(5))
|
||||
const bool skipped = static_cast<int32_t>(*feature->markerValue - frame) > 0;
|
||||
// The GPU may have filled our slot between the loop condition and
|
||||
// the aggregate-marker read. Recheck before deciding it was skipped.
|
||||
if (*inputMarker == frame)
|
||||
break;
|
||||
if (feature->retiring.load() || skipped || std::chrono::steady_clock::now() - start > std::chrono::seconds(5))
|
||||
{
|
||||
logf("frame %u: %s waiting for GPU marker (at %u); dropping", frame,
|
||||
feature->retiring.load() ? "feature retired while" : "timed out", *feature->markerValue);
|
||||
feature->retiring.load() ? "feature retired while" : skipped ? "input submission skipped while" : "timed out",
|
||||
*inputMarker);
|
||||
slot.busy = false;
|
||||
frame_retired(feature, frame);
|
||||
return;
|
||||
@@ -4522,7 +4528,8 @@ D4R_EXPORT NgxResult NVSDK_NGX_D3D12_CreateFeature(ID3D12GraphicsCommandList*, u
|
||||
delete feature;
|
||||
return NGX_FAIL_PLATFORM_ERROR;
|
||||
}
|
||||
*feature->markerValue = 0;
|
||||
for (int index = 0; index <= kSlots; ++index)
|
||||
feature->markerValue[index] = 0;
|
||||
feature->resources = new DeferredResources(feature);
|
||||
feature->handle.Id = g.nextHandleId++;
|
||||
{
|
||||
@@ -4809,9 +4816,15 @@ static NgxResult evaluate(ID3D12GraphicsCommandList* list, const NgxHandle* hand
|
||||
logf("ID3D12GraphicsCommandList2 unavailable; cannot place frame marker");
|
||||
return NGX_FAIL_PLATFORM_ERROR;
|
||||
}
|
||||
D3D12_WRITEBUFFERIMMEDIATE_PARAMETER marker = {feature->marker->GetGPUVirtualAddress(), frame};
|
||||
D3D12_WRITEBUFFERIMMEDIATE_MODE mode = D3D12_WRITEBUFFERIMMEDIATE_MODE_MARKER_OUT;
|
||||
list2->WriteBufferImmediate(1, &marker, &mode);
|
||||
const D3D12_GPU_VIRTUAL_ADDRESS markerAddress = feature->marker->GetGPUVirtualAddress();
|
||||
D3D12_WRITEBUFFERIMMEDIATE_PARAMETER markers[] = {
|
||||
{markerAddress + sizeof(uint32_t) * (1 + slotIndex), frame},
|
||||
{markerAddress, frame},
|
||||
};
|
||||
D3D12_WRITEBUFFERIMMEDIATE_MODE modes[] = {
|
||||
D3D12_WRITEBUFFERIMMEDIATE_MODE_MARKER_OUT, D3D12_WRITEBUFFERIMMEDIATE_MODE_MARKER_OUT,
|
||||
};
|
||||
list2->WriteBufferImmediate(2, markers, modes);
|
||||
list2->Release();
|
||||
|
||||
// Present the most recent finished DLSS result.
|
||||
@@ -4873,6 +4886,11 @@ static NgxResult evaluate(ID3D12GraphicsCommandList* list, const NgxHandle* hand
|
||||
|
||||
slot.busy = true;
|
||||
feature->inFlight.fetch_add(1);
|
||||
if (feature->split)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(feature->splitMutex);
|
||||
feature->splitCompletion.admit(frame);
|
||||
}
|
||||
g.prep.post([feature, slotIndex, frame, p, timing] { prepare_inputs(feature, slotIndex, frame, p, timing); });
|
||||
if (frame <= 3 || frame % 600 == 0 || GetEnvironmentVariableA("D4R_SHIM_TRACE_FRAMES", nullptr, 0) != 0)
|
||||
logf("evaluate frame %u queued (slot %d, showing result %d, jitter %.6f,%.6f, reset %d, "
|
||||
|
||||
Reference in new issue
Block a user