diff --git a/engine/game_rr.cpp b/engine/game_rr.cpp index b53d08a..79aa379 100644 --- a/engine/game_rr.cpp +++ b/engine/game_rr.cpp @@ -13,7 +13,7 @@ struct GameDenoiser::Impl { REngine engine; uint32_t width, height, outputWidth, outputHeight; game_detail::CanonicalInputs fallback; // colour, motion, depth - std::array guides; // the guides and optional inputs, when one cannot be sampled directly + std::array guides; // the guides and optional inputs, when one cannot be sampled directly bool started = false; // debugging: D4R_ENGINE_RR_DUMP=DIR copies every engine image after each frame into a host-visible buffer and // writes it at the next frame's recording (by then the game has waited for the frame), as DIR/F-NAME.bin @@ -100,10 +100,12 @@ void GameDenoiser::record(VkCommandBuffer cb, const GameFrameR& frame) { }; const bool directColor = direct(frame.color), directMotion = direct(frame.motion), directDepth = direct(frame.depth); // the four guides, then the two optional inputs (kept in their own one-channel formats when copied) - const GameImage* guides[6] = {&frame.normals, &frame.roughness, &frame.diffuseAlbedo, &frame.specularAlbedo, - &frame.specularHitDistance, &frame.disocclusionMask}; - const VkFormat guideFormats[6] = {VK_FORMAT_R16G16B16A16_SFLOAT, VK_FORMAT_R16G16B16A16_SFLOAT, VK_FORMAT_R16G16B16A16_SFLOAT, - VK_FORMAT_R16G16B16A16_SFLOAT, VK_FORMAT_R32_SFLOAT, VK_FORMAT_R16_SFLOAT}; + const GameImage* guides[8] = {&frame.normals, &frame.roughness, &frame.diffuseAlbedo, &frame.specularAlbedo, + &frame.specularHitDistance, &frame.disocclusionMask, &frame.subsurfaceGuide, + &frame.colorBeforeParticles}; + const VkFormat guideFormats[8] = {VK_FORMAT_R16G16B16A16_SFLOAT, VK_FORMAT_R16G16B16A16_SFLOAT, VK_FORMAT_R16G16B16A16_SFLOAT, + VK_FORMAT_R16G16B16A16_SFLOAT, VK_FORMAT_R32_SFLOAT, VK_FORMAT_R16_SFLOAT, + VK_FORMAT_R16_SFLOAT, VK_FORMAT_R16G16B16A16_SFLOAT}; auto present = [&](int i) { return i < 4 || guides[i]->image != VK_NULL_HANDLE; }; if (!g.started) { if (!g.model.uploadRecorded()) g.model.recordUpload(cb); @@ -111,7 +113,7 @@ void GameDenoiser::record(VkCommandBuffer cb, const GameFrameR& frame) { g.started = true; } g.fallback.record(cb, frame.color, frame.motion, frame.depth, w, h, w, h, directColor, directMotion, directDepth); - for (int i = 0; i < 6; ++i) { + for (int i = 0; i < 8; ++i) { if (!present(i) || direct(*guides[i])) continue; if (!g.guides[i].image) { g.fallback.ensure(cb, g.guides[i], guideFormats[i], w, h); @@ -126,9 +128,10 @@ void GameDenoiser::record(VkCommandBuffer cb, const GameFrameR& frame) { images.color = directColor ? frame.color.view : g.fallback.color.view; images.motion = directMotion ? frame.motion.view : g.fallback.motion.view; images.depth = directDepth ? frame.depth.view : g.fallback.depth.view; - VkImageView* guideViews[6] = {&images.normals, &images.roughness, &images.diffuseAlbedo, &images.specularAlbedo, - &images.specularHitDistance, &images.disocclusionMask}; - for (int i = 0; i < 6; ++i) + VkImageView* guideViews[8] = {&images.normals, &images.roughness, &images.diffuseAlbedo, &images.specularAlbedo, + &images.specularHitDistance, &images.disocclusionMask, &images.subsurfaceGuide, + &images.colorBeforeParticles}; + for (int i = 0; i < 8; ++i) if (present(i)) *guideViews[i] = direct(*guides[i]) ? guides[i]->view : g.guides[i].view; const bool directOutput = frame.output.view != VK_NULL_HANDLE && frame.output.format == VK_FORMAT_R16G16B16A16_SFLOAT && frame.output.layout == VK_IMAGE_LAYOUT_GENERAL && !frame.output.x && !frame.output.y && diff --git a/engine/game_rr.h b/engine/game_rr.h index 2acc6f8..8b191ba 100644 --- a/engine/game_rr.h +++ b/engine/game_rr.h @@ -9,6 +9,8 @@ struct GameFrameR { GameImage color, depth, motion, normals, roughness, diffuseAlbedo, specularAlbedo, output; // optional (image null: the game passes none): DLSSD.SpecularHitDistance, DLSS.DisocclusionMask GameImage specularHitDistance, disocclusionMask; + // optional too: DLSSD.ScreenSpaceSubsurfaceScatteringGuide, DLSSD.ColorBeforeParticles + GameImage subsurfaceGuide, colorBeforeParticles; RFrameSettings settings; }; class GameDenoiser { diff --git a/engine/replay_rr.cpp b/engine/replay_rr.cpp index f17fa68..23999ab 100644 --- a/engine/replay_rr.cpp +++ b/engine/replay_rr.cpp @@ -159,8 +159,8 @@ int main(int argc, char** argv) try { VK_FORMAT_R8G8B8A8_UNORM}; std::vector inputs; for (VkFormat f : formats) inputs.emplace_back(c, f, rw, rh); - const char* optionalNames[] = {"hitdistance", "disocclusion"}; - const VkFormat optionalFormats[] = {VK_FORMAT_R32_SFLOAT, VK_FORMAT_R16_SFLOAT}; + const char* optionalNames[] = {"hitdistance", "disocclusion", "sss", "particles"}; + const VkFormat optionalFormats[] = {VK_FORMAT_R32_SFLOAT, VK_FORMAT_R16_SFLOAT, VK_FORMAT_R16_SFLOAT, VK_FORMAT_R16G16B16A16_SFLOAT}; std::vector optional; for (VkFormat f : optionalFormats) optional.emplace_back(c, f, rw, rh); HostBuffer staging(c, VkDeviceSize(ow) * oh * 8); @@ -178,7 +178,7 @@ int main(int argc, char** argv) try { frames >> f >> s.jitter[0] >> s.jitter[1] >> s.motionScale[0] >> s.motionScale[1] >> s.preExposure >> reset >> inverted; for (float& v : s.worldToView) frames >> v; for (float& v : s.viewToClip) frames >> v; - s.reset = reset != 0; s.depthInverted = inverted != 0; + s.reset = reset != 0; s.depthInverted = (inverted & 1) != 0; s.hardwareDepth = (inverted & 2) != 0; // bit 1: DLSS.Use.HW.Depth for (size_t i = 0; i < inputs.size(); ++i) { auto data = read(inputDir + "/" + std::to_string(f) + "-" + names[i] + ".bin"); if (data.size() != size_t(rw) * rh * texelBytes(formats[i])) throw std::runtime_error(std::string("input size: ") + names[i]); @@ -195,7 +195,7 @@ int main(int argc, char** argv) try { images.normals = inputs[3].view; images.roughness = inputs[4].view; images.diffuseAlbedo = inputs[5].view; images.specularAlbedo = inputs[6].view; // optional inputs, present when the capture had them (rr_inputs.py) - for (int k = 0; k < 2; ++k) { + for (int k = 0; k < 4; ++k) { std::ifstream probe(inputDir + "/" + std::to_string(f) + "-" + optionalNames[k] + ".bin", std::ios::binary); if (!probe) continue; auto data = read(inputDir + "/" + std::to_string(f) + "-" + optionalNames[k] + ".bin"); @@ -207,7 +207,8 @@ int main(int argc, char** argv) try { vkCmdCopyBufferToImage(cb, staging.buffer, optional[k].image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); barrier(cb, optional[k].image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL); }); - (k == 0 ? images.specularHitDistance : images.disocclusionMask) = optional[k].view; + VkImageView* slots[] = {&images.specularHitDistance, &images.disocclusionMask, &images.subsurfaceGuide, &images.colorBeforeParticles}; + *slots[k] = optional[k].view; } engine.setFrame(images, s); if (first) c.submit([&](VkCommandBuffer cb) { model.recordUpload(cb); engine.recordInitialize(cb); first = false; }); diff --git a/engine/rr/README.md b/engine/rr/README.md index 0201652..36efe29 100644 --- a/engine/rr/README.md +++ b/engine/rr/README.md @@ -171,3 +171,21 @@ exp2 / reciprocal (rrsim's exact arithmetic is further from the capture than the single-use multiply-subtract pairs into fma, merging the two constant multiplications after the log transform, `precise` on the inlined exp2, NoContraction on its conversions. The differing values come in spatial clusters, one colour channel at a time, always one ulp, mostly just above 0.5 in surface 224. + +## Cyberpunk 2077: more inputs and settings (2026-10-12) + +Found on harness captures (`D4R_HARNESS_RR_EXTRA=particles,sss`, `D4R_HARNESS_RR_HW_DEPTH=1`, `D4R_RR_PRESET=N`): + +| what the game passes | where NGX puts it | +|---|---| +| `DLSSD.ScreenSpaceSubsurfaceScatteringGuide` (R16F) | encoder 0 texture 264 (its handle is the only switch) | +| `DLSSD.ColorBeforeParticles` (RGBA16F) | output stage texture 416, flag byte 432, and a second output-size history: surface 384, read back next frame as texture 392 (linear) | +| `DLSS.Use.HW.Depth = 1` | encoder 0 byte 980, output stage byte 443 | +| preset hint | 5 runs preset E; every other value (0-4, 6, 0x80000002, 0x80000005) runs preset D | + +Integer texture fetches outside the image return zero (`fetch0`), which is what the reference does; CUDA would clamp. +With Cyberpunk's set (hit distance, particles, subsurface guide, hardware depth) the engine's presented image is +byte-identical to the CUDA path through the shim on the frames the harness can compare (640x360 -> 1920x1080), and +with RE Requiem's set the network is identical on every frame (output 107-109 dB). At 1280x720 and 1920x1080 -> 4K +without these inputs the output still falls to 43-53 dB after the first frame (the output stage's one-ulp values); +a correctly rounded log2 there matches the capture worse than the GPU instruction does. diff --git a/engine/rr/ptx2rr.py b/engine/rr/ptx2rr.py index 836fa5d..741e6cb 100755 --- a/engine/rr/ptx2rr.py +++ b/engine/rr/ptx2rr.py @@ -66,6 +66,7 @@ uint HU2_(u16vec2 u) { return uint(u.x) | (uint(u.y) << 16); } uint H2U(f16vec2 h) { return HU2_(float16BitsToUint16(h)); } int s16(uint u) { return (int(u) << 16) >> 16; } float rcp_a(float x) { return 1.0 / x; } +vec4 fetch0(sampler2D t, ivec2 c) { const ivec2 n = textureSize(t, 0); return (c.x < 0 || c.y < 0 || c.x >= n.x || c.y >= n.y) ? vec4(0.0) : texelFetch(t, c, 0); } // x + copysign(0.5, x) rounded toward zero and then truncated: round half away from zero, exactly float round_half_away(float x) { float t = trunc(x); return abs(x - t) >= 0.5 ? t + sign(x) : t; } uint PARAM32(uint o) { return param.w[o >> 2]; } @@ -509,10 +510,11 @@ class RRT(base.T): off = self.handle_off(h) self.textures.add(off) if 'base' in o and o[-1] == 's32': - # integer coordinates still go through the texture's address mode in CUDA (clamp for every denoiser - # texture); texelFetch does not clamp, and what it returns out of range depends on the device's robustness - fetch = (f'texelFetch(T{off}, clamp(ivec2({self.val(c[0], "s32")}, {self.val(c[1], "s32")}), ivec2(0), ' - f'textureSize(T{off}, 0) - 1), 0)') + # an integer fetch outside the image: CUDA applies the texture's address mode (clamp for every denoiser + # texture), but the reference this engine is checked against is ZLUDA, whose image load returns zero there + # (zluda_ptx_impl.cpp fetch_2D). The output stage's specular reprojection reads such neighbours at the image + # border: with clamp its surfaces were 99.92% identical to the capture, with zero 99.997% (capDX, 2026-10-12) + fetch = f'fetch0(T{off}, ivec2({self.val(c[0], "s32")}, {self.val(c[1], "s32")}))' elif 'base' in o and o[-1] == 'f32': fetch = f'textureLod(T{off}, vec2({self.val(c[0], "f32")}, {self.val(c[1], "f32")}), 0.0)' elif 'level' in o and o[-1] == 'f32': diff --git a/engine/rr/rr_inputs.py b/engine/rr/rr_inputs.py index d06b126..d48c209 100644 --- a/engine/rr/rr_inputs.py +++ b/engine/rr/rr_inputs.py @@ -20,7 +20,8 @@ import numpy as np sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from rr_capture import Capture # noqa: E402 -OPTIONAL = {'hitdistance': 248, 'disocclusion': 296} +OPTIONAL = {'hitdistance': 248, 'disocclusion': 296, 'sss': 264} # encoder 0 textures +OPTIONAL_OUTPUT = {'particles': 416} # output stage textures INPUTS = {'color': 160, 'motion': 168, 'normals': 184, 'roughness': 192, 'diffuse': 232, 'specular': 240, 'depth': 512} @@ -49,6 +50,10 @@ def main(): for name, off in OPTIONAL.items(): # DLSSD.SpecularHitDistance, DLSS.DisocclusionMask when the game passed them if off in images: (o.out / f'{f}-{name}.bin').write_bytes(images[off].path.read_bytes()) + out_images = {i.offset: i for i in cap.find('hkpn_output', f).images} + for name, off in OPTIONAL_OUTPUT.items(): # DLSSD.ColorBeforeParticles + if off in out_images: + (o.out / f'{f}-{name}.bin').write_bytes(out_images[off].path.read_bytes()) clip = mat(660) view = mat(596) @ np.linalg.inv(clip) view[3, :3] = 0.0 @@ -56,7 +61,7 @@ def main(): mvx, mvy = f32(344) * rw / ow, f32(348) * rh / oh reset = 1 if f == 1 or not o.no_reset else 0 values = ' '.join(f'{v:.9g}' for v in list(view.flatten()) + list(clip.flatten())) - lines.append(f'frame {f} {jx:.9g} {jy:.9g} {mvx:.9g} {mvy:.9g} 1 {reset} {u32(528)} {values}') + lines.append(f'frame {f} {jx:.9g} {jy:.9g} {mvx:.9g} {mvy:.9g} 1 {reset} {u32(528) | (a[980] << 1)} {values}') (o.out / 'frames.txt').write_text('\n'.join(lines) + '\n') print(f'{o.frames} frames of {o.capture} in {o.out}') diff --git a/engine/runtime.h b/engine/runtime.h index 3956a05..4ab8ed5 100644 --- a/engine/runtime.h +++ b/engine/runtime.h @@ -255,6 +255,8 @@ struct RFrameImages { VkImageView color, motion, depth, normals, roughness, diffuseAlbedo, specularAlbedo; // Optional inputs (null: the game passes none): DLSSD.SpecularHitDistance and DLSS.DisocclusionMask, one channel each VkImageView specularHitDistance = VK_NULL_HANDLE, disocclusionMask = VK_NULL_HANDLE; + // DLSSD.ScreenSpaceSubsurfaceScatteringGuide (one channel) and DLSSD.ColorBeforeParticles (colour), optional too + VkImageView subsurfaceGuide = VK_NULL_HANDLE, colorBeforeParticles = VK_NULL_HANDLE; VkImageLayout layout = VK_IMAGE_LAYOUT_GENERAL; // of every view }; struct RFrameSettings { @@ -264,6 +266,7 @@ struct RFrameSettings { float frameTimeMs = 16.666667f; // FrameTimeDeltaInMsec bool reset = false; bool depthInverted = false; // creation flag NVSDK_NGX_DLSS_Feature_Flags_DepthInverted + bool hardwareDepth = false; // DLSS.Use.HW.Depth: the depth input is the nonlinear depth buffer (Cyberpunk 2077) bool roughnessPacked = false; // DLSS.Roughness.Mode 1: the roughness is the normals' w (no roughness texture) float worldToView[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1}; // row-major, as the SDK passes them float viewToClip[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1}; diff --git a/engine/runtime_rr.inc b/engine/runtime_rr.inc index ed83500..a617ef3 100644 --- a/engine/runtime_rr.inc +++ b/engine/runtime_rr.inc @@ -173,6 +173,10 @@ struct REngine::Impl { std::array q, r, h, e; // ping-pong: enc0's 304 / 224, dec0's 144 / enc0's 488, the output-size history, // and e: this frame's and the previous frame's exposure (1x1) Tex out; // the presented image (the output stage's surface 240) + // the output stage's second history (surface 384, read back as texture 392), kept only for games that pass + // DLSSD.ColorBeforeParticles; created on first use + Tex pb[2]; + bool pbNew = false; Tex zero; // a zeroed 1x1 texture for the handles NGX leaves null (they read zero) std::map spare; // per format, a 1x1 surface for stores NGX leaves unbound VkDescriptorPool pool = VK_NULL_HANDLE; @@ -201,6 +205,7 @@ struct REngine::Impl { // the optional inputs (capDX, 2026-10-12): unbound without them, like NGX case 248: return in.specularHitDistance ? G(in.specularHitDistance) : Resource{}; case 296: return in.disocclusionMask ? G(in.disocclusionMask) : Resource{}; + case 264: return in.subsurfaceGuide ? G(in.subsurfaceGuide) : Resource{}; // capDXsss case 488: return T(r[par ^ 1], true); case 496: return T(h[par ^ 1], true); case 512: return G(in.depth); } else switch (offset) { case 112: return T(s112[0]); case 120: return T(s120[0]); case 128: return T(s128[0]); case 304: return T(q[par]); @@ -222,8 +227,12 @@ struct REngine::Impl { case 176: return G(in.color, true); case 192: return G(in.depth); case 200: return T(e[0]); case 208: return T(e[1]); case 216: return T(h[par ^ 1], true); case 280: return G(in.diffuseAlbedo); case 288: return G(in.specularAlbedo); case 296: return T(q[par]); case 448: return T(d112[0]); + // DLSSD.ColorBeforeParticles and its history (capDXparticles, 2026-10-12) + case 416: return in.colorBeforeParticles ? G(in.colorBeforeParticles) : Resource{}; + case 392: return in.colorBeforeParticles ? T(pb[par ^ 1], true) : Resource{}; } else switch (offset) { case 224: return T(h[par]); case 240: return Resource{output, false, false}; + case 384: return in.colorBeforeParticles ? T(pb[par]) : Resource{}; } } return {}; @@ -398,6 +407,8 @@ void REngine::setFrame(const RFrameImages& images, const RFrameSettings& setting p[539] = images.disocclusionMask ? 1u : 0u; put32(p, 432, e.rw - 1); put32(p, 436, e.rh - 1); put32(p, 472, e.ow); put32(p, 476, e.oh); put32(p, 528, settings.depthInverted ? 1u : 0u); + // byte 980 (and the output stage's byte 443): DLSS.Use.HW.Depth (capDXhw, 2026-10-12) + p[980] = settings.hardwareDepth ? 1u : 0u; // the camera: the view's rotation (its translation row dropped) times the projection, its inverse, and both // composed with the render viewport (NDC <-> render pixels, y down) Mat4 view, clip; @@ -420,9 +431,10 @@ void REngine::setFrame(const RFrameImages& images, const RFrameSettings& setting put32(o, 56, e.w0); put32(o, 60, e.h0); put32(o, 140, e.rw - 1); put32(o, 144, e.rh - 1); putF(o, 152, settings.frameTimeMs); o[160] = images.specularHitDistance ? 1u : 0u; + o[432] = images.colorBeforeParticles ? 1u : 0u; // the particle layer is composited (textures 392 / 416, surface 384) put64(o, 256, e.arenaOffset(RHkpnArena)); putF(o, 336, settings.motionScale[0] * (ow / rw)); putF(o, 340, settings.motionScale[1] * (oh / rh)); - put32(o, 440, (reset ? 1u : 0u) | (settings.depthInverted ? 0x10000u : 0u)); + put32(o, 440, (reset ? 1u : 0u) | (settings.depthInverted ? 0x10000u : 0u) | (settings.hardwareDepth ? 0x1000000u : 0u)); // the exposure: NGX's MODE 1 measurement (engine/exposure_k.comp) on 1 / pre-exposure scaled colour const uint32_t gx = exposureGrid(e.rw, e.ow), gy = exposureGrid(e.rh, e.oh); e.exposure.origin[0] = e.exposure.origin[1] = 0; @@ -440,6 +452,10 @@ void REngine::setFrame(const RFrameImages& images, const RFrameSettings& setting e.reset = reset; // descriptors for the caller's images, cached per combination: every stage, both frame parities, bound by the // argument offsets its shader declares (rr.txt) + if (images.colorBeforeParticles && !e.pb[0].handle) { + for (auto& t : e.pb) t.create(d, e.ow, e.oh, VK_FORMAT_R16G16B16A16_SFLOAT); + e.pbNew = true; + } // texture and surface handles of the two stages with optional inputs: 1 where something is bound, 0 where NGX // leaves the argument null. The kernels branch on that (enc0 takes its specular hit distance path only when // argument 248's handle is not null; capDX, 2026-10-12). @@ -516,6 +532,18 @@ void REngine::recordFrame(VkCommandBuffer cb, VkQueryPool stamps, uint32_t first memoryBarrier(cb, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); }; + if (e.pbNew) { // the particle history starts from zero, like the rest of the temporal state + VkClearColorValue zero{}; + VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + for (auto& t : e.pb) { + imageBarrier(cb, t.handle, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT); + vkCmdClearColorImage(cb, t.handle, VK_IMAGE_LAYOUT_GENERAL, &zero, 1, &range); + } + memoryBarrier(cb, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, + VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + e.pbNew = false; + } stamp(0); // the frame's parameter blocks, then (on a reset) a clean temporal state memoryBarrier(cb, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); diff --git a/tools/d3d12_dlss_harness.cpp b/tools/d3d12_dlss_harness.cpp index 95d8b0c..c76af3e 100644 --- a/tools/d3d12_dlss_harness.cpp +++ b/tools/d3d12_dlss_harness.cpp @@ -1336,6 +1336,8 @@ int main(int argc, char** argv) // capture shows where the denoiser reads them. ID3D12Resource* hitDistance = nullptr; ID3D12Resource* disocclusionMask = nullptr; + ID3D12Resource* colorBeforeParticles = nullptr; + ID3D12Resource* subsurfaceGuide = nullptr; if (const char* extra = reconstruction ? std::getenv("D4R_HARNESS_RR_EXTRA") : nullptr) { if (std::strstr(extra, "hit") != nullptr) @@ -1360,6 +1362,31 @@ int main(int argc, char** argv) if (!disocclusionMask) return 1; upload(disocclusionMask, mask.data(), resourceWidth * sizeof(uint16_t), srv); } + // "particles" / "sss": Cyberpunk 2077's DLSSD.ColorBeforeParticles (RGBA16F) and + // DLSSD.ScreenSpaceSubsurfaceScatteringGuide (R16F) + if (std::strstr(extra, "particles") != nullptr) + { + std::vector before(static_cast(resourceWidth) * inHeight * 4); + for (UINT y = 0; y < inHeight; ++y) + for (UINT x = 0; x < resourceWidth; ++x) + for (UINT c = 0; c < 4; ++c) + before[(static_cast(y) * resourceWidth + x) * 4 + c] = std::bit_cast(static_cast<_Float16>( + c == 3 ? 1.0f : 0.05f + 0.6f * static_cast((x / 20 + 2 * (y / 20) + c) % 5) / 4.0f)); + colorBeforeParticles = create_texture(resourceWidth, inHeight, DXGI_FORMAT_R16G16B16A16_FLOAT, D3D12_RESOURCE_FLAG_NONE); + if (!colorBeforeParticles) return 1; + upload(colorBeforeParticles, before.data(), resourceWidth * 4 * sizeof(uint16_t), srv); + } + if (std::strstr(extra, "sss") != nullptr) + { + std::vector guide(static_cast(resourceWidth) * inHeight); + for (UINT y = 0; y < inHeight; ++y) + for (UINT x = 0; x < resourceWidth; ++x) + guide[static_cast(y) * resourceWidth + x] = std::bit_cast(static_cast<_Float16>( + ((x / 28 + y / 28) % 3 == 0) ? 0.75f : 0.0f)); + subsurfaceGuide = create_texture(resourceWidth, inHeight, DXGI_FORMAT_R16_FLOAT, D3D12_RESOURCE_FLAG_NONE); + if (!subsurfaceGuide) return 1; + upload(subsurfaceGuide, guide.data(), resourceWidth * sizeof(uint16_t), srv); + } std::printf("Ray Reconstruction extra inputs:%s%s\n", hitDistance ? " DLSSD.SpecularHitDistance" : "", disocclusionMask ? " DLSS.DisocclusionMask" : ""); } @@ -1520,7 +1547,9 @@ int main(int argc, char** argv) { d4r_ngx_set_int(parameters, "DLSS.Denoise.Mode", 1); d4r_ngx_set_int(parameters, "DLSS.Roughness.Mode", 0); - d4r_ngx_set_int(parameters, "DLSS.Use.HW.Depth", 0); + // D4R_HARNESS_RR_HW_DEPTH=1: the depth is the hardware (nonlinear) depth buffer, as Cyberpunk 2077 declares it + d4r_ngx_set_int(parameters, "DLSS.Use.HW.Depth", + std::getenv("D4R_HARNESS_RR_HW_DEPTH") != nullptr ? std::atoi(std::getenv("D4R_HARNESS_RR_HW_DEPTH")) : 0); d4r_ngx_set_void(parameters, "WorldToViewMatrix", worldToView.data()); d4r_ngx_set_void(parameters, "ViewToClipMatrix", viewToClip.data()); const char* preset = std::getenv("D4R_HARNESS_RR_PRESET"); @@ -1580,6 +1609,10 @@ int main(int argc, char** argv) d4r_ngx_set_d3d12_resource(parameters, "DLSSD.SpecularHitDistance", hitDistance); if (disocclusionMask != nullptr && replayDir == nullptr) d4r_ngx_set_d3d12_resource(parameters, "DLSS.DisocclusionMask", disocclusionMask); + if (colorBeforeParticles != nullptr && replayDir == nullptr) + d4r_ngx_set_d3d12_resource(parameters, "DLSSD.ColorBeforeParticles", colorBeforeParticles); + if (subsurfaceGuide != nullptr && replayDir == nullptr) + d4r_ngx_set_d3d12_resource(parameters, "DLSSD.ScreenSpaceSubsurfaceScatteringGuide", subsurfaceGuide); // DLSSD.Alpha is a guide the capture records in its own right, so the synthetic alpha // scenario would double-bind that name; it belongs to the static probe only. if (alphaScenario && replayDir == nullptr) diff --git a/tools/d4r_nvngx_shim.cpp b/tools/d4r_nvngx_shim.cpp index 631d70f..254b510 100644 --- a/tools/d4r_nvngx_shim.cpp +++ b/tools/d4r_nvngx_shim.cpp @@ -8300,10 +8300,10 @@ static bool engine_evaluate_rr(Feature& feature, ID3D12GraphicsCommandList* list feature.handle.Id, frame); return false; }; - // NGX 310.7 runs preset D's kernels when the game names no preset (harness, 2026-10-11). + // NGX 310.7 runs preset E's kernels for the hint 5 only and preset D's (diamond_wallaby) for every other value: + // none, the retired A / B / C (1-3), 6, and values with the top bit set such as Cyberpunk 2077's 0x80000002 or + // 0x80000005 (harness captures with D4R_RR_PRESET, 2026-10-12). const bool presetE = feature.preset == 5; - if (feature.preset != 0 && feature.preset != 4 && feature.preset != 5) - return refuse("only presets D and E are implemented"); std::call_once(g_engineR.once[presetE], [presetE] { g_engineR.state[presetE] = 1; // D4R_ENGINE_RR_SYNC=1 (harness comparisons): load in place so the engine serves the first frame @@ -8348,7 +8348,10 @@ static bool engine_evaluate_rr(Feature& feature, ID3D12GraphicsCommandList* list // the optional inputs a game may add (RE Requiem passes both): bound when present, like NGX ID3D12Resource* const hitDistance = get_resource(parameters, "DLSSD.SpecularHitDistance"); ID3D12Resource* const disocclusion = get_resource(parameters, "DLSS.DisocclusionMask"); - for (ID3D12Resource* input : {color, depth, motion, guides[0], guides[1], guides[2], guides[3], hitDistance, disocclusion}) + // and Cyberpunk 2077's: the subsurface scattering guide and the colour before particles + ID3D12Resource* const subsurface = get_resource(parameters, "DLSSD.ScreenSpaceSubsurfaceScatteringGuide"); + ID3D12Resource* const beforeParticles = get_resource(parameters, "DLSSD.ColorBeforeParticles"); + for (ID3D12Resource* input : {color, depth, motion, guides[0], guides[1], guides[2], guides[3], hitDistance, disocclusion, subsurface, beforeParticles}) if (input == output) return refuse("input/output texture aliasing is not supported"); d4r::GameFrameR game; @@ -8372,6 +8375,10 @@ static bool engine_evaluate_rr(Feature& feature, ID3D12GraphicsCommandList* list sources.push_back({hitDistance, &game.specularHitDistance, 0, 0, rw, rh, inputState, D3D12_RESOURCE_STATE_COPY_SOURCE}); if (disocclusion != nullptr) sources.push_back({disocclusion, &game.disocclusionMask, 0, 0, rw, rh, inputState, D3D12_RESOURCE_STATE_COPY_SOURCE}); + if (subsurface != nullptr) + sources.push_back({subsurface, &game.subsurfaceGuide, 0, 0, rw, rh, inputState, D3D12_RESOURCE_STATE_COPY_SOURCE}); + if (beforeParticles != nullptr) + sources.push_back({beforeParticles, &game.colorBeforeParticles, 0, 0, rw, rh, inputState, D3D12_RESOURCE_STATE_COPY_SOURCE}); // a guide with its own subrect origin (DLSS.Input.Normals.Subrect.Base etc.) is not implemented for (size_t i = 0; i < sizeof(kAuxSubrectNames) / sizeof(kAuxSubrectNames[0]); ++i) if (p.auxSubrect[i][0] != 0 || p.auxSubrect[i][1] != 0) @@ -8437,11 +8444,14 @@ static bool engine_evaluate_rr(Feature& feature, ID3D12GraphicsCommandList* list s.frameTimeMs = p.frameTime; s.reset = p.reset != 0 || feature.engineR == nullptr; s.depthInverted = (feature.flags & 0x8) != 0; + s.hardwareDepth = get_uint_or(parameters, "DLSS.Use.HW.Depth", 0) != 0; s.roughnessPacked = roughnessPacked; if (frame <= 2) - logf("engine backend (Ray Reconstruction): roughness %s, specular hit distance %s, disocclusion mask %s", + logf("engine backend (Ray Reconstruction): roughness %s, specular hit distance %s, disocclusion mask %s, " + "subsurface guide %s, colour before particles %s, hardware depth %d", roughnessPacked ? "packed in the normals' w" : "texture", hitDistance != nullptr ? "bound" : "none", - disocclusion != nullptr ? "bound" : "none"); + disocclusion != nullptr ? "bound" : "none", subsurface != nullptr ? "bound" : "none", + beforeParticles != nullptr ? "bound" : "none", s.hardwareDepth ? 1 : 0); std::copy(std::begin(p.auxMatrix[0]), std::end(p.auxMatrix[0]), s.worldToView); std::copy(std::begin(p.auxMatrix[1]), std::end(p.auxMatrix[1]), s.viewToClip); try