| 1 | /**************************************************************************/ |
| 2 | /* image_compress_etcpak.cpp */ |
| 3 | /**************************************************************************/ |
| 4 | /* This file is part of: */ |
| 5 | /* GODOT ENGINE */ |
| 6 | /* https://godotengine.org */ |
| 7 | /**************************************************************************/ |
| 8 | /* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ |
| 9 | /* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ |
| 10 | /* */ |
| 11 | /* Permission is hereby granted, free of charge, to any person obtaining */ |
| 12 | /* a copy of this software and associated documentation files (the */ |
| 13 | /* "Software"), to deal in the Software without restriction, including */ |
| 14 | /* without limitation the rights to use, copy, modify, merge, publish, */ |
| 15 | /* distribute, sublicense, and/or sell copies of the Software, and to */ |
| 16 | /* permit persons to whom the Software is furnished to do so, subject to */ |
| 17 | /* the following conditions: */ |
| 18 | /* */ |
| 19 | /* The above copyright notice and this permission notice shall be */ |
| 20 | /* included in all copies or substantial portions of the Software. */ |
| 21 | /* */ |
| 22 | /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ |
| 23 | /* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ |
| 24 | /* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ |
| 25 | /* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ |
| 26 | /* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ |
| 27 | /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ |
| 28 | /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ |
| 29 | /**************************************************************************/ |
| 30 | |
| 31 | #include "image_compress_etcpak.h" |
| 32 | |
| 33 | #include "core/os/os.h" |
| 34 | #include "core/string/print_string.h" |
| 35 | |
| 36 | #include <ProcessDxtc.hpp> |
| 37 | #include <ProcessRGB.hpp> |
| 38 | |
| 39 | EtcpakType _determine_etc_type(Image::UsedChannels p_channels) { |
| 40 | switch (p_channels) { |
| 41 | case Image::USED_CHANNELS_L: |
| 42 | return EtcpakType::ETCPAK_TYPE_ETC1; |
| 43 | case Image::USED_CHANNELS_LA: |
| 44 | return EtcpakType::ETCPAK_TYPE_ETC2_ALPHA; |
| 45 | case Image::USED_CHANNELS_R: |
| 46 | return EtcpakType::ETCPAK_TYPE_ETC2; |
| 47 | case Image::USED_CHANNELS_RG: |
| 48 | return EtcpakType::ETCPAK_TYPE_ETC2_RA_AS_RG; |
| 49 | case Image::USED_CHANNELS_RGB: |
| 50 | return EtcpakType::ETCPAK_TYPE_ETC2; |
| 51 | case Image::USED_CHANNELS_RGBA: |
| 52 | return EtcpakType::ETCPAK_TYPE_ETC2_ALPHA; |
| 53 | default: |
| 54 | return EtcpakType::ETCPAK_TYPE_ETC2_ALPHA; |
| 55 | } |
| 56 | } |
| 57 | |
| 58 | EtcpakType _determine_dxt_type(Image::UsedChannels p_channels) { |
| 59 | switch (p_channels) { |
| 60 | case Image::USED_CHANNELS_L: |
| 61 | return EtcpakType::ETCPAK_TYPE_DXT1; |
| 62 | case Image::USED_CHANNELS_LA: |
| 63 | return EtcpakType::ETCPAK_TYPE_DXT5; |
| 64 | case Image::USED_CHANNELS_R: |
| 65 | return EtcpakType::ETCPAK_TYPE_DXT5; |
| 66 | case Image::USED_CHANNELS_RG: |
| 67 | return EtcpakType::ETCPAK_TYPE_DXT5_RA_AS_RG; |
| 68 | case Image::USED_CHANNELS_RGB: |
| 69 | return EtcpakType::ETCPAK_TYPE_DXT1; |
| 70 | case Image::USED_CHANNELS_RGBA: |
| 71 | return EtcpakType::ETCPAK_TYPE_DXT5; |
| 72 | default: |
| 73 | return EtcpakType::ETCPAK_TYPE_DXT5; |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | void _compress_etc1(Image *r_img) { |
| 78 | _compress_etcpak(EtcpakType::ETCPAK_TYPE_ETC1, r_img); |
| 79 | } |
| 80 | |
| 81 | void _compress_etc2(Image *r_img, Image::UsedChannels p_channels) { |
| 82 | EtcpakType type = _determine_etc_type(p_channels); |
| 83 | _compress_etcpak(type, r_img); |
| 84 | } |
| 85 | |
| 86 | void _compress_bc(Image *r_img, Image::UsedChannels p_channels) { |
| 87 | EtcpakType type = _determine_dxt_type(p_channels); |
| 88 | _compress_etcpak(type, r_img); |
| 89 | } |
| 90 | |
| 91 | void _compress_etcpak(EtcpakType p_compresstype, Image *r_img) { |
| 92 | uint64_t start_time = OS::get_singleton()->get_ticks_msec(); |
| 93 | |
| 94 | Image::Format img_format = r_img->get_format(); |
| 95 | if (img_format >= Image::FORMAT_DXT1) { |
| 96 | return; // Do not compress, already compressed. |
| 97 | } |
| 98 | if (img_format > Image::FORMAT_RGBA8) { |
| 99 | // TODO: we should be able to handle FORMAT_RGBA4444 and FORMAT_RGBA5551 eventually |
| 100 | return; |
| 101 | } |
| 102 | |
| 103 | // Use RGBA8 to convert. |
| 104 | if (img_format != Image::FORMAT_RGBA8) { |
| 105 | r_img->convert(Image::FORMAT_RGBA8); |
| 106 | } |
| 107 | |
| 108 | // Determine output format based on Etcpak type. |
| 109 | Image::Format target_format = Image::FORMAT_RGBA8; |
| 110 | if (p_compresstype == EtcpakType::ETCPAK_TYPE_ETC1) { |
| 111 | target_format = Image::FORMAT_ETC; |
| 112 | r_img->convert_rgba8_to_bgra8(); // It's badly documented but ETCPAK seems to be expected BGRA8 for ETC. |
| 113 | } else if (p_compresstype == EtcpakType::ETCPAK_TYPE_ETC2) { |
| 114 | target_format = Image::FORMAT_ETC2_RGB8; |
| 115 | r_img->convert_rgba8_to_bgra8(); // It's badly documented but ETCPAK seems to be expected BGRA8 for ETC. |
| 116 | } else if (p_compresstype == EtcpakType::ETCPAK_TYPE_ETC2_RA_AS_RG) { |
| 117 | target_format = Image::FORMAT_ETC2_RA_AS_RG; |
| 118 | r_img->convert_rg_to_ra_rgba8(); |
| 119 | r_img->convert_rgba8_to_bgra8(); // It's badly documented but ETCPAK seems to be expected BGRA8 for ETC. |
| 120 | } else if (p_compresstype == EtcpakType::ETCPAK_TYPE_ETC2_ALPHA) { |
| 121 | target_format = Image::FORMAT_ETC2_RGBA8; |
| 122 | r_img->convert_rgba8_to_bgra8(); // It's badly documented but ETCPAK seems to be expected BGRA8 for ETC. |
| 123 | } else if (p_compresstype == EtcpakType::ETCPAK_TYPE_DXT1) { |
| 124 | target_format = Image::FORMAT_DXT1; |
| 125 | } else if (p_compresstype == EtcpakType::ETCPAK_TYPE_DXT5_RA_AS_RG) { |
| 126 | target_format = Image::FORMAT_DXT5_RA_AS_RG; |
| 127 | r_img->convert_rg_to_ra_rgba8(); |
| 128 | } else if (p_compresstype == EtcpakType::ETCPAK_TYPE_DXT5) { |
| 129 | target_format = Image::FORMAT_DXT5; |
| 130 | } else { |
| 131 | ERR_FAIL_MSG("Invalid or unsupported etcpak compression format, not ETC or DXT." ); |
| 132 | } |
| 133 | |
| 134 | // Compress image data and (if required) mipmaps. |
| 135 | |
| 136 | const bool mipmaps = r_img->has_mipmaps(); |
| 137 | int width = r_img->get_width(); |
| 138 | int height = r_img->get_height(); |
| 139 | |
| 140 | /* |
| 141 | The first mipmap level of a compressed texture must be a multiple of 4. Quote from D3D11.3 spec: |
| 142 | |
| 143 | BC format surfaces are always multiples of full blocks, each block representing 4x4 pixels. |
| 144 | For mipmaps, the top level map is required to be a multiple of 4 size in all dimensions. |
| 145 | The sizes for the lower level maps are computed as they are for all mipmapped surfaces, |
| 146 | and thus may not be a multiple of 4, for example a top level map of 20 results in a second level |
| 147 | map size of 10. For these cases, there is a differing 'physical' size and a 'virtual' size. |
| 148 | The virtual size is that computed for each mip level without adjustment, which is 10 for the example. |
| 149 | The physical size is the virtual size rounded up to the next multiple of 4, which is 12 for the example, |
| 150 | and this represents the actual memory size. The sampling hardware will apply texture address |
| 151 | processing based on the virtual size (using, for example, border color if specified for accesses |
| 152 | beyond 10), and thus for the example case will not access the 11th and 12th row of the resource. |
| 153 | So for mipmap chains when an axis becomes < 4 in size, only texels 'a','b','e','f' |
| 154 | are used for a 2x2 map, and texel 'a' is used for 1x1. Note that this is similar to, but distinct from, |
| 155 | the surface pitch, which can encompass additional padding beyond the physical surface size. |
| 156 | */ |
| 157 | int next_width = width <= 2 ? width : (width + 3) & ~3; |
| 158 | int next_height = height <= 2 ? height : (height + 3) & ~3; |
| 159 | if (next_width != width || next_height != height) { |
| 160 | r_img->resize(next_width, next_height, Image::INTERPOLATE_LANCZOS); |
| 161 | width = r_img->get_width(); |
| 162 | height = r_img->get_height(); |
| 163 | } |
| 164 | // ERR_FAIL_COND(width % 4 != 0 || height % 4 != 0); // FIXME: No longer guaranteed. |
| 165 | // Multiple-of-4 should be guaranteed by above. |
| 166 | // However, power-of-two 3d textures will create Nx2 and Nx1 mipmap levels, |
| 167 | // which are individually compressed Image objects that violate the above rule. |
| 168 | // Hence, we allow Nx1 and Nx2 images through without forcing to multiple-of-4. |
| 169 | |
| 170 | const uint8_t *src_read = r_img->get_data().ptr(); |
| 171 | |
| 172 | print_verbose(vformat("etcpak: Encoding image size %dx%d to format %s%s." , width, height, Image::get_format_name(target_format), mipmaps ? ", with mipmaps" : "" )); |
| 173 | |
| 174 | int dest_size = Image::get_image_data_size(width, height, target_format, mipmaps); |
| 175 | Vector<uint8_t> dest_data; |
| 176 | dest_data.resize(dest_size); |
| 177 | uint8_t *dest_write = dest_data.ptrw(); |
| 178 | |
| 179 | int mip_count = mipmaps ? Image::get_image_required_mipmaps(width, height, target_format) : 0; |
| 180 | Vector<uint32_t> padded_src; |
| 181 | |
| 182 | for (int i = 0; i < mip_count + 1; i++) { |
| 183 | // Get write mip metrics for target image. |
| 184 | int orig_mip_w, orig_mip_h; |
| 185 | int mip_ofs = Image::get_image_mipmap_offset_and_dimensions(width, height, target_format, i, orig_mip_w, orig_mip_h); |
| 186 | // Ensure that mip offset is a multiple of 8 (etcpak expects uint64_t pointer). |
| 187 | ERR_FAIL_COND(mip_ofs % 8 != 0); |
| 188 | uint64_t *dest_mip_write = (uint64_t *)&dest_write[mip_ofs]; |
| 189 | |
| 190 | // Block size. Align stride to multiple of 4 (RGBA8). |
| 191 | int mip_w = (orig_mip_w + 3) & ~3; |
| 192 | int mip_h = (orig_mip_h + 3) & ~3; |
| 193 | const uint32_t blocks = mip_w * mip_h / 16; |
| 194 | |
| 195 | // Get mip data from source image for reading. |
| 196 | int src_mip_ofs = r_img->get_mipmap_offset(i); |
| 197 | const uint32_t *src_mip_read = (const uint32_t *)&src_read[src_mip_ofs]; |
| 198 | |
| 199 | // Pad textures to nearest block by smearing. |
| 200 | if (mip_w != orig_mip_w || mip_h != orig_mip_h) { |
| 201 | padded_src.resize(mip_w * mip_h); |
| 202 | uint32_t *ptrw = padded_src.ptrw(); |
| 203 | int x = 0, y = 0; |
| 204 | for (y = 0; y < orig_mip_h; y++) { |
| 205 | for (x = 0; x < orig_mip_w; x++) { |
| 206 | ptrw[mip_w * y + x] = src_mip_read[orig_mip_w * y + x]; |
| 207 | } |
| 208 | // First, smear in x. |
| 209 | for (; x < mip_w; x++) { |
| 210 | ptrw[mip_w * y + x] = ptrw[mip_w * y + x - 1]; |
| 211 | } |
| 212 | } |
| 213 | // Then, smear in y. |
| 214 | for (; y < mip_h; y++) { |
| 215 | for (x = 0; x < mip_w; x++) { |
| 216 | ptrw[mip_w * y + x] = ptrw[mip_w * y + x - mip_w]; |
| 217 | } |
| 218 | } |
| 219 | // Override the src_mip_read pointer to our temporary Vector. |
| 220 | src_mip_read = padded_src.ptr(); |
| 221 | } |
| 222 | if (p_compresstype == EtcpakType::ETCPAK_TYPE_ETC1) { |
| 223 | CompressEtc1RgbDither(src_mip_read, dest_mip_write, blocks, mip_w); |
| 224 | } else if (p_compresstype == EtcpakType::ETCPAK_TYPE_ETC2) { |
| 225 | CompressEtc2Rgb(src_mip_read, dest_mip_write, blocks, mip_w, true); |
| 226 | } else if (p_compresstype == EtcpakType::ETCPAK_TYPE_ETC2_ALPHA || p_compresstype == EtcpakType::ETCPAK_TYPE_ETC2_RA_AS_RG) { |
| 227 | CompressEtc2Rgba(src_mip_read, dest_mip_write, blocks, mip_w, true); |
| 228 | } else if (p_compresstype == EtcpakType::ETCPAK_TYPE_DXT1) { |
| 229 | CompressDxt1Dither(src_mip_read, dest_mip_write, blocks, mip_w); |
| 230 | } else if (p_compresstype == EtcpakType::ETCPAK_TYPE_DXT5 || p_compresstype == EtcpakType::ETCPAK_TYPE_DXT5_RA_AS_RG) { |
| 231 | CompressDxt5(src_mip_read, dest_mip_write, blocks, mip_w); |
| 232 | } else { |
| 233 | ERR_FAIL_MSG("etcpak: Invalid or unsupported compression format." ); |
| 234 | } |
| 235 | } |
| 236 | |
| 237 | // Replace original image with compressed one. |
| 238 | r_img->set_data(width, height, mipmaps, target_format, dest_data); |
| 239 | |
| 240 | print_verbose(vformat("etcpak: Encoding took %s ms." , rtos(OS::get_singleton()->get_ticks_msec() - start_time))); |
| 241 | } |
| 242 | |