BC3 re-encode of ASTC arrays: one layer a run (Hyrule Field grid lines) #956

Merged
YunaraDebugs merged 1 commit from bc3-array-layers into main 2026-11-09 04:59:40 +00:00
YunaraDebugs commented 2026-11-09 03:15:05 +00:00 (Migrated from github.com)

Hyrule Field's ground shows its tiles outlined in dark lines when seen from a distance; up close the lines go away. Seen on an RX 6600 (Windows 11), likely part of #468.

Cause
When the BC3 path re-encodes an ASTC texture array, upload_astc_gpu decodes and encodes up to kBcLayers (8) consecutive layers in one run. In the smaller mip levels, every layer after the run's first comes out wrong; single-layer textures are unaffected. The terrain's material arrays (1365 x 1365 x 90, ASTC 8x8) are sampled at those levels from a distance, hence the lines.

How it was isolated (RX 6600): the lines went away

  • with samplers capped at mip 0 (a local test switch),
  • with the arrays kept as RGBA8 instead of BC3,
  • with one layer a run.

Fix
One layer a run by default. The arrays stay BC3, so there is no VRAM cost (RGBA8 cost ~1.3 GB, too much for an 8 GB card). RX_VK_BC_LAYERS=<n> allows up to n layers a run (8: as before).

I couldn't read bc_encoder.comp (only its SPIR-V header is in the tree), so this works around the multi-layer path rather than fixing its addressing. If you have the shader source, the per-layer output offset for levels smaller than the first is probably what's off; with that fixed, runs could go back to 8.

Tested: RX 6600 + Ryzen 5 5600G, Windows 11: lines gone in Hyrule Field, no other texture changes seen in Kokiri. Builds on current main (32df58e). Not tested on NVIDIA/Intel/Linux; the change applies to every GPU that takes the BC3 path.

🤖 Generated with Claude Code

Hyrule Field's ground shows its tiles outlined in dark lines when seen from a distance; up close the lines go away. Seen on an RX 6600 (Windows 11), likely part of #468. **Cause** When the BC3 path re-encodes an ASTC texture array, `upload_astc_gpu` decodes and encodes up to `kBcLayers` (8) consecutive layers in one run. In the smaller mip levels, every layer after the run's first comes out wrong; single-layer textures are unaffected. The terrain's material arrays (1365 x 1365 x 90, ASTC 8x8) are sampled at those levels from a distance, hence the lines. **How it was isolated** (RX 6600): the lines went away - with samplers capped at mip 0 (a local test switch), - with the arrays kept as RGBA8 instead of BC3, - with one layer a run. **Fix** One layer a run by default. The arrays stay BC3, so there is no VRAM cost (RGBA8 cost ~1.3 GB, too much for an 8 GB card). `RX_VK_BC_LAYERS=<n>` allows up to n layers a run (8: as before). I couldn't read `bc_encoder.comp` (only its SPIR-V header is in the tree), so this works around the multi-layer path rather than fixing its addressing. If you have the shader source, the per-layer output offset for levels smaller than the first is probably what's off; with that fixed, runs could go back to 8. **Tested:** RX 6600 + Ryzen 5 5600G, Windows 11: lines gone in Hyrule Field, no other texture changes seen in Kokiri. Builds on current main (32df58e). Not tested on NVIDIA/Intel/Linux; the change applies to every GPU that takes the BC3 path. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
madebyjordan commented 2026-11-09 04:01:30 +00:00 (Migrated from github.com)

Thanks for isolating this. I wrote #705 (the AMD resize these arrays go through), so I checked your theory against the shaders: the encoder's addressing is fine, and the multi-layer step that goes wrong is most likely driver-side.

The shaders handle layers correctly. I disassembled both SPIR-V headers with spirv-cross (Vulkan SDK):

  • bc_encoder_comp_spv.h writes layer z at block (z * blocks_y + y) * blocks_x + x (16 bytes each, out_offset 0). That's the tight packing bc_encode_into's VkBufferImageCopy expects (rowLength/imageHeight 0), at every level size.
  • astc_decoder_comp_spv.h reads layer z at z * layer_stride = tail, the stride upload_astc_gpu stages with.

What changes with n > 1 is driver-side. On AMD both terrain arrays are resized (#705), so each run does:

  1. vkCmdBlitImage with layerCount = n (bc_encode_resized)
  2. vkCmdCopyBufferToImage with layerCount = n into the BC3 level

AMD packs small levels into a mip tail. A driver issue with multi-layer blits or copies there would match what you saw: only the small levels, only the layers after the first. Same family as the AMD BC level bug #705 works around.

Suggested narrower fix: keep runs of 8 and split only those two calls into one region per layer (no extra dispatches or barriers):

// bc_encode_into
const uint64_t layer_bytes = (uint64_t)pc.blocks_x * pc.blocks_y * 16;
VkBufferImageCopy c[kBcLayers];
for (uint32_t z = 0; z < n; z++)
    c[z] = {z * layer_bytes, 0, 0, {VK_IMAGE_ASPECT_COLOR_BIT, level, layer + z, 1}, {dx, dy, 0}, {w, h, 1}};
vkCmdCopyBufferToImage(cmd, bc_out, dst.image, VK_IMAGE_LAYOUT_GENERAL, n, c);

// bc_encode_resized
VkImageBlit bl[kBcLayers]{};
for (uint32_t z = 0; z < n; z++) {
    bl[z].srcSubresource = bl[z].dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, z, 1};
    bl[z].srcOffsets[1] = {(int32_t)w, (int32_t)h, 1};
    bl[z].dstOffsets[1] = {(int32_t)ow, (int32_t)oh, 1};
}
vkCmdBlitImage(cmd, bc_scratch, VK_IMAGE_LAYOUT_GENERAL, bc_scratch2, VK_IMAGE_LAYOUT_GENERAL, n, bl, VK_FILTER_LINEAR);

Testing them one at a time (copy only, then blit only) would show which call it is.

Why this over 1 layer a run:

  • Runs of 1 mean 8x the dispatches, blits and copies, each with several full_barriers: ~1,440 runs for the two arrays. #900 is trying to keep that same rebuild under Windows' 2 s TDR.
  • texture_copy_into_bc also does multi-layer runs and isn't covered by #956; fixing it in bc_encode_into / bc_encode_resized covers it too.
  • If it's an AMD driver issue, it can be gated to AMD like #705, so NVIDIA/Intel stay unchanged.

Heads-up: this and #900 edit the same run loop, so whichever merges second needs a rebase.

Thanks for isolating this. I wrote #705 (the AMD resize these arrays go through), so I checked your theory against the shaders: the encoder's addressing is fine, and the multi-layer step that goes wrong is most likely driver-side. **The shaders handle layers correctly.** I disassembled both SPIR-V headers with spirv-cross (Vulkan SDK): - `bc_encoder_comp_spv.h` writes layer z at block `(z * blocks_y + y) * blocks_x + x` (16 bytes each, `out_offset` 0). That's the tight packing `bc_encode_into`'s `VkBufferImageCopy` expects (rowLength/imageHeight 0), at every level size. - `astc_decoder_comp_spv.h` reads layer z at `z * layer_stride` = `tail`, the stride `upload_astc_gpu` stages with. **What changes with n > 1 is driver-side.** On AMD both terrain arrays are resized (#705), so each run does: 1. `vkCmdBlitImage` with `layerCount = n` (`bc_encode_resized`) 2. `vkCmdCopyBufferToImage` with `layerCount = n` into the BC3 level AMD packs small levels into a mip tail. A driver issue with multi-layer blits or copies there would match what you saw: only the small levels, only the layers after the first. Same family as the AMD BC level bug #705 works around. **Suggested narrower fix:** keep runs of 8 and split only those two calls into one region per layer (no extra dispatches or barriers): ```cpp // bc_encode_into const uint64_t layer_bytes = (uint64_t)pc.blocks_x * pc.blocks_y * 16; VkBufferImageCopy c[kBcLayers]; for (uint32_t z = 0; z < n; z++) c[z] = {z * layer_bytes, 0, 0, {VK_IMAGE_ASPECT_COLOR_BIT, level, layer + z, 1}, {dx, dy, 0}, {w, h, 1}}; vkCmdCopyBufferToImage(cmd, bc_out, dst.image, VK_IMAGE_LAYOUT_GENERAL, n, c); // bc_encode_resized VkImageBlit bl[kBcLayers]{}; for (uint32_t z = 0; z < n; z++) { bl[z].srcSubresource = bl[z].dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, z, 1}; bl[z].srcOffsets[1] = {(int32_t)w, (int32_t)h, 1}; bl[z].dstOffsets[1] = {(int32_t)ow, (int32_t)oh, 1}; } vkCmdBlitImage(cmd, bc_scratch, VK_IMAGE_LAYOUT_GENERAL, bc_scratch2, VK_IMAGE_LAYOUT_GENERAL, n, bl, VK_FILTER_LINEAR); ``` Testing them one at a time (copy only, then blit only) would show which call it is. Why this over 1 layer a run: - Runs of 1 mean 8x the dispatches, blits and copies, each with several `full_barrier`s: ~1,440 runs for the two arrays. #900 is trying to keep that same rebuild under Windows' 2 s TDR. - `texture_copy_into_bc` also does multi-layer runs and isn't covered by #956; fixing it in `bc_encode_into` / `bc_encode_resized` covers it too. - If it's an AMD driver issue, it can be gated to AMD like #705, so NVIDIA/Intel stay unchanged. Heads-up: this and #900 edit the same run loop, so whichever merges second needs a rebase.
Commenting is not possible because the repository is archived.
No description provided.