1/**************************************************************************/
2/* image_compress_astcenc.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_astcenc.h"
32
33#include "core/os/os.h"
34#include "core/string/print_string.h"
35
36#include <astcenc.h>
37
38void _compress_astc(Image *r_img, Image::ASTCFormat p_format) {
39 uint64_t start_time = OS::get_singleton()->get_ticks_msec();
40
41 // TODO: See how to handle lossy quality.
42
43 Image::Format img_format = r_img->get_format();
44 if (img_format >= Image::FORMAT_DXT1) {
45 return; // Do not compress, already compressed.
46 }
47
48 bool is_hdr = false;
49 if ((img_format >= Image::FORMAT_RH) && (img_format <= Image::FORMAT_RGBE9995)) {
50 is_hdr = true;
51 r_img->convert(Image::FORMAT_RGBAF);
52 } else {
53 r_img->convert(Image::FORMAT_RGBA8);
54 }
55
56 // Determine encoder output format from our enum.
57
58 Image::Format target_format = Image::FORMAT_RGBA8;
59 astcenc_profile profile = ASTCENC_PRF_LDR;
60 unsigned int block_x = 4;
61 unsigned int block_y = 4;
62
63 if (p_format == Image::ASTCFormat::ASTC_FORMAT_4x4) {
64 if (is_hdr) {
65 target_format = Image::FORMAT_ASTC_4x4_HDR;
66 profile = ASTCENC_PRF_HDR;
67 } else {
68 target_format = Image::FORMAT_ASTC_4x4;
69 }
70 } else if (p_format == Image::ASTCFormat::ASTC_FORMAT_8x8) {
71 if (is_hdr) {
72 target_format = Image::FORMAT_ASTC_8x8_HDR;
73 profile = ASTCENC_PRF_HDR;
74 } else {
75 target_format = Image::FORMAT_ASTC_8x8;
76 }
77 block_x = 8;
78 block_y = 8;
79 }
80
81 // Compress image data and (if required) mipmaps.
82
83 const bool mipmaps = r_img->has_mipmaps();
84 int width = r_img->get_width();
85 int height = r_img->get_height();
86 int required_width = (width % block_x) != 0 ? width + (block_x - (width % block_x)) : width;
87 int required_height = (height % block_y) != 0 ? height + (block_y - (height % block_y)) : height;
88
89 if (width != required_width || height != required_height) {
90 // Resize texture to fit block size.
91 r_img->resize(required_width, required_height);
92 width = required_width;
93 height = required_height;
94 }
95
96 print_verbose(vformat("astcenc: Encoding image size %dx%d to format %s%s.", width, height, Image::get_format_name(target_format), mipmaps ? ", with mipmaps" : ""));
97
98 // Initialize astcenc.
99
100 int dest_size = Image::get_image_data_size(width, height, target_format, mipmaps);
101 Vector<uint8_t> dest_data;
102 dest_data.resize(dest_size);
103 uint8_t *dest_write = dest_data.ptrw();
104
105 astcenc_config config;
106 config.block_x = block_x;
107 config.block_y = block_y;
108 config.profile = profile;
109
110 const float quality = ASTCENC_PRE_MEDIUM;
111 astcenc_error status = astcenc_config_init(profile, block_x, block_y, 1, quality, 0, &config);
112 ERR_FAIL_COND_MSG(status != ASTCENC_SUCCESS,
113 vformat("astcenc: Configuration initialization failed: %s.", astcenc_get_error_string(status)));
114
115 // Context allocation.
116
117 astcenc_context *context;
118 const unsigned int thread_count = 1; // Godot compresses multiple images each on a thread, which is more efficient for large amount of images imported.
119 status = astcenc_context_alloc(&config, thread_count, &context);
120 ERR_FAIL_COND_MSG(status != ASTCENC_SUCCESS,
121 vformat("astcenc: Context allocation failed: %s.", astcenc_get_error_string(status)));
122
123 Vector<uint8_t> image_data = r_img->get_data();
124
125 int mip_count = mipmaps ? Image::get_image_required_mipmaps(width, height, target_format) : 0;
126 for (int i = 0; i < mip_count + 1; i++) {
127 int src_mip_w, src_mip_h;
128 int src_ofs = Image::get_image_mipmap_offset_and_dimensions(width, height, r_img->get_format(), i, src_mip_w, src_mip_h);
129
130 const uint8_t *slices = &image_data.ptr()[src_ofs];
131
132 int dst_mip_w, dst_mip_h;
133 int dst_ofs = Image::get_image_mipmap_offset_and_dimensions(width, height, target_format, i, dst_mip_w, dst_mip_h);
134 // Ensure that mip offset is a multiple of 8 (etcpak expects uint64_t pointer).
135 ERR_FAIL_COND(dst_ofs % 8 != 0);
136 uint8_t *dest_mip_write = (uint8_t *)&dest_write[dst_ofs];
137
138 // Compress image.
139
140 astcenc_image image;
141 image.dim_x = src_mip_w;
142 image.dim_y = src_mip_h;
143 image.dim_z = 1;
144 image.data_type = ASTCENC_TYPE_U8;
145 if (is_hdr) {
146 image.data_type = ASTCENC_TYPE_F32;
147 }
148 image.data = (void **)(&slices);
149
150 // Compute the number of ASTC blocks in each dimension.
151 unsigned int block_count_x = (src_mip_w + block_x - 1) / block_x;
152 unsigned int block_count_y = (src_mip_h + block_y - 1) / block_y;
153 size_t comp_len = block_count_x * block_count_y * 16;
154
155 const astcenc_swizzle swizzle = {
156 ASTCENC_SWZ_R, ASTCENC_SWZ_G, ASTCENC_SWZ_B, ASTCENC_SWZ_A
157 };
158
159 status = astcenc_compress_image(context, &image, &swizzle, dest_mip_write, comp_len, 0);
160
161 ERR_BREAK_MSG(status != ASTCENC_SUCCESS,
162 vformat("astcenc: ASTC image compression failed: %s.", astcenc_get_error_string(status)));
163 astcenc_compress_reset(context);
164 }
165
166 astcenc_context_free(context);
167
168 // Replace original image with compressed one.
169
170 r_img->set_data(width, height, mipmaps, target_format, dest_data);
171
172 print_verbose(vformat("astcenc: Encoding took %s ms.", rtos(OS::get_singleton()->get_ticks_msec() - start_time)));
173}
174
175void _decompress_astc(Image *r_img) {
176 uint64_t start_time = OS::get_singleton()->get_ticks_msec();
177
178 // Determine decompression parameters from image format.
179
180 Image::Format img_format = r_img->get_format();
181 bool is_hdr = false;
182 unsigned int block_x = 0;
183 unsigned int block_y = 0;
184 if (img_format == Image::FORMAT_ASTC_4x4) {
185 block_x = 4;
186 block_y = 4;
187 is_hdr = false;
188 } else if (img_format == Image::FORMAT_ASTC_4x4_HDR) {
189 block_x = 4;
190 block_y = 4;
191 is_hdr = true;
192 } else if (img_format == Image::FORMAT_ASTC_8x8) {
193 block_x = 8;
194 block_y = 8;
195 is_hdr = false;
196 } else if (img_format == Image::FORMAT_ASTC_8x8_HDR) {
197 block_x = 8;
198 block_y = 8;
199 is_hdr = true;
200 } else {
201 ERR_FAIL_MSG("astcenc: Cannot decompress Image with a non-ASTC format.");
202 }
203
204 // Initialize astcenc.
205
206 astcenc_profile profile = ASTCENC_PRF_LDR;
207 if (is_hdr) {
208 profile = ASTCENC_PRF_HDR;
209 }
210 astcenc_config config;
211 const float quality = ASTCENC_PRE_MEDIUM;
212
213 astcenc_error status = astcenc_config_init(profile, block_x, block_y, 1, quality, 0, &config);
214 ERR_FAIL_COND_MSG(status != ASTCENC_SUCCESS,
215 vformat("astcenc: Configuration initialization failed: %s.", astcenc_get_error_string(status)));
216
217 // Context allocation.
218
219 astcenc_context *context = nullptr;
220 const unsigned int thread_count = 1;
221
222 status = astcenc_context_alloc(&config, thread_count, &context);
223 ERR_FAIL_COND_MSG(status != ASTCENC_SUCCESS,
224 vformat("astcenc: Context allocation failed: %s.", astcenc_get_error_string(status)));
225
226 Image::Format target_format = is_hdr ? Image::FORMAT_RGBAF : Image::FORMAT_RGBA8;
227
228 const bool mipmaps = r_img->has_mipmaps();
229 int width = r_img->get_width();
230 int height = r_img->get_height();
231 int dest_size = Image::get_image_data_size(width, height, target_format, mipmaps);
232 Vector<uint8_t> dest_data;
233 dest_data.resize(dest_size);
234 uint8_t *dest_write = dest_data.ptrw();
235
236 // Decompress image.
237
238 Vector<uint8_t> image_data = r_img->get_data();
239 int mip_count = mipmaps ? Image::get_image_required_mipmaps(width, height, target_format) : 0;
240
241 for (int i = 0; i < mip_count + 1; i++) {
242 int src_mip_w, src_mip_h;
243
244 int src_ofs = Image::get_image_mipmap_offset_and_dimensions(width, height, r_img->get_format(), i, src_mip_w, src_mip_h);
245 const uint8_t *src_data = &image_data.ptr()[src_ofs];
246 int src_size;
247 if (i == mip_count) {
248 src_size = image_data.size() - src_ofs;
249 } else {
250 int auxw, auxh;
251 src_size = Image::get_image_mipmap_offset_and_dimensions(width, height, r_img->get_format(), i + 1, auxw, auxh) - src_ofs;
252 }
253
254 int dst_mip_w, dst_mip_h;
255 int dst_ofs = Image::get_image_mipmap_offset_and_dimensions(width, height, target_format, i, dst_mip_w, dst_mip_h);
256 // Ensure that mip offset is a multiple of 8 (etcpak expects uint64_t pointer).
257 ERR_FAIL_COND(dst_ofs % 8 != 0);
258 uint8_t *dest_mip_write = (uint8_t *)&dest_write[dst_ofs];
259
260 astcenc_image image;
261 image.dim_x = dst_mip_w;
262 image.dim_y = dst_mip_h;
263 image.dim_z = 1;
264 image.data_type = ASTCENC_TYPE_U8;
265 if (is_hdr) {
266 target_format = Image::FORMAT_RGBAF;
267 image.data_type = ASTCENC_TYPE_F32;
268 }
269
270 image.data = (void **)(&dest_mip_write);
271
272 const astcenc_swizzle swizzle = {
273 ASTCENC_SWZ_R, ASTCENC_SWZ_G, ASTCENC_SWZ_B, ASTCENC_SWZ_A
274 };
275
276 status = astcenc_decompress_image(context, src_data, src_size, &image, &swizzle, 0);
277 ERR_BREAK_MSG(status != ASTCENC_SUCCESS,
278 vformat("astcenc: ASTC decompression failed: %s.", astcenc_get_error_string(status)));
279 ERR_BREAK_MSG(image.dim_z > 1,
280 "astcenc: ASTC decompression failed because this is a 3D texture, which is not supported.");
281 astcenc_compress_reset(context);
282 }
283 astcenc_context_free(context);
284
285 // Replace original image with compressed one.
286
287 r_img->set_data(width, height, mipmaps, target_format, dest_data);
288
289 print_verbose(vformat("astcenc: Decompression took %s ms.", rtos(OS::get_singleton()->get_ticks_msec() - start_time)));
290}
291