Fix VRAM leak on quality changes and RGBA16 motion vectors; more games

- Shim: reuse VRAM interop buffers across features instead of freeing them;
  ROCm 7.2.x's hipExternalMemoryGetMappedBuffer retains its view twice, so
  freed buffers never released their memory (~130 MB per quality change)
- Bridge: close the exported fd after hipImportExternalMemory
- Shim: accept R16G16B16A16_TYPELESS motion vectors and convert 4-channel
  motion vectors to RG16F on the GPU, keeping VRAM interop (Dying Light:
  The Beast rendered black)
- Harness: D4R_HARNESS_RECREATE=N quality-change cycles,
  D4R_HARNESS_MV_RGBA16=1 motion vector format
- Supported games: TLOU, Control, Cyberpunk 2077, Rift Apart, HZD Remastered,
  Subnautica 2, Alan Wake 2, Silent Hill 2, Dying Light: The Beast; general notes
- README: GPU support section (native kernels only for gfx1101); ignore videos/
This commit is contained in:
ayo committed 2026-09-28 01:26:51 -04:00
1 parent 1952452830
commit da263ecf75
6 files changed
+249 -28

No files matched your search

+92 -10
View File
@@ -836,7 +836,8 @@ static bool supported_input(Plane plane, DXGI_FORMAT format)
case Plane::Motion:
return format == DXGI_FORMAT_R16G16_FLOAT || format == DXGI_FORMAT_R16G16_TYPELESS ||
format == DXGI_FORMAT_R32G32_FLOAT || format == DXGI_FORMAT_R32G32_TYPELESS ||
format == DXGI_FORMAT_R16G16B16A16_FLOAT || format == DXGI_FORMAT_R32G32B32A32_FLOAT;
format == DXGI_FORMAT_R16G16B16A16_FLOAT || format == DXGI_FORMAT_R16G16B16A16_TYPELESS ||
format == DXGI_FORMAT_R32G32B32A32_FLOAT;
case Plane::Exposure:
return format == DXGI_FORMAT_R32_FLOAT || format == DXGI_FORMAT_R32_TYPELESS ||
format == DXGI_FORMAT_R16_FLOAT || format == DXGI_FORMAT_R16_TYPELESS ||
@@ -959,6 +960,7 @@ static void convert_row_in(Plane plane, DXGI_FORMAT format, const uint8_t* sourc
std::memcpy(half + x * 2, source + x * 4, 4);
break;
case DXGI_FORMAT_R16G16B16A16_FLOAT:
case DXGI_FORMAT_R16G16B16A16_TYPELESS:
std::memcpy(half + x * 2, source + x * 8, 4);
break;
case DXGI_FORMAT_R32G32B32A32_FLOAT:
@@ -1556,6 +1558,7 @@ struct Feature
// formats later stop qualifying.
bool vram = false, vramDecided = false;
VramImage colorConversion;
VramImage motionConversion;
VramImage exposureConversion;
VramImage outputConversion;
// Split frames: the rest of frame N's command list is submitted only once
@@ -2159,9 +2162,33 @@ static int device_local_type(uint32_t typeBits)
return -1;
}
// Buffers of released features, reused by later ones. ROCm up to at least 7.2.4 never frees memory mapped with
// hipExternalMemoryGetMappedBuffer (the mapping's buffer view is retained twice but released once by hipFree), so
// destroying a buffer would leak its memory: each DLSS quality change (a new feature) would add ~100 MB of VRAM.
static std::mutex g_vramPoolMutex;
static std::vector<VramBuffer> g_vramPool;
constexpr size_t kVramPoolLimit = 64;
// The smallest pooled buffer that holds `bytes`.
static bool take_pooled_vram_buffer(VramBuffer& target, size_t bytes)
{
std::lock_guard<std::mutex> lock(g_vramPoolMutex);
auto best = g_vramPool.end();
for (auto it = g_vramPool.begin(); it != g_vramPool.end(); ++it)
if (it->bytes >= bytes && (best == g_vramPool.end() || it->bytes < best->bytes))
best = it;
if (best == g_vramPool.end())
return false;
target = *best;
g_vramPool.erase(best);
return true;
}
// Game thread. The CUDA import runs on the worker, which owns the context.
static bool create_vram_buffer(VramBuffer& target, size_t bytes)
{
if (take_pooled_vram_buffer(target, bytes))
return true;
VramBuffer buffer;
VkExternalMemoryBufferCreateInfo external = {VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO};
external.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_WIN32_BIT;
@@ -2212,8 +2239,11 @@ static void destroy_vram_buffer(VramBuffer& buffer)
{
if (buffer.external != nullptr)
g.worker.call([&buffer] {
g.cu.memFree(buffer.device);
return g_vk.release(buffer.external);
const int freed = g.cu.memFree(buffer.device);
const int released = g_vk.release(buffer.external);
if (freed != 0 || released != 0)
logf("VRAM interop: freeing %zu-byte buffer: cuMemFree %d, release %d", buffer.bytes, freed, released);
return released;
});
if (buffer.memory != VK_NULL_HANDLE)
g_vk.free(g_vk.device, buffer.memory, nullptr);
@@ -2222,6 +2252,34 @@ static void destroy_vram_buffer(VramBuffer& buffer)
buffer = VramBuffer{};
}
// Only once no command list or CUDA work uses the buffer: keeps it for a later feature (see g_vramPool).
static void recycle_vram_buffer(VramBuffer& buffer)
{
if (buffer.buffer == VK_NULL_HANDLE)
return;
{
std::lock_guard<std::mutex> lock(g_vramPoolMutex);
if (g_vramPool.size() < kVramPoolLimit)
{
g_vramPool.push_back(buffer);
buffer = VramBuffer{};
return;
}
}
destroy_vram_buffer(buffer);
}
static void free_vram_pool()
{
std::vector<VramBuffer> pool;
{
std::lock_guard<std::mutex> lock(g_vramPoolMutex);
pool.swap(g_vramPool);
}
for (VramBuffer& buffer : pool)
destroy_vram_buffer(buffer);
}
static void destroy_vram_image(VramImage& image)
{
if (image.memory != VK_NULL_HANDLE)
@@ -2349,7 +2407,21 @@ static bool vram_exposure_blit_supported(VkFormat format)
}
}
// Whether `resource` can be copied raw (or, for colour/exposure, blitted)
// Motion vectors in a wider float format keep their first two components (as the host path does).
static bool vram_motion_blit_supported(VkFormat format)
{
switch (format)
{
case VK_FORMAT_R16G16B16A16_SFLOAT:
case VK_FORMAT_R32G32_SFLOAT:
case VK_FORMAT_R32G32B32A32_SFLOAT:
return vram_blit_supported(format, VK_FORMAT_R16G16_SFLOAT);
default:
return false;
}
}
// Whether `resource` can be copied raw (or, for colour/motion/exposure, blitted)
// to/from the canonical layout of `plane`.
static bool describe_vram_copy(ID3D12Resource* resource, Plane plane, VramCopy& copy, bool output = false)
{
@@ -2374,6 +2446,8 @@ static bool describe_vram_copy(ID3D12Resource* resource, Plane plane, VramCopy&
}
if (plane == Plane::Exposure)
copy.convert = vram_exposure_blit_supported(format);
else if (plane == Plane::Motion)
copy.convert = vram_motion_blit_supported(format);
return copy.convert || vk_texel_bytes(format) == canonical_texel_bytes(plane);
}
@@ -2415,9 +2489,10 @@ static bool record_vram_inputs(Feature& feature, ID3D12GraphicsCommandList* list
g_vk.copyImageToBuffer(cmd, copy.image, layout, slot.vram[index].buffer, 1, &region);
continue;
}
// Convert colour to RGBA16F or exposure's first component to R32F,
// then copy from the canonical image into the shared buffer.
// Convert colour to RGBA16F, motion to RG16F or exposure's first component
// to R32F, then copy from the canonical image into the shared buffer.
VkImage conversion = kPlanes[index] == Plane::Exposure ? feature.exposureConversion.image
: kPlanes[index] == Plane::Motion ? feature.motionConversion.image
: feature.colorConversion.image;
VkImageMemoryBarrier toDestination = {VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
toDestination.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
@@ -2633,14 +2708,15 @@ static void release_vram(Feature& feature)
g_vk.destroySemaphore(g_vk.device, feature.splitSemaphore, nullptr);
for (InputSlot& slot : feature.inputs)
for (VramBuffer& buffer : slot.vram)
destroy_vram_buffer(buffer);
recycle_vram_buffer(buffer);
for (OutputSlot& slot : feature.outputs)
destroy_vram_buffer(slot.vram);
destroy_vram_buffer(feature.gpuMarker);
recycle_vram_buffer(slot.vram);
recycle_vram_buffer(feature.gpuMarker);
for (VramBuffer& buffer : feature.retiredBuffers)
destroy_vram_buffer(buffer);
recycle_vram_buffer(buffer);
feature.retiredBuffers.clear();
destroy_vram_image(feature.colorConversion);
destroy_vram_image(feature.motionConversion);
destroy_vram_image(feature.exposureConversion);
destroy_vram_image(feature.outputConversion);
for (VramImage& image : feature.retiredImages)
@@ -4437,6 +4513,10 @@ static NgxResult evaluate(ID3D12GraphicsCommandList* list, const NgxHandle* hand
vramInputs[0].width, vramInputs[0].height,
VK_FORMAT_R16G16B16A16_SFLOAT))
return NGX_FAIL_PLATFORM_ERROR;
if (vramInputs[2].convert && !ensure_conversion_image(*feature, feature->motionConversion,
vramInputs[2].width, vramInputs[2].height,
VK_FORMAT_R16G16_SFLOAT))
return NGX_FAIL_PLATFORM_ERROR;
if (inputCount == 4 && vramInputs[3].convert &&
!ensure_conversion_image(*feature, feature->exposureConversion,
vramInputs[3].width, vramInputs[3].height, VK_FORMAT_R32_SFLOAT))
@@ -4688,6 +4768,8 @@ D4R_EXPORT NgxResult NVSDK_NGX_D3D12_Shutdown()
}
for (Feature* feature : features)
release_feature(feature);
if (g_vk.ready)
free_vram_pool();
std::lock_guard<std::mutex> lock(g.mutex);
if (g.ngxInitialized)
{