Engine: support dynamic input resolution for presets K, L and M

This commit is contained in:
woutermans committed 2026-10-11 20:45:26 +02:00
1 parent 653460f7b2
commit 37d1055228
11 files changed
+190 -47

No files matched your search

+35 -3
View File
@@ -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`.
+7 -6
View File
@@ -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)')
+2
View File
@@ -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)));
}
+7 -1
View File
@@ -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);
}
+5 -1
View File
@@ -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];
+4 -2
View File
@@ -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
+4 -1
View File
@@ -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);
+5 -1
View File
@@ -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> 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();
+34 -17
View File
@@ -77,9 +77,9 @@ struct MModel::Impl {
bool planes = false;
std::array<Pipeline, 2> 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<uint32_t, 74> 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<Buffer, MLayers> 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);
+47 -3
View File
@@ -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<char> 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<char> 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<char>& expected,const std::vector<char>& actu
std::cout<<name<<": byte-identical (six temporal frames)\n";
}
} // namespace
// Normalized bilinear sampling can round differently when the allocation extent changes.
// FP8 quantization can amplify a rounding difference in a few recurrent pixels.
// Require less than 1% per-channel error and at least 86 dB PSNR at peak 1.
void expectCapacityEquivalent(const std::vector<char>& expected, const std::vector<char>& 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<const _Float16*>(expected.data())[i]);
const float b = float(reinterpret_cast<const _Float16*>(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<<name<<": RMSE "<<rmse<<", max relative error "<<relative<<"\n";
}
void checkDynamicM(Context& c, d4r::MModel& m) {
Case dynamic; dynamic.dynamicM = true;
const auto reference = evaluateM(c,m,dynamic,0,false);
expectEqual(reference,evaluateM(c,m,dynamic,15,true),"M/L dynamic sizes with queued direct inputs");
dynamic.explicitSizeResetM = true;
expectEqual(reference,evaluateM(c,m,dynamic,0,true),"M/L size changes reset history automatically");
dynamic.explicitSizeResetM = false;
dynamic.largerCapacityM = true;
expectCapacityEquivalent(reference,evaluateM(c,m,dynamic,0,true),"M/L dynamic sizes within output-sized capacity");
expectCapacityEquivalent(reference,evaluateM(c,m,dynamic,15,true),"M/L dynamic sizes with larger capacity and direct inputs");
}
int main(int argc,char** argv) {
try {
if(argc!=2 && argc!=3) throw std::runtime_error("usage: test-game K_MODEL_DIR [M_MODEL_DIR]");
if (argc == 3 && !std::strcmp(argv[1], "--dynamic-ml")) {
Context c; c.create(2,true);
d4r::MModel m(c.physical,c.device,argv[2],c.cache);
c.begin(); m.recordUpload(c.command); c.submit();
checkDynamicM(c,m);
return 0;
}
if(argc!=2 && argc!=3) throw std::runtime_error("usage: test-game K_MODEL_DIR [M_MODEL_DIR] | --dynamic-ml M_OR_L_MODEL_DIR");
// The reconstruction table NGX itself selected at these sizes (measured through its launch arguments),
// on both sides of every threshold and for unequal axis ratios: {render w, h, output w, h, table}.
{
@@ -332,6 +375,7 @@ int main(int argc,char** argv) {
if (argc==3) {
d4r::MModel m(c.physical,c.device,argv[2],c.cache);
c.begin();m.recordUpload(c.command);c.submit();
checkDynamicM(c, m);
for (bool depthAspect:{false,true}) {
Case test;test.depthAspect=depthAspect;
auto reference=evaluateM(c,m,test,0,false);