engine/game_rr: per-input ring-buffer detection to prevent descriptor set cache overflow

The Ray Reconstruction engine caches one immutable descriptor set per unique combination of input views (hard limit: 16). Games that rotate their intermediate textures through a ring buffer generate a new combination each frame; with K independent ring buffers of size N the number of distinct combinations grows as N^K, quickly exceeding the cache limit.

Fix: track the number of distinct VkImageView handles seen per input (11 inputs: colour, motion, depth, 8 guides). When an input exceeds 4 distinct views it is flagged as rotating and its data is copied to a canonical plane (a stable internal texture) instead of being borrowed directly. The `direct` lambda now takes an index and checks the per-input flag, so only the inputs that actually rotate incur a blit.

Stable inputs (single view, no ring buffer) continue to be borrowed directly with zero extra cost. The worst-case overhead is one blit per rotating input per frame (~0.05 ms each).

The flag is one-way: once an input is marked as rotating it stays that way for the lifetime of the engine. If a game changes its rendering mode mid-session the flag persists, adding a negligible number of extra blits.
This commit is contained in:
Tk-Glitch committed 2026-10-19 05:05:23 +02:00
1 parent 689ac6f270
commit 474ffad227
1 file changed
+30 -5
+30 -5
View File
@@ -1,5 +1,6 @@
#include "game_rr.h" #include "game_rr.h"
#include "game_internal.h" #include "game_internal.h"
#include <algorithm>
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
#include <fstream> #include <fstream>
@@ -15,6 +16,12 @@ struct GameDenoiser::Impl {
game_detail::CanonicalInputs fallback; // colour, motion, depth game_detail::CanonicalInputs fallback; // colour, motion, depth
std::array<game_detail::Plane, 8> guides; // the guides and optional inputs, when one cannot be sampled directly std::array<game_detail::Plane, 8> guides; // the guides and optional inputs, when one cannot be sampled directly
bool started = false; bool started = false;
// Per-input ring-buffer detection: tracks distinct views for each of the 11 inputs
// (color, motion, depth, 8 guides). When an input exceeds the threshold, it is
// copied to a canonical plane so the engine sees a stable set of views.
static constexpr int NUM_INPUTS = 11;
std::array<std::vector<VkImageView>, NUM_INPUTS> seenViews;
std::array<bool, NUM_INPUTS> needsCopy = {};
// debugging: D4R_ENGINE_RR_DUMP=DIR copies every engine image after each frame into a host-visible buffer and // debugging: D4R_ENGINE_RR_DUMP=DIR copies every engine image after each frame into a host-visible buffer and
// writes it at the next frame's recording (by then the game has waited for the frame), as DIR/F-NAME.bin // writes it at the next frame's recording (by then the game has waited for the frame), as DIR/F-NAME.bin
VkPhysicalDevice physical; VkPhysicalDevice physical;
@@ -95,10 +102,28 @@ void GameDenoiser::record(VkCommandBuffer cb, const GameFrameR& frame) {
throw std::runtime_error("unsupported game texture format"); throw std::runtime_error("unsupported game texture format");
// The stages sample every input at its texel coordinates from the origin, so only views at origin zero are // The stages sample every input at its texel coordinates from the origin, so only views at origin zero are
// borrowed; anything else is copied first. Every input must be in GENERAL when borrowed (one layout for all). // borrowed; anything else is copied first. Every input must be in GENERAL when borrowed (one layout for all).
auto direct = [](const GameImage& image) { // Per-input ring-buffer detection: track distinct views and flag inputs that rotate.
return image.view != VK_NULL_HANDLE && !image.x && !image.y && image.layout == VK_IMAGE_LAYOUT_GENERAL; // The engine caches descriptor sets per unique combination of input views (limit 16).
// If K inputs each rotate through N views, there are up to N^K combinations.
// Copying any rotating input to a canonical plane removes it from the combination.
const VkImageView inputViews[11] = {
frame.color.view, frame.motion.view, frame.depth.view,
frame.normals.view, frame.roughness.view, frame.diffuseAlbedo.view,
frame.specularAlbedo.view, frame.specularHitDistance.view,
frame.disocclusionMask.view, frame.subsurfaceGuide.view, frame.colorBeforeParticles.view
}; };
const bool directColor = direct(frame.color), directMotion = direct(frame.motion), directDepth = direct(frame.depth); for (int i = 0; i < 11; ++i) {
if (inputViews[i] == VK_NULL_HANDLE) continue;
auto& seen = g.seenViews[i];
if (std::find(seen.begin(), seen.end(), inputViews[i]) == seen.end()) {
seen.push_back(inputViews[i]);
if (seen.size() > 4) g.needsCopy[i] = true;
}
}
auto direct = [&](const GameImage& image, int idx) {
return !g.needsCopy[idx] && image.view != VK_NULL_HANDLE && !image.x && !image.y && image.layout == VK_IMAGE_LAYOUT_GENERAL;
};
const bool directColor = direct(frame.color, 0), directMotion = direct(frame.motion, 1), directDepth = direct(frame.depth, 2);
// the four guides, then the two optional inputs (kept in their own one-channel formats when copied) // the four guides, then the two optional inputs (kept in their own one-channel formats when copied)
const GameImage* guides[8] = {&frame.normals, &frame.roughness, &frame.diffuseAlbedo, &frame.specularAlbedo, const GameImage* guides[8] = {&frame.normals, &frame.roughness, &frame.diffuseAlbedo, &frame.specularAlbedo,
&frame.specularHitDistance, &frame.disocclusionMask, &frame.subsurfaceGuide, &frame.specularHitDistance, &frame.disocclusionMask, &frame.subsurfaceGuide,
@@ -114,7 +139,7 @@ void GameDenoiser::record(VkCommandBuffer cb, const GameFrameR& frame) {
} }
g.fallback.record(cb, frame.color, frame.motion, frame.depth, w, h, w, h, directColor, directMotion, directDepth); g.fallback.record(cb, frame.color, frame.motion, frame.depth, w, h, w, h, directColor, directMotion, directDepth);
for (int i = 0; i < 8; ++i) { for (int i = 0; i < 8; ++i) {
if (!present(i) || direct(*guides[i])) continue; if (!present(i) || direct(*guides[i], 3 + i)) continue;
if (!g.guides[i].image) { if (!g.guides[i].image) {
g.fallback.ensure(cb, g.guides[i], guideFormats[i], w, h); g.fallback.ensure(cb, g.guides[i], guideFormats[i], w, h);
} }
@@ -132,7 +157,7 @@ void GameDenoiser::record(VkCommandBuffer cb, const GameFrameR& frame) {
&images.specularHitDistance, &images.disocclusionMask, &images.subsurfaceGuide, &images.specularHitDistance, &images.disocclusionMask, &images.subsurfaceGuide,
&images.colorBeforeParticles}; &images.colorBeforeParticles};
for (int i = 0; i < 8; ++i) for (int i = 0; i < 8; ++i)
if (present(i)) *guideViews[i] = direct(*guides[i]) ? guides[i]->view : g.guides[i].view; if (present(i)) *guideViews[i] = direct(*guides[i], 3 + i) ? guides[i]->view : g.guides[i].view;
const bool directOutput = frame.output.view != VK_NULL_HANDLE && frame.output.format == VK_FORMAT_R16G16B16A16_SFLOAT && const bool directOutput = frame.output.view != VK_NULL_HANDLE && frame.output.format == VK_FORMAT_R16G16B16A16_SFLOAT &&
frame.output.layout == VK_IMAGE_LAYOUT_GENERAL && !frame.output.x && !frame.output.y && frame.output.layout == VK_IMAGE_LAYOUT_GENERAL && !frame.output.x && !frame.output.y &&
frame.output.image != frame.color.image && frame.output.image != frame.motion.image && frame.output.image != frame.depth.image; frame.output.image != frame.color.image && frame.output.image != frame.motion.image && frame.output.image != frame.depth.image;