// basisu_dds_export.cpp // Copyright (C) 2019-2025 Binomial LLC. All Rights Reserved. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // See basisu_dds_export.h for an overview. #include "basisu_dds_export.h" #include "basisu_comp.h" #include "basisu_bc7e_scalar.h" #include "../transcoder/basisu_transcoder_internal.h" #include "../transcoder/basisu_transcoder_uastc.h" namespace basisu { // --- DXGI format codes (only the ones we emit). --- enum { DXGI_FORMAT_R8G8B8A8_UNORM = 28, DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = 29, DXGI_FORMAT_R8G8_UNORM = 49, DXGI_FORMAT_R8_UNORM = 61, DXGI_FORMAT_BC1_UNORM = 71, DXGI_FORMAT_BC1_UNORM_SRGB = 72, DXGI_FORMAT_BC2_UNORM = 74, DXGI_FORMAT_BC2_UNORM_SRGB = 75, DXGI_FORMAT_BC3_UNORM = 77, DXGI_FORMAT_BC3_UNORM_SRGB = 78, DXGI_FORMAT_BC4_UNORM = 80, DXGI_FORMAT_BC5_UNORM = 83, DXGI_FORMAT_B5G6R5_UNORM = 85, DXGI_FORMAT_B5G5R5A1_UNORM = 86, DXGI_FORMAT_B8G8R8A8_UNORM = 87, DXGI_FORMAT_B8G8R8A8_UNORM_SRGB = 91, DXGI_FORMAT_BC7_UNORM = 98, DXGI_FORMAT_BC7_UNORM_SRGB = 99, DXGI_FORMAT_B4G4R4A4_UNORM = 115 }; // --- DDS header flag/cap constants. --- enum { DDSD_CAPS = 0x1, DDSD_HEIGHT = 0x2, DDSD_WIDTH = 0x4, DDSD_PITCH = 0x8, DDSD_PIXELFORMAT = 0x1000, DDSD_MIPMAPCOUNT = 0x20000, DDSD_LINEARSIZE = 0x80000, DDPF_FOURCC = 0x4, DDSCAPS_COMPLEX = 0x8, DDSCAPS_TEXTURE = 0x1000, DDSCAPS_MIPMAP = 0x400000, DDSCAPS2_CUBEMAP = 0x200, DDSCAPS2_CUBEMAP_ALLFACES = 0xFC00, DDS_MAGIC = 0x20534444, // "DDS " DDS_DX10_FOURCC = 0x30315844, // "DX10" DDS_DIMENSION_TEXTURE2D = 3, DDS_RESOURCE_MISC_TEXTURECUBE = 0x4 }; // Per-format static info. struct dds_format_info { const char* m_pToken; bool m_block_compressed; uint32_t m_bytes; // bytes per 4x4 block (compressed) or bytes per pixel (uncompressed) uint32_t m_dxgi_unorm; uint32_t m_dxgi_srgb; // 0 if no sRGB variant exists }; static const dds_format_info g_dds_format_info[cDDSFmtTotal] = { // token block bytes unorm srgb { "bc1", true, 8, DXGI_FORMAT_BC1_UNORM, DXGI_FORMAT_BC1_UNORM_SRGB }, { "bc2", true, 16, DXGI_FORMAT_BC2_UNORM, DXGI_FORMAT_BC2_UNORM_SRGB }, { "bc3", true, 16, DXGI_FORMAT_BC3_UNORM, DXGI_FORMAT_BC3_UNORM_SRGB }, { "bc4", true, 8, DXGI_FORMAT_BC4_UNORM, 0 }, { "bc5", true, 16, DXGI_FORMAT_BC5_UNORM, 0 }, { "bc7", true, 16, DXGI_FORMAT_BC7_UNORM, DXGI_FORMAT_BC7_UNORM_SRGB }, { "a8r8g8b8", false, 4, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_B8G8R8A8_UNORM_SRGB }, { "a8b8g8r8", false, 4, DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_FORMAT_R8G8B8A8_UNORM_SRGB }, { "r8", false, 1, DXGI_FORMAT_R8_UNORM, 0 }, { "r8g8", false, 2, DXGI_FORMAT_R8G8_UNORM, 0 }, { "r5g6b5", false, 2, DXGI_FORMAT_B5G6R5_UNORM, 0 }, { "a1r5g5b5", false, 2, DXGI_FORMAT_B5G5R5A1_UNORM, 0 }, { "a4r4g4b4", false, 2, DXGI_FORMAT_B4G4R4A4_UNORM, 0 } }; bool parse_dds_output_format(const char* pToken, dds_output_format& fmt) { fmt = cDDSFmtInvalid; if (!pToken) return false; for (int i = 0; i < (int)cDDSFmtTotal; i++) { if (strcasecmp(pToken, g_dds_format_info[i].m_pToken) == 0) { fmt = (dds_output_format)i; return true; } } // Tolerate the (geometrically impossible) "a1r5g6b5" spelling as an alias for a1r5g5b5. if (strcasecmp(pToken, "a1r5g6b5") == 0) { fmt = cDDSFmtA1R5G5B5; return true; } return false; } const char* get_dds_output_format_string(dds_output_format fmt) { if ((fmt < 0) || (fmt >= cDDSFmtTotal)) return "?"; return g_dds_format_info[fmt].m_pToken; } bool dds_output_format_has_srgb_variant(dds_output_format fmt) { if ((fmt < 0) || (fmt >= cDDSFmtTotal)) return false; return g_dds_format_info[fmt].m_dxgi_srgb != 0; } // --- Little-endian append helpers. --- static inline void append_u16(uint8_vec& v, uint32_t x) { v.push_back((uint8_t)(x & 0xFF)); v.push_back((uint8_t)((x >> 8) & 0xFF)); } static inline void append_u32(uint8_vec& v, uint32_t x) { v.push_back((uint8_t)(x & 0xFF)); v.push_back((uint8_t)((x >> 8) & 0xFF)); v.push_back((uint8_t)((x >> 16) & 0xFF)); v.push_back((uint8_t)((x >> 24) & 0xFF)); } // Packs a single RGBA8 texel into the uncompressed output bytes for fmt (truncating 8->N bits, which is // the exact inverse of the reader's bit-replication expansion). Channel byte orders match the DXGI formats. static void pack_uncompressed_pixel(uint8_vec& out, const color_rgba& c, dds_output_format fmt) { switch (fmt) { case cDDSFmtA8R8G8B8: // DXGI B8G8R8A8 -> memory order B,G,R,A out.push_back(c.b); out.push_back(c.g); out.push_back(c.r); out.push_back(c.a); break; case cDDSFmtA8B8G8R8: // DXGI R8G8B8A8 -> memory order R,G,B,A out.push_back(c.r); out.push_back(c.g); out.push_back(c.b); out.push_back(c.a); break; case cDDSFmtR8: // DXGI R8 -> just R out.push_back(c.r); break; case cDDSFmtR8G8: // DXGI R8G8 -> source R into R, source G into G (matches BC5; swizzle input for other mappings) out.push_back(c.r); out.push_back(c.g); break; case cDDSFmtR5G6B5: append_u16(out, (uint32_t)(((c.r >> 3) << 11) | ((c.g >> 2) << 5) | (c.b >> 3))); break; case cDDSFmtA1R5G5B5: append_u16(out, (uint32_t)((c.a >= 128 ? 0x8000 : 0) | ((c.r >> 3) << 10) | ((c.g >> 3) << 5) | (c.b >> 3))); break; case cDDSFmtA4R4G4B4: append_u16(out, (uint32_t)(((c.a >> 4) << 12) | ((c.r >> 4) << 8) | ((c.g >> 4) << 4) | (c.b >> 4))); break; default: assert(0); break; } } // Prebuilt BC7 packing context (built once per build_dds, shared read-only across slices). struct bc7_pack_context { dds_bc7_encoder m_encoder; uint32_t m_bc7f_flags; // bc7f packer flags (when m_encoder == bc7f) bc7e_scalar::bc7e_compress_block_params m_bc7e_params; // initialized only when m_encoder == bc7e_scalar }; // Packs one prepared slice image into the bytes for fmt, using logical dims (orig_width/orig_height). // For block formats this iterates whole 4x4 blocks (the slice image is already block-padded); for // uncompressed it emits tight rows of exactly orig_width*orig_height pixels. static void pack_slice(uint8_vec& out, const image& img, uint32_t orig_width, uint32_t orig_height, dds_output_format fmt, const bc7_pack_context& bc7ctx) { const dds_format_info& info = g_dds_format_info[fmt]; if (info.m_block_compressed) { const uint32_t blocks_x = (orig_width + 3) / 4; const uint32_t blocks_y = (orig_height + 3) / 4; for (uint32_t by = 0; by < blocks_y; by++) { for (uint32_t bx = 0; bx < blocks_x; bx++) { color_rgba blk[16]; img.extract_block_clamped(blk, bx * 4, by * 4, 4, 4); uint8_t dst[16]; switch (fmt) { case cDDSFmtBC1: basist::encode_bc1(dst, (const uint8_t*)blk, basist::cEncodeBC1HighQuality); break; case cDDSFmtBC2: // 8 bytes explicit 4-bit alpha (one LE 16-bit word per row, texel x at nibble x), then a // BC1 color block (encode_bc1 emits 4-color blocks, which BC2's always-4-color decode needs). for (uint32_t ry = 0; ry < 4; ry++) { uint32_t row = 0; for (uint32_t rx = 0; rx < 4; rx++) row |= ((uint32_t)(blk[ry * 4 + rx].a >> 4)) << (rx * 4); dst[ry * 2] = (uint8_t)(row & 0xFF); dst[ry * 2 + 1] = (uint8_t)(row >> 8); } basist::encode_bc1(dst + 8, (const uint8_t*)blk, basist::cEncodeBC1HighQuality); break; case cDDSFmtBC3: basist::encode_bc4(dst, &blk[0].a, sizeof(color_rgba)); basist::encode_bc1(dst + 8, (const uint8_t*)blk, basist::cEncodeBC1HighQuality); break; case cDDSFmtBC4: basist::encode_bc4(dst, &blk[0].r, sizeof(color_rgba)); break; case cDDSFmtBC5: basist::encode_bc4(dst, &blk[0].r, sizeof(color_rgba)); basist::encode_bc4(dst + 8, &blk[0].g, sizeof(color_rgba)); break; case cDDSFmtBC7: // basist::color_rgba and basisu::color_rgba have identical layout (r,g,b,a uint8_t). if (bc7ctx.m_encoder == cDDSBC7Encoder_BC7E_Scalar) { uint64_t blk64[2]; bc7e_scalar::bc7e_compress_blocks(1, blk64, reinterpret_cast(blk), &bc7ctx.m_bc7e_params, nullptr); memcpy(dst, blk64, 16); } else { basist::bc7f::fast_pack_bc7_auto_rgba(dst, reinterpret_cast(blk), bc7ctx.m_bc7f_flags); } break; default: assert(0); break; } out.append(dst, info.m_bytes); } } } else { for (uint32_t y = 0; y < orig_height; y++) for (uint32_t x = 0; x < orig_width; x++) pack_uncompressed_pixel(out, img(x, y), fmt); } } // Builds the DDS magic + DDS_HEADER + DDS_HEADER_DXT10 (148 bytes total). static void build_dx10_header(uint8_vec& out, uint32_t width, uint32_t height, uint32_t mip_count, uint32_t array_size, bool is_cubemap, uint32_t dxgi_format, const dds_format_info& info) { // dwPitchOrLinearSize (informational; readers generally recompute). uint32_t pitch_or_linear_size; uint32_t flags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT; if (info.m_block_compressed) { pitch_or_linear_size = ((width + 3) / 4) * ((height + 3) / 4) * info.m_bytes; flags |= DDSD_LINEARSIZE; } else { pitch_or_linear_size = width * info.m_bytes; flags |= DDSD_PITCH; } if (mip_count > 1) flags |= DDSD_MIPMAPCOUNT; uint32_t caps = DDSCAPS_TEXTURE; if ((mip_count > 1) || is_cubemap || (array_size > 1)) caps |= DDSCAPS_COMPLEX; if (mip_count > 1) caps |= DDSCAPS_MIPMAP; const uint32_t caps2 = is_cubemap ? (DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_ALLFACES) : 0; append_u32(out, DDS_MAGIC); // DDS_HEADER (124 bytes). append_u32(out, 124); // dwSize append_u32(out, flags); // dwFlags append_u32(out, height); // dwHeight append_u32(out, width); // dwWidth append_u32(out, pitch_or_linear_size); append_u32(out, 0); // dwDepth append_u32(out, mip_count); // dwMipMapCount for (uint32_t i = 0; i < 11; i++) // dwReserved1[11] append_u32(out, 0); // DDS_PIXELFORMAT (32 bytes) - DX10 indirection. append_u32(out, 32); // ddspf.dwSize append_u32(out, DDPF_FOURCC); // ddspf.dwFlags append_u32(out, DDS_DX10_FOURCC); // ddspf.dwFourCC = "DX10" append_u32(out, 0); // dwRGBBitCount append_u32(out, 0); // dwRBitMask append_u32(out, 0); // dwGBitMask append_u32(out, 0); // dwBBitMask append_u32(out, 0); // dwABitMask append_u32(out, caps); // dwCaps append_u32(out, caps2); // dwCaps2 append_u32(out, 0); // dwCaps3 append_u32(out, 0); // dwCaps4 append_u32(out, 0); // dwReserved2 // DDS_HEADER_DXT10 (20 bytes). append_u32(out, dxgi_format); // dxgiFormat append_u32(out, DDS_DIMENSION_TEXTURE2D); append_u32(out, is_cubemap ? DDS_RESOURCE_MISC_TEXTURECUBE : 0); // miscFlag append_u32(out, array_size); // arraySize (number of array elements; cubes for a cubemap) append_u32(out, 0); // miscFlags2 } bool build_dds(uint8_vec& dds_data, const basis_compressor& comp, const dds_export_params& params, std::string& error_msg, uint32_t* pOut_width, uint32_t* pOut_height, uint32_t* pOut_levels, uint32_t* pOut_layers, uint32_t* pOut_faces) { error_msg.clear(); dds_data.resize(0); const dds_output_format fmt = params.m_format; if ((fmt < 0) || (fmt >= cDDSFmtTotal)) { error_msg = "invalid output format"; return false; } const dds_format_info& info = g_dds_format_info[fmt]; const basisu::vector& slices = comp.get_slice_images(); const basisu_backend_slice_desc_vec& descs = comp.get_slice_descs(); if (!slices.size() || (slices.size() != descs.size())) { error_msg = "no prepared slices (was process_source_images() run successfully?)"; return false; } const bool is_cubemap = (comp.get_params().m_tex_type == basist::cBASISTexTypeCubemapArray); // Determine base dims, layer count, mip level count, and face count - mirrors create_ktx2_file(). uint32_t base_width = 0, base_height = 0, total_layers = 0, total_levels = 0, total_faces = 1; for (uint32_t i = 0; i < descs.size(); i++) { if ((descs[i].m_mip_index == 0) && (!base_width)) { base_width = descs[i].m_orig_width; base_height = descs[i].m_orig_height; } total_layers = maximum(total_layers, descs[i].m_source_file_index + 1); if (!descs[i].m_source_file_index) total_levels = maximum(total_levels, descs[i].m_mip_index + 1); } if (is_cubemap) { if ((total_layers % 6) != 0) { error_msg = "cubemap source image count is not a multiple of 6"; return false; } total_layers /= 6; total_faces = 6; } if (!base_width || !base_height || !total_layers || !total_levels) { error_msg = "could not determine texture dimensions from the prepared slices"; return false; } // Build a (layer, face, level) -> slice index map using the same decomposition as create_ktx2_file(). const uint32_t total_subresources = total_layers * total_faces * total_levels; basisu::vector slice_map(total_subresources); for (uint32_t i = 0; i < total_subresources; i++) slice_map[i] = -1; for (uint32_t i = 0; i < descs.size(); i++) { // Note: descs[i].m_alpha just flags that the slice contains alpha; in the XUBC7 (m_uastc) path each // slice holds full RGBA, so it is NOT a separate alpha-only slice (that only happens for ETC1S). const uint32_t level_index = descs[i].m_mip_index; uint32_t layer_index = descs[i].m_source_file_index; uint32_t face_index = 0; if (is_cubemap) { face_index = layer_index % 6; layer_index /= 6; } if ((layer_index >= total_layers) || (face_index >= total_faces) || (level_index >= total_levels)) { error_msg = "slice descriptor out of range (internal error)"; return false; } const uint32_t map_index = (layer_index * total_faces + face_index) * total_levels + level_index; // Two slices mapping to the same subresource would indicate RGB/alpha slice splitting (ETC1S), which // we don't expect here since we force the XUBC7 path. if (slice_map[map_index] >= 0) { error_msg = "multiple slices map to the same (layer, face, level) - RGB/alpha slice splitting is not supported"; return false; } slice_map[map_index] = (int)i; } for (uint32_t i = 0; i < total_subresources; i++) { if (slice_map[i] < 0) { error_msg = "missing slice for a (layer, face, level) - source images are not all the same size / mip count"; return false; } } // Select the DXGI variant (UNORM vs sRGB). const bool want_srgb = comp.get_params().m_ktx2_and_basis_srgb_transfer_function; const uint32_t dxgi_format = (want_srgb && info.m_dxgi_srgb) ? info.m_dxgi_srgb : info.m_dxgi_unorm; if (want_srgb && !info.m_dxgi_srgb && comp.get_params().m_status_output) printf("Note: format %s has no sRGB DXGI variant; writing UNORM (texel data is unchanged regardless).\n", info.m_pToken); // Build the BC7 packing context once (only relevant for the bc7 format). bc7_pack_context bc7ctx; memset(&bc7ctx, 0, sizeof(bc7ctx)); bc7ctx.m_encoder = params.m_bc7_encoder; switch (clamp(params.m_bc7f_level, cDDSBC7FLevel_Analytical, cDDSBC7FLevel_NonAnalytical)) { case cDDSBC7FLevel_Analytical: bc7ctx.m_bc7f_flags = basist::bc7f::cPackBC7FlagDefault; break; case cDDSBC7FLevel_NonAnalytical: bc7ctx.m_bc7f_flags = basist::bc7f::cPackBC7FlagDefaultNonAnalytical; break; default: bc7ctx.m_bc7f_flags = basist::bc7f::cPackBC7FlagDefaultPartiallyAnalytical; break; } if ((fmt == cDDSFmtBC7) && (params.m_bc7_encoder == cDDSBC7Encoder_BC7E_Scalar)) { bc7e_scalar::bc7e_compress_block_init(); typedef void (*bc7e_init_func)(bc7e_scalar::bc7e_compress_block_params*, bool); static const bc7e_init_func s_bc7e_level_init[7] = { &bc7e_scalar::bc7e_compress_block_params_init_ultrafast, // 0 &bc7e_scalar::bc7e_compress_block_params_init_veryfast, // 1 &bc7e_scalar::bc7e_compress_block_params_init_fast, // 2 &bc7e_scalar::bc7e_compress_block_params_init_basic, // 3 &bc7e_scalar::bc7e_compress_block_params_init_slow, // 4 &bc7e_scalar::bc7e_compress_block_params_init_veryslow, // 5 &bc7e_scalar::bc7e_compress_block_params_init_slowest, // 6 }; const int lvl = clamp(params.m_bc7e_scalar_level, 0, 6); // Mirrors the XUBC7 path: for perceptual (sRGB) sources run bc7e in its built-in perceptual error // mode (it ignores m_weights then); for linear sources run linear and hand it the same RGBA channel // weights XUASTC LDR / XUBC7 use. const bool perceptual = comp.get_params().m_perceptual; s_bc7e_level_init[lvl](&bc7ctx.m_bc7e_params, perceptual); if (!perceptual) { for (uint32_t i = 0; i < 4; i++) bc7ctx.m_bc7e_params.m_weights[i] = comp.get_params().m_xuastc_ldr_channel_weights[i]; } } // Optional debug logging (gated by the compressor's m_debug param, same flag -debug/-verbose set). const bool debug = comp.get_params().m_debug; if (debug) { const bool wrote_srgb_dbg = want_srgb && (info.m_dxgi_srgb != 0); debug_printf("DDS export: format \"%s\" (%s), DXGI=%u, %ux%u, %u level(s), %u layer(s), %u face(s) (%s), %u subresource(s)\n", info.m_pToken, wrote_srgb_dbg ? "sRGB" : "UNORM", dxgi_format, base_width, base_height, total_levels, total_layers, total_faces, is_cubemap ? "cubemap" : "2D", total_subresources); debug_printf("DDS export: %s, %u byte(s) per %s\n", info.m_block_compressed ? "block-compressed" : "uncompressed", info.m_bytes, info.m_block_compressed ? "block" : "pixel"); if (fmt == cDDSFmtBC7) { // Print the FULL BC7 configuration (both encoders' settings) regardless of which encoder // is active, so the actual values used can be verified at a glance. The "encoder=" line // states which one is in effect; the line for the other encoder is informational. const bool using_bc7e = (params.m_bc7_encoder == cDDSBC7Encoder_BC7E_Scalar); const int bc7f_lvl = clamp(params.m_bc7f_level, cDDSBC7FLevel_Analytical, cDDSBC7FLevel_NonAnalytical); const char* pBc7fLvl = (bc7f_lvl == cDDSBC7FLevel_Analytical) ? "analytical" : (bc7f_lvl == cDDSBC7FLevel_NonAnalytical) ? "non-analytical" : "partially-analytical"; const int bc7e_lvl = clamp(params.m_bc7e_scalar_level, 0, 6); const bool perceptual = comp.get_params().m_perceptual; const uint32_t* pW = comp.get_params().m_xuastc_ldr_channel_weights; debug_printf("DDS export: BC7 encoder=%s\n", using_bc7e ? "bc7e_scalar" : "bc7f"); debug_printf("DDS export: bc7f level=%d (%s), pack flags=0x%X%s\n", bc7f_lvl, pBc7fLvl, bc7ctx.m_bc7f_flags, using_bc7e ? " (inactive)" : " (active)"); debug_printf("DDS export: bc7e_scalar level=%d, perceptual=%u, weights=[%u %u %u %u]%s%s\n", bc7e_lvl, (uint32_t)perceptual, pW[0], pW[1], pW[2], pW[3], perceptual ? " (weights ignored in perceptual mode)" : "", using_bc7e ? " (active)" : " (inactive)"); } } // Assemble the file into the caller's buffer: header then subresources in DDS order (layer, face, mip). build_dx10_header(dds_data, base_width, base_height, total_levels, total_layers, is_cubemap, dxgi_format, info); if (comp.get_params().m_status_output) printf("Writing DDS (format \"%s\", %ux%u): %u subresource(s) [%u level(s), %u layer(s), %u face(s)]\n", info.m_pToken, base_width, base_height, total_subresources, total_levels, total_layers, total_faces); for (uint32_t layer = 0; layer < total_layers; layer++) { for (uint32_t face = 0; face < total_faces; face++) { for (uint32_t level = 0; level < total_levels; level++) { const uint32_t map_index = (layer * total_faces + face) * total_levels + level; const int slice_index = slice_map[map_index]; const basisu_backend_slice_desc& desc = descs[slice_index]; if (comp.get_params().m_status_output) printf("DDS: packing subresource %u/%u (layer %u, face %u, level %u): %ux%u\n", map_index + 1, total_subresources, layer, face, level, desc.m_orig_width, desc.m_orig_height); const size_t before_size = dds_data.size(); pack_slice(dds_data, slices[slice_index], desc.m_orig_width, desc.m_orig_height, fmt, bc7ctx); if (debug) debug_printf(" subresource [layer %u, face %u, level %u]: %ux%u -> %u byte(s)\n", layer, face, level, desc.m_orig_width, desc.m_orig_height, (uint32_t)(dds_data.size() - before_size)); } } } if (pOut_width) *pOut_width = base_width; if (pOut_height) *pOut_height = base_height; if (pOut_levels) *pOut_levels = total_levels; if (pOut_layers) *pOut_layers = total_layers; if (pOut_faces) *pOut_faces = total_faces; return true; } } // namespace basisu