2023-01-05 13:25:55 +01:00
/**************************************************************************/
/* image_compress_etcpak.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
2021-04-07 07:05:56 +02:00
2021-04-15 14:02:45 +02:00
# include "image_compress_etcpak.h"
2021-04-07 07:05:56 +02:00
# include "core/os/os.h"
# include "core/string/print_string.h"
2022-12-20 19:54:01 +01:00
# include <ProcessDxtc.hpp>
# include <ProcessRGB.hpp>
2021-04-07 07:05:56 +02:00
2021-04-15 14:02:45 +02:00
EtcpakType _determine_etc_type ( Image : : UsedChannels p_channels ) {
switch ( p_channels ) {
2021-04-07 07:05:56 +02:00
case Image : : USED_CHANNELS_L :
return EtcpakType : : ETCPAK_TYPE_ETC1 ;
case Image : : USED_CHANNELS_LA :
return EtcpakType : : ETCPAK_TYPE_ETC2_ALPHA ;
case Image : : USED_CHANNELS_R :
return EtcpakType : : ETCPAK_TYPE_ETC2 ;
case Image : : USED_CHANNELS_RG :
return EtcpakType : : ETCPAK_TYPE_ETC2_RA_AS_RG ;
case Image : : USED_CHANNELS_RGB :
return EtcpakType : : ETCPAK_TYPE_ETC2 ;
case Image : : USED_CHANNELS_RGBA :
return EtcpakType : : ETCPAK_TYPE_ETC2_ALPHA ;
default :
return EtcpakType : : ETCPAK_TYPE_ETC2_ALPHA ;
}
}
2021-04-15 14:02:45 +02:00
EtcpakType _determine_dxt_type ( Image : : UsedChannels p_channels ) {
switch ( p_channels ) {
2021-04-07 07:05:56 +02:00
case Image : : USED_CHANNELS_L :
return EtcpakType : : ETCPAK_TYPE_DXT1 ;
case Image : : USED_CHANNELS_LA :
return EtcpakType : : ETCPAK_TYPE_DXT5 ;
case Image : : USED_CHANNELS_R :
return EtcpakType : : ETCPAK_TYPE_DXT5 ;
case Image : : USED_CHANNELS_RG :
return EtcpakType : : ETCPAK_TYPE_DXT5_RA_AS_RG ;
case Image : : USED_CHANNELS_RGB :
return EtcpakType : : ETCPAK_TYPE_DXT5 ;
case Image : : USED_CHANNELS_RGBA :
return EtcpakType : : ETCPAK_TYPE_DXT5 ;
default :
return EtcpakType : : ETCPAK_TYPE_DXT5 ;
}
}
2021-04-15 14:02:45 +02:00
void _compress_etc1 ( Image * r_img , float p_lossy_quality ) {
_compress_etcpak ( EtcpakType : : ETCPAK_TYPE_ETC1 , r_img , p_lossy_quality ) ;
2021-04-07 07:05:56 +02:00
}
2021-04-15 14:02:45 +02:00
void _compress_etc2 ( Image * r_img , float p_lossy_quality , Image : : UsedChannels p_channels ) {
EtcpakType type = _determine_etc_type ( p_channels ) ;
_compress_etcpak ( type , r_img , p_lossy_quality ) ;
2021-04-07 07:05:56 +02:00
}
2021-04-15 14:02:45 +02:00
void _compress_bc ( Image * r_img , float p_lossy_quality , Image : : UsedChannels p_channels ) {
EtcpakType type = _determine_dxt_type ( p_channels ) ;
_compress_etcpak ( type , r_img , p_lossy_quality ) ;
2021-04-07 07:05:56 +02:00
}
2021-04-15 14:02:45 +02:00
void _compress_etcpak ( EtcpakType p_compresstype , Image * r_img , float p_lossy_quality ) {
uint64_t start_time = OS : : get_singleton ( ) - > get_ticks_msec ( ) ;
2021-04-07 07:05:56 +02:00
2021-04-15 14:02:45 +02:00
// TODO: See how to handle lossy quality.
Image : : Format img_format = r_img - > get_format ( ) ;
2021-04-07 07:05:56 +02:00
if ( img_format > = Image : : FORMAT_DXT1 ) {
2021-04-15 14:02:45 +02:00
return ; // Do not compress, already compressed.
2021-04-07 07:05:56 +02:00
}
if ( img_format > Image : : FORMAT_RGBA8 ) {
// TODO: we should be able to handle FORMAT_RGBA4444 and FORMAT_RGBA5551 eventually
return ;
}
2021-04-15 14:02:45 +02:00
// Use RGBA8 to convert.
if ( img_format ! = Image : : FORMAT_RGBA8 ) {
r_img - > convert ( Image : : FORMAT_RGBA8 ) ;
2021-04-07 07:05:56 +02:00
}
2021-04-15 14:02:45 +02:00
// Determine output format based on Etcpak type.
Image : : Format target_format = Image : : FORMAT_RGBA8 ;
if ( p_compresstype = = EtcpakType : : ETCPAK_TYPE_ETC1 ) {
target_format = Image : : FORMAT_ETC ;
2022-12-01 15:34:05 +01:00
r_img - > convert_rgba8_to_bgra8 ( ) ; // It's badly documented but ETCPAK seems to be expected BGRA8 for ETC.
2021-04-07 07:05:56 +02:00
} else if ( p_compresstype = = EtcpakType : : ETCPAK_TYPE_ETC2 ) {
2021-04-15 14:02:45 +02:00
target_format = Image : : FORMAT_ETC2_RGB8 ;
2022-12-01 15:34:05 +01:00
r_img - > convert_rgba8_to_bgra8 ( ) ; // It's badly documented but ETCPAK seems to be expected BGRA8 for ETC.
2021-04-07 07:05:56 +02:00
} else if ( p_compresstype = = EtcpakType : : ETCPAK_TYPE_ETC2_RA_AS_RG ) {
2021-04-15 14:02:45 +02:00
target_format = Image : : FORMAT_ETC2_RA_AS_RG ;
r_img - > convert_rg_to_ra_rgba8 ( ) ;
2023-01-11 23:03:07 +01:00
r_img - > convert_rgba8_to_bgra8 ( ) ; // It's badly documented but ETCPAK seems to be expected BGRA8 for ETC.
2021-04-07 07:05:56 +02:00
} else if ( p_compresstype = = EtcpakType : : ETCPAK_TYPE_ETC2_ALPHA ) {
2021-04-15 14:02:45 +02:00
target_format = Image : : FORMAT_ETC2_RGBA8 ;
2022-12-01 15:34:05 +01:00
r_img - > convert_rgba8_to_bgra8 ( ) ; // It's badly documented but ETCPAK seems to be expected BGRA8 for ETC.
2021-04-07 07:05:56 +02:00
} else if ( p_compresstype = = EtcpakType : : ETCPAK_TYPE_DXT1 ) {
2021-04-15 14:02:45 +02:00
target_format = Image : : FORMAT_DXT1 ;
2021-04-07 07:05:56 +02:00
} else if ( p_compresstype = = EtcpakType : : ETCPAK_TYPE_DXT5_RA_AS_RG ) {
2021-04-15 14:02:45 +02:00
target_format = Image : : FORMAT_DXT5_RA_AS_RG ;
r_img - > convert_rg_to_ra_rgba8 ( ) ;
2021-04-07 07:05:56 +02:00
} else if ( p_compresstype = = EtcpakType : : ETCPAK_TYPE_DXT5 ) {
2021-04-15 14:02:45 +02:00
target_format = Image : : FORMAT_DXT5 ;
2021-04-07 07:05:56 +02:00
} else {
2022-12-20 19:54:01 +01:00
ERR_FAIL_MSG ( " Invalid or unsupported etcpak compression format, not ETC or DXT. " ) ;
2021-04-07 07:05:56 +02:00
}
2021-04-15 14:02:45 +02:00
// Compress image data and (if required) mipmaps.
const bool mipmaps = r_img - > has_mipmaps ( ) ;
2022-01-19 05:29:39 +01:00
int width = r_img - > get_width ( ) ;
int height = r_img - > get_height ( ) ;
/*
The first mipmap level of a compressed texture must be a multiple of 4. Quote from D3D11 .3 spec :
BC format surfaces are always multiples of full blocks , each block representing 4 x4 pixels .
For mipmaps , the top level map is required to be a multiple of 4 size in all dimensions .
The sizes for the lower level maps are computed as they are for all mipmapped surfaces ,
and thus may not be a multiple of 4 , for example a top level map of 20 results in a second level
map size of 10. For these cases , there is a differing ' physical ' size and a ' virtual ' size .
The virtual size is that computed for each mip level without adjustment , which is 10 for the example .
The physical size is the virtual size rounded up to the next multiple of 4 , which is 12 for the example ,
and this represents the actual memory size . The sampling hardware will apply texture address
processing based on the virtual size ( using , for example , border color if specified for accesses
beyond 10 ) , and thus for the example case will not access the 11 th and 12 th row of the resource .
So for mipmap chains when an axis becomes < 4 in size , only texels ' a ' , ' b ' , ' e ' , ' f '
are used for a 2 x2 map , and texel ' a ' is used for 1 x1 . Note that this is similar to , but distinct from ,
the surface pitch , which can encompass additional padding beyond the physical surface size .
*/
2022-01-23 20:32:05 +01:00
int next_width = width < = 2 ? width : ( width + 3 ) & ~ 3 ;
int next_height = height < = 2 ? height : ( height + 3 ) & ~ 3 ;
2022-01-19 05:29:39 +01:00
if ( next_width ! = width | | next_height ! = height ) {
r_img - > resize ( next_width , next_height , Image : : INTERPOLATE_LANCZOS ) ;
width = r_img - > get_width ( ) ;
height = r_img - > get_height ( ) ;
}
2022-01-23 20:32:05 +01:00
// ERR_FAIL_COND(width % 4 != 0 || height % 4 != 0); // FIXME: No longer guaranteed.
// Multiple-of-4 should be guaranteed by above.
// However, power-of-two 3d textures will create Nx2 and Nx1 mipmap levels,
// which are individually compressed Image objects that violate the above rule.
// Hence, we allow Nx1 and Nx2 images through without forcing to multiple-of-4.
2022-01-19 05:29:39 +01:00
2021-04-15 14:02:45 +02:00
const uint8_t * src_read = r_img - > get_data ( ) . ptr ( ) ;
2021-04-07 07:05:56 +02:00
2022-12-20 19:54:01 +01:00
print_verbose ( vformat ( " etcpak: Encoding image size %dx%d to format %s%s. " , width , height , Image : : get_format_name ( target_format ) , mipmaps ? " , with mipmaps " : " " ) ) ;
2021-04-07 07:05:56 +02:00
2021-04-15 14:02:45 +02:00
int dest_size = Image : : get_image_data_size ( width , height , target_format , mipmaps ) ;
Vector < uint8_t > dest_data ;
dest_data . resize ( dest_size ) ;
uint8_t * dest_write = dest_data . ptrw ( ) ;
int mip_count = mipmaps ? Image : : get_image_required_mipmaps ( width , height , target_format ) : 0 ;
2022-01-19 05:29:39 +01:00
Vector < uint32_t > padded_src ;
2021-04-15 14:02:45 +02:00
for ( int i = 0 ; i < mip_count + 1 ; i + + ) {
// Get write mip metrics for target image.
2022-01-19 05:29:39 +01:00
int orig_mip_w , orig_mip_h ;
int mip_ofs = Image : : get_image_mipmap_offset_and_dimensions ( width , height , target_format , i , orig_mip_w , orig_mip_h ) ;
2021-04-15 14:02:45 +02:00
// Ensure that mip offset is a multiple of 8 (etcpak expects uint64_t pointer).
ERR_FAIL_COND ( mip_ofs % 8 ! = 0 ) ;
uint64_t * dest_mip_write = ( uint64_t * ) & dest_write [ mip_ofs ] ;
// Block size. Align stride to multiple of 4 (RGBA8).
2022-01-19 05:29:39 +01:00
int mip_w = ( orig_mip_w + 3 ) & ~ 3 ;
int mip_h = ( orig_mip_h + 3 ) & ~ 3 ;
2021-04-15 14:02:45 +02:00
const uint32_t blocks = mip_w * mip_h / 16 ;
// Get mip data from source image for reading.
int src_mip_ofs = r_img - > get_mipmap_offset ( i ) ;
const uint32_t * src_mip_read = ( const uint32_t * ) & src_read [ src_mip_ofs ] ;
2022-01-19 05:29:39 +01:00
// Pad textures to nearest block by smearing.
if ( mip_w ! = orig_mip_w | | mip_h ! = orig_mip_h ) {
padded_src . resize ( mip_w * mip_h ) ;
uint32_t * ptrw = padded_src . ptrw ( ) ;
int x = 0 , y = 0 ;
for ( y = 0 ; y < orig_mip_h ; y + + ) {
for ( x = 0 ; x < orig_mip_w ; x + + ) {
ptrw [ mip_w * y + x ] = src_mip_read [ orig_mip_w * y + x ] ;
}
// First, smear in x.
for ( ; x < mip_w ; x + + ) {
ptrw [ mip_w * y + x ] = ptrw [ mip_w * y + x - 1 ] ;
}
}
// Then, smear in y.
for ( ; y < mip_h ; y + + ) {
for ( x = 0 ; x < mip_w ; x + + ) {
ptrw [ mip_w * y + x ] = ptrw [ mip_w * y + x - mip_w ] ;
}
}
// Override the src_mip_read pointer to our temporary Vector.
src_mip_read = padded_src . ptr ( ) ;
}
2021-04-15 14:02:45 +02:00
if ( p_compresstype = = EtcpakType : : ETCPAK_TYPE_ETC1 ) {
CompressEtc1RgbDither ( src_mip_read , dest_mip_write , blocks , mip_w ) ;
2023-01-11 23:03:07 +01:00
} else if ( p_compresstype = = EtcpakType : : ETCPAK_TYPE_ETC2 ) {
2021-09-22 13:09:26 +02:00
CompressEtc2Rgb ( src_mip_read , dest_mip_write , blocks , mip_w , true ) ;
2023-01-11 23:03:07 +01:00
} else if ( p_compresstype = = EtcpakType : : ETCPAK_TYPE_ETC2_ALPHA | | p_compresstype = = EtcpakType : : ETCPAK_TYPE_ETC2_RA_AS_RG ) {
2021-09-22 13:09:26 +02:00
CompressEtc2Rgba ( src_mip_read , dest_mip_write , blocks , mip_w , true ) ;
2021-04-07 07:05:56 +02:00
} else if ( p_compresstype = = EtcpakType : : ETCPAK_TYPE_DXT1 ) {
2021-04-15 14:02:45 +02:00
CompressDxt1Dither ( src_mip_read , dest_mip_write , blocks , mip_w ) ;
} else if ( p_compresstype = = EtcpakType : : ETCPAK_TYPE_DXT5 | | p_compresstype = = EtcpakType : : ETCPAK_TYPE_DXT5_RA_AS_RG ) {
CompressDxt5 ( src_mip_read , dest_mip_write , blocks , mip_w ) ;
2021-04-07 07:05:56 +02:00
} else {
2022-12-20 19:54:01 +01:00
ERR_FAIL_MSG ( " etcpak: Invalid or unsupported compression format. " ) ;
2021-04-07 07:05:56 +02:00
}
}
2021-04-15 14:02:45 +02:00
// Replace original image with compressed one.
2022-07-22 20:06:19 +02:00
r_img - > set_data ( width , height , mipmaps , target_format , dest_data ) ;
2021-04-15 14:02:45 +02:00
2022-12-20 19:54:01 +01:00
print_verbose ( vformat ( " etcpak: Encoding took %s ms. " , rtos ( OS : : get_singleton ( ) - > get_ticks_msec ( ) - start_time ) ) ) ;
2021-04-07 07:05:56 +02:00
}