#include "evas_common_private.h" #include "evas_private.h" #ifdef ENABLE_LIBLZ4 # include # include #else # include "lz4.h" # include "lz4hc.h" #endif #include "rg_etc1.h" // FIXME: Remove DEBUG //#ifndef DEBUG //# define DEBUG //#endif #if defined(DEBUG) && defined(HAVE_CLOCK_GETTIME) && defined(_POSIX_MONOTONIC_CLOCK) # include # define DEBUG_STATS #endif static int _block_size_get(int size) { static const int MAX_BLOCK = 6; // 256 pixels int k = 0; while ((4 << k) < size) k++; k = MAX(0, k - 1); if ((size * 3 / 2) >= (4 << k)) return MAX(0, MIN(k - 1, MAX_BLOCK)); return MIN(k, MAX_BLOCK); } static inline void _alpha_to_greyscale_convert(uint32_t *data, int len) { for (int k = 0; k < len; k++) { int alpha = A_VAL(data); *data++ = ARGB_JOIN(alpha, alpha, alpha, alpha); } } static int _save_direct_tgv(RGBA_Image *im, const char *file, int compress) { // TODO: Add block by block compression. int image_width, image_height, planes = 1; uint32_t width, height; uint8_t header[8] = "TGV1"; int etc_block_size, etc_data_size, buffer_size, data_size, remain; uint8_t *buffer = NULL; uint8_t *data, *ptr; FILE *f; if (!im->cache_entry.borders.l || !im->cache_entry.borders.t || !im->cache_entry.borders.r || !im->cache_entry.borders.b) WRN("No im->cache_entry.borders on ETC image. Final image may have wrong dimensions."); image_width = im->cache_entry.w + im->cache_entry.borders.l + im->cache_entry.borders.r; image_height = im->cache_entry.h + im->cache_entry.borders.t + im->cache_entry.borders.b; data = im->image.data8; width = eina_htonl(image_width); height = eina_htonl(image_height); compress = !!compress; if ((image_width & 0x3) || (image_height & 0x3)) return 0; // header[4]: block size info, unused header[4] = 0; // header[5]: 0 for ETC1 switch (im->cache_entry.space) { case EVAS_COLORSPACE_ETC1: etc_block_size = 8; header[5] = 0; break; case EVAS_COLORSPACE_RGB8_ETC2: etc_block_size = 8; header[5] = 1; break; case EVAS_COLORSPACE_RGBA8_ETC2_EAC: etc_block_size = 16; header[5] = 2; break; case EVAS_COLORSPACE_ETC1_ALPHA: // FIXME: Properly handle premul vs. unpremul data etc_block_size = 8; header[5] = 3; planes = 2; break; default: return 0; } // header[6]: 0 for raw, 1, for LZ4 compressed, 2 for block-less mode header[6] = compress | 0x2; // header[7]: options (unused) header[7] = 0; f = fopen(file, "wb"); if (!f) return 0; // Write header if (fwrite(header, sizeof (uint8_t), 8, f) != 8) goto on_error; if (fwrite(&width, sizeof (uint32_t), 1, f) != 1) goto on_error; if (fwrite(&height, sizeof (uint32_t), 1, f) != 1) goto on_error; etc_data_size = image_width * image_height * etc_block_size * planes / 16; if (compress) { buffer_size = LZ4_compressBound(etc_data_size); buffer = malloc(buffer_size); if (!buffer) goto on_error; data_size = LZ4_compress_HC ((char *)data, (char *)buffer, etc_data_size, buffer_size, 16); } else { data_size = buffer_size = etc_data_size; buffer = data; } // Write block length header -- We keep this even in block-less mode if (data_size > 0) { unsigned int blen = data_size; while (blen) { unsigned char plen; plen = blen & 0x7F; blen = blen >> 7; if (blen) plen = 0x80 | plen; if (fwrite(&plen, 1, 1, f) != 1) goto on_error; } } // Write data ptr = buffer; remain = data_size; while (remain > 0) { int written = fwrite(ptr, 1, remain, f); if (written < 0) goto on_error; remain -= written; ptr += written; } if (compress) free(buffer); fclose(f); return 1; on_error: if (compress) free(buffer); fclose(f); return 0; } static int evas_image_save_file_tgv(RGBA_Image *im, const char *file, const char *key EINA_UNUSED, int quality, int compress, const char *encoding) { rg_etc1_pack_params param; FILE *f; uint8_t *comp = NULL; uint8_t *buffer; uint32_t *data = NULL; uint32_t nl_width, nl_height; uint8_t header[8] = "TGV1"; int block_width, block_height, macro_block_width, macro_block_height; int block_count, image_stride, image_height, etc_block_size; Evas_Colorspace cspace; Eina_Bool alpha, alpha_texture = EINA_FALSE, unpremul = EINA_FALSE; int num_planes = 1; #ifdef DEBUG_STATS struct timespec ts1, ts2; long long tsdiff, mse = 0, mse_div = 0, mse_alpha = 0, pixels_count = 0; double mean_x = 0, mean_y = 0, var_x = 0, var_y = 0, cov_xy = 0; clock_gettime(CLOCK_MONOTONIC, &ts1); #endif if (!im || !im->image.data || !file) return 0; switch (im->cache_entry.space) { case EVAS_COLORSPACE_ARGB8888: alpha = im->cache_entry.flags.alpha; break; case EVAS_COLORSPACE_ETC1: case EVAS_COLORSPACE_RGB8_ETC2: case EVAS_COLORSPACE_RGBA8_ETC2_EAC: case EVAS_COLORSPACE_ETC1_ALPHA: // Note: This case should probably never happen if (encoding) WRN("Ignoring 'encoding' argument the data is already ETC1/2"); return _save_direct_tgv(im, file, compress); default: return 0; } image_stride = im->cache_entry.w; image_height = im->cache_entry.h; nl_width = eina_htonl(image_stride); nl_height = eina_htonl(image_height); compress = !!compress; // Disable dithering, as it will deteriorate the quality of flat surfaces param.m_dithering = 0; if (quality > 95) param.m_quality = rg_etc1_high_quality; else if (quality > 30) param.m_quality = rg_etc1_medium_quality; else param.m_quality = rg_etc1_low_quality; // header[4]: 4 bit block width, 4 bit block height block_width = _block_size_get(image_stride + 2); block_height = _block_size_get(image_height + 2); header[4] = (block_height << 4) | block_width; // header[5]: 0 for ETC1, 1 for RGB8_ETC2, 2 for RGBA8_ETC2_EAC, 3 for ETC1+Alpha if (!encoding) encoding = "etc2"; if (!strcasecmp(encoding, "etc1")) { if (alpha) { ERR("ETC1 does not support alpha encoding. Abort. " "Please use 'encoding=etc1+alpha' for ETC1+Alpha encoding."); return 0; } cspace = EVAS_COLORSPACE_ETC1; etc_block_size = 8; header[5] = 0; } else if (!strcasecmp(encoding, "etc2")) { if (!alpha) { cspace = EVAS_COLORSPACE_RGB8_ETC2; etc_block_size = 8; header[5] = 1; } else { cspace = EVAS_COLORSPACE_RGBA8_ETC2_EAC; etc_block_size = 16; header[5] = 2; } } else if (!strcasecmp(encoding, "etc1+alpha")) { if (!alpha) { INF("Selected etc1+alpha but image has no alpha, reverting to etc1."); cspace = EVAS_COLORSPACE_ETC1; etc_block_size = 8; header[5] = 0; } else { // Save as two textures, and unpremul the data alpha_texture = EINA_TRUE; unpremul = EINA_TRUE; num_planes = 2; // RGB and Alpha cspace = EVAS_COLORSPACE_ETC1_ALPHA; etc_block_size = 8; header[5] = 3; } } else { ERR("Unknown encoding '%.8s' selected. Abort.", encoding); return 0; } // header[6]: 0 for raw, 1, for LZ4 compressed, 4 for unpremultiplied RGBA // blockless mode (0x2) is never used here header[6] = (compress ? 0x1 : 0x0) | (unpremul ? 0x4 : 0x0); // header[7]: unused options // Note: consider extending the header instead of filling all the bits here header[7] = 0; f = fopen(file, "wb"); if (!f) return 0; // Write header if (fwrite(header, sizeof (uint8_t), 8, f) != 8) goto on_error; if (fwrite(&nl_width, sizeof (uint32_t), 1, f) != 1) goto on_error; if (fwrite(&nl_height, sizeof (uint32_t), 1, f) != 1) goto on_error; // Real block size in pixels, obviously a multiple of 4 macro_block_width = 4 << block_width; macro_block_height = 4 << block_height; // Number of ETC1 blocks in a compressed block block_count = (macro_block_width * macro_block_height) / (4 * 4); buffer = alloca(block_count * etc_block_size); if (compress) comp = alloca(LZ4_compressBound(block_count * etc_block_size)); // Write a whole plane (RGB or Alpha) for (int plane = 0; plane < num_planes; plane++) { if (!alpha_texture) { // Normal mode data = im->image.data; } else if (!plane) { int len = image_stride * image_height; // RGB plane for ETC1+Alpha data = malloc(len * 4); if (!data) goto on_error; memcpy(data, im->image.data, len * 4); if (unpremul) evas_data_argb_unpremul(data, len); } else { // Alpha plane for ETC1+Alpha _alpha_to_greyscale_convert(data, image_stride * image_height); } // Write macro block for (int y = 0; y < image_height + 2; y += macro_block_height) { uint32_t *input, *last_col, *last_row, *last_pix; int real_y; int wlen; if (y == 0) real_y = 0; else if (y < image_height + 1) real_y = y - 1; else real_y = image_height - 1; for (int x = 0; x < image_stride + 2; x += macro_block_width) { uint8_t *offset = buffer; int real_x = x; if (x == 0) real_x = 0; else if (x < image_stride + 1) real_x = x - 1; else real_x = image_stride - 1; input = data + real_y * image_stride + real_x; last_row = data + image_stride * (image_height - 1) + real_x; last_col = data + (real_y + 1) * image_stride - 1; last_pix = data + image_height * image_stride - 1; for (int by = 0; by < macro_block_height; by += 4) { int dup_top = ((y + by) == 0) ? 1 : 0; int max_row = MAX(0, MIN(4, image_height - real_y - by)); int oy = (y == 0) ? 1 : 0; for (int bx = 0; bx < macro_block_width; bx += 4) { int dup_left = ((x + bx) == 0) ? 1 : 0; int max_col = MAX(0, MIN(4, image_stride - real_x - bx)); uint32_t todo[16] = { 0 }; int row, col; int ox = (x == 0) ? 1 : 0; if (dup_left) { // Duplicate left column for (row = 0; row < max_row; row++) todo[row * 4] = input[row * image_stride]; for (row = max_row; row < 4; row++) todo[row * 4] = last_row[0]; } if (dup_top) { // Duplicate top row for (col = 0; col < max_col; col++) todo[col] = input[MAX(col + bx - ox, 0)]; for (col = max_col; col < 4; col++) todo[col] = last_col[0]; } for (row = dup_top; row < 4; row++) { for (col = dup_left; col < max_col; col++) { if (row < max_row) { // Normal copy todo[row * 4 + col] = input[(row + by - oy) * image_stride + bx + col - ox]; } else { // Copy last line todo[row * 4 + col] = last_row[col + bx - ox]; } } for (col = max_col; col < 4; col++) { // Right edge if (row < max_row) { // Duplicate last column todo[row * 4 + col] = last_col[MAX(row + by - oy, 0) * image_stride]; } else { // Duplicate very last pixel again and again todo[row * 4 + col] = *last_pix; } } } switch (cspace) { case EVAS_COLORSPACE_ETC1: case EVAS_COLORSPACE_ETC1_ALPHA: rg_etc1_pack_block(offset, (uint32_t *) todo, ¶m); break; case EVAS_COLORSPACE_RGB8_ETC2: etc2_rgb8_block_pack(offset, (uint32_t *) todo, ¶m); break; case EVAS_COLORSPACE_RGBA8_ETC2_EAC: etc2_rgba8_block_pack(offset, (uint32_t *) todo, ¶m); break; default: goto on_error; } #ifdef DEBUG_STATS if (plane == 0) { // Decode to compute PSNR, this is slow. uint32_t done[16]; if (alpha) rg_etc2_rgba8_decode_block(offset, done); else rg_etc2_rgb8_decode_block(offset, done); for (int k = 0; k < 16; k++) { const int r = (R_VAL(&(todo[k])) - R_VAL(&(done[k]))); const int g = (G_VAL(&(todo[k])) - G_VAL(&(done[k]))); const int b = (B_VAL(&(todo[k])) - B_VAL(&(done[k]))); const int a = (A_VAL(&(todo[k])) - A_VAL(&(done[k]))); mse += r*r + g*g + b*b; /*refer http://planetmath.org/onepassalgorithmtocomputesamplevariance*/ const double delta_x = (double)todo[k] - mean_x; const double delta_y = (double)done[k] - mean_y; mean_x = mean_x + (double)(delta_x / (pixels_count + 1)); mean_y = mean_y + (double)(delta_y / (pixels_count + 1)); var_x = var_x + ((double)(todo[k] - mean_x) * delta_x); var_y = var_y + ((double)(done[k] - mean_y) * delta_y); cov_xy = cov_xy + ((double)(todo[k] - mean_x) * (double)(done[k] - mean_y)); pixels_count++; if (alpha) mse_alpha += a*a; mse_div++; } } #endif offset += etc_block_size; } } if (compress) { wlen = LZ4_compress_HC ((char *)buffer, (char *)comp, block_count * etc_block_size, LZ4_compressBound(block_count * etc_block_size), 16); } else { comp = buffer; wlen = block_count * etc_block_size; } if (wlen > 0) { unsigned int blen = wlen; while (blen) { unsigned char plen; plen = blen & 0x7F; blen = blen >> 7; if (blen) plen = 0x80 | plen; if (fwrite(&plen, 1, 1, f) != 1) goto on_error; } if (fwrite(comp, wlen, 1, f) != 1) goto on_error; } } // 4 rows } // macroblocks } // planes fclose(f); #ifdef DEBUG_STATS if (mse_div && mse) { /* Calculating dssim http://en.wikipedia.org/wiki/Structural_similarity */ double c1 = 0.01 * 255.0; double c2 = 0.03 * 255.0; double temp = (mean_x * mean_x + mean_y * mean_y + c1) * (var_x * var_x + var_y * var_y + c2); double ssim = (2 * mean_x * mean_y + c1) * ( 2 * cov_xy + c2) / temp; double dssim = (1 - ssim) / 2.0; double dmse = (double) mse / (double) (mse_div * 3.0); double psnr = 20 * log10(255.0) - 10 * log10(dmse); double dmse_alpha = (double) mse_alpha / (double) mse_div; double psnr_alpha = (dmse_alpha > 0.0) ? (20 * log10(255.0) - 10 * log10(dmse_alpha)) : 0; clock_gettime(CLOCK_MONOTONIC, &ts2); tsdiff = ((ts2.tv_sec - ts1.tv_sec) * 1000LL) + ((ts2.tv_nsec - ts1.tv_nsec) / 1000000LL); if (!alpha) { INF("ETC%d encoding stats: %dx%d, Time: %lldms, RGB PSNR: %.02fdB DSSIM: %.02f", alpha ? 2 : 1, image_stride, image_height, tsdiff, psnr, dssim); } else { INF("ETC%d encoding stats: %dx%d, Time: %lldms, RGB PSNR: %.02fdB, Alpha PSNR: %.02fdB", alpha ? 2 : 1, image_stride, image_height, tsdiff, psnr, psnr_alpha); } } #endif if (alpha_texture) free(data); return 1; on_error: if (alpha_texture) free(data); fclose(f); return 0; } static Evas_Image_Save_Func evas_image_save_tgv_func = { evas_image_save_file_tgv }; static int module_open(Evas_Module *em) { if (!em) return 0; em->functions = (void *)(&evas_image_save_tgv_func); rg_etc1_pack_block_init(); return 1; } static void module_close(Evas_Module *em EINA_UNUSED) { } static Evas_Module_Api evas_modapi = { EVAS_MODULE_API_VERSION, "tgv", "none", { module_open, module_close } }; EVAS_MODULE_DEFINE(EVAS_MODULE_TYPE_IMAGE_SAVER, image_saver, tgv); #ifndef EVAS_STATIC_BUILD_TGV EVAS_EINA_MODULE_DEFINE(image_saver, tgv); #endif