diff options
| author | Takoroka <137927095+Takoroka@users.noreply.github.com> | 2026-10-28 20:43:01 -0400 |
|---|---|---|
| committer | Takoroka <137927095+Takoroka@users.noreply.github.com> | 2026-10-28 20:43:01 -0400 |
| commit | 0d5534a2883329183ae8aa89b3fe1c7b67291ece (patch) | |
| tree | aabad4260f301a8a52af16e15e7fb76a700d642f /src/video_core/host_shaders/bc_encoder.comp | |
| parent | e4deef6ba4ea76aac997ed3102f8e9801fd170b9 (diff) | |
ASTC: decode and re-compress to BC1/BC3 on the GPU; texture cache budget
- accelerate_astc=Gpu with astc_recompression=Bc1/Bc3 now decodes ASTC
with the GPU decoder into a temporary RGBA8 image (dedicated memory,
freed once the GPU is past it) and compresses it with a new compute
shader (bc_encoder.comp: bounding-box endpoints along the colours'
covariance, inset 1/16, nearest palette entry; BC3 alpha ramp; BC1
punch-through), then copies the blocks into the image. No CPU decode,
so streamed textures are ready the frame they are first used instead
of showing placeholders for seconds; the decode pass no longer waits
for the GPU on this path.
- The CPU path (CpuAsynchronous) fills a new image with zero blocks
before its first decode instead of showing stale VRAM, and the
transcode pool is a quarter of the hardware threads.
- Switch 2 titles: collector thresholds near the device budget, re-read
every 60 frames from VK_EXT_memory_budget (another game shrank it from
7.0 to 5.4 GB).
- EDEN_NX2_HIDE_WINDOW=0 now leaves the window visible.
Diffstat (limited to 'src/video_core/host_shaders/bc_encoder.comp')
| -rw-r--r-- | src/video_core/host_shaders/bc_encoder.comp | 183 |
1 files changed, 183 insertions, 0 deletions
diff --git a/src/video_core/host_shaders/bc_encoder.comp b/src/video_core/host_shaders/bc_encoder.comp new file mode 100644 index 0000000000..3a87e92dcb --- /dev/null +++ b/src/video_core/host_shaders/bc_encoder.comp @@ -0,0 +1,183 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// Compresses one level of an RGBA8 image (the ASTC decoder's output) to BC1 or BC3, one 4x4 block +// per invocation, so re-compressed ASTC never round-trips through the CPU. Colour endpoints are +// the block's bounding box, oriented along the colours' covariance and inset by 1/16; each texel +// takes the nearest palette entry. Values are encoded as stored (sRGB data stays sRGB, like the +// CPU path). + +#version 460 core + +layout (local_size_x = 8, local_size_y = 8, local_size_z = 1) in; + +layout (set = 0, binding = 0, rgba8) uniform readonly image2DArray source; + +layout (std430, set = 0, binding = 1) writeonly buffer Output { + uvec2 blocks[]; +}; + +layout (push_constant) uniform PushConstants { + uint blocks_x; + uint blocks_y; + uint width; + uint height; + uint bc3; + uint out_offset; // in 8-byte units +}; + +uint Pack565(vec3 color) { + const uvec3 q = uvec3(round(clamp(color, 0.0, 1.0) * vec3(31.0, 63.0, 31.0))); + return (q.r << 11) | (q.g << 5) | q.b; +} + +vec3 Unpack565(uint value) { + const uvec3 q = uvec3((value >> 11) & 31u, (value >> 5) & 63u, value & 31u); + const uvec3 e = uvec3((q.r << 3) | (q.r >> 2), (q.g << 2) | (q.g >> 4), (q.b << 3) | (q.b >> 2)); + return vec3(e) / 255.0; +} + +float Distance2(vec3 a, vec3 b) { + const vec3 d = a - b; + return dot(d, d); +} + +// BC1 colour block. With punch_through (BC1 only), texels with alpha < 0.5 use the transparent +// index of the three-colour mode; otherwise the four-colour mode is used (BC3 always decodes so). +uvec2 EncodeColor(in vec4 texels[16], bool punch_through) { + vec3 lo = vec3(1.0); + vec3 hi = vec3(0.0); + for (int i = 0; i < 16; ++i) { + if (punch_through && texels[i].a < 0.5) { + continue; + } + lo = min(lo, texels[i].rgb); + hi = max(hi, texels[i].rgb); + } + if (any(greaterThan(lo, hi))) { + // Every texel transparent: colour0 == colour1 selects three-colour mode, index 3. + return uvec2(0u, 0xFFFFFFFFu); + } + const vec3 center = (lo + hi) * 0.5; + const vec3 extent = hi - lo; + const int major = extent.g >= extent.r && extent.g >= extent.b ? 1 : (extent.r >= extent.b ? 0 : 2); + vec3 covariance = vec3(0.0); + for (int i = 0; i < 16; ++i) { + if (punch_through && texels[i].a < 0.5) { + continue; + } + const vec3 d = texels[i].rgb - center; + covariance += d * d[major]; + } + vec3 e0 = hi; + vec3 e1 = lo; + for (int c = 0; c < 3; ++c) { + if (covariance[c] < 0.0) { + e0[c] = lo[c]; + e1[c] = hi[c]; + } + } + const vec3 inset = (e0 - e1) / 16.0; + e0 -= inset; + e1 += inset; + + uint q0 = Pack565(e0); + uint q1 = Pack565(e1); + // Four-colour mode needs colour0 > colour1, three-colour mode colour0 <= colour1. + if (punch_through ? q0 > q1 : q0 < q1) { + const uint t = q0; + q0 = q1; + q1 = t; + } + if (!punch_through && q0 == q1) { + return uvec2(q0 | (q1 << 16), 0u); + } + vec3 palette[4]; + palette[0] = Unpack565(q0); + palette[1] = Unpack565(q1); + if (punch_through) { + palette[2] = (palette[0] + palette[1]) * 0.5; + palette[3] = vec3(0.0); + } else { + palette[2] = (2.0 * palette[0] + palette[1]) / 3.0; + palette[3] = (palette[0] + 2.0 * palette[1]) / 3.0; + } + const int entries = punch_through ? 3 : 4; + uint indices = 0u; + for (int i = 0; i < 16; ++i) { + uint best = 0u; + if (punch_through && texels[i].a < 0.5) { + best = 3u; + } else { + float best_distance = Distance2(texels[i].rgb, palette[0]); + for (int p = 1; p < entries; ++p) { + const float distance = Distance2(texels[i].rgb, palette[p]); + if (distance < best_distance) { + best_distance = distance; + best = uint(p); + } + } + } + indices |= best << (2 * i); + } + return uvec2(q0 | (q1 << 16), indices); +} + +// BC3 alpha block: eight-value mode (alpha0 > alpha1), indices 0 = alpha0, 1 = alpha1, 2..7 the +// ramp from alpha0 to alpha1. +uvec2 EncodeAlpha(in vec4 texels[16]) { + float lo = 1.0; + float hi = 0.0; + for (int i = 0; i < 16; ++i) { + lo = min(lo, texels[i].a); + hi = max(hi, texels[i].a); + } + const uint a0 = uint(round(hi * 255.0)); + const uint a1 = uint(round(lo * 255.0)); + uint bits_lo = a0 | (a1 << 8); + uint bits_hi = 0u; + if (a0 == a1) { + return uvec2(bits_lo, bits_hi); + } + for (uint i = 0u; i < 16u; ++i) { + const float t = (texels[i].a * 255.0 - float(a1)) / float(a0 - a1); + const uint q = uint(clamp(round(t * 7.0), 0.0, 7.0)); + const uint index = q == 7u ? 0u : (q == 0u ? 1u : 8u - q); + const uint bit = 16u + 3u * i; + if (bit < 32u) { + bits_lo |= index << bit; + if (bit > 29u) { + bits_hi |= index >> (32u - bit); + } + } else { + bits_hi |= index << (bit - 32u); + } + } + return uvec2(bits_lo, bits_hi); +} + +void main() { + const uvec3 id = gl_GlobalInvocationID; + if (id.x >= blocks_x || id.y >= blocks_y) { + return; + } + vec4 texels[16]; + const ivec2 base = ivec2(id.xy) * 4; + const ivec2 last = ivec2(width, height) - 1; + bool any_transparent = false; + for (int y = 0; y < 4; ++y) { + for (int x = 0; x < 4; ++x) { + // Edge blocks repeat the last row/column so padding doesn't widen the endpoints. + const ivec2 coord = min(base + ivec2(x, y), last); + texels[y * 4 + x] = imageLoad(source, ivec3(coord, int(id.z))); + any_transparent = any_transparent || texels[y * 4 + x].a < 0.5; + } + } + const uint block = (id.z * blocks_y + id.y) * blocks_x + id.x; + if (bc3 != 0u) { + blocks[out_offset + block * 2u] = EncodeAlpha(texels); + blocks[out_offset + block * 2u + 1u] = EncodeColor(texels, false); + } else { + blocks[out_offset + block] = EncodeColor(texels, any_transparent); + } +} |
