perf(vk): the BC3 scratch has the layers a run uses, 20 MB instead of 160 #384

Open
p0lux wants to merge 2 commits from p0lux/Longshot:perf/bc-scratch-one-layer into main
Contributor

Why

Every card without ASTC of its own (every PC card, and a Steam Deck) re-encodes the game's big ASTC textures to BC3 through a scratch image and its buffer, kept once made. They are made for 8 layers, 160 MB, but an upload has gone one layer at a time since the fix for Hyrule Field's dark tile outlines: 140 MB held for the session and never written, of VRAM on a card and of the 16 GB a Steam Deck shares with the game.

What changed

  • textures: bc_layers() in gpu/vk/vk_texture.cpp, the layers of one run: 1, or RX_VK_BC_LAYERS up to 8. bc_scratch_make(), bc_scratch2_make() and bc_scratch_layout() make the two scratches and set their first layout for that many layers, not for kBcLayers: 16 MB of image and 4 MB of buffer a layer.
  • textures: upload_astc_gpu() takes its run from bc_layers(), which is what it read from the switch before. texture_copy_into_bc() goes by the scratch's layers too, so a copy of several layers into a BC3 texture is one run a layer, as an upload's.
  • log: the "given back" line (RX_VK_BC_IDLE_S) says the scratch's size as it is.
  • checks: tests/test_bc_scratch_layers.py (new) checks both scratches, their layout and both users ask bc_layers(), runs it for the switch's values, and runs the two loops themselves: texture_copy_into_bc() whole and the lines of upload_astc_gpu() that form a run are cut into tests/bc_scratch_batches_test.cpp (new) with a spy in place of Vulkan (a request larger than the scratch, a last partial run, source and destination layers that are not 0, an array's dirty layers with gaps, the resized path). tests/parallel_init_order_test.cpp stubs bc_layers() in place of the constant.
  • docs: RX_VK_BC_LAYERS in docs/development.md's switch reference (it was not there); a run file in docs/tested/.

Flags and settings

Flag What it does Default now Recommendation Helps on Doesn't help on Untested on
RX_VK_BC_LAYERS=<n> (existing) The layers of one decode and encode run, 1 to 8. It now sizes the scratch too: 20 MB a layer 1, as before keep 1 every GPU that takes the BC3 route: 140 MB less held a GPU with ASTC of its own, or with ASTC kept as RGBA8 (no scratch) everything but a Steam Deck OLED

No new flag. RX_VK_BC_LAYERS=8 gives the scratch its old size, with runs of 8 layers.

Measurements

main: fbd42024 (2026-11-13)
this PR: perf/bc-scratch-one-layer @ 000cc9f2 (the change on that main). The branch is now that change rebased onto main 0caf0618 (b7b039c8) and a docs commit; the rebase had no conflict, and main has not touched the scratch's code in between.
Hardware: Steam Deck OLED (AMD Custom APU 0932, RADV VANGOGH, Mesa 26.2.0-devel), 16 GB shared with the GPU, SteamOS 3.9.5, kernel 7.2.7-valve2; the GPU's clock fixed at 1600 MHz
Scene: three saves, standing still: a heavy view (about 2,150 draws a frame), Kokiri Forest (about 2,600), Hyrule Field (about 1,600); no screen (headless), handheld profile, Minimum preset, the game's own upscaler, no frame generation, no cap
Method: the back end built alone from each commit, on a host and game folder of main 000f19fb (vk_api.h is the same). Runs alternated main, this PR (and a third renderer that is not this PR's), 3 rounds on the heavy view and 2 on the others: 7 pairs. Each run starts the game, loads the save by script, waits 3 log windows, measures exactly 720 presents, then reads what the driver counts for the process (drm-memory-gtt and drm-memory-vram in /proc/<pid>/fdinfo: on this machine GPU memory is system memory) and MemAvailable in /proc/meminfo
How to reproduce: a save in Hyrule Field or any area with terrain; after the load, grep drm-memory /proc/$(pgrep -x longshot)/fdinfo/* with main and with this PR (or, on one build of this PR, with RX_VK_BC_LAYERS=8 against unset)

main @ fbd42024 this PR @ 000cc9f2 Δ
GPU memory in system RAM (drm-memory-gtt), heavy view, median of 3 3,884 MiB 3,737 MiB -147 MiB
the same, Kokiri Forest, mean of 2 4,071 MiB 3,910 MiB -161 MiB
the same, Hyrule Field, mean of 2 3,496 MiB 3,367 MiB -130 MiB
the 7 pairs' differences, median -139 MiB
MemAvailable of the whole system, the 7 pairs' differences, median +145 MiB
fps, heavy view, median of 3 24.34 24.47 +0.13
the render thread's instructions a frame, heavy view 62.10 M 61.98 M -0.2%
start to in game, median of 7 40 s 38 s -2 s
drm-memory-gtt (MiB)  heavy view  main 3884 3908 3800 | this PR 3675 3772 3737
                      Kokiri      main 4077 4064      | this PR 3901 3919
                      Field       main 3482 3510      | this PR 3362 3371
pair differences      -209 -136 -63 -176 -145 -120 -139
MemAvailable (MiB)    heavy view  main 1408 1365 1531 | this PR 1764 1638 1543
                      Kokiri      main 1085 1066      | this PR 1258 1204
                      Field       main 2206 2033      | this PR 2217 2178
fps                   heavy view  main 24.34 24.27 24.35 | this PR 24.52 24.37 24.47
                      Kokiri      main 23.01 22.87       | this PR 22.95 23.05
                      Field       main 21.26 21.33       | this PR 21.49 21.28
start to in game (s)  heavy view  main 39 40 42 | this PR 38 41 37
                      Kokiri      main 42 38    | this PR 38 38
                      Field       main 38 41    | this PR 39 36

The values are the kernel's counters in KiB divided by 1,024 (MiB). MemAvailable is the whole system's and is there as context, not as a second saving to add to the first. The fps and the start times show no cost; they are not a claim of speed.

The order of each round was main, this PR, then the third renderer, which is main with the six commits of #391 (nothing of this PR). A log window is 120 presents (the log's "N presents, X fps" line): the 3 windows waited are the first 360 presents after the save is in game, before the 720 measured. The measured commit is kept on the fork as measured/bc-scratch-000cc9f2 (https://git.magicbean.dev/p0lux/Longshot/src/branch/measured/bc-scratch-000cc9f2).

The same 139 MiB (median of 7 pairs, the same scenes) came out a few hours earlier with this commit on top of other renderer-only changes against the same tree without them. A run's GPU memory varies by about 100 MB from one start to the next, hence the pairs.

Testing

Tested:

  • python -I -B tests/run_all.py on macOS with the Vulkan headers given: 487 passed, 36 failed, 39 skipped; the 36 fail the same way on main 0caf0618 on that machine (no /proc, no posix_fallocate, Python 3.9, the Linux launch scripts and the like): nx2/test_nup_zstd_heads.py, and in tests/: test_appimage_package_retry, test_appimage_source_headers, test_bake_portability, test_chunk_split, test_collect_wide, test_control, test_frame_rate_cap_step, test_framegen_resources, test_game_layout, test_gpu_hang_watch, test_kept_translation, test_lift_cache, test_linux_preflight, test_longshot_launch, test_low_memory_notice, test_make_game_cache, test_mem_gate, test_mem_gate_children, test_mirror_trim, test_missing_nup, test_module_ids, test_movie_frame_count, test_oot_saves, test_oot_state_tap, test_pad_layout, test_pcache_header_read, test_pipeline_cache_file, test_pipeline_cache_save, test_pipeline_play_cache, test_prof_symbols, test_program_cache_cleanup, test_program_cache_one_variant, test_prompt_skip, test_sticky_hot_pieces, test_tested_configurations. test_bc_scratch_layers.py fails on main (no bc_layers()) and passes here; test_parallel_init_order.py, test_mem_trim.py and test_bc_upload_retry.py, which read this code, pass.
  • In the game, a Steam Deck OLED, without a screen: 7 runs of 000cc9f2 on the three scenes above and 3 more with pictures, and more than 80 runs of the same commit on top of other renderer-only changes: no error, no GPU timeout in the kernel log. The log confirms the route is taken there (vk: big ASTC textures are kept as BC3).
  • The picture, by the renderer's own captures (RX_VK_SHOTS), standing still on the three scenes: main twice, then this PR, the last capture both runs have compared pixel by pixel (the mean absolute difference of a channel, 0 to 255). Two runs of main differ by 18.3 to 18.9, since a scene starts in one of two states (Hyrule Field at 1,571 or 1,614 draws) and never stands still; this PR against the run of main in its own state: 8.1 (Hyrule Field, whose ground samples the terrain's material arrays), 12.0 (Kokiri Forest), 5.7 (heavy view); against the other run 18.9, 20.4 and 17.1, as main against main.
  • With the picture, by a person: a build holding this change with other renderer-only ones (3a3a6475, on main 514591a3) played on the Steam Deck OLED on 2026-11-14: nothing wrong seen on screen.
  • Linux: the back end builds (clang, on the Deck). Windows: longshot_vk.dll cross-builds with llvm-mingw 20261104 from a branch holding this commit and other renderer-only ones, 0 failed.

Not tested:

  • A copy of several layers into a BC3 texture in particular: these scenes may not make one, and the log does not say.
  • A desktop card (the saving should be the same 140 MB of VRAM); the second scratch (RX_VK_UNEVEN_BC=0, AMD's uneven block levels), which is off by default.
  • Windows in the game.

Needs other testers:

  • A desktop card, any maker, 4 to 8 GB above all: the textures are intact in Hyrule Field and a dungeon, and the VRAM the game holds after the first load is about 140 MB lower than on main (request to open)

Screenshots

Nothing on screen.

Risk and rollback

The scratch can no longer hold 8 layers unless the switch asks for them, so a copy of several layers into a BC3 texture takes one run a layer instead of one run for up to 8: more commands for the same GPU work, and no longer the runs of several layers that an upload stopped making because they drew layers wrong (upload_astc_gpu()'s comment; with RX_VK_BC_LAYERS=8 the pictures of Kokiri Forest and the heavy view here differ from the default's by 19 to 27, well above the 7 to 8 of two runs alike). RX_VK_BC_LAYERS=8 brings back the old size and the old copy; reverting the commit removes the change.

Reviewer notes

  • @jcabarca claimed fix/vk-bc-copy-extent, in texture_copy_into_bc() too (the team room, 2026-11-14); I have not seen its branch published yet. This PR's hunk there is the two lines of the loop over the layers (l0 += bc_layers() and std::min(bc_layers(), layers - l0)), nothing of the extents. Whichever goes second, the combined version has to keep bc_layers() in that loop: kBcLayers there would write scratch layers that are not made.
  • bc_idle_s()'s comment says keeping the scratch costs "160-288 MB of VRAM held after the first decode" (issue 993); it is 20 to 36 MB with this.

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 file in docs/tested/ for each new measured run (format in docs/tested-configurations.md; no appended row or assembled view).
  • 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.
  • Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths.
  • Docs updated.
## Why Every card without ASTC of its own (every PC card, and a Steam Deck) re-encodes the game's big ASTC textures to BC3 through a scratch image and its buffer, kept once made. They are made for 8 layers, 160 MB, but an upload has gone one layer at a time since the fix for Hyrule Field's dark tile outlines: 140 MB held for the session and never written, of VRAM on a card and of the 16 GB a Steam Deck shares with the game. ## What changed - textures: `bc_layers()` in `gpu/vk/vk_texture.cpp`, the layers of one run: 1, or `RX_VK_BC_LAYERS` up to 8. `bc_scratch_make()`, `bc_scratch2_make()` and `bc_scratch_layout()` make the two scratches and set their first layout for that many layers, not for `kBcLayers`: 16 MB of image and 4 MB of buffer a layer. - textures: `upload_astc_gpu()` takes its run from `bc_layers()`, which is what it read from the switch before. `texture_copy_into_bc()` goes by the scratch's layers too, so a copy of several layers into a BC3 texture is one run a layer, as an upload's. - log: the "given back" line (`RX_VK_BC_IDLE_S`) says the scratch's size as it is. - checks: `tests/test_bc_scratch_layers.py` (new) checks both scratches, their layout and both users ask `bc_layers()`, runs it for the switch's values, and runs the two loops themselves: `texture_copy_into_bc()` whole and the lines of `upload_astc_gpu()` that form a run are cut into `tests/bc_scratch_batches_test.cpp` (new) with a spy in place of Vulkan (a request larger than the scratch, a last partial run, source and destination layers that are not 0, an array's dirty layers with gaps, the resized path). `tests/parallel_init_order_test.cpp` stubs `bc_layers()` in place of the constant. - docs: `RX_VK_BC_LAYERS` in `docs/development.md`'s switch reference (it was not there); a run file in `docs/tested/`. ## Flags and settings | Flag | What it does | Default now | Recommendation | Helps on | Doesn't help on | Untested on | |---|---|---|---|---|---|---| | `RX_VK_BC_LAYERS=<n>` (existing) | The layers of one decode and encode run, 1 to 8. It now sizes the scratch too: 20 MB a layer | 1, as before | keep 1 | every GPU that takes the BC3 route: 140 MB less held | a GPU with ASTC of its own, or with ASTC kept as RGBA8 (no scratch) | everything but a Steam Deck OLED | No new flag. `RX_VK_BC_LAYERS=8` gives the scratch its old size, with runs of 8 layers. ## Measurements main: fbd42024 (2026-11-13) this PR: perf/bc-scratch-one-layer @ 000cc9f2 (the change on that main). The branch is now that change rebased onto main 0caf0618 (b7b039c8) and a docs commit; the rebase had no conflict, and main has not touched the scratch's code in between. Hardware: Steam Deck OLED (AMD Custom APU 0932, RADV VANGOGH, Mesa 26.2.0-devel), 16 GB shared with the GPU, SteamOS 3.9.5, kernel 7.2.7-valve2; the GPU's clock fixed at 1600 MHz Scene: three saves, standing still: a heavy view (about 2,150 draws a frame), Kokiri Forest (about 2,600), Hyrule Field (about 1,600); no screen (headless), handheld profile, Minimum preset, the game's own upscaler, no frame generation, no cap Method: the back end built alone from each commit, on a host and game folder of main 000f19fb (`vk_api.h` is the same). Runs alternated main, this PR (and a third renderer that is not this PR's), 3 rounds on the heavy view and 2 on the others: 7 pairs. Each run starts the game, loads the save by script, waits 3 log windows, measures exactly 720 presents, then reads what the driver counts for the process (`drm-memory-gtt` and `drm-memory-vram` in `/proc/<pid>/fdinfo`: on this machine GPU memory is system memory) and `MemAvailable` in `/proc/meminfo` How to reproduce: a save in Hyrule Field or any area with terrain; after the load, `grep drm-memory /proc/$(pgrep -x longshot)/fdinfo/*` with main and with this PR (or, on one build of this PR, with `RX_VK_BC_LAYERS=8` against unset) | | main @ fbd42024 | this PR @ 000cc9f2 | Δ | |---|---|---|---| | GPU memory in system RAM (`drm-memory-gtt`), heavy view, median of 3 | 3,884 MiB | 3,737 MiB | -147 MiB | | the same, Kokiri Forest, mean of 2 | 4,071 MiB | 3,910 MiB | -161 MiB | | the same, Hyrule Field, mean of 2 | 3,496 MiB | 3,367 MiB | -130 MiB | | the 7 pairs' differences, median | | | -139 MiB | | `MemAvailable` of the whole system, the 7 pairs' differences, median | | | +145 MiB | | fps, heavy view, median of 3 | 24.34 | 24.47 | +0.13 | | the render thread's instructions a frame, heavy view | 62.10 M | 61.98 M | -0.2% | | start to in game, median of 7 | 40 s | 38 s | -2 s | ``` drm-memory-gtt (MiB) heavy view main 3884 3908 3800 | this PR 3675 3772 3737 Kokiri main 4077 4064 | this PR 3901 3919 Field main 3482 3510 | this PR 3362 3371 pair differences -209 -136 -63 -176 -145 -120 -139 MemAvailable (MiB) heavy view main 1408 1365 1531 | this PR 1764 1638 1543 Kokiri main 1085 1066 | this PR 1258 1204 Field main 2206 2033 | this PR 2217 2178 fps heavy view main 24.34 24.27 24.35 | this PR 24.52 24.37 24.47 Kokiri main 23.01 22.87 | this PR 22.95 23.05 Field main 21.26 21.33 | this PR 21.49 21.28 start to in game (s) heavy view main 39 40 42 | this PR 38 41 37 Kokiri main 42 38 | this PR 38 38 Field main 38 41 | this PR 39 36 ``` The values are the kernel's counters in KiB divided by 1,024 (MiB). `MemAvailable` is the whole system's and is there as context, not as a second saving to add to the first. The fps and the start times show no cost; they are not a claim of speed. The order of each round was main, this PR, then the third renderer, which is main with the six commits of https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/391 (nothing of this PR). A log window is 120 presents (the log's "N presents, X fps" line): the 3 windows waited are the first 360 presents after the save is in game, before the 720 measured. The measured commit is kept on the fork as `measured/bc-scratch-000cc9f2` (https://git.magicbean.dev/p0lux/Longshot/src/branch/measured/bc-scratch-000cc9f2). The same 139 MiB (median of 7 pairs, the same scenes) came out a few hours earlier with this commit on top of other renderer-only changes against the same tree without them. A run's GPU memory varies by about 100 MB from one start to the next, hence the pairs. ## Testing **Tested:** - `python -I -B tests/run_all.py` on macOS with the Vulkan headers given: 487 passed, 36 failed, 39 skipped; the 36 fail the same way on main 0caf0618 on that machine (no `/proc`, no `posix_fallocate`, Python 3.9, the Linux launch scripts and the like): `nx2/test_nup_zstd_heads.py`, and in `tests/`: `test_appimage_package_retry`, `test_appimage_source_headers`, `test_bake_portability`, `test_chunk_split`, `test_collect_wide`, `test_control`, `test_frame_rate_cap_step`, `test_framegen_resources`, `test_game_layout`, `test_gpu_hang_watch`, `test_kept_translation`, `test_lift_cache`, `test_linux_preflight`, `test_longshot_launch`, `test_low_memory_notice`, `test_make_game_cache`, `test_mem_gate`, `test_mem_gate_children`, `test_mirror_trim`, `test_missing_nup`, `test_module_ids`, `test_movie_frame_count`, `test_oot_saves`, `test_oot_state_tap`, `test_pad_layout`, `test_pcache_header_read`, `test_pipeline_cache_file`, `test_pipeline_cache_save`, `test_pipeline_play_cache`, `test_prof_symbols`, `test_program_cache_cleanup`, `test_program_cache_one_variant`, `test_prompt_skip`, `test_sticky_hot_pieces`, `test_tested_configurations`. `test_bc_scratch_layers.py` fails on main (no `bc_layers()`) and passes here; `test_parallel_init_order.py`, `test_mem_trim.py` and `test_bc_upload_retry.py`, which read this code, pass. - In the game, a Steam Deck OLED, without a screen: 7 runs of 000cc9f2 on the three scenes above and 3 more with pictures, and more than 80 runs of the same commit on top of other renderer-only changes: no error, no GPU timeout in the kernel log. The log confirms the route is taken there (`vk: big ASTC textures are kept as BC3`). - The picture, by the renderer's own captures (`RX_VK_SHOTS`), standing still on the three scenes: main twice, then this PR, the last capture both runs have compared pixel by pixel (the mean absolute difference of a channel, 0 to 255). Two runs of main differ by 18.3 to 18.9, since a scene starts in one of two states (Hyrule Field at 1,571 or 1,614 draws) and never stands still; this PR against the run of main in its own state: 8.1 (Hyrule Field, whose ground samples the terrain's material arrays), 12.0 (Kokiri Forest), 5.7 (heavy view); against the other run 18.9, 20.4 and 17.1, as main against main. - With the picture, by a person: a build holding this change with other renderer-only ones (3a3a6475, on main 514591a3) played on the Steam Deck OLED on 2026-11-14: nothing wrong seen on screen. - Linux: the back end builds (clang, on the Deck). Windows: `longshot_vk.dll` cross-builds with llvm-mingw 20261104 from a branch holding this commit and other renderer-only ones, 0 failed. **Not tested:** - A copy of several layers into a BC3 texture in particular: these scenes may not make one, and the log does not say. - A desktop card (the saving should be the same 140 MB of VRAM); the second scratch (`RX_VK_UNEVEN_BC=0`, AMD's uneven block levels), which is off by default. - Windows in the game. **Needs other testers:** - A desktop card, any maker, 4 to 8 GB above all: the textures are intact in Hyrule Field and a dungeon, and the VRAM the game holds after the first load is about 140 MB lower than on main (request to open) ## Screenshots Nothing on screen. ## Risk and rollback The scratch can no longer hold 8 layers unless the switch asks for them, so a copy of several layers into a BC3 texture takes one run a layer instead of one run for up to 8: more commands for the same GPU work, and no longer the runs of several layers that an upload stopped making because they drew layers wrong (`upload_astc_gpu()`'s comment; with `RX_VK_BC_LAYERS=8` the pictures of Kokiri Forest and the heavy view here differ from the default's by 19 to 27, well above the 7 to 8 of two runs alike). `RX_VK_BC_LAYERS=8` brings back the old size and the old copy; reverting the commit removes the change. ## Reviewer notes - @jcabarca claimed `fix/vk-bc-copy-extent`, in `texture_copy_into_bc()` too (the team room, 2026-11-14); I have not seen its branch published yet. This PR's hunk there is the two lines of the loop over the layers (`l0 += bc_layers()` and `std::min(bc_layers(), layers - l0)`), nothing of the extents. Whichever goes second, the combined version has to keep `bc_layers()` in that loop: `kBcLayers` there would write scratch layers that are not made. - `bc_idle_s()`'s comment says keeping the scratch costs "160-288 MB of VRAM held after the first decode" (issue 993); it is 20 to 36 MB with this. ## 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 file in `docs/tested/` for each new measured run (format in `docs/tested-configurations.md`; no appended row or assembled view). - [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] Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. - [x] Docs updated.
Decoded ASTC is encoded to BC3 through a 2048 x 2048 RGBA8 scratch image and its output buffer, kept once made. Both
were made for 8 layers (128 + 32 MB), but an upload's run has been one layer since Hyrule Field's dark tile outlines
(RX_VK_BC_LAYERS=<n> for more): 140 of the 160 MB were held and never written. Only a copy into a BC3 texture still
went 8 layers at a time.

bc_layers() is the run's layers, 1 or RX_VK_BC_LAYERS, and the scratch, the second scratch (RX_VK_UNEVEN_BC=0) and
their first layout change are made for that many: 20 MB, 36 with the second. A copy into a BC3 texture goes by the
same runs as an upload.

tests/test_bc_scratch_layers.py checks the scratch and both users ask bc_layers(), and runs it;
tests/parallel_init_order_test.cpp stubs it in place of the constant.

Assisted-by: Claude Fable 5.2
Co-Authored-By: Claude Fable 5.2 <[email protected]>
docs(tested): the BC3 scratch's layers on a Steam Deck OLED
Some checks failed
Website / Website on Cloudflare (pull_request_target) Has been skipped
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
Website / Website on Cloudflare (pull_request) Has been cancelled
8b7e3c17fa
Main fbd42024 against the change at 000cc9f2, 7 pairs alternated on three scenes: the process's GPU memory, the
memory left available, fps, the load time, and the renderer's own pictures against two runs of main.

Assisted-by: Claude Fable 5.2
Co-Authored-By: Claude Fable 5.2 <[email protected]>
mooki left a comment

Read head 8b7e3c17 against main 0caf0618: the complete five-file diff, description, empty comments and reviews, scratch allocation and retirement, both callers and the ASTC/BC3/BC7 shader consumers; ran the new scratch test and the changed parallel-initialization fixture with GCC and Clang, plus the memory-trim and BC upload-retry checks. All six invocations pass. No game, renderer build or full suite was run.

What's good

  • The allocation cost is real. Main reserves a 2048 × 2048 RGBA8 image and a BC output buffer for 8 layers even when uploads process one. The requested storage falls from 160 MiB to 20 MiB by default, or from 288 MiB to 36 MiB when the optional second image is needed. Driver allocation overhead is separate.
  • Both image constructors, both views, the first layout transitions, the output buffer and both batch loops use the same cached layer count. This is not just a smaller allocation with an unchanged eight-layer writer.
  • The reported memory samples consistently favor the change. They support a memory saving on the tested Deck, without needing a frame-rate gain to justify it.

Must change

No patch-introduced functional defect found by tracing these paths or running the focused checks. It merges cleanly with main 0caf0618; the WIP status is not a merge conflict.

Scratch users and larger requests

  • The capacity is fixed, not demand-grown. gpu/vk/vk_texture.cpp, lines 637-640, caches RX_VK_BC_LAYERS on the first call: 1 when unset, bounded to 1 through 8 by the existing parsing. The images and buffer are allocated lazily at that capacity. Changing the environment later does not resize them. Optional idle retirement and later recreation use the same cached value. The second image can be added later, but that does not grow the layer capacity.
  • Uploads split the request. Lines 1068-1093 group only consecutive selected layers, stop at bc_layers(), decode into scratch layers starting at 0, and encode to the original destination layer. A texture or dirty selection larger than the scratch becomes more batches, not a larger dispatch into the same storage. Gaps in a dirty selection end a batch.
  • Texture copies can still request several layers. gpu/vk/vk_xfer.cpp, lines 264-276, passes the source/destination-clamped layer count to texture_copy_into_bc(). Its changed loop at gpu/vk/vk_texture.cpp, lines 2977-2986, splits that count, advances both source and destination by l0, and reuses scratch layers starting at 0. With capacity 4, a 9-layer copy is batches of 4, 4 and 1; with the default it is nine single-layer batches. The extra commands are an intentional trade-off, not dropped layers.
  • The encoder and resized path remain bounded. bc_encode_into(), lines 811-830, dispatches exactly the batch's n layers and copies those blocks from bc_out. Both BC3 and BC7 write 16 bytes per 4 × 4 block, packed by the batch's dimensions and layer number. bc_encode_resized(), lines 833-842, blits the same n layers to the optional second scratch and uses that same output buffer. These helpers do not clamp independently, but their only callers supply the bounded batches. Existing image-selection and copy guards keep their dimensions within 2048 × 2048.
  • Other multi-layer paths do not use this scratch. The non-BC ASTC upload at lines 1100-1133 can dispatch all texture layers, but writes the texture's own view. Native/raw BC copies use their own images; buffer-to-ASTC copies update guest storage and reach the upload path again. Retirement at lines 890-905 and 4204-4222 keeps both image and output-buffer ownership until the recorded GPU work completes. No additional scratch writer was found.

These are source traces, not a GPU reproduction of a multi-layer or resized copy.

Measurements and smaller points

  1. The memory comparison meets the basic pairing rule. It names baseline fbd42024, candidate 000cc9f2, one Deck and matched settings, and reports 3 interleaved pairs in the heavy view and 2 in each other scene. With two observations, the displayed mean is also the conventional median. All 7 process-GPU-memory differences are negative, from -209 to -63 MB in the displayed units, with median -139 MB. Within-scene ranges are separate: heavy view 3800-3908 versus 3675-3772 MB, Kokiri 4064-4077 versus 3901-3919 MB, Field 3482-3510 versus 3362-3371 MB. The heavy-view repeat spreads are 108 and 97 MB. That supports a saving around the intended allocation reduction here, not exactly 140 MB less total process memory in every run. The +145 MB median MemAvailable difference is whole-system context, not another saving to add to the GPU-memory result.

  2. Keep the evidence reproducible and the speed claim narrow. Retain the per-run memory-counter lines, timestamps and exact order including the third renderer, identify that renderer, and say what the three warm-up log windows cover. Label the conversion to MB or MiB. The memory table is useful, but the raw counter records and run logs are not attached. The small FPS differences do not establish a speed gain, and the 40 s versus 38 s startup row has no per-run values to check. No p99 frame-time or 1% low evidence is supplied. The current allocation and batching changes can be read directly, but 000cc9f2 is not available from the fetched history or its public commit endpoint, so I could not independently compare the measured patch with this rebased head. Preserve that commit or its exact patch and build identity when carrying the numbers forward.

  3. Exercise the batch behavior, not only its spelling. tests/test_bc_scratch_layers.py, lines 55-78, checks source strings and runs the real setting reader; its capacity calculation is a separate formula in the fixture. It does not execute allocation, the upload loop, the copy loop or an encoder. A game-free recording test using the production loops could check a request larger than capacity, a final partial batch, nonzero source/destination layers, sparse dirty layers and the resized path. The existing initialization test preserves its ordering checks, but its Vulkan spy does not check layer bounds. This is a coverage improvement, not an observed defect.

  4. Pin the other copy change before calling the rebase two lines. The description names fix/vk-bc-copy-extent. The overlap on this side is texture_copy_into_bc(), lines 2958-2988, specifically the two batch-size lines at 2977-2978 beside the copy-region and encoder arguments at 2980-2986. This patch changes layer batching, not the copy extent. I could not locate the named counterpart in the 134 public open pull requests or fetched branch history, so its hunks and the claimed two-line rebase remain unchecked. Link its published head; any combined version must retain the bc_layers() bound rather than restore kBcLayers batching against a one-layer allocation.

Not checked

No Windows fixture execution, Vulkan validation, desktop GPU, BC7 gameplay, resized scratch, confirmed multi-layer copy or independent memory measurement here. The reported pixel-difference totals are not a visual inspection, and no pictures were supplied to inspect. A bounded, inspected run covering the actual copy path and the optional second scratch would settle the remaining runtime coverage. Keep the explicit WIP limitations, open the stated desktop test request before readying, and name the 36 inherited macOS test failures rather than only their general causes.

Read head `8b7e3c17` against main `0caf0618`: the complete five-file diff, description, empty comments and reviews, scratch allocation and retirement, both callers and the ASTC/BC3/BC7 shader consumers; ran the new scratch test and the changed parallel-initialization fixture with GCC and Clang, plus the memory-trim and BC upload-retry checks. All six invocations pass. No game, renderer build or full suite was run. ### What's good - The allocation cost is real. Main reserves a 2048 × 2048 RGBA8 image and a BC output buffer for 8 layers even when uploads process one. The requested storage falls from 160 MiB to 20 MiB by default, or from 288 MiB to 36 MiB when the optional second image is needed. Driver allocation overhead is separate. - Both image constructors, both views, the first layout transitions, the output buffer and both batch loops use the same cached layer count. This is not just a smaller allocation with an unchanged eight-layer writer. - The reported memory samples consistently favor the change. They support a memory saving on the tested Deck, without needing a frame-rate gain to justify it. ### Must change No patch-introduced functional defect found by tracing these paths or running the focused checks. It merges cleanly with main `0caf0618`; the WIP status is not a merge conflict. ### Scratch users and larger requests - **The capacity is fixed, not demand-grown.** `gpu/vk/vk_texture.cpp`, lines 637-640, caches `RX_VK_BC_LAYERS` on the first call: 1 when unset, bounded to 1 through 8 by the existing parsing. The images and buffer are allocated lazily at that capacity. Changing the environment later does not resize them. Optional idle retirement and later recreation use the same cached value. The second image can be added later, but that does not grow the layer capacity. - **Uploads split the request.** Lines 1068-1093 group only consecutive selected layers, stop at `bc_layers()`, decode into scratch layers starting at 0, and encode to the original destination layer. A texture or dirty selection larger than the scratch becomes more batches, not a larger dispatch into the same storage. Gaps in a dirty selection end a batch. - **Texture copies can still request several layers.** `gpu/vk/vk_xfer.cpp`, lines 264-276, passes the source/destination-clamped layer count to `texture_copy_into_bc()`. Its changed loop at `gpu/vk/vk_texture.cpp`, lines 2977-2986, splits that count, advances both source and destination by `l0`, and reuses scratch layers starting at 0. With capacity 4, a 9-layer copy is batches of 4, 4 and 1; with the default it is nine single-layer batches. The extra commands are an intentional trade-off, not dropped layers. - **The encoder and resized path remain bounded.** `bc_encode_into()`, lines 811-830, dispatches exactly the batch's `n` layers and copies those blocks from `bc_out`. Both BC3 and BC7 write 16 bytes per 4 × 4 block, packed by the batch's dimensions and layer number. `bc_encode_resized()`, lines 833-842, blits the same `n` layers to the optional second scratch and uses that same output buffer. These helpers do not clamp independently, but their only callers supply the bounded batches. Existing image-selection and copy guards keep their dimensions within 2048 × 2048. - **Other multi-layer paths do not use this scratch.** The non-BC ASTC upload at lines 1100-1133 can dispatch all texture layers, but writes the texture's own view. Native/raw BC copies use their own images; buffer-to-ASTC copies update guest storage and reach the upload path again. Retirement at lines 890-905 and 4204-4222 keeps both image and output-buffer ownership until the recorded GPU work completes. No additional scratch writer was found. These are source traces, not a GPU reproduction of a multi-layer or resized copy. ### Measurements and smaller points 1. **The memory comparison meets the basic pairing rule.** It names baseline `fbd42024`, candidate `000cc9f2`, one Deck and matched settings, and reports 3 interleaved pairs in the heavy view and 2 in each other scene. With two observations, the displayed mean is also the conventional median. All 7 process-GPU-memory differences are negative, from -209 to -63 MB in the displayed units, with median -139 MB. Within-scene ranges are separate: heavy view 3800-3908 versus 3675-3772 MB, Kokiri 4064-4077 versus 3901-3919 MB, Field 3482-3510 versus 3362-3371 MB. The heavy-view repeat spreads are 108 and 97 MB. That supports a saving around the intended allocation reduction here, not exactly 140 MB less total process memory in every run. The +145 MB median `MemAvailable` difference is whole-system context, not another saving to add to the GPU-memory result. 2. **Keep the evidence reproducible and the speed claim narrow.** Retain the per-run memory-counter lines, timestamps and exact order including the third renderer, identify that renderer, and say what the three warm-up log windows cover. Label the conversion to MB or MiB. The memory table is useful, but the raw counter records and run logs are not attached. The small FPS differences do not establish a speed gain, and the 40 s versus 38 s startup row has no per-run values to check. No p99 frame-time or 1% low evidence is supplied. The current allocation and batching changes can be read directly, but `000cc9f2` is not available from the fetched history or its public commit endpoint, so I could not independently compare the measured patch with this rebased head. Preserve that commit or its exact patch and build identity when carrying the numbers forward. 3. **Exercise the batch behavior, not only its spelling.** `tests/test_bc_scratch_layers.py`, lines 55-78, checks source strings and runs the real setting reader; its capacity calculation is a separate formula in the fixture. It does not execute allocation, the upload loop, the copy loop or an encoder. A game-free recording test using the production loops could check a request larger than capacity, a final partial batch, nonzero source/destination layers, sparse dirty layers and the resized path. The existing initialization test preserves its ordering checks, but its Vulkan spy does not check layer bounds. This is a coverage improvement, not an observed defect. 4. **Pin the other copy change before calling the rebase two lines.** The description names `fix/vk-bc-copy-extent`. The overlap on this side is `texture_copy_into_bc()`, lines 2958-2988, specifically the two batch-size lines at 2977-2978 beside the copy-region and encoder arguments at 2980-2986. This patch changes layer batching, not the copy extent. I could not locate the named counterpart in the 134 public open pull requests or fetched branch history, so its hunks and the claimed two-line rebase remain unchecked. Link its published head; any combined version must retain the `bc_layers()` bound rather than restore `kBcLayers` batching against a one-layer allocation. ### Not checked No Windows fixture execution, Vulkan validation, desktop GPU, BC7 gameplay, resized scratch, confirmed multi-layer copy or independent memory measurement here. The reported pixel-difference totals are not a visual inspection, and no pictures were supplied to inspect. A bounded, inspected run covering the actual copy path and the optional second scratch would settle the remaining runtime coverage. Keep the explicit WIP limitations, open the stated desktop test request before readying, and name the 36 inherited macOS test failures rather than only their general causes.
Author
Contributor

Played with the picture on the Steam Deck OLED: a build holding this change with other renderer-only ones (3a3a6475, on main 514591a3), nothing wrong seen on screen. Testing has it now; what is left before ready is a test on a desktop card.

Played with the picture on the Steam Deck OLED: a build holding this change with other renderer-only ones (3a3a6475, on main 514591a3), nothing wrong seen on screen. Testing has it now; what is left before ready is a test on a desktop card.
p0lux force-pushed perf/bc-scratch-one-layer from 8b7e3c17fa
Some checks failed
Website / Website on Cloudflare (pull_request_target) Has been skipped
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
Website / Website on Cloudflare (pull_request) Has been cancelled
to a9b0c8804d
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 08:55:01 +00:00
Compare
Author
Contributor

@mooki's review, point by point (head a9b0c880):

  • Units and context (1): the memory numbers are the kernel's KiB divided by 1,024; the table and the block now say MiB, and MemAvailable is marked as the whole system's, context and not a second saving. The fps and start times are stated as "no cost", not as a gain.
  • Reproducible evidence (2): the description now gives the order of a round (main, this PR, then a third renderer: main with the six commits of #391), what the 3 log windows are (the first 360 presents in game, before the 720 measured), and the start times run by run. The measured commit is on the fork as measured/bc-scratch-000cc9f2. Against it this head has the rebase, RX_VK_BC_LAYERS's row in docs/development.md and the new test; the scratch's code is the same.
  • Batch behaviour, run and not only read (3): done. tests/bc_scratch_batches_test.cpp runs the production loops with a spy in place of Vulkan (texture_copy_into_bc() whole, and the lines of upload_astc_gpu() that form a run, cut in by tests/test_bc_scratch_layers.py): 9 layers into a scratch of 1, 4 and 8 layers, a last partial run, source and destination layers 3 and 10, the resized path, a copy the scratch cannot take, an array's dirty layers with gaps. It fails on the three mutations I tried: a copy by 8, an upload run of 8, a dropped last partial run.
  • The other copy change (4): fix/vk-bc-copy-extent is not published yet as far as I can see; the note now names the two lines that overlap and says a combined version has to keep bc_layers() in that loop.
  • The inherited failures: the 36 checks that fail on main on that Mac are named in Testing.
  • A person's eyes: the build was played on the Deck since, nothing wrong seen (Testing).

Not done: the raw counter records as files (every value is in the description's block; say if you want them attached), p99 and 1% lows (no speed claim is made), and a GPU run known to make a multi-layer copy or to use the second scratch: I do not know that these scenes make one, which is what the desktop test is asked for.

@mooki's review, point by point (head a9b0c880): - **Units and context (1):** the memory numbers are the kernel's KiB divided by 1,024; the table and the block now say MiB, and `MemAvailable` is marked as the whole system's, context and not a second saving. The fps and start times are stated as "no cost", not as a gain. - **Reproducible evidence (2):** the description now gives the order of a round (main, this PR, then a third renderer: main with the six commits of #391), what the 3 log windows are (the first 360 presents in game, before the 720 measured), and the start times run by run. The measured commit is on the fork as `measured/bc-scratch-000cc9f2`. Against it this head has the rebase, `RX_VK_BC_LAYERS`'s row in `docs/development.md` and the new test; the scratch's code is the same. - **Batch behaviour, run and not only read (3):** done. `tests/bc_scratch_batches_test.cpp` runs the production loops with a spy in place of Vulkan (`texture_copy_into_bc()` whole, and the lines of `upload_astc_gpu()` that form a run, cut in by `tests/test_bc_scratch_layers.py`): 9 layers into a scratch of 1, 4 and 8 layers, a last partial run, source and destination layers 3 and 10, the resized path, a copy the scratch cannot take, an array's dirty layers with gaps. It fails on the three mutations I tried: a copy by 8, an upload run of 8, a dropped last partial run. - **The other copy change (4):** `fix/vk-bc-copy-extent` is not published yet as far as I can see; the note now names the two lines that overlap and says a combined version has to keep `bc_layers()` in that loop. - **The inherited failures:** the 36 checks that fail on main on that Mac are named in Testing. - **A person's eyes:** the build was played on the Deck since, nothing wrong seen (Testing). Not done: the raw counter records as files (every value is in the description's block; say if you want them attached), p99 and 1% lows (no speed claim is made), and a GPU run known to make a multi-layer copy or to use the second scratch: I do not know that these scenes make one, which is what the desktop test is asked for.
p0lux changed title from WIP: perf(vk): the BC3 scratch has the layers a run uses, 20 MB instead of 160 to perf(vk): the BC3 scratch has the layers a run uses, 20 MB instead of 160 2026-11-14 12:06:55 +00:00
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 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 'perf/bc-scratch-one-layer:p0lux-perf/bc-scratch-one-layer'
git switch 'p0lux-perf/bc-scratch-one-layer'
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!384
No description provided.