rr: Cyberpunk's extra inputs (SSS guide, colour before particles) and hardware depth

- GameFrameR/RFrameImages: DLSSD.ScreenSpaceSubsurfaceScatteringGuide (arg 264)
  and DLSSD.ColorBeforeParticles (output stage, texture 416 with its own history
  pair 384/392, zero-initialised on first use); guides array 6 -> 8
- DLSS.Use.HW.Depth: byte 980 and output byte 443 bit 24 (Cyberpunk passes the
  nonlinear depth buffer); replay's 'inverted' field becomes a bitmask
- shim: bind the two new resources, read the HW-depth flag, and accept every
  preset hint except E's 5 by mapping it to D's diamond_wallaby kernels
  (harness captures show 0x80000002/0x80000005 run preset D)
- harness: D4R_HARNESS_RR_EXTRA=particles,sss and D4R_HARNESS_RR_HW_DEPTH=1
- ptx2rr: out-of-range integer fetches return zero, matching ZLUDA's behaviour
  (the reference the stages are checked against)
- engine/rr/README: the capture findings behind the offsets
This commit is contained in:
woutermans committed 2026-10-12 13:06:59 +02:00
1 parent 50fa65d87b
commit db24805160
10 files changed
+133 -28

No files matched your search

+12 -9
View File
@@ -13,7 +13,7 @@ struct GameDenoiser::Impl {
REngine engine; REngine engine;
uint32_t width, height, outputWidth, outputHeight; uint32_t width, height, outputWidth, outputHeight;
game_detail::CanonicalInputs fallback; // colour, motion, depth game_detail::CanonicalInputs fallback; // colour, motion, depth
std::array<game_detail::Plane, 6> guides; // the guides and optional inputs, when one cannot be sampled directly std::array<game_detail::Plane, 8> guides; // the guides and optional inputs, when one cannot be sampled directly
bool started = false; bool started = false;
// debugging: D4R_ENGINE_RR_DUMP=DIR copies every engine image after each frame into a host-visible buffer and // 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 // 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); 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) // 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, const GameImage* guides[8] = {&frame.normals, &frame.roughness, &frame.diffuseAlbedo, &frame.specularAlbedo,
&frame.specularHitDistance, &frame.disocclusionMask}; &frame.specularHitDistance, &frame.disocclusionMask, &frame.subsurfaceGuide,
const VkFormat guideFormats[6] = {VK_FORMAT_R16G16B16A16_SFLOAT, VK_FORMAT_R16G16B16A16_SFLOAT, VK_FORMAT_R16G16B16A16_SFLOAT, &frame.colorBeforeParticles};
VK_FORMAT_R16G16B16A16_SFLOAT, VK_FORMAT_R32_SFLOAT, VK_FORMAT_R16_SFLOAT}; 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; }; auto present = [&](int i) { return i < 4 || guides[i]->image != VK_NULL_HANDLE; };
if (!g.started) { if (!g.started) {
if (!g.model.uploadRecorded()) g.model.recordUpload(cb); if (!g.model.uploadRecorded()) g.model.recordUpload(cb);
@@ -111,7 +113,7 @@ void GameDenoiser::record(VkCommandBuffer cb, const GameFrameR& frame) {
g.started = true; g.started = true;
} }
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);
for (int i = 0; i < 6; ++i) { for (int i = 0; i < 8; ++i) {
if (!present(i) || direct(*guides[i])) continue; if (!present(i) || direct(*guides[i])) continue;
if (!g.guides[i].image) { if (!g.guides[i].image) {
g.fallback.ensure(cb, g.guides[i], guideFormats[i], w, h); 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.color = directColor ? frame.color.view : g.fallback.color.view;
images.motion = directMotion ? frame.motion.view : g.fallback.motion.view; images.motion = directMotion ? frame.motion.view : g.fallback.motion.view;
images.depth = directDepth ? frame.depth.view : g.fallback.depth.view; images.depth = directDepth ? frame.depth.view : g.fallback.depth.view;
VkImageView* guideViews[6] = {&images.normals, &images.roughness, &images.diffuseAlbedo, &images.specularAlbedo, VkImageView* guideViews[8] = {&images.normals, &images.roughness, &images.diffuseAlbedo, &images.specularAlbedo,
&images.specularHitDistance, &images.disocclusionMask}; &images.specularHitDistance, &images.disocclusionMask, &images.subsurfaceGuide,
for (int i = 0; i < 6; ++i) &images.colorBeforeParticles};
for (int i = 0; i < 8; ++i)
if (present(i)) *guideViews[i] = direct(*guides[i]) ? guides[i]->view : g.guides[i].view; 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 && 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 && frame.output.layout == VK_IMAGE_LAYOUT_GENERAL && !frame.output.x && !frame.output.y &&
+2
View File
@@ -9,6 +9,8 @@ struct GameFrameR {
GameImage color, depth, motion, normals, roughness, diffuseAlbedo, specularAlbedo, output; GameImage color, depth, motion, normals, roughness, diffuseAlbedo, specularAlbedo, output;
// optional (image null: the game passes none): DLSSD.SpecularHitDistance, DLSS.DisocclusionMask // optional (image null: the game passes none): DLSSD.SpecularHitDistance, DLSS.DisocclusionMask
GameImage specularHitDistance, disocclusionMask; GameImage specularHitDistance, disocclusionMask;
// optional too: DLSSD.ScreenSpaceSubsurfaceScatteringGuide, DLSSD.ColorBeforeParticles
GameImage subsurfaceGuide, colorBeforeParticles;
RFrameSettings settings; RFrameSettings settings;
}; };
class GameDenoiser { class GameDenoiser {
+6 -5
View File
@@ -159,8 +159,8 @@ int main(int argc, char** argv) try {
VK_FORMAT_R8G8B8A8_UNORM}; VK_FORMAT_R8G8B8A8_UNORM};
std::vector<Input> inputs; std::vector<Input> inputs;
for (VkFormat f : formats) inputs.emplace_back(c, f, rw, rh); for (VkFormat f : formats) inputs.emplace_back(c, f, rw, rh);
const char* optionalNames[] = {"hitdistance", "disocclusion"}; const char* optionalNames[] = {"hitdistance", "disocclusion", "sss", "particles"};
const VkFormat optionalFormats[] = {VK_FORMAT_R32_SFLOAT, VK_FORMAT_R16_SFLOAT}; const VkFormat optionalFormats[] = {VK_FORMAT_R32_SFLOAT, VK_FORMAT_R16_SFLOAT, VK_FORMAT_R16_SFLOAT, VK_FORMAT_R16G16B16A16_SFLOAT};
std::vector<Input> optional; std::vector<Input> optional;
for (VkFormat f : optionalFormats) optional.emplace_back(c, f, rw, rh); for (VkFormat f : optionalFormats) optional.emplace_back(c, f, rw, rh);
HostBuffer staging(c, VkDeviceSize(ow) * oh * 8); 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; 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.worldToView) frames >> v;
for (float& v : s.viewToClip) 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) { for (size_t i = 0; i < inputs.size(); ++i) {
auto data = read(inputDir + "/" + std::to_string(f) + "-" + names[i] + ".bin"); 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]); 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.normals = inputs[3].view; images.roughness = inputs[4].view;
images.diffuseAlbedo = inputs[5].view; images.specularAlbedo = inputs[6].view; images.diffuseAlbedo = inputs[5].view; images.specularAlbedo = inputs[6].view;
// optional inputs, present when the capture had them (rr_inputs.py) // 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); std::ifstream probe(inputDir + "/" + std::to_string(f) + "-" + optionalNames[k] + ".bin", std::ios::binary);
if (!probe) continue; if (!probe) continue;
auto data = read(inputDir + "/" + std::to_string(f) + "-" + optionalNames[k] + ".bin"); 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, &region); vkCmdCopyBufferToImage(cb, staging.buffer, optional[k].image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region);
barrier(cb, optional[k].image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL); 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); engine.setFrame(images, s);
if (first) c.submit([&](VkCommandBuffer cb) { model.recordUpload(cb); engine.recordInitialize(cb); first = false; }); if (first) c.submit([&](VkCommandBuffer cb) { model.recordUpload(cb); engine.recordInitialize(cb); first = false; });
+18
View File
@@ -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, 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 `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. 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.
+6 -4
View File
@@ -66,6 +66,7 @@ uint HU2_(u16vec2 u) { return uint(u.x) | (uint(u.y) << 16); }
uint H2U(f16vec2 h) { return HU2_(float16BitsToUint16(h)); } uint H2U(f16vec2 h) { return HU2_(float16BitsToUint16(h)); }
int s16(uint u) { return (int(u) << 16) >> 16; } int s16(uint u) { return (int(u) << 16) >> 16; }
float rcp_a(float x) { return 1.0 / x; } 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 // 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; } 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]; } uint PARAM32(uint o) { return param.w[o >> 2]; }
@@ -509,10 +510,11 @@ class RRT(base.T):
off = self.handle_off(h) off = self.handle_off(h)
self.textures.add(off) self.textures.add(off)
if 'base' in o and o[-1] == 's32': if 'base' in o and o[-1] == 's32':
# integer coordinates still go through the texture's address mode in CUDA (clamp for every denoiser # an integer fetch outside the image: CUDA applies the texture's address mode (clamp for every denoiser
# texture); texelFetch does not clamp, and what it returns out of range depends on the device's robustness # texture), but the reference this engine is checked against is ZLUDA, whose image load returns zero there
fetch = (f'texelFetch(T{off}, clamp(ivec2({self.val(c[0], "s32")}, {self.val(c[1], "s32")}), ivec2(0), ' # (zluda_ptx_impl.cpp fetch_2D). The output stage's specular reprojection reads such neighbours at the image
f'textureSize(T{off}, 0) - 1), 0)') # 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': 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)' 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': elif 'level' in o and o[-1] == 'f32':
+7 -2
View File
@@ -20,7 +20,8 @@ import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from rr_capture import Capture # noqa: E402 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} 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 for name, off in OPTIONAL.items(): # DLSSD.SpecularHitDistance, DLSS.DisocclusionMask when the game passed them
if off in images: if off in images:
(o.out / f'{f}-{name}.bin').write_bytes(images[off].path.read_bytes()) (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) clip = mat(660)
view = mat(596) @ np.linalg.inv(clip) view = mat(596) @ np.linalg.inv(clip)
view[3, :3] = 0.0 view[3, :3] = 0.0
@@ -56,7 +61,7 @@ def main():
mvx, mvy = f32(344) * rw / ow, f32(348) * rh / oh mvx, mvy = f32(344) * rw / ow, f32(348) * rh / oh
reset = 1 if f == 1 or not o.no_reset else 0 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())) 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') (o.out / 'frames.txt').write_text('\n'.join(lines) + '\n')
print(f'{o.frames} frames of {o.capture} in {o.out}') print(f'{o.frames} frames of {o.capture} in {o.out}')
+3
View File
@@ -255,6 +255,8 @@ struct RFrameImages {
VkImageView color, motion, depth, normals, roughness, diffuseAlbedo, specularAlbedo; VkImageView color, motion, depth, normals, roughness, diffuseAlbedo, specularAlbedo;
// Optional inputs (null: the game passes none): DLSSD.SpecularHitDistance and DLSS.DisocclusionMask, one channel each // Optional inputs (null: the game passes none): DLSSD.SpecularHitDistance and DLSS.DisocclusionMask, one channel each
VkImageView specularHitDistance = VK_NULL_HANDLE, disocclusionMask = VK_NULL_HANDLE; 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 VkImageLayout layout = VK_IMAGE_LAYOUT_GENERAL; // of every view
}; };
struct RFrameSettings { struct RFrameSettings {
@@ -264,6 +266,7 @@ struct RFrameSettings {
float frameTimeMs = 16.666667f; // FrameTimeDeltaInMsec float frameTimeMs = 16.666667f; // FrameTimeDeltaInMsec
bool reset = false; bool reset = false;
bool depthInverted = false; // creation flag NVSDK_NGX_DLSS_Feature_Flags_DepthInverted 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) 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 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}; float viewToClip[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1};
+29 -1
View File
@@ -173,6 +173,10 @@ struct REngine::Impl {
std::array<Tex, 2> q, r, h, e; // ping-pong: enc0's 304 / 224, dec0's 144 / enc0's 488, the output-size history, std::array<Tex, 2> 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) // and e: this frame's and the previous frame's exposure (1x1)
Tex out; // the presented image (the output stage's surface 240) 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) Tex zero; // a zeroed 1x1 texture for the handles NGX leaves null (they read zero)
std::map<std::string, Tex> spare; // per format, a 1x1 surface for stores NGX leaves unbound std::map<std::string, Tex> spare; // per format, a 1x1 surface for stores NGX leaves unbound
VkDescriptorPool pool = VK_NULL_HANDLE; VkDescriptorPool pool = VK_NULL_HANDLE;
@@ -201,6 +205,7 @@ struct REngine::Impl {
// the optional inputs (capDX, 2026-10-12): unbound without them, like NGX // the optional inputs (capDX, 2026-10-12): unbound without them, like NGX
case 248: return in.specularHitDistance ? G(in.specularHitDistance) : Resource{}; case 248: return in.specularHitDistance ? G(in.specularHitDistance) : Resource{};
case 296: return in.disocclusionMask ? G(in.disocclusionMask) : 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); 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) { } 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]); 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 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 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]); 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) { } else switch (offset) {
case 224: return T(h[par]); case 240: return Resource{output, false, false}; case 224: return T(h[par]); case 240: return Resource{output, false, false};
case 384: return in.colorBeforeParticles ? T(pb[par]) : Resource{};
} }
} }
return {}; return {};
@@ -398,6 +407,8 @@ void REngine::setFrame(const RFrameImages& images, const RFrameSettings& setting
p[539] = images.disocclusionMask ? 1u : 0u; 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, 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); 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 // 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) // composed with the render viewport (NDC <-> render pixels, y down)
Mat4 view, clip; 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); 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); putF(o, 152, settings.frameTimeMs);
o[160] = images.specularHitDistance ? 1u : 0u; 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)); put64(o, 256, e.arenaOffset(RHkpnArena));
putF(o, 336, settings.motionScale[0] * (ow / rw)); putF(o, 340, settings.motionScale[1] * (oh / rh)); 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 // 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); const uint32_t gx = exposureGrid(e.rw, e.ow), gy = exposureGrid(e.rh, e.oh);
e.exposure.origin[0] = e.exposure.origin[1] = 0; 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; e.reset = reset;
// descriptors for the caller's images, cached per combination: every stage, both frame parities, bound by the // descriptors for the caller's images, cached per combination: every stage, both frame parities, bound by the
// argument offsets its shader declares (rr.txt) // 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 // 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 // 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). // 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, 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); 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); stamp(0);
// the frame's parameter blocks, then (on a reset) a clean temporal state // 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); memoryBarrier(cb, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT);
+34 -1
View File
@@ -1336,6 +1336,8 @@ int main(int argc, char** argv)
// capture shows where the denoiser reads them. // capture shows where the denoiser reads them.
ID3D12Resource* hitDistance = nullptr; ID3D12Resource* hitDistance = nullptr;
ID3D12Resource* disocclusionMask = nullptr; ID3D12Resource* disocclusionMask = nullptr;
ID3D12Resource* colorBeforeParticles = nullptr;
ID3D12Resource* subsurfaceGuide = nullptr;
if (const char* extra = reconstruction ? std::getenv("D4R_HARNESS_RR_EXTRA") : nullptr) if (const char* extra = reconstruction ? std::getenv("D4R_HARNESS_RR_EXTRA") : nullptr)
{ {
if (std::strstr(extra, "hit") != nullptr) if (std::strstr(extra, "hit") != nullptr)
@@ -1360,6 +1362,31 @@ int main(int argc, char** argv)
if (!disocclusionMask) return 1; if (!disocclusionMask) return 1;
upload(disocclusionMask, mask.data(), resourceWidth * sizeof(uint16_t), srv); 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<uint16_t> before(static_cast<size_t>(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<size_t>(y) * resourceWidth + x) * 4 + c] = std::bit_cast<uint16_t>(static_cast<_Float16>(
c == 3 ? 1.0f : 0.05f + 0.6f * static_cast<float>((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<uint16_t> guide(static_cast<size_t>(resourceWidth) * inHeight);
for (UINT y = 0; y < inHeight; ++y)
for (UINT x = 0; x < resourceWidth; ++x)
guide[static_cast<size_t>(y) * resourceWidth + x] = std::bit_cast<uint16_t>(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" : "", std::printf("Ray Reconstruction extra inputs:%s%s\n", hitDistance ? " DLSSD.SpecularHitDistance" : "",
disocclusionMask ? " DLSS.DisocclusionMask" : ""); 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.Denoise.Mode", 1);
d4r_ngx_set_int(parameters, "DLSS.Roughness.Mode", 0); 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, "WorldToViewMatrix", worldToView.data());
d4r_ngx_set_void(parameters, "ViewToClipMatrix", viewToClip.data()); d4r_ngx_set_void(parameters, "ViewToClipMatrix", viewToClip.data());
const char* preset = std::getenv("D4R_HARNESS_RR_PRESET"); 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); d4r_ngx_set_d3d12_resource(parameters, "DLSSD.SpecularHitDistance", hitDistance);
if (disocclusionMask != nullptr && replayDir == nullptr) if (disocclusionMask != nullptr && replayDir == nullptr)
d4r_ngx_set_d3d12_resource(parameters, "DLSS.DisocclusionMask", disocclusionMask); 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 // 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. // scenario would double-bind that name; it belongs to the static probe only.
if (alphaScenario && replayDir == nullptr) if (alphaScenario && replayDir == nullptr)
+16 -6
View File
@@ -8300,10 +8300,10 @@ static bool engine_evaluate_rr(Feature& feature, ID3D12GraphicsCommandList* list
feature.handle.Id, frame); feature.handle.Id, frame);
return false; 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; 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] { std::call_once(g_engineR.once[presetE], [presetE] {
g_engineR.state[presetE] = 1; g_engineR.state[presetE] = 1;
// D4R_ENGINE_RR_SYNC=1 (harness comparisons): load in place so the engine serves the first frame // 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 // 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 hitDistance = get_resource(parameters, "DLSSD.SpecularHitDistance");
ID3D12Resource* const disocclusion = get_resource(parameters, "DLSS.DisocclusionMask"); 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) if (input == output)
return refuse("input/output texture aliasing is not supported"); return refuse("input/output texture aliasing is not supported");
d4r::GameFrameR game; 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}); sources.push_back({hitDistance, &game.specularHitDistance, 0, 0, rw, rh, inputState, D3D12_RESOURCE_STATE_COPY_SOURCE});
if (disocclusion != nullptr) if (disocclusion != nullptr)
sources.push_back({disocclusion, &game.disocclusionMask, 0, 0, rw, rh, inputState, D3D12_RESOURCE_STATE_COPY_SOURCE}); 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 // 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) for (size_t i = 0; i < sizeof(kAuxSubrectNames) / sizeof(kAuxSubrectNames[0]); ++i)
if (p.auxSubrect[i][0] != 0 || p.auxSubrect[i][1] != 0) 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.frameTimeMs = p.frameTime;
s.reset = p.reset != 0 || feature.engineR == nullptr; s.reset = p.reset != 0 || feature.engineR == nullptr;
s.depthInverted = (feature.flags & 0x8) != 0; s.depthInverted = (feature.flags & 0x8) != 0;
s.hardwareDepth = get_uint_or(parameters, "DLSS.Use.HW.Depth", 0) != 0;
s.roughnessPacked = roughnessPacked; s.roughnessPacked = roughnessPacked;
if (frame <= 2) 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", 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[0]), std::end(p.auxMatrix[0]), s.worldToView);
std::copy(std::begin(p.auxMatrix[1]), std::end(p.auxMatrix[1]), s.viewToClip); std::copy(std::begin(p.auxMatrix[1]), std::end(p.auxMatrix[1]), s.viewToClip);
try try