Files
arthurhfcardosoandClaude Sonnet 5.5 dd496bfd5d Encode decoded ASTC textures to BC3 on GPUs without ASTC (astc_bc)
A GPU that cannot sample ASTC gets the textures decoded to RGBA8, four times the video memory of the ASTC blocks. With astc_bc = 1 (default 0, GPU decoder only) a second compute pass, bc3_encode.comp, encodes the decoded texels to BC3 and the image is BC3_UNORM or BC3_SRGB: 8 bits per texel instead of 32. The encoder is the usual bounding-box method with the box diagonal turned to the sign of the colour covariance, and the eight-value alpha mode.

A region of a WriteTexels that does not start on a 4x4 block corner is left as it was and logged. The setting is in totk.ini and the Advanced tab. The SPIR-V header was built with glslang 16.6 (build_shaders.py uses glslc) with the same source hash.

On a GTX 1650 (4 GiB) in the Fire Temple the GPU time per frame went from 10-23 ms to 3-12 ms and the frame rate held at 59-60 fps.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-31 07:27:46 +03:00

90 lines
4.1 KiB
Plaintext

// RGBA8 texels to BC3 blocks on the GPU, one invocation per 4x4 block: the second pass of the ASTC decode when
// TOTK_ASTC_BC is on (vk_astc.c), so a decoded ASTC texture takes 8 bits per texel in video memory instead of 32.
// Texels past the right or bottom edge repeat the last column or row.
//
// Colour: the BC1 four-colour block of the box around the block's colours, its diagonal turned to the sign of the
// colours' covariance and inset by 1/16 of the range (the bounding-box method of stb_dxt and of yuzu's BC1 recompression),
// then each texel takes the nearest of the four palette entries along that axis. Alpha: the eight-value BC3 alpha
// block between the block's smallest and largest alpha. The bytes are what the texels hold, so an sRGB image keeps
// sRGB-encoded values, as BC3_SRGB expects.
// Compiled to runtime/bc3_encode_spv.h by tools/build_shaders.py; vk_astc.c records the dispatch.
#version 450
layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
layout(std430, set = 0, binding = 0) readonly buffer Texels { uint px[]; };
layout(std430, set = 0, binding = 1) writeonly buffer Blocks { uvec4 blk[]; };
layout(push_constant) uniform Params {
uint in_base; // first texel of slice 0 (index into px)
uint out_base; // first block of slice 0 (uvec4 index into blk)
uint width, height;
uint bx; // blocks per row
uint bslice; // blocks per slice
} pc;
vec4 texel(uint x, uint y, uint z) {
x = min(x, pc.width - 1u), y = min(y, pc.height - 1u);
return unpackUnorm4x8(px[pc.in_base + (z * pc.height + y) * pc.width + x]) * 255.0;
}
uint to565(vec3 c) {
uvec3 q = uvec3(round(clamp(c, 0.0, 255.0) * vec3(31.0, 63.0, 31.0) / 255.0));
return q.r << 11 | q.g << 5 | q.b;
}
vec3 from565(uint v) {
return vec3(float(v >> 11 & 31u) * 255.0 / 31.0, float(v >> 5 & 63u) * 255.0 / 63.0, float(v & 31u) * 255.0 / 31.0);
}
void main() {
uvec3 id = gl_GlobalInvocationID;
uint by = (pc.height + 3u) / 4u;
if (id.x >= pc.bx || id.y >= by) return;
vec4 t[16];
vec4 lo = vec4(255.0), hi = vec4(0.0), mean = vec4(0.0);
for (uint i = 0u; i < 16u; i++) {
t[i] = texel(id.x * 4u + (i & 3u), id.y * 4u + (i >> 2), id.z);
lo = min(lo, t[i]), hi = max(hi, t[i]), mean += t[i];
}
mean /= 16.0;
// alpha: a0 > a1 selects the eight-value mode; index 0 is a0, 1 is a1, 2..7 the six steps from a0 to a1
uint a0 = uint(hi.a), a1 = uint(lo.a);
uvec2 ab = uvec2(a0 | a1 << 8, 0u);
if (a0 > a1) {
float range = float(a0 - a1);
for (uint i = 0u; i < 16u; i++) {
uint s = uint(clamp(round((float(a0) - t[i].a) * 7.0 / range), 0.0, 7.0)); // 0 = a0 .. 7 = a1
uint code = s == 0u ? 0u : s == 7u ? 1u : s + 1u;
uint bit = 16u + 3u * i;
if (bit < 32u) ab.x |= code << bit;
if (bit + 3u > 32u) ab.y |= code >> (32u - min(bit, 32u)) << (bit > 32u ? bit - 32u : 0u);
}
}
// colour: the box diagonal along the covariance of red and blue with green
vec3 cmin = lo.rgb, cmax = hi.rgb;
float crg = 0.0, cbg = 0.0;
for (uint i = 0u; i < 16u; i++) {
vec3 d = t[i].rgb - mean.rgb;
crg += d.r * d.g, cbg += d.b * d.g;
}
if (crg < 0.0) { float s = cmin.r; cmin.r = cmax.r; cmax.r = s; }
if (cbg < 0.0) { float s = cmin.b; cmin.b = cmax.b; cmax.b = s; }
vec3 inset = (cmax - cmin) / 16.0;
uint c0 = to565(cmax - inset), c1 = to565(cmin + inset);
uint idx = 0u;
if (c0 != c1) {
if (c0 < c1) { uint s = c0; c0 = c1; c1 = s; } // c0 > c1: the four-colour mode
vec3 e0 = from565(c0), e1 = from565(c1), axis = e1 - e0;
float len2 = max(dot(axis, axis), 1e-6);
for (uint i = 0u; i < 16u; i++) {
uint s = uint(clamp(round(dot(t[i].rgb - e0, axis) / len2 * 3.0), 0.0, 3.0)); // 0 = e0 .. 3 = e1
uint code = s == 0u ? 0u : s == 3u ? 1u : s + 1u; // palette order e0, e1, 2/3 e0 + 1/3 e1, 1/3 e0 + 2/3 e1
idx |= code << (2u * i);
}
}
blk[pc.out_base + id.z * pc.bslice + id.y * pc.bx + id.x] = uvec4(ab.x, ab.y, c0 | c1 << 16, idx);
}