Skip to content

fix(vk): DLSS comes back after a feature that was not made, and frame generation takes the multiplier its sets were made for - #2658

Merged
hckrman101 merged 2 commits into
NotRexed:mainfrom
notquiteog:fix/dlss-retry-and-framegen-multiplier
Nov 12, 2026
Merged

hckrman101 merged 2 commits into
NotRexed:mainfrom
notquiteog:fix/dlss-retry-and-framegen-multiplier

Conversation

@notquiteog

Copy link
Copy Markdown
Contributor

After one failed DLSS feature creation, DLSS stays off until the player changes the DLSS model or the output size; and stepping the Frame generation multiplier up in the menu can make a frame be copied into an image that does not exist. Both are in code that #2550 and #2574 touched, found by reading the merged result.

What changed

Frame generation: the multiplier is read once (vk_framegen.cpp). framegen_record called multiplier() again after ensure_size had made the sets for fg.mult. The menu runs on the display thread (menu_tick from present_image), the record on the replay thread, with a wait for a free set in between. A step from 2x to 3x in that window gives m = fg.mult + 1, and the last vkCmdCopyImage targets set->frame[fg.mult], a null image, which the display thread then presents. It takes fg.mult now, and reads the Auto test once for the same reason.

DLSS: refused is cleared where a retry should happen (vk_dlss.cpp). Since #2550 a creation that fails returns 0 with s.refused = key, and the key is the output size, preset and HDR. The old feature is already let go, so from then on:

  • the Render resolution row does not retry (same output, same key);
  • DLSS to DLAA does not retry (DLAA's output is the display's, the same key);
  • the Upscaler away and back does not retry (release() left refused set).

#2574's temporal_reset_failure does not help there: the chain's failed flag is not what holds DLSS off. Now the quality mode is part of the key, release() clears refused, and temporal_reset_failure clears it too (dlss_retry, called with the chain's lock held, as prepare has it).

DLSS: a failing evaluation no longer logs on every frame (vk_dlss.cpp). With retry = true (#2574) an evaluation that fails every frame wrote its evaluation failed 60 times a second for the rest of the run. The line is spaced like the chain's own reason (when the result changes, and every 600th time), and after 30 failures in a row the feature is let go once so prepare makes a new one.

How it was tested

Not tested

  • A real failed feature creation or failed evaluation: neither could be provoked on this card (16 GB), so the retry and the spaced log line are checked by reading, not by a run.
  • The race itself is a timing window; the run shows the fix does no harm under quick steps, not that the old code crashed here.
  • Windows, AMD and Intel.

Review notes that are not fixed here, for whoever owns them: FSR 4's engine still has the permanent latch #2574 removed for DLSS (vk_fsr4.cpp prepare returns -1, no retry); with Native HDR, frame generation's images are RGBA8 (vk_framegen.cpp ensure_size), so the game's 10-bit PQ picture passes through 8 bits; DLSS-G's feature keeps the render size it was created with when the Render resolution changes.

🤖 Generated with Claude Code

… generation takes the multiplier its sets were made for

- vk_framegen.cpp: framegen_record took the multiplier from the row again after
  ensure_size had made the sets; a step up in the menu (the display thread) in between
  copied the game's frame into an image that was never made. It takes fg.mult.
- vk_dlss.cpp: a feature that was not made (NotRexed#2550) was asked for again only when the
  output, preset or HDR changed. The quality mode is in the key, release() clears it,
  and temporal_reset_failure clears it (dlss_retry).
- vk_dlss.cpp: with retry (NotRexed#2574) a failing evaluation logged on every frame for good.
  Its line is spaced as the chain's reason is, and after 30 in a row the feature is
  made again once.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@Jiro131

Jiro131 commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

A Windows test of the frame-generation race in the first part of this PR. Current main crashed twice here, and this PR did not crash under the same multiplier steps.

  • Hardware: RTX 4090, NVIDIA 621.01, Windows 11, i9-13900K, 144 Hz display.
  • Builds, both from source with NGX (llvm-mingw, SDK 310.9.16), on the same base:
    • main: hookshot v2.1.0-rc.3-256-g51da0464 (source 18ed87b5);
    • main + this PR (343bee0 cherry-picked): hookshot v2.1.0-rc.3-257-g204757a2 (source 39f17cc9).
  • Settings, the same options file for both: DLSS DLAA (preset K) at 2560x1440, Frame generation by Auto, Frame pacing on, cap 60, V-Sync Auto.
  • The RTX 4090's DLSS-G runtime makes 1 frame, so at 3x and 4x the renderer's own generator makes the frames.

main, two crashes, both the same:

[hos Presentation Thread] CRASH: exception 0xc0000005 (read 0x590, guest 0xfffff00000000590) at nvoglv64.dll+0xea4c1a
[hos Presentation Thread] CRASH: host backtrace: nvoglv64.dll+0xea4c1a nvoglv64.dll+0xe99c39 nvoglv64.dll+0xe35298 hookshot_vk.dll+0x271ef1 hookshot_vk.dll+0x346225 hookshot_vk.dll+0x32f32e ...

The three hookshot_vk.dll frames, symbolized against that build's DLL with llvm-symbolizer, are mkwvk::framegen_record, called from do_present, called from present. The driver reads at 0x590 from a null base while it records framegen_record's commands.

  1. In a window, Reflex off, stepping the multiplier up in the menu from 2x to 3x, then 3x to 4x. The crash came at the step to 4x, and the swapchain was being remade for it at the same moment:
    [hos host] vk: surface caps images 2..8 extent 2558x1360..2558x1360 usage 0x9f alpha 0x1 transform 0x1
    [hos Presentation Thread] CRASH: exception 0xc0000005 (read 0x590, ...) at nvoglv64.dll+0xea4c1a
    [hos host] vk: V-Sync Auto with frame generation at 4x, its frames the renderer's own (DLSS-G does not make them): Auto's mode, as without DLSS-G
    [hos host] vk: swapchain 2558x1360, 6 images, mailbox (V-Sync Auto)
    
  2. At 4x all along, no multiplier change, Reflex on. The game started fullscreen (5120x1440) and went to a window before play (2 draws a frame). The swapchain was remade twice at 1280x720, then the same crash came.

With this PR, no crash: three rounds of 2x → 3x → 4x and back in the menu, in a window: 11 swapchain remakes (4, 5 and 6 images), the game ran on and exited normally.

This PR also held up for the second case. At 4x before play, the game went from fullscreen to a window and back 25 times each (display: fullscreen / display: windowed), with 62 swapchain remakes between 5120x1440 and 1280x720. There was no crash, and the game exited normally. On main, one such switch had been enough.

These are small counts: one crash per case on main, and a timing window. Still, both cases end in the same driver read inside framegen_record, and neither came back with this PR.

Not tested: Linux, AMD and Intel; the DLSS retry part of this PR (no failed feature creation here).

🤖 Generated with Claude Code

@polackangry

polackangry commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

The race in the first part happens in the game: a crash on Windows when stepping the multiplier up, at the copy this PR fixes.

  • main at 51da046 (v2.1.0-rc.3-260-g62c03b1), without this PR; Windows 11 Pro 25H2, RTX 5060 Ti 16 GB, driver 617.42; 3440x1440, DLAA, DLSS Frame Generation (nvngx_dlssg.dll 310.9.16 in ngx\), V-Sync Auto (FIFO).
  • Frame generation 2x → 3x → 4x worked (DLSS-G made 481 of 600 at 2x). 4x → 6x in the Insert menu: the game closed at once, before any swapchain ... 7 images line for 6x.
CRASH: exception 0xc0000005 (read 0x590, guest 0xfffff00000000590) at nvoglv64.dll+0xe80cba
CRASH: host backtrace: nvoglv64.dll+0xe80cba nvoglv64.dll+0xe760bd nvoglv64.dll+0xe13d68 hookshot_vk.dll+0x271ef1 hookshot_vk.dll+0x346225 hookshot_vk.dll+0x32f32e ...
CRASH: thread Presentation Thread

With the build's hookshot_vk.map: hookshot_vk.dll+0x271ef1 is mkwvk::framegen_record +0x1db1, the return from the last vkCmdCopyImage (set->frame[set->made].image, loaded from the set's frame array); +0x346225 is do_present, +0x32f32e present. So the destination was the null image of a set made for 4x (frame[0..3]) while m was already 6: the driver reads 0x590 past a null handle.

So it isn't only a window seen in reading: one step up was enough here.

🤖 Generated with Claude Code

@Joey-Magnus Joey-Magnus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I merged this into current main and ran the DLSS/framegen/present/temporal/HDR checks (run_all.py dlss framegen present temporal hdr upscale: 26 passed, 4 skipped for Vulkan headers). The test_hdr_encode failure on this head was main's own (kEngine's retry, fixed by #2648), so a re-run should clear it.

The framegen fix is right and matters. ensure_size builds the sets for fg.mult, and framegen_record re-read multiplier() afterwards, so a menu step on the display thread in between made the last vkCmdCopyImage target a frame that was never created. Taking fg.mult at gpu/vk/vk_framegen.cpp:791 closes that. Jiro131 and polackangry both reproduced the nvoglv64.dll read at 0x590 in framegen_record on main, and neither saw it with this change.

On DLSS: adding the quality mode to the refused key, clearing refused in release() (vk_dlss.cpp:481) and calling dlss_retry from temporal_reset_failure look correct. The key's mode bits (8-10) don't overlap the preset bits. Note that the release after 30 failed evaluations (:469) fires only once per run of the same error code, which I think is what you want. I found no open PR touching these same lines (#2788 and #2665 change other parts of these files).

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@hckrman101
hckrman101 merged commit 1bf4284 into NotRexed:main Nov 12, 2026
14 checks passed
dazemc added a commit to dazemc/Hookshot-rs that referenced this pull request Nov 12, 2026
…e generation

Port three upstream NVIDIA fixes that the fork did not have:

- 1bf4284 (NotRexed#2658): the quality mode joins the DLSS refusal key, release()
  and the menu's scaling rows clear a refused feature, a failing
  evaluation is logged when its result changes and every 600th time and
  remakes the feature after 30 failures in a row, and frame generation
  reads the multiplier its sets were made for instead of the menu again.
- 6cdee4c: a fixed frame-generation multiplier makes no more frames than
  the display can show (RX_VK_FRAMEGEN_UNCAPPED=1 keeps the old count),
  and the display thread leaves a made frame out when the game's next
  frame is already waiting.
- f3dd448 (NotRexed#2145): switching from scaled DLSS to DLAA waits up to three
  scene frames for the native scene, then uses native AA at the size the
  scene keeps; the engine's native-AA intent is part of feature reuse.

builder/rust/tests/nvidia_parity.rs checks all three game-free, with
compiled fixtures for fixed_frames and the DLAA transition/reuse, and
pins the already-ported Reflex submit IDs, V-Sync Auto and both NGX
runtimes. No NVIDIA hardware was available here.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants