fix(vk): actor motion pairs a model's copies by place, so the Gossip Stones stay whole under frame generation #59

Open
pmwork-personal wants to merge 2 commits from pmwork-personal/Longshot:fix/actor-motion-copies into main
First-time contributor

Why

Under DLSS Frame Generation the four Gossip Stones by the Temple of Time break apart in the made frames: a stone is drawn part way to its neighbour, doubled or smeared. A player reported it (RTX 5080, 4x); it happens on an RTX 5090 at 3x too.

What changed

The stones are one skinned model drawn four times, and actor motion paired each draw with the frame before's by draw order. The game draws the copies in another order from frame to frame, uses two models for them, and a stone that comes into view has no draw in the frame before, so a stone took its neighbour's pose as its own and its motion pointed at the neighbour.

  • Each skinned draw's pose (origin at the object's +0x40, then its bones) is read at the draw with ub_read and staged into the pose buffer, not copied from the game's memory after the frame. A pose that cannot be read falls back to the old copy; RX_VK_DLSS_ACTOR_LIVE=1 forces it for an A/B.
  • Copies of one model pair by place: the origin plus the bones' mean translation (dlss_actor::place_of; the copies share one origin on a grid of 10 units and are placed by their bones). All copies at once, the nearest pairs first, never further than RX_VK_DLSS_ACTOR_REACH world units in one frame (default 1; Link running moves about 0.1, the stones stand 2.3 apart); on a tie at one place the same n-th first (dlss_actor::pair_by_place). A copy without a match gets the camera's motion for that frame.
  • The status line counts the poses read at the draw and those copied later.
  • A draw whose frame before's copies were not read at their draws pairs by its n-th, as before.

Actor motion feeds the whole temporal chain, so FSR 4 and FSR 3.1 get the same motion; with RX_VK_DLSS_ACTORS=0 nothing changes.

Cost, and a follow-up: the pose reads run in temporal_note_skinned_draw while the temporal lock is held, one allocation a draw (up to 13 KB). A follow-up commit here moves them out of the lock, into one arena a frame, after a game run.

Measurements

Before: Hookshot main @ ba3d3f73 (Longshot's base before the rename)
After: the same with this change, #2787's layouts and the close-up fix (the actor-motion close-up PR) @ 06ad86fe (log v2.1.0-rc.3-281-g06ad86fe); this branch is that change on Longshot main @ d4a69456
Hardware: RTX 5090 32 GB, driver 621.24, Ryzen 7 9800X3D, Windows 11 Pro
Scene: Castle Town, the four Gossip Stones by the Temple of Time, running past them while the camera turns; 3840x2160, DLAA, DLSS Frame Generation 3x (NGX), Reflex On + Boost, no frame rate cap
Method: about 30 s a run, judged by eye in play; the numbers from the status lines in the log

Before After
Stones in made frames break apart (with RX_VK_DLSS_ACTORS=0: whole) whole
Poses read at the draw (no counter) all; 0 copied later
Actor motion CPU a frame not noted 0.15-0.28 ms (51-74 skinned draws a frame)
DLSS-G presents 600 of 600 590 of 600 in play
vk: status: 600 frames, the slowest 55.1 ms; ... NVIDIA DLAA: 600 frames drawn, 3 made, jitter +0.438 +0.389 px, actor motion 31096 draws in 598 frames (0.170 ms CPU a frame; poses 31200 read at the draw, 0 copied later)
vk: DLSS Frame Gen (NGX): of the last 600 presents 590 made by DLSS-G (0 told to reset, 59 with the camera still, 0 NGX said not to show); ... 0 refused

One run each, not interleaved: the fault is either there or not, so no median was taken.

Not tested

Not run on Longshot main itself (only the checks below), nor on AMD or Intel GPUs: the actor motion feeds DLSS and FSR alike, so a run with FSR 3.1/4 would help. On this branch: the syntax check of vk_temporal.cpp and vk_draw.cpp passes; python -I -B tests/run_all.py: 488 passed, 37 skipped (Linux only), 4 failed, none from this change: test_portable_builder_startup.py (this PC's .NET packages), test_fpx_sticky.py and test_spawn_emit.py (an a64-kit test cannot find <algorithm> here; both fail on main the same way), test_invalidate_vcore.py (passes when run alone: flaky under 16 jobs); tests/test_dlss_actor.py with the SPIR-V header check (the Khronos glslang 16.6.0) passes.

Screenshots

None yet: the fault shows only in made frames. A before/after clip will follow.

Issues

Upstream: the same change is proposed to NotRexed/Hookshot as https://github.com/NotRexed/Hookshot/pull/2992.

Checklist

For everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description).
  • Rebased on current main.
  • python -I -B tests/run_all.py run and passing (any failure that also fails on main is named above).
  • Tested in the game, or this PR is a draft.
  • Measurements before and after, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. (One run each: a yes/no fault.)
  • No game files, dumps, build output or generated files.
  • third_party/ untouched (a new folder with a PROVENANCE.md instead).
  • AI assistance, if any, recorded with Assisted-by: / Co-Authored-By: trailers.
  • The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled.

If applicable

  • Screenshots for a change that shows on screen. (A clip will follow.)
  • A row in docs/tested-configurations.md for each measured run.
  • A game-free test added for the fix, failing on main and passing here.
  • Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. (Windows built; no Linux build was run.)

🤖 Generated with Claude Code

## Why Under DLSS Frame Generation the four Gossip Stones by the Temple of Time break apart in the made frames: a stone is drawn part way to its neighbour, doubled or smeared. A player reported it (RTX 5080, 4x); it happens on an RTX 5090 at 3x too. ## What changed The stones are one skinned model drawn four times, and actor motion paired each draw with the frame before's by draw order. The game draws the copies in another order from frame to frame, uses two models for them, and a stone that comes into view has no draw in the frame before, so a stone took its neighbour's pose as its own and its motion pointed at the neighbour. - Each skinned draw's pose (origin at the object's +0x40, then its bones) is read at the draw with `ub_read` and staged into the pose buffer, not copied from the game's memory after the frame. A pose that cannot be read falls back to the old copy; `RX_VK_DLSS_ACTOR_LIVE=1` forces it for an A/B. - Copies of one model pair by place: the origin plus the bones' mean translation (`dlss_actor::place_of`; the copies share one origin on a grid of 10 units and are placed by their bones). All copies at once, the nearest pairs first, never further than `RX_VK_DLSS_ACTOR_REACH` world units in one frame (default 1; Link running moves about 0.1, the stones stand 2.3 apart); on a tie at one place the same n-th first (`dlss_actor::pair_by_place`). A copy without a match gets the camera's motion for that frame. - The status line counts the poses read at the draw and those copied later. - A draw whose frame before's copies were not read at their draws pairs by its n-th, as before. Actor motion feeds the whole temporal chain, so FSR 4 and FSR 3.1 get the same motion; with `RX_VK_DLSS_ACTORS=0` nothing changes. Cost, and a follow-up: the pose reads run in `temporal_note_skinned_draw` while the temporal lock is held, one allocation a draw (up to 13 KB). A follow-up commit here moves them out of the lock, into one arena a frame, after a game run. ## Measurements Before: Hookshot main @ ba3d3f73 (Longshot's base before the rename) After: the same with this change, #2787's layouts and the close-up fix (the actor-motion close-up PR) @ 06ad86fe (log `v2.1.0-rc.3-281-g06ad86fe`); this branch is that change on Longshot main @ d4a69456 Hardware: RTX 5090 32 GB, driver 621.24, Ryzen 7 9800X3D, Windows 11 Pro Scene: Castle Town, the four Gossip Stones by the Temple of Time, running past them while the camera turns; 3840x2160, DLAA, DLSS Frame Generation 3x (NGX), Reflex On + Boost, no frame rate cap Method: about 30 s a run, judged by eye in play; the numbers from the status lines in the log | | Before | After | |---|---|---| | Stones in made frames | break apart (with `RX_VK_DLSS_ACTORS=0`: whole) | whole | | Poses read at the draw | (no counter) | all; 0 copied later | | Actor motion CPU a frame | not noted | 0.15-0.28 ms (51-74 skinned draws a frame) | | DLSS-G presents | 600 of 600 | 590 of 600 in play | ``` vk: status: 600 frames, the slowest 55.1 ms; ... NVIDIA DLAA: 600 frames drawn, 3 made, jitter +0.438 +0.389 px, actor motion 31096 draws in 598 frames (0.170 ms CPU a frame; poses 31200 read at the draw, 0 copied later) vk: DLSS Frame Gen (NGX): of the last 600 presents 590 made by DLSS-G (0 told to reset, 59 with the camera still, 0 NGX said not to show); ... 0 refused ``` One run each, not interleaved: the fault is either there or not, so no median was taken. ## Not tested Not run on Longshot main itself (only the checks below), nor on AMD or Intel GPUs: the actor motion feeds DLSS and FSR alike, so a run with FSR 3.1/4 would help. On this branch: the syntax check of `vk_temporal.cpp` and `vk_draw.cpp` passes; `python -I -B tests/run_all.py`: 488 passed, 37 skipped (Linux only), 4 failed, none from this change: `test_portable_builder_startup.py` (this PC's .NET packages), `test_fpx_sticky.py` and `test_spawn_emit.py` (an a64-kit test cannot find `<algorithm>` here; both fail on main the same way), `test_invalidate_vcore.py` (passes when run alone: flaky under 16 jobs); `tests/test_dlss_actor.py` with the SPIR-V header check (the Khronos glslang 16.6.0) passes. ## Screenshots None yet: the fault shows only in made frames. A before/after clip will follow. ## Issues Upstream: the same change is proposed to NotRexed/Hookshot as https://github.com/NotRexed/Hookshot/pull/2992. ## Checklist ### For everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`). - [x] Rebased on current `main`. - [x] `python -I -B tests/run_all.py` run and passing (any failure that also fails on `main` is named above). - [x] Tested in the game, or this PR is a draft. - [ ] Measurements before and after, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. (One run each: a yes/no fault.) - [x] No game files, dumps, build output or generated files. - [x] `third_party/` untouched (a new folder with a `PROVENANCE.md` instead). - [x] AI assistance, if any, recorded with `Assisted-by:` / `Co-Authored-By:` trailers. - [x] The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled. ### If applicable - [ ] Screenshots for a change that shows on screen. (A clip will follow.) - [x] A row in `docs/tested-configurations.md` for each measured run. - [x] A game-free test added for the fix, failing on `main` and passing here. - [x] Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. (Windows built; no Linux build was run.) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Why: under DLSS Frame Generation the Gossip Stones in Hyrule Town broke apart in made frames, each one drawn part way
to its neighbour (a player's report, RTX 5080, 4x; reproduced on an RTX 5090, 3x). Their actor motion came from
another stone.

- The pose (bones and origin) of each skinned draw is read when the draw is recorded (ub_read), not copied from the
  game's buffers after the frame, when an inline uniform update may have changed them. The status line counts the
  poses read at the draw and the poses copied later.
- Copies of one model paired with the frame before's copies by draw order (the n-th draw of the model). The game
  draws the copies in another order from frame to frame, the stones with two models, and a copy that comes into
  view has no frame before. Copies now pair by place: the origin plus the bones' mean translation (the copies share
  one origin on a grid of 10 units and are placed by their bones), all copies of a model at once, the nearest pairs
  first, never further apart than RX_VK_DLSS_ACTOR_REACH world units in one frame (default 1; a running Link moves
  about 0.1 a frame, the stones stand 2.3 apart). On a tie at one place the same n-th pairs first. A draw without a
  pose read at the draw pairs by its n-th, as before.

dlss_actor::place_of and dlss_actor::pair_by_place are checked in tests/test_dlss_actor.cpp.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This pull request has changes conflicting with the target branch.
  • docs/tested-configurations.md
View command line instructions

Manual merge helper

Use this merge commit message when completing the merge manually.

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u 'fix/actor-motion-copies:pmwork-personal-fix/actor-motion-copies'
git switch 'pmwork-personal-fix/actor-motion-copies'
Sign in to join this conversation.
No description provided.