From 37d1055228886de5666a01480f420c15865da14d Mon Sep 17 00:00:00 2001 From: woutermans Date: Sun, 11 Oct 2026 20:45:26 +0200 Subject: [PATCH] Engine: support dynamic input resolution for presets K, L and M --- engine/README.md | 38 +++++++++++++++++++++++++--- engine/compile_m.py | 13 +++++----- engine/dec0_m.comp | 2 ++ engine/enc0_features.glsl | 8 +++++- engine/game_m.cpp | 6 ++++- engine/game_m.h | 6 +++-- engine/post_m.comp | 5 +++- engine/runtime.h | 6 ++++- engine/runtime_m.inc | 51 +++++++++++++++++++++++++------------- engine/test_game.cpp | 50 ++++++++++++++++++++++++++++++++++--- tools/d4r_nvngx_shim.cpp | 52 ++++++++++++++++++++++++++++++--------- 11 files changed, 190 insertions(+), 47 deletions(-) diff --git a/engine/README.md b/engine/README.md index 520a5ab..a3c1424 100644 --- a/engine/README.md +++ b/engine/README.md @@ -32,7 +32,7 @@ frame color/depth/motion ──► Engine input stage ──► 11 fused Swin la - Input images are borrowed Vulkan views. The caller provides the device, synchronization, and command buffer, so a future game adapter can schedule inference with its own rendering work. - Per-frame parameters include exposure, jitter, motion conventions, reset, and each layer's window shifts. - Window shifts can change without rebuilding shaders or buffers. Resolution changes require a new `Engine`. + Window shifts and input resolution can change without rebuilding shaders or buffers. Output resolution changes require a new `Engine`. - Frame parameters and window shifts are written by commands inside the frame's own command buffer, so a frame can be recorded while earlier ones are still queued on the GPU. Each input/output view and layout combination gets immutable descriptors, cached and reused; first use allocates descriptors, steady recording does not. Borrowed @@ -620,6 +620,38 @@ but reduced it by 0.83 dB on the independent capture. All-explicit gains improve consistently, so no accuracy variant became the default. `--unfold-gains` remains available; the retained enc0 change preserves numerical behavior on this device. +## Dynamic input resolution (K, L and M) + +The engine accepts changing render subrect dimensions with a fixed output size. K keeps its output-sized +network and temporal history, updates the frame's motion conversion and reconstruction table, and reserves +conversion inputs up to the output size. Use a K package with all sixteen reconstruction tables for changing +upscaling ratios; older single-table packages cannot match NGX at every ratio. + +L and M update the Vulkan network's active token grid, exposure grid and reconstruction tier per frame. +Their input and history planes reserve the constructor's render capacity; the shim reserves output-sized +capacity for the Vulkan path. Bilinear reads clamp to the active rectangle. A render-size change resets +L/M temporal history. Input dimensions must be at least 64 on each axis and cannot exceed the output. +Standalone callers set `GameFrameM::renderWidth/renderHeight` or `MFrameSettings::renderWidth/renderHeight`; +zero keeps the constructor size. + +L/M's HIP network retains its fixed constructor grid. When a feature renders at another size, the shim uses +one Vulkan engine for the changing sizes and retains the original HIP engine and shared buffers until queued +command lists retire. Returning to the original size resumes HIP with reset history. This uses additional +VRAM once the Vulkan engine is allocated. K's HIP network remains usable across all input-size changes. + +Rebuild L and M packages with the current `compile_m.py` (`D4RL0002` / `D4RM0004`). Earlier packages retain fixed-size support but lack +active-rectangle sampling, so a size change uses the CUDA fallback. The dynamic regression can be run independently: + +```sh +build/native-engine/test-game --dynamic-ml PATH_TO_M_OR_L_MODEL +``` + +On the RX 9070 XT, six-frame HDR sequences for both L and M pass shrinking/growing input sizes, +Quality/Performance tier changes and queued direct/copy comparisons. Direct and copied inputs are byte-identical, +and implicit resize resets match explicit resets. Increasing allocation capacity changes a few recurrent pixels +through normalized sampling and FP8 rounding: RMSE is 7.29e-6 (M) and 8.12e-6 (L), within the test's +5e-5 RMSE and 1% per-channel error limits. This is an adapter regression, not an in-game or NGX parity claim. + ## Preset M (runtime and shim path exist; experimental) Preset M (DLSS 4.5, `rrlite_*`) is a different pipeline: an input stage that embeds 2x2-pixel tokens, a network of @@ -687,7 +719,7 @@ each), and the engine output is byte-identical for each too. The shim records preset M through `engine_evaluate_m` when `[Engine] Enabled` is set and the model is in `engine\m` next to the DLL (or `D4R_ENGINE_MODEL_M_DIR`). -M packages now use `D4RM0003`. Rebuild with the current `compile_m.py`: the blocked FP8 operands, +M packages now use `D4RM0004` (L: `D4RL0002`). Rebuild with the current `compile_m.py`: the blocked FP8 operands, RGBA32F half-history bindings and 8×64 downsample dispatch must match the runtime. `D4RM0001` and `D4RM0002` packages are rejected rather than silently loading incompatible shaders. Native `hipnet.bin` remains `D4RMHIP1`; its tensor and weight layout is unchanged. @@ -859,7 +891,7 @@ per-pixel head (expansion), where M has single folded products, and a reconstruc history blended in linear light. `L_NOTES.md` has the algorithms. The network is M's ten layers with L's weights. The runtime is M's (`MModel`, `MEngine`, `GameUpscalerM`, `d4r-m`): `compile_m.py` recognises a capture of preset L by -its unfolded kernels and writes an L model directory (`offsets.bin` `D4RL0001`: the input and expansion stages as +its unfolded kernels and writes an L model directory (`offsets.bin` `D4RL0002`: the input and expansion stages as layer blocks of `layer_m.comp` KIND 3 and 4, and L's five constants), which `MModel` loads as preset L (`isPresetL()`). The shim runs preset 12 with the model in `engine\l` / `engine\l-ldr`; the log says `engine backend (L): feature N runs preset L in the game's command list`. diff --git a/engine/compile_m.py b/engine/compile_m.py index 022f587..44a2125 100644 --- a/engine/compile_m.py +++ b/engine/compile_m.py @@ -5,14 +5,15 @@ usage: compile_m.py CAPTURE_DIR OUT_DIR [--frame N] CAPTURE_DIR/replay must hold the frame's rrlite_enc0 and rrlite_dec0 launches and its ten Swin layer launches, with their weight allocations (D4R_CUDA_REPLAY_DUMP_DIR, filter `rrlite_`). A capture of preset M has the `_folded` input and expansion kernels; one of preset L (DLSS preset 12) has the unfolded ones, and gets an L model directory - (offsets.bin `D4RL0001`, L_NOTES.md): the same network, layer_m.comp's L input and expansion stages, post_m.comp's + (offsets.bin `D4RL0002`, L_NOTES.md): the same network, layer_m.comp's L input and expansion stages, post_m.comp's L reconstruction. Writes weights8.bin (the layers' FP8 matrices in the blocked fixed-16 tile layout of test_m.pack8, with the front-end enc0/dec0 matrices plain and NGX-ordered as their own stages read them), weights16.bin (f16 vectors and the dither table), offsets.bin and the compiled shaders. The weights are values read from NVIDIA's DLSS and are not redistributable with d4r; the shaders are d4r's own. """ -# D4RM0003 couples the blocked FP8 operands, RGBA32F half-history backing and 8x64 downsample dispatch; +# D4RM0004 / D4RL0002 add active-rectangle sampling for dynamic input sizes. +# D4RM0003 coupled the blocked FP8 operands, RGBA32F half-history backing and 8x64 downsample dispatch; # the previous package's shader workgroup dimensions are not interchangeable with this runtime. import argparse, glob, hashlib, json, os, struct, subprocess, sys from pathlib import Path @@ -86,7 +87,7 @@ def main(): layers.append((add8(b8), add16(b16), o)) blob8, blob16 = np.concatenate(w8), np.concatenate(w16) blob8.tofile(a.output / 'weights8.bin'); blob16.tofile(a.output / 'weights16.bin') - head = struct.pack('<8sII', b'D4RM0003', blob8.size, blob16.size) + struct.pack('<5I', *enc0, *dec0) + head = struct.pack('<8sII', b'D4RM0004', blob8.size, blob16.size) + struct.pack('<5I', *enc0, *dec0) for o8, o16, o in layers: head += struct.pack('<2I72I', o8, o16, *o) (a.output / 'offsets.bin').write_bytes(head) def glsl(src, out, flags=()): @@ -108,7 +109,7 @@ def main(): elif marker.exists(): marker.unlink() for p in '01': glsl('exposure_k.comp', f'exposure_m{p}.spv', ['-DMODE=1', '-DPASS=' + p]) files = sorted(a.output.glob('*.bin')) + sorted(a.output.glob('*.spv')) + sorted(a.output.glob('hip/*.hsaco')) - (a.output / 'model.json').write_text(json.dumps({'format': 'D4RM0003', 'preset': 'M', 'files': {str(f.relative_to(a.output)): hashlib.sha256(f.read_bytes()).hexdigest() for f in files}}, indent=1)) + (a.output / 'model.json').write_text(json.dumps({'format': 'D4RM0004', 'preset': 'M', 'files': {str(f.relative_to(a.output)): hashlib.sha256(f.read_bytes()).hexdigest() for f in files}}, indent=1)) print(f'Compiled native preset M model in {a.output} ({blob8.size} FP8 bytes, {blob16.size} bytes of f16 data)') @@ -138,7 +139,7 @@ def compile_l(a, one, R): consts = (arg(f'rrlite_enc0_4x4_mvlo_{R}', 268), arg(f'rrlite_enc0_4x4_mvlo_{R}', 272), arg('rrlite_dec0_4x4', 160), arg('rrlite_dec0_4x4', 164), arg(next(k for k in (f'rrlite_post_3_1_mvlo_{R}', f'rrlite_post_3_2_mvlo_{R}') if glob.glob(f'{a.capture}/replay/replay-*-{k}')), 120)) - head = struct.pack('<8sIII5f', b'D4RL0001', blob8.size, blob16.size, dither, *consts) + head = struct.pack('<8sIII5f', b'D4RL0002', blob8.size, blob16.size, dither, *consts) for o8, o16, o in blocks: head += struct.pack('<2I72I', o8, o16, *o) (a.output / 'offsets.bin').write_bytes(head) def glsl(src, out, flags=()): @@ -164,7 +165,7 @@ def compile_l(a, one, R): for stale in ('enc0_m.spv', 'dec0_m.spv', 'post_m.spv', 'post_m_q.spv', 'post_m_c.spv', 'enc0_m_planes.spv', 'dec0_m_planes.spv'): (a.output / stale).unlink(missing_ok=True) files = sorted(a.output.glob('*.bin')) + sorted(a.output.glob('*.spv')) + sorted(a.output.glob('hip/*.hsaco')) - (a.output / 'model.json').write_text(json.dumps({'format': 'D4RL0001', 'preset': 'L', 'files': {str(f.relative_to(a.output)): hashlib.sha256(f.read_bytes()).hexdigest() for f in files}}, indent=1)) + (a.output / 'model.json').write_text(json.dumps({'format': 'D4RL0002', 'preset': 'L', 'files': {str(f.relative_to(a.output)): hashlib.sha256(f.read_bytes()).hexdigest() for f in files}}, indent=1)) print(f'Compiled native preset L model in {a.output} ({blob8.size} FP8 bytes, {blob16.size} bytes of f16 data)') diff --git a/engine/dec0_m.comp b/engine/dec0_m.comp index 99dabec..0734c55 100644 --- a/engine/dec0_m.comp +++ b/engine/dec0_m.comp @@ -133,6 +133,8 @@ void main() if (p.reset == 0u && all(greaterThanEqual(f, vec2(0.0))) && all(lessThanEqual(f, p.hiSize))) { precise vec2 normalized = fma(f, 1.0 / p.hiSize, p.histOffs * (1.0 / vec2(p.size))); precise vec2 texels = normalized * vec2(p.size); + if (any(notEqual(textureSize(featTex, 0), p.size))) + texels = clamp(texels, vec2(0.5), vec2(p.size) - 0.5); precise vec2 uv = texels * p.renderInv; hist = f16vec4(roundHalfValue(textureLod(featTex, uv, 0.0))); } diff --git a/engine/enc0_features.glsl b/engine/enc0_features.glsl index e60c99e..6bb8726 100644 --- a/engine/enc0_features.glsl +++ b/engine/enc0_features.glsl @@ -65,7 +65,11 @@ void gatherFeatures(uint pi, ivec2 block, float e1, float e2, bool fusedFourth, const vec3 own = texelFetch(colorTex, texel(m), 0).rgb; const vec2 centre = vec2(m) + 0.5, f = centre - EP.jitter, inv = 1.0 / vec2(EP.size); // how much the motion changed since the previous frame - const vec2 prev = textureLod(prevMvTex, clamp(((f + EP.prevJitter) + mvA * EP.hi2rnd) * vec2(EP.renderInvX, EP.renderInvY), vec2(0.0), vec2(EP.size) * vec2(EP.renderInvX, EP.renderInvY)), 0.0).xy; + vec2 prevPixel = clamp(((f + EP.prevJitter) + mvA * EP.hi2rnd) * vec2(EP.renderInvX, EP.renderInvY), vec2(0.0), vec2(EP.size) * vec2(EP.renderInvX, EP.renderInvY)); + if (any(notEqual(textureSize(prevMvTex, 0), EP.size))) + prevPixel = clamp(prevPixel, 0.5 * vec2(EP.renderInvX, EP.renderInvY), + (vec2(EP.size) - 0.5) * vec2(EP.renderInvX, EP.renderInvY)); + const vec2 prev = textureLod(prevMvTex, prevPixel, 0.0).xy; const vec2 dm = (EP.changeScale * 540.0 / float(EP.size.y)) * (mvA * EP.hi2rnd - EP.hi2rnd * prev); const float d2 = fma(dm.x, dm.x, dm.y * dm.y); const float change = EP.changeGain * (log2(d2 > 0.0 ? sqrt(d2) + 1.0 : 1.0) * 0.6931472); @@ -102,6 +106,8 @@ void gatherFeatures(uint pi, ivec2 block, float e1, float e2, bool fusedFourth, { precise vec2 normalized = fma(q[0], 1.0 / EP.hiSize, EP.prevJitter * inv); precise vec2 texels = normalized * vec2(EP.size); + if (any(notEqual(textureSize(featTex, 0), EP.size))) + texels = clamp(texels, vec2(0.5), vec2(EP.size) - 0.5); precise vec2 uv = texels * vec2(EP.renderInvX, EP.renderInvY); feat = textureLod(featTex, uv, 0.0); } diff --git a/engine/game_m.cpp b/engine/game_m.cpp index a1c213d..975ba53 100644 --- a/engine/game_m.cpp +++ b/engine/game_m.cpp @@ -23,7 +23,10 @@ GameUpscalerM::~GameUpscalerM() = default; // the game's inputs into the engine's images, and the frame's settings void GameUpscalerM::Impl::inputs(VkCommandBuffer cb, const GameFrameM& frame) { auto& g = *this; - const uint32_t w = g.width, h = g.height; + const uint32_t w = frame.renderWidth ? frame.renderWidth : g.width; + const uint32_t h = frame.renderHeight ? frame.renderHeight : g.height; + if (w < 64 || h < 64 || w > g.width || h > g.height) + throw std::runtime_error("render size exceeds the adapter's inputs or is below 64"); if (!gameColorFormat(frame.color.format) || !gameMotionFormat(frame.motion.format) || !gameDepthFormat(frame.depth.format, frame.depth.aspect) || !gameOutputFormat(frame.output.format)) throw std::runtime_error("unsupported game texture format"); @@ -40,6 +43,7 @@ void GameUpscalerM::Impl::inputs(VkCommandBuffer cb, const GameFrameM& frame) { } g.fallback.record(cb, frame.color, frame.motion, frame.depth, w, h, w, h, directColor, directMotion, directDepth); MFrameSettings s; + s.renderWidth = w; s.renderHeight = h; s.jitter[0] = frame.jitter[0]; s.jitter[1] = frame.jitter[1]; s.motionScale[0] = frame.motionToRender[0]; s.motionScale[1] = frame.motionToRender[1]; s.motionOffset[0] = frame.motionOffset[0]; s.motionOffset[1] = frame.motionOffset[1]; diff --git a/engine/game_m.h b/engine/game_m.h index b6c2cc5..5cdfe2a 100644 --- a/engine/game_m.h +++ b/engine/game_m.h @@ -5,16 +5,18 @@ namespace d4r { struct GameFrameM { - GameImage color, depth, motion, output; // as for GameUpscaler; views are not used (inputs are always copied) + GameImage color, depth, motion, output; // as for GameUpscaler; compatible views at origin zero may be borrowed float jitter[2] = {0, 0}; float motionToRender[2] = {1, 1}; // (motion texel + motionOffset) * this = render pixels float motionOffset[2] = {0, 0}; // jittered motion vectors: jitter - previous jitter, motion texel units bool reset = false; bool depthInverted = true; + uint32_t renderWidth = 0, renderHeight = 0; // zero selects the adapter capacity }; class GameUpscalerM { public: - // The model must outlive this object. The render size is fixed and at most half the output size. + // The model must outlive this object. Constructor render dimensions allocate capacity, at most the output size. + // Vulkan frames may change active size within that capacity; external networks keep a fixed grid. GameUpscalerM(MModel& model, VkPhysicalDevice physical, VkDevice device, uint32_t outputWidth, uint32_t outputHeight, uint32_t renderWidth, uint32_t renderHeight, const ExternalNetwork* external = nullptr); ~GameUpscalerM(); // only once every recorded frame has completed on the GPU diff --git a/engine/post_m.comp b/engine/post_m.comp index 5ede581..ab02dc6 100644 --- a/engine/post_m.comp +++ b/engine/post_m.comp @@ -197,7 +197,10 @@ void main() const float inv = 1.0 / W; const vec3 cur = inv * vec3(a0.rgb), m = inv * vec3(a0.w, a1.x, a1.y), g = inv * vec3(a1.z, a1.w, a2); // the two most likely motion candidates - const vec2 mvA = texelFetch(mvATex, near, 0).xy, mvB = texelFetch(mvBTex, near, 0).xy, mvC = textureLod(mvCTex, pos * p.invRnd, 0.0).xy; + const vec2 mvPos = any(notEqual(textureSize(mvCTex, 0), p.rndSize)) + ? clamp(pos, vec2(0.5), vec2(p.rndSize) - 0.5) : pos; + const vec2 mvA = texelFetch(mvATex, near, 0).xy, mvB = texelFetch(mvBTex, near, 0).xy, + mvC = textureLod(mvCTex, mvPos * p.invRnd, 0.0).xy; vec3 big = vec3(mvA, m.x), small = vec3(mvC, m.y); if (big.z < small.z) { const vec3 t = big; big = small; small = t; } vec3 second = !(small.z < m.z) ? small : vec3(mvB, m.z); diff --git a/engine/runtime.h b/engine/runtime.h index 064984e..a29ae66 100644 --- a/engine/runtime.h +++ b/engine/runtime.h @@ -192,6 +192,7 @@ struct MFrameSettings { float motionOffset[2] = {0, 0}; // jittered motion vectors: jitter - previous jitter, in motion texel units bool reset = false; bool depthInverted = true; + uint32_t renderWidth = 0, renderHeight = 0; // zero uses the constructor size; otherwise within its capacity }; class MModel { public: @@ -203,7 +204,8 @@ public: bool uploadRecorded() const; bool hasExternalNetworkStages() const; // the model directory has the plane-layout input and expansion stages bool hasRgb10Output() const; - bool isPresetL() const; // a preset L model directory (offsets.bin D4RL0001): MEngine then runs preset L + bool hasDynamicResolution() const; // active-rectangle sampling: D4RM0004 / D4RL0002 + bool isPresetL() const; // a preset L model directory (offsets.bin D4RL0002): MEngine then runs preset L private: struct Impl; std::unique_ptr impl; @@ -215,6 +217,8 @@ private: // rounded up to 32, halved). class MEngine { public: + // Constructor render dimensions allocate capacity. Vulkan frames may use smaller active dimensions; + // external networks retain their constructor grid. A size change resets temporal history. MEngine(MModel& model, uint32_t renderWidth, uint32_t renderHeight, uint32_t outputWidth, uint32_t outputHeight, const ExternalNetwork* external = nullptr); static VkDeviceSize networkBufferBytes(uint32_t renderWidth, uint32_t renderHeight); ~MEngine(); diff --git a/engine/runtime_m.inc b/engine/runtime_m.inc index 2f083bd..15fad8c 100644 --- a/engine/runtime_m.inc +++ b/engine/runtime_m.inc @@ -77,9 +77,9 @@ struct MModel::Impl { bool planes = false; std::array exposure; bool uploaded = false, ldr = false; - // preset L (offsets.bin D4RL0001, L_NOTES.md): the input and expansion stages are layer_m.comp's KIND 3 and 4, + // preset L (offsets.bin D4RL0002, L_NOTES.md): the input and expansion stages are layer_m.comp's KIND 3 and 4, // with their own layer offset blocks; constants: change scale and gain, sigma, rho, the first gate's gain - bool presetL = false; + bool presetL = false, dynamicResolution = false; std::array lEnc0{}, lDec0{}; uint32_t dither16 = 0; float lConst[5]{}; @@ -90,9 +90,10 @@ MModel::MModel(VkPhysicalDevice physical, VkDevice device, const std::string& di auto& m = *impl; m.ldr = exists(directory + "/ldr"); // the stages were compiled for a game without the HDR flag: no exposure auto head = read(directory + "/offsets.bin"); - m.presetL = head.size() >= 8 && !std::memcmp(head.data(), "D4RL0001", 8); + m.presetL = head.size() >= 8 && (!std::memcmp(head.data(), "D4RL0002", 8) || !std::memcmp(head.data(), "D4RL0001", 8)); + m.dynamicResolution = head.size() >= 8 && (!std::memcmp(head.data(), "D4RM0004", 8) || !std::memcmp(head.data(), "D4RL0002", 8)); const size_t expect = m.presetL ? 40 + (MLayers + 2) * 74 * 4 : 16 + 5 * 4 + MLayers * 74 * 4; - if (head.size() != expect || (!m.presetL && std::memcmp(head.data(), "D4RM0003", 8))) throw std::runtime_error("incompatible M/L model metadata"); + if (head.size() != expect || (!m.presetL && !m.dynamicResolution && std::memcmp(head.data(), "D4RM0003", 8))) throw std::runtime_error("incompatible M/L model metadata"); uint32_t n8 = 0, n16 = 0; std::memcpy(&n8, head.data() + 8, 4); std::memcpy(&n16, head.data() + 12, 4); auto b8 = read(directory + "/weights8.bin"), b16 = read(directory + "/weights16.bin"); @@ -143,6 +144,7 @@ MModel::~MModel() = default; bool MModel::uploadRecorded() const { return impl->uploaded; } bool MModel::hasExternalNetworkStages() const { return impl->planes; } bool MModel::hasRgb10Output() const { return impl->downRgb10.handle != VK_NULL_HANDLE; } +bool MModel::hasDynamicResolution() const { return impl->dynamicResolution; } bool MModel::isPresetL() const { return impl->presetL; } void MModel::recordUpload(VkCommandBuffer cb) { auto& m = *impl; @@ -155,7 +157,7 @@ void MModel::recordUpload(VkCommandBuffer cb) { struct MEngine::Impl { MModel::Impl& model; - uint32_t rw, rh, ow, oh, hw, hh, gw, gh; // render, output, reconstruction, token pixel grid + uint32_t rw, rh, maxRw, maxRh, ow, oh, hw, hh, gw, gh; // render, output, reconstruction, token pixel grid Buffer enc0P, dec0P, postP, downP, expP, expSums, expState, dummy, packed; std::array layerP; Buffer tokens, skip1, merged1, skip2, merged2, tubeA, tubeB, dec2Out, dec1Out; @@ -199,7 +201,7 @@ MEngine::MEngine(MModel& model, uint32_t renderWidth, uint32_t renderHeight, uin e.external = external != nullptr; if (renderWidth < 64 || renderHeight < 64 || outputWidth % 2 || outputHeight % 2 || outputWidth > 16384 || outputHeight > 16384) throw std::runtime_error("unsupported preset M dimensions"); - e.rw = renderWidth; e.rh = renderHeight; e.ow = outputWidth; e.oh = outputHeight; + e.rw = e.maxRw = renderWidth; e.rh = e.maxRh = renderHeight; e.ow = outputWidth; e.oh = outputHeight; e.hw = outputWidth / 2 * 3; e.hh = outputHeight / 2 * 3; if (renderWidth > outputWidth || renderHeight > outputHeight) throw std::runtime_error("preset M render dimensions must not exceed the output"); @@ -212,7 +214,7 @@ MEngine::MEngine(MModel& model, uint32_t renderWidth, uint32_t renderHeight, uin const VkBufferUsageFlags sb = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, sbt = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; e.enc0P.create(d, sizeof(MEnc0Params), sbt); e.dec0P.create(d, sizeof(MDec0Params), sbt); e.postP.create(d, sizeof(MPostParams), sbt); e.downP.create(d, sizeof(MDownParams), sbt); e.expP.create(d, sizeof(ExposureParams), sbt); - e.expSums.create(d, VkDeviceSize((exposureGrid(renderWidth, outputWidth) + 31) / 32) * ((exposureGrid(renderHeight, outputHeight) + 31) / 32) * 4, sb); + e.expSums.create(d, VkDeviceSize((std::max(outputWidth / 2, 256u) + 31) / 32) * ((std::max(outputHeight / 2, 256u) + 31) / 32) * 4, sb); e.expState.create(d, 8, sbt); e.dummy.create(d, 256, sb); e.packed.create(d, VkDeviceSize(e.hw) * e.hh * 4, sb); auto tok = [&](Buffer& b, unsigned scale, unsigned channels) { b.create(d, VkDeviceSize(tw / scale) * (th / scale) * channels + 64, sb); }; @@ -325,17 +327,30 @@ void MEngine::setFrame(const FrameImages& images, const MFrameSettings& s, Frame if (finalOutput.view && finalOutput.format != VK_FORMAT_R16G16B16A16_SFLOAT && (finalOutput.format != VK_FORMAT_A2B10G10R10_UNORM_PACK32 || !e.model.downRgb10.handle)) throw std::runtime_error("unsupported borrowed output format or missing RGB10A2 shader"); - const bool reset = s.reset || e.frame == 0; + const uint32_t rw = s.renderWidth ? s.renderWidth : e.maxRw; + const uint32_t rh = s.renderHeight ? s.renderHeight : e.maxRh; + if (rw < 64 || rh < 64 || rw > e.maxRw || rh > e.maxRh) + throw std::runtime_error("render size exceeds the engine's capacity or is below 64"); + if (!e.model.dynamicResolution && (rw != e.maxRw || rh != e.maxRh)) + throw std::runtime_error("recompile the M/L model for dynamic render sizes"); + if (e.external && (rw != e.maxRw || rh != e.maxRh)) + throw std::runtime_error("external M/L network requires its constructor render size"); + const bool reset = s.reset || e.frame == 0 || rw != e.rw || rh != e.rh; + e.rw = rw; e.rh = rh; + e.gw = (rw + 31) / 32 * 32; e.gh = (rh + 31) / 32 * 32; + e.quality = uint64_t(e.hw) < 3ull * rw || uint64_t(e.hh) < 3ull * rh; + e.compact = e.quality && e.model.postCompact.handle && + 20ull * rw <= 9ull * e.hw && 20ull * rh <= 9ull * e.hh; const bool L = e.model.presetL; const float fx = float(e.hw) / float(e.rw), fy = float(e.hh) / float(e.rh); e.enc0 = {{int32_t(e.rw), int32_t(e.rh)}, {2, 2}, {int32_t(e.gw / 2), int32_t(e.gh / 2)}, {s.jitter[0], s.jitter[1]}, {e.prevJitter[0], e.prevJitter[1]}, {s.motionScale[0] * fx, s.motionScale[1] * fy}, {s.motionOffset[0], s.motionOffset[1]}, {fx, fy}, {1.f / fx, 1.f / fy}, {float(e.hw), float(e.hh)}, L ? e.model.lConst[0] : 9.045065879821777f, L ? e.model.lConst[1] : 0.22356846928596497f, - s.depthInverted ? 1u : 0u, reset ? 1u : 0u, e.frame & 7u, 1.f, 0.f, {1.f / float(e.hw), 1.f / float(e.hh)}, {1.f / float(e.rw), 1.f / float(e.rh)}}; + s.depthInverted ? 1u : 0u, reset ? 1u : 0u, e.frame & 7u, 1.f, 0.f, {1.f / float(e.hw), 1.f / float(e.hh)}, {1.f / float(e.maxRw), 1.f / float(e.maxRh)}}; e.dec0 = {{int32_t(e.rw), int32_t(e.rh)}, int32_t(e.gw / 2), reset ? 1u : 0u, {s.jitter[0], s.jitter[1]}, {fx, fy}, {float(e.hw), float(e.hh)}, - {e.prevJitter[0], e.prevJitter[1]}, L ? e.model.lConst[2] : 8.f, L ? e.model.lConst[3] : 0.07863261550664902f, {1.f / float(e.rw), 1.f / float(e.rh)}}; + {e.prevJitter[0], e.prevJitter[1]}, L ? e.model.lConst[2] : 8.f, L ? e.model.lConst[3] : 0.07863261550664902f, {1.f / float(e.maxRw), 1.f / float(e.maxRh)}}; e.post = {{0, 0}, {int32_t(e.hw), int32_t(e.hh)}, {int32_t(e.rw), int32_t(e.rh)}, {1.f / fx, 1.f / fy}, {s.jitter[0], s.jitter[1]}, - {1.f / float(e.rw), 1.f / float(e.rh)}, L ? e.model.lConst[4] : 2.7530112f, reset ? 1u : 0u, 1.f, 0.f}; + {1.f / float(e.maxRw), 1.f / float(e.maxRh)}, L ? e.model.lConst[4] : 2.7530112f, reset ? 1u : 0u, 1.f, 0.f}; e.down = {{int32_t(e.hw), int32_t(e.hh)}, {0, 0}, {0, 0}, {int32_t(e.rw), int32_t(e.rh)}, {float(e.rw) / float(e.ow), float(e.rh) / float(e.oh)}, {s.jitter[0], s.jitter[1]}, 1.f}; const uint32_t sw = exposureGrid(e.rw, e.ow), sh = exposureGrid(e.rh, e.oh); const uint32_t bx = (sw + 31) / 32, by = (sh + 31) / 32; @@ -435,14 +450,16 @@ void MEngine::recordFront(VkCommandBuffer cb, VkQueryPool stamps, uint32_t first vkCmdUpdateBuffer(cb, e.postP.handle, 0, sizeof(e.post), &e.post); vkCmdUpdateBuffer(cb, e.downP.handle, 0, sizeof(e.down), &e.down); vkCmdUpdateBuffer(cb, e.expP.handle, 0, sizeof(e.exposure), &e.exposure); if (!e.external) for (unsigned l = 0; l < MLayers; ++l) { - const int32_t shifts[2] = {MShift[l] ^ int32_t((e.frame & 1u) * 2u), MShift[l]}; - vkCmdUpdateBuffer(cb, e.layerP[l].handle, offsetof(MLayerParams, sx), sizeof(shifts), shifts); + const int32_t grid[4] = {int32_t(e.gw / 2 / MScale[l]), int32_t(e.gh / 2 / MScale[l]), + MShift[l] ^ int32_t((e.frame & 1u) * 2u), MShift[l]}; + vkCmdUpdateBuffer(cb, e.layerP[l].handle, 0, sizeof(grid), grid); } if (m.presetL) { // NGX alternates the horizontal window shift every frame: input stage (2, 2) / (0, 2), expansion (0, 0) / (2, 0) - const int32_t in[2] = {2 ^ int32_t((e.frame & 1u) * 2u), 2}, ex[2] = {int32_t((e.frame & 1u) * 2u), 0}; - vkCmdUpdateBuffer(cb, e.enc0LP.handle, offsetof(MLayerParams, sx), sizeof(in), in); - vkCmdUpdateBuffer(cb, e.dec0LP.handle, offsetof(MLayerParams, sx), sizeof(ex), ex); + const int32_t in[4] = {int32_t(e.gw), int32_t(e.gh), 2 ^ int32_t((e.frame & 1u) * 2u), 2}; + const int32_t ex[4] = {int32_t(e.gw), int32_t(e.gh), int32_t((e.frame & 1u) * 2u), 0}; + vkCmdUpdateBuffer(cb, e.enc0LP.handle, 0, sizeof(in), in); + vkCmdUpdateBuffer(cb, e.dec0LP.handle, 0, sizeof(ex), ex); } memoryBarrier(cb, VK_PIPELINE_STAGE_TRANSFER_BIT, compute, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); uint32_t query = first; @@ -479,7 +496,7 @@ void MEngine::recordBack(VkCommandBuffer cb, VkQueryPool stamps, uint32_t first) dispatch(cb, compact ? m.postCompact : (e.quality ? m.postQuality : m.post), e.postSets[i], (e.hw + 15) / 16, (e.hh + 15) / 16); nextStage(cb); stamp(); - // D4RM0003's downsample workgroup covers 8x64 output pixels. + // D4RM0004's downsample workgroup covers 8x64 output pixels. dispatch(cb, e.rgb10Output ? m.downRgb10 : m.down, e.downSet, (e.ow + 7) / 8, (e.oh + 63) / 64); stamp(); closeStages(cb); diff --git a/engine/test_game.cpp b/engine/test_game.cpp index d192fda..9a1a3d1 100644 --- a/engine/test_game.cpp +++ b/engine/test_game.cpp @@ -72,6 +72,7 @@ struct Case { bool rgb10Output = false, rotateOutputFormats = false; bool regularDepth = false, swapDepth = false; // K: depth convention; near and far values exchanged float exposureScale = 1, preExposure = 1; // K: the game's factors on the measured exposure + bool dynamicM = false, largerCapacityM = false, explicitSizeResetM = false; bool displayMotion = false; // K: motion vectors at output resolution, in output pixels bool gameExposure = false; float exposureValue = 1; // K: the game's 1x1 exposure texture instead of measurement }; @@ -201,7 +202,7 @@ std::vector evaluateM(Context& c, d4r::MModel& model, const Case& test, un } std::memset(output[slot]->upload.map,0,output[slot]->upload.size); } - d4r::GameUpscalerM adapter(model,c.physical,c.device,ow,oh,rw,rh); + d4r::GameUpscalerM adapter(model,c.physical,c.device,ow,oh,test.largerCapacityM ? ow : rw,test.largerCapacityM ? oh : rh); const auto bytes = std::max({output[0]->upload.size, output[1]->upload.size, output[2]->upload.size}); Buffer results(c,bytes*count); std::memset(results.map,0,results.size); @@ -219,7 +220,12 @@ std::vector evaluateM(Context& c, d4r::MModel& model, const Case& test, un const unsigned frameMask = rotateInputs ? rotatingMasks[frame] : mask; f.color=color[slot]->game(0,0,frameMask&1,layout); f.motion=motion[slot]->game(0,0,frameMask&2,layout); f.depth=depth[slot]->game(0,0,frameMask&4,layout); f.output=output[slot]->game(0,0,frameMask&8,VK_IMAGE_LAYOUT_GENERAL); - f.reset=frame==0||frame==4; + if (test.dynamicM) { + const uint32_t widths[count] = {640, 608, 448, 448, 640, 640}; + const uint32_t heights[count] = {360, 344, 256, 256, 360, 360}; + f.renderWidth = widths[frame]; f.renderHeight = heights[frame]; + } + f.reset=frame==0||frame==4||(test.explicitSizeResetM && (frame==1||frame==2)); f.jitter[0]=frame%2 ? .25f : -.125f; f.jitter[1]=frame%3 ? .375f : -.25f; adapter.record(c.command,f); auto r=region(ow,oh); r.bufferOffset=frame*bytes; @@ -241,9 +247,46 @@ void expectEqual(const std::vector& expected,const std::vector& actu std::cout<& expected, const std::vector& actual, const std::string& name) { + if (expected.size() != actual.size()) throw std::runtime_error(name + ": size mismatch"); + double squared = 0; float relative = 0; + for (size_t i = 0; i < expected.size()/2; ++i) { + const float a = float(reinterpret_cast(expected.data())[i]); + const float b = float(reinterpret_cast(actual.data())[i]); + const float error = std::abs(a-b); + if (!std::isfinite(error)) throw std::runtime_error(name + ": nonfinite output"); + squared += double(error)*error; + relative = std::max(relative,error/std::max(1.f,std::abs(a))); + } + const double rmse = std::sqrt(squared/(expected.size()/2)); + if (relative > .01f || rmse > 5e-5) + throw std::runtime_error(name + ": capacity changed the result (relative " + std::to_string(relative) + ", RMSE " + std::to_string(rmse) + ")"); + std::cout<> engineViews; // destroyed after engine descriptor pools std::unique_ptr engine; - std::unique_ptr engineM; // preset M + std::unique_ptr engineM; // preset M/L, original network + std::unique_ptr engineMDynamic; // Vulkan at changing sizes; original HIP resources stay alive + unsigned int engineMLastWidth = 0, engineMLastHeight = 0; unsigned int engineMRenderWidth = 0, engineMRenderHeight = 0; // first evaluation's effective extent // Preset M and preset K with the network on the native HIP layers ([Engine] Network): the buffers shared // with HIP and the bridge's network. The game's list is split around the network, as for the CUDA backend's @@ -5771,11 +5773,11 @@ static bool engine_evaluate_m(Feature& feature, ID3D12GraphicsCommandList* list, } if ((feature.flags & 0x2) == 0) return refuse("needs render-resolution motion vectors"); + if (!model->hasDynamicResolution() && feature.engineM != nullptr && + (p.renderWidth != feature.engineMRenderWidth || p.renderHeight != feature.engineMRenderHeight)) + return refuse("dynamic render sizes need a model rebuilt by the current compile_m.py"); // NGX measures the exposure for this preset, so the AutoExposure flag, the game's exposure texture, the // pre-exposure and the exposure scale are all ignored here (CUDA output is identical for each of them). - if (feature.engineM != nullptr && - (p.renderWidth != feature.engineMRenderWidth || p.renderHeight != feature.engineMRenderHeight)) - return refuse("changing the effective render resolution after initialization is not implemented"); if (color == output || depth == output || motion == output) return refuse("input/output texture aliasing is not supported"); d4r::GameFrameM game; @@ -5824,7 +5826,10 @@ static bool engine_evaluate_m(Feature& feature, ID3D12GraphicsCommandList* list, game.motionOffset[0] = (p.jitterX - feature.engineJitter[0]) / p.mvScaleX; game.motionOffset[1] = (p.jitterY - feature.engineJitter[1]) / p.mvScaleY; } - game.reset = p.reset != 0 || feature.engineM == nullptr; + game.renderWidth = p.renderWidth; + game.renderHeight = p.renderHeight; + game.reset = p.reset != 0 || feature.engineM == nullptr || + p.renderWidth != feature.engineMLastWidth || p.renderHeight != feature.engineMLastHeight; game.depthInverted = (feature.flags & 0x8) != 0; try { @@ -5836,7 +5841,8 @@ static bool engine_evaluate_m(Feature& feature, ID3D12GraphicsCommandList* list, bytes, bytes); const d4r::ExternalNetwork external = {feature.engineTokens.buffer, feature.engineHead.buffer}; feature.engineM = std::make_unique(*model, g_vk.physical, g_vk.device, feature.outWidth, - feature.outHeight, p.renderWidth, p.renderHeight, + feature.outHeight, (hip || !model->hasDynamicResolution()) ? p.renderWidth : feature.outWidth, + (hip || !model->hasDynamicResolution()) ? p.renderHeight : feature.outHeight, hip ? &external : nullptr); feature.engineMRenderWidth = p.renderWidth; feature.engineMRenderHeight = p.renderHeight; @@ -5855,7 +5861,30 @@ static bool engine_evaluate_m(Feature& feature, ID3D12GraphicsCommandList* list, release_engine_network(feature); return refuse("the engine could not be created"); } - if (feature.engineNet != nullptr) + d4r::GameUpscalerM* adapter = feature.engineM.get(); + const bool resizedHip = feature.engineNet != nullptr && + (p.renderWidth != feature.engineMRenderWidth || p.renderHeight != feature.engineMRenderHeight); + if (resizedHip) + { + try + { + if (!feature.engineMDynamic) + { + feature.engineMDynamic = std::make_unique(*model, g_vk.physical, g_vk.device, + feature.outWidth, feature.outHeight, feature.outWidth, feature.outHeight); + logf("engine backend (%s): dynamic render sizes use the Vulkan network; original HIP resources retained", name); + } + adapter = feature.engineMDynamic.get(); + } + catch (const std::exception& error) + { + logf("engine backend (%s): %s", name, error.what()); + return refuse("the dynamic engine could not be created"); + } + } + feature.engineMLastWidth = p.renderWidth; + feature.engineMLastHeight = p.renderHeight; + if (feature.engineNet != nullptr && !resizedHip) { // The native HIP network: the game's list is split around it. Its native completion callback releases // the second part, which records expansion, reconstruction and the final store (or the conversion blit @@ -5869,14 +5898,14 @@ static bool engine_evaluate_m(Feature& feature, ID3D12GraphicsCommandList* list, { if (front) { - feature.engineM->recordFront(command, game); + adapter->recordFront(command, game); // The HIP network's stream waits for this frame's number (record_engine_input_ready): the // fill is part of the first half, so the network starts the moment the front's GPU work ends. if (feature.engineGpuWait.load()) record_engine_input_ready(feature, command, frame); } else - feature.engineM->recordBack(command, game); + adapter->recordBack(command, game); } catch (const std::exception& error) { @@ -5906,7 +5935,7 @@ static bool engine_evaluate_m(Feature& feature, ID3D12GraphicsCommandList* list, { try { - feature.engineM->record(command, game); + adapter->record(command, game); } catch (const std::exception& error) { @@ -6079,8 +6108,7 @@ static bool engine_evaluate(Feature& feature, ID3D12GraphicsCommandList* list, I const d4r::ExternalNetwork external = {feature.engineTokens.buffer, feature.engineHead.buffer}; feature.engine = std::make_unique(*model, g_vk.physical, g_vk.device, feature.outWidth, feature.outHeight, - std::max(feature.width, p.renderWidth), - std::max(feature.height, p.renderHeight), + feature.outWidth, feature.outHeight, hip ? &external : nullptr); logf("engine backend: feature %u runs preset K in the game's command list (%ux%u -> %ux%u), network on %s", feature.handle.Id, p.renderWidth, p.renderHeight, feature.outWidth, feature.outHeight,