perf(vk): re-upload only the part of a texture a GPU write covered #282

Open
bluetoe wants to merge 6 commits from bluetoe/Longshot:lb/part-upload into main
Contributor

Why

In Castle Town, one of the game's compute pipelines binds a 1.75 MB write buffer whose range lands inside a finalized 2560x1440 render target's storage - and every frame of that burst the renderer copied the whole 15 MB image back from guest memory, on a path where nothing at all had been written by the CPU (the log says so: upload over a drawn image ... 0 KB dirty in 0 ranges, a GPU buffer wrote it). Players on cards already at their texture budget feel it as hitching in exactly the areas the writeback work found worst.

What changed

  • gpu/vk/vk_texture.cpp: gpu_wrote_into now reports the ranges it matched, clipped to the texture's storage and merged, in image-relative offsets; texture_touch uploads those ranges instead of the whole image, plus the CPU's writes since the last upload in the same pass (required: the whole upload used to cover those pages and the upload moves upload_gen past them, so a partial that skipped them would lose them permanently). Sparse images keep the whole path; an event where the CPU also wrote still uploads the full amount.
  • gpu/vk/vk_texture.cpp: a tally so anyone can see whether the path is hot in their own scene: vk: gpu-write uploads: N (X MB copied, the whole image would have been Y MB).
  • tests/test_gpu_write_part.py (new) cuts the recorder, texture_gpu_wrote and gpu_wrote_into out of the source and runs them against stand-ins: a write inside reports its part; a write spanning either end is clipped; adjacent writes merge; an unfinished submission reports nothing; the same write is not reported to the same texture twice; a write elsewhere reports nothing. Red on main (the source it cuts does not exist there), green here.
  • docs/development.md: the new switch in the table.

Flags and settings

Flag What it does Default now Recommendation Helps on Doesn't help on Untested on
RX_VK_GPU_WRITE_WHOLE (set) A/B: copy the whole image again on a GPU write, as before the change off (parts are uploaded) keep off; it exists to A/B the path reproducing the old behaviour anything else AMD vendors

Measurements

main: 7315957c
this PR: lb/part-upload @ 53b99634
Hardware: RTX 5070 Ti 16 GB, 595.99.02, i9-13900K (32 threads), 62 GB, Ubuntu 24.04, headless runs
Scene: five area changes in one process (Hyrule Field, Market, Kokiri Forest, Hyrule Field, Market), 2560x1440, default settings, HOS_FPS_MAX=240
Method: one save, one process, the tour run twice per arm, both arms this same build with the flag off and on (main needs no separate run: RX_VK_GPU_WRITE_WHOLE=1 reproduces its exact copy path, which is the Baseline column below); the numbers are the vk: gpu-write uploads tally lines in the run log
How to reproduce: the test save, the scripted teleport tour; the whole-image arm with RX_VK_GPU_WRITE_WHOLE=1, the parts arm unset. The patched binary's tally prints both sides of every event, so one run gives the comparison.
Baseline: this PR @ 53b99634, RX_VK_GPU_WRITE_WHOLE=1

main @ 7315957c this PR @ 53b99634
per bind-only write-back event 15,360 KB 1,800 KB
event where the CPU had also written 15,360 KB 15,360 KB (the union, by design)
the path over a tour, sample 1 150 events, 1,462 MB 150 events, 648 MB
the path over a tour, sample 2 150 events, 1,434 MB 150 events, 632 MB
retirement past the budget, last leg 2,910 textures (841 retire lines) 3,264 textures (840 lines): no eviction regression
fps leg medians 91.8/94.0/96.0/81.9/90.3, then 91.4/88.9/90.1/93.1/88.2 88.0/92.5/77.1/85.5/87.4, then 91.3/90.5/91.5/92.4/92.0

56% less copied on the path, 802-814 MB not copied per tour. No fps effect is claimed: the same build in the same leg varied by up to 19 fps between samples, which is the noise floor these runs established. On 8-12 GB cards, where the same events compete with an already-over-budget texture set, the saved copying should matter more.

Testing

Tested: the full suite on this base and branch: python -I -B tests/run_all.py 501 passed, 0 failed, 28 skipped (Linux; the skips want spirv-val and Windows-only tools); the focused test green on this head; the in-game tour whose numbers are above, twice per arm.

Not tested: the Windows build of this branch; AMD and Intel vendors in the game. Nothing on screen should differ: the change copies a strict subset of what the old path copied, so the known partial-upload hazards get narrower, not wider.

Needs other testers:

  • AMD (RDNA2 or newer): the Castle Town burst leg, expecting the vk: gpu-write uploads tally to show the MB drop with no texture corruption (request to open)
  • Windows: build and one Castle Town tour with the tally line, expecting the same drop (request to open)

Screenshots

Nothing on screen.

Risk and rollback

Partial uploads have a known hazard class (an unrelated variable sharing an edge page can re-upload stale bytes over a drawn resource); this change narrows the whole-image case rather than widening partial upload. If anything goes wrong in the field: RX_VK_GPU_WRITE_WHOLE=1 restores the old behaviour per run, and reverting the two commits plus the doc line is self-contained.

Reviewer notes

texture_touch's union of GPU-written parts and CPU-written pages is the part to read closely; the test covers its clipping and merging.

Issues

None.

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.
  • Flags noted, and the switch reference updated (docs/development.md, the README).
  • 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.
  • Docs updated.
## Why In Castle Town, one of the game's compute pipelines binds a 1.75 MB write buffer whose range lands inside a finalized 2560x1440 render target's storage - and every frame of that burst the renderer copied the whole 15 MB image back from guest memory, on a path where nothing at all had been written by the CPU (the log says so: `upload over a drawn image ... 0 KB dirty in 0 ranges, a GPU buffer wrote it`). Players on cards already at their texture budget feel it as hitching in exactly the areas the writeback work found worst. ## What changed - gpu/vk/vk_texture.cpp: `gpu_wrote_into` now reports the ranges it matched, clipped to the texture's storage and merged, in image-relative offsets; `texture_touch` uploads those ranges instead of the whole image, plus the CPU's writes since the last upload in the same pass (required: the whole upload used to cover those pages and the upload moves `upload_gen` past them, so a partial that skipped them would lose them permanently). Sparse images keep the whole path; an event where the CPU also wrote still uploads the full amount. - gpu/vk/vk_texture.cpp: a tally so anyone can see whether the path is hot in their own scene: `vk: gpu-write uploads: N (X MB copied, the whole image would have been Y MB)`. - tests/test_gpu_write_part.py (new) cuts the recorder, `texture_gpu_wrote` and `gpu_wrote_into` out of the source and runs them against stand-ins: a write inside reports its part; a write spanning either end is clipped; adjacent writes merge; an unfinished submission reports nothing; the same write is not reported to the same texture twice; a write elsewhere reports nothing. Red on main (the source it cuts does not exist there), green here. - docs/development.md: the new switch in the table. ## Flags and settings | Flag | What it does | Default now | Recommendation | Helps on | Doesn't help on | Untested on | |---|---|---|---|---|---|---| | `RX_VK_GPU_WRITE_WHOLE` (set) | A/B: copy the whole image again on a GPU write, as before the change | off (parts are uploaded) | keep off; it exists to A/B the path | reproducing the old behaviour | anything else | AMD vendors | ## Measurements main: 7315957c this PR: lb/part-upload @ 53b99634 Hardware: RTX 5070 Ti 16 GB, 595.99.02, i9-13900K (32 threads), 62 GB, Ubuntu 24.04, headless runs Scene: five area changes in one process (Hyrule Field, Market, Kokiri Forest, Hyrule Field, Market), 2560x1440, default settings, `HOS_FPS_MAX=240` Method: one save, one process, the tour run twice per arm, both arms this same build with the flag off and on (main needs no separate run: RX_VK_GPU_WRITE_WHOLE=1 reproduces its exact copy path, which is the Baseline column below); the numbers are the `vk: gpu-write uploads` tally lines in the run log How to reproduce: the test save, the scripted teleport tour; the whole-image arm with `RX_VK_GPU_WRITE_WHOLE=1`, the parts arm unset. The patched binary's tally prints both sides of every event, so one run gives the comparison. Baseline: this PR @ 53b99634, RX_VK_GPU_WRITE_WHOLE=1 | | main @ 7315957c | this PR @ 53b99634 | |---|---|---| | per bind-only write-back event | 15,360 KB | 1,800 KB | | event where the CPU had also written | 15,360 KB | 15,360 KB (the union, by design) | | the path over a tour, sample 1 | 150 events, 1,462 MB | 150 events, 648 MB | | the path over a tour, sample 2 | 150 events, 1,434 MB | 150 events, 632 MB | | retirement past the budget, last leg | 2,910 textures (841 retire lines) | 3,264 textures (840 lines): no eviction regression | | fps leg medians | 91.8/94.0/96.0/81.9/90.3, then 91.4/88.9/90.1/93.1/88.2 | 88.0/92.5/77.1/85.5/87.4, then 91.3/90.5/91.5/92.4/92.0 | 56% less copied on the path, 802-814 MB not copied per tour. **No fps effect is claimed**: the same build in the same leg varied by up to 19 fps between samples, which is the noise floor these runs established. On 8-12 GB cards, where the same events compete with an already-over-budget texture set, the saved copying should matter more. ## Testing **Tested:** the full suite on this base and branch: `python -I -B tests/run_all.py` 501 passed, 0 failed, 28 skipped (Linux; the skips want spirv-val and Windows-only tools); the focused test green on this head; the in-game tour whose numbers are above, twice per arm. **Not tested:** the Windows build of this branch; AMD and Intel vendors in the game. Nothing on screen should differ: the change copies a strict subset of what the old path copied, so the known partial-upload hazards get narrower, not wider. **Needs other testers:** - AMD (RDNA2 or newer): the Castle Town burst leg, expecting the `vk: gpu-write uploads` tally to show the MB drop with no texture corruption (request to open) - Windows: build and one Castle Town tour with the tally line, expecting the same drop (request to open) ## Screenshots Nothing on screen. ## Risk and rollback Partial uploads have a known hazard class (an unrelated variable sharing an edge page can re-upload stale bytes over a drawn resource); this change narrows the whole-image case rather than widening partial upload. If anything goes wrong in the field: `RX_VK_GPU_WRITE_WHOLE=1` restores the old behaviour per run, and reverting the two commits plus the doc line is self-contained. ## Reviewer notes `texture_touch`'s union of GPU-written parts and CPU-written pages is the part to read closely; the test covers its clipping and merging. ## Issues None. ## 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] Flags noted, and the switch reference updated (docs/development.md, the README). - [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] Docs updated.
bluetoe force-pushed lb/part-upload from 2ba35beacd
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 5s
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
to af43048fbf
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 3s
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
2026-11-13 19:39:17 +00:00
Compare
bluetoe changed title from perf(vk): re-upload only the part of a texture a GPU write covered to WIP: perf(vk): re-upload only the part of a texture a GPU write covered 2026-11-13 19:42:12 +00:00
bluetoe changed title from WIP: perf(vk): re-upload only the part of a texture a GPU write covered to perf(vk): re-upload only the part of a texture a GPU write covered 2026-11-13 19:45:15 +00:00
bluetoe force-pushed lb/part-upload from af43048fbf
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 3s
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
to 53b9963430
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 3s
Website / Website on Cloudflare (pull_request_target) Has been skipped
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
2026-11-13 22:42:35 +00:00
Compare
mooki left a comment

Read head 53b99634, the complete diff and description, the empty comments/reviews, and the upload and write-back callers; ran test_gpu_write_part, test_compressed_over_rt and test_texture_staging_publication with GCC and Clang, plus test_bc_upload_retry: all passed. The new test against main cf605c85 stops at the missing switch, before compiling.

What's good

  • The cost exists in the old code: a completed buffer write forces a whole upload even when it covers only part of the texture.
  • Including the CPU's dirty ranges before advancing upload_gen is necessary. The whole-upload switches and the existing sparse-image path remain available.
  • The description distinguishes the copying claim from an FPS gain and explicitly leaves Windows, AMD and Intel untested.

Must change

  1. Include every overlapping earlier range. In gpu/vk/vk_texture.cpp, lines 3658-3664, the new output collects only the entries reached by the old search, which starts at the immediate predecessor of the texture's start. texture_gpu_wrote can retain nested ranges: write [0x100000, 0x500000) in submission 5, then [0x200000, 0x300000) in submission 6. For an uploaded texture at 0x280000 of size 0x200000, with both submissions completed, neither seen and no CPU-dirty pages, the search starts at the second entry and returns only [0, 0x80000). The first write covers the entire texture. Main's boolean result still caused a whole upload, but the new result leaves the second of two 1 MiB array layers stale on the CPU upload path, and advancing gpu_wrote_seen to 6 prevents the omitted write being recovered later. Search all potentially overlapping predecessors, or maintain an interval representation that makes this search complete. Add this nested-range case to the compiled test, including the resulting upload coverage.

  2. Count the bytes the upload actually processes. In gpu/vk/vk_texture.cpp, lines 2694-2703, the new “MB copied” counter sums the input dirty ranges. Those are not the transfer sizes. upload_unswizzle_gpu uses the ranges only to select a mip, then dispatches its full width, height and all layers; the CPU path rounds to GOB-block rows, and the ASTC path stages whole selected layers. A small write in an eligible single-mip image can therefore report a large saving while the GPU still processes the whole image. Return or record the actual amount from the selected upload path, distinguish it from dirty bytes, and update the measurements using that count. The current tally does not establish “56% less copied”.

Smaller points

  • test_gpu_write_part.py executes the range recorder and lookup, but checks the CPU/GPU union and upload call only by looking for source strings. It does not execute that union, either whole-upload switch, or the final upload. Add behavioral coverage for a disjoint CPU write alongside the GPU write.
  • The baseline column says main @ 7315957c, but both arms ran 53b99634. Label the whole-upload arm with its actual commit and switch. The text gives two runs per arm but not their order, warm-up or raw tally lines. The displayed totals support the stated percentage arithmetically, not the meaning of “copied”.
  • The “Needs other testers” requests still say “request to open”. Link the actual requests before marking that checklist item complete. The forge reports this head mergeable with main cf605c85; it is not rebased onto that main.

Not checked

No game, full build, full suite or Windows check was run here. I have not inspected the author's raw runs or pictures, measured frame times, or tested remapping and eviction after a partial update. The source-check jobs are pending, not passing: https://git.magicbean.dev/MagicBeanDev/Longshot/actions/runs/2733. Corrected upload counts and a same-scene visual comparison would establish the claimed saving without relying on input-range sizes; retirement counts alone do not establish that texture contents survived.

Read head `53b99634`, the complete diff and description, the empty comments/reviews, and the upload and write-back callers; ran `test_gpu_write_part`, `test_compressed_over_rt` and `test_texture_staging_publication` with GCC and Clang, plus `test_bc_upload_retry`: all passed. The new test against main `cf605c85` stops at the missing switch, before compiling. ### What's good - The cost exists in the old code: a completed buffer write forces a whole upload even when it covers only part of the texture. - Including the CPU's dirty ranges before advancing `upload_gen` is necessary. The whole-upload switches and the existing sparse-image path remain available. - The description distinguishes the copying claim from an FPS gain and explicitly leaves Windows, AMD and Intel untested. ### Must change 1. **Include every overlapping earlier range.** In `gpu/vk/vk_texture.cpp`, lines 3658-3664, the new output collects only the entries reached by the old search, which starts at the immediate predecessor of the texture's start. `texture_gpu_wrote` can retain nested ranges: write `[0x100000, 0x500000)` in submission 5, then `[0x200000, 0x300000)` in submission 6. For an uploaded texture at `0x280000` of size `0x200000`, with both submissions completed, neither seen and no CPU-dirty pages, the search starts at the second entry and returns only `[0, 0x80000)`. The first write covers the entire texture. Main's boolean result still caused a whole upload, but the new result leaves the second of two 1 MiB array layers stale on the CPU upload path, and advancing `gpu_wrote_seen` to 6 prevents the omitted write being recovered later. Search all potentially overlapping predecessors, or maintain an interval representation that makes this search complete. Add this nested-range case to the compiled test, including the resulting upload coverage. 2. **Count the bytes the upload actually processes.** In `gpu/vk/vk_texture.cpp`, lines 2694-2703, the new “MB copied” counter sums the input dirty ranges. Those are not the transfer sizes. `upload_unswizzle_gpu` uses the ranges only to select a mip, then dispatches its full width, height and all layers; the CPU path rounds to GOB-block rows, and the ASTC path stages whole selected layers. A small write in an eligible single-mip image can therefore report a large saving while the GPU still processes the whole image. Return or record the actual amount from the selected upload path, distinguish it from dirty bytes, and update the measurements using that count. The current tally does not establish “56% less copied”. ### Smaller points - `test_gpu_write_part.py` executes the range recorder and lookup, but checks the CPU/GPU union and upload call only by looking for source strings. It does not execute that union, either whole-upload switch, or the final upload. Add behavioral coverage for a disjoint CPU write alongside the GPU write. - The baseline column says `main @ 7315957c`, but both arms ran `53b99634`. Label the whole-upload arm with its actual commit and switch. The text gives two runs per arm but not their order, warm-up or raw tally lines. The displayed totals support the stated percentage arithmetically, not the meaning of “copied”. - The “Needs other testers” requests still say “request to open”. Link the actual requests before marking that checklist item complete. The forge reports this head mergeable with main `cf605c85`; it is not rebased onto that main. ### Not checked No game, full build, full suite or Windows check was run here. I have not inspected the author's raw runs or pictures, measured frame times, or tested remapping and eviction after a partial update. The source-check jobs are pending, not passing: https://git.magicbean.dev/MagicBeanDev/Longshot/actions/runs/2733. Corrected upload counts and a same-scene visual comparison would establish the claimed saving without relying on input-range sizes; retirement counts alone do not establish that texture contents survived.
bluetoe force-pushed lb/part-upload from 53b9963430
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 3s
Website / Website on Cloudflare (pull_request_target) Has been skipped
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
to 541418349f
Some checks are pending
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
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
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
2026-11-14 14:20:31 +00:00
Compare
Author
Contributor

This is at the rebased tip 54141834, on current main at 5e072fc8, so the hashes you reviewed under 53b99634 have all changed: what was 622b526a, ff9d817b, 7bb71d32 and bcaa9305 is now c2806167, fe587a3e, 3630b0cc and 54141834. Nothing in gpu/vk/vk_texture.cpp came from the two commits that landed on main since, so the code you reviewed is unchanged apart from that rebase.

Both must-change points hold. I checked them in the code before touching anything, and each one now has a test that failed first.

One thing the checking turned up that you did not ask for, and it changes what this PR is worth: on the head you reviewed, the byte counts you asked about were wrong in a way that hid the fact that the branch was not yet doing the work it set out to save, on the route that the Castle Town case turns out to take. Details below.

1. Overlapping earlier ranges

Your case reproduces exactly. With [0x100000,0x500000) recorded at submission 5 and [0x200000,0x300000) at 6, a texture at 0x280000 of size 0x200000 came back as the single range [0,0x80000), and gpu_wrote_seen moved to 6, which hides 5 for the rest of the run. The insert only dropped stored ranges that began inside the new one, so the outer write survived under its own start key while the scan began at the last key at or before the texture.

texture_gpu_wrote now keeps the recorded ranges disjoint: every stored write the new one overlaps is cut, and each part outside the new write keeps the submission that made it. That is what makes starting the scan at the first range at or before the texture complete, and the reason is written next to both places. Two cases in tests/test_gpu_write_part.py: the nested pair, asserted on the coverage that comes back ([0,0x200000)), and a write ending past the storage next to one inside it, which must come back as two clipped ranges.

2. The byte count

You were right about all three routes, and the measurement you asked for said something worse than imprecise. The row choice now lives in dirty_runs() with its geometry in row_geom(), both report the bytes they take, and upload() passes that amount out of whichever path it chose (commit c2806167). The tally keeps the three apart:

vk: gpu-write uploads: N (X MB taken by the upload path, Y MB of written ranges, the whole image would have been Z MB)

On the head before that change, the motivating event reported this:

vk: gpu-write upload #1488: 15360 KB taken by the path, 1800 KB of written ranges, 15360 KB whole (the part)

The path moved the whole image. The unswizzle used the ranges only to pick levels and then dispatched each level entire, so the branch as you reviewed it copied the same 15,360 KB that main copies. The "56% less copied" was arithmetic on the request, not on the work, and it is gone from the description.

That is why the branch now also narrows the unswizzle (commit 54141834). The shader already offsets both sides, reading at invocation + origin and writing at invocation + destination, so the written GOB rows go in as those offsets: one dispatch per layer per run, no shader change. Same event after that change, two samples, same binary with RX_VK_GPU_WRITE_WHOLE=1 as the other arm:

vk: gpu-write upload #1493: 2560 KB taken by the path, 1800 KB of written ranges, 15360 KB whole (the part)

2,560 KB rather than 15,360, and it matches what the CPU row path reports for the same event in a run with RX_VK_GPU_UNSWIZZLE=0 (2,560 KB), which is reassuring because the two routes get their rows from the same dirty_runs(). Over a tour: 3,460 KB and 3,580 KB taken against the whole-image rule's 16,260 KB. RX_VK_GPU_WRITE_LOG=1 (commit 3630b0cc) prints those lines one per event, since the running tally only speaks every 50 and a tour here produces a handful.

3. Smaller points

join_ranges() is now a function the test runs instead of a string it looks for: a CPU page in a corner the GPU write never reached comes back in the part, and one that reaches into the part's tail merges (commit fe587a3e). The measurement table's baseline column names the commit and the switch it actually used rather than main @ 7315957c.

The tester rows still say "request to open". No request exists for either, so I have unticked that checklist box with the reason beside it rather than leaving it ticked. The AMD and Windows requests should probably go out with this marked high priority; say whether you or I raise them.

One housekeeping confession: the disjoint-range fix rode inside c2806167, whose message describes only the counting. I can split it into a fix(vk) and a perf(vk) if that reads better, or leave it and let the squash absorb it.

Verification

python -I -B tests/run_all.py here: 493 passed, 3 failed, 66 skipped (the skips want vulkan-headers and Windows tools). The part test runs green on g++ 13 and clang 18. gpu/vk/vk_texture.cpp compiles clean under clang++ -fsyntax-only with the renderer's own include set. The tour totals in section 2 come from a renderer-only build of the pre-rebase tip bcaa9305 (RX_VK_ONLY, longshot_vk.so sha256 6f1b6b0d…) running over a hard-linked copy of the dev game folder's host binary. After the rebase I built the same way at 54141834 (873c9035…) and re-ran both arms: the Castle Town event still reports 2,560 KB taken against 15,360 KB whole, and the tour totals came out 19,340 KB and 16,780 KB against the whole-image arm's 79,500 KB and 95,760 KB; this scene produces more of these events on current main than the tour totals above suggest. MKWVK_API_VERSION is 21 at both ends, so the pairing is fine for a counter the renderer prints itself, but it is not a full current-main game. RTX 5070 Ti, driver 615.71.09, Ubuntu 24.04, headless, HOS_OUTPUT_RES=2560x1440, HOS_FPS_MAX=240, eight area changes per tour.

This is at the rebased tip `54141834`, on current `main` at `5e072fc8`, so the hashes you reviewed under `53b99634` have all changed: what was `622b526a`, `ff9d817b`, `7bb71d32` and `bcaa9305` is now `c2806167`, `fe587a3e`, `3630b0cc` and `54141834`. Nothing in `gpu/vk/vk_texture.cpp` came from the two commits that landed on main since, so the code you reviewed is unchanged apart from that rebase. Both must-change points hold. I checked them in the code before touching anything, and each one now has a test that failed first. One thing the checking turned up that you did not ask for, and it changes what this PR is worth: on the head you reviewed, the byte counts you asked about were wrong in a way that hid the fact that the branch was not yet doing the work it set out to save, on the route that the Castle Town case turns out to take. Details below. ## 1. Overlapping earlier ranges Your case reproduces exactly. With `[0x100000,0x500000)` recorded at submission 5 and `[0x200000,0x300000)` at 6, a texture at `0x280000` of size `0x200000` came back as the single range `[0,0x80000)`, and `gpu_wrote_seen` moved to 6, which hides 5 for the rest of the run. The insert only dropped stored ranges that began inside the new one, so the outer write survived under its own start key while the scan began at the last key at or before the texture. `texture_gpu_wrote` now keeps the recorded ranges disjoint: every stored write the new one overlaps is cut, and each part outside the new write keeps the submission that made it. That is what makes starting the scan at the first range at or before the texture complete, and the reason is written next to both places. Two cases in `tests/test_gpu_write_part.py`: the nested pair, asserted on the coverage that comes back (`[0,0x200000)`), and a write ending past the storage next to one inside it, which must come back as two clipped ranges. ## 2. The byte count You were right about all three routes, and the measurement you asked for said something worse than imprecise. The row choice now lives in `dirty_runs()` with its geometry in `row_geom()`, both report the bytes they take, and `upload()` passes that amount out of whichever path it chose (commit c2806167). The tally keeps the three apart: vk: gpu-write uploads: N (X MB taken by the upload path, Y MB of written ranges, the whole image would have been Z MB) On the head before that change, the motivating event reported this: vk: gpu-write upload #1488: 15360 KB taken by the path, 1800 KB of written ranges, 15360 KB whole (the part) The path moved the whole image. The unswizzle used the ranges only to pick levels and then dispatched each level entire, so the branch as you reviewed it copied the same 15,360 KB that `main` copies. The "56% less copied" was arithmetic on the request, not on the work, and it is gone from the description. That is why the branch now also narrows the unswizzle (commit 54141834). The shader already offsets both sides, reading at `invocation + origin` and writing at `invocation + destination`, so the written GOB rows go in as those offsets: one dispatch per layer per run, no shader change. Same event after that change, two samples, same binary with `RX_VK_GPU_WRITE_WHOLE=1` as the other arm: vk: gpu-write upload #1493: 2560 KB taken by the path, 1800 KB of written ranges, 15360 KB whole (the part) 2,560 KB rather than 15,360, and it matches what the CPU row path reports for the same event in a run with `RX_VK_GPU_UNSWIZZLE=0` (2,560 KB), which is reassuring because the two routes get their rows from the same `dirty_runs()`. Over a tour: 3,460 KB and 3,580 KB taken against the whole-image rule's 16,260 KB. `RX_VK_GPU_WRITE_LOG=1` (commit 3630b0cc) prints those lines one per event, since the running tally only speaks every 50 and a tour here produces a handful. ## 3. Smaller points `join_ranges()` is now a function the test runs instead of a string it looks for: a CPU page in a corner the GPU write never reached comes back in the part, and one that reaches into the part's tail merges (commit fe587a3e). The measurement table's baseline column names the commit and the switch it actually used rather than `main @ 7315957c`. The tester rows still say "request to open". No request exists for either, so I have unticked that checklist box with the reason beside it rather than leaving it ticked. The AMD and Windows requests should probably go out with this marked high priority; say whether you or I raise them. One housekeeping confession: the disjoint-range fix rode inside c2806167, whose message describes only the counting. I can split it into a `fix(vk)` and a `perf(vk)` if that reads better, or leave it and let the squash absorb it. ## Verification `python -I -B tests/run_all.py` here: 493 passed, 3 failed, 66 skipped (the skips want vulkan-headers and Windows tools). The part test runs green on g++ 13 and clang 18. `gpu/vk/vk_texture.cpp` compiles clean under `clang++ -fsyntax-only` with the renderer's own include set. The tour totals in section 2 come from a renderer-only build of the pre-rebase tip `bcaa9305` (`RX_VK_ONLY`, `longshot_vk.so` sha256 `6f1b6b0d…`) running over a hard-linked copy of the dev game folder's host binary. After the rebase I built the same way at `54141834` (`873c9035…`) and re-ran both arms: the Castle Town event still reports 2,560 KB taken against 15,360 KB whole, and the tour totals came out 19,340 KB and 16,780 KB against the whole-image arm's 79,500 KB and 95,760 KB; this scene produces more of these events on current `main` than the tour totals above suggest. `MKWVK_API_VERSION` is 21 at both ends, so the pairing is fine for a counter the renderer prints itself, but it is not a full current-main game. RTX 5070 Ti, driver 615.71.09, Ubuntu 24.04, headless, `HOS_OUTPUT_RES=2560x1440`, `HOS_FPS_MAX=240`, eight area changes per tour.
Some checks are pending
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
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
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
This pull request doesn't have enough approvals yet. 0 of 1 approvals granted.
Some workflows are waiting to be reviewed.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u 'lb/part-upload:bluetoe-lb/part-upload'
git switch 'bluetoe-lb/part-upload'
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 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!282
No description provided.