Files
woutermans e0d1549a21 Engine: native presets K and M in the D3D12 shim, wider game coverage
A standalone Vulkan inference engine (engine/) for DLSS presets K and M that
runs from weights extracted from the user's own nvngx_dlss.dll, with d4r's
own input, network and reconstruction shaders or the native HIP network
layers, recorded into the game's command list by the shim. Opt-in
([Engine] Enabled / D4R_ENGINE=1); anything uncovered falls back to the
CUDA/ZLUDA path with the reason logged.

Byte-identical to NGX's CUDA backend under the phase-capable reference
(1706x960 -> 2560x1440, native HIP network, eleven frames, RX 9070 XT):
- K render- and display-resolution motion vectors, HDR and LDR
- K game-supplied exposure and automatic exposure with any pre-exposure
  and exposure scale (one residual: effective exposure exactly 2 or 4)
- K and M with the DLSS indicator's inverted-axis parameters
- M with any exposure settings (NGX ignores them for M)
K packages need the current compile_k.py (marker D4RO0003).

Also: HIP shared buffers are pooled across feature recreation (ROCm leaks
destroyed imports), hipnet.bin must match the preset, M-only games do not
load the K model, install_d4r_runtime.sh stages k, k-ldr, m, m-ldr with
hip/, the activation table has a generator (gen_activation_m.py), and
package_release.sh can bundle the engine's shaders (never its weights).

Signed-off-by: woutermans <[email protected]>
2026-10-10 20:54:01 +02:00

181 lines
9.3 KiB
Plaintext

#version 460
// d4r engine, preset K: the input stage, written from the algorithm described in INPUT_K_NOTES.md. It turns the frame
// into the network's input: 16 values per token (4x4 output pixels), four per 2x2-pixel sub-block:
// the current luma and the reprojected history luma of one pixel (range-compressed, sRGB-encoded),
// one channel of the previous frame's token feature, reprojected and averaged over the token,
// and how far the history's colour lies outside the current 3x3 neighbourhood.
// One thread computes one token (the reference uses one thread per sub-block and warp shuffles for the average).
#extension GL_EXT_shader_explicit_arithmetic_types : require
layout(local_size_x = 8, local_size_y = 8) in;
layout(set = 0, binding = 0, std430) readonly buffer Params
{
vec2 out2rnd, rndOffs; // render position = (pixel + .5) * out2rnd - .5 + rndOffs
vec2 mvScale, mvOffs; // previous position = pixel + .5 + mvScale * (mv + mvOffs)
vec2 invHist, invValid; // previous position -> history uv; -> [0, 1] on-screen test
vec2 tok2uv; // output pixels -> uv of the token feature texture
ivec2 colOrigin, colSize, depOrigin, depSize, mvOrigin, mvSize;
int tokW;
uint flags; // bit 0: reset; bit 1: regular depth (larger is farther), otherwise inverted;
// bit 2: display-resolution motion (one vector per output pixel, no depth)
float expo, expoIn, expoRatio; // exposure of the statistics; of the luma channels; previous / current
float minW;
} p;
layout(set = 0, binding = 1) uniform sampler2D colorTex;
layout(set = 0, binding = 2) uniform sampler2D histTex; // compressed Y, Co, Cg; bilinear, as NGX creates it
layout(set = 0, binding = 3) uniform sampler2D motionTex;
layout(set = 0, binding = 4) uniform sampler2D depthTex;
layout(set = 0, binding = 5) uniform sampler2D featTex; // previous frame's token feature; bilinear
layout(set = 0, binding = 6, std430) writeonly buffer Out { f16vec4 v[]; } outb;
float roundHalfAway(float x) { const float t = trunc(x); return abs(x - t) >= 0.5 ? t + sign(x) : t; }
int clamp1(int v, int origin, int hi) { return v > 0 ? min(v + origin, hi) : origin; }
// The reference's expansion of the stored history luma: rcp.approx(1 - v) below 0.99900102 (its threshold,
// one ulp above 0.999), otherwise ex2(((v + 0x3C4FAE29) * 9) * log2e) - 1) / (9 v).
float expandK(float v)
{
if (v < uintBitsToFloat(0x3F7FBE88u))
return 1.0 / (1.0 - v);
precise const float y = (v + uintBitsToFloat(0x3C4FAE29u)) * 9.0;
return (exp2(y * uintBitsToFloat(0x3FB8AA3Bu)) - 1.0) / (9.0 * v);
}
vec3 ycocg(vec3 c) { return vec3(0.25 * c.r + 0.5 * c.g + 0.25 * c.b, 0.5 * c.r - 0.5 * c.b, 0.5 * c.g - 0.25 * c.r - 0.25 * c.b); }
// LDR (the game did not set the HDR flag; NGX's *_ldr_* kernels): the colour is display-referred, so there is no
// exposure and no range compression: plain averages, a linear contrast weight, and the luma channels go through
// an inverse and a forward Reinhard curve.
#ifndef LDR
#define LDR 0
#endif
#if LDR
float logK(float Y) { return fma(Y, 3.0, 1.0); }
#else
float logK(float Y) { precise const float m = max(Y, 0.0); return (log2(fma(m, 12.0, 1.0)) * uintBitsToFloat(0x3F317218u)) / (m + 1e-6); }
#endif
float srgb(float v) { const float x = clamp(v, 0.0, 1.0); return x > 0.0031308 ? fma(exp2(log2(x) * (1.0 / 2.4)), 1.055, -0.055) : 12.92 * x; }
ivec2 nearest(vec2 pixCentre) { const vec2 r = pixCentre * p.out2rnd - 0.5 + p.rndOffs; return ivec2(roundHalfAway(r.x), roundHalfAway(r.y)); }
// Depth with larger = nearer: regular depth is negated, which turns its "nearest" comparisons into the inverted ones.
float depthAt(ivec2 t)
{
const ivec2 hi = p.depOrigin + p.depSize;
const float d = texelFetch(depthTex, ivec2(clamp1(t.x, p.depOrigin.x, hi.x), clamp1(t.y, p.depOrigin.y, hi.y)), 0).r;
return (p.flags & 2u) != 0u ? -d : d;
}
// motion vector of a texel, clamped to the motion rectangle, in output pixels
vec2 motionTexel(ivec2 t)
{
const ivec2 hi = p.mvOrigin + p.mvSize;
return p.mvScale * (texelFetch(motionTex, ivec2(clamp1(t.x, p.mvOrigin.x, hi.x), clamp1(t.y, p.mvOrigin.y, hi.y)), 0).xy + p.mvOffs);
}
// motion of the render texel nearest to a pixel, taken from the nearest of its diagonal depth neighbours;
// with display-resolution vectors (flag 4) t is the pixel itself, and its own vector is used
vec2 motionOf(ivec2 t)
{
if ((p.flags & 4u) != 0u) return motionTexel(t);
const float d0 = depthAt(t), dmm = depthAt(t + ivec2(-1, -1)), dpm = depthAt(t + ivec2(1, -1)), dmp = depthAt(t + ivec2(-1, 1)), dpp = depthAt(t + ivec2(1, 1));
ivec2 d = ivec2(0);
if (d0 < 0.5 * (dmm + dpp) - 0.001 * abs(d0) || d0 < 0.5 * (dpm + dmp) - 0.001 * abs(d0))
{
float m = d0;
if (dmm > m) { m = dmm; d = ivec2(-1, -1); }
if (dpm > m) { m = dpm; d = ivec2(1, -1); }
if (dmp > m) { m = dmp; d = ivec2(-1, 1); }
if (dpp > m) { d = ivec2(1, 1); }
}
const ivec2 v = t + d, hi = p.mvOrigin + p.mvSize;
return p.mvScale * (texelFetch(motionTex, ivec2(clamp1(v.x, p.mvOrigin.x, hi.x), clamp1(v.y, p.mvOrigin.y, hi.y)), 0).xy + p.mvOffs);
}
vec3 colourAt(ivec2 t) // exposed, non-negative
{
const ivec2 hi = p.colOrigin + p.colSize;
return p.expo * max(texelFetch(colorTex, ivec2(clamp1(t.x, p.colOrigin.x, hi.x), clamp1(t.y, p.colOrigin.y, hi.y)), 0).rgb, 0.0);
}
void main()
{
const ivec2 tok = ivec2(gl_GlobalInvocationID.xy);
const vec2 tokCentre = (vec2(tok) + 0.5) * 4.0;
vec4 feat[4]; // the token feature seen from each sub-block
vec4 res[4];
for (int s = 0; s < 4; ++s)
{
const ivec2 sub = ivec2(s & 1, s >> 1);
const ivec2 X = tok * 4 + sub * 2; // top-left pixel of the sub-block
// history colour of the four pixels, in linear exposed units of this frame
vec3 lin[4];
vec2 dq[4];
bool invalid = (p.flags & 1u) != 0u;
for (int j = 0; j < 4; ++j)
{
const vec2 pc = vec2(X + ivec2(j & 1, j >> 1)) + 0.5;
// display-resolution vectors belong to the pixel itself; render-resolution ones to its nearest texel
dq[j] = motionOf((p.flags & 4u) != 0u ? X + ivec2(j & 1, j >> 1) : nearest(pc));
const vec2 q = pc + dq[j], qv = q * p.invValid;
invalid = invalid || qv.x < 0.0 || qv.x > 1.0 || qv.y < 0.0 || qv.y > 1.0;
const vec3 h = textureLod(histTex, q * p.invHist, 0.0).rgb;
#if LDR
lin[j] = h;
#else
lin[j] = (h * expandK(h.r)) * p.expoRatio;
#endif
}
// the current 3x3 neighbourhood around the sub-block's centre
const ivec2 c0 = nearest(vec2(X) + 1.0);
precise vec3 S = vec3(0.0);
vec3 lo = vec3(1e30), hi = vec3(-1e30);
for (int k = 0; k < 9; ++k)
{
const vec3 c = ycocg(colourAt(c0 + ivec2(k % 3 - 1, k / 3 - 1)));
#if LDR
S += c;
#else
S = fma(vec3(max(1.0 / (c.x + 1.0), p.minW)), c, S); // fused in the reference
#endif
const vec3 L = c * logK(c.x);
lo = min(lo, L); hi = max(hi, L);
}
const vec3 m = S * (1.0 / 9.0);
#if LDR
const vec3 avg = m;
#else
const vec3 avg = m / max(1.0 - m.x, p.minW);
#endif
const vec3 A = avg * logK(avg.x);
const vec3 halfr = 0.5 * (hi - lo), den = 8.0 * (hi - lo) + 1.0;
precise float mismatch = 0.0;
for (int j = 0; j < 4; ++j)
{
// The reference rounds the compressed history before subtracting it. Left to the compiler, the product
// is fused into the subtraction, and after the cancellation that leaves the mismatch one half-float
// code off in about 3% of the sub-blocks, which the recurrent network then spreads over the frame.
precise const vec3 Hl = lin[j] * logK(lin[j].x);
precise const vec3 diff = A - Hl;
const vec3 d = max(abs(diff) - halfr, 0.0) / den;
precise const float e = fma(d.z, 0.5, fma(d.x, 2.0, d.y * 0.5));
mismatch += min(e, 1.0);
}
mismatch *= 0.25;
// the sub-block's representative pixel
const int jr = 2 * sub.x + (1 - sub.y);
const ivec2 pr = X + ivec2(1 - sub.y, sub.x);
const vec3 cr = colourAt(nearest(vec2(pr) + 0.5));
const float Yc = 0.25 * cr.r + 0.5 * cr.g + 0.25 * cr.b;
feat[s] = invalid ? vec4(0.0) : textureLod(featTex, (tokCentre + dq[jr]) * p.tok2uv, 0.0);
const float Yh = invalid ? Yc : lin[jr].x;
if (invalid) mismatch = 0.0;
// luma channels: exposure ratio of the network's own scale (p.expoIn relative to p.expo)
#if LDR
const float tc = p.expoIn * (Yc / max(1.0 - Yc, 0.2)), th = p.expoIn * (Yh / max(1.0 - Yh, 0.2));
#else
const float tc = p.expoIn * Yc, th = p.expoIn * Yh;
#endif
res[s] = vec4(srgb(tc * max(1.0 / (tc + 1.0), p.minW)), srgb(th * max(1.0 / (th + 1.0), p.minW)), 0.0, mismatch);
}
const vec4 fs = 0.25 * (feat[0] + feat[1] + feat[2] + feat[3]);
const uint o = uint(tok.y * p.tokW + tok.x) * 4u;
for (int s = 0; s < 4; ++s)
{
float f = fs[s], mm = res[s].w;
if (!(abs(f) < uintBitsToFloat(0x7F800000u))) f = 0.0;
if (!(abs(mm) < uintBitsToFloat(0x7F800000u))) mm = 0.0;
outb.v[o + uint(s)] = f16vec4(res[s].x, res[s].y, f, mm);
}
}