Skip to content

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

Merged
NotRexed merged 1 commit into
NotRexed:mainfrom
YunaraDebugs:bc3-array-layers
Nov 9, 2026
Merged

NotRexed merged 1 commit into
NotRexed:mainfrom
YunaraDebugs:bc3-array-layers

Conversation

@YunaraDebugs

Copy link
Copy Markdown
Contributor

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

When the BC3 path re-encodes an ASTC texture array, it decoded and encoded
up to kBcLayers consecutive layers in one run. In the smaller levels, every
layer after the run's first came out wrong. Single-layer textures were
unaffected. The terrain's material arrays (1365 x 1365 x 90, ASTC 8x8) are
sampled at those levels from a distance, so Hyrule Field's ground showed
its tiles outlined in dark lines that went away up close.

On an RX 6600 the lines went away with one layer a run, with the arrays
kept as RGBA8, and with samplers capped at mip 0. One layer a run keeps
the arrays BC3, so there is no VRAM cost (RGBA8 cost ~1.3 GB, too much for
an 8 GB card). RX_VK_BC_LAYERS=<n>: up to n layers a run, as before with 8.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@madebyjordan

Copy link
Copy Markdown
Contributor

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:

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants