/* * System Interface Library for games * Copyright (c) 2007-2020 Andrew Church * Released under the GNU GPL version 3 or later; NO WARRANTY is provided. * See the file COPYING.txt for details. * * tools/pngtotex.c: Program to convert PNG images to the *.tex custom * texture format used by this library. Requires libpng for reading PNG * images. */ /* * To use, run this program as: * * pngtotex [options] file1.png [file2.png...] * * This will convert all named PNG files to the custom format used by this * library, saving the converted files with an extension of .tex replacing * any .png extension in the input filenames. * * The following options can be given (all options must precede the first * filename): * * -8: Convert textures to 8-bit indexed format. * * -a=LO,HI: Specify the low and high alpha thresholds when encoding to * non-32bpp formats; alpha values less than or equal to LO will be * forced to 0, and alpha values greater than or equal to HI will * be forced to 255. This can improve compression quality by * minimizing variations in alpha that will typically be invisible. * The default is no thresholding, equivalent to -a=0,255. * * -bgra: Write 32bpp textures in BGRA pixel order. If not given, RGBA * pixel order is used. This does not affect 8bpp paletted textures. * * -crop=X,Y+WxH: Crop the input image to X,Y+WxH before resizing and * converting. * * -dxt{1|3|5}: Convert textures to DXT1/3/5 compressed format. If the * texture is opaque, DXT1 is always used because all three formats * encode color components the same way. Requires a program * "dxtcomp" in the current executable search path which can be * called as: * dxtcomp -{1|3|5} in.rgba out.raw width height * where in.rgba and out.rgba are raw 32bpp pixel data in RGBA byte * order. On Windows, or on Linux with WINE installed, PVRTexTool * from version 3.23 of the PowerVR SDK can be used for DXTn * compression by using the following Perl script for "dxtcomp": * use strict; use warnings; die "Usage: $0 {-1|-3|-5} infile outfile width height\n" if @ARGV != 5; my $format = "DXT" . substr($ARGV[0],1,1); my $strip_alpha = ($format eq "DXT1"); shift @ARGV; my ($infile, $outfile, $width, $height) = @ARGV; open IN, "<$infile" or die "$infile: $!\n"; binmode IN; open OUT, ">$outfile.tga" or die "$outfile.tga: $!"; binmode OUT; print OUT "\0\0\2\0\0\0\0\0\0\0\0\0" or die "$outfile.tga: $!"; print OUT pack("vvv", $width, $height, $strip_alpha ? 24 : 32) or die "$outfile.tga: $!"; my $data; read(IN, $data, $width*$height*4) == $width*$height*4 or die "$infile: Premature EOF"; my @data = unpack("N*", $data); for (my $y = $height-1; $y >= 0; $y--) { my @row = @data[$y*$width..($y+1)*$width-1]; # The byte order difference here is intentional: PVRTexTool expects # RGB for 24-bit data but BGRA for 32-bit data. if ($strip_alpha) { print OUT join( "", map {pack("CCC", $_>>24 & 255, $_>>16 & 255, $_>>8 & 255)} @row ); for (my $i = 0; $i < @row; $i++) { print OUT pack("CCC", $row[$i]>>24 & 255, $row[$i]>>16 & 255, $row[$i]>>8 & 255); } } else { print OUT pack("V*", map {$_>>8 | $_<<24} @row); } } close IN; close OUT; my @program; if (`uname -o 2>/dev/null` =~ /Linux/) { @program = ("wine", "PVRTexTool.exe"); } else { @program = ("PVRTexTool"); } system @program, "-silent", "-dds", "-f$format", "-i$outfile.tga", "-o$outfile.dds"; $? == 0 or exit $?; unlink "$outfile.tga" or print STDERR "warning: unlink($outfile.tga): $!\n"; open IN, "<$outfile.dds" or die "$outfile.dds: $!\n"; binmode IN; open OUT, ">$outfile" or die "$outfile: $!"; binmode OUT; seek IN, 128, 0; while (read(IN, $data, 65536) > 0) { print OUT $data; } close IN; close OUT; unlink "$outfile.dds" or print STDERR "warning: unlink($outfile.dds): $!\n"; * * -hq: When converting to PVRTC format, use the high-quality (slow) * compressor. * * -make-square[-center]: When converting to DXT or PVRTC format, if * the texture is not a power-of-two-sized square, expand it unless * doing so would make the compressed data the same size as or * larger than the original data, in which case the data is output * as 32bpp RGBA instead (with no change in size). If -center is * appended to the option name, the original image is centered in * the final texture; otherwise, the texture data is left at the * (0,0) corner. The expanded area is filled with transparent black * if the texture is not opaque, else with opaque black. * * -make-pot: * texture is not square, expand it unless doing so would make the * compressed data the same size as or larger than the original * data, in which case the data is output as 32bpp RGBA instead. * If -center is appended to the option name, the original image is * centered in the final texture; otherwise, the texture data is * left at the (0,0) corner. The expanded area is filled with * transparent black if the texture is not opaque, else with opaque * black. * * -mipmaps[=N]: Generate mipmaps for the texture. If N is specified, * only N mipmaps will be generated; otherwise, as many as * appropriate for the texture size will be generated. * * -mipmap-regions=x:y:w:h[,...]: Specify subtexture areas for mipmap * generation, such as for a texture atlas. Each of these areas * will be shrunk separately, and the result will replace the * corresponding region of the shrunk original texture. Regions * with odd coordinates, either because they were specified that * way or due to shrinking in previous mipmaps, are discarded since * they cannot be mapped directly to a set of output pixels. * * -mipmaps-transparent-at=N: Force all mipmaps at level N and greater * (i.e., of size 1/2^N and smaller) to be completely transparent. * This can help avoid graphical glitches due to degenerate or * very thin triangles which select inappropriately small mipmaps. * * -opaque-bitmap: Include a bitmap of opaque pixels in the texture. * Useful if opacity data is needed by the program, since texture * pixel data cannot be read on OpenGL ES platforms. * * -outdir=OUTDIR: Write output files to the directory OUTDIR. For * example, "pngtotex [...] -outdir=outdir sub/file.png" writes the * converted texture to "outdir/file.tex". * * -psp: Encode textures for the PSP. Textures will be scaled to half * size (unless -resize is given), set to a scale factor of 0.5 * (unless -scale is given), aligned to PSP alignment requiements, * and swizzled. Note that compression (-dxt, -pvrtc) cannot be * used in PSP mode. * * -pvrtc{2|4}: Convert textures to 2-bit (-pvrtc2) or 4-bit (-pvrtc4) * PVRTC compressed format. Requires the PVRTexToolCLI program to * be located in the current executable search path, unless the * -pvrtextool option is used to specify the program's path. * * -pvrtextool=PATH: Specify the path for the PVRTexToolCLI program. * * -resize=WxH: Resize the input image to the given size before * converting. For PSP, this overrides the default action of * shrinking to half size. * * -scale=N: Specify the scale factor for the input image (default 1) * without changing the input data. A scale factor of 0.5 indicates * that the input image is half the size of the original texture; * this allows shrinking of specific textures to save space without * affecting program behavior. * * -verbose: Enable verbose output. */ #include "tool-common.h" #include #include #include /* The OSX SDK improperly locks the mkdtemp() declaration behind * __DARWIN_C_FULL rather than POSIX-2008.1 (_POSIX_C_SOURCE=200809L), * so we have to declare it manually. */ #ifdef __APPLE__ extern char *mkdtemp(char *); #endif #include "../src/texture.h" #include "../src/utility/tex-file.h" #include "quantize.h" #include "zoom.h" #define PNG_USER_MEM_SUPPORTED #include static jmp_buf png_jmpbuf; /*************************************************************************/ /* Structure for holding texture data. */ typedef struct Texture Texture; struct Texture { int16_t width, height; // Texture size (pixels). int16_t stride; // Texture line stride (pixels, always a // multiple of 16 bytes for the PSP). uint8_t format; // Pixel format (TEX_FORMAT_*). uint8_t swizzled; // 1 = data is swizzled (PSP only). uint8_t mipmaps; // Number of mipmap levels, _not_ including // primary texture data; odd sizes are // rounded down when halving to compute // mipmap width/height. uint32_t *palette; // Color palette (for indexed-color images). uint8_t *pixels; // Pixel data; mipmaps are appended immediately // following the primary texture data in // decreasing size order. uint8_t *opaque_bitmap; // Opaque bitmap data, or NULL if not present. }; /*-----------------------------------------------------------------------*/ /* Target pixel format. */ static int format = TEX_FORMAT_RGBA8888; /* Alpha thresholds. (-a) */ static uint8_t alpha_lo = 0, alpha_hi = 255; /* Use BGRA pixel order for 32bpp textures? (-bgra) */ static uint8_t bgra = 0; /* Crop region (all zero if none). (-crop) */ static unsigned int crop_x, crop_y, crop_w, crop_h; /* Use high-quality compression? (-hq) */ static uint8_t hq = 0; /* Force texture size to square in DXT/PVRTC mode? (-make-square) */ static uint8_t make_square = 0; /* Center texture when expanding to a square? (-make-square-center) */ static uint8_t make_square_center = 0; /* How many mipmaps should we add? (-mipmaps) */ static int num_mipmaps = 0; /* List of mipmap regions for independent shrinking. (-mipmap-regions) */ static struct { unsigned int x, y, w, h; } *mipmap_regions; static int num_mipmap_regions; /* At what mipmap level should we make mipmaps transparent? (0 = never) * (-mipmaps-transparent-at) */ static int mipmaps_transparent_at = 0; /* Generate opaque pixel bitmap? (-opaque-bitmap) */ static uint8_t do_opaque_bitmap = 0; /* Output directory, or NULL to output to the source file's directory. * (-outdir) */ static const char *outdir = NULL; /* Output for PSP? (-psp) */ static uint8_t psp = 0; /* PVRTexToolCLI program path. (-pvrtextool) */ static const char *pvrtextool = "PVRTexToolCLI"; /* Target size for resizing (all zero if no resizing). (-resize) */ static unsigned int resize_w, resize_h; /* Texture scale factor, as a 16.16 fixed-point value. (-scale) */ static int32_t scale_fixed = 0; // Default depends on whether -psp is given. /* Enable verbose output? (-verbose) */ static uint8_t verbose; /*-----------------------------------------------------------------------*/ static Texture *read_png(const char *path); static void png_warning_callback(png_structp png, const char *message); static void png_error_callback(png_structp png, const char *message); static int write_tex(Texture *texture, const char *path); static Texture *convert_format(Texture *texture, const char *path); static int crop_texture(Texture *texture, int left, int top, int width, int height); static int shrink_texture(Texture *texture, int new_width, int new_height); static Texture *generate_mipmaps(Texture *texture, int mipmaps, uint32_t *extra_pixels_ret); static int quantize_texture(Texture *texture); static int generate_opaque_bitmap(Texture *texture); static Texture *align_texture_psp(Texture *texture); static int swizzle_texture(Texture *texture); static int compress_dxt(Texture *texture, int type); static int run_dxtcomp(const uint8_t *src, unsigned int width, unsigned int height, unsigned int type, uint8_t *dest); static int compress_pvrtc(Texture *texture, unsigned int bpp); static int run_pvrtextool(const uint8_t *src, unsigned int width, unsigned int height, unsigned int bpp, int alpha, uint8_t *dest); static void convert_rgba_to_bgra(Texture *texture); static void spread_border(uint8_t * const pixels, const unsigned int width, const unsigned int height); static void fix_pvrtc4_alpha(const uint8_t *original, uint8_t *compressed, unsigned int width, unsigned int height); static CONST_FUNCTION int morton_index(unsigned int x, unsigned int y, unsigned int w, unsigned int h); static inline void block_data_to_colors( uint32_t block_data, unsigned int *rA, unsigned int *gA, unsigned int *bA, unsigned int *aA, unsigned int *rB, unsigned int *gB, unsigned int *bB, unsigned int *aB); static CONST_FUNCTION inline uint32_t colordiff_sq( unsigned int r1, unsigned int g1, unsigned int b1, unsigned int a1, unsigned int r2, unsigned int g2, unsigned int b2, unsigned int a2); static void free_texture(Texture *texture); /*************************************************************************/ /*************************************************************************/ /** * main: Program entry point. Parses command-line parameters and converts * each input file in turn. * * [Parameters] * argc: Command line argument count. * argv: Command line argument array. * [Return value] * Zero on successful completion, nonzero if an error occurred. */ int main(int argc, char **argv) { int argi = 1; while (argi < argc && argv[argi][0] == '-') { if (strcmp(argv[argi], "-8") == 0) { format = TEX_FORMAT_PALETTE8_RGBA8888; } else if (strcmp(argv[argi], "-alpha") == 0) { format = TEX_FORMAT_A8; } else if (strncmp(argv[argi], "-a=", 3) == 0) { if (argv[argi][3] == 0) { fprintf(stderr, "Missing argument for option -a\n"); goto usage; } char *s; alpha_lo = strtoul(&argv[argi][3], &s, 10); if (*s != ',') { fprintf(stderr, "Invalid argument for option -a\n"); goto usage; } alpha_hi = strtoul(s+1, &s, 10); if (*s != 0) { fprintf(stderr, "Invalid argument for option -a\n"); goto usage; } if (alpha_lo >= alpha_hi) { fprintf(stderr, "Invalid argument (LO >= HI) for option -a\n"); goto usage; } } else if (strcmp(argv[argi], "-bgra") == 0) { bgra = 1; } else if (strncmp(argv[argi], "-crop=", 6) == 0) { if (sscanf(&argv[argi][6], "%u,%u+%ux%u", &crop_x, &crop_y, &crop_w, &crop_h) != 4) { fprintf(stderr, "Invalid argument for option -crop\n"); goto usage; } if (crop_w == 0 || crop_h == 0) { fprintf(stderr, "Invalid argument for option -crop" " (size is zero)\n"); goto usage; } } else if (strcmp(argv[argi], "-dxt1") == 0) { format = TEX_FORMAT_S3TC_DXT1; } else if (strcmp(argv[argi], "-dxt3") == 0) { /* DXT3/5 formats will be automatically downgraded to DXT1 * (non-alpha) if the texture is completely opaque, since * all three share the same color data format. */ format = TEX_FORMAT_S3TC_DXT3; } else if (strcmp(argv[argi], "-dxt5") == 0) { format = TEX_FORMAT_S3TC_DXT5; } else if (strcmp(argv[argi], "-hq") == 0) { hq = 1; } else if (strcmp(argv[argi], "-make-square") == 0) { make_square = 1; make_square_center = 0; } else if (strcmp(argv[argi], "-make-square-center") == 0) { make_square = 1; make_square_center = 1; } else if (strncmp(argv[argi], "-mipmaps", 8) == 0 && (argv[argi][8] == 0 || argv[argi][8] == '=')) { if (argv[argi][8] == 0) { num_mipmaps = 99; } else { char *s; num_mipmaps = strtoul(argv[argi]+9, &s, 10); if (*s) { fprintf(stderr, "Invalid argument for option -mipmaps\n"); goto usage; } } } else if (strncmp(argv[argi], "-mipmap-regions=", 16) == 0) { char *s = &argv[argi][16]; do { char *start = s; unsigned int x, y, w, h; x = strtoul(s, &s, 0); if (*s != ':') { goto mipmap_regions_error; } y = strtoul(s+1, &s, 0); if (*s != ':') { goto mipmap_regions_error; } w = strtoul(s+1, &s, 0); if (*s != ':') { goto mipmap_regions_error; } h = strtoul(s+1, &s, 0); if (*s != 0 && *s != ',') { mipmap_regions_error: s = strchr(start, ','); if (s) { *s = 0; } fprintf(stderr, "Invalid mipmap region: %s\n", start); goto usage; } const unsigned int index = num_mipmap_regions++; mipmap_regions = realloc( mipmap_regions, num_mipmap_regions * sizeof(*mipmap_regions)); if (!mipmap_regions) { fprintf(stderr, "Out of memory processing -mipmap-regions\n"); return 1; } mipmap_regions[index].x = x; mipmap_regions[index].y = y; mipmap_regions[index].w = w; mipmap_regions[index].h = h; } while (*s++ != 0); } else if (strncmp(argv[argi], "-mipmaps-transparent-at=", 24) == 0) { char *s; mipmaps_transparent_at = strtoul(argv[argi]+24, &s, 10); if (*s) { fprintf(stderr, "Invalid argument for option" " -mipmaps-transparent-at\n"); goto usage; } } else if (strcmp(argv[argi], "-opaque-bitmap") == 0) { do_opaque_bitmap = 1; } else if (strncmp(argv[argi], "-outdir=", 8) == 0) { if (!argv[argi][8]) { fprintf(stderr, "Missing argument for option -outdir\n"); goto usage; } outdir = &argv[argi][8]; } else if (strcmp(argv[argi], "-psp") == 0) { psp = 1; } else if (strcmp(argv[argi], "-pvrtc2") == 0) { format = TEX_FORMAT_PVRTC2_RGBA; } else if (strcmp(argv[argi], "-pvrtc4") == 0) { format = TEX_FORMAT_PVRTC4_RGBA; } else if (strncmp(argv[argi], "-pvrtextool=", 12) == 0) { if (!argv[argi][12]) { fprintf(stderr, "Missing argument for option -pvrtextool\n"); goto usage; } pvrtextool = &argv[argi][12]; } else if (strncmp(argv[argi], "-resize=", 8) == 0) { if (sscanf(&argv[argi][8], "%ux%u", &resize_w, &resize_h) != 2) { fprintf(stderr, "Invalid argument for option -resize\n"); goto usage; } if (resize_w == 0 || resize_h == 0) { fprintf(stderr, "Invalid argument for option -resize" " (size is zero)\n"); goto usage; } } else if (strncmp(argv[argi], "-scale=", 7) == 0) { char *s; float scale = strtof(argv[argi]+7, &s); if (*s) { fprintf(stderr, "Invalid argument for option -scale\n"); goto usage; } if (scale < 1/65536.0f || scale >= 65536 || (double)scale*65536 != floor((double)scale*65536)) { fprintf(stderr, "Invalid scale value; must be a positive" " multiple of 1/65536 less than 65536\n"); goto usage; } scale_fixed = (int32_t)floorf(scale*65536); } else if (strcmp(argv[argi], "-verbose") == 0) { verbose = 1; } else { goto usage; } argi++; } if (psp) { // The PSP only supports up to 8 levels. num_mipmaps = ubound(num_mipmaps, 7); } if (!scale_fixed) { if (psp) { scale_fixed = 1<<15; // 0.5 } else { scale_fixed = 1<<16; // 1.0 } } if (argi >= argc) { usage: fprintf(stderr, "\n" "Usage: %s [options] file1.png [file2.png...]\n" "\n" "Options:\n" "\n" "-8 will quantize to 8bpp indexed textures.\n" "\n" "-alpha will write alpha-only textures, discarding all color\n" "information in the input files.\n" "\n" "-a=LO,HI specifies low and high alpha thresholds when using\n" "compressed texture formats. Do not include a space between\n" "\"-a\" and the argument.\n" "\n" "-bgra will write 32bpp textures in BGRA pixel order instead\n" "of the default RGBA pixel order. This does not affect 8bpp\n" "paletted textures.\n" "\n" "-crop=X,Y+WxH crops the input image to the given region\n" "before resizing and converting.\n" "\n" "-dxt1, -dxt3, and -dxt5 select S3TC DXTn-compressed output.\n" "The dxtcomp program must be available in the executable\n" "search path (see source code comments for details).\n" "\n" "-make-square (only valid in DXT or PVRTC output modes)\n" "forces the texture to be a square if it is not already\n" "square. Append -center to the option name to center the\n" "original image in the final texture.\n" "\n" "-mipmaps will generate mipmaps for each texture; adding a\n" "number (like -mipmaps=2) limits the number of additional\n" "mipmaps to that number or fewer.\n" "\n" "-mipmap-regions=x:y:w:h[,...] will shrink the specified\n" "areas independently when generating mipmaps, to prevent\n" "adjacent subtextures from leaking into each other.\n" "\n" "-mipmaps-transparent-at=N will force mipmaps at level N and\n" "greater (relative size 1/2^N and smaller) to be completely\n" "transparent, to help avoid graphical glitches caused by\n" "degenerate geometry.\n" "\n" "-opaque-bitmap will generate a bitmap of opaque pixels to\n" "allow the program to read texture opacity data.\n" "\n" "-outdir=OUTDIR specifies the output directory for all files.\n" "\n" "-psp selects PSP output mode, with automatic resize to half\n" "size (unless -resize is given), scale factor 0.5 (unless\n" "-scale is given), data alignment, and swizzling.\n" "\n" "-pvrtc2 and -pvrtc4 select (respectively) 2bpp and 4bpp\n" "PVRTC-compressed output. The PVRTexToolCLI program must\n" "be available in the executable search path, unless the\n" "-pvrtextool option is given.\n" "\n" "-pvrtextool=PATH specifies the path (including filename) of\n" "the PVRTexToolCLI program used for PVRTC texture compression.\n" "\n" "-resize=WxH resizes the input image to the given size\n" "before converting. The scale factor is not affected.\n" "\n" "-scale=N gives the scale factor of the input image relative\n" "to the original texture. For example, use -scale=0.5 if\n" "the input image has been shrunk by half; this would cause\n" "the texture size to be reported as twice the image size.\n" "\n", argv[0]); return 1; } for (; argi < argc; argi++) { const char *infile = argv[argi]; Texture *texture = read_png(infile); if (!texture) { fprintf(stderr, "Failed to read %s\n", infile); return 1; } texture = convert_format(texture, infile); if (!texture) { return 1; } char pathbuf[1000]; unsigned int pathlen; if (outdir) { const char *filename = strrchr(infile, '/'); if (filename) { filename++; } else { filename = infile; } pathlen = snprintf(pathbuf, sizeof(pathbuf)-4, "%s/%s", outdir, filename); } else { pathlen = snprintf(pathbuf, sizeof(pathbuf)-4, "%s", infile); } if (pathlen >= sizeof(pathbuf)-4) { fprintf(stderr, "Pathname buffer overflow on %s\n", infile); return 1; } if (stricmp(pathbuf + pathlen - 4, ".png") == 0) { pathbuf[pathlen-4] = 0; pathlen -= 4; } memcpy(pathbuf + pathlen, ".tex", 5); if (!write_tex(texture, pathbuf)) { fprintf(stderr, "Failed to write %s\n", pathbuf); return 1; } free_texture(texture); } return 0; } /*************************************************************************/ /************************** PNG input functions **************************/ /*************************************************************************/ /** * read_png: Read a PNG file into a Texture data structure. * * [Parameters] * path: Pathname of file to read. * [Return value] * Texture, or NULL on error. */ static Texture *read_png(const char *path) { /* We have to be able to free these on error, so we need volatile * declarations. */ volatile FILE *f_volatile = NULL; volatile png_structp png_volatile = NULL; volatile png_infop info_volatile = NULL; volatile Texture *texture_volatile = NULL; volatile void *row_buffer_volatile = NULL; if (setjmp(png_jmpbuf) != 0) { /* libpng jumped back here with an error, so return the error. */ error: // Let's reuse it for our own error handling, too. free((void *)row_buffer_volatile); free((void *)texture_volatile); png_destroy_read_struct((png_structpp)&png_volatile, (png_infopp)&info_volatile, NULL); if (f_volatile) { fclose((FILE *)f_volatile); } return NULL; } /* Open the requested file. */ FILE *f = fopen(path, "rb"); if (!f) { perror(path); return NULL; } f_volatile = f; /* Set up the PNG reader instance. */ png_structp png = png_create_read_struct( PNG_LIBPNG_VER_STRING, (void *)path, png_error_callback, png_warning_callback); png_volatile = png; png_infop info = png_create_info_struct(png); info_volatile = info; png_init_io(png, f); /* Read the image information. */ png_read_info(png, info); const unsigned int width = png_get_image_width(png, info); const unsigned int height = png_get_image_height(png, info); const unsigned int bit_depth = png_get_bit_depth(png, info); const unsigned int color_type = png_get_color_type(png, info); if (png_get_interlace_type(png, info) != PNG_INTERLACE_NONE) { fprintf(stderr, "Interlaced images not supported\n"); goto error; } if (bit_depth < 8) { fprintf(stderr, "Bit depth %d not supported\n", bit_depth); goto error; } /* Set up image transformation parameters. */ if (color_type == PNG_COLOR_TYPE_PALETTE) { png_set_palette_to_rgb(png); } else if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA) { png_set_gray_to_rgb(png); } if (bit_depth == 16) { png_set_strip_16(png); } if (png_get_valid(png, info, PNG_INFO_tRNS)) { png_set_tRNS_to_alpha(png); } if (color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_PALETTE || color_type == PNG_COLOR_TYPE_GRAY) { png_set_add_alpha(png, 0xFF, PNG_FILLER_AFTER); } png_read_update_info(png, info); /* Create the texture structure. */ Texture *texture; const uint32_t struct_size = align_up(sizeof(*texture), 64); texture = malloc(struct_size + (width * height * 4)); if (!texture) { fprintf(stderr, "Out of memory for texture (%ux%u, %u bytes)\n", width, height, struct_size + (width * height * 4)); goto error; } texture->width = width; texture->height = height; texture->stride = width; texture->format = TEX_FORMAT_RGBA8888; texture->swizzled = 0; texture->mipmaps = 0; texture->palette = NULL; texture->pixels = (uint8_t *)texture + struct_size; texture->opaque_bitmap = NULL; texture_volatile = texture; /* Read the image in one row at a time. */ uint8_t *row_buffer; uint32_t rowbytes = png_get_rowbytes(png, info); row_buffer = malloc(rowbytes); if (!row_buffer) { fprintf(stderr, "Out of memory for pixel read buffer (%u bytes)\n", rowbytes); goto error; } row_buffer_volatile = row_buffer; uint8_t *dest = texture->pixels; for (unsigned int y = 0; y < height; y++, dest += texture->stride * 4) { png_read_row(png, row_buffer, NULL); memcpy(dest, row_buffer, rowbytes); } row_buffer_volatile = NULL; free(row_buffer); /* Done! Close down the PNG reader and return success. */ png_read_end(png, NULL); png_destroy_read_struct(&png, &info, NULL); fclose(f); return texture; } /*-----------------------------------------------------------------------*/ /** * png_warning_callback, png_error_callback: Error handling functions for * libpng. * * [Parameters] * png: PNG reader structure. * message: Warning or error message. * [Return value] * None (png_error_callback() does not return). */ static void png_warning_callback(png_structp png, const char *message) { fprintf(stderr, "libpng warning: %s: %s\n", (char *)png_get_error_ptr(png), message); } static void png_error_callback(png_structp png, const char *message) { fprintf(stderr, "libpng error: %s: %s\n", (char *)png_get_error_ptr(png), message); longjmp(png_jmpbuf, 1); } /*************************************************************************/ /*********************** Texture output functions ************************/ /*************************************************************************/ /** * write_tex: Write a SIL-format *.tex file for the given texture. * * [Parameters] * texture: Texture to write. * path: Pathname for new file. * [Return value] * True on success, false on error. */ static int write_tex(Texture *texture, const char *path) { TexFileHeader header; memcpy(header.magic, TEX_FILE_MAGIC, sizeof(header.magic)); header.version = TEX_FILE_VERSION; header.format = texture->format; header.mipmaps = texture->mipmaps; header.opaque_bitmap = (texture->opaque_bitmap != NULL) ? 1 : 0; header.width = s16_to_be(texture->width); header.height = s16_to_be(texture->height); header.scale_int = s32_to_be(scale_fixed); header.pixels_offset = s32_to_be(psp ? align_up(sizeof(header), 64) : sizeof(header)); header.pixels_size = s32_to_be(0); // Will be set later. header.bitmap_offset = s32_to_be(0); // Will be set later if needed. header.bitmap_size = s32_to_be(0); // Will be set later if needed. FILE *f = fopen(path, "wb"); if (!f) { perror(path); goto error_return; } if (fwrite(&header, sizeof(header), 1, f) != 1) { perror(path); goto error_close_file; } if (fseek(f, be_to_s32(header.pixels_offset), SEEK_SET) != 0) { perror(path); goto error_close_file; } uint32_t pixel_bytes_written = 0; if (texture->format == TEX_FORMAT_PALETTE8_RGBA8888 || texture->format == TEX_FORMAT_PSP_PALETTE8_RGBA8888 || texture->format == TEX_FORMAT_PSP_PALETTE8_RGBA8888_SWIZZLED) { if (fwrite(texture->palette, 4, 256, f) != 256) { perror(path); goto error_close_file; } pixel_bytes_written += 256*4; } unsigned int bpp; switch (texture->format) { case TEX_FORMAT_RGBA8888: case TEX_FORMAT_BGRA8888: case TEX_FORMAT_PSP_RGBA8888: case TEX_FORMAT_PSP_RGBA8888_SWIZZLED: bpp = 32; break; case TEX_FORMAT_RGB565: case TEX_FORMAT_RGBA5551: case TEX_FORMAT_RGBA4444: case TEX_FORMAT_BGR565: case TEX_FORMAT_BGRA5551: case TEX_FORMAT_BGRA4444: case TEX_FORMAT_PSP_RGB565: case TEX_FORMAT_PSP_RGBA5551: case TEX_FORMAT_PSP_RGBA4444: case TEX_FORMAT_PSP_RGB565_SWIZZLED: case TEX_FORMAT_PSP_RGBA5551_SWIZZLED: case TEX_FORMAT_PSP_RGBA4444_SWIZZLED: bpp = 16; break; case TEX_FORMAT_PALETTE8_RGBA8888: case TEX_FORMAT_A8: case TEX_FORMAT_PSP_PALETTE8_RGBA8888: case TEX_FORMAT_PSP_A8: case TEX_FORMAT_PSP_PALETTE8_RGBA8888_SWIZZLED: case TEX_FORMAT_PSP_A8_SWIZZLED: case TEX_FORMAT_S3TC_DXT3: case TEX_FORMAT_S3TC_DXT5: bpp = 8; break; case TEX_FORMAT_PVRTC2_RGBA: case TEX_FORMAT_PVRTC2_RGB: bpp = 2; break; case TEX_FORMAT_S3TC_DXT1: case TEX_FORMAT_PVRTC4_RGBA: case TEX_FORMAT_PVRTC4_RGB: bpp = 4; break; default: fprintf(stderr, "Unknown texture format %u, assuming 32bpp\n", texture->format); bpp = 32; break; } unsigned int width = texture->width, height = texture->height; unsigned int stride = texture->stride; const uint8_t *pixels = texture->pixels; for (unsigned int level = 0; level <= texture->mipmaps; level++) { unsigned int data_width, data_height; if (texture->format == TEX_FORMAT_PVRTC2_RGBA || texture->format == TEX_FORMAT_PVRTC4_RGBA || texture->format == TEX_FORMAT_PVRTC2_RGB || texture->format == TEX_FORMAT_PVRTC4_RGB) { data_width = lbound(width, 32/bpp); data_height = lbound(height, 8); } else if (texture->format == TEX_FORMAT_S3TC_DXT1 || texture->format == TEX_FORMAT_S3TC_DXT3 || texture->format == TEX_FORMAT_S3TC_DXT5) { data_width = lbound(width, 4); data_height = lbound(height, 4); } else { data_width = stride; data_height = texture->swizzled ? align_up(height, 8) : height; } if (fwrite(pixels, (data_width * data_height * bpp) / 8, 1, f) != 1) { perror(path); goto error_close_file; } pixel_bytes_written += (data_width * data_height * bpp) / 8; pixels += (data_width * data_height * bpp) / 8; width = lbound(width/2, 1); height = lbound(height/2, 1); if (psp) { stride = align_up(stride/2, 128/bpp); } else { stride = lbound(stride/2, 1); } } header.pixels_size = s32_to_be(pixel_bytes_written); if (texture->opaque_bitmap) { const unsigned int rowsize = (texture->width+7) / 8; header.bitmap_offset = s32_to_be(ftell(f)); header.bitmap_size = s32_to_be(rowsize * texture->height); if (fwrite(texture->opaque_bitmap, rowsize * texture->height, 1, f) != 1) { perror(path); goto error_close_file; } } if (fseek(f, 0, SEEK_SET) != 0 || fwrite(&header, sizeof(header), 1, f) != 1) { perror(path); goto error_close_file; } fclose(f); return 1; error_close_file: fclose(f); remove(path); error_return: return 0; } /*************************************************************************/ /********** Format conversion and image manipulation functions ***********/ /*************************************************************************/ /** * convert_format: Convert a 32bpp uncompressed texture to the format * specified by the command-line flags. The input texture's memory is * either reused or freed. * * [Parameters] * texture: Texture to convert. * path: File pathname (for error messages). * [Return value] * Converted texture, or NULL on error. */ static Texture *convert_format(Texture *texture, const char *path) { if (format == TEX_FORMAT_A8) { /* Force all pixels' color data to white so subsequent operations * don't produce suboptimal results due to the color data (which * will just be ignored anyway). */ for (int i = 0; i < texture->stride * texture->height; i++) { texture->pixels[i*4+0] = 255; texture->pixels[i*4+1] = 255; texture->pixels[i*4+2] = 255; } } if (crop_w && crop_h) { if ((int)(crop_x + crop_w) > texture->width || (int)(crop_y + crop_h) > texture->height) { fprintf(stderr, "%s: Crop rectangle (%u,%u+%ux%u) is outside" " texture bounds (%dx%d)\n", path, crop_x, crop_y, crop_w, crop_h, texture->width, texture->height); free_texture(texture); return 0; } if (!crop_texture(texture, crop_x, crop_y, crop_w, crop_h)) { fprintf(stderr, "%s: Shrinking failed\n", path); free_texture(texture); return 0; } } int resize_width = resize_w, resize_height = resize_h; if (!(resize_width && resize_height) && psp) { resize_width = lbound(texture->width/2, 1); resize_height = lbound(texture->height/2, 1); } if (resize_width && resize_height) { if ((int)resize_width > texture->width || (int)resize_height > texture->height) { fprintf(stderr, "%s: Expanding resize not currently supported\n", path); free_texture(texture); return 0; } if (!shrink_texture(texture, resize_width, resize_height)) { fprintf(stderr, "%s: Resizing failed\n", path); free_texture(texture); return 0; } } if (make_square && (format == TEX_FORMAT_S3TC_DXT1 || format == TEX_FORMAT_S3TC_DXT3 || format == TEX_FORMAT_S3TC_DXT5 || format == TEX_FORMAT_PVRTC2_RGBA || format == TEX_FORMAT_PVRTC4_RGBA || format == TEX_FORMAT_PVRTC2_RGB || format == TEX_FORMAT_PVRTC4_RGB)) { /* Calculate the required size. */ int new_width, new_height; for (new_width = 1; new_width < texture->width; new_width <<= 1) {} for (new_height = 1; new_height < texture->height; new_height <<= 1) {} if (new_width < new_height) { new_width = new_height; } else if (new_height < new_width) { new_height = new_width; } /* Check whether this would kill the compression ratio. */ int max_factor; if (format == TEX_FORMAT_PVRTC2_RGBA || format == TEX_FORMAT_PVRTC2_RGB) { max_factor = 16; } else if (format == TEX_FORMAT_PVRTC4_RGBA || format == TEX_FORMAT_PVRTC4_RGB || format == TEX_FORMAT_S3TC_DXT1) { max_factor = 8; } else { max_factor = 4; } if (new_width * new_height >= texture->width * texture->height * max_factor) { fprintf(stderr, "%s: warning: expanding texture would waste space;" " ignoring -make-square and writing as 32bpp RGBA\n", path); format = TEX_FORMAT_RGBA8888; } else { /* It's safe to expand, so do so. */ uint8_t fill_alpha = 0xFF; for (int i = 3; i < texture->width*texture->height*4; i += 4) { if (texture->pixels[i] != 0xFF) { fill_alpha = 0; break; } } const uint32_t alloc_width = align_up(new_width, 4); const uint32_t alloc_height = align_up(new_height, 8); const uint32_t struct_size = align_up(sizeof(*texture), 64); Texture *new_texture = malloc(struct_size + (alloc_width * alloc_height * 4)); if (!new_texture) { fprintf(stderr, "%s: Out of memory\n", path); free_texture(texture); return 0; } *new_texture = *texture; new_texture->width = new_width; new_texture->height = new_height; new_texture->stride = new_width; new_texture->pixels = (uint8_t *)new_texture + struct_size; const int offset_x = make_square_center ? (new_width - texture->width) / 2 : 0; const int offset_y = make_square_center ? (new_height - texture->height) / 2 : 0; const uint8_t *src = texture->pixels; uint8_t *dest = new_texture->pixels; for (int y = 0; y < offset_y; y++) { for (int x = 0; x < new_width; x++) { *dest++ = 0; *dest++ = 0; *dest++ = 0; *dest++ = fill_alpha; } } for (int y = 0; y < texture->height; y++, src += texture->stride*4) { for (int x = 0; x < offset_x; x++) { *dest++ = 0; *dest++ = 0; *dest++ = 0; *dest++ = fill_alpha; } memcpy(dest, src, texture->width*4); dest += texture->width*4; for (int x = offset_x + texture->width; x < new_width; x++) { *dest++ = 0; *dest++ = 0; *dest++ = 0; *dest++ = fill_alpha; } } for (int y = offset_y + texture->height; y < new_height; y++) { for (int x = 0; x < new_width; x++) { *dest++ = 0; *dest++ = 0; *dest++ = 0; *dest++ = fill_alpha; } } free_texture(texture); texture = new_texture; } } uint32_t extra_pixels = 0; if (num_mipmaps > 0) { if ((texture->width & (texture->width - 1)) != 0 || (texture->height & (texture->height - 1)) != 0) { /* X & (X-1) is zero iff X is either zero or a power of two. */ fprintf(stderr, "%s: Not generating mipmaps (size %dx%d is not a" " power of 2)\n", path, texture->width, texture->height); } else { texture = generate_mipmaps(texture, num_mipmaps, &extra_pixels); if (!texture) { fprintf(stderr, "%s: Failed to generate mipmaps\n", path); return 0; } } } const uint32_t total_pixels = texture->width * texture->height + extra_pixels; if (do_opaque_bitmap && !generate_opaque_bitmap(texture)) { fprintf(stderr, "%s: Failed to generate opaque bitmap\n", path); free_texture(texture); return 0; } if (format == TEX_FORMAT_PALETTE8_RGBA8888) { texture->palette = calloc(256, 4); if (!texture->palette) { fprintf(stderr, "%s: Out of memory\n", path); free_texture(texture); return 0; } generate_palette((uint32_t *)texture->pixels, total_pixels, 1, total_pixels, texture->palette, 0, NULL); if (!quantize_texture(texture)) { fprintf(stderr, "%s: Color quantization failed\n", path); free_texture(texture); return 0; } } else if (format == TEX_FORMAT_A8) { for (uint32_t i = 0; i < total_pixels; i++) { texture->pixels[i] = texture->pixels[i*4+3]; } texture->format = TEX_FORMAT_A8; } else if (format == TEX_FORMAT_S3TC_DXT1 || format == TEX_FORMAT_S3TC_DXT3 || format == TEX_FORMAT_S3TC_DXT5) { const int format_num = (format==TEX_FORMAT_S3TC_DXT1 ? 1 : format==TEX_FORMAT_S3TC_DXT3 ? 3 : 5); if (!compress_dxt(texture, format_num)) { fprintf(stderr, "%s: Compression failed\n", path); free_texture(texture); return 0; } } else if (format == TEX_FORMAT_PVRTC2_RGBA || format == TEX_FORMAT_PVRTC4_RGBA || format == TEX_FORMAT_PVRTC2_RGB || format == TEX_FORMAT_PVRTC4_RGB) { const int bpp = (format==TEX_FORMAT_PVRTC2_RGBA || format==TEX_FORMAT_PVRTC2_RGB ? 2 : 4); if (!compress_pvrtc(texture, bpp)) { fprintf(stderr, "%s: Compression failed\n", path); free_texture(texture); return 0; } } if (psp) { if (!(texture = align_texture_psp(texture))) { fprintf(stderr, "%s: Failed to align pixel data\n", path); return 0; } if (!swizzle_texture(texture)) { fprintf(stderr, "%s: Swizzling failed\n", path); free_texture(texture); return 0; } if (format == TEX_FORMAT_RGBA8888) { texture->format = TEX_FORMAT_PSP_RGBA8888_SWIZZLED; } else if (format == TEX_FORMAT_RGB565) { texture->format = TEX_FORMAT_PSP_RGB565_SWIZZLED; } else if (format == TEX_FORMAT_RGBA5551) { texture->format = TEX_FORMAT_PSP_RGBA5551_SWIZZLED; } else if (format == TEX_FORMAT_RGBA4444) { texture->format = TEX_FORMAT_PSP_RGBA4444_SWIZZLED; } else if (format == TEX_FORMAT_PALETTE8_RGBA8888) { texture->format = TEX_FORMAT_PSP_PALETTE8_RGBA8888_SWIZZLED; } else if (format == TEX_FORMAT_A8) { texture->format = TEX_FORMAT_PSP_A8_SWIZZLED; } else { fprintf(stderr, "%s: Invalid texture format for PSP: 0x%02X\n", path, texture->format); } } if (bgra && format == TEX_FORMAT_RGBA8888) { convert_rgba_to_bgra(texture); format = TEX_FORMAT_BGRA8888; } return texture; } /*-----------------------------------------------------------------------*/ /** * crop_texture: Crop the given texture to the given region. The texture * buffer is _not_ reallocated. * * [Parameters] * texture: Texture to crop. * left, top: Coordinates of upper-left corner of crop region, in pixels. * width, height: Size of crop region, in pixels. * [Return value] * True on success, false on error. */ static int crop_texture(Texture *texture, int left, int top, int width, int height) { const uint8_t *src = &texture->pixels[(top*texture->stride + left) * 4]; uint8_t *dest = texture->pixels; for (int y = 0; y < height; y++, src += texture->stride*4, dest += width*4) { memmove(dest, src, width*4); } texture->width = width; texture->height = height; texture->stride = width; return 1; } /*-----------------------------------------------------------------------*/ /** * shrink_texture: Shrink the given texture to the given size. The * texture buffer is _not_ reallocated. * * [Parameters] * texture: Texture to shrink. * new_width: New texture width, in pixels. * new_height: New texture width, in pixels. * [Return value] * True on success, false on error. */ static int shrink_texture(Texture *texture, int new_width, int new_height) { new_width = ubound(new_width, texture->width); new_height = ubound(new_height, texture->height); if (new_width == texture->width && new_height == texture->height) { return 1; } void *tempbuf = malloc(new_width * new_height * 4); if (!tempbuf) { fprintf(stderr, "Out of memory for shrink buffer (%d bytes)\n", new_width * new_height * 4); } ZoomInfo *zi = zoom_init(texture->width, texture->height, new_width, new_height, 4, texture->stride*4, new_width*4, 1, TCV_ZOOM_CUBIC_KEYS4); if (!zi) { fprintf(stderr, "zoom_init() failed\n"); free(tempbuf); return 0; } zoom_process(zi, texture->pixels, tempbuf); zoom_free(zi); texture->width = new_width; texture->height = new_height; texture->stride = new_width; memcpy(texture->pixels, tempbuf, new_width * new_height * 4); free(tempbuf); return 1; } /*-----------------------------------------------------------------------*/ /** * generate_mipmaps: Generate the given number of mipmaps for the given * 32bpp texture, stopping if the mipmap size reaches 1x1. The texture * buffer is reallocated by this function, and freed on error. * * [Parameters] * tex: Texture to generate mipmaps for. * mipmaps: Maximum number of mipmaps to generate. * extra_pixels_ret: Pointer to variable to receive the number of * pixels added, or NULL if not needed. * [Return value] * New texture pointer, or NULL on error. */ static Texture *generate_mipmaps(Texture *texture, int mipmaps, uint32_t *extra_pixels_ret) { /* Worst case is a 1x2^n texture, which will have mipmaps of size * 1x2^(n-1), 1x2^(n-2), ..., 1x4, 1x2, 1x1, for a total of 2^(n+1)-1 * pixels. */ const uint32_t struct_size = align_up(sizeof(*texture), 64); const uint32_t alloc_size = struct_size + (texture->stride * texture->height * 4) * 2; Texture *new_texture = realloc(texture, alloc_size); if (!new_texture) { fprintf(stderr, "Out of memory generating mipmaps\n"); free_texture(texture); return NULL; } texture = new_texture; texture->pixels = (uint8_t *)texture + struct_size; if (extra_pixels_ret) { *extra_pixels_ret = 0; } unsigned int width = texture->width; unsigned int height = texture->height; unsigned int stride = texture->stride; uint8_t * pixels = texture->pixels; for (int level = 1; level <= mipmaps && (width > 1 || height > 1); level++) { const unsigned int old_width = width; const unsigned int old_height = height; const unsigned int old_stride = stride; uint8_t * const old_pixels = pixels; pixels += stride * height * 4; width = lbound(width/2, 1); height = lbound(height/2, 1); stride = lbound(stride/2, 1); ZoomInfo *zi = zoom_init(old_width, old_height, width, height, 4, old_stride*4, stride*4, 1, TCV_ZOOM_CUBIC_KEYS4); if (!zi) { fprintf(stderr, "zoom_init() failed\n"); free_texture(texture); return NULL; } zoom_process(zi, old_pixels, pixels); zoom_free(zi); for (int i = 0; i < num_mipmap_regions; i++) { if (mipmap_regions[i].w == 0 || mipmap_regions[i].h == 0) { /* Invalid or previously disabled. */ } else if ((mipmap_regions[i].x % 2) != 0 || (mipmap_regions[i].y % 2) != 0 || (mipmap_regions[i].w % 2) != 0 || (mipmap_regions[i].h % 2) != 0) { /* Doesn't shrink to pixel boundaries, so disable the entry. */ mipmap_regions[i].w = mipmap_regions[i].h = 0; } else { zi = zoom_init(mipmap_regions[i].w, mipmap_regions[i].h, mipmap_regions[i].w/2, mipmap_regions[i].h/2, 4, old_stride*4, stride*4, 1, TCV_ZOOM_CUBIC_KEYS4); if (!zi) { fprintf(stderr, "zoom_init() failed for mipmap region %d\n", i); free_texture(texture); return NULL; } const uint8_t *src = old_pixels + (mipmap_regions[i].y * old_stride + mipmap_regions[i].x) * 4; uint8_t *dest = pixels + ((mipmap_regions[i].y/2) * stride + (mipmap_regions[i].x/2)) * 4; zoom_process(zi, src, dest); zoom_free(zi); mipmap_regions[i].x /= 2; mipmap_regions[i].y /= 2; mipmap_regions[i].w /= 2; mipmap_regions[i].h /= 2; } } // for (i = 0; i < num_mipmap_regions; i++) if (mipmaps_transparent_at != 0 && level >= mipmaps_transparent_at) { /* Retain the color data (for inter-mipmap interpolation); just * clear the alpha channel. */ for (uint32_t i = 0; i < stride*height; i++) { pixels[i*4+3] = 0; } } texture->mipmaps++; if (extra_pixels_ret) { *extra_pixels_ret += width * height; } } return texture; } /*-----------------------------------------------------------------------*/ /** * quantize_texture: Convert the given texture to indexed-color 8bpp by * quantizing the color palette down to the 256 colors specified in * texture->palette. The texture buffer is _not_ reallocated. * * [Parameters] * texture: Texture to quantize. * [Return value] * True on success, false on error. */ static int quantize_texture(Texture *texture) { unsigned int width = texture->width; unsigned int height = texture->height; unsigned int stride = texture->stride; uint32_t *pixels_in = (uint32_t *)texture->pixels; uint8_t *pixels_out = texture->pixels; for (unsigned int level = 0; level <= texture->mipmaps; level++) { if (!quantize_image(pixels_in, stride, pixels_out, stride, width, height, texture->palette, 256)) { fprintf(stderr, "quantize_image() failed for level %u\n", level); return 0; } pixels_in += stride * height; pixels_out += stride * height; width = lbound(width/2, 1); height = lbound(height/2, 1); stride = lbound(stride/2, 1); } texture->format = TEX_FORMAT_PALETTE8_RGBA8888; return 1; } /*-----------------------------------------------------------------------*/ /** * generate_opaque_bitmap: Generate a bitmap of opaque pixels for the * given texture. For textures with mipmaps, the bitmap reflects the * pixels in the base image. The bitmap is stored in a separately- * allocated buffer. * * [Parameters] * tex: Texture for which to generate an opaque pixel bitmap. * [Return value] * True on success, false on error. */ static int generate_opaque_bitmap(Texture *texture) { const unsigned int width = texture->width; const unsigned int height = texture->height; const unsigned int rowsize = (width+7) / 8; texture->opaque_bitmap = calloc(rowsize * height, 1); if (!texture->opaque_bitmap) { fprintf(stderr, "Out of memory generating opaque bitmap (%u bytes)\n", rowsize * height); return 0; } const uint8_t *src = texture->pixels; const uint32_t stride = texture->stride; uint8_t *dest = texture->opaque_bitmap; switch (texture->format) { case TEX_FORMAT_RGBA8888: { for (unsigned int y = 0; y < height; y++, src += stride*4, dest += rowsize) { for (unsigned int x = 0; x < width; x++) { dest[x/8] |= (src[x*4+3]==255 ? 1 : 0) << (x%8); } } break; } case TEX_FORMAT_PALETTE8_RGBA8888: { const uint32_t * const palette = texture->palette; for (unsigned int y = 0; y < height; y++, src += stride, dest += rowsize) { for (unsigned int x = 0; x < width; x++) { dest[x/8] |= ((palette[src[x]]>>24)==255 ? 1 : 0) << (x%8); } } break; } case TEX_FORMAT_A8: { for (unsigned int y = 0; y < height; y++, src += stride, dest += rowsize) { for (unsigned int x = 0; x < width; x++) { dest[x/8] |= (src[x]==255 ? 1 : 0) << (x%8); } } break; } default: fprintf(stderr, "Can't generate opaque bitmap for format %u\n", texture->format); free(texture->opaque_bitmap); texture->opaque_bitmap = NULL; return 0; } return 1; } /*-----------------------------------------------------------------------*/ /** * align_texture_psp: Align the given texture's pixel data for use on the * PSP (64-byte aligned, using 16-byte by 8-line blocks). The texture * buffer is reallocated by this function, and freed on error. * * [Parameters] * tex: Texture to modify. * [Return value] * New texture pointer, or NULL on error. */ static Texture *align_texture_psp(Texture *texture) { const unsigned int Bpp = (texture->format == TEX_FORMAT_PALETTE8_RGBA8888 || texture->format == TEX_FORMAT_A8) ? 1 : 4; const unsigned int block_width = 16 / Bpp; uint32_t total_pixel_bytes = 0; unsigned int width = texture->width; unsigned int height = texture->height; for (unsigned int level = 0; level <= texture->mipmaps; level++) { const unsigned int stride = align_up(width, block_width); total_pixel_bytes += stride * align_up(height, 8) * Bpp; width = lbound(width/2, 1); height = lbound(height/2, 1); } const uint32_t struct_size = align_up(sizeof(*texture), 64); const uint32_t alloc_size = struct_size + total_pixel_bytes; Texture *new_texture = malloc(alloc_size); if (!new_texture) { fprintf(stderr, "Out of memory generating mipmaps\n"); free_texture(texture); return NULL; } *new_texture = *texture; new_texture->stride = align_up(texture->width, block_width); new_texture->pixels = (uint8_t *)new_texture + struct_size; const uint8_t *src = texture->pixels; uint8_t *dest = new_texture->pixels; width = texture->width; height = texture->height; unsigned int stride = texture->stride; for (unsigned int level = 0; level <= texture->mipmaps; level++) { const unsigned int new_stride = align_up(width, block_width); memset(dest, 0, new_stride * align_up(height, 8) * Bpp); for (unsigned int y = 0; y < height; y++, src += stride*Bpp, dest += new_stride*Bpp) { memcpy(dest, src, width*Bpp); } dest += new_stride * (align_up(height, 8) - height) * Bpp; width = lbound(width/2, 1); height = lbound(height/2, 1); stride = lbound(stride/2, 1); } free(texture); // NOT free_texture(texture)! (this is just a realloc) return new_texture; } /*-----------------------------------------------------------------------*/ /** * swizzle_texture: Swizzle the given texture's pixel data, which is * assumed to have been aligned with align_texture_psp(). The texture * buffer is _not_ reallocated. * * [Parameters] * texture: Texture to swizzle. * [Return value] * True on success, false on error. */ static int swizzle_texture(Texture *texture) { unsigned int height = align_up(texture->height, 8); unsigned int stride = texture->stride; /* The pixels can be either 8bpp or 32bpp, but we process them as * 32bpp for speed. (The swizzle block width is 16 bytes either way.) */ uint32_t *pixels = (uint32_t *)texture->pixels; unsigned int stride_words = (texture->format == TEX_FORMAT_PALETTE8_RGBA8888 || texture->format == TEX_FORMAT_A8 ? stride/4 : stride); uint32_t *tempbuf = malloc(8 * (stride_words*4)); if (!tempbuf) { fprintf(stderr, "Out of memory for temporary buffer (%u bytes)\n", 8 * (stride_words*4)); return 0; } for (unsigned int level = 0; level <= texture->mipmaps; level++) { const uint32_t *src = pixels; uint32_t *dest = pixels; for (unsigned int y = 0; y < height; y += 8, src += 8*stride_words) { memcpy(tempbuf, src, 8 * (stride_words*4)); const uint32_t *tempsrc = tempbuf; for (unsigned int x = 0; x < stride_words; x += 4, tempsrc += 4) { const uint32_t *linesrc = tempsrc; for (unsigned int line = 0; line < 8; line++, linesrc += stride_words, dest += 4) { const uint32_t pixel0 = linesrc[0]; const uint32_t pixel1 = linesrc[1]; const uint32_t pixel2 = linesrc[2]; const uint32_t pixel3 = linesrc[3]; dest[0] = pixel0; dest[1] = pixel1; dest[2] = pixel2; dest[3] = pixel3; } } } pixels += stride_words * height; height = align_up(height/2, 8); stride_words = align_up(stride_words/2, 4); } free(tempbuf); texture->swizzled = 1; return 1; } /*-----------------------------------------------------------------------*/ /** * compress_dxt: Compress the given texture's pixel data using the given * S3TC DXTn compression method. If the texture is fully opaque, DXT1 is * used regardless of the requested compression method. The texture buffer * is _not_ reallocated. * * [Parameters] * tex: Texture to compress. * type: Compression type (1 = DXT1, 3 = DXT3, 5 = DXT5). * [Return value] * True on success, false on error. */ static int compress_dxt(Texture *texture, int type) { if (type != 1 && type != 3 && type != 5) { fprintf(stderr, "Invalid type for DXT: %d\n", type); return 0; } if (texture->format != TEX_FORMAT_RGBA8888) { fprintf(stderr, "Texture format must be RGBA8888 for DXT\n"); return 0; } /* Check whether the texture data has any non-opaque pixels, * and set the texture format as appropriate. */ int has_alpha = 0; for (int i = 3; i < texture->width * texture->height * 4; i += 4) { if (texture->pixels[i] != 0xFF) { has_alpha = 1; break; } } if (has_alpha) { if (type == 1) { fprintf(stderr, "warning: Conversion to DXT1 will drop alpha" " channel\n"); } texture->format = (type==1 ? TEX_FORMAT_S3TC_DXT1 : type==3 ? TEX_FORMAT_S3TC_DXT3 : TEX_FORMAT_S3TC_DXT5); } else { type = 1; texture->format = TEX_FORMAT_S3TC_DXT1; } /* Call out to dxtcomp to actually generate the data for each mipmap * level. */ const unsigned int bpp = (type==1 ? 4 : 8); uint8_t *src = texture->pixels; uint8_t *dest = texture->pixels; unsigned int width = texture->width; unsigned int height = texture->height; uint8_t *temp = malloc(lbound(width,4) * lbound(height,4) * bpp/8 * 2); if (!temp) { fprintf(stderr, "Out of memory compressing to DXT\n"); return 0; } for (unsigned int i = 0; i <= texture->mipmaps; i++) { if (!run_dxtcomp(src, width, height, type, temp)) { fprintf(stderr, "dxtcomp failed\n"); return 0; } const unsigned int dxt_width = lbound(width, 4); const unsigned int dxt_height = lbound(height, 4); memcpy(dest, temp, (dxt_width * dxt_height * bpp) / 8); src += width * height * 4; dest += (dxt_width * dxt_height * bpp) / 8; width = lbound(width/2, 1); height = lbound(height/2, 1); } free(temp); return type; } /*----------------------------------*/ /** * run_dxtcomp: Run the external dxtcomp program (assumed to be in the * executable path) to compress pixel data. Note that DXTn data has a * minimum encoded size of 4x4, regardless of the actual image size, and * the caller must ensure enough output buffer space is available. * * [Parameters] * src: Input pixel data, in RGBA8888 format (destroyed). * width: Texture width, in pixels. * height: Texture height, in pixels. * type: DXTn compression type (1, 3, or 5). * dest: Output data pointer. * [Return value] * True on success, false on error. */ static int run_dxtcomp(const uint8_t *src, unsigned int width, unsigned int height, unsigned int type, uint8_t *dest) { const unsigned int bpp = (type==1 ? 4 : 8); char dir[1000], infile[1009], outfile[1009]; /* Create a temporary directory for the dxtcomp input/output files. * We ignore a value of $TMPDIR containing an apostrophe because it'll * wreak havoc with our command line below. */ const char *tmpdir = getenv("TMPDIR"); if (!tmpdir || strchr(tmpdir, '\'') != NULL) { tmpdir = "/tmp"; } if (snprintf(dir, sizeof(dir), "%s/pngtotexXXXXXX", tmpdir) >= (int)sizeof(dir)) { fprintf(stderr, "Buffer overflow on temporary directory\n"); goto error_return; } if (!mkdtemp(dir)) { fprintf(stderr, "Failed to create temporary directory (%s): %s\n", dir, strerror(errno)); goto error_return; } /* These are guaranteed to fit. */ snprintf(infile, sizeof(infile), "%s/in.rgba", dir); snprintf(outfile, sizeof(outfile), "%s/out.dxt", dir); /* Create a temporary buffer, expanding (with repetition) to the * minimum size for the selected format and forcing the alpha bytes to * full opacity if using DXT1 compression. */ const unsigned int dxt_width = lbound(width, 4); const unsigned int dxt_height = lbound(height, 4); uint8_t *tempbuf = malloc(dxt_width * dxt_height * 4); if (!tempbuf) { fprintf(stderr, "Failed to allocate temporary buffer (%u bytes)\n", dxt_width * dxt_height * 4); goto error_rmdir; } if (width != dxt_width || height != dxt_height) { uint8_t *out = tempbuf; for (unsigned int y = 0; y < dxt_height; y++) { const uint8_t *row = src + (y % height) * width * 4; for (unsigned int x = 0; x < dxt_width; x++, out += 4) { const uint8_t r = row[(x % width) * 4 + 0]; const uint8_t g = row[(x % width) * 4 + 1]; const uint8_t b = row[(x % width) * 4 + 2]; const uint8_t a = row[(x % width) * 4 + 3]; out[0] = b; out[1] = g; out[2] = r; out[3] = (type==1 ? 255 : a); } } } else if (type == 1) { for (unsigned int i = 0; i < width*height*4; i += 4) { const uint8_t r = src[i+0]; const uint8_t g = src[i+1]; const uint8_t b = src[i+2]; tempbuf[i+0] = b; tempbuf[i+1] = g; tempbuf[i+2] = r; tempbuf[i+3] = 255; } spread_border(tempbuf, width, height); } else { memcpy(tempbuf, src, width*height*4); } /* Write the pixel data as a raw RGBA file. */ FILE *f = fopen(infile, "wb"); if (!f) { fprintf(stderr, "Failed to create %s: %s\n", infile, strerror(errno)); goto error_free_tempbuf; } if (fwrite(tempbuf, dxt_width*dxt_height*4, 1, f) != 1) { fprintf(stderr, "Failed to write data to %s: %s\n", infile, strerror(errno)); fclose(f); goto error_remove_infile; } if (fclose(f) != 0) { fprintf(stderr, "Failed to close %s: %s\n", infile, strerror(errno)); goto error_remove_infile; } /* Call out to dxtcomp. */ char cmdbuf[4000]; // Long enough to avoid truncation. snprintf(cmdbuf, sizeof(cmdbuf), "dxtcomp -%d '%s' '%s' %u %u", type, infile, outfile, dxt_width, dxt_height); if (verbose) { fprintf(stderr, "Executing: %s\n", cmdbuf); } if (system(cmdbuf) != 0) { fprintf(stderr, "dxtcomp call failed\n"); goto error_remove_outfile; } /* Read the compressed pixel data from the output file. */ f = fopen(outfile, "r"); if (!f) { fprintf(stderr, "Failed to open %s: %s\n", outfile, strerror(errno)); goto error_remove_outfile; } if (fread(dest, (dxt_width * dxt_height * bpp) / 8, 1, f) != 1) { fprintf(stderr, "Failed to read data from %s\n", outfile); fclose(f); goto error_remove_outfile; } fclose(f); /* All done! Clean up our files/buffers and return. */ remove(outfile); remove(infile); rmdir(dir); free(tempbuf); return 1; /* Handle all error returns here so we don't leave junk behind. */ error_remove_outfile: remove(outfile); error_remove_infile: remove(infile); error_free_tempbuf: free(tempbuf); error_rmdir: rmdir(dir); error_return: return 0; } /*-----------------------------------------------------------------------*/ /** * compress_pvrtc: Compress the given texture's pixel data using the * PVRTC compression method. The texture buffer is _not_ reallocated. * * [Parameters] * tex: Texture to compress. * bpp: Desired bits per pixel (2 or 4). * [Return value] * True on success, false on error. */ static int compress_pvrtc(Texture *texture, unsigned int bpp) { if (bpp != 2 && bpp != 4) { fprintf(stderr, "Invalid bits per pixel for PVRTC: %d\n", bpp); return 0; } if (texture->format != TEX_FORMAT_RGBA8888) { fprintf(stderr, "Texture format must be RGBA8888 for PVRTC\n"); return 0; } /* Check whether the texture data has any non-opaque pixels, * and set the texture format as appropriate. */ int has_alpha = 0; for (int i = 3; i < texture->width*texture->height*4; i += 4) { if (texture->pixels[i] != 0xFF) { has_alpha = 1; break; } } texture->format = (has_alpha ? (bpp==2 ? TEX_FORMAT_PVRTC2_RGBA : TEX_FORMAT_PVRTC4_RGBA) : (bpp==2 ? TEX_FORMAT_PVRTC2_RGB : TEX_FORMAT_PVRTC4_RGB)); /* Call out to PVRTexToolCLI to actually generate the data for each * mipmap level. */ uint8_t *src = texture->pixels; uint8_t *dest = texture->pixels; unsigned int width = texture->width; unsigned int height = texture->height; uint8_t *temp = malloc(lbound(width,16) * lbound(height,8) * bpp/8 * 2); if (!temp) { fprintf(stderr, "Out of memory compressing to PVRTC\n"); return 0; } for (unsigned int i = 0; i <= texture->mipmaps; i++) { if (!run_pvrtextool(src, width, height, bpp, has_alpha, temp)) { fprintf(stderr, "PVRTC compression failed\n"); return 0; } if (bpp == 4 && width >= 4 && height >= 4 && width == height) { fix_pvrtc4_alpha(src, temp, width, height); } const unsigned int pvr_width = lbound(width, 32/bpp); const unsigned int pvr_height = lbound(height, 8); memcpy(dest, temp, (pvr_width * pvr_height * bpp) / 8); src += width * height * 4; dest += (pvr_width * pvr_height * bpp) / 8; width = lbound(width/2, 1); height = lbound(height/2, 1); } free(temp); return 1; } /*----------------------------------*/ /** * run_pvrtextool: Run the external PVRTexToolCLI program to compress * pixel data. Note that PVRTC data has a minimum encoded size of 8x8 for * 4bpp or 16x8 for 2bpp, regardless of the actual image size, and the * caller must ensure enough output buffer space is available. * * [Parameters] * src: Input pixel data, in RGBA8888 format (destroyed). * width: Texture width, in pixels. * height: Texture height, in pixels. * bpp: Target bits per pixel (2 or 4). * alpha: True if the texture uses the alpha channel, false if the * texture is opaque. * dest: Output data pointer. * [Return value] * True on success, false on error. */ static int run_pvrtextool(const uint8_t *src, unsigned int width, unsigned int height, unsigned int bpp, int alpha, uint8_t *dest) { char dir[1000], infile[1008], outfile[1009]; /* Create a temporary directory for the PVRTexTool input/output files. * We ignore a value of $TMPDIR containing an apostrophe because it'll * wreak havoc with our command line below. */ const char *tmpdir = getenv("TMPDIR"); if (!tmpdir || strchr(tmpdir, '\'') != NULL) { tmpdir = "/tmp"; } if (snprintf(dir, sizeof(dir), "%s/pngtotexXXXXXX", tmpdir) >= (int)sizeof(dir)) { fprintf(stderr, "Buffer overflow on temporary directory\n"); goto error_return; } if (!mkdtemp(dir)) { fprintf(stderr, "Failed to create temporary directory (%s): %s\n", dir, strerror(errno)); goto error_return; } /* These are guaranteed to fit. */ snprintf(infile, sizeof(infile), "%s/in.tga", dir); snprintf(outfile, sizeof(outfile), "%s/out.pvr", dir); /* Create a temporary buffer and massage the pixel data into BGR order, * expanding (with repetition) to the minimum size for the selected * format and deleting the alpha bytes if it's a non-alpha image. */ const unsigned int pvr_width = lbound(width, 32/bpp); const unsigned int pvr_height = lbound(height, 32/bpp); uint8_t *tempbuf = malloc(pvr_width * pvr_height * (alpha ? 4 : 3)); if (!tempbuf) { fprintf(stderr, "Failed to allocate temporary buffer (%u bytes)\n", pvr_width * pvr_height * (alpha ? 4 : 3)); goto error_rmdir; } if (width != pvr_width || height != pvr_height) { uint8_t *out = tempbuf; for (unsigned int y = 0; y < pvr_height; y++) { const uint8_t *row = src + (y % height) * width * 4; for (unsigned int x = 0; x < pvr_width; x++, out += (alpha ? 4 : 3)) { const uint8_t r = row[(x % width) * 4 + 0]; const uint8_t g = row[(x % width) * 4 + 1]; const uint8_t b = row[(x % width) * 4 + 2]; const uint8_t a = row[(x % width) * 4 + 3]; out[0] = b; out[1] = g; out[2] = r; if (alpha) { out[3] = a; } } } } else if (alpha) { for (unsigned int i = 0; i < width*height*4; i += 4) { const uint8_t r = src[i+0]; const uint8_t g = src[i+1]; const uint8_t b = src[i+2]; const uint8_t a = src[i+3]; tempbuf[i+0] = b; tempbuf[i+1] = g; tempbuf[i+2] = r; tempbuf[i+3] = a; } spread_border(tempbuf, width, height); } else { for (unsigned int i = 0, j = 0; i < width*height*4; i += 4, j += 3) { const uint8_t r = src[i+0]; const uint8_t g = src[i+1]; const uint8_t b = src[i+2]; tempbuf[j+0] = b; tempbuf[j+1] = g; tempbuf[j+2] = r; } } /* Write the pixel data as a simple TGA file. */ FILE *f = fopen(infile, "wb"); if (!f) { fprintf(stderr, "Failed to create %s: %s\n", infile, strerror(errno)); goto error_free_tempbuf; } uint8_t tga_header[18]; memcpy(tga_header, "\0\0\2\0\0\0\0\0\0\0\0\0", 12); tga_header[12] = pvr_width >>0 & 0xFF; tga_header[13] = pvr_width >>8 & 0xFF; tga_header[14] = pvr_height>>0 & 0xFF; tga_header[15] = pvr_height>>8 & 0xFF; tga_header[16] = alpha ? 32 : 24; tga_header[17] = 0; if (fwrite(tga_header, 18, 1, f) != 1) { fprintf(stderr, "Failed to write header to %s: %s\n", infile, strerror(errno)); fclose(f); goto error_remove_infile; } const unsigned int rowsize = alpha ? pvr_width*4 : pvr_width*3; /* PVRTexTool expects the data in bottom-up order. */ for (int y = pvr_height-1; y >= 0; y--) { if (fwrite(tempbuf + y*rowsize, rowsize, 1, f) != 1) { fprintf(stderr, "Failed to write row %d data to %s: %s\n", y, infile, strerror(errno)); fclose(f); goto error_remove_infile; } } if (fclose(f) != 0) { fprintf(stderr, "Failed to close %s: %s\n", infile, strerror(errno)); goto error_remove_infile; } /* Call out to PVRTexTool. */ char cmdbuf[4000]; // Long enough to avoid truncation. snprintf(cmdbuf, sizeof(cmdbuf), "%s -f PVRTC1_%u%s -i '%s' -q pvrtc%s" " -o '%s'%s", pvrtextool, bpp, alpha ? "" : "_RGB", infile, hq ? "best" : "normal", outfile, verbose ? "" : " >/dev/null 2>&1"); if (verbose) { fprintf(stderr, "Executing: %s\n", cmdbuf); } if (system(cmdbuf) != 0) { fprintf(stderr, "PVRTexToolCLI call failed%s\n", verbose ? "" : " (use -verbose to see errors)"); if (strchr(pvrtextool, '/')) { fprintf(stderr, "Check that the path to the PVRTexToolCLI program" " is correct.\n"); } else { fprintf(stderr, "Check that the \"%s\" program can be found in" " your PATH.\n", pvrtextool); } goto error_remove_outfile; } /* Read the compressed pixel data from the output file. */ f = fopen(outfile, "r"); if (!f) { fprintf(stderr, "Failed to open %s: %s\n", outfile, strerror(errno)); goto error_remove_outfile; } uint8_t pvr_headerlen_buf[4]; if (fread(pvr_headerlen_buf, 4, 1, f) != 1) { fprintf(stderr, "Failed to read header length from %s\n", outfile); fclose(f); goto error_remove_outfile; } uint32_t pvr_headerlen; if (memcmp(pvr_headerlen_buf, "PVR\3", 4) == 0) { /* PVR version 3 files have a fixed header size of 52 bytes plus * an arbitrary amount of metadata. */ pvr_headerlen = 52; if (fseek(f, 48, SEEK_SET) != 0) { fprintf(stderr, "Failed to seek to metadata in %s: %s\n", outfile, strerror(errno)); fclose(f); goto error_remove_outfile; } if (fread(pvr_headerlen_buf, 4, 1, f) != 1) { fprintf(stderr, "Failed to read metadata length from %s\n", outfile); fclose(f); goto error_remove_outfile; } pvr_headerlen += pvr_headerlen_buf[0]<< 0 | pvr_headerlen_buf[1]<< 8 | pvr_headerlen_buf[2]<<16 | pvr_headerlen_buf[3]<<24; } else { pvr_headerlen = pvr_headerlen_buf[0]<< 0 | pvr_headerlen_buf[1]<< 8 | pvr_headerlen_buf[2]<<16 | pvr_headerlen_buf[3]<<24; } if (fseek(f, pvr_headerlen, SEEK_SET) != 0) { fprintf(stderr, "Failed to seek to data in %s: %s\n", outfile, strerror(errno)); fclose(f); goto error_remove_outfile; } if (fread(dest, (pvr_width * pvr_height * bpp) / 8, 1, f) != 1) { fprintf(stderr, "Failed to read data from %s\n", outfile); fclose(f); goto error_remove_outfile; } fclose(f); /* All done! Clean up our files/buffers and return. */ remove(outfile); remove(infile); rmdir(dir); free(tempbuf); return 1; /* Handle all error returns here so we don't leave junk behind. */ error_remove_outfile: remove(outfile); error_remove_infile: remove(infile); error_free_tempbuf: free(tempbuf); error_rmdir: rmdir(dir); error_return: return 0; } /*-----------------------------------------------------------------------*/ /** * convert_rgba_to_bgra: Convert the given texture (which must be in the * RGBA8888 format) to BGRA8888 format. The texture buffer is _not_ * reallocated. * * [Parameters] * texture: Texture to convert. */ static void convert_rgba_to_bgra(Texture *texture) { ASSERT(texture->format == TEX_FORMAT_RGBA8888); const int num_pixels = texture->width * texture->height; uint8_t *pixels = texture->pixels; for (int i = 0; i < num_pixels; i++) { uint8_t temp = pixels[i*4+0]; pixels[i*4+0] = pixels[i*4+2]; pixels[i*4+2] = temp; } texture->format = TEX_FORMAT_BGRA8888; } /*************************************************************************/ /******************** Miscellaneous utility functions ********************/ /*************************************************************************/ /** * spread_border: Spread the border colors of the image throughout its * transparent region, to avoid "white halo" effect from texture * compression. * * [Parameters] * pixels: Pixel buffer, in RGBA format. * width: Image width, in pixels. * height: Image height, in pixels. */ static void spread_border(uint8_t * const pixels, const unsigned int width, const unsigned int height) { uint8_t *saved_alpha = malloc(width*height); if (!saved_alpha) { fprintf(stderr, "Out of memory (can't allocate %u bytes)\n", width*height); exit(1); } { for (unsigned int i = 0; i < width*height; i++) { saved_alpha[i] = pixels[i*4+3]; } } uint8_t *updated = calloc(width*height, 1); if (!updated) { fprintf(stderr, "Out of memory (can't allocate %u bytes)\n", width*height); exit(1); } int changed; do { changed = 0; for (unsigned int i = 0, y = 0; y < height; y++) { for (unsigned int x = 0; x < width; x++, i += 4) { if (pixels[i+3] == 0) { unsigned int r = 0, g = 0, b = 0, weight = 0; unsigned int j; if (y > 0) { if (x > 0) { j = i - width*4 - 4; if (pixels[j+3] != 0) { const unsigned int w = pixels[j+3] * 7; r += pixels[j+0] * w; g += pixels[j+1] * w; b += pixels[j+2] * w; weight += w; } } j = i - width*4; if (pixels[j+3] != 0) { const unsigned int w = pixels[j+3] * 10; r += pixels[j+0] * w; g += pixels[j+1] * w; b += pixels[j+2] * w; weight += w; } if (x < width-1) { j = i - width*4 + 4; if (pixels[j+3] != 0) { const unsigned int w = pixels[j+3] * 7; r += pixels[j+0] * w; g += pixels[j+1] * w; b += pixels[j+2] * w; weight += w; } } } if (x > 0) { j = i - 4; if (pixels[j+3] != 0) { const unsigned int w = pixels[j+3] * 10; r += pixels[j+0] * w; g += pixels[j+1] * w; b += pixels[j+2] * w; weight += w; } } if (x < width-1) { j = i + 4; if (pixels[j+3] != 0) { const unsigned int w = pixels[j+3] * 10; r += pixels[j+0] * w; g += pixels[j+1] * w; b += pixels[j+2] * w; weight += w; } } if (y < height-1) { if (x > 0) { j = i + width*4 - 4; if (pixels[j+3] != 0) { const unsigned int w = pixels[j+3] * 7; r += pixels[j+0] * w; g += pixels[j+1] * w; b += pixels[j+2] * w; weight += w; } } j = i + width*4; if (pixels[j+3] != 0) { const unsigned int w = pixels[j+3] * 10; r += pixels[j+0] * w; g += pixels[j+1] * w; b += pixels[j+2] * w; weight += w; } if (x < width-1) { j = i + width*4 + 4; if (pixels[j+3] != 0) { const unsigned int w = pixels[j+3] * 7; r += pixels[j+0] * w; g += pixels[j+1] * w; b += pixels[j+2] * w; weight += w; } } } if (weight > 0) { pixels[i+0] = (r + weight/2) / weight; pixels[i+1] = (g + weight/2) / weight; pixels[i+2] = (b + weight/2) / weight; updated[i/4] = 1; changed = 1; } } // if (pixels[i+3] == 0) } // for (x = 0; x < width; x++, i += 4) } // for (i = y = 0; y < height; y++) for (unsigned int i = 0; i < width*height; i++) { if (updated[i]) { pixels[i*4+3] = 1; // Don't recalculate it next time. } } } while (changed); free(updated); { for (unsigned int i = 0; i < width*height; i++) { pixels[i*4+3] = saved_alpha[i]; } } free(saved_alpha); } /*-----------------------------------------------------------------------*/ /** * fix_pvrtc4_alpha: Correct the "ring" effect in PVRTexTool-compressed * textures by recalculating the modulation codes for each pixel using an * alpha-aware color-difference algorithm, and switching blocks to * punch-through alpha mode where necessary to ensure that transparent * pixels stay transparent. * * [Parameters] * original: Original RGBA pixel data. * compressed: PVRTC4-compressed pixel data. * width: Texture width, in pixels. * height: Texture height, in pixels (must be equal to width). */ static void fix_pvrtc4_alpha(const uint8_t *original, uint8_t *compressed, unsigned int width, unsigned int height) { const uint8_t *src = original; for (unsigned int y = 0; y < height/4; y++, src += (3*width)*4) { for (unsigned int x = 0; x < width/4; x++, src += 4*4) { redo:; /* Load the data words for this block and the 8 surrounding it. */ uint32_t block_data[3][3]; unsigned int yy; int block_y; for (yy = 0, block_y = (int)y-1; yy < 3; yy++, block_y++) { int real_y = (block_y + (height/4)) % (height/4); unsigned int xx; int block_x; for (xx = 0, block_x = (int)x-1; xx < 3; xx++, block_x++) { int real_x = (block_x + (width/4)) % (width/4); /* Inverted X and Y are intentional here -- apparently * PVRTC puts Y in the lowest bit. */ const uint8_t *block = compressed + 8*morton_index(real_y, real_x, height/4, width/4); block_data[yy][xx] = block[4] << 0 | block[5] << 8 | block[6] << 16 | block[7] << 24; } } /* Extract the block mode for this block. */ const unsigned int mode = block_data[1][1] & 1; /* Interpolate to find the effective color pairs for each * pixel in the block. */ unsigned int rAs[4][4], gAs[4][4], bAs[4][4], aAs[4][4]; unsigned int rBs[4][4], gBs[4][4], bBs[4][4], aBs[4][4]; for (yy = 0; yy < 4; yy++) { const unsigned int yfrac = (yy+2) % 4; uint32_t *row0, *row1; if (yy < 2) { row0 = block_data[0]; row1 = block_data[1]; } else { row0 = block_data[1]; row1 = block_data[2]; } unsigned int rA00, gA00, bA00, aA00; unsigned int rA01, gA01, bA01, aA01; unsigned int rA10, gA10, bA10, aA10; unsigned int rA11, gA11, bA11, aA11; unsigned int rB00, gB00, bB00, aB00; unsigned int rB01, gB01, bB01, aB01; unsigned int rB10, gB10, bB10, aB10; unsigned int rB11, gB11, bB11, aB11; block_data_to_colors(row0[0], &rA00, &gA00, &bA00, &aA00, &rB00, &gB00, &bB00, &aB00); block_data_to_colors(row0[1], &rA01, &gA01, &bA01, &aA01, &rB01, &gB01, &bB01, &aB01); block_data_to_colors(row1[0], &rA10, &gA10, &bA10, &aA10, &rB10, &gB10, &bB10, &aB10); block_data_to_colors(row1[1], &rA11, &gA11, &bA11, &aA11, &rB11, &gB11, &bB11, &aB11); for (unsigned int xx = 0; xx < 4; xx++) { const unsigned int xfrac = (xx+2) % 4; const unsigned int weight00 = (4-xfrac)*(4-yfrac); const unsigned int weight01 = ( xfrac)*(4-yfrac); const unsigned int weight10 = (4-xfrac)*( yfrac); const unsigned int weight11 = ( xfrac)*( yfrac); if (xx == 2) { row0++; row1++; block_data_to_colors(row0[0], &rA00, &gA00, &bA00, &aA00, &rB00, &gB00, &bB00, &aB00); block_data_to_colors(row0[1], &rA01, &gA01, &bA01, &aA01, &rB01, &gB01, &bB01, &aB01); block_data_to_colors(row1[0], &rA10, &gA10, &bA10, &aA10, &rB10, &gB10, &bB10, &aB10); block_data_to_colors(row1[1], &rA11, &gA11, &bA11, &aA11, &rB11, &gB11, &bB11, &aB11); } rAs[yy][xx] = (rA00*weight00 + rA01*weight01 + rA10*weight10 + rA11*weight11) / 16; gAs[yy][xx] = (gA00*weight00 + gA01*weight01 + gA10*weight10 + gA11*weight11) / 16; bAs[yy][xx] = (bA00*weight00 + bA01*weight01 + bA10*weight10 + bA11*weight11) / 16; aAs[yy][xx] = (aA00*weight00 + aA01*weight01 + aA10*weight10 + aA11*weight11) / 16; rBs[yy][xx] = (rB00*weight00 + rB01*weight01 + rB10*weight10 + rB11*weight11) / 16; gBs[yy][xx] = (gB00*weight00 + gB01*weight01 + gB10*weight10 + gB11*weight11) / 16; bBs[yy][xx] = (bB00*weight00 + bB01*weight01 + bB10*weight10 + bB11*weight11) / 16; aBs[yy][xx] = (aB00*weight00 + aB01*weight01 + aB10*weight10 + aB11*weight11) / 16; } } /* Recalculate the modulation values for this block, using a * color-difference algorithm that ensures all transparent * pixels stay transparent. */ uint8_t *block = compressed + 8*morton_index(y, x, height/4, width/4); const uint8_t *src_row; for (src_row = src, yy = 0; yy < 4; yy++, src_row += width*4) { for (unsigned int xx = 0; xx < 4; xx++) { /* Pull out colors A and B from the precomputed array. */ const unsigned int rA = rAs[yy][xx]; const unsigned int gA = gAs[yy][xx]; const unsigned int bA = bAs[yy][xx]; const unsigned int aA = aAs[yy][xx]; const unsigned int rB = rBs[yy][xx]; const unsigned int gB = gBs[yy][xx]; const unsigned int bB = bBs[yy][xx]; const unsigned int aB = aBs[yy][xx]; /* Colors C and D correspond to modulation values 01 * and 10, respectively. */ unsigned int rC, gC, bC, aC; unsigned int rD, gD, bD, aD; if (mode == 0) { rC = (rA*5 + rB*3) / 8; gC = (gA*5 + gB*3) / 8; bC = (bA*5 + bB*3) / 8; aC = (aA*5 + aB*3) / 8; rD = (rA*3 + rB*5) / 8; gD = (gA*3 + gB*5) / 8; bD = (bA*3 + bB*5) / 8; aD = (aA*3 + aB*5) / 8; } else { // mode == 1 rC = rD = (rA + rB) / 2; gC = gD = (gA + gB) / 2; bC = bD = (bA + bB) / 2; aC = (aA + aB) / 2; aD = 0; } /* Load the colors for this pixel. */ const unsigned int r = src_row[xx*4+0]; const unsigned int g = src_row[xx*4+1]; const unsigned int b = src_row[xx*4+2]; const unsigned int a = src_row[xx*4+3]; /* If this is a transparent pixel and none of the * available colors is transparent, switch to block * mode 1 and reprocess the block. */ if (a == 0 && aA != 0 && aB != 0 && aC != 0 && aD != 0) { block[4] |= 1; goto redo; } /* Find the best-fit modulation value for this pixel. */ const uint32_t dAsq = colordiff_sq(r,g,b,a, rA,gA,bA,aA); const uint32_t dBsq = colordiff_sq(r,g,b,a, rB,gB,bB,aB); const uint32_t dCsq = colordiff_sq(r,g,b,a, rC,gC,bC,aC); const uint32_t dDsq = colordiff_sq(r,g,b,a, rD,gD,bD,aD); uint8_t M; if (dAsq <= dBsq && dAsq <= dCsq && dAsq <= dDsq) { M = 0; } else if (dBsq <= dAsq && dBsq <= dCsq && dBsq <= dDsq) { M = 3; } else if (dCsq <= dAsq && dCsq <= dBsq && dCsq <= dDsq) { M = 1; } else { M = 2; } /* Replace the pixel's modulation value in the * compressed data. */ block[yy] &= ~(3 << (xx*2)); block[yy] |= M << (xx*2); } } } } } /*----------------------------------*/ /** * morton_index: Return the Morton (Z-order) index for the given X and Y * coordinates. Helper function for do_pvrtc4(). * * [Parameters] * x, y: Coordinates to convert. * w, h: Range (max+1) of the X and Y coordinates (must be powers of two). * [Return value] * Corresponding Morton index. */ static CONST_FUNCTION int morton_index(unsigned int x, unsigned int y, unsigned int w, unsigned int h) { int index = 0, shift = 0; while (w > 0 || h > 0) { if (w > 0) { index |= (x & 1) << shift; shift++; x >>= 1; w >>= 1; } if (h > 0) { index |= (y & 1) << shift; shift++; y >>= 1; h >>= 1; } } return index; } /*----------------------------------*/ /** * block_data_to_colors: Extract the color components of the two colors * specified by the given block's data word. * * [Parameters] * block_data: 32-bit block data word. * rA, gA, bA, aA: Pointers to variables to receive the RGBA components * of color A. * rB, gB, bB, aB: Pointers to variables to receive the RGBA components * of color B. */ static inline void block_data_to_colors( uint32_t block_data, unsigned int *rA, unsigned int *gA, unsigned int *bA, unsigned int *aA, unsigned int *rB, unsigned int *gB, unsigned int *bB, unsigned int *aB) { const unsigned int color_A = block_data & 0xFFFE; const unsigned int color_B = block_data >> 16; if (color_A & 0x8000) { *rA = (color_A>>10 & 0x1F) * 33 / 4; *gA = (color_A>> 5 & 0x1F) * 33 / 4; *bA = ((color_A>>1 & 0x0F) << 1 | (color_A>>4 & 1)) * 33 / 4; *aA = 0xFF; } else { *rA = ((color_A>> 8 & 0xF) << 1 | (color_A>>11 & 1)) * 33 / 4; *gA = ((color_A>> 4 & 0xF) << 1 | (color_A>> 7 & 1)) * 33 / 4; *bA = ((color_A>> 1 & 0x7) << 2 | (color_A>> 2 & 3)) * 33 / 4; *aA = ((color_A>>12 & 0x7) << 1) * 17; } if (color_B & 0x8000) { *rB = (color_B>>10 & 0x1F) * 33 / 4; *gB = (color_B>> 5 & 0x1F) * 33 / 4; *bB = (color_B>> 0 & 0x1F) * 33 / 4; *aB = 0xFF; } else { *rB = ((color_B>> 8 & 0xF) << 1 | (color_B>>11 & 1)) * 33 / 4; *gB = ((color_B>> 4 & 0xF) << 1 | (color_B>> 7 & 1)) * 33 / 4; *bB = ((color_B>> 0 & 0xF) << 1 | (color_B>> 3 & 1)) * 33 / 4; *aB = ((color_B>>12 & 0x7) << 1) * 17; } } /*----------------------------------*/ /** * colordiff_sq: Return the square of the 4-dimensional color difference * between the two given color values, taking into account alpha values. * (RGB component differences between low-alpha pixels are not as important * as between high-alpha pixels, but alpha component differences are always * important.) * * [Parameters] * r1, g1, b1, a1: RGBA components of color 1. * r2, g2, b2, a2: RGBA components of color 2. * [Return value] * Square of color difference (unsigned 32-bit value). */ static CONST_FUNCTION inline uint32_t colordiff_sq( unsigned int r1, unsigned int g1, unsigned int b1, unsigned int a1, unsigned int r2, unsigned int g2, unsigned int b2, unsigned int a2) { const int dr = (int)r1 - (int)r2; const int dg = (int)g1 - (int)g2; const int db = (int)b1 - (int)b2; const int da = (int)a1 - (int)a2; /* Divide each component's difference by 4 to avoid integer overflow. * (This is the same algorithm used by colordiff_sq() in quantize.c for * 8bpp color quantization.) */ return ((uint32_t)(dr*dr) * (a1*a2+1)) / 4 + ((uint32_t)(dg*dg) * (a1*a2+1)) / 4 + ((uint32_t)(db*db) * (a1*a2+1)) / 4 + ((uint32_t)(da*da) * (255*255+1)) / 4; } /*-----------------------------------------------------------------------*/ /** * free_texture: Free all memory associated with the given texture. * Does nothing if texture == NULL. * * [Parameters] * texture: Texture to free. */ static void free_texture(Texture *texture) { if (texture) { free(texture->palette); free(texture->opaque_bitmap); free(texture); } } /*************************************************************************/ /*************************************************************************/