efl/src/modules/evas/image_savers/tgv/evas_image_save_tgv.c

588 lines
20 KiB
C

#include "evas_common_private.h"
#include "evas_private.h"
#ifdef ENABLE_LIBLZ4
# include <lz4.h>
# include <lz4hc.h>
#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 <time.h>
# 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, &param);
break;
case EVAS_COLORSPACE_RGB8_ETC2:
etc2_rgb8_block_pack(offset, (uint32_t *) todo, &param);
break;
case EVAS_COLORSPACE_RGBA8_ETC2_EAC:
etc2_rgba8_block_pack(offset, (uint32_t *) todo, &param);
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