/*
===========================================================================
Doom 3 BFG Edition GPL Source Code
Copyright (C) 1993-2012 id Software LLC, a ZeniMax Media company.
This file is part of the Doom 3 BFG Edition GPL Source Code ("Doom 3 BFG Edition Source Code").
Doom 3 BFG Edition 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.
Doom 3 BFG Edition 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 Doom 3 BFG Edition Source Code. If not, see .
In addition, the Doom 3 BFG Edition 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 Doom 3 BFG Edition 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 id Software LLC, c/o ZeniMax Media Inc., Suite 120, Rockville, Maryland 20850 USA.
===========================================================================
*/
#pragma hdrstop
#include "precompiled.h"
#include "tr_local.h"
/*
================
BitsForFormat
================
*/
int BitsForFormat( textureFormat_t format )
{
switch( format )
{
case FMT_NONE:
return 0;
case FMT_RGBA8:
return 32;
case FMT_XRGB8:
return 32;
case FMT_RGB565:
return 16;
case FMT_L8A8:
return 16;
case FMT_ALPHA:
return 8;
case FMT_LUM8:
return 8;
case FMT_INT8:
return 8;
case FMT_DXT1:
return 4;
case FMT_DXT5:
return 8;
case FMT_SHADOW_ARRAY:
return ( 32 * 6 );
case FMT_DEPTH:
return 32;
case FMT_X16:
return 16;
case FMT_Y16_X16:
return 32;
default:
assert( 0 );
return 0;
}
}
/*
========================
idImage::DeriveOpts
========================
*/
ID_INLINE void idImage::DeriveOpts()
{
if( opts.format == FMT_NONE )
{
opts.colorFormat = CFM_DEFAULT;
switch( usage )
{
case TD_COVERAGE:
opts.format = FMT_DXT1;
opts.colorFormat = CFM_GREEN_ALPHA;
break;
case TD_DEPTH:
opts.format = FMT_DEPTH;
break;
case TD_SHADOW_ARRAY:
opts.format = FMT_SHADOW_ARRAY;
break;
case TD_DIFFUSE:
// TD_DIFFUSE gets only set to when its a diffuse texture for an interaction
opts.gammaMips = true;
opts.format = FMT_DXT5;
opts.colorFormat = CFM_YCOCG_DXT5;
break;
case TD_SPECULAR:
opts.gammaMips = true;
opts.format = FMT_DXT1;
opts.colorFormat = CFM_DEFAULT;
break;
case TD_DEFAULT:
opts.gammaMips = true;
opts.format = FMT_DXT5;
opts.colorFormat = CFM_DEFAULT;
break;
case TD_BUMP:
opts.format = FMT_DXT5;
opts.colorFormat = CFM_NORMAL_DXT5;
break;
case TD_FONT:
opts.format = FMT_DXT1;
opts.colorFormat = CFM_GREEN_ALPHA;
opts.numLevels = 4; // We only support 4 levels because we align to 16 in the exporter
opts.gammaMips = true;
break;
case TD_LIGHT:
opts.format = FMT_RGB565;
opts.gammaMips = true;
break;
case TD_LOOKUP_TABLE_MONO:
opts.format = FMT_INT8;
break;
case TD_LOOKUP_TABLE_ALPHA:
opts.format = FMT_ALPHA;
break;
case TD_LOOKUP_TABLE_RGB1:
case TD_LOOKUP_TABLE_RGBA:
opts.format = FMT_RGBA8;
break;
default:
assert( false );
opts.format = FMT_RGBA8;
}
}
if( opts.numLevels == 0 )
{
opts.numLevels = 1;
if( filter == TF_LINEAR || filter == TF_NEAREST )
{
// don't create mip maps if we aren't going to be using them
}
else
{
int temp_width = opts.width;
int temp_height = opts.height;
while( temp_width > 1 || temp_height > 1 )
{
temp_width >>= 1;
temp_height >>= 1;
if( ( opts.format == FMT_DXT1 || opts.format == FMT_DXT5 ) &&
( ( temp_width & 0x3 ) != 0 || ( temp_height & 0x3 ) != 0 ) )
{
break;
}
opts.numLevels++;
}
}
}
}
/*
========================
idImage::AllocImage
========================
*/
void idImage::AllocImage( const idImageOpts& imgOpts, textureFilter_t tf, textureRepeat_t tr )
{
filter = tf;
repeat = tr;
opts = imgOpts;
DeriveOpts();
AllocImage();
}
/*
================
GenerateImage
================
*/
void idImage::GenerateImage( const byte* pic, int width, int height, textureFilter_t filterParm, textureRepeat_t repeatParm, textureUsage_t usageParm )
{
PurgeImage();
filter = filterParm;
repeat = repeatParm;
usage = usageParm;
cubeFiles = CF_2D;
opts.textureType = TT_2D;
opts.width = width;
opts.height = height;
opts.numLevels = 0;
DeriveOpts();
// 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 the render starts would miss
// the generated texture
if( !R_IsInitialized() )
{
return;
}
idBinaryImage im( GetName() );
im.Load2DFromMemory( width, height, pic, opts.numLevels, opts.format, opts.colorFormat, opts.gammaMips );
AllocImage();
for( int i = 0; i < im.NumImages(); i++ )
{
const bimageImage_t& img = im.GetImageHeader( i );
const byte* data = im.GetImageData( i );
SubImageUpload( img.level, 0, 0, img.destZ, img.width, img.height, data );
}
}
/*
====================
GenerateCubeImage
Non-square cube sides are not allowed
====================
*/
void idImage::GenerateCubeImage( const byte* pic[6], int size, textureFilter_t filterParm, textureUsage_t usageParm )
{
PurgeImage();
filter = filterParm;
repeat = TR_CLAMP;
usage = usageParm;
cubeFiles = CF_NATIVE;
opts.textureType = TT_CUBIC;
opts.width = size;
opts.height = size;
opts.numLevels = 0;
DeriveOpts();
// 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 the render starts would miss
// the generated texture
if( !R_IsInitialized() )
{
return;
}
idBinaryImage im( GetName() );
im.LoadCubeFromMemory( size, pic, opts.numLevels, opts.format, opts.gammaMips );
AllocImage();
for( int i = 0; i < im.NumImages(); i++ )
{
const bimageImage_t& img = im.GetImageHeader( i );
const byte* data = im.GetImageData( i );
SubImageUpload( img.level, 0, 0, img.destZ, img.width, img.height, data );
}
}
// RB begin
void idImage::GenerateShadowArray( int width, int height, textureFilter_t filterParm, textureRepeat_t repeatParm, textureUsage_t usageParm )
{
PurgeImage();
filter = filterParm;
repeat = repeatParm;
usage = usageParm;
cubeFiles = CF_2D_ARRAY;
opts.textureType = TT_2D_ARRAY;
opts.width = width;
opts.height = height;
opts.numLevels = 0;
DeriveOpts();
// 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 the render starts would miss
// the generated texture
if( !R_IsInitialized() )
{
return;
}
//idBinaryImage im( GetName() );
//im.Load2DFromMemory( width, height, pic, opts.numLevels, opts.format, opts.colorFormat, opts.gammaMips );
AllocImage();
/*
for( int i = 0; i < im.NumImages(); i++ )
{
const bimageImage_t& img = im.GetImageHeader( i );
const byte* data = im.GetImageData( i );
SubImageUpload( img.level, 0, 0, img.destZ, img.width, img.height, data );
}
*/
}
// RB end
/*
===============
GetGeneratedName
name contains GetName() upon entry
===============
*/
void idImage::GetGeneratedName( idStr& _name, const textureUsage_t& _usage, const cubeFiles_t& _cube )
{
idStrStatic< 64 > extension;
_name.ExtractFileExtension( extension );
_name.StripFileExtension();
_name += va( "#__%02d%02d", ( int )_usage, ( int )_cube );
if( extension.Length() > 0 )
{
_name.SetFileExtension( extension );
}
}
/*
===============
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 fromBackEnd )
{
// if we don't have a rendering context yet, just return
if( !R_IsInitialized() )
{
return;
}
// this is the ONLY place generatorFunction will ever be called
if( generatorFunction )
{
generatorFunction( this );
return;
}
if( com_productionMode.GetInteger() != 0 )
{
sourceFileTime = FILE_NOT_FOUND_TIMESTAMP;
if( cubeFiles != CF_2D )
{
opts.textureType = TT_CUBIC;
repeat = TR_CLAMP;
}
}
else
{
// RB begin
if( cubeFiles == CF_2D_ARRAY )
{
opts.textureType = TT_2D_ARRAY;
}
// RB end
else if( cubeFiles != CF_2D )
{
opts.textureType = TT_CUBIC;
repeat = TR_CLAMP;
R_LoadCubeImages( GetName(), cubeFiles, NULL, NULL, &sourceFileTime );
}
else
{
opts.textureType = TT_2D;
R_LoadImageProgram( GetName(), NULL, NULL, NULL, &sourceFileTime, &usage );
}
}
// Figure out opts.colorFormat and opts.format so we can make sure the binary image is up to date
DeriveOpts();
idStrStatic< MAX_OSPATH > generatedName = GetName();
GetGeneratedName( generatedName, usage, cubeFiles );
idBinaryImage im( generatedName );
binaryFileTime = im.LoadFromGeneratedFile( sourceFileTime );
// BFHACK, do not want to tweak on buildgame so catch these images here
if( binaryFileTime == FILE_NOT_FOUND_TIMESTAMP && fileSystem->UsingResourceFiles() )
{
int c = 1;
while( c-- > 0 )
{
if( generatedName.Find( "guis/assets/white#__0000", false ) >= 0 )
{
generatedName.Replace( "white#__0000", "white#__0200" );
im.SetName( generatedName );
binaryFileTime = im.LoadFromGeneratedFile( sourceFileTime );
break;
}
if( generatedName.Find( "guis/assets/white#__0100", false ) >= 0 )
{
generatedName.Replace( "white#__0100", "white#__0200" );
im.SetName( generatedName );
binaryFileTime = im.LoadFromGeneratedFile( sourceFileTime );
break;
}
if( generatedName.Find( "textures/black#__0100", false ) >= 0 )
{
generatedName.Replace( "black#__0100", "black#__0200" );
im.SetName( generatedName );
binaryFileTime = im.LoadFromGeneratedFile( sourceFileTime );
break;
}
if( generatedName.Find( "textures/decals/bulletglass1_d#__0100", false ) >= 0 )
{
generatedName.Replace( "bulletglass1_d#__0100", "bulletglass1_d#__0200" );
im.SetName( generatedName );
binaryFileTime = im.LoadFromGeneratedFile( sourceFileTime );
break;
}
if( generatedName.Find( "models/monsters/skeleton/skeleton01_d#__1000", false ) >= 0 )
{
generatedName.Replace( "skeleton01_d#__1000", "skeleton01_d#__0100" );
im.SetName( generatedName );
binaryFileTime = im.LoadFromGeneratedFile( sourceFileTime );
break;
}
}
}
const bimageFile_t& header = im.GetFileHeader();
if( ( fileSystem->InProductionMode() && binaryFileTime != FILE_NOT_FOUND_TIMESTAMP ) || ( ( binaryFileTime != FILE_NOT_FOUND_TIMESTAMP )
&& ( header.colorFormat == opts.colorFormat )
&& ( header.format == opts.format )
&& ( header.textureType == opts.textureType )
) )
{
opts.width = header.width;
opts.height = header.height;
opts.numLevels = header.numLevels;
opts.colorFormat = ( textureColor_t )header.colorFormat;
opts.format = ( textureFormat_t )header.format;
opts.textureType = ( textureType_t )header.textureType;
if( cvarSystem->GetCVarBool( "fs_buildresources" ) )
{
// for resource gathering write this image to the preload file for this map
fileSystem->AddImagePreload( GetName(), filter, repeat, usage, cubeFiles );
}
}
else
{
if( cubeFiles != CF_2D )
{
int size;
byte* pics[6];
if( !R_LoadCubeImages( GetName(), cubeFiles, pics, &size, &sourceFileTime ) || size == 0 )
{
idLib::Warning( "Couldn't load cube image: %s", GetName() );
return;
}
opts.textureType = TT_CUBIC;
repeat = TR_CLAMP;
opts.width = size;
opts.height = size;
opts.numLevels = 0;
DeriveOpts();
im.LoadCubeFromMemory( size, ( const byte** )pics, opts.numLevels, opts.format, opts.gammaMips );
repeat = TR_CLAMP;
for( int i = 0; i < 6; i++ )
{
if( pics[i] )
{
Mem_Free( pics[i] );
}
}
}
else
{
int width, height;
byte* pic;
// load the full specification, and perform any image program calculations
R_LoadImageProgram( GetName(), &pic, &width, &height, &sourceFileTime, &usage );
if( pic == NULL )
{
idLib::Warning( "Couldn't load image: %s : %s", GetName(), generatedName.c_str() );
// create a default so it doesn't get continuously reloaded
opts.width = 8;
opts.height = 8;
opts.numLevels = 1;
DeriveOpts();
AllocImage();
// clear the data so it's not left uninitialized
idTempArray clear( opts.width * opts.height * 4 );
memset( clear.Ptr(), 0, clear.Size() );
for( int level = 0; level < opts.numLevels; level++ )
{
SubImageUpload( level, 0, 0, 0, opts.width >> level, opts.height >> level, clear.Ptr() );
}
return;
}
opts.width = width;
opts.height = height;
opts.numLevels = 0;
DeriveOpts();
im.Load2DFromMemory( opts.width, opts.height, pic, opts.numLevels, opts.format, opts.colorFormat, opts.gammaMips );
Mem_Free( pic );
}
binaryFileTime = im.WriteGeneratedFile( sourceFileTime );
}
AllocImage();
for( int i = 0; i < im.NumImages(); i++ )
{
const bimageImage_t& img = im.GetImageHeader( i );
const byte* data = im.GetImageData( i );
SubImageUpload( img.level, 0, 0, img.destZ, img.width, img.height, data );
}
}
/*
==============
Bind
Automatically enables 2D mapping or cube mapping if needed
==============
*/
void idImage::Bind()
{
RENDERLOG_PRINTF( "idImage::Bind( %s )\n", GetName() );
// load the image if necessary (FIXME: not SMP safe!)
if( !IsLoaded() )
{
// load the image on demand here, which isn't our normal game operating mode
ActuallyLoadImage( true );
}
const int texUnit = backEnd.glState.currenttmu;
tmu_t* tmu = &backEnd.glState.tmu[texUnit];
// bind the texture
if( opts.textureType == TT_2D )
{
if( tmu->current2DMap != texnum )
{
tmu->current2DMap = texnum;
// RB begin
if( glConfig.directStateAccess )
{
glBindMultiTextureEXT( GL_TEXTURE0 + texUnit, GL_TEXTURE_2D, texnum );
}
else
{
glActiveTexture( GL_TEXTURE0 + texUnit );
glBindTexture( GL_TEXTURE_2D, texnum );
}
// RB end
}
}
else if( opts.textureType == TT_CUBIC )
{
if( tmu->currentCubeMap != texnum )
{
tmu->currentCubeMap = texnum;
// RB begin
#if !defined(USE_GLES2) && !defined(USE_GLES3)
if( glConfig.directStateAccess )
{
glBindMultiTextureEXT( GL_TEXTURE0 + texUnit, GL_TEXTURE_CUBE_MAP, texnum );
}
else
#endif
{
glActiveTexture( GL_TEXTURE0 + texUnit );
glBindTexture( GL_TEXTURE_CUBE_MAP, texnum );
}
// RB end
}
}
else if( opts.textureType == TT_2D_ARRAY )
{
if( tmu->current2DArray != texnum )
{
tmu->current2DArray = texnum;
// RB begin
#if !defined(USE_GLES2) && !defined(USE_GLES3)
if( glConfig.directStateAccess )
{
glBindMultiTextureEXT( GL_TEXTURE0 + texUnit, GL_TEXTURE_2D_ARRAY, texnum );
}
else
#endif
{
glActiveTexture( GL_TEXTURE0 + texUnit );
glBindTexture( GL_TEXTURE_2D_ARRAY, texnum );
}
// RB end
}
}
}
/*
================
MakePowerOfTwo
================
*/
int MakePowerOfTwo( int num )
{
int pot;
for( pot = 1; pot < num; pot <<= 1 )
{
}
return pot;
}
/*
====================
CopyFramebuffer
====================
*/
void idImage::CopyFramebuffer( int x, int y, int imageWidth, int imageHeight )
{
glBindTexture( ( opts.textureType == TT_CUBIC ) ? GL_TEXTURE_CUBE_MAP : GL_TEXTURE_2D, texnum );
glReadBuffer( GL_BACK );
opts.width = imageWidth;
opts.height = imageHeight;
glCopyTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA8, x, y, imageWidth, imageHeight, 0 );
// these shouldn't be necessary if the image was initialized properly
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.pc.c_copyFrameBuffer++;
}
/*
====================
CopyDepthbuffer
====================
*/
void idImage::CopyDepthbuffer( int x, int y, int imageWidth, int imageHeight )
{
glBindTexture( ( opts.textureType == TT_CUBIC ) ? GL_TEXTURE_CUBE_MAP : GL_TEXTURE_2D, texnum );
opts.width = imageWidth;
opts.height = imageHeight;
glCopyTexImage2D( GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, x, y, imageWidth, imageHeight, 0 );
backEnd.pc.c_copyFrameBuffer++;
}
/*
=============
RB_UploadScratchImage
if rows = cols * 6, assume it is a cube map animation
=============
*/
void idImage::UploadScratch( const byte* data, int cols, int rows )
{
// if rows = cols * 6, assume it is a cube map animation
if( rows == cols * 6 )
{
rows /= 6;
const byte* pic[6];
for( int i = 0; i < 6; i++ )
{
pic[i] = data + cols * rows * 4 * i;
}
if( opts.textureType != TT_CUBIC || usage != TD_LOOKUP_TABLE_RGBA )
{
GenerateCubeImage( pic, cols, TF_LINEAR, TD_LOOKUP_TABLE_RGBA );
return;
}
if( opts.width != cols || opts.height != rows )
{
opts.width = cols;
opts.height = rows;
AllocImage();
}
SetSamplerState( TF_LINEAR, TR_CLAMP );
for( int i = 0; i < 6; i++ )
{
SubImageUpload( 0, 0, 0, i, opts.width, opts.height, pic[i] );
}
}
else
{
if( opts.textureType != TT_2D || usage != TD_LOOKUP_TABLE_RGBA )
{
GenerateImage( data, cols, rows, TF_LINEAR, TR_REPEAT, TD_LOOKUP_TABLE_RGBA );
return;
}
if( opts.width != cols || opts.height != rows )
{
opts.width = cols;
opts.height = rows;
AllocImage();
}
SetSamplerState( TF_LINEAR, TR_REPEAT );
SubImageUpload( 0, 0, 0, 0, opts.width, opts.height, data );
}
}
/*
==================
StorageSize
==================
*/
int idImage::StorageSize() const
{
if( !IsLoaded() )
{
return 0;
}
int baseSize = opts.width * opts.height;
if( opts.numLevels > 1 )
{
baseSize *= 4;
baseSize /= 3;
}
baseSize *= BitsForFormat( opts.format );
baseSize /= 8;
return baseSize;
}
/*
==================
Print
==================
*/
void idImage::Print() const
{
if( generatorFunction )
{
common->Printf( "F" );
}
else
{
common->Printf( " " );
}
switch( opts.textureType )
{
case TT_2D:
common->Printf( " " );
break;
case TT_CUBIC:
common->Printf( "C" );
break;
default:
common->Printf( "", opts.textureType );
break;
}
common->Printf( "%4i %4i ", opts.width, opts.height );
switch( opts.format )
{
#define NAME_FORMAT( x ) case FMT_##x: common->Printf( "%-6s ", #x ); break;
NAME_FORMAT( NONE );
NAME_FORMAT( RGBA8 );
NAME_FORMAT( XRGB8 );
NAME_FORMAT( RGB565 );
NAME_FORMAT( L8A8 );
NAME_FORMAT( ALPHA );
NAME_FORMAT( LUM8 );
NAME_FORMAT( INT8 );
NAME_FORMAT( DXT1 );
NAME_FORMAT( DXT5 );
NAME_FORMAT( DEPTH );
NAME_FORMAT( X16 );
NAME_FORMAT( Y16_X16 );
default:
common->Printf( "<%3i>", opts.format );
break;
}
switch( filter )
{
case TF_DEFAULT:
common->Printf( "mip " );
break;
case TF_LINEAR:
common->Printf( "linr " );
break;
case TF_NEAREST:
common->Printf( "nrst " );
break;
default:
common->Printf( "", filter );
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( "", repeat );
break;
}
common->Printf( "%4ik ", StorageSize() / 1024 );
common->Printf( " %s\n", GetName() );
}
/*
===============
idImage::Reload
===============
*/
void idImage::Reload( bool force )
{
// always regenerate functional images
if( generatorFunction )
{
common->DPrintf( "regenerating %s.\n", GetName() );
generatorFunction( this );
return;
}
// check file times
if( !force )
{
ID_TIME_T current;
if( cubeFiles != CF_2D )
{
R_LoadCubeImages( imgName, cubeFiles, NULL, NULL, ¤t );
}
else
{
// get the current values
R_LoadImageProgram( imgName, NULL, NULL, NULL, ¤t );
}
if( current <= sourceFileTime )
{
return;
}
}
common->DPrintf( "reloading %s.\n", GetName() );
PurgeImage();
// Load is from the front end, so the back end must be synced
ActuallyLoadImage( false );
}
/*
========================
idImage::SetSamplerState
========================
*/
void idImage::SetSamplerState( textureFilter_t tf, textureRepeat_t tr )
{
if( tf == filter && tr == repeat )
{
return;
}
filter = tf;
repeat = tr;
glBindTexture( ( opts.textureType == TT_CUBIC ) ? GL_TEXTURE_CUBE_MAP : GL_TEXTURE_2D, texnum );
SetTexParameters();
}