fix(vk): DLSS and DLAA sample textures at NVIDIA's mip bias #2441

Merged
hckrman101 merged 1 commit from fix/dlss-sharpness into main 2026-11-11 19:27:07 +00:00
hckrman101 commented 2026-11-11 19:18:21 +00:00 (Migrated from github.com)

Why DLSS looked soft

DLSS builds a picture finer than its input pixels by combining several frames, each rendered with a slightly different sub-pixel camera offset. For that to work, the input has to carry the fine texture detail. NVIDIA's DLSS guide (section 3.5, Mip-Map Bias) says to sample textures at the output size's mip level and one step finer: native bias + log2(render / output) - 1.0.

Hookshot added only log2(share / 0.75) on top of the game's own -0.5 bias (the game's bias for its own upscaler at 3/4). That gave:

Mode Extra bias before NVIDIA's guide
DLSS Quality (67%) -0.17 -1.17
DLAA 0 -0.58
Render resolution Auto 0 -1

So DLSS was given the softer mips of the smaller 3D and upscaled those.

What changes

  • DLSS and DLAA: the world's materials (the samplers the game biases below 0) now get log2(share / 0.75) - 1 on top of the game's bias. That is Quality -1.17, Balanced -1.37, Performance -1.58, Ultra Performance -2.17, DLAA -0.58. Auto is taken as the game's 3/4, which gives -1.
  • FSR 3.1 and FSR 4: unchanged.
  • RX_VK_DLSS_LOD_OFFSET=<steps> sets the -1 for one run (0 gives the old behaviour, -3 is the lowest). RX_VK_UPSCALE_LOD_BIAS=0 still turns the whole bias off.
  • The rule is a plain function, upscaler_rows::upscale_lod_bias_for, checked by tests/test_upscaler_rows.cpp (no game or GPU needed). The readme's texture-detail note is updated.

Before / after

All shots are the game's own frames at the 2560x1440 output, same save and same spot (Kokiri Forest, standing still), crops scaled 2x with nearest-neighbour. Each image is four tiles, left to right:

  1. DLSS Quality before
  2. DLSS Quality after
  3. Upscaler Off at 100% (native, no anti-aliasing)
  4. the game's own upscaler

Images:

Stone path, fine texture:

stone path, fine texture

Grass field:

grass field

The wooden fence on the house:

the wooden fence on the house

DLAA with the new bias, next to native 100%:

DLAA with the new bias, next to native 100%

I measured detail as the RMS of the luma Laplacian over fixed crops (higher is more detail). DLSS Quality:

Crop Before Bias -0.5 After (-1) Native 100%
Path 12.8 14.0 14.6 21.5
Grass 6.8 7.2 7.3 14.6
Fence 6.1 6.5 6.5 11.7
Distant 4.6 5.2 5.6 9.6

Frame-to-frame change with the camera still stayed at 0.13 to 0.18 (out of 255) before and after, so the extra texture detail did not bring new shimmer. DLAA with the new bias measured 0.09 to 0.13.

This is a real but modest improvement. DLSS Quality is still softer than native and than the game's own upscaler, which visibly sharpens its output. The Sharpness row (around 80% for DLSS, as the readme says) is still the way to match that.

Checked and ruled out

I tested these with the same setup and changed nothing for them:

  • Jitter direction: correct on both axes. With x flipped, the picture was softer and flickered 3 to 4 times more. With y flipped, the fence posts smeared.
  • Grass jitter: the grass is jittered like everything else. In frames of the game's own upscaler between DLSS frames, the grass moves by the same sub-pixel offsets as the fence and path.
  • Alpha-to-coverage on foliage: turning it off made the grass worse.
  • Motion vectors in a camera pan: they point the right way, and their sizes look plausible for the orbiting camera (near and far depths move by different amounts, as they should). Fast pans still smear somewhat. That is not explained by a wrong sign or scale, and this PR does not try to fix it.

Tested

  • python -I -B tests/test_upscaler_rows.py (new checks for the bias rule: NVIDIA's formula at each fixed share, DLAA, Auto, the old behaviour at offset 0, FSR unchanged, and no bias for the game's own, Off and FSR 4 native AA)
  • Linux, RTX 3090, DLSS runtime 310.9.16, preset K, 2560x1440 output:
    • DLSS Quality before and after, plus the -0.5 offset in between
    • DLAA after
    • native 100% and the game's own as references
    • still camera, plus a camera pan for the motion-vector check

Not tested

  • The frame-rate cost. Sampling one mip step finer reads more texture data; NVIDIA's guide expects this to be small, but it was not timed here (a timed before/after with DLSS Quality is queued and will be added).

  • Windows

  • Balanced, Performance, Ultra Performance, and Auto with DLSS (the formula is the same; only Quality was captured)

  • A before shot of DLAA (only after)

  • Areas other than Kokiri Forest, and long play for moiré on high-frequency textures (NVIDIA warns a bias can add it; RX_VK_DLSS_LOD_OFFSET makes it easy to back off)

  • Presets other than K

## Why DLSS looked soft DLSS builds a picture finer than its input pixels by combining several frames, each rendered with a slightly different sub-pixel camera offset. For that to work, the input has to carry the fine texture detail. NVIDIA's DLSS guide (section 3.5, Mip-Map Bias) says to sample textures at the *output* size's mip level and one step finer: `native bias + log2(render / output) - 1.0`. Hookshot added only `log2(share / 0.75)` on top of the game's own -0.5 bias (the game's bias for its own upscaler at 3/4). That gave: | Mode | Extra bias before | NVIDIA's guide | |---|---|---| | DLSS Quality (67%) | -0.17 | -1.17 | | DLAA | 0 | -0.58 | | Render resolution Auto | 0 | -1 | So DLSS was given the softer mips of the smaller 3D and upscaled those. ## What changes - **DLSS and DLAA:** the world's materials (the samplers the game biases below 0) now get `log2(share / 0.75) - 1` on top of the game's bias. That is Quality -1.17, Balanced -1.37, Performance -1.58, Ultra Performance -2.17, DLAA -0.58. Auto is taken as the game's 3/4, which gives -1. - **FSR 3.1 and FSR 4:** unchanged. - `RX_VK_DLSS_LOD_OFFSET=<steps>` sets the -1 for one run (0 gives the old behaviour, -3 is the lowest). `RX_VK_UPSCALE_LOD_BIAS=0` still turns the whole bias off. - The rule is a plain function, `upscaler_rows::upscale_lod_bias_for`, checked by `tests/test_upscaler_rows.cpp` (no game or GPU needed). The readme's texture-detail note is updated. ## Before / after All shots are the game's own frames at the 2560x1440 output, same save and same spot (Kokiri Forest, standing still), crops scaled 2x with nearest-neighbour. Each image is four tiles, left to right: 1. DLSS Quality before 2. DLSS Quality after 3. Upscaler Off at 100% (native, no anti-aliasing) 4. the game's own upscaler Images: **Stone path, fine texture:** ![stone path, fine texture](https://raw.githubusercontent.com/hckrman101/Hookshot/23d0eb9a8561858bc76d62d6c3dc478a3766d02b/dlss-sharp/lod-path.png) **Grass field:** ![grass field](https://raw.githubusercontent.com/hckrman101/Hookshot/23d0eb9a8561858bc76d62d6c3dc478a3766d02b/dlss-sharp/lod-grass.png) **The wooden fence on the house:** ![the wooden fence on the house](https://raw.githubusercontent.com/hckrman101/Hookshot/23d0eb9a8561858bc76d62d6c3dc478a3766d02b/dlss-sharp/lod-fence.png) **DLAA with the new bias, next to native 100%:** ![DLAA with the new bias, next to native 100%](https://raw.githubusercontent.com/hckrman101/Hookshot/23d0eb9a8561858bc76d62d6c3dc478a3766d02b/dlss-sharp/dlaa-fence.png) I measured detail as the RMS of the luma Laplacian over fixed crops (higher is more detail). DLSS Quality: | Crop | Before | Bias -0.5 | After (-1) | Native 100% | |---|---|---|---|---| | Path | 12.8 | 14.0 | 14.6 | 21.5 | | Grass | 6.8 | 7.2 | 7.3 | 14.6 | | Fence | 6.1 | 6.5 | 6.5 | 11.7 | | Distant | 4.6 | 5.2 | 5.6 | 9.6 | Frame-to-frame change with the camera still stayed at 0.13 to 0.18 (out of 255) before and after, so the extra texture detail did not bring new shimmer. DLAA with the new bias measured 0.09 to 0.13. This is a real but modest improvement. DLSS Quality is still softer than native and than the game's own upscaler, which visibly sharpens its output. The Sharpness row (around 80% for DLSS, as the readme says) is still the way to match that. ## Checked and ruled out I tested these with the same setup and changed nothing for them: - **Jitter direction:** correct on both axes. With x flipped, the picture was softer and flickered 3 to 4 times more. With y flipped, the fence posts smeared. - **Grass jitter:** the grass is jittered like everything else. In frames of the game's own upscaler between DLSS frames, the grass moves by the same sub-pixel offsets as the fence and path. - **Alpha-to-coverage on foliage:** turning it off made the grass worse. - **Motion vectors in a camera pan:** they point the right way, and their sizes look plausible for the orbiting camera (near and far depths move by different amounts, as they should). Fast pans still smear somewhat. That is not explained by a wrong sign or scale, and this PR does not try to fix it. ## Tested - `python -I -B tests/test_upscaler_rows.py` (new checks for the bias rule: NVIDIA's formula at each fixed share, DLAA, Auto, the old behaviour at offset 0, FSR unchanged, and no bias for the game's own, Off and FSR 4 native AA) - Linux, RTX 3090, DLSS runtime 310.9.16, preset K, 2560x1440 output: - DLSS Quality before and after, plus the -0.5 offset in between - DLAA after - native 100% and the game's own as references - still camera, plus a camera pan for the motion-vector check ## Not tested - The frame-rate cost. Sampling one mip step finer reads more texture data; NVIDIA's guide expects this to be small, but it was not timed here (a timed before/after with DLSS Quality is queued and will be added). - Windows - Balanced, Performance, Ultra Performance, and Auto with DLSS (the formula is the same; only Quality was captured) - A before shot of DLAA (only after) - Areas other than Kokiri Forest, and long play for moiré on high-frequency textures (NVIDIA warns a bias can add it; `RX_VK_DLSS_LOD_OFFSET` makes it easy to back off) - Presets other than K
hckrman101 commented 2026-11-11 20:36:48 +00:00 (Migrated from github.com)

Follow-up on the cost of the finer mip, which the PR listed as not measured: the GPU time doesn't change.

GPU time per frame (mean of the 120-present log lines)
RX_VK_DLSS_LOD_OFFSET=0 (before this PR) 11.87 ms (6 samples)
default −1 (this PR) 11.88 ms (5 samples)

Setup: DLSS Quality 67% (1707x960 → 2560x1440), preset K, RTX 3090, Linux, headless, Kokiri Forest standing still, about 25 s each, the same build with only the switch changed. The headless runs were CPU-bound and noisy (19–34 fps either way), so no frame-rate difference can be pinned on the bias in either direction. A windowed run with a frame log would be needed for a real median and p99.

Follow-up on the cost of the finer mip, which the PR listed as not measured: the GPU time doesn't change. | | GPU time per frame (mean of the 120-present log lines) | |---|---| | `RX_VK_DLSS_LOD_OFFSET=0` (before this PR) | 11.87 ms (6 samples) | | default −1 (this PR) | 11.88 ms (5 samples) | Setup: DLSS Quality 67% (1707x960 → 2560x1440), preset K, RTX 3090, Linux, headless, Kokiri Forest standing still, about 25 s each, the same build with only the switch changed. The headless runs were CPU-bound and noisy (19–34 fps either way), so no frame-rate difference can be pinned on the bias in either direction. A windowed run with a frame log would be needed for a real median and p99.
Commenting is not possible because the repository is archived.
No description provided.