Skip to content

perf(agc): copy a pending block-compressed surface into its sampled texture on the GPU - #4747

Merged
oneandonlydean merged 1 commit into
boykopovar:mainfrom
TideOPL:perf/bc-block-copy
Oct 14, 2026
Merged

oneandonlydean merged 1 commit into
boykopovar:mainfrom
TideOPL:perf/bc-block-copy

Conversation

@TideOPL

@TideOPL TideOPL commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

What

A compute pass can write a block-compressed surface through an uncompressed storage view whose element is one block: 32_32_UINT for 8-byte blocks (BC1, BC4) and 32_32_32_32_UINT for 16-byte blocks (BC2, BC3, BC5, BC6H, BC7). Unreal Engine's runtime virtual textures encode their physical pages this way every frame. Minecraft Dungeons II (PPSA16064) has three 4096x4096 pages (BC5, BC6H and BC7, 16 MiB each) that are written by compute and sampled in the same frame.

cachedTexture found the pending storage image but dropped it, because Texture::CanCopyFrom rejects block-compressed descriptors. The lookup then took the snapshot path every time:

  1. StorageTexture::FlushPending stored the whole 16 MiB surface to guest memory.
  2. The 64 KiB blocks it stamped failed UnchangedSince, so EqualsCommitted compared all 16 MiB with the snapshot. That took 1 to 8 ms per lookup.
  3. When the page had changed, ReadTextureSurface read the surface again and a new texture uploaded it.

With this change the BC texture is a copy of the storage image instead:

  • Texture::CanBlockCopyFrom accepts a storage image over the same memory with:
    • the same tile mode, pipe/bank XOR, dimension and array range;
    • an uncompressed element of the same size as the block;
    • an extent equal to the surface's extent in blocks on every copied level, each of which must be whole blocks (width and height multiples of 4).
  • The BlockCopyTag texture owns a BC image. RefreshDepthCopy refreshes it with one vkCmdCopyImage per level from the storage image, whenever the image's Version() moved. This is the same refresh-on-reuse scheme the depth copy uses: no flush, no CPU compare, no upload.
  • cachedTexture keeps such a pending source and makes or reuses the block copy, in both the cached and the uncached path.

Sources for the semantics:

  • Same memory layout. A GFX10 BC surface is addressed as an array of 8- or 16-byte elements, one per 4x4 block, and the swizzle works on elements. Mesa src/amd/common/ac_surface.c (ac_compute_surface: blk_w/blk_h = 4 and bpe = 8/16 for compressed formats) and addrlib's Gfx10Lib element-based swizzle equations cover this. An uncompressed view with the same element size and the extent in blocks therefore addresses the same bytes.
  • The copy. Vulkan allows vkCmdCopyImage between size-compatible uncompressed and compressed formats. The extent is given in texels of the source image (Vulkan spec, "Copying Data Between Images", VkImageCopy).

Tested

  • System: Windows 11 (26200), RTX 5070, NVIDIA Vulkan 1.4, WinLibs GCC, Release, BUILD_TESTING=ON, rebased on main 90c6839. The block-copy texture is a PerUseCopy::RefreshedOnReuse copy like the depth copy, so it goes through IsDepthCopy/RefreshDepthCopy unchanged.

  • agc_driver_bc_block_copy (new execution test): for BC7 (through 32_32_32_32_UINT) and BC1 (through 32_32_UINT), a compute pass stores blocks, the test samples the BC view while the results are pending, then stores and samples again.

    • It checks the results are still pending after the sample, so nothing was stored to guest memory.
    • It checks the sampled texels equal a sample of the same blocks taken from a separate surface through the snapshot path, so the copy is bit-exact.
    • It fails on main with sampling the compressed view stored the pending blocks to guest memory, and passes with this change.
  • Suite: ctest -R "^agc" after the rebase: 447 tests, all pass. agc_driver_stencil_ref_export is skipped, as on main. The new test fails on this main too.

  • Minecraft Dungeons II (PPSA16064), title screen. This is not a clean-main A/B: the title does not reach its title screen on plain main yet (it needs feat(agc): depth planes sampled as 2D arrays, cubes and integer views #425 and fix(agc): take an HTILE fill from a value in memory as a depth fast clear #3816). I tested on main e7c70a2 plus those PRs and the local patches the title needs, none of which touch this path. Same capture window (APS5_TRACE_SECONDS=30:10), two runs of each:

    before after
    frame p50 153.3 / 148.6 ms 138.5 / 139.1 ms
    frame p99 235.9 / 206.7 ms 192.6 / 195.2 ms
    presents in the window 63 / 65 70 / 70
    Resources.Complete 60.6 / 58.6 ms per frame 48.2 / 48.9 ms per frame

    The three pages became block copies. Screenshots of the title render the same as before.

  • Not run: Linux, lavapipe, AMD.

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • Unblocks: none
  • AI-assisted: yes. Written with AI assistance (Claude Code); I reviewed and tested it.

@github-actions github-actions Bot added the agc libSceAgc* label Oct 12, 2026
@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 13, 2026
…exture on the GPU

A compute pass can write a block-compressed surface through an uncompressed
storage view whose element is one block (32_32_UINT for 8-byte blocks,
32_32_32_32_UINT for 16-byte ones). cachedTexture rejected such a storage
image for the BC descriptor, so every sample stored the whole surface to
guest memory, compared it with the snapshot and uploaded it again.

Texture::CanBlockCopyFrom accepts a storage image over the same memory,
tiling and swizzle whose element is the block and whose extent is the
surface's extent in blocks, with whole blocks on every copied level. The
sampled texture then owns a BC image that vkCmdCopyImage refreshes from the
storage image when its version moved, through the same refreshed-on-reuse
path as the depth copy.
@TideOPL
TideOPL force-pushed the perf/bc-block-copy branch from 7d38543 to 126a1e4 Compare October 13, 2026 00:29
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 13, 2026
@kr4y4

kr4y4 commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

An Intel run, a second driver for the bit-exact check, and the full suite.

Setup: Windows 11 (26200), WinLibs GCC 15.2.0 r7, Release, CI configure flags, an RTX 4070 Laptop GPU and an Intel Raptor Lake-S iGPU. The head 126a1e4 merged into main 495bf91, which passes 1023 of 1023 on both GPUs.

  • agc_driver_bc_block_copy passes on both GPUs with --repeat until-fail:3 and prints bc block copy tests passed on each. So on the Intel driver too, the BC7 and BC1 texels sampled from the copy equal the separate surface's snapshot sample, and the results stay pending.
  • Full suite (ctest --timeout 120 --parallel 4): 1024 of 1024 on both GPUs.
  • With Texture.hpp, Texture.cpp and ShaderResources.cpp as on main, it fails on both GPUs with BC7 through 32x4 uint blocks: sampling the compressed view stored the pending blocks to guest memory, as in the description.

Written with AI assistance.

@adriadam10

Copy link
Copy Markdown
Collaborator

Checked on Linux (RADV): agc_driver_bc_block_copy passes, and fails with Texture.hpp/Texture.cpp/ShaderResources.cpp reverted. The layout condition in CanBlockCopyFrom (same tile mode, XOR, element size, extent in blocks, whole-block mips) is conservative and everything else keeps the old path. Cross-checked with kr4y4's Intel and NVIDIA runs (full suite green). Merging once the Windows build is green.

@oneandonlydean oneandonlydean self-assigned this Oct 13, 2026
@oneandonlydean
oneandonlydean merged commit af991ac into boykopovar:main Oct 14, 2026
9 checks passed
@oneandonlydean

Copy link
Copy Markdown
Collaborator

agc_driver_bc_block_copy fails on NVIDIA on current main (cd90ca50b, RTX 4090, driver 610.57.04, Linux): BC7 through 32x4 uint blocks: sample of blocks stored again: thread 0 component 0 read 1064168942, expected 1058905502. CI's lavapipe passes it. Could you check whether the stored-again sample depends on how the device reads back the copied blocks?

@simonbbby

Copy link
Copy Markdown
Contributor

Independent reproduction on the same reported main commit cd90ca50bec81619f9a3d0913bc4cd3ecf2ca49d, Linux Release, SPIRV-Tools ON, no production changes:

GPU Original agc_driver_bc_block_copy, 3 independent runs
RTX 3090, NVIDIA 595.99.02, subgroup32 0/3, identical BC7 mismatch
gfx1036 (1002:13c0), RADV Mesa 25.2.8 3/3, BC7 and BC1 complete

Pinned ICDs, explicit GPU selection, ANYPS5_REQUIRE_VULKAN=1, ANYPS5_NO_SHADER_CACHE=1, serial CTest, unchanged 30-second limit. NVIDIA fails at the third BC7 sample before the BC1 case is reached:

sample of blocks stored again: thread 0 component 0
read 1064168942, expected 1058905502

I then changed only the temporary fixture to inspect the result:

  • The first-reference value is 1058905502; the second-reference value is 1064168942. The entire third sampled vector (128 words) equals the previous second-reference vector, not just its first channel.
  • Adding the fixture's existing Settle(device, texels, bytes) immediately after the third store still fails 3/3 on NVIDIA, with that same stale vector. Settle flushes pending storage, waits idle, and collects writes.
  • With that settle followed by a raw backing-memory read, the first BC7 block is 13572440,00000000,13572440,00000000 on both GPUs: the new First pattern is present in the backing bytes. NVIDIA nevertheless samples the Second pattern; RADV samples correctly.

So this reproduces on another NVIDIA generation/driver, and the explicit-settle control does not point to incorrect first-block bytes from GPU writeback. The stale copied/sampled image or its reuse remains to be isolated; these checks do not prove a particular cache or synchronization fix. The original fixture was restored byte-for-byte and the borrowed isolated checkout restored/rebuilt to its prior head. No title run in this report.

@milescoviello

Copy link
Copy Markdown
Contributor

@TideOPL agc_driver_bc_block_copy fails on Linux with an NVIDIA GPU (Ubuntu 24.04, RTX 4070 Ti, driver 580.142), both at this PR's merge commit af991ac and on current main: sample of blocks stored again reads the previous copy's blocks. On that sample the build takes the record's texture through fastTexture, which does not decline a copy refreshed on reuse, so RefreshDepthCopy is never called. #5654 has a one-line fix; with it the test passes 10 of 10.

AI-assisted: yes

oneandonlydean pushed a commit that referenced this pull request Oct 14, 2026
fastTexture hands back the texture of a stage-A record when its memory is
unchanged, and declines only depth surfaces and textures remade per use.
The block copy of #4747 is a copy refreshed on reuse over a surface that
is not a depth surface, so the fast path returned it without
RefreshDepthCopy: after a compute pass stored new blocks into its storage
image, a sample read the blocks of the previous copy.

Decline copies refreshed on reuse in fastTexture as well, so the lookup
takes cachedTexture, whose cache hit refreshes them.

agc_driver_bc_block_copy failed on main at "sample of blocks stored
again" (Linux, NVIDIA) and passes with this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc*

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants