Files
DoomRTX/neo/engine/renderer/Image_load.cpp
T
2026-05-29 19:41:58 -07:00

2795 lines
78 KiB
C++

/*
===========================================================================
IceTech GPL Source Code
Copyright (C) 2026 Justin Marshall
This file is part of the IceTech GPL Source Code (?IceTech Source Code?).
IceTech 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 3 of the License, or
(at your option) any later version.
IceTech 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 IceTech Source Code. If not, see <http://www.gnu.org/licenses/>.
In addition, the IceTech Source Code is also subject to certain additional terms. You should have received a copy of these additional terms immediately following the terms and conditions of the GNU General Public License which accompanied the IceTech Source Code. If not, please request a copy in writing from id Software at the address below.
If you have questions concerning this license or the applicable additional terms, you may contact in writing Justin Marshall, justinmarshall20@gmail.com
===========================================================================
*/
#include "precompiled.h"
#pragma hdrstop
#include "tr_local.h"
/*
PROBLEM: compressed textures may break the zero clamp rule!
*/
static bool FormatIsDXT( int internalFormat ) {
if ( internalFormat < GL_COMPRESSED_RGB_S3TC_DXT1_EXT
|| internalFormat > GL_COMPRESSED_RGBA_S3TC_DXT5_EXT ) {
return false;
}
return true;
}
static bool FormatIsBC7( int internalFormat ) {
return internalFormat == GL_COMPRESSED_RGBA_BPTC_UNORM_ARB || internalFormat == GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM_ARB;
}
static bool FormatIsBlockCompressed( int internalFormat ) {
return FormatIsDXT( internalFormat ) || FormatIsBC7( internalFormat );
}
static int BlockBytesForInternalFormat( int internalFormat ) {
if ( internalFormat == GL_COMPRESSED_RGB_S3TC_DXT1_EXT || internalFormat == GL_COMPRESSED_RGBA_S3TC_DXT1_EXT ) {
return 8;
}
if ( internalFormat == GL_COMPRESSED_RGBA_S3TC_DXT3_EXT ||
internalFormat == GL_COMPRESSED_RGBA_S3TC_DXT5_EXT ||
FormatIsBC7( internalFormat ) ) {
return 16;
}
return 0;
}
static void R_SetImageAverageColor( idImage *image, const byte *pic, int width, int height ) {
if ( !image || !pic || width <= 0 || height <= 0 ) {
return;
}
uint64_t sum[4] = { 0, 0, 0, 0 };
const int pixelCount = width * height;
for ( int i = 0; i < pixelCount; i++ ) {
const byte *rgba = pic + i * 4;
sum[0] += rgba[0];
sum[1] += rgba[1];
sum[2] += rgba[2];
sum[3] += rgba[3];
}
const float scale = 1.0f / ( 255.0f * (float)pixelCount );
image->averageColor[0] = sum[0] * scale;
image->averageColor[1] = sum[1] * scale;
image->averageColor[2] = sum[2] * scale;
image->averageColor[3] = sum[3] * scale;
}
static void R_SetCubeImageAverageColor( idImage *image, const byte *pic[6], int size ) {
if ( !image || !pic || size <= 0 ) {
return;
}
uint64_t sum[4] = { 0, 0, 0, 0 };
int faceCount = 0;
const int facePixels = size * size;
for ( int face = 0; face < 6; face++ ) {
if ( !pic[face] ) {
continue;
}
faceCount++;
for ( int i = 0; i < facePixels; i++ ) {
const byte *rgba = pic[face] + i * 4;
sum[0] += rgba[0];
sum[1] += rgba[1];
sum[2] += rgba[2];
sum[3] += rgba[3];
}
}
if ( faceCount == 0 ) {
return;
}
const float scale = 1.0f / ( 255.0f * (float)( facePixels * faceCount ) );
image->averageColor[0] = sum[0] * scale;
image->averageColor[1] = sum[1] * scale;
image->averageColor[2] = sum[2] * scale;
image->averageColor[3] = sum[3] * scale;
}
int MakePowerOfTwo( int num ) {
int pot;
for (pot = 1 ; pot < num ; pot<<=1) {
}
return pot;
}
/*
================
BitsForInternalFormat
Used for determining memory utilization
================
*/
int idImage::BitsForInternalFormat( int internalFormat ) const {
switch ( internalFormat ) {
case GL_INTENSITY8:
case 1:
return 8;
case 2:
case GL_LUMINANCE8_ALPHA8:
return 16;
case 3:
return 32; // on some future hardware, this may actually be 24, but be conservative
case 4:
return 32;
case GL_LUMINANCE8:
return 8;
case GL_ALPHA8:
return 8;
case GL_RGBA8:
return 32;
case GL_RGB8:
return 32; // on some future hardware, this may actually be 24, but be conservative
case GL_COMPRESSED_RGB_S3TC_DXT1_EXT:
return 4;
case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT:
return 4;
case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT:
return 8;
case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT:
return 8;
case GL_COMPRESSED_RGBA_BPTC_UNORM_ARB:
case GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM_ARB:
return 8;
case GL_RGBA4:
return 16;
case GL_RGB5:
return 16;
case GL_COLOR_INDEX8_EXT:
return 8;
case GL_COLOR_INDEX:
return 8;
case GL_COMPRESSED_RGB_ARB:
return 4; // not sure
case GL_COMPRESSED_RGBA_ARB:
return 8; // not sure
default:
common->Error( "R_BitsForInternalFormat: BAD FORMAT:%i", internalFormat );
}
return 0;
}
/*
==================
UploadCompressedNormalMap
Create a 256 color palette to be used by compressed normal maps
==================
*/
void idImage::UploadCompressedNormalMap( int width, int height, const byte *rgba, int mipLevel ) {
byte *normals;
const byte *in;
byte *out;
int i, j;
int x, y, z;
int row;
// OpenGL's pixel packing rule
row = width < 4 ? 4 : width;
normals = (byte *)_alloca( row * height );
if ( !normals ) {
common->Error( "R_UploadCompressedNormalMap: _alloca failed" );
}
in = rgba;
out = normals;
for ( i = 0 ; i < height ; i++, out += row, in += width * 4 ) {
for ( j = 0 ; j < width ; j++ ) {
x = in[ j * 4 + 0 ];
y = in[ j * 4 + 1 ];
z = in[ j * 4 + 2 ];
int c;
if ( x == 128 && y == 128 && z == 128 ) {
// the "nullnormal" color
c = 255;
} else {
c = ( globalImages->originalToCompressed[x] << 4 ) | globalImages->originalToCompressed[y];
if ( c == 255 ) {
c = 254; // don't use the nullnormal color
}
}
out[j] = c;
}
}
if ( mipLevel == 0 ) {
// Optionally write out the paletized normal map to a .tga
if ( globalImages->image_writeNormalTGAPalletized.GetBool() ) {
char filename[MAX_IMAGE_NAME];
ImageProgramStringToCompressedFileName( imgName, filename );
char *ext = strrchr(filename, '.');
if ( ext ) {
strcpy(ext, "_pal.tga");
R_WritePalTGA( filename, normals, globalImages->compressedPalette, width, height);
}
}
}
if ( glConfig.sharedTexturePaletteAvailable ) {
glTexImage2D( GL_TEXTURE_2D,
mipLevel,
GL_COLOR_INDEX8_EXT,
width,
height,
0,
GL_COLOR_INDEX,
GL_UNSIGNED_BYTE,
normals );
}
}
//=======================================================================
static 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},
};
/*
===============
SelectInternalFormat
This may need to scan six cube map images
===============
*/
GLenum idImage::SelectInternalFormat( const byte **dataPtrs, int numDataPtrs, int width, int height,
textureDepth_t minimumDepth, bool *monochromeResult ) const {
int i, c;
const byte *scan;
int rgbOr, rgbAnd, aOr, aAnd;
int rgbDiffer, rgbaDiffer;
// determine if the rgb channels are all the same
// and if either all rgb or all alpha are 255
c = width*height;
rgbDiffer = 0;
rgbaDiffer = 0;
rgbOr = 0;
rgbAnd = -1;
aOr = 0;
aAnd = -1;
*monochromeResult = true; // until shown otherwise
for ( int side = 0 ; side < numDataPtrs ; side++ ) {
scan = dataPtrs[side];
for ( i = 0; i < c; i++, scan += 4 ) {
int cor, cand;
aOr |= scan[3];
aAnd &= scan[3];
cor = scan[0] | scan[1] | scan[2];
cand = scan[0] & scan[1] & scan[2];
// if rgb are all the same, the or and and will match
rgbDiffer |= ( cor ^ cand );
// our "isMonochrome" test is more lax than rgbDiffer,
// allowing the values to be off by several units and
// still use the NV20 mono path
if ( *monochromeResult ) {
if ( abs( scan[0] - scan[1] ) > 16
|| abs( scan[0] - scan[2] ) > 16 ) {
*monochromeResult = false;
}
}
rgbOr |= cor;
rgbAnd &= cand;
cor |= scan[3];
cand &= scan[3];
rgbaDiffer |= ( cor ^ cand );
}
}
// we assume that all 0 implies that the alpha channel isn't needed,
// because some tools will spit out 32 bit images with a 0 alpha instead
// of 255 alpha, but if the alpha actually is referenced, there will be
// different behavior in the compressed vs uncompressed states.
bool needAlpha;
if ( aAnd == 255 || aOr == 0 ) {
needAlpha = false;
} else {
needAlpha = true;
}
// catch normal maps first
if ( minimumDepth == TD_BUMP ) {
return GL_RGBA8;
}
// allow a complete override of image compression with a cvar
if ( !globalImages->image_useCompression.GetBool() ) {
minimumDepth = TD_HIGH_QUALITY;
}
if ( minimumDepth == TD_SPECULAR ) {
// we are assuming that any alpha channel is unintentional
if ( glConfig.textureCompressionAvailable ) {
return GL_COMPRESSED_RGB_S3TC_DXT1_EXT;
} else {
return GL_RGB5;
}
}
if ( minimumDepth == TD_DIFFUSE ) {
// we might intentionally have an alpha channel for alpha tested textures
if ( glConfig.textureCompressionAvailable ) {
if ( !needAlpha ) {
return GL_COMPRESSED_RGB_S3TC_DXT1_EXT;
} else {
return GL_COMPRESSED_RGBA_S3TC_DXT3_EXT;
}
} else if ( ( aAnd == 255 || aOr == 0 ) ) {
return GL_RGB5;
} else {
return GL_RGBA4;
}
}
// there will probably be some drivers that don't
// correctly handle the intensity/alpha/luminance/luminance+alpha
// formats, so provide a fallback that only uses the rgb/rgba formats
if ( !globalImages->image_useAllFormats.GetBool() ) {
// pretend rgb is varying and inconsistant, which
// prevents any of the more compact forms
rgbDiffer = 1;
rgbaDiffer = 1;
rgbAnd = 0;
}
// cases without alpha
if ( !needAlpha ) {
if ( minimumDepth == TD_HIGH_QUALITY ) {
return GL_RGB8; // four bytes
}
if ( glConfig.textureCompressionAvailable ) {
return GL_COMPRESSED_RGB_S3TC_DXT1_EXT; // half byte
}
return GL_RGB5; // two bytes
}
// cases with alpha
if ( !rgbaDiffer ) {
if ( minimumDepth != TD_HIGH_QUALITY && glConfig.textureCompressionAvailable ) {
return GL_COMPRESSED_RGBA_S3TC_DXT3_EXT; // one byte
}
return GL_INTENSITY8; // single byte for all channels
}
#if 0
// we don't support alpha textures any more, because there
// is a discrepancy in the definition of TEX_ENV_COMBINE that
// causes them to be treated as 0 0 0 A, instead of 1 1 1 A as
// normal texture modulation treats them
if ( rgbAnd == 255 ) {
return GL_ALPHA8; // single byte, only alpha
}
#endif
if ( minimumDepth == TD_HIGH_QUALITY ) {
return GL_RGBA8; // four bytes
}
if ( glConfig.textureCompressionAvailable ) {
return GL_COMPRESSED_RGBA_S3TC_DXT3_EXT; // one byte
}
if ( !rgbDiffer ) {
return GL_LUMINANCE8_ALPHA8; // two bytes, max quality
}
return GL_RGBA4; // two bytes
}
/*
==================
SetImageFilterAndRepeat
==================
*/
void idImage::SetImageFilterAndRepeat() const {
// set the minimize / maximize filtering
switch( filter ) {
case TF_DEFAULT:
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, globalImages->textureMinFilter );
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, globalImages->textureMaxFilter );
break;
case TF_LINEAR:
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
break;
case TF_NEAREST:
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST );
break;
default:
common->FatalError( "R_CreateImage: bad texture filter" );
}
if ( glConfig.anisotropicAvailable ) {
// only do aniso filtering on mip mapped images
if ( filter == TF_DEFAULT ) {
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, globalImages->textureAnisotropy );
} else {
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 1 );
}
}
if ( glConfig.textureLODBiasAvailable ) {
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS_EXT, globalImages->textureLODBias );
}
// set the wrap/clamp modes
switch( repeat ) {
case TR_REPEAT:
glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT );
glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT );
break;
case TR_CLAMP_TO_BORDER:
glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER );
glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER );
break;
case TR_CLAMP_TO_ZERO:
case TR_CLAMP_TO_ZERO_ALPHA:
case TR_CLAMP:
glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
break;
default:
common->FatalError( "R_CreateImage: bad texture repeat" );
}
}
/*
================
idImage::Downsize
helper function that takes the current width/height and might make them smaller
================
*/
void idImage::GetDownsize( int &scaled_width, int &scaled_height ) const {
}
/*
================
GenerateImage
The alpha channel bytes should be 255 if you don't
want the channel.
We need a material characteristic to ask for specific texture modes.
Designed limitations of flexibility:
No support for texture borders.
No support for texture border color.
No support for texture environment colors or GL_BLEND or GL_DECAL
texture environments, because the automatic optimization to single
or dual component textures makes those modes potentially undefined.
No non-power-of-two images.
No palettized textures.
There is no way to specify separate wrap/clamp values for S and T
There is no way to specify explicit mip map levels
================
*/
void idImage::GenerateImage( const byte *pic, int width, int height,
textureFilter_t filterParm, bool allowDownSizeParm,
textureRepeat_t repeatParm, textureDepth_t depthParm ) {
bool preserveBorder;
byte *scaledBuffer;
int scaled_width, scaled_height;
byte *shrunk;
PurgeImage();
filter = filterParm;
allowDownSize = allowDownSizeParm;
repeat = repeatParm;
depth = depthParm;
R_SetImageAverageColor( this, pic, width, height );
// if we don't have a rendering context, just return after we
// have filled in the parms. We must have the values set, or
// an image match from a shader before OpenGL starts would miss
// the generated texture
if ( !glConfig.isInitialized ) {
return;
}
// don't let mip mapping smear the texture into the clamped border
if ( repeat == TR_CLAMP_TO_ZERO ) {
preserveBorder = true;
} else {
preserveBorder = false;
}
// make sure it is a power of 2
scaled_width = width;
scaled_height = height;
if ( scaled_width != width || scaled_height != height ) {
common->Error( "R_CreateImage: not a power of 2 image" );
}
// Optionally modify our width/height based on options/hardware
GetDownsize( scaled_width, scaled_height );
scaledBuffer = NULL;
// generate the texture number
glGenTextures( 1, &texnum );
glTextureAverageColorQD3D12( texnum, averageColor[0], averageColor[1], averageColor[2], averageColor[3] );
// select proper internal format before we resample
internalFormat = SelectInternalFormat( &pic, 1, width, height, depth, &isMonochrome );
// copy or resample data as appropriate for first MIP level
if ( ( scaled_width == width ) && ( scaled_height == height ) ) {
// we must copy even if unchanged, because the border zeroing
// would otherwise modify const data
scaledBuffer = (byte *)R_StaticAlloc( sizeof( unsigned ) * scaled_width * scaled_height );
memcpy (scaledBuffer, pic, width*height*4);
} else {
// resample down as needed (FIXME: this doesn't seem like it resamples anymore!)
// scaledBuffer = R_ResampleTexture( pic, width, height, width >>= 1, height >>= 1 );
scaledBuffer = R_MipMap( pic, width, height, preserveBorder );
width >>= 1;
height >>= 1;
if ( width < 1 ) {
width = 1;
}
if ( height < 1 ) {
height = 1;
}
while ( width > scaled_width || height > scaled_height ) {
shrunk = R_MipMap( scaledBuffer, width, height, preserveBorder );
R_StaticFree( scaledBuffer );
scaledBuffer = shrunk;
width >>= 1;
height >>= 1;
if ( width < 1 ) {
width = 1;
}
if ( height < 1 ) {
height = 1;
}
}
// one might have shrunk down below the target size
scaled_width = width;
scaled_height = height;
}
uploadHeight = scaled_height;
uploadWidth = scaled_width;
type = TT_2D;
// zero the border if desired, allowing clamped projection textures
// even after picmip resampling or careless artists.
if ( repeat == TR_CLAMP_TO_ZERO ) {
byte rgba[4];
rgba[0] = rgba[1] = rgba[2] = 0;
rgba[3] = 255;
R_SetBorderTexels( (byte *)scaledBuffer, width, height, rgba );
}
if ( repeat == TR_CLAMP_TO_ZERO_ALPHA ) {
byte rgba[4];
rgba[0] = rgba[1] = rgba[2] = 255;
rgba[3] = 0;
R_SetBorderTexels( (byte *)scaledBuffer, width, height, rgba );
}
if ( generatorFunction == NULL && ( depth == TD_BUMP && globalImages->image_writeNormalTGA.GetBool() || depth != TD_BUMP && globalImages->image_writeTGA.GetBool() ) ) {
// Optionally write out the texture to a .tga
char filename[MAX_IMAGE_NAME];
ImageProgramStringToCompressedFileName( imgName, filename );
char *ext = strrchr(filename, '.');
if ( ext ) {
strcpy( ext, ".tga" );
// swap the red/alpha for the write
/*
if ( depth == TD_BUMP ) {
for ( int i = 0; i < scaled_width * scaled_height * 4; i += 4 ) {
scaledBuffer[ i ] = scaledBuffer[ i + 3 ];
scaledBuffer[ i + 3 ] = 0;
}
}
*/
R_WriteTGA( filename, scaledBuffer, scaled_width, scaled_height, false );
// put it back
/*
if ( depth == TD_BUMP ) {
for ( int i = 0; i < scaled_width * scaled_height * 4; i += 4 ) {
scaledBuffer[ i + 3 ] = scaledBuffer[ i ];
scaledBuffer[ i ] = 0;
}
}
*/
}
}
// upload the main image level
Bind();
if ( internalFormat == GL_COLOR_INDEX8_EXT ) {
/*
if ( depth == TD_BUMP ) {
for ( int i = 0; i < scaled_width * scaled_height * 4; i += 4 ) {
scaledBuffer[ i ] = scaledBuffer[ i + 3 ];
scaledBuffer[ i + 3 ] = 0;
}
}
*/
UploadCompressedNormalMap( scaled_width, scaled_height, scaledBuffer, 0 );
} else {
glTexImage2D( GL_TEXTURE_2D, 0, internalFormat, scaled_width, scaled_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, scaledBuffer );
}
// create and upload the mip map levels, which we do in all cases, even if we don't think they are needed
int miplevel;
miplevel = 0;
while ( scaled_width > 1 || scaled_height > 1 ) {
// preserve the border after mip map unless repeating
shrunk = R_MipMap( scaledBuffer, scaled_width, scaled_height, preserveBorder );
R_StaticFree( scaledBuffer );
scaledBuffer = shrunk;
scaled_width >>= 1;
scaled_height >>= 1;
if ( scaled_width < 1 ) {
scaled_width = 1;
}
if ( scaled_height < 1 ) {
scaled_height = 1;
}
miplevel++;
// this is a visualization tool that shades each mip map
// level with a different color so you can see the
// rasterizer's texture level selection algorithm
// Changing the color doesn't help with lumminance/alpha/intensity formats...
if ( depth == TD_DIFFUSE && globalImages->image_colorMipLevels.GetBool() ) {
R_BlendOverTexture( (byte *)scaledBuffer, scaled_width * scaled_height, mipBlendColors[miplevel] );
}
// upload the mip map
if ( internalFormat == GL_COLOR_INDEX8_EXT ) {
UploadCompressedNormalMap( scaled_width, scaled_height, scaledBuffer, miplevel );
} else {
glTexImage2D( GL_TEXTURE_2D, miplevel, internalFormat, scaled_width, scaled_height,
0, GL_RGBA, GL_UNSIGNED_BYTE, scaledBuffer );
}
}
if ( scaledBuffer != 0 ) {
R_StaticFree( scaledBuffer );
}
SetImageFilterAndRepeat();
// see if we messed anything up
GL_CheckErrors();
}
/*
==================
Generate3DImage
==================
*/
void idImage::Generate3DImage( const byte *pic, int width, int height, int picDepth,
textureFilter_t filterParm, bool allowDownSizeParm,
textureRepeat_t repeatParm, textureDepth_t minDepthParm ) {
int scaled_width, scaled_height, scaled_depth;
PurgeImage();
filter = filterParm;
allowDownSize = allowDownSizeParm;
repeat = repeatParm;
depth = minDepthParm;
R_SetImageAverageColor( this, pic, width, height * picDepth );
// if we don't have a rendering context, just return after we
// have filled in the parms. We must have the values set, or
// an image match from a shader before OpenGL starts would miss
// the generated texture
if ( !glConfig.isInitialized ) {
return;
}
// make sure it is a power of 2
scaled_width = MakePowerOfTwo( width );
scaled_height = MakePowerOfTwo( height );
scaled_depth = MakePowerOfTwo( picDepth );
if ( scaled_width != width || scaled_height != height || scaled_depth != picDepth ) {
common->Error( "R_Create3DImage: not a power of 2 image" );
}
// FIXME: allow picmip here
// generate the texture number
glGenTextures( 1, &texnum );
glTextureAverageColorQD3D12( texnum, averageColor[0], averageColor[1], averageColor[2], averageColor[3] );
// select proper internal format before we resample
// this function doesn't need to know it is 3D, so just make it very "tall"
internalFormat = SelectInternalFormat( &pic, 1, width, height * picDepth, minDepthParm, &isMonochrome );
uploadHeight = scaled_height;
uploadWidth = scaled_width;
uploadDepth = scaled_depth;
type = TT_3D;
// upload the main image level
Bind();
glTexImage3D(GL_TEXTURE_3D, 0, internalFormat, scaled_width, scaled_height, scaled_depth,
0, GL_RGBA, GL_UNSIGNED_BYTE, pic );
// create and upload the mip map levels
int miplevel;
byte *scaledBuffer, *shrunk;
scaledBuffer = (byte *)R_StaticAlloc( scaled_width * scaled_height * scaled_depth * 4 );
memcpy( scaledBuffer, pic, scaled_width * scaled_height * scaled_depth * 4 );
miplevel = 0;
while ( scaled_width > 1 || scaled_height > 1 || scaled_depth > 1 ) {
// preserve the border after mip map unless repeating
shrunk = R_MipMap3D( scaledBuffer, scaled_width, scaled_height, scaled_depth,
(bool)(repeat != TR_REPEAT) );
R_StaticFree( scaledBuffer );
scaledBuffer = shrunk;
scaled_width >>= 1;
scaled_height >>= 1;
scaled_depth >>= 1;
if ( scaled_width < 1 ) {
scaled_width = 1;
}
if ( scaled_height < 1 ) {
scaled_height = 1;
}
if ( scaled_depth < 1 ) {
scaled_depth = 1;
}
miplevel++;
// upload the mip map
glTexImage3D(GL_TEXTURE_3D, miplevel, internalFormat, scaled_width, scaled_height, scaled_depth,
0, GL_RGBA, GL_UNSIGNED_BYTE, scaledBuffer );
}
R_StaticFree( scaledBuffer );
// set the minimize / maximize filtering
switch( filter ) {
case TF_DEFAULT:
glTexParameterf(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, globalImages->textureMinFilter );
glTexParameterf(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, globalImages->textureMaxFilter );
break;
case TF_LINEAR:
glTexParameterf(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
glTexParameterf(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
break;
case TF_NEAREST:
glTexParameterf(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );
glTexParameterf(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_NEAREST );
break;
default:
common->FatalError( "R_CreateImage: bad texture filter" );
}
// set the wrap/clamp modes
switch( repeat ) {
case TR_REPEAT:
glTexParameterf( GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_REPEAT );
glTexParameterf( GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_REPEAT );
glTexParameterf( GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_REPEAT );
break;
case TR_CLAMP_TO_BORDER:
glTexParameterf( GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER );
glTexParameterf( GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER );
break;
case TR_CLAMP_TO_ZERO:
case TR_CLAMP_TO_ZERO_ALPHA:
case TR_CLAMP:
glTexParameterf( GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
glTexParameterf( GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
glTexParameterf( GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE );
break;
default:
common->FatalError( "R_CreateImage: bad texture repeat" );
}
// see if we messed anything up
GL_CheckErrors();
}
/*
====================
GenerateCubeImage
Non-square cube sides are not allowed
====================
*/
void idImage::GenerateCubeImage( const byte *pic[6], int size,
textureFilter_t filterParm, bool allowDownSizeParm,
textureDepth_t depthParm ) {
int scaled_width, scaled_height;
int width, height;
int i;
PurgeImage();
filter = filterParm;
allowDownSize = allowDownSizeParm;
depth = depthParm;
R_SetCubeImageAverageColor( this, pic, size );
type = TT_CUBIC;
// if we don't have a rendering context, just return after we
// have filled in the parms. We must have the values set, or
// an image match from a shader before OpenGL starts would miss
// the generated texture
if ( !glConfig.isInitialized ) {
return;
}
if ( ! glConfig.cubeMapAvailable ) {
return;
}
width = height = size;
// generate the texture number
glGenTextures( 1, &texnum );
glTextureAverageColorQD3D12( texnum, averageColor[0], averageColor[1], averageColor[2], averageColor[3] );
// select proper internal format before we resample
internalFormat = SelectInternalFormat( pic, 6, width, height, depth, &isMonochrome );
// don't bother with downsample for now
scaled_width = width;
scaled_height = height;
uploadHeight = scaled_height;
uploadWidth = scaled_width;
Bind();
// no other clamp mode makes sense
glTexParameteri(GL_TEXTURE_CUBE_MAP_EXT, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP_EXT, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
// set the minimize / maximize filtering
switch( filter ) {
case TF_DEFAULT:
glTexParameterf(GL_TEXTURE_CUBE_MAP_EXT, GL_TEXTURE_MIN_FILTER, globalImages->textureMinFilter );
glTexParameterf(GL_TEXTURE_CUBE_MAP_EXT, GL_TEXTURE_MAG_FILTER, globalImages->textureMaxFilter );
break;
case TF_LINEAR:
glTexParameterf(GL_TEXTURE_CUBE_MAP_EXT, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
glTexParameterf(GL_TEXTURE_CUBE_MAP_EXT, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
break;
case TF_NEAREST:
glTexParameterf(GL_TEXTURE_CUBE_MAP_EXT, GL_TEXTURE_MIN_FILTER, GL_NEAREST );
glTexParameterf(GL_TEXTURE_CUBE_MAP_EXT, GL_TEXTURE_MAG_FILTER, GL_NEAREST );
break;
default:
common->FatalError( "R_CreateImage: bad texture filter" );
}
// upload the base level
// FIXME: support GL_COLOR_INDEX8_EXT?
for ( i = 0 ; i < 6 ; i++ ) {
glTexImage2D( GL_TEXTURE_CUBE_MAP_POSITIVE_X_EXT+i, 0, internalFormat, scaled_width, scaled_height, 0,
GL_RGBA, GL_UNSIGNED_BYTE, pic[i] );
}
// create and upload the mip map levels
int miplevel;
byte *shrunk[6];
for ( i = 0 ; i < 6 ; i++ ) {
shrunk[i] = R_MipMap( pic[i], scaled_width, scaled_height, false );
}
miplevel = 1;
while ( scaled_width > 1 ) {
for ( i = 0 ; i < 6 ; i++ ) {
byte *shrunken;
glTexImage2D( GL_TEXTURE_CUBE_MAP_POSITIVE_X_EXT+i, miplevel, internalFormat,
scaled_width / 2, scaled_height / 2, 0,
GL_RGBA, GL_UNSIGNED_BYTE, shrunk[i] );
if ( scaled_width > 2 ) {
shrunken = R_MipMap( shrunk[i], scaled_width/2, scaled_height/2, false );
} else {
shrunken = NULL;
}
R_StaticFree( shrunk[i] );
shrunk[i] = shrunken;
}
scaled_width >>= 1;
scaled_height >>= 1;
miplevel++;
}
// see if we messed anything up
GL_CheckErrors();
}
/*
================
ImageProgramStringToFileCompressedFileName
================
*/
void idImage::ImageProgramStringToCompressedFileName( const char *imageProg, char *fileName ) const {
const char *s;
char *f;
strcpy( fileName, "dds/" );
f = fileName + strlen( fileName );
int depth = 0;
// convert all illegal characters to underscores
// this could conceivably produce a duplicated mapping, but we aren't going to worry about it
for ( s = imageProg ; *s ; s++ ) {
if ( *s == '/' || *s == '\\' || *s == '(') {
if ( depth < 4 ) {
*f = '/';
depth ++;
} else {
*f = ' ';
}
f++;
} else if ( *s == '<' || *s == '>' || *s == ':' || *s == '|' || *s == '"' || *s == '.' ) {
*f = '_';
f++;
} else if ( *s == ' ' && *(f-1) == '/' ) { // ignore a space right after a slash
} else if ( *s == ')' || *s == ',' ) { // always ignore these
} else {
*f = *s;
f++;
}
}
*f++ = 0;
strcat( fileName, ".dds" );
}
static const char *R_BC7DepthFolder( textureDepth_t depth ) {
switch ( depth ) {
case TD_SPECULAR:
return "specular";
case TD_BUMP:
return "normal";
case TD_HIGH_QUALITY:
return "high";
case TD_DIFFUSE:
return "diffuse";
case TD_DEFAULT:
default:
return "default";
}
}
static bool R_DepthWantsGeneratedBC7( textureDepth_t depth ) {
if ( depth == TD_HIGH_QUALITY ) {
return false;
}
if ( depth == TD_BUMP && !globalImages->image_bc7NormalMaps.GetBool() ) {
return false;
}
return depth == TD_DIFFUSE || depth == TD_DEFAULT || depth == TD_SPECULAR || depth == TD_BUMP;
}
static void R_ImageProgramStringToGeneratedBC7FileName( const char *imageProg, textureDepth_t depth, char *fileName ) {
const char *s;
char *f;
idStr::snPrintf( fileName, MAX_IMAGE_NAME, "generated/images/bc7/%s/", R_BC7DepthFolder( depth ) );
f = fileName + strlen( fileName );
int folderDepth = 0;
for ( s = imageProg ; *s && ( f - fileName ) < MAX_IMAGE_NAME - 6 ; s++ ) {
if ( *s == '/' || *s == '\\' || *s == '(' ) {
if ( folderDepth < 4 ) {
*f = '/';
folderDepth++;
} else {
*f = '_';
}
f++;
} else if ( *s == '<' || *s == '>' || *s == ':' || *s == '|' || *s == '"' || *s == '.' ) {
*f++ = '_';
} else if ( *s == ' ' && f > fileName && *( f - 1 ) == '/' ) {
} else if ( *s == ')' || *s == ',' ) {
} else {
*f++ = *s;
}
}
*f = 0;
idStr::Copynz( f, ".dds", MAX_IMAGE_NAME - (int)( f - fileName ) );
}
static void R_WriteBitsLSB( byte *block, int &bit, unsigned int value, int count ) {
for ( int i = 0; i < count; i++ ) {
if ( value & ( 1u << i ) ) {
block[bit >> 3] |= (byte)( 1u << ( bit & 7 ) );
}
bit++;
}
}
static int R_BC7Luma( const byte *rgba ) {
return rgba[0] * 54 + rgba[1] * 183 + rgba[2] * 19;
}
static byte *R_MipMapNormalMap( const byte *in, int width, int height, bool preserveBorder ) {
if ( width < 1 || height < 1 || ( width + height == 2 ) ) {
common->FatalError( "R_MipMapNormalMap called with size %i,%i", width, height );
}
const byte border[4] = { in[0], in[1], in[2], in[3] };
const int newWidth = ( width > 1 ) ? width >> 1 : 1;
const int newHeight = ( height > 1 ) ? height >> 1 : 1;
byte *out = (byte *)R_StaticAlloc( newWidth * newHeight * 4 );
for ( int y = 0; y < newHeight; y++ ) {
for ( int x = 0; x < newWidth; x++ ) {
float nx = 0.0f;
float ny = 0.0f;
float nz = 0.0f;
int alpha = 0;
int samples = 0;
for ( int yy = 0; yy < 2; yy++ ) {
const int sy = ( height == 1 ) ? 0 : idMath::ClampInt( 0, height - 1, y * 2 + yy );
for ( int xx = 0; xx < 2; xx++ ) {
const int sx = ( width == 1 ) ? 0 : idMath::ClampInt( 0, width - 1, x * 2 + xx );
const byte *p = in + ( sy * width + sx ) * 4;
nx += p[0] * ( 2.0f / 255.0f ) - 1.0f;
ny += p[1] * ( 2.0f / 255.0f ) - 1.0f;
nz += p[2] * ( 2.0f / 255.0f ) - 1.0f;
alpha += p[3];
samples++;
}
}
float len = idMath::Sqrt( nx * nx + ny * ny + nz * nz );
if ( len > 1.0e-6f ) {
len = 1.0f / len;
nx *= len;
ny *= len;
nz *= len;
} else {
nx = 0.0f;
ny = 0.0f;
nz = 1.0f;
}
byte *dst = out + ( y * newWidth + x ) * 4;
dst[0] = (byte)idMath::ClampInt( 0, 255, (int)( ( nx * 0.5f + 0.5f ) * 255.0f + 0.5f ) );
dst[1] = (byte)idMath::ClampInt( 0, 255, (int)( ( ny * 0.5f + 0.5f ) * 255.0f + 0.5f ) );
dst[2] = (byte)idMath::ClampInt( 0, 255, (int)( ( nz * 0.5f + 0.5f ) * 255.0f + 0.5f ) );
dst[3] = (byte)idMath::ClampInt( 0, 255, ( alpha + samples / 2 ) / samples );
}
}
if ( preserveBorder ) {
R_SetBorderTexels( out, newWidth, newHeight, border );
}
return out;
}
static byte *R_MipMapForBC7Depth( const byte *in, int width, int height, bool preserveBorder, textureDepth_t depth ) {
return ( depth == TD_BUMP ) ? R_MipMapNormalMap( in, width, height, preserveBorder ) : R_MipMap( in, width, height, preserveBorder );
}
static float R_NormalProjection( const byte *rgba, const idVec3 &axis ) {
return ( rgba[0] * ( 2.0f / 255.0f ) - 1.0f ) * axis.x +
( rgba[1] * ( 2.0f / 255.0f ) - 1.0f ) * axis.y +
( rgba[2] * ( 2.0f / 255.0f ) - 1.0f ) * axis.z;
}
static void R_EncodeBC7Mode6Block( const byte *rgba, int width, int height, int x, int y, bool normalMap, byte *block ) {
memset( block, 0, 16 );
byte minColor[4] = { 255, 255, 255, 255 };
byte maxColor[4] = { 0, 0, 0, 0 };
int minLuma = 2147483647;
int maxLuma = -2147483648;
float minProjection = idMath::INFINITY;
float maxProjection = -idMath::INFINITY;
idVec3 averageNormal( 0.0f, 0.0f, 0.0f );
idVec3 variance( 0.0f, 0.0f, 0.0f );
idVec3 axis( 1.0f, 0.0f, 0.0f );
if ( normalMap ) {
for ( int by = 0; by < 4; by++ ) {
const int sy = ( y + by < height ) ? y + by : height - 1;
for ( int bx = 0; bx < 4; bx++ ) {
const int sx = ( x + bx < width ) ? x + bx : width - 1;
const byte *p = rgba + ( sy * width + sx ) * 4;
averageNormal.x += p[0] * ( 2.0f / 255.0f ) - 1.0f;
averageNormal.y += p[1] * ( 2.0f / 255.0f ) - 1.0f;
averageNormal.z += p[2] * ( 2.0f / 255.0f ) - 1.0f;
}
}
averageNormal *= 1.0f / 16.0f;
for ( int by = 0; by < 4; by++ ) {
const int sy = ( y + by < height ) ? y + by : height - 1;
for ( int bx = 0; bx < 4; bx++ ) {
const int sx = ( x + bx < width ) ? x + bx : width - 1;
const byte *p = rgba + ( sy * width + sx ) * 4;
const float nx = p[0] * ( 2.0f / 255.0f ) - 1.0f;
const float ny = p[1] * ( 2.0f / 255.0f ) - 1.0f;
const float nz = p[2] * ( 2.0f / 255.0f ) - 1.0f;
variance.x += ( nx - averageNormal.x ) * ( nx - averageNormal.x );
variance.y += ( ny - averageNormal.y ) * ( ny - averageNormal.y );
variance.z += ( nz - averageNormal.z ) * ( nz - averageNormal.z );
}
}
if ( variance.y > variance.x && variance.y >= variance.z ) {
axis.Set( 0.0f, 1.0f, 0.0f );
} else if ( variance.z > variance.x && variance.z > variance.y ) {
axis.Set( 0.0f, 0.0f, 1.0f );
}
}
for ( int by = 0; by < 4; by++ ) {
const int sy = ( y + by < height ) ? y + by : height - 1;
for ( int bx = 0; bx < 4; bx++ ) {
const int sx = ( x + bx < width ) ? x + bx : width - 1;
const byte *p = rgba + ( sy * width + sx ) * 4;
if ( normalMap ) {
const float projection = R_NormalProjection( p, axis );
if ( projection < minProjection ) {
minProjection = projection;
memcpy( minColor, p, 4 );
}
if ( projection > maxProjection ) {
maxProjection = projection;
memcpy( maxColor, p, 4 );
}
continue;
}
const int luma = R_BC7Luma( p );
if ( luma < minLuma ) {
minLuma = luma;
memcpy( minColor, p, 4 );
}
if ( luma > maxLuma ) {
maxLuma = luma;
memcpy( maxColor, p, 4 );
}
}
}
int bit = 0;
R_WriteBitsLSB( block, bit, 0x40, 7 ); // BC7 mode 6: six zero mode bits followed by one.
const byte *ep[2] = { minColor, maxColor };
for ( int c = 0; c < 4; c++ ) {
R_WriteBitsLSB( block, bit, ep[0][c] >> 1, 7 );
R_WriteBitsLSB( block, bit, ep[1][c] >> 1, 7 );
}
R_WriteBitsLSB( block, bit, ep[0][0] & 1, 1 );
R_WriteBitsLSB( block, bit, ep[1][0] & 1, 1 );
const int range = maxLuma - minLuma;
const float projectionRange = maxProjection - minProjection;
for ( int i = 0; i < 16; i++ ) {
const int bx = i & 3;
const int by = i >> 2;
const int sx = ( x + bx < width ) ? x + bx : width - 1;
const int sy = ( y + by < height ) ? y + by : height - 1;
const byte *p = rgba + ( sy * width + sx ) * 4;
int index = 0;
if ( normalMap && projectionRange > 1.0e-6f ) {
index = idMath::ClampInt( 0, 15, (int)( ( ( R_NormalProjection( p, axis ) - minProjection ) / projectionRange ) * 15.0f + 0.5f ) );
} else if ( range > 0 ) {
index = idMath::ClampInt( 0, 15, ( ( R_BC7Luma( p ) - minLuma ) * 15 + range / 2 ) / range );
}
if ( i == 0 ) {
R_WriteBitsLSB( block, bit, index & 7, 3 );
} else {
R_WriteBitsLSB( block, bit, index, 4 );
}
}
}
static void R_EncodeBC7Image( const byte *rgba, int width, int height, textureDepth_t depth, idList<byte> &out ) {
const int blocksWide = ( width + 3 ) / 4;
const int blocksHigh = ( height + 3 ) / 4;
out.SetNum( blocksWide * blocksHigh * 16 );
const bool normalMap = ( depth == TD_BUMP );
byte *dst = out.Ptr();
for ( int by = 0; by < blocksHigh; by++ ) {
for ( int bx = 0; bx < blocksWide; bx++ ) {
R_EncodeBC7Mode6Block( rgba, width, height, bx * 4, by * 4, normalMap, dst );
dst += 16;
}
}
}
static void R_AppendBytes( idList<byte> &dst, const void *src, int bytes ) {
const int oldNum = dst.Num();
dst.SetNum( oldNum + bytes );
memcpy( dst.Ptr() + oldNum, src, bytes );
}
static unsigned long R_GeneratedBC7CookVersion() {
return DDS_MAKEFOURCC( 'B', '7', 'C', '3' );
}
static bool R_GetDDSAverageColor( const byte *data, int len, float averageColor[4] ) {
if ( len < 4 + (int)sizeof( ddsFileHeader_t ) ) {
return false;
}
const ddsFileHeader_t *header = (const ddsFileHeader_t *)( data + 4 );
if ( LittleLong( header->dwReserved1[0] ) != DDS_MAKEFOURCC( 'A', 'V', 'G', '1' ) ) {
return false;
}
for ( int i = 0; i < 4; i++ ) {
unsigned long bits = LittleLong( header->dwReserved1[1 + i] );
memcpy( &averageColor[i], &bits, sizeof( float ) );
}
return true;
}
bool idImage::CheckGeneratedBC7Image( bool fullLoad, ID_TIME_T sourceTimestamp ) {
if ( !globalImages->image_useBC7GeneratedTextures.GetBool() ||
!R_DepthWantsGeneratedBC7( depth ) ||
fileSystem->PerformingCopyFiles() ||
!renderSystem->IsOpenGLRunning() ||
cubeFiles != CF_2D ) {
return false;
}
char filename[MAX_IMAGE_NAME];
R_ImageProgramStringToGeneratedBC7FileName( imgName, depth, filename );
ID_TIME_T generatedTimestamp;
fileSystem->ReadFile( filename, NULL, &generatedTimestamp );
if ( generatedTimestamp == FILE_NOT_FOUND_TIMESTAMP ) {
return false;
}
if ( sourceTimestamp != FILE_NOT_FOUND_TIMESTAMP && generatedTimestamp < sourceTimestamp ) {
return false;
}
idFile *f = fileSystem->OpenFileRead( filename );
if ( !f ) {
return false;
}
int len = f->Length();
if ( len < 4 + (int)sizeof( ddsFileHeader_t ) + (int)sizeof( ddsFileHeaderDX10_t ) ) {
fileSystem->CloseFile( f );
return false;
}
if ( !fullLoad && len > globalImages->image_cacheMinK.GetInteger() * 1024 ) {
len = globalImages->image_cacheMinK.GetInteger() * 1024;
}
byte *data = (byte *)R_StaticAlloc( len );
f->Read( data, len );
fileSystem->CloseFile( f );
if ( LittleLong( *(unsigned long *)data ) != DDS_MAKEFOURCC( 'D', 'D', 'S', ' ' ) ) {
R_StaticFree( data );
return false;
}
const ddsFileHeader_t *header = (const ddsFileHeader_t *)( data + 4 );
if ( LittleLong( header->dwReserved1[5] ) != R_GeneratedBC7CookVersion() ) {
R_StaticFree( data );
return false;
}
UploadPrecompressedImage( data, len );
timestamp = generatedTimestamp;
R_StaticFree( data );
return true;
}
bool idImage::CookGeneratedBC7Image( const byte *pic, int width, int height, ID_TIME_T sourceTimestamp ) {
if ( !globalImages->image_cookBC7GeneratedTextures.GetBool() ||
!globalImages->image_useBC7GeneratedTextures.GetBool() ||
!R_DepthWantsGeneratedBC7( depth ) ||
globalImages->keepPixelsResident ||
cubeFiles != CF_2D ||
!pic || width <= 0 || height <= 0 ) {
return false;
}
R_SetImageAverageColor( this, pic, width, height );
float sourceAverageColor[4];
for ( int i = 0; i < 4; i++ ) {
sourceAverageColor[i] = averageColor[i];
}
bool preserveBorder = ( repeat == TR_CLAMP_TO_ZERO );
int scaled_width = width;
int scaled_height = height;
GetDownsize( scaled_width, scaled_height );
byte *scaledBuffer = NULL;
if ( scaled_width == width && scaled_height == height ) {
scaledBuffer = (byte *)R_StaticAlloc( sizeof( unsigned ) * scaled_width * scaled_height );
memcpy( scaledBuffer, pic, width * height * 4 );
} else {
scaledBuffer = R_MipMapForBC7Depth( pic, width, height, preserveBorder, depth );
width >>= 1;
height >>= 1;
if ( width < 1 ) {
width = 1;
}
if ( height < 1 ) {
height = 1;
}
while ( width > scaled_width || height > scaled_height ) {
byte *shrunk = R_MipMapForBC7Depth( scaledBuffer, width, height, preserveBorder, depth );
R_StaticFree( scaledBuffer );
scaledBuffer = shrunk;
width >>= 1;
height >>= 1;
if ( width < 1 ) {
width = 1;
}
if ( height < 1 ) {
height = 1;
}
}
scaled_width = width;
scaled_height = height;
}
if ( repeat == TR_CLAMP_TO_ZERO ) {
byte rgba[4] = { 0, 0, 0, 255 };
if ( depth == TD_BUMP ) {
rgba[0] = 128;
rgba[1] = 128;
rgba[2] = 255;
}
R_SetBorderTexels( scaledBuffer, scaled_width, scaled_height, rgba );
}
if ( repeat == TR_CLAMP_TO_ZERO_ALPHA ) {
byte rgba[4] = { 255, 255, 255, 0 };
if ( depth == TD_BUMP ) {
rgba[0] = 128;
rgba[1] = 128;
rgba[2] = 255;
}
R_SetBorderTexels( scaledBuffer, scaled_width, scaled_height, rgba );
}
for ( int i = 0; i < 4; i++ ) {
averageColor[i] = sourceAverageColor[i];
}
idList<byte> fileBytes;
fileBytes.SetGranularity( 1024 );
const unsigned long magic = DDS_MAKEFOURCC( 'D', 'D', 'S', ' ' );
const int numLevels = NumLevelsForImageSize( scaled_width, scaled_height );
ddsFileHeader_t header;
memset( &header, 0, sizeof( header ) );
header.dwSize = sizeof( header );
header.dwFlags = DDSF_CAPS | DDSF_PIXELFORMAT | DDSF_WIDTH | DDSF_HEIGHT | DDSF_LINEARSIZE;
header.dwHeight = scaled_height;
header.dwWidth = scaled_width;
header.dwPitchOrLinearSize = ( ( scaled_width + 3 ) / 4 ) * 16;
header.dwMipMapCount = numLevels;
header.dwFlags |= DDSF_MIPMAPCOUNT;
header.dwCaps1 = DDSF_TEXTURE | DDSF_MIPMAP | DDSF_COMPLEX;
header.ddspf.dwSize = sizeof( header.ddspf );
header.ddspf.dwFlags = DDSF_FOURCC;
header.ddspf.dwFourCC = DDS_MAKEFOURCC( 'D', 'X', '1', '0' );
header.dwReserved1[0] = DDS_MAKEFOURCC( 'A', 'V', 'G', '1' );
for ( int i = 0; i < 4; i++ ) {
memcpy( &header.dwReserved1[1 + i], &averageColor[i], sizeof( float ) );
}
header.dwReserved1[5] = R_GeneratedBC7CookVersion();
ddsFileHeaderDX10_t header10;
memset( &header10, 0, sizeof( header10 ) );
header10.dxgiFormat = DDS_DXGI_FORMAT_BC7_UNORM;
header10.resourceDimension = DDS_DIMENSION_TEXTURE2D;
header10.arraySize = 1;
R_AppendBytes( fileBytes, &magic, sizeof( magic ) );
R_AppendBytes( fileBytes, &header, sizeof( header ) );
R_AppendBytes( fileBytes, &header10, sizeof( header10 ) );
byte *mipBuffer = scaledBuffer;
int mipWidth = scaled_width;
int mipHeight = scaled_height;
for ( int level = 0; level < numLevels; level++ ) {
idList<byte> encoded;
R_EncodeBC7Image( mipBuffer, mipWidth, mipHeight, depth, encoded );
R_AppendBytes( fileBytes, encoded.Ptr(), encoded.Num() );
if ( level + 1 < numLevels ) {
byte *shrunk = R_MipMapForBC7Depth( mipBuffer, mipWidth, mipHeight, preserveBorder, depth );
if ( mipBuffer != scaledBuffer ) {
R_StaticFree( mipBuffer );
}
mipBuffer = shrunk;
mipWidth >>= 1;
mipHeight >>= 1;
if ( mipWidth < 1 ) {
mipWidth = 1;
}
if ( mipHeight < 1 ) {
mipHeight = 1;
}
}
}
if ( mipBuffer != scaledBuffer ) {
R_StaticFree( mipBuffer );
}
R_StaticFree( scaledBuffer );
char filename[MAX_IMAGE_NAME];
R_ImageProgramStringToGeneratedBC7FileName( imgName, depth, filename );
fileSystem->WriteFile( filename, fileBytes.Ptr(), fileBytes.Num(), "fs_savepath" );
UploadPrecompressedImage( fileBytes.Ptr(), fileBytes.Num() );
if ( texnum != TEXTURE_NOT_LOADED ) {
glTextureAverageColorQD3D12( texnum, averageColor[0], averageColor[1], averageColor[2], averageColor[3] );
}
timestamp = sourceTimestamp;
precompressedFile = true;
return texnum != TEXTURE_NOT_LOADED;
}
/*
==================
NumLevelsForImageSize
==================
*/
int idImage::NumLevelsForImageSize( int width, int height ) const {
int numLevels = 1;
while ( width > 1 || height > 1 ) {
numLevels++;
width >>= 1;
height >>= 1;
}
return numLevels;
}
/*
================
WritePrecompressedImage
When we are happy with our source data, we can write out precompressed
versions of everything to speed future load times.
================
*/
void idImage::WritePrecompressedImage() {
#if 0
// Always write the precompressed image if we're making a build
if ( !com_makingBuild.GetBool() ) {
if ( !globalImages->image_writePrecompressedTextures.GetBool() || !globalImages->image_usePrecompressedTextures.GetBool() ) {
return;
}
}
if ( !glConfig.isInitialized ) {
return;
}
char filename[MAX_IMAGE_NAME];
ImageProgramStringToCompressedFileName( imgName, filename );
int numLevels = NumLevelsForImageSize( uploadWidth, uploadHeight );
if ( numLevels > MAX_TEXTURE_LEVELS ) {
common->Warning( "R_WritePrecompressedImage: level > MAX_TEXTURE_LEVELS for image %s", filename );
return;
}
// glGetTexImage only supports a small subset of all the available internal formats
// We have to use BGRA because DDS is a windows based format
int altInternalFormat = 0;
int bitSize = 0;
switch ( internalFormat ) {
case GL_COLOR_INDEX8_EXT:
case GL_COLOR_INDEX:
// this will not work with dds viewers but we need it in this format to save disk
// load speed ( i.e. size )
altInternalFormat = GL_COLOR_INDEX;
bitSize = 24;
break;
case 1:
case GL_INTENSITY8:
case GL_LUMINANCE8:
case 3:
case GL_RGB8:
altInternalFormat = GL_BGR_EXT;
bitSize = 24;
break;
case GL_LUMINANCE8_ALPHA8:
case 4:
case GL_RGBA8:
altInternalFormat = GL_BGRA_EXT;
bitSize = 32;
break;
case GL_ALPHA8:
altInternalFormat = GL_ALPHA;
bitSize = 8;
break;
default:
if ( FormatIsDXT( internalFormat ) ) {
altInternalFormat = internalFormat;
} else {
common->Warning("Unknown or unsupported format for %s", filename);
return;
}
}
if ( globalImages->image_useOffLineCompression.GetBool() && FormatIsDXT( altInternalFormat ) ) {
idStr outFile = fileSystem->RelativePathToOSPath( filename, "fs_basepath" );
idStr inFile = outFile;
inFile.StripFileExtension();
inFile.SetFileExtension( "tga" );
idStr format;
if ( depth == TD_BUMP ) {
format = "RXGB +red 0.0 +green 0.5 +blue 0.5";
} else {
switch ( altInternalFormat ) {
case GL_COMPRESSED_RGB_S3TC_DXT1_EXT:
format = "DXT1";
break;
case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT:
format = "DXT1 -alpha_threshold";
break;
case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT:
format = "DXT3";
break;
case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT:
format = "DXT5";
break;
}
}
globalImages->AddDDSCommand( va( "z:/d3xp/compressonator/thecompressonator -convert \"%s\" \"%s\" %s -mipmaps\n", inFile.c_str(), outFile.c_str(), format.c_str() ) );
return;
}
ddsFileHeader_t header;
memset( &header, 0, sizeof(header) );
header.dwSize = sizeof(header);
header.dwFlags = DDSF_CAPS | DDSF_PIXELFORMAT | DDSF_WIDTH | DDSF_HEIGHT;
header.dwHeight = uploadHeight;
header.dwWidth = uploadWidth;
// hack in our monochrome flag for the NV20 optimization
if ( isMonochrome ) {
header.dwFlags |= DDSF_ID_MONOCHROME;
}
if ( FormatIsDXT( altInternalFormat ) ) {
// size (in bytes) of the compressed base image
header.dwFlags |= DDSF_LINEARSIZE;
header.dwPitchOrLinearSize = ( ( uploadWidth + 3 ) / 4 ) * ( ( uploadHeight + 3 ) / 4 )*
(altInternalFormat <= GL_COMPRESSED_RGBA_S3TC_DXT1_EXT ? 8 : 16);
}
else {
// 4 Byte aligned line width (from nv_dds)
header.dwFlags |= DDSF_PITCH;
header.dwPitchOrLinearSize = ( ( uploadWidth * bitSize + 31 ) & -32 ) >> 3;
}
header.dwCaps1 = DDSF_TEXTURE;
if ( numLevels > 1 ) {
header.dwMipMapCount = numLevels;
header.dwFlags |= DDSF_MIPMAPCOUNT;
header.dwCaps1 |= DDSF_MIPMAP | DDSF_COMPLEX;
}
header.ddspf.dwSize = sizeof(header.ddspf);
if ( FormatIsDXT( altInternalFormat ) ) {
header.ddspf.dwFlags = DDSF_FOURCC;
switch ( altInternalFormat ) {
case GL_COMPRESSED_RGB_S3TC_DXT1_EXT:
header.ddspf.dwFourCC = DDS_MAKEFOURCC('D','X','T','1');
break;
case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT:
header.ddspf.dwFlags |= DDSF_ALPHAPIXELS;
header.ddspf.dwFourCC = DDS_MAKEFOURCC('D','X','T','1');
break;
case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT:
header.ddspf.dwFourCC = DDS_MAKEFOURCC('D','X','T','3');
break;
case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT:
header.ddspf.dwFourCC = DDS_MAKEFOURCC('D','X','T','5');
break;
}
} else {
header.ddspf.dwFlags = ( internalFormat == GL_COLOR_INDEX8_EXT ) ? DDSF_RGB | DDSF_ID_INDEXCOLOR : DDSF_RGB;
header.ddspf.dwRGBBitCount = bitSize;
switch ( altInternalFormat ) {
case GL_BGRA_EXT:
case GL_LUMINANCE_ALPHA:
header.ddspf.dwFlags |= DDSF_ALPHAPIXELS;
header.ddspf.dwABitMask = 0xFF000000;
// Fall through
case GL_BGR_EXT:
case GL_LUMINANCE:
case GL_COLOR_INDEX:
header.ddspf.dwRBitMask = 0x00FF0000;
header.ddspf.dwGBitMask = 0x0000FF00;
header.ddspf.dwBBitMask = 0x000000FF;
break;
case GL_ALPHA:
header.ddspf.dwFlags = DDSF_ALPHAPIXELS;
header.ddspf.dwABitMask = 0xFF000000;
break;
default:
common->Warning( "Unknown or unsupported format for %s", filename );
return;
}
}
idFile *f = fileSystem->OpenFileWrite( filename );
if ( f == NULL ) {
common->Warning( "Could not open %s trying to write precompressed image", filename );
return;
}
common->Printf( "Writing precompressed image: %s\n", filename );
f->Write( "DDS ", 4 );
f->Write( &header, sizeof(header) );
// bind to the image so we can read back the contents
Bind();
//glPixelStorei( GL_PACK_ALIGNMENT, 1 ); // otherwise small rows get padded to 32 bits
int uw = uploadWidth;
int uh = uploadHeight;
// Will be allocated first time through the loop
byte *data = NULL;
for ( int level = 0 ; level < numLevels ; level++ ) {
int size = 0;
if ( FormatIsDXT( altInternalFormat ) ) {
size = ( ( uw + 3 ) / 4 ) * ( ( uh + 3 ) / 4 ) *
(altInternalFormat <= GL_COMPRESSED_RGBA_S3TC_DXT1_EXT ? 8 : 16);
} else {
size = uw * uh * (bitSize / 8);
}
if (data == NULL) {
data = (byte *)R_StaticAlloc( size );
}
if ( FormatIsDXT( altInternalFormat ) ) {
glGetCompressedTexImageARB( GL_TEXTURE_2D, level, data );
} else {
glGetTexImage( GL_TEXTURE_2D, level, altInternalFormat, GL_UNSIGNED_BYTE, data );
}
f->Write( data, size );
uw /= 2;
uh /= 2;
if (uw < 1) {
uw = 1;
}
if (uh < 1) {
uh = 1;
}
}
if (data != NULL) {
R_StaticFree( data );
}
fileSystem->CloseFile( f );
#endif
}
/*
================
ShouldImageBePartialCached
Returns true if there is a precompressed image, and it is large enough
to be worth caching
================
*/
bool idImage::ShouldImageBePartialCached() {
if ( !glConfig.textureCompressionAvailable ) {
return false;
}
if ( !globalImages->image_useCache.GetBool() ) {
return false;
}
// the allowDownSize flag does double-duty as don't-partial-load
if ( !allowDownSize ) {
return false;
}
if ( globalImages->image_cacheMinK.GetInteger() <= 0 ) {
return false;
}
// if we are doing a copyFiles, make sure the original images are referenced
if ( fileSystem->PerformingCopyFiles() ) {
return false;
}
char filename[MAX_IMAGE_NAME];
ImageProgramStringToCompressedFileName( imgName, filename );
// get the file timestamp
fileSystem->ReadFile( filename, NULL, &timestamp );
if ( timestamp == FILE_NOT_FOUND_TIMESTAMP ) {
return false;
}
// open it and get the file size
idFile *f;
f = fileSystem->OpenFileRead( filename );
if ( !f ) {
return false;
}
int len = f->Length();
fileSystem->CloseFile( f );
if ( len <= globalImages->image_cacheMinK.GetInteger() * 1024 ) {
return false;
}
// we do want to do a partial load
return true;
}
/*
================
CheckPrecompressedImage
If fullLoad is false, only the small mip levels of the image will be loaded
================
*/
bool idImage::CheckPrecompressedImage( bool fullLoad ) {
#if 1 // ( _D3XP had disabled ) - Allow grabbing of DDS's from original Doom pak files
// if we are doing a copyFiles, make sure the original images are referenced
if ( fileSystem->PerformingCopyFiles() ) {
return false;
}
#endif
if ( depth == TD_BUMP && globalImages->image_useNormalCompression.GetInteger() != 2 ) {
return false;
}
// god i love last minute hacks :-)
if ( com_machineSpec.GetInteger() >= 1 && com_videoRam.GetInteger() >= 128 && imgName.Icmpn( "lights/", 7 ) == 0 ) {
return false;
}
if (!renderSystem->IsOpenGLRunning()) {
return false;
}
char filename[MAX_IMAGE_NAME];
ImageProgramStringToCompressedFileName( imgName, filename );
// get the file timestamp
ID_TIME_T precompTimestamp;
fileSystem->ReadFile( filename, NULL, &precompTimestamp );
if ( precompTimestamp == FILE_NOT_FOUND_TIMESTAMP ) {
return false;
}
if ( !generatorFunction && timestamp != FILE_NOT_FOUND_TIMESTAMP ) {
if ( precompTimestamp < timestamp ) {
// The image has changed after being precompressed
return false;
}
}
timestamp = precompTimestamp;
// open it and just read the header
idFile *f;
f = fileSystem->OpenFileRead( filename );
if ( !f ) {
return false;
}
int len = f->Length();
if ( len < sizeof( ddsFileHeader_t ) ) {
fileSystem->CloseFile( f );
return false;
}
if ( !fullLoad && len > globalImages->image_cacheMinK.GetInteger() * 1024 ) {
len = globalImages->image_cacheMinK.GetInteger() * 1024;
}
byte *data = (byte *)R_StaticAlloc( len );
f->Read( data, len );
fileSystem->CloseFile( f );
unsigned long magic = LittleLong( *(unsigned long *)data );
ddsFileHeader_t *_header = (ddsFileHeader_t *)(data + 4);
int ddspf_dwFlags = LittleLong( _header->ddspf.dwFlags );
if ( magic != DDS_MAKEFOURCC('D', 'D', 'S', ' ')) {
common->Printf( "CheckPrecompressedImage( %s ): magic != 'DDS '\n", imgName.c_str() );
R_StaticFree( data );
return false;
}
// if we don't support color index textures, we must load the full image
// should we just expand the 256 color image to 32 bit for upload?
if ( ddspf_dwFlags & DDSF_ID_INDEXCOLOR && !glConfig.sharedTexturePaletteAvailable ) {
R_StaticFree( data );
return false;
}
// upload all the levels
UploadPrecompressedImage( data, len );
R_StaticFree( data );
return true;
}
/*
===================
UploadPrecompressedImage
This can be called by the front end during nromal loading,
or by the backend after a background read of the file
has completed
===================
*/
void idImage::UploadPrecompressedImage( byte *data, int len ) {
ddsFileHeader_t *header = (ddsFileHeader_t *)(data + 4);
float ddsAverageColor[4];
const bool hasDDSAverageColor = R_GetDDSAverageColor( data, len, ddsAverageColor );
// ( not byte swapping dwReserved1 dwReserved2 )
header->dwSize = LittleLong( header->dwSize );
header->dwFlags = LittleLong( header->dwFlags );
header->dwHeight = LittleLong( header->dwHeight );
header->dwWidth = LittleLong( header->dwWidth );
header->dwPitchOrLinearSize = LittleLong( header->dwPitchOrLinearSize );
header->dwDepth = LittleLong( header->dwDepth );
header->dwMipMapCount = LittleLong( header->dwMipMapCount );
header->dwCaps1 = LittleLong( header->dwCaps1 );
header->dwCaps2 = LittleLong( header->dwCaps2 );
header->ddspf.dwSize = LittleLong( header->ddspf.dwSize );
header->ddspf.dwFlags = LittleLong( header->ddspf.dwFlags );
header->ddspf.dwFourCC = LittleLong( header->ddspf.dwFourCC );
header->ddspf.dwRGBBitCount = LittleLong( header->ddspf.dwRGBBitCount );
header->ddspf.dwRBitMask = LittleLong( header->ddspf.dwRBitMask );
header->ddspf.dwGBitMask = LittleLong( header->ddspf.dwGBitMask );
header->ddspf.dwBBitMask = LittleLong( header->ddspf.dwBBitMask );
header->ddspf.dwABitMask = LittleLong( header->ddspf.dwABitMask );
// generate the texture number
glGenTextures( 1, &texnum );
if ( hasDDSAverageColor ) {
for ( int i = 0; i < 4; i++ ) {
averageColor[i] = ddsAverageColor[i];
}
glTextureAverageColorQD3D12( texnum, averageColor[0], averageColor[1], averageColor[2], averageColor[3] );
}
int externalFormat = 0;
const ddsFileHeaderDX10_t *header10 = NULL;
int dataOffset = sizeof( ddsFileHeader_t ) + 4;
precompressedFile = true;
uploadWidth = header->dwWidth;
uploadHeight = header->dwHeight;
if ( header->ddspf.dwFlags & DDSF_FOURCC ) {
switch ( header->ddspf.dwFourCC ) {
case DDS_MAKEFOURCC( 'D', 'X', 'T', '1' ):
if ( header->ddspf.dwFlags & DDSF_ALPHAPIXELS ) {
internalFormat = GL_COMPRESSED_RGBA_S3TC_DXT1_EXT;
} else {
internalFormat = GL_COMPRESSED_RGB_S3TC_DXT1_EXT;
}
break;
case DDS_MAKEFOURCC( 'D', 'X', 'T', '3' ):
internalFormat = GL_COMPRESSED_RGBA_S3TC_DXT3_EXT;
break;
case DDS_MAKEFOURCC( 'D', 'X', 'T', '5' ):
internalFormat = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;
break;
case DDS_MAKEFOURCC( 'R', 'X', 'G', 'B' ):
internalFormat = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;
break;
case DDS_MAKEFOURCC( 'D', 'X', '1', '0' ):
if ( len < (int)( sizeof( ddsFileHeader_t ) + 4 + sizeof( ddsFileHeaderDX10_t ) ) ) {
common->Warning( "Invalid DX10 DDS header\n" );
return;
}
header10 = (const ddsFileHeaderDX10_t *)( data + sizeof( ddsFileHeader_t ) + 4 );
switch ( LittleLong( header10->dxgiFormat ) ) {
case DDS_DXGI_FORMAT_BC7_UNORM:
internalFormat = GL_COMPRESSED_RGBA_BPTC_UNORM_ARB;
break;
default:
common->Warning( "Unsupported DX10 DDS format %i\n", LittleLong( header10->dxgiFormat ) );
return;
}
dataOffset += sizeof( ddsFileHeaderDX10_t );
break;
default:
common->Warning( "Invalid compressed internal format\n" );
return;
}
} else if ( ( header->ddspf.dwFlags & DDSF_RGBA ) && header->ddspf.dwRGBBitCount == 32 ) {
externalFormat = GL_BGRA_EXT;
internalFormat = GL_RGBA8;
} else if ( ( header->ddspf.dwFlags & DDSF_RGB ) && header->ddspf.dwRGBBitCount == 32 ) {
externalFormat = GL_BGRA_EXT;
internalFormat = GL_RGBA8;
} else if ( ( header->ddspf.dwFlags & DDSF_RGB ) && header->ddspf.dwRGBBitCount == 24 ) {
if ( header->ddspf.dwFlags & DDSF_ID_INDEXCOLOR ) {
externalFormat = GL_COLOR_INDEX;
internalFormat = GL_COLOR_INDEX8_EXT;
} else {
externalFormat = GL_BGR_EXT;
internalFormat = GL_RGB8;
}
} else if ( header->ddspf.dwRGBBitCount == 8 ) {
externalFormat = GL_ALPHA;
internalFormat = GL_ALPHA8;
} else {
common->Warning( "Invalid uncompressed internal format\n" );
return;
}
// we need the monochrome flag for the NV20 optimized path
if ( header->dwFlags & DDSF_ID_MONOCHROME ) {
isMonochrome = true;
}
type = TT_2D; // FIXME: we may want to support pre-compressed cube maps in the future
Bind();
int numMipmaps = 1;
if ( header->dwFlags & DDSF_MIPMAPCOUNT ) {
numMipmaps = header->dwMipMapCount;
}
int uw = uploadWidth;
int uh = uploadHeight;
// We may skip some mip maps if we are downsizing
int skipMip = 0;
GetDownsize( uploadWidth, uploadHeight );
byte *imagedata = data + dataOffset;
for ( int i = 0 ; i < numMipmaps; i++ ) {
int size = 0;
if ( FormatIsBlockCompressed( internalFormat ) ) {
size = ( ( uw + 3 ) / 4 ) * ( ( uh + 3 ) / 4 ) *
BlockBytesForInternalFormat( internalFormat );
} else {
size = uw * uh * (header->ddspf.dwRGBBitCount / 8);
}
if ( uw > uploadWidth || uh > uploadHeight ) {
skipMip++;
} else {
if ( FormatIsBlockCompressed( internalFormat ) ) {
glCompressedTexImage2DARB( GL_TEXTURE_2D, i - skipMip, internalFormat, uw, uh, 0, size, imagedata );
} else {
glTexImage2D( GL_TEXTURE_2D, i - skipMip, internalFormat, uw, uh, 0, externalFormat, GL_UNSIGNED_BYTE, imagedata );
}
}
imagedata += size;
uw /= 2;
uh /= 2;
if (uw < 1) {
uw = 1;
}
if (uh < 1) {
uh = 1;
}
}
SetImageFilterAndRepeat();
}
/*
===============
ActuallyLoadImage
Absolutely every image goes through this path
On exit, the idImage will have a valid OpenGL texture number that can be bound
===============
*/
void idImage::ActuallyLoadImage( bool checkForPrecompressed, bool fromBackEnd ) {
int width, height;
byte *pic;
// this is the ONLY place generatorFunction will ever be called
if ( generatorFunction ) {
generatorFunction( this );
return;
}
// if we are a partial image, we are only going to load from a compressed file
if ( isPartialImage ) {
if ( CheckPrecompressedImage( false ) ) {
return;
}
// this is an error -- the partial image failed to load
MakeDefault();
return;
}
//
// load the image from disk
//
if ( cubeFiles != CF_2D ) {
byte *pics[6];
// we don't check for pre-compressed cube images currently
R_LoadCubeImages( imgName, cubeFiles, pics, &width, &timestamp );
if ( pics[0] == NULL ) {
common->Warning( "Couldn't load cube image: %s", imgName.c_str() );
MakeDefault();
return;
}
GenerateCubeImage( (const byte **)pics, width, filter, allowDownSize, depth );
precompressedFile = false;
for ( int i = 0 ; i < 6 ; i++ ) {
if ( pics[i] ) {
R_StaticFree( pics[i] );
}
}
} else {
ID_TIME_T sourceTimestamp = FILE_NOT_FOUND_TIMESTAMP;
const bool bc7GeneratedWanted = globalImages->image_useBC7GeneratedTextures.GetBool() && R_DepthWantsGeneratedBC7( depth );
if ( bc7GeneratedWanted ) {
R_LoadImageProgram( imgName, NULL, NULL, NULL, &sourceTimestamp, NULL );
if ( CheckGeneratedBC7Image( true, sourceTimestamp ) ) {
return;
}
}
// see if we have a pre-generated image file that is
// already image processed and compressed
const bool shouldCookBC7FromSource = bc7GeneratedWanted &&
globalImages->image_cookBC7GeneratedTextures.GetBool() &&
sourceTimestamp != FILE_NOT_FOUND_TIMESTAMP;
if ( !shouldCookBC7FromSource && checkForPrecompressed && globalImages->image_usePrecompressedTextures.GetBool() ) {
if ( CheckPrecompressedImage( true ) ) {
// we got the precompressed image
return;
}
// fall through to load the normal image
}
R_LoadImageProgram( imgName, &pic, &width, &height, &timestamp, &depth );
if ( pic == NULL ) {
common->Warning( "Couldn't load image: %s", imgName.c_str() );
MakeDefault();
return;
}
/*
// swap the red and alpha for rxgb support
// do this even on tga normal maps so we only have to use
// one fragment program
// if the image is precompressed ( either in palletized mode or true rxgb mode )
// then it is loaded above and the swap never happens here
if ( depth == TD_BUMP && globalImages->image_useNormalCompression.GetInteger() != 1 ) {
for ( int i = 0; i < width * height * 4; i += 4 ) {
pic[ i + 3 ] = pic[ i ];
pic[ i ] = 0;
}
}
*/
// build a hash for checking duplicate image files
// NOTE: takes about 10% of image load times (SD)
// may not be strictly necessary, but some code uses it, so let's leave it in
imageHash = MD4_BlockChecksum( pic, width * height * 4 );
if ( CookGeneratedBC7Image( pic, width, height, timestamp ) ) {
if ( globalImages->keepPixelsResident ) {
if ( residentPixels ) {
R_StaticFree( residentPixels );
}
residentPixels = pic;
} else {
R_StaticFree( pic );
}
return;
}
GenerateImage( pic, width, height, filter, allowDownSize, repeat, depth );
timestamp = timestamp;
precompressedFile = false;
if (globalImages->keepPixelsResident)
{
if (residentPixels) {
R_StaticFree(residentPixels);
residentPixels = NULL;
}
residentPixels = pic;
}
else
{
R_StaticFree(pic);
}
// write out the precompressed version of this file if needed
WritePrecompressedImage();
}
}
//=========================================================================================================
/*
===============
PurgeImage
===============
*/
void idImage::PurgeImage() {
if ( texnum != TEXTURE_NOT_LOADED ) {
// sometimes is NULL when exiting with an error
if ( glDeleteTextures ) {
glDeleteTextures( 1, &texnum ); // this should be the ONLY place it is ever called!
}
texnum = TEXTURE_NOT_LOADED;
}
if (residentPixels) {
R_StaticFree(residentPixels);
residentPixels = NULL;
}
// clear all the current binding caches, so the next bind will do a real one
for ( int i = 0 ; i < MAX_MULTITEXTURE_UNITS ; i++ ) {
backEnd.glState.tmu[i].current2DMap = -1;
backEnd.glState.tmu[i].current3DMap = -1;
backEnd.glState.tmu[i].currentCubeMap = -1;
}
}
/*
==============
Bind
Automatically enables 2D mapping, cube mapping, or 3D texturing if needed
==============
*/
void idImage::Bind() {
if ( tr.logFile ) {
RB_LogComment( "idImage::Bind( %s )\n", imgName.c_str() );
}
// if this is an image that we are caching, move it to the front of the LRU chain
if ( partialImage ) {
if ( cacheUsageNext ) {
// unlink from old position
cacheUsageNext->cacheUsagePrev = cacheUsagePrev;
cacheUsagePrev->cacheUsageNext = cacheUsageNext;
}
// link in at the head of the list
cacheUsageNext = globalImages->cacheLRU.cacheUsageNext;
cacheUsagePrev = &globalImages->cacheLRU;
cacheUsageNext->cacheUsagePrev = this;
cacheUsagePrev->cacheUsageNext = this;
}
// load the image if necessary (FIXME: not SMP safe!)
if ( texnum == TEXTURE_NOT_LOADED ) {
if ( partialImage ) {
// if we have a partial image, go ahead and use that
this->partialImage->Bind();
// start a background load of the full thing if it isn't already in the queue
if ( !backgroundLoadInProgress ) {
StartBackgroundImageLoad();
}
return;
}
// load the image on demand here, which isn't our normal game operating mode
ActuallyLoadImage( true, true ); // check for precompressed, load is from back end
}
// bump our statistic counters
frameUsed = backEnd.frameCount;
bindCount++;
tmu_t *tmu = &backEnd.glState.tmu[backEnd.glState.currenttmu];
// enable or disable apropriate texture modes
if ( tmu->textureType != type && ( backEnd.glState.currenttmu < glConfig.maxTextureUnits ) ) {
if ( tmu->textureType == TT_CUBIC ) {
glDisable( GL_TEXTURE_CUBE_MAP_EXT );
} else if ( tmu->textureType == TT_3D ) {
glDisable( GL_TEXTURE_3D );
} else if ( tmu->textureType == TT_2D ) {
glDisable( GL_TEXTURE_2D );
}
if ( type == TT_CUBIC ) {
glEnable( GL_TEXTURE_CUBE_MAP_EXT );
} else if ( type == TT_3D ) {
glEnable( GL_TEXTURE_3D );
} else if ( type == TT_2D ) {
glEnable( GL_TEXTURE_2D );
}
tmu->textureType = type;
}
// bind the texture
if ( type == TT_2D ) {
if ( tmu->current2DMap != texnum ) {
tmu->current2DMap = texnum;
glBindTexture( GL_TEXTURE_2D, texnum );
}
} else if ( type == TT_CUBIC ) {
if ( tmu->currentCubeMap != texnum ) {
tmu->currentCubeMap = texnum;
glBindTexture( GL_TEXTURE_CUBE_MAP_EXT, texnum );
}
} else if ( type == TT_3D ) {
if ( tmu->current3DMap != texnum ) {
tmu->current3DMap = texnum;
glBindTexture( GL_TEXTURE_3D, texnum );
}
}
if ( com_purgeAll.GetBool() ) {
GLclampf priority = 1.0f;
//glPrioritizeTextures( 1, &texnum, &priority );
}
}
/*
==============
BindFragment
Fragment programs explicitly say which type of map they want, so we don't need to
do any enable / disable changes
==============
*/
void idImage::BindFragment() {
if ( tr.logFile ) {
RB_LogComment( "idImage::BindFragment %s )\n", imgName.c_str() );
}
// if this is an image that we are caching, move it to the front of the LRU chain
if ( partialImage ) {
if ( cacheUsageNext ) {
// unlink from old position
cacheUsageNext->cacheUsagePrev = cacheUsagePrev;
cacheUsagePrev->cacheUsageNext = cacheUsageNext;
}
// link in at the head of the list
cacheUsageNext = globalImages->cacheLRU.cacheUsageNext;
cacheUsagePrev = &globalImages->cacheLRU;
cacheUsageNext->cacheUsagePrev = this;
cacheUsagePrev->cacheUsageNext = this;
}
// load the image if necessary (FIXME: not SMP safe!)
if ( texnum == TEXTURE_NOT_LOADED ) {
if ( partialImage ) {
// if we have a partial image, go ahead and use that
this->partialImage->BindFragment();
// start a background load of the full thing if it isn't already in the queue
if ( !backgroundLoadInProgress ) {
StartBackgroundImageLoad();
}
return;
}
// load the image on demand here, which isn't our normal game operating mode
ActuallyLoadImage( true, true ); // check for precompressed, load is from back end
}
// bump our statistic counters
frameUsed = backEnd.frameCount;
bindCount++;
// bind the texture
if ( type == TT_2D ) {
glBindTexture( GL_TEXTURE_2D, texnum );
} else if ( type == TT_RECT ) {
glBindTexture( GL_TEXTURE_RECTANGLE_NV, texnum );
} else if ( type == TT_CUBIC ) {
glBindTexture( GL_TEXTURE_CUBE_MAP_EXT, texnum );
} else if ( type == TT_3D ) {
glBindTexture( GL_TEXTURE_3D, texnum );
}
}
/*
====================
CopyFramebuffer
====================
*/
void idImage::CopyFramebuffer( int x, int y, int imageWidth, int imageHeight, bool useOversizedBuffer ) {
Bind();
if ( cvarSystem->GetCVarBool( "g_lowresFullscreenFX" ) ) {
imageWidth = 512;
imageHeight = 512;
}
// if the size isn't a power of 2, the image must be increased in size
int potWidth, potHeight;
potWidth = MakePowerOfTwo( imageWidth );
potHeight = MakePowerOfTwo( imageHeight );
GetDownsize( imageWidth, imageHeight );
GetDownsize( potWidth, potHeight );
glReadBuffer( GL_BACK );
// only resize if the current dimensions can't hold it at all,
// otherwise subview renderings could thrash this
if ( ( useOversizedBuffer && ( uploadWidth < potWidth || uploadHeight < potHeight ) )
|| ( !useOversizedBuffer && ( uploadWidth != potWidth || uploadHeight != potHeight ) ) ) {
uploadWidth = potWidth;
uploadHeight = potHeight;
if ( potWidth == imageWidth && potHeight == imageHeight ) {
glCopyTexImage2D( GL_TEXTURE_2D, 0, GL_RGB8, x, y, imageWidth, imageHeight, 0 );
} else {
byte *junk;
// we need to create a dummy image with power of two dimensions,
// then do a glCopyTexSubImage2D of the data we want
// this might be a 16+ meg allocation, which could fail on _alloca
junk = (byte *)Mem_Alloc( potWidth * potHeight * 4 );
memset( junk, 0, potWidth * potHeight * 4 ); //!@#
#if 0 // Disabling because it's unnecessary and introduces a green strip on edge of _currentRender
for ( int i = 0 ; i < potWidth * potHeight * 4 ; i+=4 ) {
junk[i+1] = 255;
}
#endif
glTexImage2D( GL_TEXTURE_2D, 0, GL_RGB, potWidth, potHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, junk );
Mem_Free( junk );
glCopyTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, x, y, imageWidth, imageHeight );
}
} else {
// otherwise, just subimage upload it so that drivers can tell we are going to be changing
// it and don't try and do a texture compression or some other silliness
glCopyTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, x, y, imageWidth, imageHeight );
}
// if the image isn't a full power of two, duplicate an extra row and/or column to fix bilerps
if ( imageWidth != potWidth ) {
glCopyTexSubImage2D( GL_TEXTURE_2D, 0, imageWidth, 0, x+imageWidth-1, y, 1, imageHeight );
}
if ( imageHeight != potHeight ) {
glCopyTexSubImage2D( GL_TEXTURE_2D, 0, 0, imageHeight, x, y+imageHeight-1, imageWidth, 1 );
}
glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
backEnd.c_copyFrameBuffer++;
}
/*
====================
CopyDepthbuffer
This should just be part of copyFramebuffer once we have a proper image type field
====================
*/
void idImage::CopyDepthbuffer( int x, int y, int imageWidth, int imageHeight ) {
Bind();
// if the size isn't a power of 2, the image must be increased in size
int potWidth, potHeight;
potWidth = MakePowerOfTwo( imageWidth );
potHeight = MakePowerOfTwo( imageHeight );
if ( uploadWidth != potWidth || uploadHeight != potHeight ) {
uploadWidth = potWidth;
uploadHeight = potHeight;
if ( potWidth == imageWidth && potHeight == imageHeight ) {
glCopyTexImage2D( GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, x, y, imageWidth, imageHeight, 0 );
} else {
// we need to create a dummy image with power of two dimensions,
// then do a glCopyTexSubImage2D of the data we want
glTexImage2D( GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, potWidth, potHeight, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_BYTE, NULL );
glCopyTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, x, y, imageWidth, imageHeight );
}
} else {
// otherwise, just subimage upload it so that drivers can tell we are going to be changing
// it and don't try and do a texture compression or some other silliness
glCopyTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, x, y, imageWidth, imageHeight );
}
// glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );
// glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST );
glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
}
/*
=============
RB_UploadScratchImage
if rows = cols * 6, assume it is a cube map animation
=============
*/
void idImage::UploadScratch( const byte *data, int cols, int rows ) {
int i;
// if rows = cols * 6, assume it is a cube map animation
if ( rows == cols * 6 ) {
if ( type != TT_CUBIC ) {
type = TT_CUBIC;
uploadWidth = -1; // for a non-sub upload
}
Bind();
rows /= 6;
// if the scratchImage isn't in the format we want, specify it as a new texture
if ( cols != uploadWidth || rows != uploadHeight ) {
uploadWidth = cols;
uploadHeight = rows;
// upload the base level
for ( i = 0 ; i < 6 ; i++ ) {
glTexImage2D( GL_TEXTURE_CUBE_MAP_POSITIVE_X_EXT+i, 0, GL_RGB8, cols, rows, 0,
GL_RGBA, GL_UNSIGNED_BYTE, data + cols*rows*4*i );
}
} else {
// otherwise, just subimage upload it so that drivers can tell we are going to be changing
// it and don't try and do a texture compression
for ( i = 0 ; i < 6 ; i++ ) {
glTexSubImage2D( GL_TEXTURE_CUBE_MAP_POSITIVE_X_EXT+i, 0, 0, 0, cols, rows,
GL_RGBA, GL_UNSIGNED_BYTE, data + cols*rows*4*i );
}
}
glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
// no other clamp mode makes sense
glTexParameteri(GL_TEXTURE_CUBE_MAP_EXT, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP_EXT, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
} else {
// otherwise, it is a 2D image
if ( type != TT_2D ) {
type = TT_2D;
uploadWidth = -1; // for a non-sub upload
}
Bind();
// if the scratchImage isn't in the format we want, specify it as a new texture
if ( cols != uploadWidth || rows != uploadHeight ) {
uploadWidth = cols;
uploadHeight = rows;
glTexImage2D( GL_TEXTURE_2D, 0, GL_RGB8, cols, rows, 0, GL_RGBA, GL_UNSIGNED_BYTE, data );
} else {
// otherwise, just subimage upload it so that drivers can tell we are going to be changing
// it and don't try and do a texture compression
glTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, cols, rows, GL_RGBA, GL_UNSIGNED_BYTE, data );
}
glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
// these probably should be clamp, but we have a lot of issues with editor
// geometry coming out with texcoords slightly off one side, resulting in
// a smear across the entire polygon
#if 1
glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT );
glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT );
#else
glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
#endif
}
}
void idImage::SetClassification( int tag ) {
classification = tag;
}
/*
==================
StorageSize
==================
*/
int idImage::StorageSize() const {
int baseSize;
if ( texnum == TEXTURE_NOT_LOADED ) {
return 0;
}
switch ( type ) {
default:
case TT_2D:
baseSize = uploadWidth*uploadHeight;
break;
case TT_3D:
baseSize = uploadWidth*uploadHeight*uploadDepth;
break;
case TT_CUBIC:
baseSize = 6 * uploadWidth*uploadHeight;
break;
}
baseSize *= BitsForInternalFormat( internalFormat );
baseSize /= 8;
// account for mip mapping
baseSize = baseSize * 4 / 3;
return baseSize;
}
/*
==================
Print
==================
*/
void idImage::Print() const {
if ( precompressedFile ) {
common->Printf( "P" );
} else if ( generatorFunction ) {
common->Printf( "F" );
} else {
common->Printf( " " );
}
switch ( type ) {
case TT_2D:
common->Printf( " " );
break;
case TT_3D:
common->Printf( "3" );
break;
case TT_CUBIC:
common->Printf( "C" );
break;
case TT_RECT:
common->Printf( "R" );
break;
default:
common->Printf( "<BAD TYPE:%i>", type );
break;
}
common->Printf( "%4i %4i ", uploadWidth, uploadHeight );
switch( filter ) {
case TF_DEFAULT:
common->Printf( "dflt " );
break;
case TF_LINEAR:
common->Printf( "linr " );
break;
case TF_NEAREST:
common->Printf( "nrst " );
break;
default:
common->Printf( "<BAD FILTER:%i>", filter );
break;
}
switch ( internalFormat ) {
case GL_INTENSITY8:
case 1:
common->Printf( "I " );
break;
case 2:
case GL_LUMINANCE8_ALPHA8:
common->Printf( "LA " );
break;
case 3:
common->Printf( "RGB " );
break;
case 4:
common->Printf( "RGBA " );
break;
case GL_LUMINANCE8:
common->Printf( "L " );
break;
case GL_ALPHA8:
common->Printf( "A " );
break;
case GL_RGBA8:
common->Printf( "RGBA8 " );
break;
case GL_RGB8:
common->Printf( "RGB8 " );
break;
case GL_COMPRESSED_RGB_S3TC_DXT1_EXT:
common->Printf( "DXT1 " );
break;
case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT:
common->Printf( "DXT1A " );
break;
case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT:
common->Printf( "DXT3 " );
break;
case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT:
common->Printf( "DXT5 " );
break;
case GL_COMPRESSED_RGBA_BPTC_UNORM_ARB:
common->Printf( "BC7 " );
break;
case GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM_ARB:
common->Printf( "BC7s " );
break;
case GL_RGBA4:
common->Printf( "RGBA4 " );
break;
case GL_RGB5:
common->Printf( "RGB5 " );
break;
case GL_COLOR_INDEX8_EXT:
common->Printf( "CI8 " );
break;
case GL_COLOR_INDEX:
common->Printf( "CI " );
break;
case GL_COMPRESSED_RGB_ARB:
common->Printf( "RGBC " );
break;
case GL_COMPRESSED_RGBA_ARB:
common->Printf( "RGBAC " );
break;
case 0:
common->Printf( " " );
break;
default:
common->Printf( "<BAD FORMAT:%i>", internalFormat );
break;
}
switch ( repeat ) {
case TR_REPEAT:
common->Printf( "rept " );
break;
case TR_CLAMP_TO_ZERO:
common->Printf( "zero " );
break;
case TR_CLAMP_TO_ZERO_ALPHA:
common->Printf( "azro " );
break;
case TR_CLAMP:
common->Printf( "clmp " );
break;
default:
common->Printf( "<BAD REPEAT:%i>", repeat );
break;
}
common->Printf( "%4ik ", StorageSize() / 1024 );
common->Printf( " %s\n", imgName.c_str() );
}