/* * The DDS Converter's block codec: decodes and encodes the BC (DXT) texture * formats a surface at a time. Decoding is bcdec (iOrange/bcdec); encoding is * rgbcx for BC1/BC3/BC4/BC5 and bc7enc for BC7 (richgel999/bc7enc_rdo). Both * are MIT or public domain. Everything else - DDS headers, uncompressed * formats, mipmaps, TGA - is done in TypeScript by the page. * * Format numbers (shared with the page): * 1 BC1 2 BC2 3 BC3 4 BC4 5 BC5 6 BC6H unsigned 7 BC7 * 14 BC4 signed 15 BC5 signed 16 BC6H signed */ #include #include #include #include #define BCDEC_IMPLEMENTATION #define BCDEC_BC4BC5_PRECISE #include "bcdec.h" #include "rgbcx.h" #include "bc7enc.h" static int block_bytes(int format) { switch (format) { case 1: case 4: case 14: return 8; default: return 16; } } static void set_bits(uint8_t *b, int pos, int count, uint32_t value) { for (int i = 0; i < count; i++, pos++) { if (value & (1u << i)) b[pos >> 3] |= (uint8_t)(1u << (pos & 7)); else b[pos >> 3] &= (uint8_t)~(1u << (pos & 7)); } } /* * bc7enc encodes an opaque block in mode 6 with alpha endpoints of 127 and * p-bits chosen for the colour, so where a p-bit is 0 alpha decodes as 254, * not 255: an opaque texture comes out very slightly transparent. Mode 6 * shares each endpoint's p-bit across all four channels, so setting both to 1 * (and alpha to 127) makes alpha exactly 255 and moves colour by at most one * step in 255. If any pixel is still not opaque, the block is encoded again * in mode 1, which has no alpha at all. */ static void fix_opaque_bc7(uint8_t *block, const uint8_t *px, const bc7enc_compress_block_params *p) { // Mode 6 is 0000001 in the first seven bits; then R0 R1 G0 G1 B0 B1 A0 A1 (7 bits each), P0, P1. if ((block[0] & 0x7f) == 0x40) { set_bits(block, 49, 7, 127); set_bits(block, 56, 7, 127); set_bits(block, 63, 1, 1); set_bits(block, 64, 1, 1); } uint8_t check[64]; bcdec_bc7(block, check, 16); for (int i = 0; i < 16; i++) { if (check[i * 4 + 3] != 255) { bc7enc_compress_block_params q = *p; q.m_mode_mask = 1u << 1; if (!q.m_max_partitions) q.m_max_partitions = 16; bc7enc_compress_block(block, px, &q); return; } } } extern "C" { EMSCRIPTEN_KEEPALIVE void *ddsw_alloc(size_t n) { return malloc(n ? n : 1); } EMSCRIPTEN_KEEPALIVE void ddsw_free(void *p) { free(p); } static int ready = 0; EMSCRIPTEN_KEEPALIVE void ddsw_init(void) { if (ready) return; rgbcx::init(rgbcx::bc1_approx_mode::cBC1Ideal); bc7enc_compress_block_init(); ready = 1; } /* * Decodes a w x h surface of 4x4 blocks. Formats 1-5, 7, 14, 15 write RGBA8 * (w * h * 4 bytes): BC4 as grey, BC5 as red and green with blue 0, signed * values shifted to 0-255. Formats 6 and 16 (BC6H) write RGBA float32 * (w * h * 4 floats) for the page to tone-map. Returns 0, or -1 when src is * too short for the size. */ EMSCRIPTEN_KEEPALIVE int ddsw_decode(int format, const uint8_t *src, size_t src_len, int w, int h, void *dst) { const int bw = (w + 3) / 4, bh = (h + 3) / 4, bb = block_bytes(format); if ((size_t)bw * bh * bb > src_len) return -1; const int hdr = format == 6 || format == 16; for (int by = 0; by < bh; by++) { for (int bx = 0; bx < bw; bx++) { const uint8_t *blk = src + ((size_t)by * bw + bx) * bb; uint8_t px[64]; float fx[48]; switch (format) { case 1: bcdec_bc1(blk, px, 16); break; case 2: bcdec_bc2(blk, px, 16); break; case 3: bcdec_bc3(blk, px, 16); break; case 7: bcdec_bc7(blk, px, 16); break; case 4: case 14: { uint8_t r[16]; bcdec_bc4(blk, r, 4, format == 14); for (int i = 0; i < 16; i++) { uint8_t v = format == 14 ? (uint8_t)((int8_t)r[i] + 128) : r[i]; px[i * 4] = px[i * 4 + 1] = px[i * 4 + 2] = v; px[i * 4 + 3] = 255; } break; } case 5: case 15: { uint8_t rg[32]; bcdec_bc5(blk, rg, 8, format == 15); for (int i = 0; i < 16; i++) { uint8_t r = rg[i * 2], g = rg[i * 2 + 1]; if (format == 15) { r = (uint8_t)((int8_t)r + 128); g = (uint8_t)((int8_t)g + 128); } px[i * 4] = r; px[i * 4 + 1] = g; px[i * 4 + 2] = 0; px[i * 4 + 3] = 255; } break; } case 6: case 16: bcdec_bc6h_float(blk, fx, 12, format == 16); break; default: return -2; } for (int y = 0; y < 4; y++) { const int py = by * 4 + y; if (py >= h) break; for (int x = 0; x < 4; x++) { const int pxx = bx * 4 + x; if (pxx >= w) break; const size_t o = ((size_t)py * w + pxx) * 4; if (hdr) { float *d = (float *)dst; const float *s = fx + (y * 4 + x) * 3; d[o] = s[0]; d[o + 1] = s[1]; d[o + 2] = s[2]; d[o + 3] = 1.0f; } else { memcpy((uint8_t *)dst + o, px + (y * 4 + x) * 4, 4); } } } } } return 0; } /* * Encodes block rows [by0, by1) of a w x h RGBA8 image into dst, which holds * the whole surface. Edge blocks repeat the last row and column. Called a few * rows at a time so the page can show progress. quality: 0 fast, 1 normal, * 2 best. Supports formats 1, 3, 4, 5 and 7. */ EMSCRIPTEN_KEEPALIVE int ddsw_encode(int format, const uint8_t *rgba, int w, int h, int by0, int by1, int quality, uint8_t *dst) { ddsw_init(); const int bw = (w + 3) / 4, bb = block_bytes(format); static const uint32_t bc1_levels[3] = {5, 10, 18}; bc7enc_compress_block_params p; if (format == 7) { bc7enc_compress_block_params_init(&p); p.m_uber_level = quality == 0 ? 0 : quality == 1 ? 1 : 4; p.m_max_partitions = quality == 0 ? 16 : BC7ENC_MAX_PARTITIONS; } for (int by = by0; by < by1; by++) { for (int bx = 0; bx < bw; bx++) { uint8_t px[64]; for (int y = 0; y < 4; y++) { const int sy = by * 4 + y < h ? by * 4 + y : h - 1; for (int x = 0; x < 4; x++) { const int sx = bx * 4 + x < w ? bx * 4 + x : w - 1; memcpy(px + (y * 4 + x) * 4, rgba + ((size_t)sy * w + sx) * 4, 4); } } uint8_t *out = dst + ((size_t)by * bw + bx) * bb; const uint32_t level = bc1_levels[quality < 0 ? 0 : quality > 2 ? 2 : quality]; switch (format) { // No 3-colour blocks: their fourth colour decodes as transparent black in DX10+. case 1: rgbcx::encode_bc1(level, out, px, false, false); break; case 3: if (quality == 2) rgbcx::encode_bc3_hq(level, out, px); else rgbcx::encode_bc3(level, out, px); break; case 4: if (quality == 2) rgbcx::encode_bc4_hq(out, px, 4); else rgbcx::encode_bc4(out, px, 4); break; case 5: if (quality == 2) rgbcx::encode_bc5_hq(out, px, 0, 1, 4); else rgbcx::encode_bc5(out, px, 0, 1, 4); break; case 7: { bc7enc_compress_block(out, px, &p); int opaque = 1; for (int i = 0; i < 16; i++) opaque &= px[i * 4 + 3] == 255; if (opaque) fix_opaque_bc7(out, px, &p); break; } default: return -2; } } } return 0; } }