/*
===========================================================================

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 <http://www.gnu.org/licenses/>.

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.

===========================================================================
*/



#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_RGBA16F:	return 64;
		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_BC7:		return 8;
		case FMT_DEPTH:		return 32;
		case FMT_DEPTH_STENCIL:	return 32;
		case FMT_X16:		return 16;
		case FMT_Y16_X16:	return 32;
		default:
			assert( 0 );
			return 0;
	}
}

/*
========================
R_ShouldSuppressMissingImageWarning

Suppress warning noise for known optional/missing stock references that
already fall back to a safe transparent placeholder.
========================
*/
static bool R_ShouldSuppressMissingImageWarning( const char * imageName ) {
	if ( imageName == NULL || imageName[0] == '\0' ) {
		return true;
	}
	if ( idStr::Icmp( imageName, "_emptyname" ) == 0 ) {
		return true;
	}
	if ( idStr::Icmp( imageName, "textures/common_misc/flickerflare" ) == 0 ) {
		return true;
	}
	if ( idStr::FindText( imageName, "_lightgrid_" ) >= 0 ) {
		return true;
	}
	return false;
}

static bool R_GetImageDownsizeLimit( textureUsage_t usage, bool allowDownSize, int &limit );
static void R_ApplyImageDownsizePolicy( textureUsage_t usage, bool allowDownSize, int &width, int &height );
static unsigned int R_GetImageDownsizeSignature( textureUsage_t usage, bool allowDownSize );
static void R_DownsizeLoadedImageData( textureUsage_t usage, bool allowDownSize, byte *&pic, int &width, int &height );
static void R_DownsizeLoadedCubeImageData( textureUsage_t usage, bool allowDownSize, byte *pics[6], int &size );

/*
========================
idImage::DeriveOpts
========================
*/
ID_INLINE void idImage::DeriveOpts() {

	if ( usage == TD_FONT ) {
		opts.format = FMT_DXT1;
		opts.colorFormat = CFM_GREEN_ALPHA;
		opts.numLevels = 4; // Retail Quake 4's generated font-atlas path keeps four mip levels.
		opts.gammaMips = true;
		return;
	}

	if ( opts.format == FMT_NONE ) {
		opts.colorFormat = CFM_DEFAULT;
// jmarshall - no need to compress
		switch (usage) {
		case TD_DEPTH:
			opts.format = FMT_DEPTH;
			break;
		case TD_LIGHT:
			opts.format = FMT_RGB565;
			opts.gammaMips = true;
			break;
		case TD_LIGHTGRID:
			opts.gammaMips = false;
			opts.colorFormat = CFM_DEFAULT;
			opts.format = glConfig.textureCompressionAvailable ? FMT_DXT1 : FMT_RGB565;
			break;
		case TD_LIGHTGRID_VISIBILITY:
			opts.gammaMips = false;
			opts.colorFormat = CFM_DEFAULT;
			opts.format = FMT_RGBA8;
			break;
		case TD_LIGHTGRID_PROBE:
			opts.gammaMips = false;
			opts.colorFormat = CFM_DEFAULT;
			opts.format = FMT_RGBA8;
			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;
		case TD_HIGH_QUALITY:
			// Preserve Quake 4's distinct "uncompressed/highquality" image bucket,
			// but keep it on openQ4's modern uncompressed RGBA8 path rather than
			// reviving older compressed-driver behavior.
			opts.gammaMips = false;
			opts.colorFormat = CFM_DEFAULT;
			opts.format = FMT_RGBA8;
			break;
		default:
				opts.gammaMips = false;
				opts.format = FMT_RGBA8;
				opts.colorFormat = CFM_DEFAULT;
				break;
		}
		
/*
		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_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_DEFAULT;
				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 ( ( flags & IMAGEFLAG_NOMIPS ) != 0 ) {
			// Keep Quake 4's "nomips" semantic as a single-level texture, but
			// let the sampler policy decide the best modern filtering for that level.
		} else 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 || opts.format == FMT_BC7 ) &&
					( ( temp_width & 0x3 ) != 0 || ( temp_height & 0x3 ) != 0 ) ) {
						break;
				}
				opts.numLevels++;
			}
		}
	}
}

static ID_INLINE bool R_GeneratedImageHeaderMatchesDerivedOpts( const bimageFile_t &header, const idImageOpts &derivedOpts, textureUsage_t usage ) {
	if ( header.colorFormat != derivedOpts.colorFormat ||
		header.format != derivedOpts.format ||
		header.textureType != derivedOpts.textureType ) {
		return false;
	}

	if ( usage == TD_FONT && header.numLevels != derivedOpts.numLevels ) {
		return false;
	}

	return true;
}

static ID_INLINE bool R_BinaryImageHeaderSupportedByRenderer( const bimageFile_t &header ) {
	const textureFormat_t format = (textureFormat_t)header.format;
	if ( ( format == FMT_DXT1 || format == FMT_DXT5 ) && !glConfig.textureCompressionAvailable ) {
		return false;
	}
	if ( format == FMT_BC7 && !glConfig.bptcTextureCompressionAvailable ) {
		return false;
	}
	return true;
}

/*
========================
idImage::AllocImage
========================
*/
void idImage::AllocImage( const idImageOpts &imgOpts, textureFilter_t tf, textureRepeat_t tr ) {
	filter = tf;
	repeat = tr;
	opts = imgOpts;
	defaulted = false;
	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;
	defaulted = false;

	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 ( !tr.IsOpenGLRunning() ) {
		return;
	}

	idBinaryImage im( GetName() );
	im.Load2DFromMemory( width, height, pic, opts.numLevels, opts.format, opts.colorFormat, opts.gammaMips, ( flags & IMAGEFLAG_FILTER_NEUTRAL_ALPHA ) != 0 );

	AllocImage();

	const int imageCount = im.NumImages();
	for ( int i = 0; i < imageCount; 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;
	defaulted = false;

	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 ( !tr.IsOpenGLRunning() ) {
		return;
	}

	idBinaryImage im( GetName() );
	im.LoadCubeFromMemory( size, pic, opts.numLevels, opts.format, opts.gammaMips );

	AllocImage();

	const int imageCount = im.NumImages();
	for ( int i = 0; i < imageCount; 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 );
	}
}

/*
===============
GetGeneratedName

name contains GetName() upon entry
===============
*/
 void idImage::GetGeneratedName( idStr &_name, const textureUsage_t &_usage, const cubeFiles_t &_cube, bool allowDownSize, unsigned int flags ) {
	idStr extension;

	_name.ExtractFileExtension( extension );
	_name.StripFileExtension();

	_name += va( "#__%02d%02d", (int)_usage, (int)_cube );
	const unsigned int downsizeSignature = R_GetImageDownsizeSignature( _usage, allowDownSize );
	if ( downsizeSignature != 0 ) {
		_name += va( "d%08x", downsizeSignature );
	}
	if ( flags != 0 ) {
		_name += va( "f%08x", flags );
	}
	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 ( !tr.IsOpenGLRunning() ) {
		return;
	}

	// this is the ONLY place generatorFunction will ever be called
	if ( generatorFunction ) {
		generatorFunction( this );
		return;
	}

	defaulted = false;
	// File-backed options may have been replaced by a directly uploaded DDS on
	// the previous load. Re-derive them from the image's declared usage so a
	// cvar/path change can safely move between BC/DXT and ordinary RGBA data.
	opts.format = FMT_NONE;
	opts.colorFormat = CFM_DEFAULT;
	opts.width = 0;
	opts.height = 0;
	opts.numLevels = 0;
	opts.gammaMips = false;

	bool sourceFileTimeKnown = false;
	if ( com_productionMode.GetInteger() != 0 ) {
		sourceFileTime = FILE_NOT_FOUND_TIMESTAMP;
		sourceFileTimeKnown = true;
		if ( cubeFiles != CF_2D ) {
			opts.textureType = TT_CUBIC;
			repeat = TR_CLAMP;
		}
	} else {
		if ( cubeFiles != CF_2D ) {
			opts.textureType = TT_CUBIC;
			repeat = TR_CLAMP;
		} else {
			opts.textureType = TT_2D;
		}
	}

	// Figure out opts.colorFormat and opts.format so we can make sure the binary image is up to date
	DeriveOpts();

	idStr generatedName = GetName();
	GetGeneratedName( generatedName, usage, cubeFiles, allowDownSize, flags );
	if ( filter == TF_LINEAR || filter == TF_NEAREST ) {
		// the unmipped sampler policy changes the generated mip count ( DeriveOpts ), so
		// keep its cache file distinct from the mipped variant of the same source
		idStr mipExt;
		generatedName.ExtractFileExtension( mipExt );
		generatedName.StripFileExtension();
		generatedName += "m0";
		if ( mipExt.Length() > 0 ) {
			generatedName.SetFileExtension( mipExt );
		}
	}
	idStr sourceExtension;
	idStr sourceName = GetName();
	sourceName.ExtractFileExtension( sourceExtension );
	const bool explicitDDSImage = idStr::Icmp( sourceExtension.c_str(), "dds" ) == 0;
	idStr preferredDDSName;
	ID_TIME_T preferredDDSFileTime = FILE_NOT_FOUND_TIMESTAMP;
	bool preferredDDSPrecompressed = false;
	bool preferredDDSImage = !explicitDDSImage &&
		cubeFiles == CF_2D &&
		R_ResolvePreferredDDSImageSource( GetName(), preferredDDSName, &preferredDDSFileTime, true, &preferredDDSPrecompressed );
	if ( preferredDDSImage && !fileSystem->InProductionMode() ) {
		ID_TIME_T originalSourceTime = FILE_NOT_FOUND_TIMESTAMP;
		R_LoadImageProgram( GetName(), NULL, NULL, NULL, &originalSourceTime, &usage );
		if ( originalSourceTime != FILE_NOT_FOUND_TIMESTAMP && preferredDDSFileTime < originalSourceTime ) {
			if ( cvarSystem->GetCVarBool( "image_showPrecompressedTextures" ) ) {
				common->Printf( "Ignoring stale DDS replacement %s for %s\n", preferredDDSName.c_str(), GetName() );
			}
			preferredDDSImage = false;
			preferredDDSPrecompressed = false;
			sourceFileTime = originalSourceTime;
			sourceFileTimeKnown = true;
		} else {
			sourceFileTime = preferredDDSFileTime;
			sourceFileTimeKnown = true;
		}
	}
	if ( preferredDDSImage && cvarSystem->GetCVarBool( "image_showPrecompressedTextures" ) ) {
		common->Printf( "Using DDS replacement %s for %s\n", preferredDDSName.c_str(), GetName() );
	}
	const char *loadSourceName = preferredDDSImage ? preferredDDSName.c_str() : GetName();
	const bool selectedDDSImage = explicitDDSImage || preferredDDSImage;
	const bool bypassGeneratedFile = explicitDDSImage || preferredDDSPrecompressed;
	idStr selectedSourceName = GetName();
	if ( preferredDDSImage ) {
		selectedSourceName = preferredDDSName;
	}
	if ( preferredDDSImage && !preferredDDSPrecompressed ) {
		generatedName = preferredDDSName;
		GetGeneratedName( generatedName, usage, cubeFiles, allowDownSize, flags );
		if ( filter == TF_LINEAR || filter == TF_NEAREST ) {
			idStr mipExt;
			generatedName.ExtractFileExtension( mipExt );
			generatedName.StripFileExtension();
			generatedName += "m0";
			if ( mipExt.Length() > 0 ) {
				generatedName.SetFileExtension( mipExt );
			}
		}
	}

	idBinaryImage im( generatedName );
	if ( bypassGeneratedFile ) {
		binaryFileTime = FILE_NOT_FOUND_TIMESTAMP;
	} else {
		binaryFileTime = im.LoadFromGeneratedFileUnchecked();
	}

	// BFHACK, do not want to tweak on buildgame so catch these images here
	if ( !bypassGeneratedFile && binaryFileTime == FILE_NOT_FOUND_TIMESTAMP ) {
		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.LoadFromGeneratedFileUnchecked();
				break;
			}
			if ( generatedName.Find( "guis/assets/white#__0100", false ) >= 0 ) {
				generatedName.Replace( "white#__0100", "white#__0200" );
				im.SetName( generatedName );
				binaryFileTime = im.LoadFromGeneratedFileUnchecked();
				break;
			}
			if ( generatedName.Find( "textures/black#__0100", false ) >= 0 ) {
				generatedName.Replace( "black#__0100", "black#__0200" );
				im.SetName( generatedName );
				binaryFileTime = im.LoadFromGeneratedFileUnchecked();
				break;
			}
			if ( generatedName.Find( "textures/decals/bulletglass1_d#__0100", false ) >= 0 ) {
				generatedName.Replace( "bulletglass1_d#__0100", "bulletglass1_d#__0200" );
				im.SetName( generatedName );
				binaryFileTime = im.LoadFromGeneratedFileUnchecked();
				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.LoadFromGeneratedFileUnchecked();
				break;
			}
		}
	}
	if ( binaryFileTime != FILE_NOT_FOUND_TIMESTAMP && !fileSystem->InProductionMode() ) {
		if ( !sourceFileTimeKnown ) {
			if ( cubeFiles != CF_2D ) {
				R_LoadCubeImages( GetName(), cubeFiles, NULL, NULL, &sourceFileTime );
			} else if ( preferredDDSImage ) {
				sourceFileTime = preferredDDSFileTime;
			} else {
				R_LoadImageProgram( GetName(), NULL, NULL, NULL, &sourceFileTime, &usage );
			}
			sourceFileTimeKnown = true;
		}
		if ( im.GetFileHeader().sourceFileTime != sourceFileTime ) {
			im.Clear();
			binaryFileTime = FILE_NOT_FOUND_TIMESTAMP;
		}
	}

	const bool binaryImageAvailable = binaryFileTime != FILE_NOT_FOUND_TIMESTAMP && R_BinaryImageHeaderSupportedByRenderer( im.GetFileHeader() );
	if ( ( fileSystem->InProductionMode() && binaryImageAvailable ) || ( binaryImageAvailable
		&& R_GeneratedImageHeaderMatchesDerivedOpts( im.GetFileHeader(), opts, usage )
		) ) {
		const bimageFile_t & header = im.GetFileHeader();
		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 {
		im.Clear();
		bool loadedPrecompressedDDS = false;
		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() );
				// 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<byte> clear( opts.width * opts.height * 4 );
				memset( clear.Ptr(), 0, clear.Size() );
				for ( int level = 0; level < opts.numLevels; level++ ) {
					for ( int side = 0; side < 6; side++ ) {
						SubImageUpload( level, 0, 0, side, opts.width >> level, opts.height >> level, clear.Ptr() );
					}
				}

				defaulted = true;
				return;
			}

			R_DownsizeLoadedCubeImageData( usage, allowDownSize, pics, size );
			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 = NULL;
			int precompressedDownsizeLimit = 0;
			R_GetImageDownsizeLimit( usage, allowDownSize, precompressedDownsizeLimit );
			const bool usePrecompressedMipmaps = ( flags & IMAGEFLAG_NOMIPS ) == 0 && filter != TF_LINEAR && filter != TF_NEAREST;
			const bool tryDirectDDSLoad = selectedDDSImage && ( explicitDDSImage || preferredDDSPrecompressed );

			if ( tryDirectDDSLoad && R_LoadPrecompressedDDS( loadSourceName, im, &sourceFileTime, usage, precompressedDownsizeLimit, usePrecompressedMipmaps ) ) {
				const bimageFile_t &header = im.GetFileHeader();
				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;
				sourceFileTimeKnown = true;
				loadedPrecompressedDDS = true;
			} else {
				const char *fallbackLoadSourceName = loadSourceName;
				if ( preferredDDSPrecompressed ) {
					common->Warning( "Couldn't load preferred precompressed DDS replacement %s for %s; falling back to original source", loadSourceName, GetName() );
					fallbackLoadSourceName = GetName();
					selectedSourceName = GetName();
					sourceFileTime = FILE_NOT_FOUND_TIMESTAMP;
				}

				// load the full specification, and perform any image program calculations
				R_LoadImageProgram( fallbackLoadSourceName, &pic, &width, &height, &sourceFileTime, &usage );
				if ( pic == NULL && preferredDDSImage && !preferredDDSPrecompressed ) {
					common->Warning( "Couldn't decode preferred DDS replacement %s for %s; falling back to original source", loadSourceName, GetName() );
					selectedSourceName = GetName();
					sourceFileTime = FILE_NOT_FOUND_TIMESTAMP;
					R_LoadImageProgram( GetName(), &pic, &width, &height, &sourceFileTime, &usage );
				}
				sourceFileTimeKnown = true;

				if ( pic == NULL ) {
					if ( !R_ShouldSuppressMissingImageWarning( GetName() ) ) {
						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<byte> 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() );
					}

					defaulted = true;
					return;
				}

				R_DownsizeLoadedImageData( usage, allowDownSize, pic, width, height );
				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, ( flags & IMAGEFLAG_FILTER_NEUTRAL_ALPHA ) != 0 );

				Mem_Free( pic );
			}
		}
		if ( !loadedPrecompressedDDS ) {
			binaryFileTime = im.WriteGeneratedFile( sourceFileTime );
		}
	}

	AllocImage();


	const int imageCount = im.NumImages();
	for ( int i = 0; i < imageCount; 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 );
	}
	loadedSourceName = selectedSourceName;
}

/*
==============
Bind

Automatically enables 2D mapping or cube mapping if needed
==============
*/
static bool R_IsValidTrackedTextureUnit( int texUnit ) {
	if ( texUnit >= 0 && texUnit < MAX_MULTITEXTURE_UNITS &&
		texUnit < glConfig.maxTextureUnits && texUnit < glConfig.maxTextureImageUnits ) {
		return true;
	}

	common->Warning(
		"idImage::Bind: tracked texture unit %d is outside limits (tracked=%d, textureUnits=%d, imageUnits=%d)",
		texUnit,
		MAX_MULTITEXTURE_UNITS,
		glConfig.maxTextureUnits,
		glConfig.maxTextureImageUnits );
	return false;
}

static bool R_BindTextureToUnit( int texUnit, GLenum target, GLuint texture ) {
	// Core/ARB DSA derives the target from an existing texture object. Binding
	// zero has broader unbind semantics, so retain the target-specific paths for
	// that case.
	if ( texture != 0 && glConfig.backendCaps.hasDSA && glBindTextureUnit != NULL ) {
		glBindTextureUnit( static_cast<GLuint>( texUnit ), texture );
		return true;
	}

	if ( GLEW_EXT_direct_state_access && glBindMultiTextureEXT != NULL ) {
		glBindMultiTextureEXT( GL_TEXTURE0_ARB + texUnit, target, texture );
		return true;
	}

	// The compatibility renderer requires ARB multitexture, but keep this
	// guard so an incomplete loader cannot turn an image bind into a null call.
	if ( glActiveTextureARB == NULL ) {
		common->Warning( "idImage::Bind: no texture-unit binding entry point is available" );
		return false;
	}

	glActiveTextureARB( GL_TEXTURE0_ARB + texUnit );
	glBindTexture( target, texture );
	return true;
}

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;
	if ( !R_IsValidTrackedTextureUnit( texUnit ) ) {
		return;
	}

	tmu_t* tmu = &backEnd.glState.tmu[texUnit];

	// enable or disable apropriate texture modes
	if (tmu->textureType != opts.textureType && (backEnd.glState.currenttmu < glConfig.maxTextureUnits)) {
		if (tmu->textureType == TT_CUBIC) {
			glDisable(GL_TEXTURE_CUBE_MAP_EXT);
		}
		else if (tmu->textureType == TT_2D) {
			glDisable(GL_TEXTURE_2D);
		}

		if (opts.textureType == TT_CUBIC) {
			glEnable(GL_TEXTURE_CUBE_MAP_EXT);
		}
		else if (opts.textureType == TT_2D) {
			glEnable(GL_TEXTURE_2D);
		}
		tmu->textureType = opts.textureType;
	}

	// bind the texture
	if (opts.textureType == TT_2D) {
		if (tmu->current2DMap != texnum) {
			if ( R_BindTextureToUnit( texUnit, GL_TEXTURE_2D, texnum ) ) {
				tmu->current2DMap = texnum;
			}
		}
	}
	else if (opts.textureType == TT_CUBIC) {
		if (tmu->currentCubeMap != texnum) {
			if ( R_BindTextureToUnit( texUnit, GL_TEXTURE_CUBE_MAP_EXT, texnum ) ) {
				tmu->currentCubeMap = texnum;
			}
		}
	}

}

/*
================
MakePowerOfTwo
================
*/
int MakePowerOfTwo( int num ) {
	int	pot;
	for ( pot = 1; pot < num; pot <<= 1 ) {
	}
	return pot;
}

static bool R_GetImageDownsizeLimit( textureUsage_t usage, bool allowDownSize, int &limit ) {
	limit = 0;

	if ( !allowDownSize ) {
		return false;
	}

	if ( usage == TD_SPECULAR && cvarSystem->GetCVarInteger( "image_downSizeSpecular" ) != 0 ) {
		limit = cvarSystem->GetCVarInteger( "image_downSizeSpecularLimit" );
	} else if ( usage == TD_BUMP && cvarSystem->GetCVarInteger( "image_downSizeBump" ) != 0 ) {
		limit = cvarSystem->GetCVarInteger( "image_downSizeBumpLimit" );
	} else if ( cvarSystem->GetCVarInteger( "image_downSize" ) != 0 ) {
		limit = cvarSystem->GetCVarInteger( "image_downSizeLimit" );
	}

	return limit > 0;
}

static void R_ApplyImageDownsizePolicy( textureUsage_t usage, bool allowDownSize, int &width, int &height ) {
	int limit = 0;
	if ( !R_GetImageDownsizeLimit( usage, allowDownSize, limit ) ) {
		return;
	}

	while ( width > limit || height > limit ) {
		const int nextWidth = Max( 1, width >> 1 );
		const int nextHeight = Max( 1, height >> 1 );
		if ( nextWidth == width && nextHeight == height ) {
			break;
		}
		width = nextWidth;
		height = nextHeight;
	}
}

static unsigned int R_GetImageDownsizeSignature( textureUsage_t usage, bool allowDownSize ) {
	int limit = 0;
	if ( !R_GetImageDownsizeLimit( usage, allowDownSize, limit ) ) {
		return 0;
	}

	return ( static_cast<unsigned int>( limit ) << 8 ) ^ static_cast<unsigned int>( usage ) ^ 0x6F713400u;
}

static void R_DownsizeLoadedImageData( textureUsage_t usage, bool allowDownSize, byte *&pic, int &width, int &height ) {
	if ( pic == NULL || width <= 0 || height <= 0 ) {
		return;
	}

	int scaledWidth = width;
	int scaledHeight = height;
	R_ApplyImageDownsizePolicy( usage, allowDownSize, scaledWidth, scaledHeight );
	if ( scaledWidth == width && scaledHeight == height ) {
		return;
	}

	byte *resampled = R_ResampleTexture( pic, width, height, scaledWidth, scaledHeight );
	if ( resampled == NULL ) {
		return;
	}

	Mem_Free( pic );
	pic = resampled;
	width = scaledWidth;
	height = scaledHeight;
}

static void R_DownsizeLoadedCubeImageData( textureUsage_t usage, bool allowDownSize, byte *pics[6], int &size ) {
	if ( pics == NULL || size <= 0 ) {
		return;
	}

	int scaledSize = size;
	int scaledHeight = size;
	R_ApplyImageDownsizePolicy( usage, allowDownSize, scaledSize, scaledHeight );
	if ( scaledSize == size && scaledHeight == size ) {
		return;
	}

	for ( int i = 0; i < 6; i++ ) {
		if ( pics[i] == NULL ) {
			continue;
		}

		byte *resampled = R_ResampleTexture( pics[i], size, size, scaledSize, scaledSize );
		if ( resampled == NULL ) {
			continue;
		}

		Mem_Free( pics[i] );
		pics[i] = resampled;
	}

	size = scaledSize;
}

/*
====================
R_BindTextureForDirectAccess

Raw glBindTexture for framebuffer copies, uploads, and sampler-state updates.
The bind replaces the active unit's binding BEHIND idImage::Bind()'s
redundant-bind filter, so the per-TMU tracker must be updated alongside or a
later Bind() of the previously tracked texture silently early-outs while
something else is actually bound. A stale tracker entry here is how the SSAO
pass ended up sampling its depth scratch image as the scene texture
(near-dark / far-white frames): a _currentRender material left
tracking[0] == scene, the depth copy raw-bound the depth texture over it,
and the scene Bind() was filtered out.
====================
*/
void R_BindTextureForDirectAccess( unsigned int target, int texnum ) {
	glBindTexture( target, texnum );
	if ( target == GL_TEXTURE_2D ) {
		backEnd.glState.tmu[backEnd.glState.currenttmu].current2DMap = texnum;
	} else if ( target == GL_TEXTURE_CUBE_MAP_EXT ) {
		backEnd.glState.tmu[backEnd.glState.currenttmu].currentCubeMap = texnum;
	}
	// other targets (e.g. multisample) are not tracked by the legacy filter
	// and do not disturb the tracked 2D / cube bindings
}

// Lazily created scratch FBOs for the CopyFramebuffer/CopyDepthbuffer blit
// paths. They are GL context objects: the names must be forgotten (and deleted
// while the old context is still current) before the context is destroyed, or
// the surviving nonzero names alias the new context's render-target FBOs and
// the next copy blit detaches a live render target's attachment.
static GLuint r_copyFramebufferFbo = 0;
static GLuint r_copyDepthbufferFbo = 0;

/*
====================
R_PurgeFramebufferCopyFBOs

Called before GLimp_Shutdown (full vid_restart and final shutdown) while the
old context is still current.
====================
*/
void R_PurgeFramebufferCopyFBOs( void ) {
	if ( r_copyFramebufferFbo != 0 ) {
		glDeleteFramebuffers( 1, &r_copyFramebufferFbo );
		r_copyFramebufferFbo = 0;
	}
	if ( r_copyDepthbufferFbo != 0 ) {
		glDeleteFramebuffers( 1, &r_copyDepthbufferFbo );
		r_copyDepthbufferFbo = 0;
	}
}

/*
====================
CopyFramebuffer
====================
*/
void idImage::CopyFramebuffer( int x, int y, int imageWidth, int imageHeight ) {
	R_BindTextureForDirectAccess( ( opts.textureType == TT_CUBIC ) ? GL_TEXTURE_CUBE_MAP_EXT : GL_TEXTURE_2D, texnum );

	const bool readingFromRenderTexture = ( backEnd.renderTexture != NULL ) && ( backEnd.renderTexture->GetNumColorImages() > 0 );
	const GLenum readAttachment = GL_COLOR_ATTACHMENT0;
	const bool needsStorageResize = ( opts.width != imageWidth ) || ( opts.height != imageHeight );

	opts.width = imageWidth;
	opts.height = imageHeight;

	if ( readingFromRenderTexture && ( GLEW_EXT_framebuffer_blit || GLEW_ARB_framebuffer_object || GLEW_VERSION_3_0 ) ) {
		GLint previousReadFbo = 0;
		GLint previousDrawFbo = 0;
		GLint previousReadBuffer = GL_BACK;
		GLint previousDrawBuffer = GL_BACK;

		glGetIntegerv( GL_READ_FRAMEBUFFER_BINDING, &previousReadFbo );
		glGetIntegerv( GL_DRAW_FRAMEBUFFER_BINDING, &previousDrawFbo );
		glGetIntegerv( GL_READ_BUFFER, &previousReadBuffer );
		glGetIntegerv( GL_DRAW_BUFFER, &previousDrawBuffer );

		if ( r_copyFramebufferFbo == 0 ) {
			glGenFramebuffers( 1, &r_copyFramebufferFbo );
		}
		const GLuint copyFbo = r_copyFramebufferFbo;

		if ( needsStorageResize ) {
			glTexImage2D( GL_TEXTURE_2D, 0, internalFormat != 0 ? internalFormat : GL_RGBA8, imageWidth, imageHeight, 0,
				dataFormat != 0 ? dataFormat : GL_RGBA, dataType != 0 ? dataType : GL_UNSIGNED_BYTE, NULL );
			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 );
		}

		glBindFramebuffer( GL_READ_FRAMEBUFFER, backEnd.renderTexture->GetDeviceHandle() );
		glReadBuffer( readAttachment );

		glBindFramebuffer( GL_DRAW_FRAMEBUFFER, copyFbo );
		glFramebufferTexture2D( GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texnum, 0 );
		glDrawBuffer( GL_COLOR_ATTACHMENT0 );

		// glBlitFramebuffer obeys scissor state; ensure the copy is not clipped by a prior light scissor.
		const GLboolean scissorWasEnabled = glIsEnabled( GL_SCISSOR_TEST );
		GLint previousScissorBox[4] = { 0, 0, 0, 0 };
		if ( scissorWasEnabled ) {
			glGetIntegerv( GL_SCISSOR_BOX, previousScissorBox );
			glDisable( GL_SCISSOR_TEST );
		}

		glBlitFramebuffer( x, y, x + imageWidth, y + imageHeight, 0, 0, imageWidth, imageHeight, GL_COLOR_BUFFER_BIT, GL_NEAREST );
		if ( scissorWasEnabled ) {
			glScissor( previousScissorBox[0], previousScissorBox[1], previousScissorBox[2], previousScissorBox[3] );
			glEnable( GL_SCISSOR_TEST );
		}

		glFramebufferTexture2D( GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0 );

		glBindFramebuffer( GL_READ_FRAMEBUFFER, previousReadFbo );
		glBindFramebuffer( GL_DRAW_FRAMEBUFFER, previousDrawFbo );
		glReadBuffer( previousReadBuffer );
		glDrawBuffer( previousDrawBuffer );
	} else {
		GLint previousReadFbo = 0;
		GLint previousReadBuffer = GL_BACK;
		glGetIntegerv( GL_READ_FRAMEBUFFER_BINDING, &previousReadFbo );
		glGetIntegerv( GL_READ_BUFFER, &previousReadBuffer );

		if ( readingFromRenderTexture ) {
			glBindFramebuffer( GL_READ_FRAMEBUFFER, backEnd.renderTexture->GetDeviceHandle() );
			glReadBuffer( readAttachment );
		} else {
			glBindFramebuffer( GL_READ_FRAMEBUFFER, 0 );
			glReadBuffer( GL_BACK );
		}

		const GLboolean scissorWasEnabled = glIsEnabled( GL_SCISSOR_TEST );
		GLint previousScissorBox[4] = { 0, 0, 0, 0 };
		if ( scissorWasEnabled ) {
			glGetIntegerv( GL_SCISSOR_BOX, previousScissorBox );
			glDisable( GL_SCISSOR_TEST );
		}

		if ( needsStorageResize ) {
			glCopyTexImage2D( GL_TEXTURE_2D, 0, internalFormat != 0 ? internalFormat : GL_RGBA8, x, y, imageWidth, imageHeight, 0 );
		} else {
			glCopyTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, x, y, imageWidth, imageHeight );
		}

		if ( scissorWasEnabled ) {
			glScissor( previousScissorBox[0], previousScissorBox[1], previousScissorBox[2], previousScissorBox[3] );
			glEnable( GL_SCISSOR_TEST );
		}

		glBindFramebuffer( GL_READ_FRAMEBUFFER, previousReadFbo );
		glReadBuffer( previousReadBuffer );

		if ( needsStorageResize ) {
			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 ) {
	R_BindTextureForDirectAccess( ( opts.textureType == TT_CUBIC ) ? GL_TEXTURE_CUBE_MAP_EXT : GL_TEXTURE_2D, texnum );

	// The destination must hold depth-renderable storage: it gets attached to
	// GL_DEPTH_ATTACHMENT for the blit path and receives GL_DEPTH_COMPONENT
	// copies otherwise. Images that were ever allocated with a color format
	// (e.g. the legacy RGBA placeholder path) would make those operations fail
	// silently every frame, so respecify them as real depth here.
	const bool hasDepthStorage =
		internalFormat == GL_DEPTH_COMPONENT
		|| internalFormat == GL_DEPTH_COMPONENT16
		|| internalFormat == GL_DEPTH_COMPONENT24
		|| internalFormat == GL_DEPTH_COMPONENT32
		|| internalFormat == GL_DEPTH_COMPONENT32F
		|| internalFormat == GL_DEPTH24_STENCIL8
		|| internalFormat == GL_DEPTH32F_STENCIL8;
	if ( !hasDepthStorage ) {
		internalFormat = GL_DEPTH_COMPONENT24;
		dataFormat = GL_DEPTH_COMPONENT;
		dataType = GL_FLOAT;
		opts.format = FMT_DEPTH;
	}

	const bool needsStorageResize = !hasDepthStorage || ( opts.width != imageWidth ) || ( opts.height != imageHeight );
	opts.width = imageWidth;
	opts.height = imageHeight;

	const bool readingFromRenderTexture = ( backEnd.renderTexture != NULL ) && ( backEnd.renderTexture->GetDepthImage() != NULL );
	const bool sourceDepthIsMSAA =
		readingFromRenderTexture
		? ( backEnd.renderTexture->GetDepthImage()->GetOpts().numMSAASamples > 1 )
		: ( r_multiSamples.GetInteger() > 1 );
	const bool canBlitDepth = ( GLEW_EXT_framebuffer_blit || GLEW_ARB_framebuffer_object || GLEW_VERSION_3_0 );

	// Prefer depth blits when sampling depth for SSAO, especially for MSAA sources.
	// glReadPixels from multisampled depth buffers is invalid on many drivers.
	if ( canBlitDepth && ( readingFromRenderTexture || sourceDepthIsMSAA ) ) {
		GLint previousReadFbo = 0;
		GLint previousDrawFbo = 0;
		GLint previousReadBuffer = GL_BACK;
		GLint previousDrawBuffer = GL_BACK;

		glGetIntegerv( GL_READ_FRAMEBUFFER_BINDING, &previousReadFbo );
		glGetIntegerv( GL_DRAW_FRAMEBUFFER_BINDING, &previousDrawFbo );
		glGetIntegerv( GL_READ_BUFFER, &previousReadBuffer );
		glGetIntegerv( GL_DRAW_BUFFER, &previousDrawBuffer );

		if ( r_copyDepthbufferFbo == 0 ) {
			glGenFramebuffers( 1, &r_copyDepthbufferFbo );
		}
		const GLuint copyDepthFbo = r_copyDepthbufferFbo;

		if ( needsStorageResize ) {
			glTexImage2D( GL_TEXTURE_2D, 0, internalFormat != 0 ? internalFormat : GL_DEPTH_COMPONENT24, imageWidth, imageHeight, 0,
				dataFormat != 0 ? dataFormat : GL_DEPTH_COMPONENT, dataType != 0 ? dataType : GL_FLOAT, NULL );
			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 );
		}

		if ( readingFromRenderTexture ) {
			glBindFramebuffer( GL_READ_FRAMEBUFFER, backEnd.renderTexture->GetDeviceHandle() );
		} else {
			glBindFramebuffer( GL_READ_FRAMEBUFFER, 0 );
			glReadBuffer( GL_BACK );
		}

		glBindFramebuffer( GL_DRAW_FRAMEBUFFER, copyDepthFbo );
		glFramebufferTexture2D( GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, texnum, 0 );
		glReadBuffer( GL_NONE );
		glDrawBuffer( GL_NONE );

		const GLboolean scissorWasEnabled = glIsEnabled( GL_SCISSOR_TEST );
		GLint previousScissorBox[4] = { 0, 0, 0, 0 };
		if ( scissorWasEnabled ) {
			glGetIntegerv( GL_SCISSOR_BOX, previousScissorBox );
			glDisable( GL_SCISSOR_TEST );
		}

		glBlitFramebuffer( x, y, x + imageWidth, y + imageHeight, 0, 0, imageWidth, imageHeight, GL_DEPTH_BUFFER_BIT, GL_NEAREST );

		if ( scissorWasEnabled ) {
			glScissor( previousScissorBox[0], previousScissorBox[1], previousScissorBox[2], previousScissorBox[3] );
			glEnable( GL_SCISSOR_TEST );
		}

		glFramebufferTexture2D( GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, 0, 0 );

		glBindFramebuffer( GL_READ_FRAMEBUFFER, previousReadFbo );
		glBindFramebuffer( GL_DRAW_FRAMEBUFFER, previousDrawFbo );
		glReadBuffer( previousReadBuffer );
		glDrawBuffer( previousDrawBuffer );
		return;
	}

	if ( sourceDepthIsMSAA ) {
		static bool msaaDepthFallbackWarned = false;
		if ( !msaaDepthFallbackWarned ) {
			common->Warning( "CopyDepthbuffer: missing framebuffer blit support for MSAA depth resolve; using readback fallback" );
			msaaDepthFallbackWarned = true;
		}

		const int pixelCount = imageWidth * imageHeight;
		if ( pixelCount <= 0 ) {
			return;
		}

		GLint previousReadFbo = 0;
		GLint previousDrawFbo = 0;
		GLint previousReadBuffer = GL_BACK;
		GLint previousDrawBuffer = GL_BACK;
		glGetIntegerv( GL_READ_FRAMEBUFFER_BINDING, &previousReadFbo );
		glGetIntegerv( GL_DRAW_FRAMEBUFFER_BINDING, &previousDrawFbo );
		glGetIntegerv( GL_READ_BUFFER, &previousReadBuffer );
		glGetIntegerv( GL_DRAW_BUFFER, &previousDrawBuffer );

		if ( readingFromRenderTexture ) {
			glBindFramebuffer( GL_READ_FRAMEBUFFER, backEnd.renderTexture->GetDeviceHandle() );
		} else {
			glBindFramebuffer( GL_READ_FRAMEBUFFER, 0 );
			glReadBuffer( GL_BACK );
		}

		const GLboolean scissorWasEnabled = glIsEnabled( GL_SCISSOR_TEST );
		GLint previousScissorBox[4] = { 0, 0, 0, 0 };
		if ( scissorWasEnabled ) {
			glGetIntegerv( GL_SCISSOR_BOX, previousScissorBox );
			glDisable( GL_SCISSOR_TEST );
		}

		static idList<float> resolvedDepthBuffer;
		resolvedDepthBuffer.SetNum( pixelCount, false );
		glReadPixels( x, y, imageWidth, imageHeight, GL_DEPTH_COMPONENT, GL_FLOAT, resolvedDepthBuffer.Ptr() );

		if ( scissorWasEnabled ) {
			glScissor( previousScissorBox[0], previousScissorBox[1], previousScissorBox[2], previousScissorBox[3] );
			glEnable( GL_SCISSOR_TEST );
		}

		// The readback buffer always holds GL_FLOAT depth values regardless of
		// the texture's preferred upload type.
		if ( needsStorageResize ) {
			glTexImage2D( GL_TEXTURE_2D, 0, internalFormat != 0 ? internalFormat : GL_DEPTH_COMPONENT24, imageWidth, imageHeight, 0,
				GL_DEPTH_COMPONENT, GL_FLOAT, resolvedDepthBuffer.Ptr() );
			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 );
		} else {
			glTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, imageWidth, imageHeight,
				GL_DEPTH_COMPONENT, GL_FLOAT, resolvedDepthBuffer.Ptr() );
		}

		glBindFramebuffer( GL_READ_FRAMEBUFFER, previousReadFbo );
		glBindFramebuffer( GL_DRAW_FRAMEBUFFER, previousDrawFbo );
		glReadBuffer( previousReadBuffer );
		glDrawBuffer( previousDrawBuffer );
	} else {
		GLint previousReadFbo = 0;
		GLint previousReadBuffer = GL_BACK;
		glGetIntegerv( GL_READ_FRAMEBUFFER_BINDING, &previousReadFbo );
		glGetIntegerv( GL_READ_BUFFER, &previousReadBuffer );

		if ( readingFromRenderTexture ) {
			glBindFramebuffer( GL_READ_FRAMEBUFFER, backEnd.renderTexture->GetDeviceHandle() );
		} else {
			glBindFramebuffer( GL_READ_FRAMEBUFFER, 0 );
		}

		const GLboolean scissorWasEnabled = glIsEnabled( GL_SCISSOR_TEST );
		GLint previousScissorBox[4] = { 0, 0, 0, 0 };
		if ( scissorWasEnabled ) {
			glGetIntegerv( GL_SCISSOR_BOX, previousScissorBox );
			glDisable( GL_SCISSOR_TEST );
		}

		if ( needsStorageResize ) {
			glCopyTexImage2D( GL_TEXTURE_2D, 0, internalFormat != 0 ? internalFormat : GL_DEPTH_COMPONENT, x, y, imageWidth, imageHeight, 0 );
		} else {
			glCopyTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, x, y, imageWidth, imageHeight );
		}

		if ( scissorWasEnabled ) {
			glScissor( previousScissorBox[0], previousScissorBox[1], previousScissorBox[2], previousScissorBox[3] );
			glEnable( GL_SCISSOR_TEST );
		}

		glBindFramebuffer( GL_READ_FRAMEBUFFER, previousReadFbo );
		glReadBuffer( previousReadBuffer );
	}

	//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( "<BAD TYPE:%i>", 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( BC7 );
		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( "<BAD FILTER:%i>", filter );
			break;
	}

	switch ( repeat ) {
		case TR_REPEAT:
			common->Printf( "rept " );
			break;
		case TR_MIRRORED_REPEAT:
			common->Printf( "mrrr " );
			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( "%5i %4ik ", GetUseCount(), 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;
	}

	// persistent images are runtime render targets, not file-backed
	if ( opts.isPersistant ) {
		common->DPrintf( "reallocating persistent %s.\n", GetName() );
		DeriveOpts();
		AllocImage();
		return;
	}

	// check file times
	if ( !force ) {
		ID_TIME_T current = FILE_NOT_FOUND_TIMESTAMP;
		idStr currentSourceName = imgName;
		if ( cubeFiles != CF_2D ) {
			R_LoadCubeImages( imgName, cubeFiles, NULL, NULL, &current );
		} else {
			idStr sourceExtension;
			idStr sourceName = imgName;
			sourceName.ExtractFileExtension( sourceExtension );
			idStr preferredDDSName;
			ID_TIME_T preferredDDSFileTime = FILE_NOT_FOUND_TIMESTAMP;
			if ( idStr::Icmp( sourceExtension.c_str(), "dds" ) != 0 &&
				 R_ResolvePreferredDDSImageSource( imgName, preferredDDSName, &preferredDDSFileTime, true, NULL ) ) {
				ID_TIME_T originalSourceTime = FILE_NOT_FOUND_TIMESTAMP;
				if ( !fileSystem->InProductionMode() ) {
					R_LoadImageProgram( imgName, NULL, NULL, NULL, &originalSourceTime );
				}
				if ( originalSourceTime != FILE_NOT_FOUND_TIMESTAMP && preferredDDSFileTime < originalSourceTime ) {
					current = originalSourceTime;
				} else {
					current = preferredDDSFileTime;
					currentSourceName = preferredDDSName;
				}
			} else {
				// get the current values
				R_LoadImageProgram( imgName, NULL, NULL, NULL, &current );
			}
		}
		const bool sourceSelectionChanged = loadedSourceName.Length() == 0 || loadedSourceName.Icmp( currentSourceName ) != 0;
		if ( !sourceSelectionChanged && 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;
	R_BindTextureForDirectAccess( ( opts.textureType == TT_CUBIC ) ? GL_TEXTURE_CUBE_MAP_EXT : GL_TEXTURE_2D, texnum );
	SetTexParameters();
}

