// basisu_dds_transcoder.inl // // Included at the END of basisu_transcoder.cpp (after basisu_xbc7_decoder.inl), OUTSIDE the file's // `namespace basist {` block -- this .inl reopens namespace basist itself. // // Two things live here, both in namespace basist: // 1. Plain, vendor-neutral BC1/BC3/BC4/BC5 (DXT1/DXT5/BC4/BC5) block UNPACKERS in namespace bcu // ("bcu" == BC Unpack), moved here from the encoder's reference unpackers in // encoder/basisu_gpu_texture.cpp (NOT the NV/AMD-specific variants, which stay encoder-only). // color_rgba here resolves to basist::color_rgba. // 2. The DDS reader/transcoder (basist::dds_transcoder), further below. // Both let the transcoder decode these formats with zero dependency on the encoder library. namespace basist { namespace bcu { //-------------------------------------------------------------------------------------------------- // BC1 // Decodes a BC1-layout color block; returns true if it used 3-color punchthrough mode. force_4color=true forces // 4-color mode (the color0<=color1 punchthrough switch is BC1-only; BC2/BC3 ignore it, per the D3D/S3TC specs). bool unpack_bc1(const void* pBlock_bits, color_rgba* pPixels, bool set_alpha, bool force_4color) { static_assert(sizeof(bc1_block) == 8, "sizeof(bc1_block) == 8"); const bc1_block* pBlock = static_cast(pBlock_bits); const uint32_t l = pBlock->get_low_color(); const uint32_t h = pBlock->get_high_color(); color_rgba c[4]; uint32_t r0, g0, b0, r1, g1, b1; bc1_block::unpack_color(l, r0, g0, b0); bc1_block::unpack_color(h, r1, g1, b1); c[0].set_noclamp_rgba(r0, g0, b0, 255); c[1].set_noclamp_rgba(r1, g1, b1, 255); bool used_punchthrough = false; if ((l > h) || force_4color) // BC2/BC3 force 4-color mode; BC1 uses it only when color0 > color1 { c[2].set_noclamp_rgba((r0 * 2 + r1) / 3, (g0 * 2 + g1) / 3, (b0 * 2 + b1) / 3, 255); c[3].set_noclamp_rgba((r1 * 2 + r0) / 3, (g1 * 2 + g0) / 3, (b1 * 2 + b0) / 3, 255); } else { c[2].set_noclamp_rgba((r0 + r1) / 2, (g0 + g1) / 2, (b0 + b1) / 2, 255); c[3].set_noclamp_rgba(0, 0, 0, 0); used_punchthrough = true; } if (set_alpha) { for (uint32_t y = 0; y < 4; y++, pPixels += 4) { pPixels[0] = c[pBlock->get_selector(0, y)]; pPixels[1] = c[pBlock->get_selector(1, y)]; pPixels[2] = c[pBlock->get_selector(2, y)]; pPixels[3] = c[pBlock->get_selector(3, y)]; } } else { for (uint32_t y = 0; y < 4; y++, pPixels += 4) { pPixels[0].set_rgb(c[pBlock->get_selector(0, y)]); pPixels[1].set_rgb(c[pBlock->get_selector(1, y)]); pPixels[2].set_rgb(c[pBlock->get_selector(2, y)]); pPixels[3].set_rgb(c[pBlock->get_selector(3, y)]); } } return used_punchthrough; } //-------------------------------------------------------------------------------------------------- // BC3-5 struct bc4_block { enum { cBC4SelectorBits = 3, cTotalSelectorBytes = 6, cMaxSelectorValues = 8 }; uint8_t m_endpoints[2]; uint8_t m_selectors[cTotalSelectorBytes]; inline uint32_t get_low_alpha() const { return m_endpoints[0]; } inline uint32_t get_high_alpha() const { return m_endpoints[1]; } inline bool is_alpha6_block() const { return get_low_alpha() <= get_high_alpha(); } inline uint64_t get_selector_bits() const { return ((uint64_t)((uint32_t)m_selectors[0] | ((uint32_t)m_selectors[1] << 8U) | ((uint32_t)m_selectors[2] << 16U) | ((uint32_t)m_selectors[3] << 24U))) | (((uint64_t)m_selectors[4]) << 32U) | (((uint64_t)m_selectors[5]) << 40U); } inline uint32_t get_selector(uint32_t x, uint32_t y, uint64_t selector_bits) const { assert((x < 4U) && (y < 4U)); return (selector_bits >> (((y * 4) + x) * cBC4SelectorBits)) & (cMaxSelectorValues - 1); } static inline uint32_t get_block_values6(uint8_t* pDst, uint32_t l, uint32_t h) { pDst[0] = static_cast(l); pDst[1] = static_cast(h); pDst[2] = static_cast((l * 4 + h) / 5); pDst[3] = static_cast((l * 3 + h * 2) / 5); pDst[4] = static_cast((l * 2 + h * 3) / 5); pDst[5] = static_cast((l + h * 4) / 5); pDst[6] = 0; pDst[7] = 255; return 6; } static inline uint32_t get_block_values8(uint8_t* pDst, uint32_t l, uint32_t h) { pDst[0] = static_cast(l); pDst[1] = static_cast(h); pDst[2] = static_cast((l * 6 + h) / 7); pDst[3] = static_cast((l * 5 + h * 2) / 7); pDst[4] = static_cast((l * 4 + h * 3) / 7); pDst[5] = static_cast((l * 3 + h * 4) / 7); pDst[6] = static_cast((l * 2 + h * 5) / 7); pDst[7] = static_cast((l + h * 6) / 7); return 8; } static inline uint32_t get_block_values(uint8_t* pDst, uint32_t l, uint32_t h) { if (l > h) return get_block_values8(pDst, l, h); else return get_block_values6(pDst, l, h); } }; // Writes a single channel per pixel (the caller selects which via pPixels/stride). void unpack_bc4(const void* pBlock_bits, uint8_t* pPixels, uint32_t stride) { static_assert(sizeof(bc4_block) == 8, "sizeof(bc4_block) == 8"); const bc4_block* pBlock = static_cast(pBlock_bits); uint8_t sel_values[8]; bc4_block::get_block_values(sel_values, pBlock->get_low_alpha(), pBlock->get_high_alpha()); const uint64_t selector_bits = pBlock->get_selector_bits(); for (uint32_t y = 0; y < 4; y++, pPixels += (stride * 4U)) { pPixels[0] = sel_values[pBlock->get_selector(0, y, selector_bits)]; pPixels[stride * 1] = sel_values[pBlock->get_selector(1, y, selector_bits)]; pPixels[stride * 2] = sel_values[pBlock->get_selector(2, y, selector_bits)]; pPixels[stride * 3] = sel_values[pBlock->get_selector(3, y, selector_bits)]; } } // BC3 = BC4 alpha block + BC1-layout color block (always 4-color, see unpack_bc1's force_4color). Alpha comes // solely from the BC4 block. void unpack_bc3(const void* pBlock_bits, color_rgba* pPixels) { unpack_bc1((const uint8_t*)pBlock_bits + sizeof(bc4_block), pPixels, true, /*force_4color*/ true); unpack_bc4(pBlock_bits, &pPixels[0].a, sizeof(color_rgba)); } // BC2 (DXT2/DXT3) = 8 bytes of EXPLICIT 4-bit-per-texel alpha, then a BC1-layout color block decoded ALWAYS in // 4-color mode (same color path as BC3). 4-bit alpha a expands to 8-bit as a*17 == (a<<4)|a. Decode alpha first, // then the color with set_alpha=false so the color path doesn't clobber it. void unpack_bc2(const void* pBlock_bits, color_rgba* pPixels) { const uint8_t* pBlock = static_cast(pBlock_bits); // Bytes 0-7: one little-endian 16-bit word per row (y); texel x is nibble (word >> 4*x) & 0xF. for (uint32_t y = 0; y < 4; y++) { const uint32_t row = (uint32_t)pBlock[y * 2] | ((uint32_t)pBlock[y * 2 + 1] << 8); for (uint32_t x = 0; x < 4; x++) { const uint32_t a4 = (row >> (x * 4)) & 0xFu; pPixels[y * 4 + x].a = (uint8_t)(a4 * 17u); // 4-bit -> 8-bit replication } } // Bytes 8-15: BC1-layout color, forced 4-color, set_alpha=false to preserve the explicit alpha above. unpack_bc1(pBlock + 8, pPixels, /*set_alpha*/ false, /*force_4color*/ true); } // Writes only the R and G channels per pixel. void unpack_bc5(const void* pBlock_bits, color_rgba* pPixels) { unpack_bc4(pBlock_bits, &pPixels[0].r, sizeof(color_rgba)); unpack_bc4((const uint8_t*)pBlock_bits + sizeof(bc4_block), &pPixels[0].g, sizeof(color_rgba)); } } // namespace bcu } // namespace basist // ============================================================================================== // DDS transcoder (basist::dds_transcoder) -- moved here from the former CLI basisu_dds_transcoder.cpp. // ============================================================================================== namespace basist { namespace { // ---- DDS on-disk constants ---- const uint32_t DDS_MAGIC = 0x20534444; // "DDS " const uint32_t DDS_HEADER_SIZE = 124; const uint32_t DDS_PIXELFORMAT_SIZE = 32; const uint32_t DDS_DX10_HEADER_SIZE = 20; // DDS_HEADER dwFlags (only DDSD_PITCH is acted on; others are advisory per the MS guide) const uint32_t DDSD_PITCH = 0x8; // DDS_PIXELFORMAT dwFlags const uint32_t DDPF_ALPHAPIXELS = 0x1; const uint32_t DDPF_ALPHA = 0x2; // alpha-only (D3DFMT_A8) const uint32_t DDPF_FOURCC = 0x4; const uint32_t DDPF_RGB = 0x40; const uint32_t DDPF_LUMINANCE = 0x20000; // dwCaps2 const uint32_t DDSCAPS2_CUBEMAP = 0x200; const uint32_t DDSCAPS2_CUBEMAP_ALLFACES = 0xFC00; // all six POSITIVEX..NEGATIVEZ bits const uint32_t DDSCAPS2_VOLUME = 0x200000; // DX10 D3D10_RESOURCE_DIMENSION const uint32_t DDS_DIMENSION_TEXTURE1D = 2; const uint32_t DDS_DIMENSION_TEXTURE2D = 3; const uint32_t DDS_DIMENSION_TEXTURE3D = 4; // DX10 miscFlag const uint32_t DDS_RESOURCE_MISC_TEXTURECUBE = 0x4; inline uint32_t make_fourcc(char a, char b, char c, char d) { return (uint32_t)(uint8_t)a | ((uint32_t)(uint8_t)b << 8) | ((uint32_t)(uint8_t)c << 16) | ((uint32_t)(uint8_t)d << 24); } // DXGI_FORMAT subset we recognize enum { DXGI_R8G8_UNORM = 49, DXGI_R8_UNORM = 61, DXGI_A8_UNORM = 65, DXGI_R8G8B8A8_UNORM = 28, DXGI_R8G8B8A8_UNORM_SRGB = 29, DXGI_BC1_UNORM = 71, DXGI_BC1_UNORM_SRGB = 72, DXGI_BC2_UNORM = 74, DXGI_BC2_UNORM_SRGB = 75, DXGI_BC3_UNORM = 77, DXGI_BC3_UNORM_SRGB = 78, DXGI_BC4_UNORM = 80, DXGI_BC4_SNORM = 81, DXGI_BC5_UNORM = 83, DXGI_BC5_SNORM = 84, DXGI_B5G6R5_UNORM = 85, DXGI_B5G5R5A1_UNORM = 86, DXGI_B8G8R8A8_UNORM = 87, DXGI_B8G8R8X8_UNORM = 88, DXGI_B8G8R8A8_UNORM_SRGB = 91, DXGI_B8G8R8X8_UNORM_SRGB = 93, DXGI_BC7_UNORM = 98, DXGI_BC7_UNORM_SRGB = 99, DXGI_B4G4R4A4_UNORM = 115 }; inline uint32_t rd_u32(const uint8_t* p) { return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); } inline uint32_t rd_u24(const uint8_t* p) { return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16); } inline uint32_t rd_u16(const uint8_t* p) { return (uint32_t)p[0] | ((uint32_t)p[1] << 8); } inline uint32_t pop_count(uint32_t v) { uint32_t n = 0; while (v) { n += (v & 1); v >>= 1; } return n; } inline uint32_t trail_zeros(uint32_t v) { if (!v) return 0; uint32_t n = 0; while (!(v & 1)) { v >>= 1; n++; } return n; } // Extract the masked field and expand to 8 bits by bit replication (e.g. 5-bit: (v<<3)|(v>>2)) -- the // GPU/DirectXTex UNORM convention (0->0, max->255, monotonic). Absent channel (m_channel_bits==0) => 255. // shift/bit-width are precomputed in init() (see dds_uncompressed_channel). inline uint32_t decode_channel(uint32_t raw, const dds_uncompressed_channel& chan) { if (!chan.m_channel_bits) return 255; const uint32_t channel_value = (raw & chan.m_mask) >> chan.m_shift; if (chan.m_channel_bits >= 8) return (channel_value >> (chan.m_channel_bits - 8)) & 0xFFu; // Replicate the m_channel_bits-wide pattern across the 8-bit output, MSB-first. uint32_t result = 0; for (int shift = 8 - (int)chan.m_channel_bits; shift > -(int)chan.m_channel_bits; shift -= (int)chan.m_channel_bits) result |= (shift >= 0) ? (channel_value << shift) : (channel_value >> (-shift)); return result & 0xFFu; } // Byte offset of a clean 8-bit channel mask -- i.e. 0xFF positioned on a byte boundary, so the mask is // one of 0x000000FF, 0x0000FF00, 0x00FF0000, 0xFF000000. Returns -1 if the mask isn't byte-aligned (so // the caller must fall back to the generic decode_channel path). Used once per image (see init()) to // precompute the uncompressed fast-path channel offsets. inline int chan_byte_offset(uint32_t mask) { if (!mask) return -1; const uint32_t shift = trail_zeros(mask); if ((shift & 7u) || (mask != (0xFFu << shift))) return -1; return (int)(shift >> 3); } // Accepted DX9 DDPF_RGB uncompressed mask layouts. We decode any of these with the generic mask // decoder, but admit ONLY this known-good list (anything else -> reject) so exotic/garbage masks // can't slip through. Masks are the standard D3DFMT layouts; the effective alpha mask is 0 unless // DDPF_ALPHAPIXELS is set (handled by the caller). Every entry is decodable by code already tested. struct rgb_mask_layout { uint32_t m_bits, m_r, m_g, m_b, m_a; }; const rgb_mask_layout k_accepted_rgb_layouts[] = { // 16-bit { 16, 0xF800u, 0x07E0u, 0x001Fu, 0x0000u }, // R5G6B5 { 16, 0x7C00u, 0x03E0u, 0x001Fu, 0x8000u }, // A1R5G5B5 { 16, 0x7C00u, 0x03E0u, 0x001Fu, 0x0000u }, // X1R5G5B5 { 16, 0x0F00u, 0x00F0u, 0x000Fu, 0xF000u }, // A4R4G4B4 { 16, 0x0F00u, 0x00F0u, 0x000Fu, 0x0000u }, // X4R4G4B4 // 24-bit { 24, 0x00FF0000u, 0x0000FF00u, 0x000000FFu, 0x00000000u }, // R8G8B8 (B,G,R in memory) { 24, 0x000000FFu, 0x0000FF00u, 0x00FF0000u, 0x00000000u }, // B8G8R8 (R,G,B in memory) // 32-bit { 32, 0x00FF0000u, 0x0000FF00u, 0x000000FFu, 0xFF000000u }, // A8R8G8B8 (BGRA in memory) { 32, 0x00FF0000u, 0x0000FF00u, 0x000000FFu, 0x00000000u }, // X8R8G8B8 (BGRX in memory) { 32, 0x000000FFu, 0x0000FF00u, 0x00FF0000u, 0xFF000000u }, // A8B8G8R8 (RGBA in memory) { 32, 0x000000FFu, 0x0000FF00u, 0x00FF0000u, 0x00000000u }, // X8B8G8R8 (RGBX in memory) }; bool is_accepted_rgb_layout(uint32_t bits, uint32_t r, uint32_t g, uint32_t b, uint32_t a) { for (const rgb_mask_layout& L : k_accepted_rgb_layouts) if ((L.m_bits == bits) && (L.m_r == r) && (L.m_g == g) && (L.m_b == b) && (L.m_a == a)) return true; return false; } // Map the detected source kind + (for uncompressed) the committed bit count and channel masks to the // exact physical dds_format. DX9 and DX10 sources converge to the same masks, so this is source-agnostic. // KEEP IN SYNC with k_accepted_rgb_layouts above: every uncompressed layout init() accepts must map to a // non-cInvalid value here (otherwise get_dds_format() silently reports cInvalid on a successfully-parsed file). dds_format classify_dds_format(dds_transcoder::source_kind kind, uint32_t bits, uint32_t r, uint32_t g, uint32_t b, uint32_t a) { typedef dds_transcoder::source_kind sk; switch (kind) { case sk::cBC1: return dds_format::cBC1; case sk::cBC2: return dds_format::cBC2; case sk::cBC3: return dds_format::cBC3; case sk::cBC4: return dds_format::cBC4; case sk::cBC5: return dds_format::cBC5; case sk::cBC7: return dds_format::cBC7; case sk::cUncompressed: break; default: return dds_format::cInvalid; } // Uncompressed: identify by bit count + channel masks (mirrors k_accepted_rgb_layouts). if (bits == 16) { if ((r == 0xF800u) && (g == 0x07E0u) && (b == 0x001Fu)) return dds_format::cR5G6B5; if ((r == 0x7C00u) && (g == 0x03E0u) && (b == 0x001Fu)) return a ? dds_format::cA1R5G5B5 : dds_format::cX1R5G5B5; if ((r == 0x0F00u) && (g == 0x00F0u) && (b == 0x000Fu)) return a ? dds_format::cA4R4G4B4 : dds_format::cX4R4G4B4; } else if (bits == 24) { if (r == 0x00FF0000u) return dds_format::cR8G8B8; if (r == 0x000000FFu) return dds_format::cB8G8R8; } else if (bits == 32) { if (r == 0x00FF0000u) return a ? dds_format::cA8R8G8B8 : dds_format::cX8R8G8B8; if (r == 0x000000FFu) return a ? dds_format::cA8B8G8R8 : dds_format::cX8B8G8R8; } return dds_format::cInvalid; } // How a target format is produced from decoded source texels. enum class repack_mode { cBlock4x4, // per-4x4 block via transcode_4x4_block (BC1/3/4/5/7, ETC1/ETC2/EAC, 32/16bpp uncompressed) cASTC4x4, // per-4x4 block via bc7f::fast_pack_astc (ASTC LDR 4x4) cPVRTC1 // whole-image via encode_pvrtc1 (PVRTC1 4bpp, power-of-2 only) }; // Target description for a transcode destination format. struct target_info { block_format m_bf; uint32_t m_out_bytes; // bytes per output block (compressed/astc/pvrtc) or per pixel (uncompressed) bool m_uncompressed; repack_mode m_mode; int m_def_channel0, m_def_channel1; }; bool get_target_info(transcoder_texture_format fmt, target_info& ti) { ti.m_bf = block_format::cBC1; ti.m_out_bytes = 16; ti.m_uncompressed = false; ti.m_mode = repack_mode::cBlock4x4; ti.m_def_channel0 = -1; ti.m_def_channel1 = -1; switch (fmt) { case transcoder_texture_format::cTFETC1_RGB: ti.m_bf = block_format::cETC1; ti.m_out_bytes = 8; break; case transcoder_texture_format::cTFETC2_RGBA: ti.m_bf = block_format::cETC2_RGBA; ti.m_out_bytes = 16; break; case transcoder_texture_format::cTFETC2_EAC_R11: ti.m_bf = block_format::cETC2_EAC_R11; ti.m_out_bytes = 8; ti.m_def_channel0 = 0; break; case transcoder_texture_format::cTFETC2_EAC_RG11: ti.m_bf = block_format::cETC2_EAC_RG11; ti.m_out_bytes = 16; ti.m_def_channel0 = 0; ti.m_def_channel1 = 1; break; case transcoder_texture_format::cTFBC1_RGB: ti.m_bf = block_format::cBC1; ti.m_out_bytes = 8; break; case transcoder_texture_format::cTFBC3_RGBA: ti.m_bf = block_format::cBC3; ti.m_out_bytes = 16; break; case transcoder_texture_format::cTFBC4_R: ti.m_bf = block_format::cBC4; ti.m_out_bytes = 8; ti.m_def_channel0 = 0; break; case transcoder_texture_format::cTFBC5_RG: ti.m_bf = block_format::cBC5; ti.m_out_bytes = 16; ti.m_def_channel0 = 0; ti.m_def_channel1 = 1; break; case transcoder_texture_format::cTFBC7_RGBA: ti.m_bf = block_format::cBC7; ti.m_out_bytes = 16; break; case transcoder_texture_format::cTFASTC_4x4_RGBA: ti.m_bf = block_format::cASTC_LDR_4x4; ti.m_out_bytes = 16; ti.m_mode = repack_mode::cASTC4x4; break; case transcoder_texture_format::cTFPVRTC1_4_RGB: ti.m_bf = block_format::cPVRTC1_4_RGB; ti.m_out_bytes = 8; ti.m_mode = repack_mode::cPVRTC1; break; case transcoder_texture_format::cTFPVRTC1_4_RGBA: ti.m_bf = block_format::cPVRTC1_4_RGBA; ti.m_out_bytes = 8; ti.m_mode = repack_mode::cPVRTC1; break; case transcoder_texture_format::cTFRGBA32: ti.m_bf = block_format::cRGBA32; ti.m_out_bytes = 4; ti.m_uncompressed = true; break; case transcoder_texture_format::cTFRGB565: ti.m_bf = block_format::cRGB565; ti.m_out_bytes = 2; ti.m_uncompressed = true; break; case transcoder_texture_format::cTFRGBA4444: ti.m_bf = block_format::cRGBA4444; ti.m_out_bytes = 2; ti.m_uncompressed = true; break; default: return false; } return true; } } // anonymous namespace dds_transcoder::dds_transcoder() { clear(); } void dds_transcoder::clear() { m_pData = nullptr; m_data_size = 0; m_init_succeeded = false; m_width = m_height = 0; m_levels = 0; m_layers = 0; m_faces = 1; m_has_alpha = false; m_is_srgb = false; m_format = transcoder_texture_format::cTFRGBA32; m_block_width = m_block_height = 1; m_bytes_per_block_or_pixel = 0; m_src_kind = source_kind::cInvalid; m_dds_format = dds_format::cInvalid; m_rgb_bit_count = 0; for (uint32_t channel_index = 0; channel_index < 4; channel_index++) m_uncomp_channels[channel_index] = dds_uncompressed_channel{ 0, 0, 0, -2 }; // 0 mask => absent => byte_offset -2 m_uncompressed_is_canonical_rgba8 = false; m_uncompressed_byte_aligned = false; for (uint32_t c = 0; c < 4; c++) { m_swizzle[c] = 0; m_and_mask[c] = 0; m_or_mask[c] = 0; } m_slices.clear(); } bool dds_transcoder::init(const void* pData, uint32_t data_size) { clear(); if (!pData) return false; const uint8_t* p = static_cast(pData); // Need magic (4) + DDS_HEADER (124) at minimum. if (data_size < 4 + DDS_HEADER_SIZE) return false; if (rd_u32(p) != DDS_MAGIC) return false; const uint8_t* h = p + 4; // DDS_HEADER const uint32_t dwSize = rd_u32(h + 0); if (dwSize != DDS_HEADER_SIZE) return false; const uint32_t dw_flags = rd_u32(h + 4); const uint32_t height = rd_u32(h + 8); const uint32_t width = rd_u32(h + 12); const uint32_t pitch_or_linear_size = rd_u32(h + 16); const uint32_t depth = rd_u32(h + 20); const uint32_t mip_count_field = rd_u32(h + 24); // DDS_PIXELFORMAT at offset 72 const uint8_t* pf = h + 72; const uint32_t pf_size = rd_u32(pf + 0); if (pf_size != DDS_PIXELFORMAT_SIZE) return false; const uint32_t pf_flags = rd_u32(pf + 4); const uint32_t pf_fourcc = rd_u32(pf + 8); const uint32_t pf_rgb_bit_count = rd_u32(pf + 12); const uint32_t pf_r_mask = rd_u32(pf + 16); const uint32_t pf_g_mask = rd_u32(pf + 20); const uint32_t pf_b_mask = rd_u32(pf + 24); const uint32_t pf_a_mask = rd_u32(pf + 28); const uint32_t caps2 = rd_u32(h + 108); // Basic dimension sanity (parser hardening). const uint32_t MAX_DIM = 32768; if ((width == 0) || (height == 0) || (width > MAX_DIM) || (height > MAX_DIM)) return false; // Reject volume textures. if ((caps2 & DDSCAPS2_VOLUME) || (depth > 1)) return false; // Is this a DX10-extended file? const bool is_dx10 = (pf_flags & DDPF_FOURCC) && (pf_fourcc == make_fourcc('D', 'X', '1', '0')); uint32_t data_start = 4 + DDS_HEADER_SIZE; uint32_t faces = 1; uint32_t eff_layers = 1; // physical array element count (>=1) bool reported_array = false; // Format detection outputs. source_kind kind = source_kind::cInvalid; transcoder_texture_format contained = transcoder_texture_format::cTFRGBA32; // --- sRGB flag policy (m_is_srgb), best-effort: DDS only signals sRGB on DX10/DXGI (the _UNORM vs // _UNORM_SRGB variants); DX9 carries no signal. --- // * is_srgb = false (known linear): DX10 formats stored as the _UNORM variant (we honor the explicit // signal, set below via "is_srgb = (dxgi_format == ..._SRGB)"), and BC4/BC5 (single/two-channel data // -- no sRGB concept, forced linear in their cases below). // * is_srgb = true (the default here): everything else -- all DX9 sources, and DX10 16-bit color with no // _SRGB variant. sRGB is the safer guess for the common albedo case. // So the same BC format can report different is_srgb() per container (DX10 BC1_UNORM=false vs DX9 DXT1=true) // -- by design (DX10 can signal, DX9 can't). Caveat: real DX10 files often mis-tag sRGB content as _UNORM, // so the signal isn't fully reliable; consumers that care expose a manual override. is_srgb() never changes // decoded pixel values -- it's a reported hint only (callers may propagate it into output file metadata). bool has_alpha = false, is_srgb = true; uint32_t bytes_per = 0, blk_w = 4, blk_h = 4; uint32_t rgb_bits = 0, m_r = 0, m_g = 0, m_b = 0, m_a = 0; bool canonical_rgba8 = false; // Byte-swizzle decode tables (built below for byte-aligned layouts incl. R8/R8G8/A8/L8/A8L8). dds_fmt_override // names the exact format for the special byte formats since classify_dds_format() is mask-based and can't ID them. bool is_byte_swizzle = false; uint8_t swiz[4] = { 0, 0, 0, 0 }, andm[4] = { 0, 0, 0, 0 }, orm[4] = { 0, 0, 0, 0 }; dds_format dds_fmt_override = dds_format::cInvalid; auto setup_uncompressed = [&](uint32_t bits, uint32_t rmask, uint32_t gmask, uint32_t bmask, uint32_t amask) { kind = source_kind::cUncompressed; rgb_bits = bits; m_r = rmask; m_g = gmask; m_b = bmask; m_a = amask; bytes_per = bits / 8; blk_w = 1; blk_h = 1; has_alpha = (amask != 0); // Choose a contained transcoder format that's the closest size/shape match. if (bits >= 24) { // 24-bit (R8G8B8 / B8G8R8) and 32-bit both decode to RGBA8. Only true memory-order R8G8B8A8 // (32-bit) qualifies for a raw-memcpy passthrough; 24-bit always decode->repacks. contained = transcoder_texture_format::cTFRGBA32; canonical_rgba8 = (bits == 32) && (rmask == 0x000000FFu) && (gmask == 0x0000FF00u) && (bmask == 0x00FF0000u) && (amask == 0xFF000000u); } else { // 16-bit: 6-bit green => 565, else 4444 bucket (covers 1555/4444). Never a raw-memcpy passthrough. contained = (pop_count(gmask) == 6) ? transcoder_texture_format::cTFRGB565 : transcoder_texture_format::cTFRGBA4444; canonical_rgba8 = false; } }; // set_byte_swizzle(): builds the byte-remap tables (swiz/andm/orm) from 4 channel source codes. Used by // setup_byte_aligned_format() (R8/R8G8/A8/L8/A8L8) and the channel-mask byte-aligned path further down. // Codes: >=0 = source byte index; cForce255 = opaque (also the m_byte_offset "absent alpha" -2 sentinel); cForce0 = zero. const int cForce255 = -2, cForce0 = -3; auto set_byte_swizzle = [&](int sr, int sg, int sb, int sa) { const int codes[4] = { sr, sg, sb, sa }; for (uint32_t c = 0; c < 4; c++) { if (codes[c] >= 0) { swiz[c] = (uint8_t)codes[c]; andm[c] = 0xFF; orm[c] = 0x00; } else if (codes[c] == cForce255) { swiz[c] = 0; andm[c] = 0x00; orm[c] = 0xFF; } else { swiz[c] = 0; andm[c] = 0x00; orm[c] = 0x00; } // cForce0 / absent color } is_byte_swizzle = true; }; // Byte-oriented single/dual-channel + luminance/alpha (R8/R8G8/A8/L8/A8L8): no channel masks; decode is the // byte remap set by set_byte_swizzle. All decode to RGBA8 so the contained transcoder format is cTFRGBA32. auto setup_byte_aligned_format = [&](uint32_t bytes, bool alpha, bool srgb, dds_format dfmt, int sr, int sg, int sb, int sa) { kind = source_kind::cUncompressed; rgb_bits = bytes * 8; bytes_per = bytes; blk_w = 1; blk_h = 1; has_alpha = alpha; is_srgb = srgb; contained = transcoder_texture_format::cTFRGBA32; canonical_rgba8 = false; dds_fmt_override = dfmt; set_byte_swizzle(sr, sg, sb, sa); }; if (is_dx10) { if (data_size < data_start + DDS_DX10_HEADER_SIZE) return false; const uint8_t* dx10 = p + data_start; const uint32_t dxgi_format = rd_u32(dx10 + 0); const uint32_t resource_dim = rd_u32(dx10 + 4); const uint32_t misc_flag = rd_u32(dx10 + 8); const uint32_t array_size = rd_u32(dx10 + 12); data_start += DDS_DX10_HEADER_SIZE; if (resource_dim == DDS_DIMENSION_TEXTURE1D) return false; if (resource_dim == DDS_DIMENSION_TEXTURE3D) return false; if (resource_dim != DDS_DIMENSION_TEXTURE2D) return false; // Note: the DX10 spec says arraySize must be >= 1, but tinydds (and the basisu DDS writer that // uses it) store 0 for a plain non-array texture. Treat 0 as 1 for robustness. uint32_t array_size_eff = array_size ? array_size : 1; if (array_size_eff > 65536) return false; const bool is_cube = (misc_flag & DDS_RESOURCE_MISC_TEXTURECUBE) != 0; faces = is_cube ? 6 : 1; eff_layers = array_size_eff; // DX10: array_size counts cubes (for cubemaps) or 2D slices reported_array = (array_size_eff > 1); switch (dxgi_format) { case DXGI_BC1_UNORM: case DXGI_BC1_UNORM_SRGB: // has_alpha = true: BC1 may carry per-block 1-bit punchthrough alpha (we decode it; DXGI BC1_UNORM is // a 4-component format). Reported conservatively without scanning blocks; opaque BC1 decodes alpha=255. kind = source_kind::cBC1; contained = transcoder_texture_format::cTFBC1_RGB; bytes_per = 8; has_alpha = true; is_srgb = (dxgi_format == DXGI_BC1_UNORM_SRGB); break; case DXGI_BC2_UNORM: case DXGI_BC2_UNORM_SRGB: // BC2 (explicit 4-bit alpha + BC1 color) is decode-only: no transcoder_texture_format exists for it, so // `contained` reports cTFBC3_RGBA as a closest-match hint (compatibility kludge). Passthrough is disabled // for cBC2 below (BC2 != BC3 bytewise) so it always decode->repacks; get_dds_format() reports the exact cBC2. kind = source_kind::cBC2; contained = transcoder_texture_format::cTFBC3_RGBA; bytes_per = 16; has_alpha = true; is_srgb = (dxgi_format == DXGI_BC2_UNORM_SRGB); break; case DXGI_BC3_UNORM: case DXGI_BC3_UNORM_SRGB: kind = source_kind::cBC3; contained = transcoder_texture_format::cTFBC3_RGBA; bytes_per = 16; has_alpha = true; is_srgb = (dxgi_format == DXGI_BC3_UNORM_SRGB); break; case DXGI_BC4_UNORM: kind = source_kind::cBC4; contained = transcoder_texture_format::cTFBC4_R; bytes_per = 8; has_alpha = false; is_srgb = false; break; // single-channel data: always linear (override the sRGB default) case DXGI_BC5_UNORM: kind = source_kind::cBC5; contained = transcoder_texture_format::cTFBC5_RG; bytes_per = 16; has_alpha = false; is_srgb = false; break; // two-channel data: always linear (override the sRGB default) case DXGI_BC7_UNORM: case DXGI_BC7_UNORM_SRGB: kind = source_kind::cBC7; contained = transcoder_texture_format::cTFBC7_RGBA; bytes_per = 16; has_alpha = true; is_srgb = (dxgi_format == DXGI_BC7_UNORM_SRGB); break; case DXGI_R8G8B8A8_UNORM: case DXGI_R8G8B8A8_UNORM_SRGB: setup_uncompressed(32, 0x000000FFu, 0x0000FF00u, 0x00FF0000u, 0xFF000000u); is_srgb = (dxgi_format == DXGI_R8G8B8A8_UNORM_SRGB); break; case DXGI_B8G8R8A8_UNORM: case DXGI_B8G8R8A8_UNORM_SRGB: setup_uncompressed(32, 0x00FF0000u, 0x0000FF00u, 0x000000FFu, 0xFF000000u); is_srgb = (dxgi_format == DXGI_B8G8R8A8_UNORM_SRGB); break; case DXGI_B8G8R8X8_UNORM: case DXGI_B8G8R8X8_UNORM_SRGB: setup_uncompressed(32, 0x00FF0000u, 0x0000FF00u, 0x000000FFu, 0x00000000u); is_srgb = (dxgi_format == DXGI_B8G8R8X8_UNORM_SRGB); break; case DXGI_B5G6R5_UNORM: // DXGI names list channels LSB-first, so B5G6R5 has B in the low bits and R in the high bits: // R=0xF800, G=0x07E0, B=0x001F. (This converges with D3D9 R5G6B5, which lists MSB-first, and // matches tinydds's encode table -- so it round-trips correctly.) setup_uncompressed(16, 0xF800u, 0x07E0u, 0x001Fu, 0x0000u); break; case DXGI_B5G5R5A1_UNORM: setup_uncompressed(16, 0x7C00u, 0x03E0u, 0x001Fu, 0x8000u); break; case DXGI_B4G4R4A4_UNORM: setup_uncompressed(16, 0x0F00u, 0x00F0u, 0x000Fu, 0xF000u); break; case DXGI_R8_UNORM: setup_byte_aligned_format(1, false, false, dds_format::cR8, 0, cForce0, cForce0, cForce255); break; // (R,0,0,255), linear (like BC4) case DXGI_R8G8_UNORM: setup_byte_aligned_format(2, false, false, dds_format::cR8G8, 0, 1, cForce0, cForce255); break; // (R,G,0,255), linear (like BC5) case DXGI_A8_UNORM: setup_byte_aligned_format(1, true, false, dds_format::cA8, cForce0, cForce0, cForce0, 0); break; // (0,0,0,A) default: // BC4_SNORM/BC5_SNORM/BC6H/float/etc. - unsupported. return false; } } else { // DX9 path. if (caps2 & DDSCAPS2_CUBEMAP) { // Require all six faces; reject partial cubemaps. if ((caps2 & DDSCAPS2_CUBEMAP_ALLFACES) != DDSCAPS2_CUBEMAP_ALLFACES) return false; faces = 6; } eff_layers = 1; // DX9 has no texture arrays reported_array = false; if (pf_flags & DDPF_FOURCC) { if (pf_fourcc == make_fourcc('D', 'X', 'T', '1')) { // has_alpha = true: DXT1/BC1 may carry per-block punchthrough alpha (see DX10 BC1 above). kind = source_kind::cBC1; contained = transcoder_texture_format::cTFBC1_RGB; bytes_per = 8; has_alpha = true; } else if ((pf_fourcc == make_fourcc('D', 'X', 'T', '5')) || (pf_fourcc == make_fourcc('D', 'X', 'T', '4'))) { // DXT4 (premultiplied) and DXT5 share the BC3 byte layout (BC4 alpha + BC1 color); we decode the bytes // identically (premultiplication is a consumer semantic), mirroring DXT2/DXT3 -> BC2 above. kind = source_kind::cBC3; contained = transcoder_texture_format::cTFBC3_RGBA; bytes_per = 16; has_alpha = true; } else if ((pf_fourcc == make_fourcc('D', 'X', 'T', '3')) || (pf_fourcc == make_fourcc('D', 'X', 'T', '2'))) { // DXT2 (premultiplied) and DXT3 share the BC2 byte layout (explicit 4-bit alpha + BC1 color). Decode-only; // `contained` reports cTFBC3 as a hint (see DX10 BC2). is_srgb stays at the DX9 default (true, no signal). kind = source_kind::cBC2; contained = transcoder_texture_format::cTFBC3_RGBA; bytes_per = 16; has_alpha = true; } else if ((pf_fourcc == make_fourcc('A', 'T', 'I', '1')) || (pf_fourcc == make_fourcc('B', 'C', '4', 'U'))) { kind = source_kind::cBC4; contained = transcoder_texture_format::cTFBC4_R; bytes_per = 8; has_alpha = false; is_srgb = false; // single-channel data: always linear (override the sRGB default) } else if ((pf_fourcc == make_fourcc('A', 'T', 'I', '2')) || (pf_fourcc == make_fourcc('B', 'C', '5', 'U'))) { kind = source_kind::cBC5; contained = transcoder_texture_format::cTFBC5_RG; bytes_per = 16; has_alpha = false; is_srgb = false; // two-channel data: always linear (override the sRGB default) } else { // A numeric D3DFMT code (float/16-bit-per-channel/etc.) - unsupported. return false; } } else if (pf_flags & DDPF_RGB) { // Accept only known-good mask layouts: 16-bit (565/1555/4444), 24-bit (R8G8B8 / B8G8R8), // 32-bit (A8R8G8B8 / X8R8G8B8 / A8B8G8R8 / X8B8G8R8). Reject anything else outright. const uint32_t eff_a = (pf_flags & DDPF_ALPHAPIXELS) ? pf_a_mask : 0; if (!is_accepted_rgb_layout(pf_rgb_bit_count, pf_r_mask, pf_g_mask, pf_b_mask, eff_a)) return false; setup_uncompressed(pf_rgb_bit_count, pf_r_mask, pf_g_mask, pf_b_mask, eff_a); } else if (pf_flags & DDPF_LUMINANCE) { // D3DFMT_L8 (8bpp, L=0xFF) -> (L,L,L,255); D3DFMT_A8L8 (16bpp, L=0xFF, A=0xFF00) -> (L,L,L,A). const uint32_t eff_a = (pf_flags & DDPF_ALPHAPIXELS) ? pf_a_mask : 0; if ((pf_rgb_bit_count == 8) && (pf_r_mask == 0x000000FFu) && (eff_a == 0)) setup_byte_aligned_format(1, false, true, dds_format::cL8, 0, 0, 0, cForce255); // L8 -> (L,L,L,255) else if ((pf_rgb_bit_count == 16) && (pf_r_mask == 0x000000FFu) && (eff_a == 0x0000FF00u)) setup_byte_aligned_format(2, true, true, dds_format::cA8L8, 0, 0, 0, 1); // A8L8 -> (L,L,L,A) else return false; } else if (pf_flags & DDPF_ALPHA) { // D3DFMT_A8 (8bpp, alpha only) -> (0,0,0,A). if ((pf_rgb_bit_count == 8) && (pf_a_mask == 0x000000FFu)) setup_byte_aligned_format(1, true, false, dds_format::cA8, cForce0, cForce0, cForce0, 0); // A8 -> (0,0,0,A) else return false; } else { // DDPF_YUV / unknown - unsupported. return false; } } if (kind == source_kind::cInvalid) return false; const bool compressed = (kind != source_kind::cUncompressed); blk_w = compressed ? 4 : 1; blk_h = compressed ? 4 : 1; // Uncompressed row pitch: default tightly packed (matches DirectXTex's loader and the basisu writer). // If the header declares DDSD_PITCH with a DWORD-aligned pitch (legacy D3DX style), honor that row // padding for every level. Any other declared pitch is treated as unreliable and ignored (tight used). bool dword_align_rows = false; if (!compressed && (dw_flags & DDSD_PITCH)) { const uint32_t tight0 = width * bytes_per; const uint32_t aligned0 = (tight0 + 3u) & ~3u; if ((pitch_or_linear_size == aligned0) && (aligned0 != tight0)) dword_align_rows = true; } // Mip count. 0 means 1. Validate against the maximum possible chain (parser hardening). uint32_t max_dim = (width > height) ? width : height; uint32_t max_mips = 1; while (max_dim > 1) { max_dim >>= 1; max_mips++; } uint32_t levels = (mip_count_field == 0) ? 1 : mip_count_field; if (levels > max_mips) return false; // ---- Compute and validate every (layer, face, level) slice offset/size ---- // Disk order: layer major, then face, then mip (matches the basisu DDS writer). Overflow/anti-DoS guard: // count images in 64-bit and reject up front if the file is too small for even one bytes_per per slice // (stops a tiny hostile header, e.g. arraySize=65536 faces=6, from forcing a huge allocation). const uint64_t total64 = (uint64_t)eff_layers * faces * levels; if (total64 > 0xFFFFFFFFu) return false; // defensive: keep the slice count in uint32 range regardless of bytes_per if (((uint64_t)data_start + total64 * bytes_per) > (uint64_t)data_size) return false; const uint32_t total = (uint32_t)total64; // safe: bounded by the checks above (data_size <= UINT32_MAX) basisu::vector slices; if (!slices.try_resize(total)) return false; uint64_t ofs = data_start; for (uint32_t layer = 0; layer < eff_layers; layer++) { for (uint32_t face = 0; face < faces; face++) { for (uint32_t level = 0; level < levels; level++) { const uint32_t mw = (width >> level) ? (width >> level) : 1; const uint32_t mh = (height >> level) ? (height >> level) : 1; const uint32_t nbx = (mw + blk_w - 1) / blk_w; const uint32_t nby = (mh + blk_h - 1) / blk_h; uint64_t size; uint32_t row_pitch = 0; if (compressed) // DDS stores exact texel dims, which for BC may be non-multiples of 4 (a 3x3 BC7 = 3x3 texels, // 1 block). Storage is always whole 4x4 blocks: block count = ceil(dim/4) (nbx/nby above). A // transcode to an uncompressed target clips back to the exact dims (see transcode_image_level). size = (uint64_t)nbx * nby * bytes_per; else { row_pitch = dword_align_rows ? (((mw * bytes_per) + 3u) & ~3u) : (mw * bytes_per); size = (uint64_t)row_pitch * mh; // honors DWORD row padding when the file declares it } // Every slice (incl. tiny tail mips, which still occupy a full 4x4 block) must be fully present. // This rejects truncated files AND ancient "bad-tail" DDS that stored sub-4x4 tails with less than // a full block (texconv's --bad-tails case): our full-block size won't fit, so we bail rather than // mis-read the tail. No --bad-tails tolerance. if ((ofs + size) > (uint64_t)data_size) return false; const uint32_t idx = (layer * faces + face) * levels + level; slice_desc& sd = slices[idx]; sd.m_ofs = ofs; sd.m_size = (uint32_t)size; sd.m_width = mw; sd.m_height = mh; sd.m_num_blocks_x = (mw + 3) / 4; // 4x4 grid used by the transcode loop, regardless of source kind sd.m_num_blocks_y = (mh + 3) / 4; sd.m_row_pitch = row_pitch; // bytes per row for uncompressed; 0 for compressed (unused) ofs += size; } } } // Commit. m_pData = p; m_data_size = data_size; m_width = width; m_height = height; m_levels = levels; m_layers = reported_array ? eff_layers : 0; // ktx2 convention: 0 == not an array m_faces = faces; m_has_alpha = has_alpha; m_is_srgb = is_srgb; m_format = contained; m_block_width = blk_w; m_block_height = blk_h; m_bytes_per_block_or_pixel = bytes_per; m_src_kind = kind; // Byte-oriented special formats (R8/R8G8/A8/L8/A8L8) name themselves via dds_fmt_override; classify_dds_format is mask-based. m_dds_format = (dds_fmt_override != dds_format::cInvalid) ? dds_fmt_override : classify_dds_format(kind, rgb_bits, m_r, m_g, m_b, m_a); m_rgb_bit_count = rgb_bits; // Precompute each channel's mask/shift/bit-width/byte-offset once (image-constant), so the per-pixel decode // doesn't recompute them. Order R,G,B,A. Compressed sources have 0 masks (never read; kind != cUncompressed). const uint32_t channel_masks[4] = { m_r, m_g, m_b, m_a }; for (uint32_t channel_index = 0; channel_index < 4; channel_index++) { const uint32_t mask = channel_masks[channel_index]; dds_uncompressed_channel& chan = m_uncomp_channels[channel_index]; chan.m_mask = mask; chan.m_shift = trail_zeros(mask); chan.m_channel_bits = pop_count(mask); // contiguous (whitelist-enforced); 0 => absent chan.m_byte_offset = mask ? chan_byte_offset(mask) : -2; // >=0 byte index; -1 not byte-aligned; -2 absent (opaque) } m_uncompressed_is_canonical_rgba8 = canonical_rgba8; // Build the byte-swizzle tables for channel-mask layouts (the R8/R8G8/A8/L8/A8L8 specials already set them). // A layout is byte-aligned iff 24/32-bit with every RGB channel a clean byte in range (alpha a byte or absent); // if so, derive the swizzle from the per-channel byte offsets. Sub-byte (565/1555/4444) -> generic mask path. if (!is_byte_swizzle) { const int bytes_per_pixel = (int)(rgb_bits / 8); bool aligned = (bytes_per_pixel == 3) || (bytes_per_pixel == 4); for (uint32_t c = 0; c < 3; c++) // R,G,B must each be a real byte in the pixel aligned = aligned && (m_uncomp_channels[c].m_byte_offset >= 0) && (m_uncomp_channels[c].m_byte_offset < bytes_per_pixel); const int a_offset = m_uncomp_channels[3].m_byte_offset; // >=0 byte, or -2 (== cForce255) when alpha absent aligned = aligned && ((a_offset == cForce255) || ((a_offset >= 0) && (a_offset < bytes_per_pixel))); if (aligned) set_byte_swizzle(m_uncomp_channels[0].m_byte_offset, m_uncomp_channels[1].m_byte_offset, m_uncomp_channels[2].m_byte_offset, a_offset); } m_uncompressed_byte_aligned = is_byte_swizzle; for (uint32_t c = 0; c < 4; c++) { m_swizzle[c] = swiz[c]; m_and_mask[c] = andm[c]; m_or_mask[c] = orm[c]; } m_slices.swap(slices); m_init_succeeded = true; return true; } bool dds_transcoder::start_transcoding() { return m_init_succeeded; } bool dds_transcoder::get_image_level_info(ktx2_image_level_info& level_info, uint32_t level_index, uint32_t layer_index, uint32_t face_index) const { if (!m_init_succeeded) return false; const uint32_t eff_layers = m_layers ? m_layers : 1; if ((level_index >= m_levels) || (layer_index >= eff_layers) || (face_index >= m_faces)) return false; const slice_desc& sd = m_slices[slice_index(level_index, layer_index, face_index)]; // Describe the CONTAINED format: BC* are 4x4 blocks; uncompressed is 1x1 (raster). const uint32_t bw = m_block_width, bh = m_block_height; const uint32_t nbx = (sd.m_width + bw - 1) / bw; const uint32_t nby = (sd.m_height + bh - 1) / bh; level_info.m_level_index = level_index; level_info.m_layer_index = layer_index; level_info.m_face_index = face_index; level_info.m_orig_width = sd.m_width; level_info.m_orig_height = sd.m_height; level_info.m_width = nbx * bw; level_info.m_height = nby * bh; level_info.m_num_blocks_x = nbx; level_info.m_num_blocks_y = nby; level_info.m_block_width = bw; level_info.m_block_height = bh; level_info.m_total_blocks = nbx * nby; level_info.m_alpha_flag = m_has_alpha; level_info.m_iframe_flag = false; return true; } // Advanced: expose a slice's stored byte range + geometry so callers can handle the raw data themselves. bool dds_transcoder::get_slice_desc(slice_desc& out, uint32_t level_index, uint32_t layer_index, uint32_t face_index) const { if (!m_init_succeeded) return false; const uint32_t eff_layers = m_layers ? m_layers : 1; if ((level_index >= m_levels) || (layer_index >= eff_layers) || (face_index >= m_faces)) return false; out = m_slices[slice_index(level_index, layer_index, face_index)]; return true; } bool dds_transcoder::is_transcode_format_supported(transcoder_texture_format fmt) const { // Takes the same transcoder_texture_format the transcoder uses. BC1-7, ETC1/ETC2/EAC, ASTC LDR 4x4, // and the uncompressed formats (RGBA32/565/4444) are always supported; PVRTC1 is supported only when // this texture's dimensions are powers of 2. Anything not in our target set is unsupported. target_info ti; if (!get_target_info(fmt, ti)) return false; if (ti.m_mode == repack_mode::cPVRTC1) return basisu::is_pow2(m_width) && basisu::is_pow2(m_height); return true; } #if BASISD_SUPPORT_XUASTC // Decodes source 4x4 block (bx,by) of a slice into 16 RGBA texels. A member (not a free helper) so the per-block // call site stays short. Only compiled when BASISD_SUPPORT_XUASTC is enabled (BC7 decode needs bc7u::unpack_bc7). void dds_transcoder::decode_source_block( const uint8_t* pSrc, uint32_t slice_w, uint32_t slice_h, uint32_t nbx, uint32_t row_pitch, uint32_t bx, uint32_t by, color32* pTexels) const { // The canonical-RGBA8 fast path memcpy's color32 rows straight from the source -- assumes color32 is a // packed 4-byte RGBA (this guards a future layout change). static_assert(sizeof(color32) == 4, "decode_source_block canonical memcpy assumes color32 is a packed 4-byte RGBA"); const uint32_t bytes_per_texel = m_bytes_per_block_or_pixel; if (m_src_kind == source_kind::cUncompressed) { // ---- Byte-aligned uncompressed: a pure byte remap to color32. One swizzle loop covers RGBA/BGRA/RGBX, // 24-bit RGB/BGR, and R8/R8G8/A8/L8/A8L8: out = (src[swizzle] & and) | or (built in init()). Canonical // R8G8B8A8 gets a specialized per-row memcpy. Sub-byte (565/1555/4444) falls to the mask path below. ---- if (m_uncompressed_byte_aligned) { const bool x_interior = ((bx * 4 + 4) <= slice_w); if (m_uncompressed_is_canonical_rgba8 && x_interior) { // Specialized hot path: canonical R8G8B8A8, fully-interior block -> one 16-byte memcpy per row // (compiles to a vector move). Bottom-edge blocks of a non-multiple-of-4 mip still clamp sy per row. for (uint32_t ty = 0; ty < 4; ty++) { uint32_t sy = by * 4 + ty; if (sy >= slice_h) sy = slice_h - 1; memcpy(&pTexels[ty * 4], pSrc + (uint64_t)sy * row_pitch + (uint64_t)bx * 4 * bytes_per_texel, 16); } return; } // General byte remap. The swizzle indices are in [0, bytes_per_texel) and forced channels read byte 0 // (then get masked away), so every pp[] access is in range regardless of layout. const uint8_t s0 = m_swizzle[0], s1 = m_swizzle[1], s2 = m_swizzle[2], s3 = m_swizzle[3]; const uint8_t a0 = m_and_mask[0], a1 = m_and_mask[1], a2 = m_and_mask[2], a3 = m_and_mask[3]; const uint8_t o0 = m_or_mask[0], o1 = m_or_mask[1], o2 = m_or_mask[2], o3 = m_or_mask[3]; for (uint32_t ty = 0; ty < 4; ty++) { uint32_t sy = by * 4 + ty; if (sy >= slice_h) sy = slice_h - 1; // clamp at bottom edge (mips not multiple of 4) const uint8_t* pRow = pSrc + (uint64_t)sy * row_pitch; for (uint32_t tx = 0; tx < 4; tx++) { uint32_t sx = bx * 4 + tx; if (sx >= slice_w) sx = slice_w - 1; // clamp at right edge (mips not multiple of 4) const uint8_t* pp = pRow + (uint64_t)sx * bytes_per_texel; color32& c = pTexels[ty * 4 + tx]; c.r = (uint8_t)((pp[s0] & a0) | o0); c.g = (uint8_t)((pp[s1] & a1) | o1); c.b = (uint8_t)((pp[s2] & a2) | o2); c.a = (uint8_t)((pp[s3] & a3) | o3); } } return; } // ---- Generic path (slow): any non-byte-aligned uncompressed layout (16-bit 565/1555/4444, or any odd mask). // Expands sub-8-bit channels to 8 bits via bit replication inside decode_channel. ---- for (uint32_t ty = 0; ty < 4; ty++) { uint32_t sy = by * 4 + ty; if (sy >= slice_h) sy = slice_h - 1; // clamp at edges (mips not multiple of 4) for (uint32_t tx = 0; tx < 4; tx++) { uint32_t sx = bx * 4 + tx; if (sx >= slice_w) sx = slice_w - 1; // Address via the row pitch (honors DWORD-padded rows), not width*bpp. const uint8_t* pp = pSrc + (uint64_t)sy * row_pitch + (uint64_t)sx * bytes_per_texel; const uint32_t raw = (bytes_per_texel == 4) ? rd_u32(pp) : ((bytes_per_texel == 3) ? rd_u24(pp) : rd_u16(pp)); color32& c = pTexels[ty * 4 + tx]; c.r = (uint8_t)decode_channel(raw, m_uncomp_channels[0]); c.g = (uint8_t)decode_channel(raw, m_uncomp_channels[1]); c.b = (uint8_t)decode_channel(raw, m_uncomp_channels[2]); c.a = (uint8_t)decode_channel(raw, m_uncomp_channels[3]); } } return; } // Compressed source: one physical block. const uint8_t* pBlock = pSrc + ((uint64_t)by * nbx + bx) * bytes_per_texel; switch (m_src_kind) { case dds_transcoder::source_kind::cBC1: { bcu::unpack_bc1(pBlock, reinterpret_cast(pTexels), true); break; } case dds_transcoder::source_kind::cBC2: { bcu::unpack_bc2(pBlock, reinterpret_cast(pTexels)); break; } case dds_transcoder::source_kind::cBC3: { bcu::unpack_bc3(pBlock, reinterpret_cast(pTexels)); break; } case dds_transcoder::source_kind::cBC4: { // BC4 decoder writes only one channel, so pre-fill RGBA with opaque black first. color32 black_color(0, 0, 0, 255); for (uint32_t i = 0; i < 16; i++) pTexels[i] = black_color; bcu::unpack_bc4(pBlock, (uint8_t *)pTexels, sizeof(color32)); break; } case dds_transcoder::source_kind::cBC5: { // BC5 decoder writes only R and G, so pre-fill the other channels first. color32 black_color(0, 0, 0, 255); for (uint32_t i = 0; i < 16; i++) pTexels[i] = black_color; bcu::unpack_bc5(pBlock, reinterpret_cast(pTexels)); break; } case dds_transcoder::source_kind::cBC7: { bc7u::unpack_bc7(pBlock, reinterpret_cast(pTexels)); break; } default: assert(0); break; } } #endif // BASISD_SUPPORT_XUASTC // ============================================================================================================ // transcode_image_level: output_row_pitch_in_blocks_or_pixels / output_rows_in_pixels CONTRACT // // DDS has no "original" (pre-block-padding) dimensions, so the caller supplies them via output pitch/rows as a // proxy. Both default to 0 == the tight image size. By destination kind: // // UNCOMPRESSED (cTFRGBA32/RGB565/RGBA4444, incl. BGRA/X8/16-bpp/byte swizzles): pitch = dest stride in PIXELS, // output_rows = rows. The dest may be SMALLER than the image (we clip the blit to the top-left overlap, e.g. // a 4x4 DDS -> 3x3 buffer) or LARGER (expanded pitch/rows). We decode ALL source blocks; boundary blocks // partial-write, fully-outside blocks are skipped; row writes never exceed the image height. Canonical RGBA8 // -> cTFRGBA32 is a passthrough copy. // // COMPRESSED (BC/ETC/EAC/ASTC4x4/PVRTC1): pitch = dest stride in BLOCKS (output_rows unused). Pitch may EXPAND // beyond num_blocks_x but can't shrink (every block is emitted). PVRTC1 is whole-image (tight pitch only). A // same-format passthrough (e.g. BC7->BC7) copies stored blocks verbatim, NEVER decoding to texels. // // In all cases a non-zero pitch/rows is obeyed and the output buffer is bounds-checked before any store. // ============================================================================================================ bool dds_transcoder::transcode_image_level( uint32_t level_index, uint32_t layer_index, uint32_t face_index, void* pOutput_blocks, uint32_t output_blocks_buf_size_in_blocks_or_pixels, transcoder_texture_format fmt, uint32_t decode_flags, uint32_t output_row_pitch_in_blocks_or_pixels, uint32_t output_rows_in_pixels, int channel0, int channel1) { if (!m_init_succeeded || !pOutput_blocks) return false; const uint32_t eff_layers = m_layers ? m_layers : 1; if ((level_index >= m_levels) || (layer_index >= eff_layers) || (face_index >= m_faces)) return false; target_info ti; if (!get_target_info(fmt, ti)) return false; const slice_desc& sd = m_slices[slice_index(level_index, layer_index, face_index)]; const uint8_t* pSrc = m_pData + sd.m_ofs; const uint32_t width = sd.m_width, height = sd.m_height; const uint32_t num_blocks_x = sd.m_num_blocks_x, num_blocks_y = sd.m_num_blocks_y; const bool src_compressed = (m_src_kind != source_kind::cUncompressed); // ---- Passthrough (memcpy) when the requested format is exactly what's stored and the byte // layout matches a transcoder output format. BGRA / X8 / 16-bpp never qualify (need swizzle/repack). ---- bool passthrough = false; if (fmt == m_format) { // cBC2 reports cTFBC3 as a compatibility hint but is NOT byte-compatible with BC3 (explicit vs // interpolated alpha), so it must NEVER passthrough -- it always decode->repacks below. if (src_compressed && (m_src_kind != source_kind::cBC2)) passthrough = true; else if ((fmt == transcoder_texture_format::cTFRGBA32) && m_uncompressed_is_canonical_rgba8) passthrough = true; } if (passthrough) { // No format change (see the contract above). Source is tightly packed. Compressed dest: copy every block, // pitch may expand but not shrink. Uncompressed canonical-RGBA8 dest: clip (smaller) or expand. Either // way, one whole-image memcpy when fully contiguous, else one memcpy per row/block-row at the dest pitch. const uint32_t unit_bytes = m_bytes_per_block_or_pixel; // per block (compressed) or per pixel (uncompressed) const uint32_t img_units_per_row = src_compressed ? num_blocks_x : width; // image blocks/row or pixels/row const uint32_t img_rows = src_compressed ? num_blocks_y : height; // image block-rows or pixel-rows const uint32_t src_row_pitch_bytes = src_compressed ? (num_blocks_x * unit_bytes) : sd.m_row_pitch; const uint32_t out_pitch_units = output_row_pitch_in_blocks_or_pixels ? output_row_pitch_in_blocks_or_pixels : img_units_per_row; const uint32_t out_rows = src_compressed ? img_rows : (output_rows_in_pixels ? output_rows_in_pixels : height); uint32_t copy_units_per_row, copy_rows; if (src_compressed) { // Compressed: the destination pitch must hold every block (>= num_blocks_x) or larger; it can't shrink. if (out_pitch_units < img_units_per_row) return false; copy_units_per_row = img_units_per_row; copy_rows = img_rows; } else { // Uncompressed: copy only the overlapping top-left region (clip if the destination is smaller). copy_units_per_row = basisu::minimum(img_units_per_row, out_pitch_units); copy_rows = basisu::minimum(img_rows, out_rows); } // The destination buffer must hold its declared extent (out_pitch_units * out_rows units). if ((uint64_t)output_blocks_buf_size_in_blocks_or_pixels < (uint64_t)out_pitch_units * out_rows) return false; uint8_t* pDst = static_cast(pOutput_blocks); const size_t row_copy_bytes = (size_t)copy_units_per_row * unit_bytes; // Whole-image memcpy when source & destination are both tightly packed and fully copied; else per-scanline. if ((out_pitch_units == img_units_per_row) && (copy_units_per_row == img_units_per_row) && (copy_rows == img_rows) && (src_row_pitch_bytes == (uint64_t)img_units_per_row * unit_bytes)) { memcpy(pDst, pSrc, row_copy_bytes * copy_rows); } else { for (uint32_t r = 0; r < copy_rows; r++) memcpy(pDst + (uint64_t)r * out_pitch_units * unit_bytes, pSrc + (uint64_t)r * src_row_pitch_bytes, row_copy_bytes); } return true; } #if BASISD_SUPPORT_XUASTC // ---- Decode -> repack ---- const bool high_quality = (decode_flags & cDecodeFlagsHighQuality) != 0; const uint32_t bc7f_flags = high_quality ? bc7f::cPackBC7FlagDefaultPartiallyAnalytical : bc7f::cPackBC7FlagDefault; const bool from_alpha = m_has_alpha && ((decode_flags & cDecodeFlagsTranscodeAlphaDataToOpaqueFormats) != 0); // has_alpha hint for the block encoders: 1 = potentially has alpha, 0 = definitely opaque. BC1 reports // m_has_alpha == true (it may carry per-block punchthrough alpha), so it correctly lands on 1 here. const int has_alpha_arg = m_has_alpha ? 1 : 0; // Resolve effective channels: caller override (>=0) wins, else the target's sensible default. const int c0 = (channel0 >= 0) ? channel0 : ti.m_def_channel0; const int c1 = (channel1 >= 0) ? channel1 : ti.m_def_channel1; color32 texels[16]; if (ti.m_mode == repack_mode::cPVRTC1) { // PVRTC1: whole-image. Requires power-of-2 dimensions. if (!basisu::is_pow2(width) || !basisu::is_pow2(height)) return false; // encode_pvrtc1 writes blocks contiguously, so a non-tight output pitch can't be honored -- reject it. if (output_row_pitch_in_blocks_or_pixels && (output_row_pitch_in_blocks_or_pixels != num_blocks_x)) return false; if (output_blocks_buf_size_in_blocks_or_pixels < (num_blocks_x * num_blocks_y)) return false; basisu::vector2D temp_image; if (!temp_image.try_resize(num_blocks_x * 4, num_blocks_y * 4)) return false; for (uint32_t by = 0; by < num_blocks_y; by++) { for (uint32_t bx = 0; bx < num_blocks_x; bx++) { decode_source_block(pSrc, width, height, num_blocks_x, sd.m_row_pitch, bx, by, texels); for (uint32_t y = 0; y < 4; y++) for (uint32_t x = 0; x < 4; x++) temp_image(bx * 4 + x, by * 4 + y) = texels[y * 4 + x]; } } encode_pvrtc1(ti.m_bf, pOutput_blocks, temp_image, num_blocks_x, num_blocks_y, from_alpha); return true; } if (ti.m_mode == repack_mode::cASTC4x4) { // ASTC LDR 4x4. A BC7 source feeds its physical block into the bc7f BC7->ASTC latent path (high fidelity; // falls back to a full re-encode only for the rare 2/3-subset patterns). Other sources unpack to texels // and re-encode via fast_pack_astc(pixels, bc7f_flags). Honor the output row pitch (in blocks). const uint32_t out_pitch_blocks = output_row_pitch_in_blocks_or_pixels ? output_row_pitch_in_blocks_or_pixels : num_blocks_x; if (out_pitch_blocks < num_blocks_x) return false; const uint64_t max_index = (uint64_t)(num_blocks_y - 1) * out_pitch_blocks + (num_blocks_x - 1); if ((uint64_t)output_blocks_buf_size_in_blocks_or_pixels <= max_index) return false; // Note: from_alpha is only meaningful for ETC1 and PVRTC1 (handled by transcode_4x4_block / // encode_pvrtc1); it has no useful meaning for ASTC, so it's ignored here. const bool bc7_source = (m_src_kind == source_kind::cBC7); for (uint32_t by = 0; by < num_blocks_y; by++) { for (uint32_t bx = 0; bx < num_blocks_x; bx++) { uint8_t* pDst = static_cast(pOutput_blocks) + ((uint64_t)by * out_pitch_blocks + bx) * 16; if (bc7_source) { // BC7 latent to ASTC latent, when possible, otherwise it'll use bc7f from texels. const basist::bc7_block* pSrc_bc7 = reinterpret_cast(pSrc + ((uint64_t)by * num_blocks_x + bx) * m_bytes_per_block_or_pixel); if (!bc7f::fast_pack_astc(pDst, *pSrc_bc7, bc7f_flags)) return false; } else { // Not BC7 - decode to texels, then use bc7f decode_source_block(pSrc, width, height, num_blocks_x, sd.m_row_pitch, bx, by, texels); if (!bc7f::fast_pack_astc(pDst, reinterpret_cast(texels), bc7f_flags)) return false; } } } return true; } // repack_mode::cBlock4x4: transcode_4x4_block targets (BC1/3/4/5/7, ETC1/ETC2/EAC, 32/16bpp uncompressed). uint32_t out_row_pitch, out_rows; if (ti.m_uncompressed) { // UNCOMPRESSED dest: clip (smaller) or expand (larger) per the contract above. We decode all source // blocks; the per-block write below clips boundary blocks and skips fully-outside ones. out_row_pitch = output_row_pitch_in_blocks_or_pixels ? output_row_pitch_in_blocks_or_pixels : width; const uint32_t dst_rows = output_rows_in_pixels ? output_rows_in_pixels : height; // Rows actually written: never past the image's own rows nor past the destination's rows. out_rows = basisu::minimum(height, dst_rows); // Writes stay within rows [0, out_rows) and columns [0, out_row_pitch); bound the buffer to that extent. const uint64_t max_index = (uint64_t)(out_rows - 1) * out_row_pitch + (out_row_pitch - 1); if ((uint64_t)output_blocks_buf_size_in_blocks_or_pixels <= max_index) return false; } else { // COMPRESSED dest: pitch (in blocks) may expand but can't shrink (see contract); every block emitted whole. out_row_pitch = output_row_pitch_in_blocks_or_pixels ? output_row_pitch_in_blocks_or_pixels : num_blocks_x; out_rows = 0; // A pitch smaller than the block-row width would make blocks in a row collide; reject loudly. if (out_row_pitch < num_blocks_x) return false; const uint64_t max_index = (uint64_t)(num_blocks_y - 1) * out_row_pitch + (num_blocks_x - 1); if ((uint64_t)output_blocks_buf_size_in_blocks_or_pixels <= max_index) return false; } etc1f::pack_etc1_state etc1_pack_state; // Decode block to texels, real-time encode to target format. for (uint32_t by = 0; by < num_blocks_y; by++) { for (uint32_t bx = 0; bx < num_blocks_x; bx++) { decode_source_block(pSrc, width, height, num_blocks_x, sd.m_row_pitch, bx, by, texels); // Uncompressed clip: skip blocks whose top-left is at/beyond the dest edge. transcode_4x4_block's // max_x/max_y are unsigned (pitch/rows - bx/by*4) and would wrap to a huge OOB bound if negative; // straddling blocks keep a positive max and partial-write. (Compressed pitch >= num_blocks_x, never skipped.) if (ti.m_uncompressed && ((bx * 4 >= out_row_pitch) || (by * 4 >= out_rows))) continue; // Block-addressed destination: used directly by compressed targets; uncompressed targets ignore it and // recompute the pixel pointer from pOutput_blocks + block_x/block_y (so it's harmless there). uint8_t* pDst_block_u8 = static_cast(pOutput_blocks) + (uint64_t)by * out_row_pitch * ti.m_out_bytes + (uint64_t)bx * ti.m_out_bytes; if (!transcode_4x4_block( ti.m_bf, bx, by, pOutput_blocks, pDst_block_u8, texels, ti.m_out_bytes, out_row_pitch, out_rows, c0, c1, high_quality, from_alpha, bc7f_flags, etc1_pack_state, has_alpha_arg)) { return false; } } } return true; #else // Re-encode paths require the XUASTC/bc7f/etc1f real-time encoders. (void)output_row_pitch_in_blocks_or_pixels; (void)output_rows_in_pixels; (void)decode_flags; (void)channel0; (void)channel1; (void)pSrc; (void)width; (void)height; (void)num_blocks_x; (void)num_blocks_y; (void)ti; return false; #endif } } // namespace basist