Skip to content

Instantly share code, notes, and snippets.

@leegao
Created May 20, 2026 16:21
Show Gist options
  • Select an option

  • Save leegao/e43435d516c5ed2e7a695f95258c16cd to your computer and use it in GitHub Desktop.

Select an option

Save leegao/e43435d516c5ed2e7a695f95258c16cd to your computer and use it in GitHub Desktop.
ETC1 real-time compression kernel
__glsl_extension(GL_EXT_shader_realtime_clock) uint2 clockRealtime2x32EXT() {
__target_switch {
case spirv:
return spirv_asm {
OpCapability ShaderClockKHR;
OpExtension "SPV_KHR_shader_clock";
result : $$uint2 = OpReadClockKHR Device;
};
case glsl:
__intrinsic_asm "clockRealtime2x32EXT()";
default:
return 0;
}
}
[[vk::binding(0, 0)]]
RWTexture2D<float4> u_InputTexture;
[[vk::binding(1, 0)]]
RWStructuredBuffer<uint4> etc1_blocks;
[[vk::binding(2, 0)]]
RWStructuredBuffer<uint4> elapsed_time;
struct PushConstants {
int2 u_TextureDimensions;
};
[[vk::push_constant]]
PushConstants pushConstants;
static const uint8_t alpha_modifier_table[4] = { 2, 5, 8, 14 };
static const uint8_t etc1_mod_table[16] = {
2, 8, 5, 17, 9, 29, 13, 42, 18, 60, 24, 80, 33, 106, 47, 183,
};
uint8_t select_table_index(float c_target) {
if (c_target < 12.5)
return 0;
if (c_target < 23.0)
return 1;
if (c_target < 35.5)
return 2;
if (c_target < 51.0)
return 3;
if (c_target < 70.0)
return 4;
if (c_target < 93.0)
return 5;
if (c_target < 144.5)
return 6;
return 7;
}
inline void get_table_modifiers(int idx, out int16_t small, out int16_t big) {
small = etc1_mod_table[idx * 2];
big = etc1_mod_table[idx * 2 + 1];
}
// 0 1 | 2 3
// 4 5 | 6 7
// -------+-------
// 8 9 | 10 11
// 12 13 | 14 15
// #define USE_FLAG_MODE_ARRAY
#ifdef USE_FLAG_MODE_ARRAY
struct FlagMode {
uint8_t idx[8];
static const FlagMode VERTICAL_LEFT = FlagMode( { 0, 1, 4, 5, 8, 9, 12, 13 });
static const FlagMode VERTICAL_RIGHT =
FlagMode( { 2, 3, 6, 7, 10, 11, 14, 15 });
static const FlagMode HORIZONTAL_TOP = FlagMode( { 0, 1, 2, 3, 4, 5, 6, 7 });
static const FlagMode HORIZONTAL_BOTTOM =
FlagMode( { 8, 9, 10, 11, 12, 13, 14, 15 });
__subscript(uint8_t n)->uint8_t {
get { return idx[n]; }
}
};
#else
// static const uint8_t LEFT_ZIG[8] = { 0, 1, 4, 5, 8, 9, 12, 13 };
// For some reason, using a mode-indexed FlagMode causes slangc to
// compile this shader incorrectly...
struct FlagMode {
uint8_t mode;
static const FlagMode HORIZONTAL_TOP = FlagMode(0);
static const FlagMode HORIZONTAL_BOTTOM = FlagMode(1);
static const FlagMode VERTICAL_LEFT = FlagMode(2);
static const FlagMode VERTICAL_RIGHT = FlagMode(3);
__subscript(uint8_t n)->uint8_t {
get {
if (mode == 0) {
return n;
} else if (mode == 1) {
return n + 8;
} else if (mode == 2) {
return (((n >> 1) << 2) + (n & 1));
// return LEFT_ZIG[n];
} else {
return (((n >> 1) << 2) + (n & 1)) + 2;
// return LEFT_ZIG[n] + 2;
}
}
}
};
#endif
struct Pixels {
uint8_t3 data[16];
__subscript(uint n)->uint8_t3 {
get { return data[n]; }
set { data[n] = newValue; }
}
Luminances Y() {
Luminances Y;
[[unroll]]
for (int i = 0; i < 16; ++i) {
Y[i] = luminance(data[i]);
}
return Y;
}
};
struct Luminances {
uint8_t data[16];
__subscript(uint n)->int16_t {
get { return int16_t(data[n]); }
set { data[n] = uint8_t(newValue); }
}
};
uint8_t luminance(uint8_t3 pixel) {
return uint8_t(int16_t(0.299h * half(pixel.x) + 0.587h * half(pixel.y) +
0.114h * half(pixel.z)));
}
inline uint2 pack_etc1_individual_payload(uint8_t3 c0, uint8_t3 c1, uint8_t t0,
uint8_t t1, bool horizontal,
const Selectors selectors) {
uint py = 0u;
py |= (uint(c0.x) << 28) | (uint(c0.y) << 20) | (uint(c0.z) << 12);
py |= (uint(c1.x) << 24) | (uint(c1.y) << 16) | (uint(c1.z) << 8);
py |= (uint(t0) << 5) | (uint(t1) << 2);
py |= (0u << 1);
py |= uint(horizontal);
uint px = 0u;
[[unroll]]
for (uint8_t i = 0; i < 16; ++i) {
int x = i % 4;
int y = i / 4;
int lp = x * 4 + y;
uint8_t sel = selectors[i];
uint lsb = uint(sel & 1);
uint msb = uint((sel >> 1) & 1);
px |= (lsb << uint(lp));
px |= (msb << uint(16 + lp));
}
return uint2(px, py);
}
void get_avg_color(const Pixels pixels, const FlagMode sb, out uint8_t3 color) {
int16_t3 avg = int16_t3(0);
for (uint8_t i = 0; i < 8; ++i) {
int idx = sb[i];
avg += pixels[idx];
}
avg /= 8;
color.x = clamp<uint8_t>(uint8_t(round(half(avg.x) / 17)), 0, 15);
color.y = clamp<uint8_t>(uint8_t(round(half(avg.y) / 17)), 0, 15);
color.z = clamp<uint8_t>(uint8_t(round(half(avg.z) / 17)), 0, 15);
}
int16_t opt_selector(int table_idx, int Y_idx, int Y_avg, out uint8_t s) {
int16_t d, c;
get_table_modifiers(table_idx, d, c);
int16_t T = c / 2 + d / 2;
var e = Y_idx - Y_avg;
uint8_t sign_bit = uint8_t((e < 0) ? 1 : 0);
uint8_t mag_bit = uint8_t((abs(e) > T) ? 1 : 0);
s = (sign_bit << 1) | mag_bit;
return int16_t((mag_bit == 1 ? c : d) * (sign_bit == 1 ? -1 : 1));
}
inline uint8_t get_table_index(const Luminances Y, const FlagMode sb) {
var high_Y_sum = 0.0h;
var low_Y_sum = 0.0h;
uint8_t high_count = 0;
uint8_t low_count = 0;
int16_t avg_Y = 0;
for (uint8_t i = 0; i < 8; ++i) {
int idx = sb[i];
avg_Y += Y[idx];
}
avg_Y /= 8;
for (uint8_t i = 0; i < 8; ++i) {
uint8_t idx = sb[i];
if (Y[idx] >= avg_Y) {
high_Y_sum += half(Y[idx]);
high_count++;
} else {
low_Y_sum += half(Y[idx]);
low_count++;
}
}
half high_avg =
(high_count > 0) ? (high_Y_sum / half(high_count)) : half(avg_Y);
half low_avg = (low_count > 0) ? (low_Y_sum / half(low_count)) : half(avg_Y);
var spread = high_avg - low_avg;
var c_target = 0.5h * spread;
return select_table_index(c_target);
}
struct Selectors {
uint8_t data[16]; // 16x 2-bit selectors
__subscript(uint8_t n)->uint8_t {
get { return data[n]; }
set { data[n] = newValue; }
}
};
void process_subblock<let nb : int8_t = 1>(const Pixels pixels,
const Luminances Y,
const FlagMode sb,
inout uint8_t3 packed_color,
out uint8_t best_table,
out Selectors best_selectors) {
get_avg_color(pixels, sb, packed_color);
var Y_base = luminance(packed_color * 17);
int table_idx = get_table_index(Y, sb);
int8_t initial_table_idx = clamp<int8_t>(int8_t(table_idx), nb, 7 - nb);
uint8_t3 base8 = packed_color * 17;
int min_block_error = 2147483647;
for (uint8_t t = initial_table_idx - nb; t <= initial_table_idx + nb; t++) {
int current_table_error = 0;
Selectors current_selectors;
[[unroll]]
for (uint8_t p = 0; p < 8; p++) {
var idx = sb[p];
uint8_t s;
var modifier = opt_selector(t, Y[idx], Y_base, s);
if (nb > 0) {
var pixel = pixels[idx];
var mod_color = clamp(base8 + modifier, 0, 255);
var diff = mod_color - int3(pixel);
var err = dot(diff, diff);
current_table_error += err;
}
current_selectors[idx] = s;
}
if (nb == 0 || current_table_error < min_block_error) {
min_block_error = current_table_error;
best_table = t;
best_selectors = current_selectors;
}
}
}
uint2 encode_etc1(Pixels pixels) {
Luminances Y = pixels.Y();
var Y_top = Y[0] + Y[1] + Y[2] + Y[3] + Y[4] + Y[5] + Y[6] + Y[7];
var Y_total =
Y_top + Y[8] + Y[9] + Y[10] + Y[11] + Y[12] + Y[13] + Y[14] + Y[15];
var Y_left = Y[0] + Y[1] + Y[4] + Y[5] + Y[8] + Y[9] + Y[12] + Y[13];
var H_contrast = abs(Y_top - (Y_total - Y_top));
var V_contrast = abs(Y_left - (Y_total - Y_left));
#ifdef USE_FLAG_MODE_ARRAY
const bool horizontal = (H_contrast > V_contrast);
#else
const bool horizontal = true;
#endif
const FlagMode sb0 =
horizontal ? FlagMode.HORIZONTAL_TOP : FlagMode.VERTICAL_LEFT;
const FlagMode sb1 =
horizontal ? FlagMode.HORIZONTAL_BOTTOM : FlagMode.VERTICAL_RIGHT;
uint8_t3 best_c0, best_c1;
uint8_t best_t0, best_t1;
Selectors best_selectors;
const int8_t nb = 1; // consider +/- 1 additional neighboring table (3 total)
process_subblock<nb>(pixels, Y, sb0, best_c0, best_t0, best_selectors);
process_subblock<nb>(pixels, Y, sb1, best_c1, best_t1, best_selectors);
return pack_etc1_individual_payload(best_c0, best_c1, best_t0, best_t1,
horizontal, best_selectors);
}
uint flip_endian_encode(uint v) {
uint4 words = uint4(v) >> uint4(0, 8, 16, 24);
words &= 0xffu;
return (words.x << 24u) | (words.y << 16u) | (words.z << 8u) |
(words.w << 0u);
}
[shader("compute")]
[numthreads(8, 8, 1)]
void main(uint3 gl_GlobalInvocationID: SV_DispatchThreadID) {
int2 block_pos = int2(gl_GlobalInvocationID.xy);
int2 tex_coord = block_pos * 4;
uint2 startTime = clockRealtime2x32EXT();
if (any(tex_coord >= pushConstants.u_TextureDimensions))
return;
Pixels pixels;
half alphas[16];
for (int y = 0; y < 4; y++) {
for (int x = 0; x < 4; x++) {
float4 tex_element = u_InputTexture[tex_coord + int2(x, y)];
pixels[y * 4 + x] = uint8_t3(round(tex_element.rgb * 255.0));
alphas[y * 4 + x] = half(tex_element.a);
}
}
// Alpha Encoding
uint min_a = 255;
uint max_a = 0;
for (int i = 0; i < 16; ++i) {
uint a = uint(alphas[i] * 255.0);
min_a = min(min_a, a);
max_a = max(max_a, a);
}
uint base = min_a;
uint range = max_a - min_a;
uint multiplier = clamp(range / 22, 0u, 15u);
uint alpha_p0 = (base & 0xFF) | ((multiplier & 0xF) << 8) | (0u << 12);
uint alpha_p1 = 0;
for (int i = 0; i < 16; ++i) {
uint a = uint(alphas[i] * 255.0);
int diff = int(a) - int(base);
int best_idx = 0;
int min_err = 1000;
for (int m = 0; m < 4; ++m) {
int val = alpha_modifier_table[m] * int(multiplier);
int err = abs(diff - val);
if (err < min_err) {
min_err = err;
best_idx = m;
}
}
if (i < 10)
alpha_p0 |= (uint(best_idx) << (16 + i * 3));
else
alpha_p1 |= (uint(best_idx) << ((i - 10) * 3));
}
uint2 packed = encode_etc1(pixels);
uint color_p0 = flip_endian_encode(packed.y);
uint color_p1 = flip_endian_encode(packed.x);
uint buffer_index =
block_pos.y * (pushConstants.u_TextureDimensions.x / 4) + block_pos.x;
etc1_blocks[buffer_index] = uint4(alpha_p0, alpha_p1, color_p0, color_p1);
// uint2 endTime = clockRealtime2x32EXT();
elapsed_time[buffer_index] =
uint4(startTime.x, startTime.y, startTime.x, startTime.y);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment