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

Merged
HumanBean merged 7 commits from pmwork-personal/Longshot:fix/actor-motion-copies into main 2026-11-14 11:26:00 +00:00
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.

  • Poses read at the draw (temporal_note_skinned_draw in gpu/vk/vk_temporal.cpp): each skinned draw's pose (origin at the object's +0x40, then its bones) is read at the draw with ub_read, not copied from the game's memory after the frame. The read runs outside the temporal lock, into a per-thread buffer; under the lock the pose is appended to one frame-wide pose_bytes, with no allocation per draw. actor_motion stages the read poses into the pose buffer in one copy. A pose that can't be read falls back to the old copy; RX_VK_DLSS_ACTOR_LIVE=1 forces the old copy for an A/B.
  • Copies of one model pair by place (dlss_actor::place_of, dlss_actor::pair_by_place in gpu/vk/dlss_actor.h): the place is the origin plus the bones' mean translation (the copies share one origin and are placed by their bones). All copies are paired at once, the nearest pairs first. Draw order is kept instead when every pair of it is within the reach and it pairs more copies, or as many with no longer a total distance. A copy without a match gets the camera's motion for that frame. A draw whose pose wasn't read pairs by its n-th, as before, unless that frame-before draw was already taken by place.
  • The reach is a speed (actor_motion): RX_VK_DLSS_ACTOR_REACH world units a second (default 60, 1 a frame at 60 fps, about ten times a running Link), times the time between the two poses, clamped to 1/60-1/30 s. So the reach is at most 2, under the stones' 2.3 spacing. Past that spacing, draw order (which wins whenever it pairs more) would pair a row of stones each with its neighbour after a stone changes model. The cost: across a gap longer than 1/30 s (a hitch, a base under 30 fps), an actor that moved more than 2 units gets only the camera's motion for that frame.
  • The status line counts the poses read at the draw and those copied later. The actor-motion CPU time no longer counts time spent waiting for the lock.
  • Tests (tests/test_dlss_actor.cpp): the stones in and out of order, a stone new in view, a lone copy, two copies passing, two moving fast for their spacing (0.6/0.35 and 0.9/0.5), and a row whose model changed, within a reach of 2 and past the spacing.
  • Docs: the actor-motion switches in docs/development.md's switch reference, and rows in docs/tested-configurations.md.

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.

Flags and settings

Flag What it does Default now Recommendation Helps on Doesn't help on Untested on
RX_VK_DLSS_ACTOR_LIVE=1 (new) Copies each pose from the game's memory at the upscale, as main does, instead of reading it at the draw Off (poses read at the draw) Off: for an A/B only - - AMD, Intel; this head
RX_VK_DLSS_ACTOR_REACH=<units a second> (new) How far a copy of a model may move between two frames and still pair with itself by place; times the gap, clamped to 1/60-1/30 s 60 (at most 2 units) Keep the default: past the stones' 2.3 they cross-pair again Copies of one model drawn in changing order (Gossip Stones) - Any value but the default; this head
RX_VK_DLSS_ACTORS=0 (existing) Actor motion off On Unchanged; turns this change off too - - -

Measurements

The first run below is the author's A/B of this head against main, in the game. The two after it are of earlier versions; since them, the reach became a speed (it was 1 unit a frame), draw order is kept where it pairs as well, and the poses are read outside the lock.

The author's A/B, 2026-11-14

main: cf605c85 (2026-11-14)
this PR: fix/actor-motion-copies @ cc7cb36a, its four commits rebased onto main cf605c85 locally (no conflict, the same diff)
Hardware: RTX 5090 32 GB, driver 621.24, Ryzen 7 9800X3D, 64 GB, Windows 11 Pro
Scene: the four Gossip Stones in front of the Temple of Time, the camera moved back and forth (main's repro), about a minute a run; fullscreen, 5120x2160 output, DLAA, DLSS Frame Generation 2x (NGX), V-Sync Auto (FIFO), Reflex On + Boost, frame pacing on
Method: scripts/dev_packs.py: one base of main cf605c85, this PR as a renderer pack, a runtime each from that base (the same caches and settings; only the renderer differs). Four runs, alternated: this PR, main, this PR, main (docs/tested/2026-11-14-gossip-stones-pmwork-personal-run01.md to run04.md). The stones judged by eye in play; CPU from the status lines' 600-frame blocks, leaving out each run's first two (the scene loading): the median of each run, then the median of the two runs
How to reproduce: stand by the four Gossip Stones in front of the Temple of Time with DLSS Frame Generation on, and move the camera back and forth
Base: the PR branch is on main 5ad0f503; it was measured rebased onto main cf605c85, and the numbers still hold. Between the two, main has 8 commits, and this PR's own added and removed lines are the same on both. The only one of them that touches a file of this PR is Longshot PR 252 (vk_temporal.cpp): it wraps the pipeline creation (make_pipeline, make_actor_pipeline, once at the start) in an Intel driver guard, not the per-frame actor motion.

main @ cf605c85 this PR @ cc7cb36a
Stones in made frames, run 1; run 2 break apart; break apart whole; whole
Actor motion CPU a frame (median of 2) 0.0555 ms (0.055; 0.056) 0.143 ms (0.135; 0.151)
Actor motion CPU a skinned draw (median of 2) 1.085 us (1.09; 1.08) 2.725 us (2.65; 2.80)
Skinned draws a frame 50.5; 51.3 50.5; 53.8
Poses read at the draw / copied later not logged all / 0

So this PR costs about 1.6 us more a skinned draw: about +0.09 ms a frame at the stones (about 50 skinned draws a frame), about +0.25 ms at 150.

vk: status: 600 frames, the slowest 22.2 ms; VRAM 13.7 of 30.7 GB; NVIDIA DLAA: 600 frames drawn, 1 made, jitter +0.438 +0.389 px, actor motion 31020 draws in 600 frames (0.055 ms CPU a frame), its images 263 MB, its own 1433 MB
vk: status: 600 frames, the slowest 31.0 ms; VRAM 13.9 of 30.7 GB; NVIDIA DLAA: 600 frames drawn, 1 made, jitter +0.438 +0.389 px, actor motion 32309 draws in 600 frames (0.151 ms CPU a frame; poses 33528 read at the draw, 0 copied later), its images 263 MB, its own 1433 MB

mooki's run, of 8a091b34 (comment)

main: d4a69456 (2026-11-13)
this PR: fix/actor-motion-copies @ 8a091b34 (the first version on Longshot main d4a69456)
Hardware: RTX 4090 24 GB, NVIDIA 615.78.08, i9-13900K, Arch Linux (kernel 7.2.15)
Scene: child Link, daylight, the four Gossip Stones outside the Temple of Time; 3840x2160, DLSS Transformer (K) Performance, Reflex On + Boost, frame generation off (DLSS-G wasn't available: its Linux library was missing), windowless
Method: native builds of both; scripted left/right passes of 100 game frames, every present captured at 960x540 with RX_VK_DLSS_DEBUG=motion, and frames with the stone signature counted by hand. Two interleaved main/PR pairs, then the PR with RX_VK_DLSS_ACTOR_LIVE=1 and with RX_VK_DLSS_ACTORS=0. CPU from the status lines; the PC was heavily loaded (load averages 38-217), so the CPU figures are diagnostics, not a speed comparison
How to reproduce: as mooki's comment describes

main @ d4a69456 this PR @ 8a091b34 this PR @ 8a091b34, RX_VK_DLSS_ACTOR_LIVE=1
Stone signature frames, run 1 / run 2 0/460; 0/460 0/460; 0/460 2/460
Actor motion CPU a frame (median of 2) 0.0955 ms 0.410 ms 0.112 ms
Actor-motion draws a frame 47.90; 47.42 50.91; 49.25 -
Poses read at the draw / copied later not logged 30714/0; 29634/0 0/28614
vk: status: 600 frames, the slowest 67.0 ms; VRAM 7.4 of 21.9 GB; NVIDIA DLSS: 600 frames drawn, 1 made, jitter +0.469 +0.093 px, actor motion 28740 draws in 600 frames (0.097 ms CPU a frame), its images 199 MB, its own 307 MB
vk: status: 600 frames, the slowest 257.0 ms; VRAM 7.6 of 21.9 GB; NVIDIA DLSS: 600 frames drawn, 1 made, jitter +0.469 +0.093 px, actor motion 30546 draws in 600 frames (0.421 ms CPU a frame; poses 30714 read at the draw, 0 copied later), its images 199 MB, its own 307 MB
vk: status: 600 frames, the slowest 46.5 ms; VRAM 7.4 of 21.9 GB; NVIDIA DLSS: 600 frames drawn, 1 made, jitter +0.438 +0.389 px, actor motion 28588 draws in 600 frames (0.112 ms CPU a frame; poses 0 read at the draw, 28614 copied later), its images 199 MB, its own 307 MB

The author's run, of the first version as a Hookshot build (not Longshot main, so its columns name the builds)

Builds: Hookshot main @ ba3d3f73 (Longshot's base before the rename) against Hookshot @ 06ad86fe (log v2.1.0-rc.3-281-g06ad86fe): the first version of this change on ba3d3f73, with NotRexed/Hookshot PR 2787's layouts and the close-up depth test
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. One run each, not interleaved: the fault is either there or not
How to reproduce: stand by the four Gossip Stones by the Temple of Time with DLSS Frame Generation on, and run past them while turning the camera; compare with RX_VK_DLSS_ACTORS=0

Hookshot main @ ba3d3f73 Hookshot @ 06ad86fe (this change's first version)
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

The 0.41 ms was the first version, with an allocation and the reads under the lock for each draw. Moving the reads out of the lock brought it down: on the author's PC, per skinned draw, the first version cost about 3.3 us (0.170 ms at 52 draws a frame), this head 2.725 us, and main 1.085 us (the A/B above).

Testing

Tested:

  • The author, on this head against main (cc7cb36a's commits on main cf605c85; RTX 5090, 621.24, Windows 11 Pro; DLAA, DLSS FG 2x; 2 alternated runs each): in front of the Temple of Time, the camera moved back and forth: on main the stones break apart in made frames, on this head they stay whole. Details under Measurements.
  • The author, on the first version as Hookshot @ 06ad86fe (RTX 5090, 621.24, Windows 11 Pro; DLAA, FG 3x): the stones whole in made frames, where Hookshot main breaks them. Details under Measurements.
  • Desmond27, on 8a091b34 (main plus this PR only; RTX 5080; DLSS FG 2x, 3x and 4x) (comment): in front of the Temple of Time, the camera moved back and forth (main's repro). No ghosting, smearing or copies of the stones seen.
  • mooki, on 8a091b34 against main d4a69456 (RTX 4090, 615.78.08, Arch Linux; DLSS K Performance, FG off): both build natively on Linux, and all six runs exited without errors. No stone signature on either side by default. With RX_VK_DLSS_ACTOR_LIVE=1 (the old copy, same head), 2/460 frames show it. The poses counter shows every pose read at the draw. mooki notes that this proves the old path's motion fault, not that the generated-frame fault is gone: FG was off, and plain main showed no signature there.
  • Game-free checks: on this head's commits rebased onto cf605c85, tests/test_dlss_actor.py and the 10 checks that read vk_temporal.cpp or dlss_actor.h pass; python -I -B tests/run_all.py: 516 passed, 8 failed, 35 skipped, the same 8 as main cf605c85 on the same PC. tests/test_dlss_actor.py passed by hand on 966c69e1, and mooki ran it on 8a091b34. The author ran python -I -B tests/run_all.py on 8a091b34: 488 passed, 4 failed, none from this change. mooki's suite run on 8a091b34: 505 passed, 4 failed (environment: a .NET pack, two compiler SIGKILLs, a shader batch that passed serially).
  • Not on 10e2b4d1 itself: no tests, builds or game runs were made for 10e2b4d1 on the PC that pushed it (cc7cb36a's commits were run above). Forgejo CI on 10e2b4d1 skips the Python source checks and the SPIR-V fixtures for this PR; the title check, the workflow lint and the third-party notes pass.

Not tested:

  • FSR 3.1 / FSR 4, and AMD or Intel GPUs: actor motion feeds them the same way.
  • A moving crowd of one model with FG off (Cuccos, Keese, guards): the cross-pairing risk the pairing rule guards against.
  • Linux frame generation: mooki's build lacked DLSS-G's Linux library.
  • DLSS FG 3x and 4x on this head on RTX 40/50 (2x is measured above).
  • AMD with FSR 4 or FSR 3.1 at the same spot and with a moving crowd.
  • Alimjan's report that the stones still break up is of the latest Longshot release, which doesn't include this PR, so it is expected.

Needs other testers:
None.

Screenshots

None: the author chose not to attach a clip. The fault shows only in made frames; the A/B by eye, 2 alternated runs on each side, is under Measurements, and Alimjan's clip on this PR shows the fault on the current release.

Risk and rollback

  • Wrong motion on skinned models for every temporal upscaler (DLSS, DLAA, FSR 4, FSR 3.1), with or without frame generation: smear or ghosting on an actor for a frame. Off: RX_VK_DLSS_ACTORS=0 (actor motion off), or RX_VK_DLSS_ACTOR_LIVE=1 (main's pose copy; the pairing by place then isn't used).
  • Across a gap longer than 1/30 s (a hitch, a base under 30 fps), an actor that moved more than 2 units gets only the camera's motion for that frame.
  • A limit, stated in the code: poses are read from what the CPU wrote. A bone palette written on the GPU earlier in the stream would read stale. Nothing found so far does that; RX_VK_DLSS_ACTOR_LIVE=1 avoids it.
  • CPU: a read of up to 13 KB per skinned draw on the recording threads, outside the temporal lock: about 1.6 us more a skinned draw than main on the author's PC (Measurements).
  • Rollback: reverting the commits is enough; nothing persists.

Reviewer notes

Start with dlss_actor::pair_by_place and its tests, then actor_motion (the reach and the pairing) and temporal_note_skinned_draw (the read outside the lock). Whether OoT moves actors by wall time or fixed ticks, which a reach set as a speed assumes, hasn't been checked; the cap at 2 units bounds the reach either way.

Issues

None on Longshot. (The same change was proposed to NotRexed/Hookshot as pull 2992; that repository has returned 404 since 2026-11-13.)

Checklist

For everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description).
  • Rebased on current main. (main @ 5ad0f503; the commits also rebase onto cf605c85 with no conflict)
  • python -I -B tests/run_all.py run and passing (any failure that also fails on main is named above). (On this head's commits rebased onto cf605c85: 516 passed, 8 failed, 35 skipped; main cf605c85 on the same PC fails the same 8: test_fpx_sticky, test_framegen_resources, test_module_ids, test_oot_state_tap, test_pad_layout, test_portable_builder_startup, test_spawn_emit, test_tested_configurations. CI skips the Python checks for this PR.)
  • Tested in the game, or what wasn't is named under Testing.
  • Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests).
  • Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. (The author's A/B of 2026-11-14: main cf605c85 and this head's commits, 2 alternated runs each, the medians under Measurements.)
  • No game files, dumps, build output or generated files.
  • third_party/ untouched (a new folder with a PROVENANCE.md instead).
  • The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled.

If applicable

  • AI assistance recorded with Assisted-by: / Co-Authored-By: trailers.
  • Flags noted, and the switch reference updated (docs/development.md, the README).
  • Screenshots for a change that shows on screen. (No clip, by the author's choice: the A/B by eye is under Measurements.)
  • A row in docs/tested-configurations.md for each measured run. (The 2026-11-14 runs: docs/tested/2026-11-14-gossip-stones-pmwork-personal-run01.md to run04.md.)
  • A game-free test added for the fix, failing on main and passing here. (tests/test_dlss_actor.py with this PR's test on main cf605c85 fails, as main has no dlss_actor::Placed or pair_by_place; on this head's commits rebased onto cf605c85 it passes. Run 2026-11-14.)
  • A check under tests/ that reads changed source updated in this PR, not weakened. (tests/test_dlss_actor.cpp is extended for pair_by_place; the 10 other checks that read vk_temporal.cpp or dlss_actor.h pass unchanged on this head.)
  • Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. (Windows: this head's commits built with llvm-mingw on 2026-11-14. Linux: not built on this head yet; its first version, 8a091b34, built there for mooki.)
  • Docs updated.

🤖 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. - **Poses read at the draw** (`temporal_note_skinned_draw` in `gpu/vk/vk_temporal.cpp`): each skinned draw's pose (origin at the object's +0x40, then its bones) is read at the draw with `ub_read`, not copied from the game's memory after the frame. The read runs outside the temporal lock, into a per-thread buffer; under the lock the pose is appended to one frame-wide `pose_bytes`, with no allocation per draw. `actor_motion` stages the read poses into the pose buffer in one copy. A pose that can't be read falls back to the old copy; `RX_VK_DLSS_ACTOR_LIVE=1` forces the old copy for an A/B. - **Copies of one model pair by place** (`dlss_actor::place_of`, `dlss_actor::pair_by_place` in `gpu/vk/dlss_actor.h`): the place is the origin plus the bones' mean translation (the copies share one origin and are placed by their bones). All copies are paired at once, the nearest pairs first. Draw order is kept instead when every pair of it is within the reach and it pairs more copies, or as many with no longer a total distance. A copy without a match gets the camera's motion for that frame. A draw whose pose wasn't read pairs by its n-th, as before, unless that frame-before draw was already taken by place. - **The reach is a speed** (`actor_motion`): `RX_VK_DLSS_ACTOR_REACH` world units a second (default 60, 1 a frame at 60 fps, about ten times a running Link), times the time between the two poses, clamped to 1/60-1/30 s. So the reach is at most 2, under the stones' 2.3 spacing. Past that spacing, draw order (which wins whenever it pairs more) would pair a row of stones each with its neighbour after a stone changes model. The cost: across a gap longer than 1/30 s (a hitch, a base under 30 fps), an actor that moved more than 2 units gets only the camera's motion for that frame. - **The status line** counts the poses read at the draw and those copied later. The actor-motion CPU time no longer counts time spent waiting for the lock. - **Tests** (`tests/test_dlss_actor.cpp`): the stones in and out of order, a stone new in view, a lone copy, two copies passing, two moving fast for their spacing (0.6/0.35 and 0.9/0.5), and a row whose model changed, within a reach of 2 and past the spacing. - **Docs**: the actor-motion switches in `docs/development.md`'s switch reference, and rows in `docs/tested-configurations.md`. 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. ## Flags and settings | Flag | What it does | Default now | Recommendation | Helps on | Doesn't help on | Untested on | |---|---|---|---|---|---|---| | `RX_VK_DLSS_ACTOR_LIVE=1` (new) | Copies each pose from the game's memory at the upscale, as main does, instead of reading it at the draw | Off (poses read at the draw) | Off: for an A/B only | - | - | AMD, Intel; this head | | `RX_VK_DLSS_ACTOR_REACH=<units a second>` (new) | How far a copy of a model may move between two frames and still pair with itself by place; times the gap, clamped to 1/60-1/30 s | 60 (at most 2 units) | Keep the default: past the stones' 2.3 they cross-pair again | Copies of one model drawn in changing order (Gossip Stones) | - | Any value but the default; this head | | `RX_VK_DLSS_ACTORS=0` (existing) | Actor motion off | On | Unchanged; turns this change off too | - | - | - | ## Measurements The first run below is the author's A/B of this head against main, in the game. The two after it are of earlier versions; since them, the reach became a speed (it was 1 unit a frame), draw order is kept where it pairs as well, and the poses are read outside the lock. **The author's A/B, 2026-11-14** main: cf605c85 (2026-11-14) this PR: fix/actor-motion-copies @ cc7cb36a, its four commits rebased onto main cf605c85 locally (no conflict, the same diff) Hardware: RTX 5090 32 GB, driver 621.24, Ryzen 7 9800X3D, 64 GB, Windows 11 Pro Scene: the four Gossip Stones in front of the Temple of Time, the camera moved back and forth (main's repro), about a minute a run; fullscreen, 5120x2160 output, DLAA, DLSS Frame Generation 2x (NGX), V-Sync Auto (FIFO), Reflex On + Boost, frame pacing on Method: `scripts/dev_packs.py`: one base of main cf605c85, this PR as a renderer pack, a runtime each from that base (the same caches and settings; only the renderer differs). Four runs, alternated: this PR, main, this PR, main (`docs/tested/2026-11-14-gossip-stones-pmwork-personal-run01.md` to `run04.md`). The stones judged by eye in play; CPU from the status lines' 600-frame blocks, leaving out each run's first two (the scene loading): the median of each run, then the median of the two runs How to reproduce: stand by the four Gossip Stones in front of the Temple of Time with DLSS Frame Generation on, and move the camera back and forth Base: the PR branch is on main 5ad0f503; it was measured rebased onto main cf605c85, and the numbers still hold. Between the two, main has 8 commits, and this PR's own added and removed lines are the same on both. The only one of them that touches a file of this PR is [Longshot PR 252](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/252) (`vk_temporal.cpp`): it wraps the pipeline creation (`make_pipeline`, `make_actor_pipeline`, once at the start) in an Intel driver guard, not the per-frame actor motion. | | main @ cf605c85 | this PR @ cc7cb36a | |---|---|---| | Stones in made frames, run 1; run 2 | break apart; break apart | whole; whole | | Actor motion CPU a frame (median of 2) | 0.0555 ms (0.055; 0.056) | 0.143 ms (0.135; 0.151) | | Actor motion CPU a skinned draw (median of 2) | 1.085 us (1.09; 1.08) | 2.725 us (2.65; 2.80) | | Skinned draws a frame | 50.5; 51.3 | 50.5; 53.8 | | Poses read at the draw / copied later | not logged | all / 0 | So this PR costs about 1.6 us more a skinned draw: about +0.09 ms a frame at the stones (about 50 skinned draws a frame), about +0.25 ms at 150. ``` vk: status: 600 frames, the slowest 22.2 ms; VRAM 13.7 of 30.7 GB; NVIDIA DLAA: 600 frames drawn, 1 made, jitter +0.438 +0.389 px, actor motion 31020 draws in 600 frames (0.055 ms CPU a frame), its images 263 MB, its own 1433 MB vk: status: 600 frames, the slowest 31.0 ms; VRAM 13.9 of 30.7 GB; NVIDIA DLAA: 600 frames drawn, 1 made, jitter +0.438 +0.389 px, actor motion 32309 draws in 600 frames (0.151 ms CPU a frame; poses 33528 read at the draw, 0 copied later), its images 263 MB, its own 1433 MB ``` **mooki's run, of 8a091b34 ([comment](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/59))** main: d4a69456 (2026-11-13) this PR: fix/actor-motion-copies @ 8a091b34 (the first version on Longshot main d4a69456) Hardware: RTX 4090 24 GB, NVIDIA 615.78.08, i9-13900K, Arch Linux (kernel 7.2.15) Scene: child Link, daylight, the four Gossip Stones outside the Temple of Time; 3840x2160, DLSS Transformer (K) Performance, Reflex On + Boost, frame generation off (DLSS-G wasn't available: its Linux library was missing), windowless Method: native builds of both; scripted left/right passes of 100 game frames, every present captured at 960x540 with `RX_VK_DLSS_DEBUG=motion`, and frames with the stone signature counted by hand. Two interleaved main/PR pairs, then the PR with `RX_VK_DLSS_ACTOR_LIVE=1` and with `RX_VK_DLSS_ACTORS=0`. CPU from the status lines; the PC was heavily loaded (load averages 38-217), so the CPU figures are diagnostics, not a speed comparison How to reproduce: as mooki's comment describes | | main @ d4a69456 | this PR @ 8a091b34 | this PR @ 8a091b34, `RX_VK_DLSS_ACTOR_LIVE=1` | |---|---|---|---| | Stone signature frames, run 1 / run 2 | 0/460; 0/460 | 0/460; 0/460 | 2/460 | | Actor motion CPU a frame (median of 2) | 0.0955 ms | 0.410 ms | 0.112 ms | | Actor-motion draws a frame | 47.90; 47.42 | 50.91; 49.25 | - | | Poses read at the draw / copied later | not logged | 30714/0; 29634/0 | 0/28614 | ``` vk: status: 600 frames, the slowest 67.0 ms; VRAM 7.4 of 21.9 GB; NVIDIA DLSS: 600 frames drawn, 1 made, jitter +0.469 +0.093 px, actor motion 28740 draws in 600 frames (0.097 ms CPU a frame), its images 199 MB, its own 307 MB vk: status: 600 frames, the slowest 257.0 ms; VRAM 7.6 of 21.9 GB; NVIDIA DLSS: 600 frames drawn, 1 made, jitter +0.469 +0.093 px, actor motion 30546 draws in 600 frames (0.421 ms CPU a frame; poses 30714 read at the draw, 0 copied later), its images 199 MB, its own 307 MB vk: status: 600 frames, the slowest 46.5 ms; VRAM 7.4 of 21.9 GB; NVIDIA DLSS: 600 frames drawn, 1 made, jitter +0.438 +0.389 px, actor motion 28588 draws in 600 frames (0.112 ms CPU a frame; poses 0 read at the draw, 28614 copied later), its images 199 MB, its own 307 MB ``` **The author's run, of the first version as a Hookshot build** (not Longshot main, so its columns name the builds) Builds: Hookshot main @ ba3d3f73 (Longshot's base before the rename) against Hookshot @ 06ad86fe (log `v2.1.0-rc.3-281-g06ad86fe`): the first version of this change on ba3d3f73, with [NotRexed/Hookshot PR 2787](https://github.com/NotRexed/Hookshot/pull/2787)'s layouts and the close-up depth test 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. One run each, not interleaved: the fault is either there or not How to reproduce: stand by the four Gossip Stones by the Temple of Time with DLSS Frame Generation on, and run past them while turning the camera; compare with `RX_VK_DLSS_ACTORS=0` | | Hookshot main @ ba3d3f73 | Hookshot @ 06ad86fe (this change's first version) | |---|---|---| | 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 ``` The 0.41 ms was the first version, with an allocation and the reads under the lock for each draw. Moving the reads out of the lock brought it down: on the author's PC, per skinned draw, the first version cost about 3.3 us (0.170 ms at 52 draws a frame), this head 2.725 us, and main 1.085 us (the A/B above). ## Testing **Tested:** - **The author, on this head against main** (cc7cb36a's commits on main cf605c85; RTX 5090, 621.24, Windows 11 Pro; DLAA, DLSS FG 2x; 2 alternated runs each): in front of the Temple of Time, the camera moved back and forth: on main the stones break apart in made frames, on this head they stay whole. Details under Measurements. - **The author, on the first version as Hookshot @ 06ad86fe** (RTX 5090, 621.24, Windows 11 Pro; DLAA, FG 3x): the stones whole in made frames, where Hookshot main breaks them. Details under Measurements. - **Desmond27, on 8a091b34** (main plus this PR only; RTX 5080; DLSS FG 2x, 3x and 4x) ([comment](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/59)): in front of the Temple of Time, the camera moved back and forth (main's repro). No ghosting, smearing or copies of the stones seen. - **mooki, on 8a091b34 against main d4a69456** (RTX 4090, 615.78.08, Arch Linux; DLSS K Performance, FG off): both build natively on Linux, and all six runs exited without errors. No stone signature on either side by default. With `RX_VK_DLSS_ACTOR_LIVE=1` (the old copy, same head), 2/460 frames show it. The poses counter shows every pose read at the draw. mooki notes that this proves the old path's motion fault, not that the generated-frame fault is gone: FG was off, and plain main showed no signature there. - **Game-free checks:** on this head's commits rebased onto cf605c85, `tests/test_dlss_actor.py` and the 10 checks that read `vk_temporal.cpp` or `dlss_actor.h` pass; `python -I -B tests/run_all.py`: 516 passed, 8 failed, 35 skipped, the same 8 as main cf605c85 on the same PC. `tests/test_dlss_actor.py` passed by hand on 966c69e1, and mooki ran it on 8a091b34. The author ran `python -I -B tests/run_all.py` on 8a091b34: 488 passed, 4 failed, none from this change. mooki's suite run on 8a091b34: 505 passed, 4 failed (environment: a .NET pack, two compiler SIGKILLs, a shader batch that passed serially). - **Not on 10e2b4d1 itself:** no tests, builds or game runs were made for 10e2b4d1 on the PC that pushed it (cc7cb36a's commits were run above). Forgejo CI on 10e2b4d1 skips the Python source checks and the SPIR-V fixtures for this PR; the title check, the workflow lint and the third-party notes pass. **Not tested:** - **FSR 3.1 / FSR 4**, and AMD or Intel GPUs: actor motion feeds them the same way. - **A moving crowd of one model with FG off** (Cuccos, Keese, guards): the cross-pairing risk the pairing rule guards against. - **Linux frame generation:** mooki's build lacked DLSS-G's Linux library. - **DLSS FG 3x and 4x on this head** on RTX 40/50 (2x is measured above). - **AMD with FSR 4 or FSR 3.1** at the same spot and with a moving crowd. - Alimjan's [report](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/59) that the stones still break up is of the latest Longshot release, which doesn't include this PR, so it is expected. **Needs other testers:** None. ## Screenshots None: the author chose not to attach a clip. The fault shows only in made frames; the A/B by eye, 2 alternated runs on each side, is under Measurements, and Alimjan's clip on this PR shows the fault on the current release. ## Risk and rollback - **Wrong motion on skinned models** for every temporal upscaler (DLSS, DLAA, FSR 4, FSR 3.1), with or without frame generation: smear or ghosting on an actor for a frame. Off: `RX_VK_DLSS_ACTORS=0` (actor motion off), or `RX_VK_DLSS_ACTOR_LIVE=1` (main's pose copy; the pairing by place then isn't used). - **Across a gap longer than 1/30 s** (a hitch, a base under 30 fps), an actor that moved more than 2 units gets only the camera's motion for that frame. - **A limit, stated in the code:** poses are read from what the CPU wrote. A bone palette written on the GPU earlier in the stream would read stale. Nothing found so far does that; `RX_VK_DLSS_ACTOR_LIVE=1` avoids it. - **CPU:** a read of up to 13 KB per skinned draw on the recording threads, outside the temporal lock: about 1.6 us more a skinned draw than main on the author's PC (Measurements). - **Rollback:** reverting the commits is enough; nothing persists. ## Reviewer notes Start with `dlss_actor::pair_by_place` and its tests, then `actor_motion` (the reach and the pairing) and `temporal_note_skinned_draw` (the read outside the lock). Whether OoT moves actors by wall time or fixed ticks, which a reach set as a speed assumes, hasn't been checked; the cap at 2 units bounds the reach either way. ## Issues None on Longshot. (The same change was proposed to NotRexed/Hookshot as pull 2992; that repository has returned 404 since 2026-11-13.) ## Checklist ### For everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`). - [x] Rebased on current `main`. (main @ 5ad0f503; the commits also rebase onto cf605c85 with no conflict) - [x] `python -I -B tests/run_all.py` run and passing (any failure that also fails on `main` is named above). (On this head's commits rebased onto cf605c85: 516 passed, 8 failed, 35 skipped; main cf605c85 on the same PC fails the same 8: `test_fpx_sticky`, `test_framegen_resources`, `test_module_ids`, `test_oot_state_tap`, `test_pad_layout`, `test_portable_builder_startup`, `test_spawn_emit`, `test_tested_configurations`. CI skips the Python checks for this PR.) - [x] Tested in the game, or what wasn't is named under Testing. - [x] Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests). - [x] Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. (The author's A/B of 2026-11-14: main cf605c85 and this head's commits, 2 alternated runs each, the medians under Measurements.) - [x] No game files, dumps, build output or generated files. - [x] `third_party/` untouched (a new folder with a `PROVENANCE.md` instead). - [x] The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled. ### If applicable - [x] AI assistance recorded with `Assisted-by:` / `Co-Authored-By:` trailers. - [x] Flags noted, and the switch reference updated (docs/development.md, the README). - [ ] Screenshots for a change that shows on screen. (No clip, by the author's choice: the A/B by eye is under Measurements.) - [x] A row in `docs/tested-configurations.md` for each measured run. (The 2026-11-14 runs: `docs/tested/2026-11-14-gossip-stones-pmwork-personal-run01.md` to `run04.md`.) - [x] A game-free test added for the fix, failing on `main` and passing here. (`tests/test_dlss_actor.py` with this PR's test on main cf605c85 fails, as main has no `dlss_actor::Placed` or `pair_by_place`; on this head's commits rebased onto cf605c85 it passes. Run 2026-11-14.) - [x] A check under `tests/` that reads changed source updated in this PR, not weakened. (`tests/test_dlss_actor.cpp` is extended for `pair_by_place`; the 10 other checks that read `vk_temporal.cpp` or `dlss_actor.h` pass unchanged on this head.) - [ ] Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. (Windows: this head's commits built with llvm-mingw on 2026-11-14. Linux: not built on this head yet; its first version, 8a091b34, built there for mooki.) - [x] Docs updated. 🤖 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]>
Member

Linux result for 8a091b34 against main @ d4a69456: both native builds succeed. The forced-old-path A/B shows the stone motion-vector signature here, but I have not verified the generated-frame fix.

Hardware: RTX 4090 24 GB, NVIDIA 615.78.08, i9-13900K, Arch Linux 7.2.15. Scene: child Link, daylight, the four Gossip Stones outside Temple of Time. Same copied checkpoint; 3840×2160, DLSS Transformer(K), Performance 50%, Reflex On+Boost, frame generation off, windowless. After warming, scripted left/right passes of 100 game frames each; every renderer present captured at 960×540. Two interleaved main/PR pairs, then the PR's LIVE=1 and ACTORS=0 controls.

I used RX_VK_DLSS_DEBUG=motion and the one-frame saturated-green method described in the issue 2761 follow-up. All candidate pictures were inspected: Link, Navi and broad camera motion also trigger the detector, so those are not counted as stones.

main @ d4a69456 this PR @ 8a091b34
Confirmed isolated white-stone signature frames, run 1/run 2 0/460; 0/460 0/460; 0/460
Logged actor CPU ms/frame, selected final 600-frame report 0.097; 0.094 0.421; 0.399
Actor-motion draws/frame in those reports 47.90; 47.42 50.91; 49.25
Poses read at draw / copied later not logged 30714/0; 29634/0

On this PR @ 8a091b34 with RX_VK_DLSS_ACTOR_LIVE=1, 2/460 frames contain white stones: presents 3261 and 3269, with their neighbours inspected. Its selected status report is 0.112 ms/frame, 0 poses read at draw and 28614 copied later. With RX_VK_DLSS_ACTORS=0: 0/460 confirmed signatures and 0 actor draws.

These are diagnostic observations, not a speed comparison. Start/end load averages ranged 38.11–216.51; views and actor counts vary, and the selected status blocks overlap different portions of the passes. The two default-arm counter medians are 0.0955/0.410 ms. Large close-up Link motion obscures part of the first main run; the zero confirmed counts do not prove all motion is correct. Plain main did not give me a confirmed stone signature, so the useful positive result is the author's same-head old-path switch, not a reproduced main→PR visual fix.

[hos Presentation Thread] vk: status: 600 frames, the slowest 67.0 ms; VRAM 7.4 of 21.9 GB; NVIDIA DLSS: 600 frames drawn, 1 made, jitter +0.469 +0.093 px, actor motion 28740 draws in 600 frames (0.097 ms CPU a frame), its images 199 MB, its own 307 MB
[hos Presentation Thread] vk: status: 600 frames, the slowest 257.0 ms; VRAM 7.6 of 21.9 GB; NVIDIA DLSS: 600 frames drawn, 1 made, jitter +0.469 +0.093 px, actor motion 30546 draws in 600 frames (0.421 ms CPU a frame; poses 30714 read at the draw, 0 copied later), its images 199 MB, its own 307 MB
[hos Presentation Thread] vk: status: 600 frames, the slowest 46.5 ms; VRAM 7.4 of 21.9 GB; NVIDIA DLSS: 600 frames drawn, 1 made, jitter +0.438 +0.389 px, actor motion 28588 draws in 600 frames (0.112 ms CPU a frame; poses 0 read at the draw, 28614 copied later), its images 199 MB, its own 307 MB

Here 1 made in the temporal status is the upscaler feature count, not generated frames. All six motion runs exited normally without fatal/device-loss/kernel-fault lines. I cannot attach pictures to this thread from my account; the before, white and after motion-vector frames of both events are kept and I can post them wherever is convenient.

Checks: the actor driver passes on both; glslang 16.6.0 was available for its automatic SPIR-V comparison. Whole head suite: 505 passed, 4 failed, 20 skipped. The missing .NET reference pack also fails on main; two compiler SIGKILLs and a shaderbatch build failure passed on main and on a serial head follow-up. I am not calling the whole suite green.

Initial DLAA/3x Auto startup logged:

vk: DLSS Frame Gen (NGX): not available (0xbad00004): nvngx_dlssg.dll is looked for where DLSS's runtime is (the game folder's ngx first), and it needs an RTX 40 or newer: off

The build had DLSS SR but lacked libnvidia-ngx-dlssg.so.310.9.16; the driver's fallback did not supply FG. The matching official Linux library has since been installed for the test copies, but post-install availability has not been tested. That initial refusal is not a general Linux-FG compatibility verdict.

Not tested: generated frames or 3x output, FSR 3.1/4, normal-view paired ghosting/smear, Windows execution, AMD/Intel. The normal DLAA camera capture was on main only. This result establishes Linux buildability, the pose-path counters and a positive old-path motion diagnostic—not that the original generated-frame artifact is fixed.

Linux result for `8a091b34` against `main @ d4a69456`: both native builds succeed. The forced-old-path A/B shows the stone motion-vector signature here, but I have **not verified the generated-frame fix**. Hardware: RTX 4090 24 GB, NVIDIA 615.78.08, i9-13900K, Arch Linux 7.2.15. Scene: child Link, daylight, the four Gossip Stones outside Temple of Time. Same copied checkpoint; 3840×2160, DLSS Transformer(K), Performance 50%, Reflex On+Boost, frame generation off, windowless. After warming, scripted left/right passes of 100 game frames each; every renderer present captured at 960×540. Two interleaved main/PR pairs, then the PR's LIVE=1 and ACTORS=0 controls. I used `RX_VK_DLSS_DEBUG=motion` and the one-frame saturated-green method described in [the issue 2761 follow-up](https://github.com/NotRexed/Hookshot/issues/2761#issuecomment-6842043843). All candidate pictures were inspected: Link, Navi and broad camera motion also trigger the detector, so those are not counted as stones. | | main @ d4a69456 | this PR @ 8a091b34 | |---|---|---| | Confirmed isolated white-stone signature frames, run 1/run 2 | 0/460; 0/460 | 0/460; 0/460 | | Logged actor CPU ms/frame, selected final 600-frame report | 0.097; 0.094 | 0.421; 0.399 | | Actor-motion draws/frame in those reports | 47.90; 47.42 | 50.91; 49.25 | | Poses read at draw / copied later | not logged | 30714/0; 29634/0 | On **this PR @ 8a091b34 with `RX_VK_DLSS_ACTOR_LIVE=1`**, **2/460** frames contain white stones: presents 3261 and 3269, with their neighbours inspected. Its selected status report is 0.112 ms/frame, 0 poses read at draw and 28614 copied later. With `RX_VK_DLSS_ACTORS=0`: 0/460 confirmed signatures and 0 actor draws. These are diagnostic observations, not a speed comparison. Start/end load averages ranged 38.11–216.51; views and actor counts vary, and the selected status blocks overlap different portions of the passes. The two default-arm counter medians are 0.0955/0.410 ms. Large close-up Link motion obscures part of the first main run; the zero confirmed counts do not prove all motion is correct. Plain main did not give me a confirmed stone signature, so the useful positive result is the author's **same-head old-path switch**, not a reproduced main→PR visual fix. ```text [hos Presentation Thread] vk: status: 600 frames, the slowest 67.0 ms; VRAM 7.4 of 21.9 GB; NVIDIA DLSS: 600 frames drawn, 1 made, jitter +0.469 +0.093 px, actor motion 28740 draws in 600 frames (0.097 ms CPU a frame), its images 199 MB, its own 307 MB [hos Presentation Thread] vk: status: 600 frames, the slowest 257.0 ms; VRAM 7.6 of 21.9 GB; NVIDIA DLSS: 600 frames drawn, 1 made, jitter +0.469 +0.093 px, actor motion 30546 draws in 600 frames (0.421 ms CPU a frame; poses 30714 read at the draw, 0 copied later), its images 199 MB, its own 307 MB [hos Presentation Thread] vk: status: 600 frames, the slowest 46.5 ms; VRAM 7.4 of 21.9 GB; NVIDIA DLSS: 600 frames drawn, 1 made, jitter +0.438 +0.389 px, actor motion 28588 draws in 600 frames (0.112 ms CPU a frame; poses 0 read at the draw, 28614 copied later), its images 199 MB, its own 307 MB ``` Here `1 made` in the temporal status is the upscaler feature count, **not generated frames**. All six motion runs exited normally without fatal/device-loss/kernel-fault lines. I cannot attach pictures to this thread from my account; the before, white and after motion-vector frames of both events are kept and I can post them wherever is convenient. Checks: the actor driver passes on both; glslang 16.6.0 was available for its automatic SPIR-V comparison. Whole head suite: **505 passed, 4 failed, 20 skipped**. The missing .NET reference pack also fails on main; two compiler SIGKILLs and a shaderbatch build failure passed on main and on a serial head follow-up. I am not calling the whole suite green. Initial DLAA/3x Auto startup logged: ```text vk: DLSS Frame Gen (NGX): not available (0xbad00004): nvngx_dlssg.dll is looked for where DLSS's runtime is (the game folder's ngx first), and it needs an RTX 40 or newer: off ``` The build had DLSS SR but lacked `libnvidia-ngx-dlssg.so.310.9.16`; the driver's fallback did not supply FG. The matching official Linux library has since been installed for the test copies, but post-install availability has not been tested. That initial refusal is not a general Linux-FG compatibility verdict. **Not tested:** generated frames or 3x output, FSR 3.1/4, normal-view paired ghosting/smear, Windows execution, AMD/Intel. The normal DLAA camera capture was on main only. This result establishes Linux buildability, the pose-path counters and a positive old-path motion diagnostic—not that the original generated-frame artifact is fixed.
First-time contributor

Tested this in main with only this PR installed:

Tested with in front of the Temple of Time with the 4 Gossip Stones with DLSS FG at FG X2, FG X3, FG X4 on an RTX 5080.
Camera movement was done back and forth (the repro behavior on main) and no additional ghosting, smearing or copies was noticed during camera movement.

Tested this in main with only this PR installed: Tested with in front of the Temple of Time with the 4 Gossip Stones with DLSS FG at FG X2, FG X3, FG X4 on an RTX 5080. Camera movement was done back and forth (the repro behavior on main) and no additional ghosting, smearing or copies was noticed during camera movement.
HumanBean left a comment

Reviewed the code only (no build, no tests run). The two-step fix is sound: reading each draw's pose at the draw with ub_read instead of copying it from the game's memory at the upscale, and pairing a model's copies by place with nearest-first and a reach. The fallbacks are careful: a pose that can't be read, a stage that can't be made, RX_VK_DLSS_ACTOR_LIVE=1, and a frame before with no placed copies all lead back to the old nth path. Staging through a host-visible ring and making one copy after the loop keeps the GPU order. The pair_by_place / place_of tests cover the stone cases well. Without a temporal upscaler nothing changes: temporal_note_skinned_draw returns on !temporal_wanted() before any read, and actor_motion only runs from the temporal path.

Must change

  • Not mergeable: docs/tested-configurations.md conflicts with main. This branch is on d4a69456, which is 61 commits behind main, and main has since added 10 rows at the same place (the top of the Runs table). The code files (vk_temporal.cpp, dlss_actor.h, tests/test_dlss_actor.cpp) aren't touched on main and should merge cleanly, but Rebased on current main is ticked and isn't true. Rebase, put the row back at the top, and re-run the checks.

  • The PR is out of WIP with a For everyone box unticked (Measurements: "one run each"). The screenshots box is unticked too ("a clip will follow"). AGENTS.md says a PR is ready only when every For everyone box is ticked. There is now more to go on: Desmond27's RTX 5080 run at FG x2/x3/x4 on main + this PR, and mooki's two interleaved main/PR pairs on Linux (FG off). Use those to settle the box, or put the PR back to WIP.

  • The description no longer matches the PR.

    • It says "A follow-up commit here moves them [the pose reads] out of the lock, into one arena a frame", but no such commit is here.
    • Not tested says it hasn't been run on Longshot main. Desmond27 has since run it there.
    • The Linux box says no Linux build was run. mooki built it on Linux.

    Update the description with pr update.

  • A bare #2787 in the new docs row, and in the description. It's a GitHub PR, so it needs the full URL: https://github.com/NotRexed/Hookshot/pull/2787.

Should change

  • The reach is absolute, measured per engine frame, and now applies to models drawn only once. On main, a lone Link, Epona, Navi or a boss always paired with its frame before by nth. With this PR, if its place (origin plus the mean of its bone translations) moves more than 1 unit between two engine frames, it gets no actor motion for that frame and only the camera's. That happens on a hitch (the log shows a 55 ms frame), at a low base rate (frame generation is often used from 30-40 fps), or with large fast models whose bone mean sweeps (Volvagia, Ganon, a horse charge). The result is a frame of ghosting on exactly the actors that move most. The lone-stone case (lone_now / lone_then) shows why a lone copy can't simply skip the reach. Better options: scale the reach by the time between the two engine frames (a speed, not a distance), or relate it to the model's spacing and size.

  • Greedy nearest-first can cross-pair copies that move fast relative to their spacing, where nth was right.

    • Example: the frame before has A at 0 and B at 0.6, and both move +0.35. Then A′–B (0.25) is taken first, and B′ falls back to A (0.95, which is within the reach of 1). Both are wrong, and B′ gets a motion of −0.95 instead of +0.35.
    • The same can happen with Cuccos, Keese, Skulltulas or guards whose draw order doesn't change.
    • Fix: keep the same-nth pairing when it is all within the reach and its total distance is no worse than the greedy one, or accept only mutual-nearest pairs. Add a test for this case.
  • Cost. mooki's counters show actor-motion CPU going from about 0.095 to 0.41 ms a frame on an i9-13900K, about 4x main. Most of it is likely in temporal_note_skinned_draw under s.mu:

    • one allocation of up to 13 KB per draw;
    • an ub_read of the bones that, at that size, skips the word cache and walks every one of the stream's uniform updates;
    • memory_read_as_submitted, which takes pools_mu for each page.

    The O(n²) pairing itself is small: at most 512 skins a frame, and the reach prunes the candidates (the worst case is about 65k candidates to sort for 256 copies of one model close together). Either land the promised arena/out-of-lock follow-up, or measure where the time goes and state the cost on a mid-range CPU, since every DLSS and FSR user pays it.

  • Frame generation off isn't a no-op. With DLSS SR, DLAA or FSR and FG off, the poses read at the draw and the place pairing apply too, so they change the motion vectors the upscaler gets. That's intended (the description says so for FSR), but the only FG-off evidence is mooki's run, which showed 0 stone signatures on both sides. A short look at a moving crowd with FG off would cover the cross-pairing risk above.

  • Question: can a bone palette be written on the GPU? ub_read reads the submission's CPU capture plus inline uniform updates. The old path copied from the bound GPU buffer. If any skinned model's palette or object block is written by a buffer copy or compute earlier in the stream, the pose read now would be stale and still count as "read at the draw". Please confirm OoT's palettes are always CPU-written.

Reviewed the code only (no build, no tests run). The two-step fix is sound: reading each draw's pose at the draw with `ub_read` instead of copying it from the game's memory at the upscale, and pairing a model's copies by place with nearest-first and a reach. The fallbacks are careful: a pose that can't be read, a stage that can't be made, `RX_VK_DLSS_ACTOR_LIVE=1`, and a frame before with no placed copies all lead back to the old nth path. Staging through a host-visible ring and making one copy after the loop keeps the GPU order. The `pair_by_place` / `place_of` tests cover the stone cases well. Without a temporal upscaler nothing changes: `temporal_note_skinned_draw` returns on `!temporal_wanted()` before any read, and `actor_motion` only runs from the temporal path. ## Must change - **Not mergeable: `docs/tested-configurations.md` conflicts with main.** This branch is on d4a69456, which is 61 commits behind `main`, and main has since added 10 rows at the same place (the top of the Runs table). The code files (`vk_temporal.cpp`, `dlss_actor.h`, `tests/test_dlss_actor.cpp`) aren't touched on main and should merge cleanly, but *Rebased on current `main`* is ticked and isn't true. Rebase, put the row back at the top, and re-run the checks. - **The PR is out of WIP with a *For everyone* box unticked** (Measurements: "one run each"). The screenshots box is unticked too ("a clip will follow"). AGENTS.md says a PR is ready only when every *For everyone* box is ticked. There is now more to go on: Desmond27's RTX 5080 run at FG x2/x3/x4 on main + this PR, and mooki's two interleaved main/PR pairs on Linux (FG off). Use those to settle the box, or put the PR back to WIP. - **The description no longer matches the PR.** - It says "A follow-up commit here moves them [the pose reads] out of the lock, into one arena a frame", but no such commit is here. - *Not tested* says it hasn't been run on Longshot main. Desmond27 has since run it there. - The Linux box says no Linux build was run. mooki built it on Linux. Update the description with `pr update`. - **A bare `#2787` in the new docs row**, and in the description. It's a GitHub PR, so it needs the full URL: https://github.com/NotRexed/Hookshot/pull/2787. ## Should change - **The reach is absolute, measured per engine frame, and now applies to models drawn only once.** On main, a lone Link, Epona, Navi or a boss always paired with its frame before by nth. With this PR, if its place (origin plus the mean of its bone translations) moves more than 1 unit between two engine frames, it gets no actor motion for that frame and only the camera's. That happens on a hitch (the log shows a 55 ms frame), at a low base rate (frame generation is often used from 30-40 fps), or with large fast models whose bone mean sweeps (Volvagia, Ganon, a horse charge). The result is a frame of ghosting on exactly the actors that move most. The lone-stone case (`lone_now` / `lone_then`) shows why a lone copy can't simply skip the reach. Better options: scale the reach by the time between the two engine frames (a speed, not a distance), or relate it to the model's spacing and size. - **Greedy nearest-first can cross-pair copies that move fast relative to their spacing, where nth was right.** - Example: the frame before has A at 0 and B at 0.6, and both move +0.35. Then A′–B (0.25) is taken first, and B′ falls back to A (0.95, which is within the reach of 1). Both are wrong, and B′ gets a motion of −0.95 instead of +0.35. - The same can happen with Cuccos, Keese, Skulltulas or guards whose draw order doesn't change. - Fix: keep the same-nth pairing when it is all within the reach and its total distance is no worse than the greedy one, or accept only mutual-nearest pairs. Add a test for this case. - **Cost.** mooki's counters show actor-motion CPU going from about 0.095 to 0.41 ms a frame on an i9-13900K, about 4x main. Most of it is likely in `temporal_note_skinned_draw` under `s.mu`: - one allocation of up to 13 KB per draw; - an `ub_read` of the bones that, at that size, skips the word cache and walks every one of the stream's uniform updates; - `memory_read_as_submitted`, which takes `pools_mu` for each page. The O(n²) pairing itself is small: at most 512 skins a frame, and the reach prunes the candidates (the worst case is about 65k candidates to sort for 256 copies of one model close together). Either land the promised arena/out-of-lock follow-up, or measure where the time goes and state the cost on a mid-range CPU, since every DLSS and FSR user pays it. - **Frame generation off isn't a no-op.** With DLSS SR, DLAA or FSR and FG off, the poses read at the draw and the place pairing apply too, so they change the motion vectors the upscaler gets. That's intended (the description says so for FSR), but the only FG-off evidence is mooki's run, which showed 0 stone signatures on both sides. A short look at a moving crowd with FG off would cover the cross-pairing risk above. - **Question: can a bone palette be written on the GPU?** `ub_read` reads the submission's CPU capture plus inline uniform updates. The old path copied from the bound GPU buffer. If any skinned model's palette or object block is written by a buffer copy or compute earlier in the stream, the pose read now would be stale and still count as "read at the draw". Please confirm OoT's palettes are always CPU-written.
@ -14,6 +14,7 @@ otherwise.
| Date | Build | OS / desktop | GPU / driver | CPU / RAM | Settings | Where | Result |
|---|---|---|---|---|---|---|---|
| 2026-11-12 | v2.1.0-rc.3-281-g06ad86fe (source 8d4d275a): main ba3d3f73 + this change (before the review's small fixes), #2787's layouts and the close-up depth test; against v2.1.0-rc.3-271-gba3d3f73, main | Windows 11 Pro, 3840x2160 output | RTX 5090 32 GB, 621.24 | Ryzen 7 9800X3D | DLAA, frame generation 3x by DLSS (NGX), Reflex On + Boost, V-Sync Auto (FIFO) | Castle Town, the four Gossip Stones by the Temple of Time: running past them while the camera turns, about 30 s | Main: the stones break apart in made frames (a stone drawn part way to its neighbour); with `RX_VK_DLSS_ACTORS=0` they stay whole. This change: whole. Every pose read at the draw ("poses N read at the draw, 0 copied later"), 51-74 skinned draws a frame, actor motion 0.15-0.28 ms of CPU a frame, DLSS-G made 590 of 600 presents in play. No device lost |
Owner

Bare #2787: it's a GitHub PR, so please link it by its full URL (https://github.com/NotRexed/Hookshot/pull/2787). This row also conflicts with the 10 rows main has added at the top of the table, so it needs a rebase.

Bare `#2787`: it's a GitHub PR, so please link it by its full URL (https://github.com/NotRexed/Hookshot/pull/2787). This row also conflicts with the 10 rows main has added at the top of the table, so it needs a rebase.
@ -24,0 +59,4 @@
std::vector<int> pairs(now.size(), -1);
std::vector<char> taken(then.size(), 0);
for (const Candidate& c : candidates)
if (pairs[c.i] < 0 && !taken[c.j]) pairs[c.i] = c.j, taken[c.j] = 1;
Owner

Greedy nearest-first can cross-pair two copies that move fast relative to their spacing, where nth would have been right. Example: the frame before has A at 0 and B at 0.6, and both move +0.35. A'-B (0.25) is taken first, then B' falls back to A (0.95, within the reach of 1), so B' gets a motion of -0.95 instead of +0.35. Suggest keeping the same-nth assignment when it is all within the reach and its total distance is no worse than the greedy one (or accepting only mutual-nearest pairs), with a test for this case.

Greedy nearest-first can cross-pair two copies that move fast relative to their spacing, where nth would have been right. Example: the frame before has A at 0 and B at 0.6, and both move +0.35. A'-B (0.25) is taken first, then B' falls back to A (0.95, within the reach of 1), so B' gets a motion of -0.95 instead of +0.35. Suggest keeping the same-nth assignment when it is all within the reach and its total distance is no worse than the greedy one (or accepting only mutual-nearest pairs), with a test for this case.
@ -522,0 +544,4 @@
// first, never across more than RX_VK_DLSS_ACTOR_REACH world units between two engine frames (default 1: a running Link
// moves about 0.1 a frame, the Gossip Stones stand 2.3 apart; after a long frame a fast actor can go further and keeps the
// camera's motion for that frame): dlss_actor::pair_by_place. Else the same n-th draw of the model as before)
static const float reach = [] {
Owner

The reach is a distance per engine frame, and it now applies to models drawn only once too (Link, Epona, Navi, bosses), which main always paired by nth. On a hitch, at a low base rate, or for a big model whose bone mean sweeps, a lone actor loses its actor motion for that frame and gets only the camera's. Could the reach be a speed (scaled by the time between the two engine frames), or relative to the model's size?

The reach is a distance per engine frame, and it now applies to models drawn only once too (Link, Epona, Navi, bosses), which main always paired by nth. On a hitch, at a low base rate, or for a big model whose bone mean sweeps, a lone actor loses its actor motion for that frame and gets only the camera's. Could the reach be a speed (scaled by the time between the two engine frames), or relative to the model's size?
@ -541,0 +620,4 @@
// draws too, else its n-th, as before)
auto it = old.end();
const auto m = placed ? models.find(k.key >> 8) : models.end();
if (m == models.end() || m->second.then.empty()) it = old.find(k.key);
Owner

Nit: a draw whose pose couldn't be read falls back to old.find(k.key) by nth, and that then-copy may already be paired by place with another draw this frame. Two draws then share one frame-before pose. It's rare, but you could skip then-keys that pair_by_place has already taken.

Nit: a draw whose pose couldn't be read falls back to old.find(k.key) by nth, and that then-copy may already be paired by place with another draw this frame. Two draws then share one frame-before pose. It's rare, but you could skip then-keys that pair_by_place has already taken.
@ -815,0 +907,4 @@
// frame's or another draw's (the place pair_by_place needs, too). RX_VK_DLSS_ACTOR_LIVE=1: as before, for an A/B)
static const bool live = env_switch("RX_VK_DLSS_ACTOR_LIVE") == 1;
if (!live) {
d.pose.resize(16 + (size_t)d.nbones * 48);
Owner

This runs under s.mu for every skinned draw: an allocation of up to 13 KB, and an ub_read that at this size walks all of the stream's uniform updates and takes pools_mu for each page. mooki's counters show actor-motion CPU going from about 0.095 to 0.41 ms a frame (13900K). The description promises a follow-up commit that moves this out of the lock into one arena a frame, but it isn't in the PR. Also: ub_read reads the CPU capture, so a palette written on the GPU earlier in the stream would come back stale here while the old GPU copy was right. Can you confirm OoT never does that?

This runs under s.mu for every skinned draw: an allocation of up to 13 KB, and an ub_read that at this size walks all of the stream's uniform updates and takes pools_mu for each page. mooki's counters show actor-motion CPU going from about 0.095 to 0.41 ms a frame (13900K). The description promises a follow-up commit that moves this out of the lock into one arena a frame, but it isn't in the PR. Also: ub_read reads the CPU capture, so a palette written on the GPU earlier in the stream would come back stale here while the old GPU copy was right. Can you confirm OoT never does that?
HumanBean added this to the v3.0.0 milestone 2026-11-13 16:59:55 +00:00
HumanBean force-pushed fix/actor-motion-copies from 8a091b34d7 to c2c5fa4364
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
PR description / Description follows the template (pull_request_target) Failing after 12s
CI / Conventional Commit title (pull_request) Successful in 8s
CI / Workflow lint (actionlint) (pull_request) Successful in 9s
Third-party parts / As their notes say (pull_request) Successful in 9s
2026-11-13 17:08:11 +00:00
Compare
HumanBean left a comment

Re-review at c2c5fa43. I read the code only: no build, no tests, no game run.

c2c5fa43 deals with most of the code points from the first review, and the move out of the lock is done carefully. The PR still can't merge as is. Nobody has run the head in the game, the Measurements box is unticked, and the description still describes the first version.

Earlier points: where they stand

  • Rebase / docs conflict: resolved. The branch is on current main@origin (2e1e2e8c), and Forgejo reports it mergeable.
  • Bare #2787 in the docs row: resolved (it is now a full URL). Still open in the description: "After: the same with this change, #2787's layouts…".
  • Reach as a distance per frame: resolved. It is now RX_VK_DLSS_ACTOR_REACH world units a second (default 60) times the time between the two poses, clamped to 1/60 s to 1/4 s.
    • poses_time is set on every actor_motion that gets past the early return, and drop_all resets it.
    • A long gap with no skins doesn't matter: s.poses was emptied on that early return, so there is nothing to pair with.
    • Units and the clamp are right. The trade-off below a 26 fps base, where a lone stone can take its neighbour's place again, is stated in the comment and is acceptable.
  • Greedy cross-pairing: partly resolved. Draw order is now kept when it is as good, and the 0.6/0.35 case has a test. One case is still wrong (see below).
  • Nit, a fallback sharing a then-pose: resolved (taken_by_place).
  • Cost / out of the lock: resolved in code, not measured. ub_read now runs into a thread_local buffer before the lock is taken.
    • Under the lock, the pose is appended to s.pose_bytes, and its offset is taken in the same critical section. actor_motion reads pose_bytes under s.mu, because temporal_upscale holds it.
    • So an offset can't go stale: the reset at the start of a frame (if (s.skins.empty()) s.pose_bytes.clear()), the 512-skin clear and drop_all all empty skins and pose_bytes together, under the lock. Growth only reallocates, and offsets are indices, not pointers.
    • Two threads appending at once are serialised by the lock. The per-thread buffer is only used between its own resize and the copy.
    • ub_read uses thread-local state and is already called from worker threads without s.mu, so moving it out of the lock adds no new race.
    • The per-draw allocation is gone. Nobody has measured the new cost: mooki's 0.41 ms was for 8a091b34.
  • Frame generation off isn't a no-op: still open. There is no FG-off run of the head.
  • GPU-written palettes: still open. The question is now a code comment ("An assumption, open with the author") rather than an answer.
  • Description out of date: still open. It is unchanged since 8a091b34 (see below).

Must change

  • Back to WIP until the head is run and measured. The only game runs are of earlier versions: the author's Hookshot build 06ad86fe (whose docs row says "before the review's small fixes"), mooki's and Desmond27's of 8a091b34. c2c5fa43 changes how the reach works (1 unit a frame became 60 a second), how pairs are chosen and where the poses are read. It needs at least the stones run again with frame generation on, plus the status line's actor-motion CPU against main, as several interleaved runs. That settles the Measurements box and shows the cost the out-of-lock change was for. The PR isn't WIP, but Measurements is unticked, so AGENTS.md says it should be WIP (pr wip 59).
  • The description is the 8a091b34 PR, not this one. Update it with pr update:
    • It still says "never further than RX_VK_DLSS_ACTOR_REACH world units in one frame (default 1…)". It is now 60 a second.
    • It still promises "a follow-up commit here moves them out of the lock, into one arena a frame". That is now done, in a different form.
    • Not tested still says it hasn't been run on Longshot main. Desmond27 has run it there, on an RTX 5080 at FG 2x/3x/4x.
    • The Linux box still says no Linux build was run. mooki built it.
    • It has a bare #2787. It should be https://github.com/NotRexed/Hookshot/pull/2787.
  • The bone-palette assumption must be settled, not merged as an open question. The comment in temporal_note_skinned_draw says "(An assumption, open with the author: …)".
    • If the author confirms that OoT writes its palettes and object blocks on the CPU only, reword the comment as a fact, with how it was checked.
    • If they can't confirm it, keep the GPU copy for any draw whose bones fall in a range the stream wrote on the GPU.

Should change

  • pair_by_place still prefers greedy when greedy pairs fewer copies (dlss_actor.h, order_n >= nearest_n && order_total <= nearest_total).
    • Example, with a reach of 1: the frame before has A at 0 (n-th 0) and B at 0.9 (n-th 1), and both move +0.5. Greedy takes A′–B (0.4), and then B′–B is blocked while B′–A (1.4) is out of reach. That gives 1 pair with a total of 0.4.
    • Draw order gives 2 pairs with a total of 1.0, all within reach, and it is right.
    • Because 1.0 > 0.4, greedy wins. A′ gets −0.4 instead of +0.5, and B′ gets only the camera's motion. Main's n-th pairing had both right.
    • A total distance over more pairs isn't comparable with one over fewer. Prefer draw order when order_n > nearest_n, or when order_n == nearest_n and its total is no longer. Add this case to tests/test_dlss_actor.cpp.
  • The CPU-time counter now includes lock waits. t0 in temporal_note_skinned_draw is taken before the first s.mu, so time a recording thread spends blocked behind temporal_upscale (which holds s.mu through the whole evaluate) counts as actor-motion CPU. That inflates the very number used to compare the cost. Time the unlocked read separately, or start t0 after the lock and add the read's own time.
  • The docs row sits at the top of a newest-first table, dated 2026-11-12, above ten 2026-11-13 rows. It also says "this change", which means nothing once it is in main; name the PR. The row records the Hookshot build, not this head, so add a row when the head is measured.

Not checked

  • No game run, build or tests.
  • Whether OoT's simulation moves actors by wall time (which is what a speed-based reach assumes) or by fixed ticks that slow down under load. With fixed ticks, a hitch widens the reach without the actors having moved further.
  • Whether the 6-entry pose_stages ring outlives every frame in flight with frame generation on. It matches the cameras' ring, which makes the same assumption.
Re-review at c2c5fa43. I read the code only: no build, no tests, no game run. c2c5fa43 deals with most of the code points from the first review, and the move out of the lock is done carefully. The PR still can't merge as is. Nobody has run the head in the game, the Measurements box is unticked, and the description still describes the first version. ## Earlier points: where they stand - **Rebase / docs conflict: resolved.** The branch is on current `main@origin` (2e1e2e8c), and Forgejo reports it mergeable. - **Bare `#2787` in the docs row: resolved** (it is now a full URL). **Still open in the description**: "After: the same with this change, #2787's layouts…". - **Reach as a distance per frame: resolved.** It is now `RX_VK_DLSS_ACTOR_REACH` world units a second (default 60) times the time between the two poses, clamped to 1/60 s to 1/4 s. - `poses_time` is set on every `actor_motion` that gets past the early return, and `drop_all` resets it. - A long gap with no skins doesn't matter: `s.poses` was emptied on that early return, so there is nothing to pair with. - Units and the clamp are right. The trade-off below a 26 fps base, where a lone stone can take its neighbour's place again, is stated in the comment and is acceptable. - **Greedy cross-pairing: partly resolved.** Draw order is now kept when it is as good, and the 0.6/0.35 case has a test. One case is still wrong (see below). - **Nit, a fallback sharing a then-pose: resolved** (`taken_by_place`). - **Cost / out of the lock: resolved in code, not measured.** `ub_read` now runs into a `thread_local` buffer before the lock is taken. - Under the lock, the pose is appended to `s.pose_bytes`, and its offset is taken in the same critical section. `actor_motion` reads `pose_bytes` under `s.mu`, because `temporal_upscale` holds it. - So an offset can't go stale: the reset at the start of a frame (`if (s.skins.empty()) s.pose_bytes.clear()`), the 512-skin clear and `drop_all` all empty `skins` and `pose_bytes` together, under the lock. Growth only reallocates, and offsets are indices, not pointers. - Two threads appending at once are serialised by the lock. The per-thread buffer is only used between its own `resize` and the copy. - `ub_read` uses thread-local state and is already called from worker threads without `s.mu`, so moving it out of the lock adds no new race. - The per-draw allocation is gone. Nobody has measured the new cost: mooki's 0.41 ms was for 8a091b34. - **Frame generation off isn't a no-op: still open.** There is no FG-off run of the head. - **GPU-written palettes: still open.** The question is now a code comment ("An assumption, open with the author") rather than an answer. - **Description out of date: still open.** It is unchanged since 8a091b34 (see below). ## Must change - **Back to WIP until the head is run and measured.** The only game runs are of earlier versions: the author's Hookshot build 06ad86fe (whose docs row says "before the review's small fixes"), mooki's and Desmond27's of 8a091b34. c2c5fa43 changes how the reach works (1 unit a frame became 60 a second), how pairs are chosen and where the poses are read. It needs at least the stones run again with frame generation on, plus the status line's actor-motion CPU against main, as several interleaved runs. That settles the Measurements box and shows the cost the out-of-lock change was for. The PR isn't WIP, but *Measurements* is unticked, so AGENTS.md says it should be WIP (`pr wip 59`). - **The description is the 8a091b34 PR, not this one.** Update it with `pr update`: - It still says "never further than `RX_VK_DLSS_ACTOR_REACH` world units in one frame (default 1…)". It is now 60 a second. - It still promises "a follow-up commit here moves them out of the lock, into one arena a frame". That is now done, in a different form. - *Not tested* still says it hasn't been run on Longshot main. Desmond27 has run it there, on an RTX 5080 at FG 2x/3x/4x. - The Linux box still says no Linux build was run. mooki built it. - It has a bare `#2787`. It should be https://github.com/NotRexed/Hookshot/pull/2787. - **The bone-palette assumption must be settled, not merged as an open question.** The comment in `temporal_note_skinned_draw` says "(An assumption, open with the author: …)". - If the author confirms that OoT writes its palettes and object blocks on the CPU only, reword the comment as a fact, with how it was checked. - If they can't confirm it, keep the GPU copy for any draw whose bones fall in a range the stream wrote on the GPU. ## Should change - **`pair_by_place` still prefers greedy when greedy pairs *fewer* copies** (`dlss_actor.h`, `order_n >= nearest_n && order_total <= nearest_total`). - Example, with a reach of 1: the frame before has A at 0 (n-th 0) and B at 0.9 (n-th 1), and both move +0.5. Greedy takes A′–B (0.4), and then B′–B is blocked while B′–A (1.4) is out of reach. That gives 1 pair with a total of 0.4. - Draw order gives 2 pairs with a total of 1.0, all within reach, and it is right. - Because 1.0 > 0.4, greedy wins. A′ gets −0.4 instead of +0.5, and B′ gets only the camera's motion. Main's n-th pairing had both right. - A total distance over more pairs isn't comparable with one over fewer. Prefer draw order when `order_n > nearest_n`, or when `order_n == nearest_n` and its total is no longer. Add this case to `tests/test_dlss_actor.cpp`. - **The CPU-time counter now includes lock waits.** `t0` in `temporal_note_skinned_draw` is taken before the first `s.mu`, so time a recording thread spends blocked behind `temporal_upscale` (which holds `s.mu` through the whole evaluate) counts as actor-motion CPU. That inflates the very number used to compare the cost. Time the unlocked read separately, or start `t0` after the lock and add the read's own time. - **The docs row** sits at the top of a newest-first table, dated 2026-11-12, above ten 2026-11-13 rows. It also says "this change", which means nothing once it is in `main`; name the PR. The row records the Hookshot build, not this head, so add a row when the head is measured. ## Not checked - No game run, build or tests. - Whether OoT's simulation moves actors by wall time (which is what a speed-based reach assumes) or by fixed ticks that slow down under load. With fixed ticks, a hitch widens the reach without the actors having moved further. - Whether the 6-entry `pose_stages` ring outlives every frame in flight with frame generation on. It matches the cameras' ring, which makes the same assumption.
@ -14,6 +14,7 @@ otherwise.
| Date | Build | OS / desktop | GPU / driver | CPU / RAM | Settings | Where | Result |
|---|---|---|---|---|---|---|---|
| 2026-11-12 | v2.1.0-rc.3-281-g06ad86fe (source 8d4d275a): main ba3d3f73 + this change (before the review's small fixes), [NotRexed/Hookshot PR 2787](https://github.com/NotRexed/Hookshot/pull/2787)'s layouts and the close-up depth test; against v2.1.0-rc.3-271-gba3d3f73, main | Windows 11 Pro, 3840x2160 output | RTX 5090 32 GB, 621.24 | Ryzen 7 9800X3D | DLAA, frame generation 3x by DLSS (NGX), Reflex On + Boost, V-Sync Auto (FIFO) | Castle Town, the four Gossip Stones by the Temple of Time: running past them while the camera turns, about 30 s | Main: the stones break apart in made frames (a stone drawn part way to its neighbour); with `RX_VK_DLSS_ACTORS=0` they stay whole. This change: whole. Every pose read at the draw ("poses N read at the draw, 0 copied later"), 51-74 skinned draws a frame, actor motion 0.15-0.28 ms of CPU a frame, DLSS-G made 590 of 600 presents in play. No device lost |
Owner

The table is newest first, and this 2026-11-12 row sits above ten 2026-11-13 rows. "This change" also means nothing once it is in main: name the PR (Longshot PR 59). The row records the Hookshot build before the review fixes. Add a row for the head once it is measured.

The table is newest first, and this 2026-11-12 row sits above ten 2026-11-13 rows. "This change" also means nothing once it is in main: name the PR (Longshot PR 59). The row records the Hookshot build before the review fixes. Add a row for the head once it is measured.
@ -24,0 +88,4 @@
if (used[j] || !(d <= (double)reach)) return pairs; // (not within the reach: nearest first; NaN fails here too)
by_order[i] = j, used[j] = 1, order_n++, order_total += d;
}
if (order_n >= nearest_n && order_total <= nearest_total + 1e-6) return by_order;
Owner

When greedy pairs fewer copies, its total is over fewer pairs and isn't comparable. Example, with a reach of 1: the frame before has A at 0 (n-th 0) and B at 0.9 (n-th 1), and both move +0.5. Greedy takes A'-B (0.4), then B'-B is blocked and B'-A (1.4) is out of reach: 1 pair, total 0.4. Draw order gives 2 pairs, total 1.0, all within reach, and it is right, but it loses here. A' then gets -0.4 instead of +0.5, and B' gets only the camera's motion (main had both right). Suggest order_n > nearest_n || (order_n == nearest_n && order_total <= nearest_total + 1e-6), with this case added to the test.

When greedy pairs fewer copies, its total is over fewer pairs and isn't comparable. Example, with a reach of 1: the frame before has A at 0 (n-th 0) and B at 0.9 (n-th 1), and both move +0.5. Greedy takes A'-B (0.4), then B'-B is blocked and B'-A (1.4) is out of reach: 1 pair, total 0.4. Draw order gives 2 pairs, total 1.0, all within reach, and it is right, but it loses here. A' then gets -0.4 instead of +0.5, and B' gets only the camera's motion (main had both right). Suggest `order_n > nearest_n || (order_n == nearest_n && order_total <= nearest_total + 1e-6)`, with this case added to the test.
@ -828,28 +945,52 @@ void temporal_note_skinned_draw(const SkinnedDraw& k) {
static const bool off = env_switch("RX_VK_DLSS_ACTORS") == 0;
if (off || !temporal_wanted() || !k.depth || !k.bones || k.bones_size < 48 || !k.object || k.object_size < 0x4c) return;
State& s = st();
const std::chrono::steady_clock::time_point t0 = std::chrono::steady_clock::now();
Owner

t0 is taken before the first s.mu, so a recording thread blocked behind temporal_upscale (which holds s.mu through the evaluate) counts its wait as actor-motion CPU. That inflates the cost the status line reports, which is exactly what this change needs to show went down. Time the unlocked read on its own and add it once the lock is held.

`t0` is taken before the first `s.mu`, so a recording thread blocked behind `temporal_upscale` (which holds `s.mu` through the evaluate) counts its wait as actor-motion CPU. That inflates the cost the status line reports, which is exactly what this change needs to show went down. Time the unlocked read on its own and add it once the lock is held.
@ -831,0 +965,4 @@
// frame's or another draw's (the place pair_by_place needs, too). RX_VK_DLSS_ACTOR_LIVE=1: as before, for an A/B.
// Read without the lock, into this thread's own buffer (its room kept from draw to draw); the lock is taken only to add it
// to the frame's poses. ub_read is this recording thread's view of the stream, as the draw's own reads of its uniforms.
// (An assumption, open with the author: OoT writes its bone palettes and object blocks on the CPU only. ub_read sees the
Owner

This shouldn't merge as an open question. If OoT's palettes and object blocks are CPU-written only, say so as a fact and how it was checked. If that can't be confirmed, keep the GPU copy for a draw whose bones lie in a range the stream wrote on the GPU.

This shouldn't merge as an open question. If OoT's palettes and object blocks are CPU-written only, say so as a fact and how it was checked. If that can't be confirmed, keep the GPU copy for a draw whose bones lie in a range the stream wrote on the GPU.
Owner

I rebased this onto main @ 2e1e2e8c and addressed my first review in one commit of mine on top of your two. It's pushed as 966c69e1, and your commits are unchanged apart from the rebase.

Rebase. The only conflict was docs/tested-configurations.md. All of main's rows are kept, and so is yours.

My commit, "actor motion's reach is a speed, draw order kept where it pairs as well, poses read outside the lock":

  • The reach is now a speed. RX_VK_DLSS_ACTOR_REACH is world units a second, default 60 (1 unit a frame at 60 fps), times the time between the two poses, clamped to 1/60–1/4 s. A model drawn once (Link, Epona, a boss) keeps its motion through a hitch or at a low frame rate. The trade-off is in the comment: below a 26 fps base, a lone stone can take its neighbour's place for a frame.

  • Draw order is kept when it pairs as well. pair_by_place keeps the draw-order pairing when every pair is within the reach, and it either pairs more copies, or pairs as many with no longer a total distance. The re-review caught that my first version also compared totals when draw order paired more; that is fixed in this push. New cases in tests/test_dlss_actor.cpp:

    • copies 0.6 apart, both moving +0.35;
    • copies 0.9 apart, both moving +0.5.

    Separately, a draw whose pose wasn't read no longer takes a copy that place pairing already gave to another draw.

  • Poses are read outside the temporal lock. Each draw reads into a reused per-thread buffer. The lock is taken only to look up the bone count and to append to one frame-wide pose_bytes, which Skin::pose now indexes. There's no allocation per draw.

  • The actor-motion CPU counter no longer counts time spent waiting for the lock.

  • The bone-palette assumption is now a comment at the read: OoT writes the palettes and object blocks on the CPU only. Please confirm it, or say if any are written on the GPU.

  • #2787 in the docs row is now a full URL.

tests/test_dlss_actor.py passes. I didn't run the suite or the game.

Still yours, before it's ready:

  • Measurements. Your earlier runs, Desmond27's and mooki's were all on 8a091b34. This head changes the reach, the pairing and where poses are read, so it needs:

    • a stones run with frame generation on;
    • actor-motion CPU against main. mooki measured 0.095 ms on main against 0.41 ms with the old code.

    Until then, please put it back to WIP (pr wip 59).

  • Description:

    • The reach is now 60 a second, not 1 a frame.
    • The promised out-of-lock follow-up is now my commit.
    • Add Desmond27's RTX 5080 frame-generation run and mooki's Linux builds and runs.
    • #2787 becomes a full URL.
  • The docs row: dated 2026-11-12 but sitting above the 2026-11-13 rows, and it says "this change" rather than naming the PR.

I rebased this onto main @ 2e1e2e8c and addressed my first review in one commit of mine on top of your two. It's pushed as 966c69e1, and your commits are unchanged apart from the rebase. **Rebase.** The only conflict was `docs/tested-configurations.md`. All of main's rows are kept, and so is yours. **My commit**, "actor motion's reach is a speed, draw order kept where it pairs as well, poses read outside the lock": - **The reach is now a speed.** `RX_VK_DLSS_ACTOR_REACH` is world units a second, default 60 (1 unit a frame at 60 fps), times the time between the two poses, clamped to 1/60–1/4 s. A model drawn once (Link, Epona, a boss) keeps its motion through a hitch or at a low frame rate. The trade-off is in the comment: below a 26 fps base, a lone stone can take its neighbour's place for a frame. - **Draw order is kept when it pairs as well.** `pair_by_place` keeps the draw-order pairing when every pair is within the reach, and it either pairs more copies, or pairs as many with no longer a total distance. The re-review caught that my first version also compared totals when draw order paired *more*; that is fixed in this push. New cases in `tests/test_dlss_actor.cpp`: - copies 0.6 apart, both moving +0.35; - copies 0.9 apart, both moving +0.5. Separately, a draw whose pose wasn't read no longer takes a copy that place pairing already gave to another draw. - **Poses are read outside the temporal lock.** Each draw reads into a reused per-thread buffer. The lock is taken only to look up the bone count and to append to one frame-wide `pose_bytes`, which `Skin::pose` now indexes. There's no allocation per draw. - **The actor-motion CPU counter** no longer counts time spent waiting for the lock. - **The bone-palette assumption** is now a comment at the read: OoT writes the palettes and object blocks on the CPU only. **Please confirm it**, or say if any are written on the GPU. - **`#2787`** in the docs row is now a full URL. `tests/test_dlss_actor.py` passes. I didn't run the suite or the game. **Still yours, before it's ready:** - **Measurements.** Your earlier runs, Desmond27's and mooki's were all on 8a091b34. This head changes the reach, the pairing and where poses are read, so it needs: - a stones run with frame generation on; - actor-motion CPU against main. mooki measured 0.095 ms on main against 0.41 ms with the old code. Until then, please put it back to WIP (`pr wip 59`). - **Description:** - The reach is now 60 a second, not 1 a frame. - The promised out-of-lock follow-up is now my commit. - Add Desmond27's RTX 5080 frame-generation run and mooki's Linux builds and runs. - `#2787` becomes a full URL. - **The docs row:** dated 2026-11-12 but sitting above the 2026-11-13 rows, and it says "this change" rather than naming the PR.
HumanBean force-pushed fix/actor-motion-copies from c2c5fa4364
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
PR description / Description follows the template (pull_request_target) Failing after 12s
CI / Conventional Commit title (pull_request) Successful in 8s
CI / Workflow lint (actionlint) (pull_request) Successful in 9s
Third-party parts / As their notes say (pull_request) Successful in 9s
to 966c69e139
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
PR description / Description follows the template (pull_request_target) Failing after 6s
CI / Conventional Commit title (pull_request) Successful in 7s
CI / Workflow lint (actionlint) (pull_request) Successful in 9s
Third-party parts / As their notes say (pull_request) Successful in 10s
2026-11-13 17:14:34 +00:00
Compare
First-time contributor

Hi @HumanBean, this problem was resolved via same PR in the Hookshot repo, but the issue still seems to persist in the latest Longshot release. I think we should look into it further. @Desmond27, sorry to bother you again you're amazing!

Hi @HumanBean, this problem was resolved via same PR in the Hookshot repo, but the issue still seems to persist in the latest Longshot release. I think we should look into it further. @Desmond27, sorry to bother you again you're amazing! <video src="/attachments/c4d435b0-660a-42c5-8575-c0949a04b370" title="gossip_stone_flicker" controls></video>
HumanBean left a comment

Third review, of the PR at 966c69e1. I read the code only: no build, no tests, no game run.

Two code points from the second review are fixed:

  • pair_by_place now prefers draw order whenever it pairs more copies. The 0.9/0.5 case is tested, and I traced it through.
  • The actor-motion CPU counter no longer counts time spent waiting for the lock.

The out-of-lock read still looks right to me. The PR still can't merge, for the reasons below. Several are the second review's, still open.

Must change

  • It conflicts with main again. main (514591a3) has added rows at the top of docs/tested-configurations.md, the place this PR's row goes, and Forgejo reports the PR not mergeable. So Rebased on current main is ticked but isn't true. Rebase, and move the row to where its date puts it (see the inline comment).

  • Nobody has run or measured this version, so it should be WIP. Every game run so far was of 8a091b34 or of the Hookshot build. This head changes the reach, the pairing and where poses are read. The Measurements box is unticked.

    • Needed: the stones run with frame generation on, plus actor-motion CPU against main, as interleaved runs.
    • Until then: pr wip 59.
  • The suite hasn't run on this version. The run in Not tested was of 8a091b34. On 966c69e1, Forgejo's runs show CI / Python source checks (both parts) and the SPIR-V fixtures as skipped, and only tests/test_dlss_actor.py was run by hand. The run_all box needs a run of the rebased head, by hand or by CI.

  • The description is still the 8a091b34 PR. pr status 59 lists 7 problems:

    • no Testing section, no Risk and rollback section, and no Flags and settings section, which should note RX_VK_DLSS_ACTOR_LIVE and RX_VK_DLSS_ACTOR_REACH;
    • the Measurements columns need named commits;
    • For everyone items are missing.

    The text itself is out of date too:

    • it says the reach is "1 a frame"; it is now 60 a second;
    • it still promises the out-of-lock follow-up, which is now done;
    • it has a bare #2787, which should be https://github.com/NotRexed/Hookshot/pull/2787;
    • Not tested and the Linux box ignore Desmond27's and mooki's runs.

    Fix it with pr update.

  • The bone-palette assumption is still an open question in the code (vk_temporal.cpp, "An assumption, open with the author"). @pmwork-personal, please confirm that OoT writes its palettes and object blocks on the CPU only, and say how you checked. Otherwise, keep the GPU copy for bones the stream wrote on the GPU.

Should change

  • The reach trade-off is wider than the comment says.
    • Draw order now wins whenever it pairs more copies, so once the reach reaches the stones' spacing (2.3), at any gap of 38 ms or more, a whole group can cross-pair, not just one lone stone. The author's own log shows a 55 ms frame.
    • Example: the frame before's model X draws stones s0 and s1, and this frame's draws s1 and s2, after a stone changes model. Nearest first pairs s1 with s1 and leaves s2 alone. Draw order pairs s1 with s0 and s2 with s1, which is 2 pairs against 1, so it wins: both stones get motion toward their neighbour, the original bug, for that frame.
    • Fix: either cap the clamp lower (for example 1/30 s), or say this in the comment. Details are inline.

Other

  • Alimjan's report that the stones still flicker "in the latest Longshot release" is expected. This PR isn't merged, so no release has it yet. It needs a run of a build with this PR.
  • Still not checked: whether OoT moves actors by wall time or by fixed ticks, which a reach set as a speed assumes.
Third review, of the PR at 966c69e1. I read the code only: no build, no tests, no game run. Two code points from the second review are fixed: - `pair_by_place` now prefers draw order whenever it pairs more copies. The 0.9/0.5 case is tested, and I traced it through. - The actor-motion CPU counter no longer counts time spent waiting for the lock. The out-of-lock read still looks right to me. The PR still can't merge, for the reasons below. Several are the second review's, still open. ## Must change - **It conflicts with main again.** `main` (514591a3) has added rows at the top of `docs/tested-configurations.md`, the place this PR's row goes, and Forgejo reports the PR not mergeable. So *Rebased on current `main`* is ticked but isn't true. Rebase, and move the row to where its date puts it (see the inline comment). - **Nobody has run or measured this version, so it should be WIP.** Every game run so far was of 8a091b34 or of the Hookshot build. This head changes the reach, the pairing and where poses are read. The *Measurements* box is unticked. - Needed: the stones run with frame generation on, plus actor-motion CPU against `main`, as interleaved runs. - Until then: `pr wip 59`. - **The suite hasn't run on this version.** The run in *Not tested* was of 8a091b34. On 966c69e1, Forgejo's runs show `CI / Python source checks` (both parts) and the SPIR-V fixtures as **skipped**, and only `tests/test_dlss_actor.py` was run by hand. The run_all box needs a run of the rebased head, by hand or by CI. - **The description is still the 8a091b34 PR.** `pr status 59` lists 7 problems: - no *Testing* section, no *Risk and rollback* section, and no *Flags and settings* section, which should note `RX_VK_DLSS_ACTOR_LIVE` and `RX_VK_DLSS_ACTOR_REACH`; - the Measurements columns need named commits; - *For everyone* items are missing. The text itself is out of date too: - it says the reach is "1 a frame"; it is now 60 a second; - it still promises the out-of-lock follow-up, which is now done; - it has a bare `#2787`, which should be https://github.com/NotRexed/Hookshot/pull/2787; - *Not tested* and the Linux box ignore Desmond27's and mooki's runs. Fix it with `pr update`. - **The bone-palette assumption is still an open question in the code** (`vk_temporal.cpp`, "An assumption, open with the author"). @pmwork-personal, please confirm that OoT writes its palettes and object blocks on the CPU only, and say how you checked. Otherwise, keep the GPU copy for bones the stream wrote on the GPU. ## Should change - **The reach trade-off is wider than the comment says.** - Draw order now wins whenever it pairs more copies, so once the reach reaches the stones' spacing (2.3), at any gap of 38 ms or more, a whole group can cross-pair, not just one lone stone. The author's own log shows a 55 ms frame. - Example: the frame before's model X draws stones s0 and s1, and this frame's draws s1 and s2, after a stone changes model. Nearest first pairs s1 with s1 and leaves s2 alone. Draw order pairs s1 with s0 and s2 with s1, which is 2 pairs against 1, so it wins: both stones get motion toward their neighbour, the original bug, for that frame. - Fix: either cap the clamp lower (for example 1/30 s), or say this in the comment. Details are inline. ## Other - Alimjan's report that the stones still flicker "in the latest Longshot release" is expected. This PR isn't merged, so no release has it yet. It needs a run of a build with this PR. - Still not checked: whether OoT moves actors by wall time or by fixed ticks, which a reach set as a speed assumes.
@ -14,6 +14,7 @@ otherwise.
| Date | Build | OS / desktop | GPU / driver | CPU / RAM | Settings | Where | Result |
|---|---|---|---|---|---|---|---|
| 2026-11-12 | v2.1.0-rc.3-281-g06ad86fe (source 8d4d275a): main ba3d3f73 + this change (before the review's small fixes), [NotRexed/Hookshot PR 2787](https://github.com/NotRexed/Hookshot/pull/2787)'s layouts and the close-up depth test; against v2.1.0-rc.3-271-gba3d3f73, main | Windows 11 Pro, 3840x2160 output | RTX 5090 32 GB, 621.24 | Ryzen 7 9800X3D | DLAA, frame generation 3x by DLSS (NGX), Reflex On + Boost, V-Sync Auto (FIFO) | Castle Town, the four Gossip Stones by the Temple of Time: running past them while the camera turns, about 30 s | Main: the stones break apart in made frames (a stone drawn part way to its neighbour); with `RX_VK_DLSS_ACTORS=0` they stay whole. This change: whole. Every pose read at the draw ("poses N read at the draw, 0 copied later"), 51-74 skinned draws a frame, actor motion 0.15-0.28 ms of CPU a frame, DLSS-G made 590 of 600 presents in play. No device lost |
Owner

Still open: this 2026-11-12 row sits above newer rows in a newest-first table, and "this change" should name the PR (Longshot PR 59: #59). It also conflicts with the rows main added at the top since 2e1e2e8c, so it needs a rebase. Add a row for this head once it has been measured.

Still open: this 2026-11-12 row sits above newer rows in a newest-first table, and "this change" should name the PR (Longshot PR 59: https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/59). It also conflicts with the rows main added at the top since 2e1e2e8c, so it needs a rebase. Add a row for this head once it has been measured.
@ -24,0 +90,4 @@
if (used[j] || !(d <= (double)reach)) return pairs; // (not within the reach: nearest first; NaN fails here too)
by_order[i] = j, used[j] = 1, order_n++, order_total += d;
}
if (order_n > nearest_n || (order_n == nearest_n && order_total <= nearest_total + 1e-6)) return by_order;
Owner

With order_n > nearest_n winning outright, the reach is now the only guard against draw order cross-pairing a group. Example: the frame before's model X has s0 (n-th 0) and s1 (n-th 1), and this frame's has s1 (n-th 0) and s2 (n-th 1), with stones 2.3 apart and a reach of 3.3 (a 55 ms frame). Nearest first gives 1 pair (s1-s1). Draw order gives 2 pairs (s1-s0, s2-s1) at 2.3 each, so draw order wins, and both stones move toward their neighbour. That is right for the 0.9/0.5 case and wrong here. It's worth a test either way, so the behaviour is pinned down.

With `order_n > nearest_n` winning outright, the reach is now the only guard against draw order cross-pairing a group. Example: the frame before's model X has s0 (n-th 0) and s1 (n-th 1), and this frame's has s1 (n-th 0) and s2 (n-th 1), with stones 2.3 apart and a reach of 3.3 (a 55 ms frame). Nearest first gives 1 pair (s1-s1). Draw order gives 2 pairs (s1-s0, s2-s1) at 2.3 each, so draw order wins, and both stones move toward their neighbour. That is right for the 0.9/0.5 case and wrong here. It's worth a test either way, so the behaviour is pinned down.
@ -543,0 +578,4 @@
// copy then is the next stone, 2.3 away), so it must not refuse an actor that really moved: as a distance a frame, a
// hitch or a low base frame rate (frame generation from 30 fps) took actors past it and they lost their motion for that
// frame, ghosting the ones that move most. 60 a second is 1 a frame at 60 fps, and some ten times a running Link (about
// 6 a second); it stays under the stones' 2.3 down to a base of 26 fps, and below that a lone stone can take its
Owner

"Below that a lone stone can take its neighbour" understates the problem. Since draw order wins whenever it pairs more, any gap of 38 ms or more (reach 2.3 or more) lets several stones cross-pair, as in the dlss_actor.h comment, and a single hitch is enough, not only a low base rate. Consider capping the clamp at 1/30 s (reach 2.0), or saying this here.

"Below that a lone stone can take its neighbour" understates the problem. Since draw order wins whenever it pairs more, any gap of 38 ms or more (reach 2.3 or more) lets several stones cross-pair, as in the dlss_actor.h comment, and a single hitch is enough, not only a low base rate. Consider capping the clamp at 1/30 s (reach 2.0), or saying this here.
@ -832,0 +974,4 @@
// frame's or another draw's (the place pair_by_place needs, too). RX_VK_DLSS_ACTOR_LIVE=1: as before, for an A/B.
// Read without the lock, into this thread's own buffer (its room kept from draw to draw); the lock is taken only to add it
// to the frame's poses. ub_read is this recording thread's view of the stream, as the draw's own reads of its uniforms.
// (An assumption, open with the author: OoT writes its bone palettes and object blocks on the CPU only. ub_read sees the
Owner

Still open from the second review: this shouldn't merge as an open question. Either state it as a fact and say how it was checked, or keep the GPU copy for bones in a range the stream wrote on the GPU.

Still open from the second review: this shouldn't merge as an open question. Either state it as a fact and say how it was checked, or keep the GPU copy for bones in a range the stream wrote on the GPU.
HumanBean force-pushed fix/actor-motion-copies from 966c69e139
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
PR description / Description follows the template (pull_request_target) Failing after 6s
CI / Conventional Commit title (pull_request) Successful in 7s
CI / Workflow lint (actionlint) (pull_request) Successful in 9s
Third-party parts / As their notes say (pull_request) Successful in 10s
to f2353ee86c
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 4s
PR description / Description follows the template (pull_request_target) Failing after 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
Third-party parts / As their notes say (pull_request) Has been cancelled
2026-11-14 00:46:09 +00:00
Compare
HumanBean force-pushed fix/actor-motion-copies from f2353ee86c
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 4s
PR description / Description follows the template (pull_request_target) Failing after 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
Third-party parts / As their notes say (pull_request) Has been cancelled
to 1168fe3a68
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
PR description / Description follows the template (pull_request_target) Failing after 6s
CI / Conventional Commit title (pull_request) Successful in 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
Third-party parts / As their notes say (pull_request) Successful in 12s
2026-11-14 00:46:18 +00:00
Compare
HumanBean force-pushed fix/actor-motion-copies from 1168fe3a68
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
PR description / Description follows the template (pull_request_target) Failing after 6s
CI / Conventional Commit title (pull_request) Successful in 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
Third-party parts / As their notes say (pull_request) Successful in 12s
to 9938524b1c
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
PR description / Description follows the template (pull_request_target) Has been cancelled
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
2026-11-14 00:46:38 +00:00
Compare
HumanBean force-pushed fix/actor-motion-copies from 9938524b1c
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
PR description / Description follows the template (pull_request_target) Has been cancelled
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
to 10e2b4d14e
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 7s
CI / Workflow lint (actionlint) (pull_request) Successful in 10s
Third-party parts / As their notes say (pull_request) Successful in 12s
PR description / Description follows the template (pull_request_target) Successful in 4s
Website / Website on Cloudflare (pull_request_target) Has been skipped
2026-11-14 00:46:49 +00:00
Compare
Owner

I rebased this onto main @ 4bab3915 and fixed review 1189's code points in one commit of mine on top: 10e2b4d1. The earlier commits are unchanged apart from the rebase. I checked that the code they carry is the same as at 966c69e1.

Rebase. docs/tested-configurations.md conflicted again. Your Hookshot row now sits after the last 2026-11-13 row, where its date puts it in the newest-first table. It names Longshot PR 59 instead of "this change", and links NotRexed/Hookshot PR 2787 in full.

My commit:

  • The reach is capped at 2 units (vk_temporal.cpp). The time between two poses is clamped to 1/60-1/30 s, where the upper bound was 1/4 s. At the default 60 a second, the reach now stays under the stones' 2.3 after any gap. Past 2.3, draw order, which wins whenever it pairs more copies, paired a whole row of stones each with its neighbour after a hitch. The comment states the cost: across a gap longer than 1/30 s, an actor that moved more than 2 units keeps only the camera's motion for that frame.
  • A test pins the case down (tests/test_dlss_actor.cpp): a row of stones whose model changed. With a reach of 2, the second stone pairs with itself and the third with none. With 3.3, each takes its neighbour, which is why the reach is capped. pair_by_place's comment in dlss_actor.h says that the reach is what guards draw order.
  • The bone-palette note is now a stated limit. It was an open question. Poses are read from what the CPU wrote, so a palette written on the GPU earlier in the stream would read stale. RX_VK_DLSS_ACTOR_LIVE=1 takes main's copy instead. The code can't confirm what OoT does, so I didn't claim it.
  • Docs. RX_VK_DLSS_ACTORS, RX_VK_DLSS_ACTOR_LIVE and RX_VK_DLSS_ACTOR_REACH are now in docs/development.md's switch reference. Desmond27's and mooki's runs of 8a091b34 have rows in docs/tested-configurations.md.

Description. I rewrote it for the PR as it is now:

  • the reach as a speed capped at 2;
  • the out-of-lock read as done, no longer promised;
  • the Testing, Flags and settings, and Risk and rollback sections;
  • the Measurements columns named by commit;
  • the full URL for NotRexed/Hookshot PR 2787.

It now includes the runs from this thread, each with the commit it ran on: yours (Hookshot 06ad86fe), Desmond27's (8a091b34, RTX 5080, FG 2x-4x, the stones whole) and mooki's (8a091b34, Linux, FG off). My third review wrongly said there had been no testing. There had, and it's in the description now.

No tests, builds or game runs were made on this PC. Forgejo's CI on 10e2b4d1 skips the Python checks and the SPIR-V fixtures for this PR.

Back to WIP. Nobody has run any version since 8a091b34 in the game. Since then the reach, the pairing and where the poses are read have all changed, so the earlier runs don't cover this code. Before it's ready, it needs:

  • the stones run on this head with frame generation on;
  • two or more interleaved runs of main and this head, for the actor-motion CPU line;
  • tests/test_dlss_actor.py (or the suite) on this head;
  • a clip.
I rebased this onto main @ 4bab3915 and fixed review 1189's code points in one commit of mine on top: 10e2b4d1. The earlier commits are unchanged apart from the rebase. I checked that the code they carry is the same as at 966c69e1. **Rebase.** `docs/tested-configurations.md` conflicted again. Your Hookshot row now sits after the last 2026-11-13 row, where its date puts it in the newest-first table. It names [Longshot PR 59](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/59) instead of "this change", and links [NotRexed/Hookshot PR 2787](https://github.com/NotRexed/Hookshot/pull/2787) in full. **My commit:** - **The reach is capped at 2 units** (`vk_temporal.cpp`). The time between two poses is clamped to 1/60-1/30 s, where the upper bound was 1/4 s. At the default 60 a second, the reach now stays under the stones' 2.3 after any gap. Past 2.3, draw order, which wins whenever it pairs more copies, paired a whole row of stones each with its neighbour after a hitch. The comment states the cost: across a gap longer than 1/30 s, an actor that moved more than 2 units keeps only the camera's motion for that frame. - **A test pins the case down** (`tests/test_dlss_actor.cpp`): a row of stones whose model changed. With a reach of 2, the second stone pairs with itself and the third with none. With 3.3, each takes its neighbour, which is why the reach is capped. `pair_by_place`'s comment in `dlss_actor.h` says that the reach is what guards draw order. - **The bone-palette note is now a stated limit.** It was an open question. Poses are read from what the CPU wrote, so a palette written on the GPU earlier in the stream would read stale. `RX_VK_DLSS_ACTOR_LIVE=1` takes main's copy instead. The code can't confirm what OoT does, so I didn't claim it. - **Docs.** `RX_VK_DLSS_ACTORS`, `RX_VK_DLSS_ACTOR_LIVE` and `RX_VK_DLSS_ACTOR_REACH` are now in `docs/development.md`'s switch reference. Desmond27's and mooki's runs of 8a091b34 have rows in `docs/tested-configurations.md`. **Description.** I rewrote it for the PR as it is now: - the reach as a speed capped at 2; - the out-of-lock read as done, no longer promised; - the Testing, Flags and settings, and Risk and rollback sections; - the Measurements columns named by commit; - the full URL for NotRexed/Hookshot PR 2787. It now includes the runs from this thread, each with the commit it ran on: yours (Hookshot 06ad86fe), Desmond27's (8a091b34, RTX 5080, FG 2x-4x, the stones whole) and mooki's (8a091b34, Linux, FG off). My third review wrongly said there had been no testing. There had, and it's in the description now. **No tests, builds or game runs were made on this PC.** Forgejo's CI on 10e2b4d1 skips the Python checks and the SPIR-V fixtures for this PR. **Back to WIP.** Nobody has run any version since 8a091b34 in the game. Since then the reach, the pairing and where the poses are read have all changed, so the earlier runs don't cover this code. Before it's ready, it needs: - the stones run on this head with frame generation on; - two or more interleaved runs of main and this head, for the actor-motion CPU line; - `tests/test_dlss_actor.py` (or the suite) on this head; - a clip.
HumanBean changed title from fix(vk): actor motion pairs a model's copies by place, so the Gossip Stones stay whole under frame generation to WIP: fix(vk): actor motion pairs a model's copies by place, so the Gossip Stones stay whole under frame generation 2026-11-14 00:48:55 +00:00
Owner

Description only: Measurements now lead with mooki's main @ d4a69456 / this PR @ 8a091b34 runs, and the author's Hookshot run names its builds in its columns, so the format check reads them right. No change to the code.

Description only: Measurements now lead with mooki's main @ d4a69456 / this PR @ 8a091b34 runs, and the author's Hookshot run names its builds in its columns, so the format check reads them right. No change to the code.
Owner

Rebased onto current main (5ad0f50395). 20 commits had merged since 4bab3915, among them #307 and #189. The four commits are rebased with no code conflict, and the first two are still yours.

Only docs/tested-configurations.md conflicted, because main had added rows at the top of the table. I kept main's rows and put this PR's after them in date order: the 2026-11-13 runs, then the 2026-11-12 row, worded as in 10e2b4d1. Each commit's added and removed lines are the same as before the rebase. No tests or builds were run on this PC.

The description's "Rebased on current main" line still names 4bab3915 and should name 5ad0f50395. Nothing else about the PR changes: it is still WIP and still needs a frame-generation run of this head and the CPU A/B.

Rebased onto current `main` (5ad0f5039525). 20 commits had merged since 4bab3915, among them https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/307 and https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/189. The four commits are rebased with no code conflict, and the first two are still yours. Only `docs/tested-configurations.md` conflicted, because `main` had added rows at the top of the table. I kept `main`'s rows and put this PR's after them in date order: the 2026-11-13 runs, then the 2026-11-12 row, worded as in 10e2b4d1. Each commit's added and removed lines are the same as before the rebase. No tests or builds were run on this PC. The description's "Rebased on current main" line still names 4bab3915 and should name 5ad0f5039525. Nothing else about the PR changes: it is still WIP and still needs a frame-generation run of this head and the CPU A/B.
HumanBean force-pushed fix/actor-motion-copies from 10e2b4d14e
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 7s
CI / Workflow lint (actionlint) (pull_request) Successful in 10s
Third-party parts / As their notes say (pull_request) Successful in 12s
PR description / Description follows the template (pull_request_target) Successful in 4s
Website / Website on Cloudflare (pull_request_target) Has been skipped
to cc7cb36aed
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 4s
PR description / Description follows the template (pull_request_target) Successful in 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 5s
Third-party parts / As their notes say (pull_request) Successful in 8s
Website / Website on Cloudflare (pull_request_target) Has been skipped
2026-11-14 01:46:39 +00:00
Compare
docs(tested): the Gossip Stones run of this head in the game (RTX 5090, DLSS FG 2x)
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 12s
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
d4dcec8702
The four commits at cc7cb36a, rebased onto main cf605c85 with no conflict and built as a renderer dev pack: in front of
the Temple of Time with the camera moving back and forth, the stones stay whole in made frames; actor motion 0.139 ms
CPU a frame.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Author
Contributor

Ran this head in the game: the stones stay whole in the made frames.

Build: Longshot main cf605c85 with this PR's four commits (cc7cb36a) rebased onto it locally, with no conflict (the same diff as cc7cb36a), built as a renderer dev pack (scripts/dev_packs.py) on a base of cf605c85.
Hardware: RTX 5090 32 GB, driver 621.24, Ryzen 7 9800X3D, 64 GB, Windows 11 Pro.
Scene and settings: the four Gossip Stones in front of the Temple of Time, the camera moved back and forth (main's repro); fullscreen, 5120x2160 output, DLAA, DLSS Frame Generation 2x (NGX), V-Sync Auto (FIFO), Reflex On + Boost, frame pacing on.

  • The stones: whole in the made frames, judged by eye in play.
  • DLSS-G: 600 of 600 presents in 7 of the 8 status blocks in play, 541 of 600 in the last. (The block before them was at the title, before the scene had DLAA inputs: the built-in frame generation made those.)
  • Poses: every block reads all poses at the draw, 0 copied later.
  • Actor motion CPU a frame: median 0.139 ms over the 6 blocks of steady play (0.133-0.178 ms); 0.616 and 0.446 ms in the first two, while the scene loaded. On the same PC the first version (Hookshot @ 06ad86fe) was 0.15-0.28 ms, and mooki's run of 8a091b34 shows 0.410 ms: moving the reads out of the lock brought it down.
  • Errors: none in the log.
  • Game-free checks on this head (rebased onto cf605c85): tests/test_dlss_actor.py and the 10 checks that read vk_temporal.cpp or dlss_actor.h pass. python -I -B tests/run_all.py: 516 passed, 8 failed, 35 skipped; main cf605c85 on the same PC fails the same 8 (test_fpx_sticky, test_framegen_resources, test_module_ids, test_oot_state_tap, test_pad_layout, test_portable_builder_startup, test_spawn_emit, test_tested_configurations), so none of them comes from this change.

Not done: main was not run on the same PC this time, and this is one run, not interleaved, so the Measured box stays unticked. FG 3x and 4x were not run on this head.

vk: status: 600 frames, the slowest 29.3 ms; VRAM 12.6 of 30.7 GB; NVIDIA DLAA: 600 frames drawn, 1 made, jitter +0.438 +0.389 px, actor motion 30361 draws in 600 frames (0.142 ms CPU a frame; poses 30494 read at the draw, 0 copied later), its images 263 MB, its own 1433 MB
vk: DLSS Frame Gen (NGX): of the last 600 presents 600 made by DLSS-G (0 told to reset, 23 with the camera still, 0 NGX said not to show); the renderer's own made the rest: 0 without new inputs (no DLSS/DLAA this frame), 0 after a gap, 0 asked more than 5 frames, 0 refused

The run is added as docs/tested/2026-11-14-gossip-stones-pmwork-personal-run01.md, and the description is updated to match.

Ran this head in the game: the stones stay whole in the made frames. **Build:** Longshot main `cf605c85` with this PR's four commits (`cc7cb36a`) rebased onto it locally, with no conflict (the same diff as `cc7cb36a`), built as a renderer dev pack (`scripts/dev_packs.py`) on a base of `cf605c85`. **Hardware:** RTX 5090 32 GB, driver 621.24, Ryzen 7 9800X3D, 64 GB, Windows 11 Pro. **Scene and settings:** the four Gossip Stones in front of the Temple of Time, the camera moved back and forth (main's repro); fullscreen, 5120x2160 output, DLAA, DLSS Frame Generation 2x (NGX), V-Sync Auto (FIFO), Reflex On + Boost, frame pacing on. - **The stones:** whole in the made frames, judged by eye in play. - **DLSS-G:** 600 of 600 presents in 7 of the 8 status blocks in play, 541 of 600 in the last. (The block before them was at the title, before the scene had DLAA inputs: the built-in frame generation made those.) - **Poses:** every block reads all poses at the draw, 0 copied later. - **Actor motion CPU a frame:** median 0.139 ms over the 6 blocks of steady play (0.133-0.178 ms); 0.616 and 0.446 ms in the first two, while the scene loaded. On the same PC the first version (Hookshot @ 06ad86fe) was 0.15-0.28 ms, and mooki's run of 8a091b34 shows 0.410 ms: moving the reads out of the lock brought it down. - **Errors:** none in the log. - **Game-free checks on this head (rebased onto `cf605c85`):** `tests/test_dlss_actor.py` and the 10 checks that read `vk_temporal.cpp` or `dlss_actor.h` pass. `python -I -B tests/run_all.py`: 516 passed, 8 failed, 35 skipped; main `cf605c85` on the same PC fails the same 8 (`test_fpx_sticky`, `test_framegen_resources`, `test_module_ids`, `test_oot_state_tap`, `test_pad_layout`, `test_portable_builder_startup`, `test_spawn_emit`, `test_tested_configurations`), so none of them comes from this change. **Not done:** main was not run on the same PC this time, and this is one run, not interleaved, so the Measured box stays unticked. FG 3x and 4x were not run on this head. ``` vk: status: 600 frames, the slowest 29.3 ms; VRAM 12.6 of 30.7 GB; NVIDIA DLAA: 600 frames drawn, 1 made, jitter +0.438 +0.389 px, actor motion 30361 draws in 600 frames (0.142 ms CPU a frame; poses 30494 read at the draw, 0 copied later), its images 263 MB, its own 1433 MB vk: DLSS Frame Gen (NGX): of the last 600 presents 600 made by DLSS-G (0 told to reset, 23 with the camera still, 0 NGX said not to show); the renderer's own made the rest: 0 without new inputs (no DLSS/DLAA this frame), 0 after a gap, 0 asked more than 5 frames, 0 refused ``` The run is added as `docs/tested/2026-11-14-gossip-stones-pmwork-personal-run01.md`, and the description is updated to match.
docs(tested): the Gossip Stones A/B, main against this head, 2 alternated runs each
Some checks are pending
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Blocked by required conditions
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Blocked by required conditions
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 16s
34d21b90b6
In front of the Temple of Time with the camera moving back and forth (DLAA, DLSS FG 2x, RTX 5090): on main cf605c85
the stones break apart in made frames, on this head's commits rebased onto it they stay whole. Actor motion: 1.085 us
a skinned draw on main, 2.725 us on this head (medians of 2 runs each).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Author
Contributor

Measured this head against main on the same PC: 2 alternated runs each (this PR, main, this PR, main), from one dev-pack base of main cf605c85, so both sides have the same caches and settings and only the renderer differs.

main @ cf605c85 this PR @ cc7cb36a
Stones in made frames, run 1; run 2 break apart; break apart whole; whole
Actor motion CPU a frame (median of 2) 0.0555 ms 0.143 ms
Actor motion CPU a skinned draw (median of 2) 1.085 us 2.725 us
Skinned draws a frame 50.5; 51.3 50.5; 53.8
  • The fault and the fix: on main the stones break apart in made frames in both runs; on this head they stay whole in both.
  • The cost: about 1.6 us more a skinned draw, so about +0.09 ms a frame at the stones (about 50 skinned draws a frame) and about +0.25 ms at 150.
  • The test: tests/test_dlss_actor.py with this PR's test fails on main (main has no dlss_actor::Placed or pair_by_place) and passes on this head.

Same setup as my last comment: RTX 5090, 621.24; fullscreen 5120x2160, DLAA, DLSS FG 2x (NGX), V-Sync Auto, Reflex On + Boost; in front of the Temple of Time, the camera moved back and forth, about a minute a run. The four runs are docs/tested/2026-11-14-gossip-stones-pmwork-personal-run01.md to run04.md, and the description's Measurements, Testing and checklist are updated (Measured and the test box ticked). Still open: a clip, a Linux build, and the FG 3x/4x and AMD test requests.

Measured this head against main on the same PC: 2 alternated runs each (this PR, main, this PR, main), from one dev-pack base of main `cf605c85`, so both sides have the same caches and settings and only the renderer differs. | | main @ cf605c85 | this PR @ cc7cb36a | |---|---|---| | Stones in made frames, run 1; run 2 | break apart; break apart | whole; whole | | Actor motion CPU a frame (median of 2) | 0.0555 ms | 0.143 ms | | Actor motion CPU a skinned draw (median of 2) | 1.085 us | 2.725 us | | Skinned draws a frame | 50.5; 51.3 | 50.5; 53.8 | - **The fault and the fix:** on main the stones break apart in made frames in both runs; on this head they stay whole in both. - **The cost:** about 1.6 us more a skinned draw, so about +0.09 ms a frame at the stones (about 50 skinned draws a frame) and about +0.25 ms at 150. - **The test:** `tests/test_dlss_actor.py` with this PR's test fails on main (main has no `dlss_actor::Placed` or `pair_by_place`) and passes on this head. Same setup as my last comment: RTX 5090, 621.24; fullscreen 5120x2160, DLAA, DLSS FG 2x (NGX), V-Sync Auto, Reflex On + Boost; in front of the Temple of Time, the camera moved back and forth, about a minute a run. The four runs are `docs/tested/2026-11-14-gossip-stones-pmwork-personal-run01.md` to `run04.md`, and the description's Measurements, Testing and checklist are updated (Measured and the test box ticked). Still open: a clip, a Linux build, and the FG 3x/4x and AMD test requests.
Author
Contributor

Description only, no code change:

  • Measurements: a note on why the numbers measured with this head's commits on main cf605c85 still hold for the PR's base 5ad0f503: between the two, main has 8 commits, this PR's own lines are the same on both, and the only one touching a file of this PR (Longshot PR 252) wraps the pipeline creation in an Intel driver guard, not the per-frame actor motion.
  • Checklist: added "A check under tests/ that reads changed source updated in this PR, not weakened", ticked. "Host code builds on both Windows and Linux" stays unticked: this head built on Windows (llvm-mingw); a Linux build of this head is still to do. "Screenshots" stays unticked: no clip, by the author's choice; the A/B by eye is under Measurements.
  • Testing: DLSS FG 3x/4x on RTX 40/50 and AMD with FSR are now under Not tested, not Needs other testers.

The PR stays WIP until this head has a Linux build.

Description only, no code change: - **Measurements:** a note on why the numbers measured with this head's commits on main `cf605c85` still hold for the PR's base `5ad0f503`: between the two, main has 8 commits, this PR's own lines are the same on both, and the only one touching a file of this PR ([Longshot PR 252](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/252)) wraps the pipeline creation in an Intel driver guard, not the per-frame actor motion. - **Checklist:** added "A check under `tests/` that reads changed source updated in this PR, not weakened", ticked. "Host code builds on both Windows and Linux" stays unticked: this head built on Windows (llvm-mingw); a Linux build of this head is still to do. "Screenshots" stays unticked: no clip, by the author's choice; the A/B by eye is under Measurements. - **Testing:** DLSS FG 3x/4x on RTX 40/50 and AMD with FSR are now under Not tested, not Needs other testers. The PR stays WIP until this head has a Linux build.
HumanBean changed title from WIP: fix(vk): actor motion pairs a model's copies by place, so the Gossip Stones stay whole under frame generation to fix(vk): actor motion pairs a model's copies by place, so the Gossip Stones stay whole under frame generation 2026-11-14 11:24:31 +00:00
Merge branch 'main' into fix/actor-motion-copies
Some checks failed
PR description / Description follows the template (pull_request_target) Failing after 12s
61e7894f45
HumanBean deleted branch fix/actor-motion-copies 2026-11-14 11:26:03 +00:00
Some checks failed
PR description / Description follows the template (pull_request_target) Failing after 12s
Some workflows are waiting to be reviewed.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
5 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/Longshot!59
No description provided.