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 | |