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
@@ -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