cnq3/code/renderer/tr_image.cpp
myT 8ef496d22b reverting to old code for .tga image decoding, but with tweaks and fixes
the problem is that stb_image can and will allocate much more than it needs to
e.g. for a 2048x2048 BGR image:
it allocates an unnecessary intermediate 12 MB buffer to decode the image
instead of decoding it directly into the final 16 MB RGBA buffer

the old CNQ3 code didn't decode greyscale properly because of a missing macro call
it also didn't range-check memory accesses at all
2020-02-18 04:42:28 +01:00

1172 lines
35 KiB
C++

/*
===========================================================================
Copyright (C) 1999-2005 Id Software, Inc.
This file is part of Quake III Arena source code.
Quake III Arena source code is free software; you can redistribute it
and/or modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of the License,
or (at your option) any later version.
Quake III Arena source code is distributed in the hope that it will be
useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Quake III Arena source code; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
===========================================================================
*/
// tr_image.c
#include "tr_local.h"
#include <setjmp.h>
#if defined (_MSC_VER)
# pragma warning (disable: 4611) // setjmp and C++ destructors
#endif
// colors are pre-multiplied, alpha indicates whether blending should occur
const vec4_t r_mipBlendColors[16] = {
{ 0.0f, 0.0f, 0.0f, 0.0f },
{ 0.5f, 0.0f, 0.0f, 1.0f },
{ 0.0f, 0.5f, 0.0f, 1.0f },
{ 0.0f, 0.0f, 0.5f, 1.0f },
{ 0.5f, 0.0f, 0.0f, 1.0f },
{ 0.0f, 0.5f, 0.0f, 1.0f },
{ 0.0f, 0.0f, 0.5f, 1.0f },
{ 0.5f, 0.0f, 0.0f, 1.0f },
{ 0.0f, 0.5f, 0.0f, 1.0f },
{ 0.0f, 0.0f, 0.5f, 1.0f },
{ 0.5f, 0.0f, 0.0f, 1.0f },
{ 0.0f, 0.5f, 0.0f, 1.0f },
{ 0.0f, 0.0f, 0.5f, 1.0f },
{ 0.5f, 0.0f, 0.0f, 1.0f },
{ 0.0f, 0.5f, 0.0f, 1.0f },
{ 0.0f, 0.0f, 0.5f, 1.0f }
};
// colors are not pre-multiplied
static const byte mipBlendColors[16][4] = {
{ 0, 0, 0, 0 },
{ 255, 0, 0, 128 },
{ 0, 255, 0, 128 },
{ 0, 0, 255, 128 },
{ 255, 0, 0, 128 },
{ 0, 255, 0, 128 },
{ 0, 0, 255, 128 },
{ 255, 0, 0, 128 },
{ 0, 255, 0, 128 },
{ 0, 0, 255, 128 },
{ 255, 0, 0, 128 },
{ 0, 255, 0, 128 },
{ 0, 0, 255, 128 },
{ 255, 0, 0, 128 },
{ 0, 255, 0, 128 },
{ 0, 0, 255, 128 }
};
#define IMAGE_HASH_SIZE 1024
static image_t* hashTable[IMAGE_HASH_SIZE];
static byte s_intensitytable[256];
void R_ImageList_f( void )
{
const char* const match = Cmd_Argc() > 1 ? Cmd_Argv( 1 ) : NULL;
ri.Printf( PRINT_ALL, "\nwide high MPI W format name\n" );
int totalByteCount = 0;
int imageCount = 0;
for ( int i = 0; i < tr.numImages; ++i ) {
const image_t* image = tr.images[i];
if ( match && !Com_Filter( match, image->name ) )
continue;
const int byteCount = image->width * image->height * (image->format == TF_RGBA8 ? 4 : 1);
if ( !(image->flags & IMG_NOMIPMAP) && (image->width > 1) && (image->height > 1) )
totalByteCount += (byteCount * 4) / 3; // not exact, but good enough
else
totalByteCount += byteCount;
imageCount++;
ri.Printf( PRINT_ALL, "%4i %4i %c%c%c ",
image->width, image->height,
(image->flags & IMG_NOMIPMAP) ? ' ' : 'M',
(image->flags & IMG_NOPICMIP) ? ' ' : 'P',
(image->flags & IMG_NOIMANIP) ? ' ' : 'I'
);
switch ( image->wrapClampMode ) {
case TW_REPEAT: ri.Printf( PRINT_ALL, "R " ); break;
case TW_CLAMP_TO_EDGE: ri.Printf( PRINT_ALL, "E " ); break;
default: ri.Printf( PRINT_ALL, "? " ); break;
}
switch ( image->format ) {
case TF_RGBA8: ri.Printf( PRINT_ALL, "RGBA8 " ); break;
default: ri.Printf( PRINT_ALL, "%5i ", image->format ); break;
}
ri.Printf( PRINT_ALL, " %s\n", image->name );
}
const char* units[] = { "KB", "MB", "GB", "TB" };
int amount = totalByteCount >> 10;
int unit = 0;
while ( amount >= 1024 ) {
amount >>= 10;
++unit;
}
ri.Printf( PRINT_ALL, "---------\n" );
ri.Printf( PRINT_ALL, "%i images found\n", imageCount );
ri.Printf( PRINT_ALL, "Estimated VRAM use: %i %s\n\n", amount, units[unit] );
}
///////////////////////////////////////////////////////////////
/*
================
Used to resample images in a more general than quartering fashion.
This will only be filtered properly if the resampled size
is greater than half the original size.
If a larger shrinking is needed, use the mipmap function
before or after.
================
*/
static void ResampleTexture( unsigned *in, int inwidth, int inheight, unsigned *out,
int outwidth, int outheight ) {
int i, j;
unsigned *inrow, *inrow2;
unsigned frac, fracstep;
unsigned p1[2048], p2[2048];
byte *pix1, *pix2, *pix3, *pix4;
if (outwidth>2048)
ri.Error(ERR_DROP, "ResampleTexture: max width");
fracstep = inwidth*0x10000/outwidth;
frac = fracstep>>2;
for ( i=0 ; i<outwidth ; i++ ) {
p1[i] = 4*(frac>>16);
frac += fracstep;
}
frac = 3*(fracstep>>2);
for ( i=0 ; i<outwidth ; i++ ) {
p2[i] = 4*(frac>>16);
frac += fracstep;
}
for (i=0 ; i<outheight ; i++, out += outwidth) {
inrow = in + inwidth*(int)((i+0.25)*inheight/outheight);
inrow2 = in + inwidth*(int)((i+0.75)*inheight/outheight);
frac = fracstep >> 1;
for (j=0 ; j<outwidth ; j++) {
pix1 = (byte *)inrow + p1[j];
pix2 = (byte *)inrow + p2[j];
pix3 = (byte *)inrow2 + p1[j];
pix4 = (byte *)inrow2 + p2[j];
((byte *)(out+j))[0] = (pix1[0] + pix2[0] + pix3[0] + pix4[0])>>2;
((byte *)(out+j))[1] = (pix1[1] + pix2[1] + pix3[1] + pix4[1])>>2;
((byte *)(out+j))[2] = (pix1[2] + pix2[2] + pix3[2] + pix4[2])>>2;
((byte *)(out+j))[3] = (pix1[3] + pix2[3] + pix3[3] + pix4[3])>>2;
}
}
}
// scale up the pixel values in a texture to increase the lighting range
static void R_LightScaleTexture( byte* p, int width, int height )
{
const int pixels = width * height;
for (int i = 0 ; i < pixels; ++i) {
p[0] = s_intensitytable[p[0]];
p[1] = s_intensitytable[p[1]];
p[2] = s_intensitytable[p[2]];
p += 4;
}
}
// operates in place, quartering the size of the texture - proper linear filter
static void R_MipMap( unsigned* in, int inWidth, int inHeight )
{
int i, j, k;
byte *outpix;
int total;
int outWidth = inWidth >> 1;
int outHeight = inHeight >> 1;
unsigned* temp = (unsigned*)ri.Hunk_AllocateTempMemory( outWidth * outHeight * 4 );
int inWidthMask = inWidth - 1;
int inHeightMask = inHeight - 1;
for ( i = 0 ; i < outHeight ; i++ ) {
for ( j = 0 ; j < outWidth ; j++ ) {
outpix = (byte *) ( temp + i * outWidth + j );
for ( k = 0 ; k < 4 ; k++ ) {
total =
1 * ((byte *)&in[ ((i*2-1)&inHeightMask)*inWidth + ((j*2-1)&inWidthMask) ])[k] +
2 * ((byte *)&in[ ((i*2-1)&inHeightMask)*inWidth + ((j*2)&inWidthMask) ])[k] +
2 * ((byte *)&in[ ((i*2-1)&inHeightMask)*inWidth + ((j*2+1)&inWidthMask) ])[k] +
1 * ((byte *)&in[ ((i*2-1)&inHeightMask)*inWidth + ((j*2+2)&inWidthMask) ])[k] +
2 * ((byte *)&in[ ((i*2)&inHeightMask)*inWidth + ((j*2-1)&inWidthMask) ])[k] +
4 * ((byte *)&in[ ((i*2)&inHeightMask)*inWidth + ((j*2)&inWidthMask) ])[k] +
4 * ((byte *)&in[ ((i*2)&inHeightMask)*inWidth + ((j*2+1)&inWidthMask) ])[k] +
2 * ((byte *)&in[ ((i*2)&inHeightMask)*inWidth + ((j*2+2)&inWidthMask) ])[k] +
2 * ((byte *)&in[ ((i*2+1)&inHeightMask)*inWidth + ((j*2-1)&inWidthMask) ])[k] +
4 * ((byte *)&in[ ((i*2+1)&inHeightMask)*inWidth + ((j*2)&inWidthMask) ])[k] +
4 * ((byte *)&in[ ((i*2+1)&inHeightMask)*inWidth + ((j*2+1)&inWidthMask) ])[k] +
2 * ((byte *)&in[ ((i*2+1)&inHeightMask)*inWidth + ((j*2+2)&inWidthMask) ])[k] +
1 * ((byte *)&in[ ((i*2+2)&inHeightMask)*inWidth + ((j*2-1)&inWidthMask) ])[k] +
2 * ((byte *)&in[ ((i*2+2)&inHeightMask)*inWidth + ((j*2)&inWidthMask) ])[k] +
2 * ((byte *)&in[ ((i*2+2)&inHeightMask)*inWidth + ((j*2+1)&inWidthMask) ])[k] +
1 * ((byte *)&in[ ((i*2+2)&inHeightMask)*inWidth + ((j*2+2)&inWidthMask) ])[k];
outpix[k] = total / 36;
}
}
}
Com_Memcpy( in, temp, outWidth * outHeight * 4 );
ri.Hunk_FreeTempMemory( temp );
}
// apply a color blend over a set of pixels - used for r_colorMipLevels
static void R_BlendOverTexture( byte *data, int pixelCount, const byte blend[4] )
{
int premult[3];
int inverseAlpha = 255 - blend[3];
premult[0] = blend[0] * blend[3];
premult[1] = blend[1] * blend[3];
premult[2] = blend[2] * blend[3];
for (int i = 0; i < pixelCount; ++i, data+=4) {
data[0] = ( data[0] * inverseAlpha + premult[0] ) >> 9;
data[1] = ( data[1] * inverseAlpha + premult[1] ) >> 9;
data[2] = ( data[2] * inverseAlpha + premult[2] ) >> 9;
}
}
static int ComputeMipCount( int scaled_width, int scaled_height )
{
int mipCount = 1;
while ( scaled_width > 1 || scaled_height > 1 ) {
scaled_width = max( scaled_width >> 1, 1 );
scaled_height = max( scaled_height >> 1, 1 );
++mipCount;
}
return mipCount;
}
// note that the "32" here is for the image's STRIDE - it has nothing to do with the actual COMPONENTS
static void Upload32( image_t* image, unsigned int* data )
{
// atlases we generate ourselves
if ( image->flags & IMG_LMATLAS ) {
image->flags |= IMG_NOMIPMAP;
image->flags |= IMG_NOAF;
gal.CreateTexture( image, 1, image->width, image->height );
return;
}
// atlases loaded from images on disk
if ( Q_stristr( image->name, "maps/" ) == image->name &&
Q_stristr( image->name + 5, "/lm_" ) != NULL ) {
image->flags |= IMG_NOPICMIP;
image->flags |= IMG_NOMIPMAP;
image->flags |= IMG_NOAF;
image->flags |= IMG_EXTLMATLAS;
if ( r_mapBrightness->value != 1.0f ) {
const int pixelCount = image->width * image->height;
byte* pixel = (byte*)data;
byte* const pixelEnd = (byte*)( data + pixelCount );
while ( pixel < pixelEnd ) {
R_ColorShiftLightingBytes( pixel, pixel );
pixel += 4;
}
}
}
int scaled_width, scaled_height;
// convert to exact power of 2 sizes
//
for ( scaled_width = 1; scaled_width < image->width; scaled_width <<= 1 )
;
for ( scaled_height = 1; scaled_height < image->height; scaled_height <<=1 )
;
if ( r_roundImagesDown->integer && scaled_width > image->width )
scaled_width >>= 1;
if ( r_roundImagesDown->integer && scaled_height > image->height )
scaled_height >>= 1;
RI_AutoPtr pResampled;
if ( scaled_width != image->width || scaled_height != image->height ) {
pResampled.Alloc( scaled_width * scaled_height * 4 );
ResampleTexture( data, image->width, image->height, pResampled.Get<unsigned int>(), scaled_width, scaled_height );
data = pResampled.Get<unsigned int>();
image->width = scaled_width;
image->height = scaled_height;
ri.Printf( PRINT_DEVELOPER, "^3WARNING: ^7'%s' doesn't have PoT dimensions.\n", image->name );
}
// perform optional picmip operation
if ( !(image->flags & IMG_NOPICMIP) ) {
scaled_width >>= r_picmip->integer;
scaled_height >>= r_picmip->integer;
}
// clamp to minimum size
scaled_width = max( scaled_width, 1 );
scaled_height = max( scaled_height, 1 );
// clamp to the current upper OpenGL limit
// scale both axis down equally so we don't have to
// deal with a half mip resampling
//
while ( scaled_width > glInfo.maxTextureSize || scaled_height > glInfo.maxTextureSize ) {
scaled_width >>= 1;
scaled_height >>= 1;
}
if ( glInfo.mipGenSupport && image->format == TF_RGBA8 && ( image->flags & IMG_NOMIPMAP ) == 0 ) {
const int w = image->width;
const int h = image->height;
const int mipCount = ComputeMipCount( w, h );
int mipOffset = 0;
while ( image->width > scaled_width || image->height > scaled_height ) {
image->width = max( image->width >> 1, 1 );
image->height = max( image->height >> 1, 1 );
mipOffset++;
}
gal.CreateTextureEx( image, mipCount, mipOffset, w, h, data );
return;
}
RI_AutoPtr pScaled( sizeof(unsigned) * scaled_width * scaled_height );
// copy or resample data as appropriate for first MIP level
if ( ( scaled_width == image->width ) && ( scaled_height == image->height ) ) {
if ( image->flags & IMG_NOMIPMAP ) {
gal.CreateTexture( image, 1, image->width, image->height );
gal.UpdateTexture( image, 0, 0, 0, image->width, image->height, data );
return;
}
Com_Memcpy( pScaled, data, image->width * image->height * 4 );
}
else
{
// use the normal mip-mapping function to go down from here
while ( image->width > scaled_width || image->height > scaled_height ) {
R_MipMap( (unsigned*)data, image->width, image->height );
image->width = max( image->width >> 1, 1 );
image->height = max( image->height >> 1, 1 );
}
Com_Memcpy( pScaled, data, image->width * image->height * 4 );
}
if ( !(image->flags & IMG_NOIMANIP) )
R_LightScaleTexture( pScaled.Get<byte>(), scaled_width, scaled_height );
const int mipCount = ( image->flags & IMG_NOMIPMAP ) ? 1 : ComputeMipCount( scaled_width, scaled_height );
gal.CreateTexture( image, mipCount, scaled_width, scaled_height );
gal.UpdateTexture( image, 0, 0, 0, scaled_width, scaled_height, pScaled );
if ( !(image->flags & IMG_NOMIPMAP) )
{
int miplevel = 0;
while (scaled_width > 1 || scaled_height > 1)
{
R_MipMap( pScaled.Get<unsigned>(), scaled_width, scaled_height );
scaled_width = max( scaled_width >> 1, 1 );
scaled_height = max( scaled_height >> 1, 1 );
++miplevel;
if ( r_colorMipLevels->integer )
R_BlendOverTexture( pScaled, scaled_width * scaled_height, mipBlendColors[miplevel] );
gal.UpdateTexture( image, miplevel, 0, 0, scaled_width, scaled_height, pScaled );
}
}
}
void R_UploadLightmapTile( image_t* image, byte* pic, int x, int y, int width, int height )
{
if ( !(image->flags & IMG_LMATLAS) )
ri.Error( ERR_DROP, "R_UploadLightmapTile: IMG_LMATLAS flag not defined\n" );
gal.UpdateTexture( image, 0, x, y, width, height, pic );
}
// this is the only way any image_t are created
// !!! i'm pretty sure this DOESN'T work correctly for non-POT images
image_t* R_CreateImage( const char* name, byte* pic, int width, int height, textureFormat_t format, int flags, textureWrap_t glWrapClampMode )
{
if (strlen(name) >= MAX_QPATH)
ri.Error( ERR_DROP, "R_CreateImage: \"%s\" is too long\n", name );
if ( tr.numImages == MAX_DRAWIMAGES )
ri.Error( ERR_DROP, "R_CreateImage: MAX_DRAWIMAGES hit\n" );
image_t* image = tr.images[tr.numImages] = RI_New<image_t>();
strcpy( image->name, name );
image->format = format;
image->flags = flags;
image->width = width;
image->height = height;
image->wrapClampMode = glWrapClampMode;
tr.numImages++;
Upload32( image, (unsigned int*)pic );
// KHB there are times we have no interest in naming an image at all (notably, font glyphs)
// but atm the rest of the system is too dependent on everything being named
//if (name) {
int hash = Q_FileHash(name, IMAGE_HASH_SIZE);
image->next = hashTable[hash];
hashTable[hash] = image;
//}
return image;
}
static qbool LoadTGA( const char* fileName, byte* buffer, int len, byte** pic, int* w, int* h, textureFormat_t* format )
{
*pic = NULL;
byte* p = buffer;
byte* const pEnd = buffer + len; // 1 byte past the end
TargaHeader targa_header;
targa_header.id_length = p[0];
targa_header.colormap_type = p[1];
targa_header.image_type = p[2];
targa_header.width = LittleShort( *(short*)&p[12] );
targa_header.height = LittleShort( *(short*)&p[14] );
targa_header.pixel_size = p[16];
targa_header.attributes = p[17];
// skip the header and the comment, if any
p += sizeof(TargaHeader);
if (targa_header.id_length != 0)
p += targa_header.id_length;
if (targa_header.image_type != 2 && targa_header.image_type != 10 && targa_header.image_type != 3)
ri.Error( ERR_DROP, "LoadTGA %s: Only type 2, 10 and 3 images supported\n", fileName );
if (targa_header.colormap_type)
ri.Error( ERR_DROP, "LoadTGA %s: Colormaps are not supported\n", fileName );
if (targa_header.image_type != 3 && targa_header.pixel_size != 32 && targa_header.pixel_size != 24)
ri.Error( ERR_DROP, "LoadTGA %s: Only 32-bit and 24-bit color images are supported\n", fileName );
if (targa_header.image_type == 3 && targa_header.pixel_size != 8)
ri.Error( ERR_DROP, "LoadTGA %s: Only 8-bit greyscale images are supported\n", fileName );
const int bpp = targa_header.pixel_size / 8;
const int width = targa_header.width;
const int height = targa_header.height;
const unsigned numBytes = width * height * 4;
if (width <= 0 || height <= 0 || numBytes > 0x7FFFFFFF || numBytes / (width * 4) != height)
ri.Error( ERR_DROP, "LoadTGA %s: Invalid image size\n", fileName );
*pic = (byte*)ri.Malloc( numBytes );
*w = width;
*h = height;
*format = TF_RGBA8;
#define UNMUNGE_PIXEL { dst[2] = pixel[0]; dst[1] = pixel[1]; dst[0] = pixel[2]; dst[3] = pixel[3]; dst += 4; }
#define WRAP_ROW if ((++x == width) && y--) { x = 0; dst = *pic + y*width*4; }
#define RANGE_CHECK(Bytes) if (p + (Bytes) > pEnd) { ri.Error( ERR_DROP, "LoadTGA %s: Truncated file\n", fileName ); }
// uncompressed luminance
if (targa_header.image_type == 3) {
RANGE_CHECK( width * height )
for (int y = height-1; y >= 0; --y) {
byte* dst = *pic + y*width * 4;
for (int x = 0; x < width; ++x) {
const byte l = *p;
dst[0] = l;
dst[1] = l;
dst[2] = l;
dst[3] = 255;
dst += 4;
p += 1;
}
}
// uncompressed BGRA
} else if (targa_header.image_type == 2 && bpp == 4) {
RANGE_CHECK( width * height * 4 )
for (int y = height-1; y >= 0; --y) {
byte* dst = *pic + y*width * 4;
for (int x = 0; x < width; ++x) {
dst[2] = p[0];
dst[1] = p[1];
dst[0] = p[2];
dst[3] = p[3];
dst += 4;
p += 4;
}
}
// uncompressed BGR
} else if (targa_header.image_type == 2 && bpp == 3) {
RANGE_CHECK( width * height * 3 )
for (int y = height-1; y >= 0; --y) {
byte* dst = *pic + y*width * 4;
for (int x = 0; x < width; ++x) {
dst[2] = p[0];
dst[1] = p[1];
dst[0] = p[2];
dst[3] = 255;
dst += 4;
p += 3;
}
}
// RLE_BGRA and RLE_BGR
} else if (targa_header.image_type == 10) {
byte pixel[4] = { 0, 0, 0, 255 };
int y = height-1;
while (y >= 0) {
byte* dst = *pic + y*width * 4;
int x = 0;
while (x < width) {
RANGE_CHECK( 1 )
const int rle = *p++;
int n = 1 + (rle & 0x7F);
if (rle & 0x80) {
// RLE packet: 1 pixel repeated n times
RANGE_CHECK( bpp )
for (int i = 0; i < bpp; ++i)
pixel[i] = *p++;
while (n--) {
UNMUNGE_PIXEL
WRAP_ROW
}
} else {
// n distinct pixels
RANGE_CHECK( bpp * n )
while (n--) {
for (int i = 0; i < bpp; ++i)
pixel[i] = *p++;
UNMUNGE_PIXEL
WRAP_ROW
}
}
}
}
}
#undef WRAP_ROW
#undef UNMUNGE_PIXEL
#undef RANGE_CHECK
if (targa_header.attributes & 0x20)
ri.Printf( PRINT_WARNING, "LoadTGA %s: Top-down declaration ignored\n", fileName );
return qtrue;
}
///////////////////////////////////////////////////////////////
typedef struct {
jmp_buf jumpBuffer;
const char* fileName;
qbool load;
} engineJPEGInfo_t;
// The only memory allocation function pointers we can override are the ones exposed in jpeg_memory_mgr.
// The problem is that it's the wrong layer for us: we want to replace malloc and free,
// not change how the pooling of allocations works.
// We are therefore re-implementing jmemnobs.c to use the engine's allocator.
extern "C"
{
#define JPEG_INTERNALS
#include "../libjpeg-turbo/jinclude.h"
#include "../libjpeg-turbo/jpeglib.h"
#include "../libjpeg-turbo/jmemsys.h"
void* jpeg_get_small( j_common_ptr cinfo, size_t sizeofobject ) { return (void*)ri.Malloc(sizeofobject); }
void jpeg_free_small( j_common_ptr cinfo, void* object, size_t sizeofobject ) { ri.Free(object); }
void* jpeg_get_large( j_common_ptr cinfo, size_t sizeofobject ) { return jpeg_get_small( cinfo, sizeofobject ); }
void jpeg_free_large( j_common_ptr cinfo, void* object, size_t sizeofobject ) { jpeg_free_small( cinfo, object, sizeofobject ); }
size_t jpeg_mem_available( j_common_ptr cinfo, size_t min_bytes_needed, size_t max_bytes_needed, size_t already_allocated ) { return max_bytes_needed; }
void jpeg_open_backing_store( j_common_ptr cinfo, backing_store_ptr info, long total_bytes_needed ) { ERREXIT(cinfo, JERR_NO_BACKING_STORE); }
long jpeg_mem_init( j_common_ptr cinfo) { return 0; }
void jpeg_mem_term( j_common_ptr cinfo) {}
void error_exit( j_common_ptr cinfo )
{
char buffer[JMSG_LENGTH_MAX];
(*cinfo->err->format_message)(cinfo, buffer);
engineJPEGInfo_t* const extra = (engineJPEGInfo_t*)cinfo->client_data;
ri.Printf(PRINT_WARNING, "libjpeg-turbo: couldn't %s %s: %s\n", extra->load ? "load" : "save", extra->fileName, buffer);
jpeg_destroy(cinfo);
longjmp(extra->jumpBuffer, -1);
}
void output_message( j_common_ptr cinfo )
{
char buffer[JMSG_LENGTH_MAX];
(*cinfo->err->format_message)(cinfo, buffer);
const engineJPEGInfo_t* const extra = (const engineJPEGInfo_t*)cinfo->client_data;
ri.Printf(PRINT_ALL, "libjpeg-turbo: while %s %s: %s\n", extra->load ? "loading" : "saving", extra->fileName, buffer);
}
};
static qbool LoadJPG( const char* fileName, byte* buffer, int len, byte** pic, int* w, int* h, textureFormat_t* format )
{
jpeg_decompress_struct cinfo;
jpeg_error_mgr jerr;
engineJPEGInfo_t extra;
if (setjmp(extra.jumpBuffer))
return qfalse;
extra.load = qtrue;
extra.fileName = fileName;
cinfo.err = jpeg_std_error( &jerr );
cinfo.err->error_exit = &error_exit;
cinfo.err->output_message = &output_message;
cinfo.client_data = &extra;
jpeg_create_decompress( &cinfo );
jpeg_mem_src( &cinfo, buffer, len );
jpeg_read_header( &cinfo, TRUE );
jpeg_start_decompress( &cinfo );
const unsigned numBytes = cinfo.output_width * cinfo.output_height * 4;
*pic = (byte*)ri.Malloc(numBytes);
*w = cinfo.output_width;
*h = cinfo.output_height;
// We set JCS_EXT_RGBA to instruct libjpeg-turbo to always
// write the alpha value as 255.
cinfo.out_color_space = JCS_EXT_RGBA;
cinfo.output_components = 4;
// go for speed
cinfo.dither_mode = JDITHER_NONE;
cinfo.dct_method = JDCT_FASTEST;
cinfo.do_fancy_upsampling = FALSE;
const unsigned rowStride = cinfo.output_width * 4;
JSAMPROW rowPointer = *pic;
while (cinfo.output_scanline < cinfo.output_height) {
jpeg_read_scanlines( &cinfo, &rowPointer, 1 );
rowPointer += rowStride;
}
jpeg_finish_decompress( &cinfo );
jpeg_destroy_decompress( &cinfo );
*format = TF_RGBA8;
return qtrue;
}
int SaveJPGToBuffer( byte* out, int quality, int image_width, int image_height, byte* image_buffer )
{
static const char* fileName = "memory buffer";
jpeg_compress_struct cinfo;
jpeg_error_mgr jerr;
engineJPEGInfo_t extra;
if (setjmp(extra.jumpBuffer))
return qfalse;
extra.load = qfalse;
extra.fileName = fileName;
cinfo.err = jpeg_std_error( &jerr );
cinfo.err->error_exit = &error_exit;
cinfo.err->output_message = &output_message;
cinfo.client_data = &extra;
jpeg_create_compress( &cinfo );
// jpeg_mem_dest only calls malloc if both outSize and outBuffer are 0
unsigned long outSize = image_width * image_height * 4;
unsigned char* outBuffer = out;
jpeg_mem_dest( &cinfo, &outBuffer, &outSize );
cinfo.image_width = image_width;
cinfo.image_height = image_height;
cinfo.input_components = 4;
cinfo.in_color_space = JCS_EXT_RGBA;
jpeg_set_defaults( &cinfo );
jpeg_set_quality( &cinfo, quality, TRUE );
jpeg_start_compress( &cinfo, TRUE );
const unsigned rowStride = image_width * 4;
JSAMPROW rowPointer = image_buffer + (cinfo.image_height - 1) * rowStride;
while (cinfo.next_scanline < cinfo.image_height) {
jpeg_write_scanlines( &cinfo, &rowPointer, 1 );
rowPointer -= rowStride;
}
jpeg_finish_compress( &cinfo );
const int csize = (int)(cinfo.dest->next_output_byte - outBuffer);
jpeg_destroy_compress( &cinfo );
return csize;
}
///////////////////////////////////////////////////////////////
extern qbool LoadSTB( const char* fileName, byte* buffer, int len, byte** pic, int* w, int* h, textureFormat_t* format );
typedef qbool (*imageLoaderFunc)( const char* fileName, byte* buffer, int len, byte** pic, int* w, int* h, textureFormat_t* format );
typedef struct {
const char* extension;
imageLoaderFunc function;
} imageLoader_t;
static const imageLoader_t imageLoaders[] = {
{ ".jpg", &LoadJPG },
{ ".tga", &LoadTGA },
{ ".png", &LoadSTB },
{ ".jpeg", &LoadJPG }
};
static void R_LoadImage( const char* name, byte** pic, int* w, int* h, textureFormat_t* format )
{
*pic = NULL;
*w = 0;
*h = 0;
const int loaderCount = ARRAY_LEN( imageLoaders );
char altName[MAX_QPATH];
byte* buffer;
int bufferSize = ri.FS_ReadFile( name, (void**)&buffer );
if ( buffer == NULL ) {
const char* lastDot = strrchr( name, '.' );
const int nameLength = lastDot != NULL ? (int)(lastDot - name) : (int)strlen( name );
if ( nameLength >= MAX_QPATH )
return;
for ( int i = 0; i < loaderCount; ++i ) {
memcpy( altName, name, nameLength );
altName[nameLength] = '\0';
Q_strcat( altName, sizeof(altName), imageLoaders[i].extension );
bufferSize = ri.FS_ReadFile( altName, (void**)&buffer );
if ( buffer != NULL ) {
name = altName;
break;
}
}
if ( buffer == NULL )
return;
}
const int nameLength = (int)strlen( name );
for ( int i = 0; i < loaderCount; ++i ) {
const int extLength = (int)strlen( imageLoaders[i].extension );
if ( extLength < nameLength &&
Q_stricmp(name + nameLength - extLength, imageLoaders[i].extension) == 0 ) {
(*imageLoaders[i].function)( name, buffer, bufferSize, pic, w, h, format );
break;
}
}
ri.FS_FreeFile( buffer );
}
struct forcedLoadImage_t {
const char* mapName;
const char* shaderName;
int shaderNameHash;
};
// map-specific fixes for textures that are used with different (incompatible) settings
static const forcedLoadImage_t g_forcedLoadImages[] = {
{ "ct3ctf1", "textures/ct3ctf1/grate_02.tga", 716 }
};
// finds or loads the given image - returns NULL if it fails, not a default image
const image_t* R_FindImageFile( const char* name, int flags, textureWrap_t glWrapClampMode )
{
if ( !name )
return NULL;
qbool forcedLoad = qfalse;
const int hash = Q_FileHash( name, IMAGE_HASH_SIZE );
const int forcedLoadImageCount = ARRAY_LEN( g_forcedLoadImages );
for ( int i = 0; i < forcedLoadImageCount; ++i ) {
const forcedLoadImage_t* const fli = g_forcedLoadImages + i;
if ( hash == fli->shaderNameHash &&
strcmp( R_GetMapName(), fli->mapName ) == 0 &&
strcmp( name, fli->shaderName ) == 0 )
forcedLoad = qtrue;
}
// see if the image is already loaded
//
if ( !forcedLoad ) {
image_t* image;
for ( image = hashTable[hash]; image; image=image->next ) {
if ( strcmp( name, image->name ) )
continue;
if ( !strcmp( name, "*white" ) )
return image;
// since this WASN'T enforced as an error, half the shaders out there (including most of id's)
// have been getting it wrong for years
// the white image can be used with any set of parms, but other mismatches are errors
if ( (image->flags & IMG_NOMIPMAP) != (flags & IMG_NOMIPMAP) ) {
ri.Printf( PRINT_DEVELOPER, "WARNING: reused image %s with mixed nomipmap settings\n", name );
}
if ( (image->flags & IMG_NOPICMIP) != (flags & IMG_NOPICMIP) ) {
ri.Printf( PRINT_DEVELOPER, "WARNING: reused image %s with mixed nopicmip settings\n", name );
}
if ( image->wrapClampMode != glWrapClampMode ) {
ri.Printf( PRINT_DEVELOPER, "WARNING: reused image %s with mixed clamp settings (map vs clampMap)\n", name );
}
return image;
}
}
// load the pic from disk
//
byte* pic;
int width, height;
textureFormat_t format;
R_LoadImage( name, &pic, &width, &height, &format );
if ( !pic )
return NULL;
image_t* const image = R_CreateImage( name, pic, width, height, format, flags, glWrapClampMode );
ri.Free( pic );
return image;
}
void R_InitFogTable()
{
const float exp = 0.5;
for (int i = 0; i < FOG_TABLE_SIZE; ++i) {
tr.fogTable[i] = pow( (float)i/(FOG_TABLE_SIZE-1), exp );
}
}
/*
Returns a 0.0 to 1.0 fog density value
This is called for each texel of the fog texture on startup
and for each vertex of transparent shaders in fog dynamically
*/
float R_FogFactor( float s, float t )
{
s -= 1.0/512;
if ( s < 0 ) {
return 0;
}
if ( t < 1.0/32 ) {
return 0;
}
if ( t < 31.0/32 ) {
s *= (t - 1.0f/32.0f) / (30.0f/32.0f);
}
// we need to leave a lot of clamp range
s *= 8;
if ( s > 1.0 ) {
s = 1.0;
}
return tr.fogTable[ (int)(s * (FOG_TABLE_SIZE-1)) ];
}
static void R_CreateFogImage()
{
const int FOG_S = 256;
const int FOG_T = 32;
RI_AutoPtr ap( FOG_S * FOG_T * 4 );
byte* p = ap;
// S is distance, T is depth
for (int x = 0; x < FOG_S; ++x) {
for (int y = 0; y < FOG_T; ++y) {
float d = R_FogFactor( ( x + 0.5f ) / FOG_S, ( y + 0.5f ) / FOG_T );
p[(y*FOG_S+x)*4+0] = p[(y*FOG_S+x)*4+1] = p[(y*FOG_S+x)*4+2] = 255;
p[(y*FOG_S+x)*4+3] = 255*d;
}
}
tr.fogImage = R_CreateImage( "*fog", p, FOG_S, FOG_T, TF_RGBA8, IMG_NOPICMIP, TW_CLAMP_TO_EDGE );
}
static void R_CreateDefaultImage()
{
const int DEFAULT_SIZE = 16;
byte data[DEFAULT_SIZE][DEFAULT_SIZE][4];
// the default image is a box showing increasing s and t
Com_Memset( data, 32, sizeof( data ) );
for ( int i = 0; i < DEFAULT_SIZE; ++i ) {
byte b = (byte)( 64 + (128 * i / DEFAULT_SIZE) );
data[0][i][0] = b;
data[0][i][3] = 255;
data[i][0][1] = b;
data[i][0][3] = 255;
data[i][i][0] = data[i][i][1] = b;
data[i][i][3] = 255;
}
tr.defaultImage = R_CreateImage( "*default", (byte*)data, DEFAULT_SIZE, DEFAULT_SIZE, TF_RGBA8, IMG_NOPICMIP | IMG_NOAF, TW_REPEAT );
}
static void R_CreateBuiltinImages()
{
int i;
byte data[4];
R_CreateDefaultImage();
// we use a solid white image instead of disabling texturing
Com_Memset( data, 255, 4 );
tr.whiteImage = R_CreateImage( "*white", data, 1, 1, TF_RGBA8, IMG_NOMIPMAP | IMG_NOAF, TW_REPEAT );
// scratchimages usually used for cinematic drawing (signal-quality effects)
// these are just placeholders: RE_StretchRaw will regenerate them when it wants them
for (i = 0; i < ARRAY_LEN(tr.scratchImage); ++i)
tr.scratchImage[i] = R_CreateImage( "*scratch", data, 1, 1, TF_RGBA8, IMG_NOMIPMAP | IMG_NOPICMIP, TW_CLAMP_TO_EDGE );
R_CreateFogImage();
}
void R_SetColorMappings()
{
tr.identityLight = 1.0f / r_brightness->value;
tr.identityLightByte = (int)( 255.0f * tr.identityLight );
for (int i = 0; i < 256; ++i) {
s_intensitytable[i] = (byte)min( r_intensity->value * i, 255.0f );
}
}
void R_InitImages()
{
Com_Memset( hashTable, 0, sizeof(hashTable) );
R_SetColorMappings(); // build brightness translation tables
R_CreateBuiltinImages(); // create default textures (white, fog, etc)
}
/*
============================================================================
SKINS
============================================================================
*/
// unfortunatly, skin files aren't compatible with our normal parsing rules. oops :/
static const char* CommaParse( const char** data )
{
static char com_token[MAX_TOKEN_CHARS];
int c = 0;
const char* p = *data;
while (*p && (*p < 32))
++p;
while ((*p > 32) && (*p != ',') && (c < MAX_TOKEN_CHARS-1))
com_token[c++] = *p++;
*data = p;
com_token[c] = 0;
return com_token;
}
qhandle_t RE_RegisterSkin( const char* name )
{
if (!name || !name[0] || (strlen(name) >= MAX_QPATH))
ri.Error( ERR_DROP, "RE_RegisterSkin: invalid name [%s]\n", name ? name : "NULL" );
skin_t* skin;
qhandle_t hSkin;
// see if the skin is already loaded
for (hSkin = 1; hSkin < tr.numSkins; ++hSkin) {
skin = tr.skins[hSkin];
if ( !Q_stricmp( skin->name, name ) ) {
return (skin->numSurfaces ? hSkin : 0);
}
}
// allocate a new skin
if ( tr.numSkins == MAX_SKINS ) {
ri.Printf( PRINT_WARNING, "WARNING: RE_RegisterSkin( '%s' ) MAX_SKINS hit\n", name );
return 0;
}
tr.numSkins++;
skin = RI_New<skin_t>();
tr.skins[hSkin] = skin;
Q_strncpyz( skin->name, name, sizeof( skin->name ) );
skin->numSurfaces = 0;
// make sure the render thread is stopped
// KHB why? we're not uploading anything... R_SyncRenderThread();
// if not a .skin file, load as a single shader
if ( Q_stricmpn( name + strlen( name ) - 5, ".skin", 6 ) ) {
skin->numSurfaces = 1;
skin->surfaces[0] = RI_New<skinSurface_t>();
skin->surfaces[0]->shader = R_FindShader( name, LIGHTMAP_NONE, qtrue );
return hSkin;
}
char* text;
// load and parse the skin file
ri.FS_ReadFile( name, (void **)&text );
if (!text)
return 0;
const char* token;
const char* p = text;
char surfName[MAX_QPATH];
while (p && *p) {
// get surface name
token = CommaParse( &p );
Q_strncpyz( surfName, token, sizeof( surfName ) );
if ( !token[0] )
break;
// lowercase the surface name so skin compares are faster
Q_strlwr( surfName );
if (*p == ',')
++p;
if ( strstr( token, "tag_" ) )
continue;
// parse the shader name
token = CommaParse( &p );
skinSurface_t* surf = skin->surfaces[ skin->numSurfaces ] = RI_New<skinSurface_t>();
Q_strncpyz( surf->name, surfName, sizeof( surf->name ) );
surf->shader = R_FindShader( token, LIGHTMAP_NONE, qtrue );
skin->numSurfaces++;
}
ri.FS_FreeFile( text );
return (skin->numSurfaces ? hSkin : 0); // never let a skin have 0 shaders
}
void R_InitSkins()
{
tr.numSkins = 1;
// make the default skin have all default shaders
tr.skins[0] = RI_New<skin_t>();
tr.skins[0]->numSurfaces = 1;
tr.skins[0]->surfaces[0] = RI_New<skinSurface_t>();
tr.skins[0]->surfaces[0]->shader = tr.defaultShader;
Q_strncpyz( tr.skins[0]->name, "<default skin>", sizeof( tr.skins[0]->name ) );
}
const skin_t* R_GetSkinByHandle( qhandle_t hSkin )
{
return ((hSkin > 0) && (hSkin < tr.numSkins) ? tr.skins[hSkin] : tr.skins[0]);
}
void R_SkinList_f( void )
{
ri.Printf( PRINT_ALL, "------------------\n" );
const char* const match = Cmd_Argc() > 1 ? Cmd_Argv( 1 ) : NULL;
int skinCount = 0;
for (int i = 0; i < tr.numSkins; ++i) {
const skin_t* skin = tr.skins[i];
if ( match && !Com_Filter( match, skin->name ) )
continue;
skinCount++;
ri.Printf( PRINT_ALL, "%3i:%s\n", i, skin->name );
for (int j = 0; j < skin->numSurfaces; ++j) {
ri.Printf( PRINT_ALL, " %s = %s\n",
skin->surfaces[j]->name, skin->surfaces[j]->shader->name );
}
}
ri.Printf( PRINT_ALL, "%i skins found\n", skinCount );
ri.Printf( PRINT_ALL, "------------------\n" );
}