1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
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);
}
}
|