WIP: fix(vk): a render target is not uploaded from memory for a GPU buffer write older than its drawing #332

Draft
Amateurwizrd wants to merge 2 commits from Amateurwizrd/Longshot:fix/drawn-target-upload into main
Contributor

Why

The first time the game samples a render target after finalizing it, the renderer asks what changed in the target's storage since. When a GPU buffer wrote that storage, it uploads the whole image from guest memory over what the pass drew. That buffer write was compared only with the last one the texture had seen, never with the drawing, so a target made over a buffer's old storage counted the older write as newer. In Hyrule Field on current main that is 4 or 5 whole uploads of 1920x1080 targets a run in the two minutes after loading.

main already fixes the other cause of the same uploads, a virtual-pool target drawn before it had a page map (#325, the port of Hookshot's 40955ed3). This PR started as our own version of that change and is now only the part #325 does not cover.

What changed

  • vk: texture_rendered (gpu/vk/vk_texture.cpp), which only a pass calls, now takes GPU buffer writes into the target's storage from earlier submissions as older than the drawing (gpu_wrote_seen), once a frame under the pass's lock. A write in the pass's own submission still counts, since which came first is not known there.
  • Copies and storage-image writes are left as they were.
  • test: tests/test_drawn_target.py cuts texture_written, texture_rendered, texture_released, texture_dirty, texture_gpu_wrote, gpu_wrote_into and the check in texture_touch out of the source and checks the buffer-write cases with the switch off and on. On main its first case fails.
  • docs: the switch in docs/development.md, and the two measured builds as run files in docs/tested/.

Flags and settings

Flag New or changed What it does Default now Goal: on by default? Helps on Doesn't help on Untested on
RX_VK_NO_DRAWN_OVER_BUFFER_WRITE=1 New Turns the change off: a buffer write before the pass counts as newer than the drawing, as on main Unset (the change is on) The change is on by default; the switch is for comparing Every GPU (the texture cache is vendor-independent) Targets with no buffer write over their storage AMD, Intel, Linux

Measurements

main: 98e2d95f (2026-11-14)
this PR: fix/drawn-target-upload @ de3b4aab
Hardware: NVIDIA GeForce RTX 5070 Ti 16 GB, driver 621.01, AMD Ryzen 7 7800X3D, 32 GB, Windows 11 Pro 26200
Scene: Hyrule Field (child) by debug teleport from a copied save, standing; Output resolution 2560x1440 in a 1707x960 window, Render resolution 75%, the game's own upscaler, no frame generation, no frame cap, sound off, the serial replay, no VRAM allowance
Method: 3 rounds, the two builds alternating in each round. One game folder (longshot v3.0.0-rc.3-27-gf516654c) with only longshot_vk.dll swapped (the host interface is unchanged since); both renderers built alone with RX_VK_ONLY=1 and the same toolchain (v3.0.0-rc.3-57-g98e2d95f and v3.0.0-rc.3-58-gde3b4aab). Each run: teleport, 25 s standing, 5 s in the pause menu. Counts are vk: upload over a drawn image lines; "for a buffer write" are those with a GPU buffer wrote it, each a whole image. Slow re-uploads are slow upload lines marked again (over 15 ms). Frame rate is the median of the log's N presents, X fps lines from present 5040 on.
How to reproduce: load a save, teleport to Hyrule Field (Debug tab), stand for 25 s, open and close the pause menu, and count the upload over a drawn image ... a GPU buffer wrote it lines in the log.

main @ 98e2d95f this PR @ de3b4aab
Whole uploads for a GPU buffer write older than the drawing, a run 4, 4, 5 (32, 32, 40 MB) 0, 0, 0
Other uploads over a drawn image, a run 0, 0, 0 0, 0, 1 (a 1016 KB range)
Re-uploads over 15 ms, a run 1, 1, 1 (34, 20, 19 ms) 0, 0, 1 (18 ms, that range's upload)
Frame rate standing in the field (median of 3) 115.7 fps 114.5 fps

Frame rate by round, main / this PR: 118.4 / 114.7, 115.7 / 114.5, 114.4 / 114.2. Main ran first in each round here and was faster in each. In the earlier set on 514591a3 (this PR's first version), this PR ran first in each round and was faster in each (118.6 / 114.2, 114.8 / 114.2, 115.3 / 114.7). The first run of a round is the faster one on this PC, so no frame rate change is claimed either way. The HUD labels were right in all six runs.

The one left with this PR is a 1016 KB range of a 1920x1080 target, a page remapped or written after its finalize: another cause, not touched here.

main @ 98e2d95f, round 1:

vk: texture #2447 0x10001950600 1920x1080 NVN 0xa: the GPU wrote its storage (submission 28165): uploaded again
vk: upload over a drawn image: #2447 0x10001950600 1920x1080x1 x1 NVN 0xa at present 4215 (texture frame 4216), finalized, 0 KB dirty in 0 ranges, a GPU buffer wrote it
vk: slow upload 34.4 ms: #2447 0x10001950600 1920x1080x1 x1 levels 1 NVN 0xa, 8640 of 8640 KB again

On a build with 40955ed3 and RX_VK_WATCH_TEX, the buffer writes behind these uploads were 1 to 3,221 frames older than the target's last drawing, in four of four.

Testing

Tested: python -I -B tests/run_all.py on Windows 11 with llvm-mingw, at c42e9b1f: 517 passed, 5 failed, 34 skipped. All five fail the same way on main 98e2d95f (test_oot_state_tap, test_pad_layout, test_portable_builder_home, test_portable_builder_startup, test_tested_configurations; the last only in its Windows line-ending case, while its check of the repository's run files passes). tests/test_drawn_target.py passes here; with main's vk_texture.cpp (--source) its first case fails. In the game on the PC above: the 6 runs under Measurements, no device lost.

Not tested: RX_VK_PARALLEL=4. Zone changes on foot (the runs teleport once). Sessions longer than two minutes. Frame times (only the 5 s frame-rate readings). AMD and Intel GPUs. Linux. GCC for the new test. Whether the uploads on main ever show on screen.

Needs other testers:

  • An AMD or Intel GPU, or Linux: 6 or more zone changes, with and without RX_VK_NO_DRAWN_OVER_BUFFER_WRITE=1, counting the upload over a drawn image ... a GPU buffer wrote it lines and watching the picture after each load (T-KUWQ)

Screenshots

None: nothing differed on screen in these runs.

Risk and rollback

If a buffer write before the pass put texels into part of the target that the pass then does not cover, those texels are now missing from the image; on main the whole drawing was missing instead. RX_VK_NO_DRAWN_OVER_BUFFER_WRITE=1 gives main's behavior without a rebuild; reverting the commit is enough.

Reviewer notes

  • The change is the 8 lines added to texture_rendered; it costs one compare and store per target per frame.
  • texture_rendered is called only by begin_pass, under the ParLock that texture_marked decides, so the store needs no lock of its own.
  • This branch was force-pushed: it first carried its own version of the page-map fix as well, which #325 made unneeded. Its first test request, T-J359, named that version's switch and is closed; T-KUWQ replaces it.

Issues

None found (the forge searched for "garbled", "rainbow" and "HUD", 2026-11-13).

Checklist

For everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description).
  • Rebased on current main.
  • python -I -B tests/run_all.py run and passing (any failure that also fails on main is named above).
  • Tested in the game, or 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.
  • 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.
  • A row in docs/tested-configurations.md for each measured run.
  • A game-free test added for the fix, failing on main and passing here.
  • A check under tests/ that reads changed source updated in this PR, not weakened.
  • Flags noted, and the switch reference updated (docs/development.md, the README).
  • Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. (Built on Windows only. The change is standard C++ with no platform call and no path.)
  • Docs updated.
## Why The first time the game samples a render target after finalizing it, the renderer asks what changed in the target's storage since. When a GPU buffer wrote that storage, it uploads the whole image from guest memory over what the pass drew. That buffer write was compared only with the last one the texture had seen, never with the drawing, so a target made over a buffer's old storage counted the older write as newer. In Hyrule Field on current `main` that is 4 or 5 whole uploads of 1920x1080 targets a run in the two minutes after loading. `main` already fixes the other cause of the same uploads, a virtual-pool target drawn before it had a page map (#325, the port of Hookshot's 40955ed3). This PR started as our own version of that change and is now only the part #325 does not cover. ## What changed - vk: `texture_rendered` (`gpu/vk/vk_texture.cpp`), which only a pass calls, now takes GPU buffer writes into the target's storage from earlier submissions as older than the drawing (`gpu_wrote_seen`), once a frame under the pass's lock. A write in the pass's own submission still counts, since which came first is not known there. - Copies and storage-image writes are left as they were. - test: `tests/test_drawn_target.py` cuts `texture_written`, `texture_rendered`, `texture_released`, `texture_dirty`, `texture_gpu_wrote`, `gpu_wrote_into` and the check in `texture_touch` out of the source and checks the buffer-write cases with the switch off and on. On `main` its first case fails. - docs: the switch in `docs/development.md`, and the two measured builds as run files in `docs/tested/`. ## Flags and settings | Flag | New or changed | What it does | Default now | Goal: on by default? | Helps on | Doesn't help on | Untested on | |---|---|---|---|---|---|---|---| | `RX_VK_NO_DRAWN_OVER_BUFFER_WRITE=1` | New | Turns the change off: a buffer write before the pass counts as newer than the drawing, as on `main` | Unset (the change is on) | The change is on by default; the switch is for comparing | Every GPU (the texture cache is vendor-independent) | Targets with no buffer write over their storage | AMD, Intel, Linux | ## Measurements main: 98e2d95f (2026-11-14) this PR: fix/drawn-target-upload @ de3b4aab Hardware: NVIDIA GeForce RTX 5070 Ti 16 GB, driver 621.01, AMD Ryzen 7 7800X3D, 32 GB, Windows 11 Pro 26200 Scene: Hyrule Field (child) by debug teleport from a copied save, standing; Output resolution 2560x1440 in a 1707x960 window, Render resolution 75%, the game's own upscaler, no frame generation, no frame cap, sound off, the serial replay, no VRAM allowance Method: 3 rounds, the two builds alternating in each round. One game folder (`longshot v3.0.0-rc.3-27-gf516654c`) with only `longshot_vk.dll` swapped (the host interface is unchanged since); both renderers built alone with `RX_VK_ONLY=1` and the same toolchain (`v3.0.0-rc.3-57-g98e2d95f` and `v3.0.0-rc.3-58-gde3b4aab`). Each run: teleport, 25 s standing, 5 s in the pause menu. Counts are `vk: upload over a drawn image` lines; "for a buffer write" are those with `a GPU buffer wrote it`, each a whole image. Slow re-uploads are `slow upload` lines marked `again` (over 15 ms). Frame rate is the median of the log's `N presents, X fps` lines from present 5040 on. How to reproduce: load a save, teleport to Hyrule Field (Debug tab), stand for 25 s, open and close the pause menu, and count the `upload over a drawn image ... a GPU buffer wrote it` lines in the log. | | main @ 98e2d95f | this PR @ de3b4aab | |---|---|---| | Whole uploads for a GPU buffer write older than the drawing, a run | 4, 4, 5 (32, 32, 40 MB) | 0, 0, 0 | | Other uploads over a drawn image, a run | 0, 0, 0 | 0, 0, 1 (a 1016 KB range) | | Re-uploads over 15 ms, a run | 1, 1, 1 (34, 20, 19 ms) | 0, 0, 1 (18 ms, that range's upload) | | Frame rate standing in the field (median of 3) | 115.7 fps | 114.5 fps | Frame rate by round, main / this PR: 118.4 / 114.7, 115.7 / 114.5, 114.4 / 114.2. Main ran first in each round here and was faster in each. In the earlier set on 514591a3 (this PR's first version), this PR ran first in each round and was faster in each (118.6 / 114.2, 114.8 / 114.2, 115.3 / 114.7). The first run of a round is the faster one on this PC, so no frame rate change is claimed either way. The HUD labels were right in all six runs. The one left with this PR is a 1016 KB range of a 1920x1080 target, a page remapped or written after its finalize: another cause, not touched here. main @ 98e2d95f, round 1: ``` vk: texture #2447 0x10001950600 1920x1080 NVN 0xa: the GPU wrote its storage (submission 28165): uploaded again vk: upload over a drawn image: #2447 0x10001950600 1920x1080x1 x1 NVN 0xa at present 4215 (texture frame 4216), finalized, 0 KB dirty in 0 ranges, a GPU buffer wrote it vk: slow upload 34.4 ms: #2447 0x10001950600 1920x1080x1 x1 levels 1 NVN 0xa, 8640 of 8640 KB again ``` On a build with 40955ed3 and `RX_VK_WATCH_TEX`, the buffer writes behind these uploads were 1 to 3,221 frames older than the target's last drawing, in four of four. ## Testing **Tested:** `python -I -B tests/run_all.py` on Windows 11 with llvm-mingw, at c42e9b1f: 517 passed, 5 failed, 34 skipped. All five fail the same way on `main` 98e2d95f (`test_oot_state_tap`, `test_pad_layout`, `test_portable_builder_home`, `test_portable_builder_startup`, `test_tested_configurations`; the last only in its Windows line-ending case, while its check of the repository's run files passes). `tests/test_drawn_target.py` passes here; with `main`'s `vk_texture.cpp` (`--source`) its first case fails. In the game on the PC above: the 6 runs under Measurements, no device lost. **Not tested:** `RX_VK_PARALLEL=4`. Zone changes on foot (the runs teleport once). Sessions longer than two minutes. Frame times (only the 5 s frame-rate readings). AMD and Intel GPUs. Linux. GCC for the new test. Whether the uploads on `main` ever show on screen. **Needs other testers:** - An AMD or Intel GPU, or Linux: 6 or more zone changes, with and without `RX_VK_NO_DRAWN_OVER_BUFFER_WRITE=1`, counting the `upload over a drawn image ... a GPU buffer wrote it` lines and watching the picture after each load (T-KUWQ) ## Screenshots None: nothing differed on screen in these runs. ## Risk and rollback If a buffer write before the pass put texels into part of the target that the pass then does not cover, those texels are now missing from the image; on `main` the whole drawing was missing instead. `RX_VK_NO_DRAWN_OVER_BUFFER_WRITE=1` gives `main`'s behavior without a rebuild; reverting the commit is enough. ## Reviewer notes - The change is the 8 lines added to `texture_rendered`; it costs one compare and store per target per frame. - `texture_rendered` is called only by `begin_pass`, under the `ParLock` that `texture_marked` decides, so the store needs no lock of its own. - This branch was force-pushed: it first carried its own version of the page-map fix as well, which #325 made unneeded. Its first test request, T-J359, named that version's switch and is closed; T-KUWQ replaces it. ## Issues None found (the forge searched for "garbled", "rainbow" and "HUD", 2026-11-13). ## Checklist ### For everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`). - [x] Rebased on current `main`. - [x] `python -I -B tests/run_all.py` run and passing (any failure that also fails on `main` is named above). - [x] Tested in the game, or 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. - [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] A row in `docs/tested-configurations.md` for each measured run. - [x] A game-free test added for the fix, failing on `main` and passing here. - [x] A check under `tests/` that reads changed source updated in this PR, not weakened. - [x] Flags noted, and the switch reference updated (docs/development.md, the README). - [ ] Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. (Built on Windows only. The change is standard C++ with no platform call and no path.) - [x] Docs updated.
The first time the game samples a render target after finalizing it, the
target's check asks what changed in its storage since, and uploads the whole
image from guest memory when anything did. Two cases took the pass's drawing
for stale:

- In a virtual pool (at or above 1 TB) the check compares each page's mapping
  with the one noted at the first upload. A target the GPU drew before any
  upload had no map, so the check took all of its storage for remapped.
- A GPU buffer's write into the storage (gpu_wrote_into) was compared only
  with the last such write the texture had seen, never with the drawing. A
  target made over a buffer's old storage counted that older write as newer.

texture_rendered, which only a pass calls, now notes the map and marks buffer
writes of earlier submissions as seen, once a frame under the pass's lock. A
write in the pass's own submission still counts, since which came first is not
known there. Copies and storage writes are left as they were.

In Hyrule Field this removes 23 to 26 whole uploads of 1920x1080 and
2560x1440 targets in the first two minutes after loading (98 to 128 MB from
memory, plus about 32 MB for the buffer case). The first case is the one
Hookshot's 40955ed3 fixes in texture_written for the HUD's button labels:
40955ed3ad

RX_VK_NO_DRAWN_TARGET_KEEP=1: as before.

Assisted-by: Claude Fable 5.2
Co-Authored-By: Claude Fable 5.2 <[email protected]>
docs: tested-configurations rows for the drawn render target runs
Some checks failed
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
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
PR description / Description follows the template (pull_request_target) Successful in 4s
Website / Website on Cloudflare (pull_request_target) Has been skipped
210511373d
Assisted-by: Claude Fable 5.2
Co-Authored-By: Claude Fable 5.2 <[email protected]>
Author
Contributor

Test request T-J359 is open (any GPU, with RX_VK_PARALLEL=4); the description names it and the Testing box is ticked. It can be claimed once a maintainer adds the testing wanted label.

Test request T-J359 is open (any GPU, with RX_VK_PARALLEL=4); the description names it and the Testing box is ticked. It can be claimed once a maintainer adds the testing wanted label.
Amateurwizrd force-pushed fix/drawn-target-upload from 210511373d
Some checks failed
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
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
PR description / Description follows the template (pull_request_target) Successful in 4s
Website / Website on Cloudflare (pull_request_target) Has been skipped
to c42e9b1f88
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
Website / Website on Cloudflare (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 3s
Website / Website on Cloudflare (pull_request_target) Has been skipped
2026-11-14 04:12:31 +00:00
Compare
Amateurwizrd changed title from WIP: fix(vk): render targets keep their drawing after the game finalizes them to WIP: fix(vk): a render target is not uploaded from memory for a GPU buffer write older than its drawing 2026-11-14 04:12:46 +00:00
Author
Contributor

Narrowed and re-measured: #325 merged the drawn page map, so this PR is now only the buffer-write case it leaves (one commit, de3b4aab, switch RX_VK_NO_DRAWN_OVER_BUFFER_WRITE). Force-pushed onto main 98e2d95f and measured there: 4, 4 and 5 whole uploads for an older buffer write a run on main, none with this PR. T-J359 named the dropped switch and is closed; T-KUWQ replaces it.

Narrowed and re-measured: #325 merged the drawn page map, so this PR is now only the buffer-write case it leaves (one commit, de3b4aab, switch RX_VK_NO_DRAWN_OVER_BUFFER_WRITE). Force-pushed onto main 98e2d95f and measured there: 4, 4 and 5 whole uploads for an older buffer write a run on main, none with this PR. T-J359 named the dropped switch and is closed; T-KUWQ replaces it.
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
Website / Website on Cloudflare (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 3s
Website / Website on Cloudflare (pull_request_target) Has been skipped
This pull request is marked as a work in progress.
Some workflows are waiting to be reviewed.
This branch is out-of-date with the base branch
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u 'fix/drawn-target-upload:Amateurwizrd-fix/drawn-target-upload'
git switch 'Amateurwizrd-fix/drawn-target-upload'
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
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!332
No description provided.