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, - 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. 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. - 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 - 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 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 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; consistently, so no accuracy variant became the default. `--unfold-gains` remains available;
the retained enc0 change preserves numerical behavior on this device. 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 (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 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 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`). (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 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` `D4RM0002` packages are rejected rather than silently loading incompatible shaders. Native `hipnet.bin`
remains `D4RMHIP1`; its tensor and weight layout is unchanged. 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. 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 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 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 (`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`. (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, 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` 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 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. L reconstruction.
Writes weights8.bin (the layers' FP8 matrices in the blocked fixed-16 tile layout of test_m.pack8, with the front-end 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 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 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. 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. # the previous package's shader workgroup dimensions are not interchangeable with this runtime.
import argparse, glob, hashlib, json, os, struct, subprocess, sys import argparse, glob, hashlib, json, os, struct, subprocess, sys
from pathlib import Path from pathlib import Path
@@ -86,7 +87,7 @@ def main():
layers.append((add8(b8), add16(b16), o)) layers.append((add8(b8), add16(b16), o))
blob8, blob16 = np.concatenate(w8), np.concatenate(w16) blob8, blob16 = np.concatenate(w8), np.concatenate(w16)
blob8.tofile(a.output / 'weights8.bin'); blob16.tofile(a.output / 'weights16.bin') 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) for o8, o16, o in layers: head += struct.pack('<2I72I', o8, o16, *o)
(a.output / 'offsets.bin').write_bytes(head) (a.output / 'offsets.bin').write_bytes(head)
def glsl(src, out, flags=()): def glsl(src, out, flags=()):
@@ -108,7 +109,7 @@ def main():
elif marker.exists(): marker.unlink() elif marker.exists(): marker.unlink()
for p in '01': glsl('exposure_k.comp', f'exposure_m{p}.spv', ['-DMODE=1', '-DPASS=' + p]) 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')) 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)') 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), 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}') 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)) 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) for o8, o16, o in blocks: head += struct.pack('<2I72I', o8, o16, *o)
(a.output / 'offsets.bin').write_bytes(head) (a.output / 'offsets.bin').write_bytes(head)
def glsl(src, out, flags=()): 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'): 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) (a.output / stale).unlink(missing_ok=True)
files = sorted(a.output.glob('*.bin')) + sorted(a.output.glob('*.spv')) + sorted(a.output.glob('hip/*.hsaco')) 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)') 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))) { 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 normalized = fma(f, 1.0 / p.hiSize, p.histOffs * (1.0 / vec2(p.size)));
precise vec2 texels = normalized * 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; precise vec2 uv = texels * p.renderInv;
hist = f16vec4(roundHalfValue(textureLod(featTex, uv, 0.0))); 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 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); 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 // 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 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 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); 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 normalized = fma(q[0], 1.0 / EP.hiSize, EP.prevJitter * inv);
precise vec2 texels = normalized * vec2(EP.size); 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); precise vec2 uv = texels * vec2(EP.renderInvX, EP.renderInvY);
feat = textureLod(featTex, uv, 0.0); 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 // the game's inputs into the engine's images, and the frame's settings
void GameUpscalerM::Impl::inputs(VkCommandBuffer cb, const GameFrameM& frame) { void GameUpscalerM::Impl::inputs(VkCommandBuffer cb, const GameFrameM& frame) {
auto& g = *this; 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) || if (!gameColorFormat(frame.color.format) || !gameMotionFormat(frame.motion.format) ||
!gameDepthFormat(frame.depth.format, frame.depth.aspect) || !gameOutputFormat(frame.output.format)) !gameDepthFormat(frame.depth.format, frame.depth.aspect) || !gameOutputFormat(frame.output.format))
throw std::runtime_error("unsupported game texture 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); g.fallback.record(cb, frame.color, frame.motion, frame.depth, w, h, w, h, directColor, directMotion, directDepth);
MFrameSettings s; MFrameSettings s;
s.renderWidth = w; s.renderHeight = h;
s.jitter[0] = frame.jitter[0]; s.jitter[1] = frame.jitter[1]; 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.motionScale[0] = frame.motionToRender[0]; s.motionScale[1] = frame.motionToRender[1];
s.motionOffset[0] = frame.motionOffset[0]; s.motionOffset[1] = frame.motionOffset[1]; s.motionOffset[0] = frame.motionOffset[0]; s.motionOffset[1] = frame.motionOffset[1];
+4 -2
View File
@@ -5,16 +5,18 @@
namespace d4r { namespace d4r {
struct GameFrameM { 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 jitter[2] = {0, 0};
float motionToRender[2] = {1, 1}; // (motion texel + motionOffset) * this = render pixels 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 float motionOffset[2] = {0, 0}; // jittered motion vectors: jitter - previous jitter, motion texel units
bool reset = false; bool reset = false;
bool depthInverted = true; bool depthInverted = true;
uint32_t renderWidth = 0, renderHeight = 0; // zero selects the adapter capacity
}; };
class GameUpscalerM { class GameUpscalerM {
public: 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, GameUpscalerM(MModel& model, VkPhysicalDevice physical, VkDevice device, uint32_t outputWidth, uint32_t outputHeight,
uint32_t renderWidth, uint32_t renderHeight, const ExternalNetwork* external = nullptr); uint32_t renderWidth, uint32_t renderHeight, const ExternalNetwork* external = nullptr);
~GameUpscalerM(); // only once every recorded frame has completed on the GPU ~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 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); 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 // 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); vec3 big = vec3(mvA, m.x), small = vec3(mvC, m.y);
if (big.z < small.z) { const vec3 t = big; big = small; small = t; } if (big.z < small.z) { const vec3 t = big; big = small; small = t; }
vec3 second = !(small.z < m.z) ? small : vec3(mvB, m.z); 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 float motionOffset[2] = {0, 0}; // jittered motion vectors: jitter - previous jitter, in motion texel units
bool reset = false; bool reset = false;
bool depthInverted = true; bool depthInverted = true;
uint32_t renderWidth = 0, renderHeight = 0; // zero uses the constructor size; otherwise within its capacity
}; };
class MModel { class MModel {
public: public:
@@ -203,7 +204,8 @@ public:
bool uploadRecorded() const; bool uploadRecorded() const;
bool hasExternalNetworkStages() const; // the model directory has the plane-layout input and expansion stages bool hasExternalNetworkStages() const; // the model directory has the plane-layout input and expansion stages
bool hasRgb10Output() const; 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: private:
struct Impl; struct Impl;
std::unique_ptr<Impl> impl; std::unique_ptr<Impl> impl;
@@ -215,6 +217,8 @@ private:
// rounded up to 32, halved). // rounded up to 32, halved).
class MEngine { class MEngine {
public: 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); 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); static VkDeviceSize networkBufferBytes(uint32_t renderWidth, uint32_t renderHeight);
~MEngine(); ~MEngine();
+34 -17
View File
@@ -77,9 +77,9 @@ struct MModel::Impl {
bool planes = false; bool planes = false;
std::array<Pipeline, 2> exposure; std::array<Pipeline, 2> exposure;
bool uploaded = false, ldr = false; 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 // 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{}; std::array<uint32_t, 74> lEnc0{}, lDec0{};
uint32_t dither16 = 0; uint32_t dither16 = 0;
float lConst[5]{}; float lConst[5]{};
@@ -90,9 +90,10 @@ MModel::MModel(VkPhysicalDevice physical, VkDevice device, const std::string& di
auto& m = *impl; auto& m = *impl;
m.ldr = exists(directory + "/ldr"); // the stages were compiled for a game without the HDR flag: no exposure m.ldr = exists(directory + "/ldr"); // the stages were compiled for a game without the HDR flag: no exposure
auto head = read(directory + "/offsets.bin"); 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; 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; uint32_t n8 = 0, n16 = 0;
std::memcpy(&n8, head.data() + 8, 4); std::memcpy(&n16, head.data() + 12, 4); 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"); 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::uploadRecorded() const { return impl->uploaded; }
bool MModel::hasExternalNetworkStages() const { return impl->planes; } bool MModel::hasExternalNetworkStages() const { return impl->planes; }
bool MModel::hasRgb10Output() const { return impl->downRgb10.handle != VK_NULL_HANDLE; } 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; } bool MModel::isPresetL() const { return impl->presetL; }
void MModel::recordUpload(VkCommandBuffer cb) { void MModel::recordUpload(VkCommandBuffer cb) {
auto& m = *impl; auto& m = *impl;
@@ -155,7 +157,7 @@ void MModel::recordUpload(VkCommandBuffer cb) {
struct MEngine::Impl { struct MEngine::Impl {
MModel::Impl& model; 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; Buffer enc0P, dec0P, postP, downP, expP, expSums, expState, dummy, packed;
std::array<Buffer, MLayers> layerP; std::array<Buffer, MLayers> layerP;
Buffer tokens, skip1, merged1, skip2, merged2, tubeA, tubeB, dec2Out, dec1Out; 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; e.external = external != nullptr;
if (renderWidth < 64 || renderHeight < 64 || outputWidth % 2 || outputHeight % 2 || outputWidth > 16384 || outputHeight > 16384) if (renderWidth < 64 || renderHeight < 64 || outputWidth % 2 || outputHeight % 2 || outputWidth > 16384 || outputHeight > 16384)
throw std::runtime_error("unsupported preset M dimensions"); 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; e.hw = outputWidth / 2 * 3; e.hh = outputHeight / 2 * 3;
if (renderWidth > outputWidth || renderHeight > outputHeight) if (renderWidth > outputWidth || renderHeight > outputHeight)
throw std::runtime_error("preset M render dimensions must not exceed the output"); 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; 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.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.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.expState.create(d, 8, sbt); e.dummy.create(d, 256, sb);
e.packed.create(d, VkDeviceSize(e.hw) * e.hh * 4, 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); }; 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 && if (finalOutput.view && finalOutput.format != VK_FORMAT_R16G16B16A16_SFLOAT &&
(finalOutput.format != VK_FORMAT_A2B10G10R10_UNORM_PACK32 || !e.model.downRgb10.handle)) (finalOutput.format != VK_FORMAT_A2B10G10R10_UNORM_PACK32 || !e.model.downRgb10.handle))
throw std::runtime_error("unsupported borrowed output format or missing RGB10A2 shader"); 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 bool L = e.model.presetL;
const float fx = float(e.hw) / float(e.rw), fy = float(e.hh) / float(e.rh); 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]}, 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)}, {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, 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.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]}, 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}; 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 sw = exposureGrid(e.rw, e.ow), sh = exposureGrid(e.rh, e.oh);
const uint32_t bx = (sw + 31) / 32, by = (sh + 31) / 32; 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.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); vkCmdUpdateBuffer(cb, e.expP.handle, 0, sizeof(e.exposure), &e.exposure);
if (!e.external) for (unsigned l = 0; l < MLayers; ++l) { 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]}; const int32_t grid[4] = {int32_t(e.gw / 2 / MScale[l]), int32_t(e.gh / 2 / MScale[l]),
vkCmdUpdateBuffer(cb, e.layerP[l].handle, offsetof(MLayerParams, sx), sizeof(shifts), shifts); MShift[l] ^ int32_t((e.frame & 1u) * 2u), MShift[l]};
vkCmdUpdateBuffer(cb, e.layerP[l].handle, 0, sizeof(grid), grid);
} }
if (m.presetL) { if (m.presetL) {
// NGX alternates the horizontal window shift every frame: input stage (2, 2) / (0, 2), expansion (0, 0) / (2, 0) // 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}; const int32_t in[4] = {int32_t(e.gw), int32_t(e.gh), 2 ^ int32_t((e.frame & 1u) * 2u), 2};
vkCmdUpdateBuffer(cb, e.enc0LP.handle, offsetof(MLayerParams, sx), sizeof(in), in); const int32_t ex[4] = {int32_t(e.gw), int32_t(e.gh), int32_t((e.frame & 1u) * 2u), 0};
vkCmdUpdateBuffer(cb, e.dec0LP.handle, offsetof(MLayerParams, sx), sizeof(ex), ex); 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); 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; 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), dispatch(cb, compact ? m.postCompact : (e.quality ? m.postQuality : m.post),
e.postSets[i], (e.hw + 15) / 16, (e.hh + 15) / 16); nextStage(cb); e.postSets[i], (e.hw + 15) / 16, (e.hh + 15) / 16); nextStage(cb);
stamp(); 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); dispatch(cb, e.rgb10Output ? m.downRgb10 : m.down, e.downSet, (e.ow + 7) / 8, (e.oh + 63) / 64);
stamp(); stamp();
closeStages(cb); closeStages(cb);
+47 -3
View File
@@ -72,6 +72,7 @@ struct Case {
bool rgb10Output = false, rotateOutputFormats = false; bool rgb10Output = false, rotateOutputFormats = false;
bool regularDepth = false, swapDepth = false; // K: depth convention; near and far values exchanged 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 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 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 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); 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}); const auto bytes = std::max({output[0]->upload.size, output[1]->upload.size, output[2]->upload.size});
Buffer results(c,bytes*count); Buffer results(c,bytes*count);
std::memset(results.map,0,results.size); 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; 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.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.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; f.jitter[0]=frame%2 ? .25f : -.125f; f.jitter[1]=frame%3 ? .375f : -.25f;
adapter.record(c.command,f); adapter.record(c.command,f);
auto r=region(ow,oh); r.bufferOffset=frame*bytes; 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"; std::cout<<name<<": byte-identical (six temporal frames)\n";
} }
} // namespace } // 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) { int main(int argc,char** argv) {
try { 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), // 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}. // 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) { if (argc==3) {
d4r::MModel m(c.physical,c.device,argv[2],c.cache); d4r::MModel m(c.physical,c.device,argv[2],c.cache);
c.begin();m.recordUpload(c.command);c.submit(); c.begin();m.recordUpload(c.command);c.submit();
checkDynamicM(c, m);
for (bool depthAspect:{false,true}) { for (bool depthAspect:{false,true}) {
Case test;test.depthAspect=depthAspect; Case test;test.depthAspect=depthAspect;
auto reference=evaluateM(c,m,test,0,false); auto reference=evaluateM(c,m,test,0,false);
+40 -12
View File
@@ -1816,7 +1816,9 @@ struct Feature
bool engineViewBudgetReported = false; bool engineViewBudgetReported = false;
std::vector<std::unique_ptr<EngineBorrowedView>> engineViews; // destroyed after engine descriptor pools std::vector<std::unique_ptr<EngineBorrowedView>> engineViews; // destroyed after engine descriptor pools
std::unique_ptr<d4r::GameUpscaler> engine; std::unique_ptr<d4r::GameUpscaler> engine;
std::unique_ptr<d4r::GameUpscalerM> engineM; // preset M std::unique_ptr<d4r::GameUpscalerM> engineM; // preset M/L, original network
std::unique_ptr<d4r::GameUpscalerM> 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 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 // 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 // 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) if ((feature.flags & 0x2) == 0)
return refuse("needs render-resolution motion vectors"); 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 // 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). // 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) if (color == output || depth == output || motion == output)
return refuse("input/output texture aliasing is not supported"); return refuse("input/output texture aliasing is not supported");
d4r::GameFrameM game; 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[0] = (p.jitterX - feature.engineJitter[0]) / p.mvScaleX;
game.motionOffset[1] = (p.jitterY - feature.engineJitter[1]) / p.mvScaleY; 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; game.depthInverted = (feature.flags & 0x8) != 0;
try try
{ {
@@ -5836,7 +5841,8 @@ static bool engine_evaluate_m(Feature& feature, ID3D12GraphicsCommandList* list,
bytes, bytes); bytes, bytes);
const d4r::ExternalNetwork external = {feature.engineTokens.buffer, feature.engineHead.buffer}; const d4r::ExternalNetwork external = {feature.engineTokens.buffer, feature.engineHead.buffer};
feature.engineM = std::make_unique<d4r::GameUpscalerM>(*model, g_vk.physical, g_vk.device, feature.outWidth, feature.engineM = std::make_unique<d4r::GameUpscalerM>(*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); hip ? &external : nullptr);
feature.engineMRenderWidth = p.renderWidth; feature.engineMRenderWidth = p.renderWidth;
feature.engineMRenderHeight = p.renderHeight; feature.engineMRenderHeight = p.renderHeight;
@@ -5855,7 +5861,30 @@ static bool engine_evaluate_m(Feature& feature, ID3D12GraphicsCommandList* list,
release_engine_network(feature); release_engine_network(feature);
return refuse("the engine could not be created"); 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<d4r::GameUpscalerM>(*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 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 // 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) 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 // 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. // fill is part of the first half, so the network starts the moment the front's GPU work ends.
if (feature.engineGpuWait.load()) if (feature.engineGpuWait.load())
record_engine_input_ready(feature, command, frame); record_engine_input_ready(feature, command, frame);
} }
else else
feature.engineM->recordBack(command, game); adapter->recordBack(command, game);
} }
catch (const std::exception& error) catch (const std::exception& error)
{ {
@@ -5906,7 +5935,7 @@ static bool engine_evaluate_m(Feature& feature, ID3D12GraphicsCommandList* list,
{ {
try try
{ {
feature.engineM->record(command, game); adapter->record(command, game);
} }
catch (const std::exception& error) 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}; const d4r::ExternalNetwork external = {feature.engineTokens.buffer, feature.engineHead.buffer};
feature.engine = std::make_unique<d4r::GameUpscaler>(*model, g_vk.physical, g_vk.device, feature.engine = std::make_unique<d4r::GameUpscaler>(*model, g_vk.physical, g_vk.device,
feature.outWidth, feature.outHeight, feature.outWidth, feature.outHeight,
std::max(feature.width, p.renderWidth), feature.outWidth, feature.outHeight,
std::max(feature.height, p.renderHeight),
hip ? &external : nullptr); hip ? &external : nullptr);
logf("engine backend: feature %u runs preset K in the game's command list (%ux%u -> %ux%u), network on %s", 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, feature.handle.Id, p.renderWidth, p.renderHeight, feature.outWidth, feature.outHeight,