// File: crn_texture_comp.cpp // See Copyright Notice and license at the end of inc/crnlib.h #include "crn_core.h" #include "crn_texture_comp.h" #include "crn_dds_comp.h" #include "crn_console.h" #include "crn_rect.h" namespace crnlib { static itexture_comp *create_texture_comp(crn_file_type file_type) { if (file_type == cCRNFileTypeCRN) return crnlib_new(); else if (file_type == cCRNFileTypeDDS) return crnlib_new(); else return NULL; } bool create_compressed_texture(const crn_comp_params ¶ms, crnlib::vector &comp_data, uint32 *pActual_quality_level, float *pActual_bitrate) { crn_comp_params local_params(params); if (pixel_format_helpers::is_crn_format_non_srgb(local_params.m_format)) { if (local_params.get_flag(cCRNCompFlagPerceptual)) { console::info("Output pixel format is swizzled or not RGB, disabling perceptual color metrics"); // Destination compressed pixel format is swizzled or not RGB at all, so be sure perceptual colorspace metrics are disabled. local_params.set_flag(cCRNCompFlagPerceptual, false); } } if (pActual_quality_level) *pActual_quality_level = 0; if (pActual_bitrate) *pActual_bitrate = 0.0f; comp_data.resize(0); itexture_comp *pTexture_comp = create_texture_comp(local_params.m_file_type); if (!pTexture_comp) return false; if (!pTexture_comp->compress_init(local_params)) { crnlib_delete(pTexture_comp); return false; } if ( (local_params.m_target_bitrate <= 0.0f) || (local_params.m_format == cCRNFmtDXT3) || ((local_params.m_file_type == cCRNFileTypeCRN) && ((local_params.m_flags & cCRNCompFlagManualPaletteSizes) != 0)) ) { if ( (local_params.m_file_type == cCRNFileTypeCRN) || ((local_params.m_file_type == cCRNFileTypeDDS) && (local_params.m_quality_level < cCRNMaxQualityLevel)) ) { console::info("Compressing using quality level %i", local_params.m_quality_level); } if (local_params.m_format == cCRNFmtDXT3) { if (local_params.m_file_type == cCRNFileTypeCRN) console::warning("CRN format doesn't support DXT3"); else if ((local_params.m_file_type == cCRNFileTypeDDS) && (local_params.m_quality_level < cCRNMaxQualityLevel)) console::warning("Clustered DDS compressor doesn't support DXT3"); } if (!pTexture_comp->compress_pass(local_params, pActual_bitrate)) { crnlib_delete(pTexture_comp); return false; } comp_data.swap(pTexture_comp->get_comp_data()); if ((pActual_quality_level) && (local_params.m_target_bitrate <= 0.0)) *pActual_quality_level = local_params.m_quality_level; crnlib_delete(pTexture_comp); return true; } // Interpolative search to find closest quality level to target bitrate. const int cLowestQuality = 0; const int cHighestQuality = cCRNMaxQualityLevel; const int cNumQualityLevels = cHighestQuality - cLowestQuality + 1; float best_bitrate = 1e+10f; int best_quality_level = -1; const uint cMaxIterations = 8; for ( ; ; ) { int low_quality = cLowestQuality; int high_quality = cHighestQuality; float cached_bitrates[cNumQualityLevels]; for (int i = 0; i < cNumQualityLevels; i++) cached_bitrates[i] = -1.0f; float highest_bitrate = 0.0f; uint iter_count = 0; bool force_binary_search = false; while (low_quality <= high_quality) { if (params.m_flags & cCRNCompFlagDebugging) { console::debug("Quality level bracket: [%u, %u]", low_quality, high_quality); } int trial_quality = (low_quality + high_quality) / 2; if ((iter_count) && (!force_binary_search)) { int bracket_low = trial_quality; while ((cached_bitrates[bracket_low] < 0) && (bracket_low > cLowestQuality)) bracket_low--; if (cached_bitrates[bracket_low] < 0) trial_quality = static_cast(math::lerp((float)low_quality, (float)high_quality, .33f)); else { int bracket_high = trial_quality + 1; if (bracket_high <= cHighestQuality) { while ((cached_bitrates[bracket_high] < 0) && (bracket_high < cHighestQuality)) bracket_high++; if (cached_bitrates[bracket_high] >= 0) { float bracket_low_bitrate = cached_bitrates[bracket_low]; float bracket_high_bitrate = cached_bitrates[bracket_high]; if ((bracket_low_bitrate < bracket_high_bitrate) && (bracket_low_bitrate < local_params.m_target_bitrate) && (bracket_high_bitrate >= local_params.m_target_bitrate)) { int quality = low_quality + static_cast( ((local_params.m_target_bitrate - bracket_low_bitrate) * (high_quality - low_quality)) / (bracket_high_bitrate - bracket_low_bitrate) ); if ((quality >= low_quality) && (quality <= high_quality)) { trial_quality = quality; } } } } } } console::info("Compressing to quality level %u", trial_quality); float bitrate = 0.0f; local_params.m_quality_level = trial_quality; if (!pTexture_comp->compress_pass(local_params, &bitrate)) { crnlib_delete(pTexture_comp); return false; } cached_bitrates[trial_quality] = bitrate; highest_bitrate = math::maximum(highest_bitrate, bitrate); console::info("\nTried quality level %u, bpp: %3.3f", trial_quality, bitrate); if ( (best_quality_level < 0) || ((bitrate <= local_params.m_target_bitrate) && (best_bitrate > local_params.m_target_bitrate)) || (((bitrate <= local_params.m_target_bitrate) || (best_bitrate > local_params.m_target_bitrate)) && (fabs(bitrate - local_params.m_target_bitrate) < fabs(best_bitrate - local_params.m_target_bitrate))) ) { best_bitrate = bitrate; comp_data.swap(pTexture_comp->get_comp_data()); best_quality_level = trial_quality; if (params.m_flags & cCRNCompFlagDebugging) { console::debug("Choose new best quality level"); } if ((best_bitrate <= local_params.m_target_bitrate) && (fabs(best_bitrate - local_params.m_target_bitrate) < .005f)) break; } if (bitrate > local_params.m_target_bitrate) high_quality = trial_quality - 1; else low_quality = trial_quality + 1; iter_count++; if (iter_count > cMaxIterations) { force_binary_search = true; } } if (((local_params.m_flags & cCRNCompFlagHierarchical) != 0) && (highest_bitrate < local_params.m_target_bitrate) && (fabs(best_bitrate - local_params.m_target_bitrate) >= .005f)) { console::info("Unable to achieve desired bitrate - disabling adaptive block sizes and retrying search."); local_params.m_flags &= ~cCRNCompFlagHierarchical; crnlib_delete(pTexture_comp); pTexture_comp = create_texture_comp(local_params.m_file_type); if (!pTexture_comp->compress_init(local_params)) { crnlib_delete(pTexture_comp); return false; } } else break; } crnlib_delete(pTexture_comp); pTexture_comp = NULL; if (best_quality_level < 0) return false; if (pActual_quality_level) *pActual_quality_level = best_quality_level; if (pActual_bitrate) *pActual_bitrate = best_bitrate; console::printf("Selected quality level %u bpp: %f", best_quality_level, best_bitrate); return true; } static bool create_dds_tex(const crn_comp_params ¶ms, mipmapped_texture &dds_tex) { image_u8 images[cCRNMaxFaces][cCRNMaxLevels]; bool has_alpha = false; for (uint face_index = 0; face_index < params.m_faces; face_index++) { for (uint level_index = 0; level_index < params.m_levels; level_index++) { const uint width = math::maximum(1U, params.m_width >> level_index); const uint height = math::maximum(1U, params.m_height >> level_index); if (!params.m_pImages[face_index][level_index]) return false; images[face_index][level_index].alias((color_quad_u8*)params.m_pImages[face_index][level_index], width, height); if (!has_alpha) has_alpha = image_utils::has_alpha(images[face_index][level_index]); } } for (uint face_index = 0; face_index < params.m_faces; face_index++) for (uint level_index = 0; level_index < params.m_levels; level_index++) images[face_index][level_index].set_component_valid(3, has_alpha); face_vec faces(params.m_faces); for (uint face_index = 0; face_index < params.m_faces; face_index++) { for (uint level_index = 0; level_index < params.m_levels; level_index++) { mip_level *pMip = crnlib_new(); image_u8 *pImage = crnlib_new(); pImage->swap(images[face_index][level_index]); pMip->assign(pImage); faces[face_index].push_back(pMip); } } dds_tex.assign(faces); #ifdef CRNLIB_BUILD_DEBUG CRNLIB_ASSERT(dds_tex.check()); #endif return true; } bool create_texture_mipmaps(mipmapped_texture &work_tex, const crn_comp_params ¶ms, const crn_mipmap_params &mipmap_params, bool generate_mipmaps) { crn_comp_params new_params(params); bool generate_new_mips = false; switch (mipmap_params.m_mode) { case cCRNMipModeUseSourceOrGenerateMips: { if (work_tex.get_num_levels() == 1) generate_new_mips = true; break; } case cCRNMipModeUseSourceMips: { break; } case cCRNMipModeGenerateMips: { generate_new_mips = true; break; } case cCRNMipModeNoMips: { work_tex.discard_mipmaps(); break; } default: { CRNLIB_ASSERT(0); break; } } rect window_rect(mipmap_params.m_window_left, mipmap_params.m_window_top, mipmap_params.m_window_right, mipmap_params.m_window_bottom); if (!window_rect.is_empty()) { if (work_tex.get_num_faces() > 1) { console::warning("Can't crop cubemap textures"); } else { console::info("Cropping input texture from window (%ux%u)-(%ux%u)", window_rect.get_left(), window_rect.get_top(), window_rect.get_right(), window_rect.get_bottom()); if (!work_tex.crop(window_rect.get_left(), window_rect.get_top(), window_rect.get_width(), window_rect.get_height())) console::warning("Failed cropping window rect"); } } int new_width = work_tex.get_width(); int new_height = work_tex.get_height(); if ((mipmap_params.m_clamp_width) && (mipmap_params.m_clamp_height)) { if ((new_width > (int)mipmap_params.m_clamp_width) || (new_height > (int)mipmap_params.m_clamp_height)) { if (!mipmap_params.m_clamp_scale) { if (work_tex.get_num_faces() > 1) { console::warning("Can't crop cubemap textures"); } else { new_width = math::minimum(mipmap_params.m_clamp_width, new_width); new_height = math::minimum(mipmap_params.m_clamp_height, new_height); console::info("Clamping input texture to %ux%u", new_width, new_height); work_tex.crop(0, 0, new_width, new_height); } } } } if (mipmap_params.m_scale_mode != cCRNSMDisabled) { bool is_pow2 = math::is_power_of_2((uint32)new_width) && math::is_power_of_2((uint32)new_height); switch (mipmap_params.m_scale_mode) { case cCRNSMAbsolute: { new_width = (uint)mipmap_params.m_scale_x; new_height = (uint)mipmap_params.m_scale_y; break; } case cCRNSMRelative: { new_width = (uint)(mipmap_params.m_scale_x * new_width + .5f); new_height = (uint)(mipmap_params.m_scale_y * new_height + .5f); break; } case cCRNSMLowerPow2: { if (!is_pow2) math::compute_lower_pow2_dim(new_width, new_height); break; } case cCRNSMNearestPow2: { if (!is_pow2) { int lwidth = new_width; int lheight = new_height; math::compute_lower_pow2_dim(lwidth, lheight); int uwidth = new_width; int uheight = new_height; math::compute_upper_pow2_dim(uwidth, uheight); if (labs(new_width - lwidth) < labs(new_width - uwidth)) new_width = lwidth; else new_width = uwidth; if (labs(new_height - lheight) < labs(new_height - uheight)) new_height = lheight; else new_height = uheight; } break; } case cCRNSMNextPow2: { if (!is_pow2) math::compute_upper_pow2_dim(new_width, new_height); break; } default: break; } } if ((mipmap_params.m_clamp_width) && (mipmap_params.m_clamp_height)) { if ((new_width > (int)mipmap_params.m_clamp_width) || (new_height > (int)mipmap_params.m_clamp_height)) { if (mipmap_params.m_clamp_scale) { new_width = math::minimum(mipmap_params.m_clamp_width, new_width); new_height = math::minimum(mipmap_params.m_clamp_height, new_height); } } } new_width = math::clamp(new_width, 1, cCRNMaxLevelResolution); new_height = math::clamp(new_height, 1, cCRNMaxLevelResolution); if ((new_width != (int)work_tex.get_width()) || (new_height != (int)work_tex.get_height())) { console::info("Resampling input texture to %ux%u", new_width, new_height); const char* pFilter = crn_get_mip_filter_name(mipmap_params.m_filter); bool srgb = mipmap_params.m_gamma_filtering != 0; mipmapped_texture::resample_params res_params; res_params.m_pFilter = pFilter; res_params.m_wrapping = mipmap_params.m_tiled != 0; if (work_tex.get_num_faces()) res_params.m_wrapping = false; res_params.m_renormalize = mipmap_params.m_renormalize != 0; res_params.m_filter_scale = 1.0f; res_params.m_gamma = mipmap_params.m_gamma; res_params.m_srgb = srgb; res_params.m_multithreaded = (params.m_num_helper_threads > 0); if (!work_tex.resize(new_width, new_height, res_params)) { console::error("Failed resizing texture!"); return false; } } if ((generate_new_mips) && (generate_mipmaps)) { bool srgb = mipmap_params.m_gamma_filtering != 0; const char* pFilter = crn_get_mip_filter_name(mipmap_params.m_filter); mipmapped_texture::generate_mipmap_params gen_params; gen_params.m_pFilter = pFilter; gen_params.m_wrapping = mipmap_params.m_tiled != 0; gen_params.m_renormalize = mipmap_params.m_renormalize != 0; gen_params.m_filter_scale = mipmap_params.m_blurriness; gen_params.m_gamma = mipmap_params.m_gamma; gen_params.m_srgb = srgb; gen_params.m_multithreaded = params.m_num_helper_threads > 0; gen_params.m_max_mips = mipmap_params.m_max_levels; gen_params.m_min_mip_size = mipmap_params.m_min_mip_size; console::info("Generating mipmaps using filter \"%s\"", pFilter); timer tm; tm.start(); if (!work_tex.generate_mipmaps(gen_params, true)) { console::error("Failed generating mipmaps!"); return false; } double t = tm.get_elapsed_secs(); console::info("Generated %u mipmap levels in %3.3fs", work_tex.get_num_levels() - 1, t); } return true; } bool create_compressed_texture(const crn_comp_params ¶ms, const crn_mipmap_params &mipmap_params, crnlib::vector &comp_data, uint32 *pActual_quality_level, float *pActual_bitrate) { comp_data.resize(0); if (pActual_bitrate) *pActual_bitrate = 0.0f; if (pActual_quality_level) *pActual_quality_level = 0; mipmapped_texture work_tex; if (!create_dds_tex(params, work_tex)) { console::error("Failed creating DDS texture from crn_comp_params!"); return false; } if (!create_texture_mipmaps(work_tex, params, mipmap_params, true)) return false; crn_comp_params new_params(params); new_params.m_levels = work_tex.get_num_levels(); memset(new_params.m_pImages, 0, sizeof(new_params.m_pImages)); for (uint f = 0; f < work_tex.get_num_faces(); f++) for (uint l = 0; l < work_tex.get_num_levels(); l++) new_params.m_pImages[f][l] = (uint32*)work_tex.get_level(f, l)->get_image()->get_ptr(); return create_compressed_texture(new_params, comp_data, pActual_quality_level, pActual_bitrate); } } // namespace crnlib