#include "tr_local.h"
#include "tr_common.h"

#define COMMON_DEPTH_STENCIL
//#define DEPTH_RENDER_BUFFER
//#define USE_FBO_BLIT

// screenMap texture dimensions
#define SCR_WIDTH 128
#define SCR_HEIGHT 64

#define BLOOM_BASE 5
#define FBO_COUNT (BLOOM_BASE+(MAX_BLUR_PASSES*2))

#if BLOOM_BASE < 2
#error no space for main/postprocess buffers
#endif

static GLuint programs[ PROGRAM_COUNT ];
static GLuint current_vp;
static GLuint current_fp;

static int programCompiled = 0;
static int programEnabled	= 0;

qboolean fboEnabled = qfalse;
qboolean fboBloomInited = qfalse;
int      fboReadIndex = 0;
GLint    fboInternalFormat;
GLint    fboTextureFormat;
GLint    fboTextureType;
int      fboBloomPasses;
int      fboBloomBlendBase;
int      fboBloomFilterSize;

qboolean windowAdjusted;
int		blitX0, blitX1;
int		blitY0, blitY1;
int		blitClear;
GLenum	blitFilter;

qboolean superSampled;

typedef struct frameBuffer_s {
	GLuint fbo;
	GLuint color;			// renderbuffer if multisampled
	GLuint depthStencil;	// renderbuffer if multisampled
	GLint  width;
	GLint  height;
	qboolean multiSampled;
} frameBuffer_t;

static GLuint commonDepthStencil;
static frameBuffer_t frameBufferMS;
static frameBuffer_t frameBuffers[ FBO_COUNT ];

static qboolean frameBufferMultiSampling = qfalse;

qboolean blitMSfbo = qfalse;

#ifndef GL_TEXTURE_IMAGE_FORMAT
#define GL_TEXTURE_IMAGE_FORMAT 0x828F
#endif

#ifndef GL_TEXTURE_IMAGE_TYPE
#define GL_TEXTURE_IMAGE_TYPE 0x8290
#endif

extern void RB_SetGL2D( void );

qboolean GL_ProgramAvailable( void )
{
	return (programCompiled != 0);
}


static void ARB_ProgramDisable( void )
{
	if ( current_vp )
		qglDisable( GL_VERTEX_PROGRAM_ARB );
	if ( current_fp )
		qglDisable( GL_FRAGMENT_PROGRAM_ARB );
	current_vp = 0;
	current_fp = 0;
	programEnabled = 0;
}


void GL_ProgramDisable( void )
{
	if ( programEnabled )
	{
		ARB_ProgramDisable();
	}
}


void ARB_ProgramEnableExt( GLuint vertexProgram, GLuint fragmentProgram )
{
	if ( programCompiled )
	{
		if ( current_vp != vertexProgram ) {
			current_vp = vertexProgram;
			if ( current_vp ) {
				qglEnable( GL_VERTEX_PROGRAM_ARB );
				qglBindProgramARB( GL_VERTEX_PROGRAM_ARB, current_vp );
			} else {
				qglDisable( GL_VERTEX_PROGRAM_ARB );
			}
		}

		if ( current_fp != fragmentProgram ) {
			current_fp = fragmentProgram;
			if ( current_fp ) {
				qglEnable( GL_FRAGMENT_PROGRAM_ARB );
				qglBindProgramARB( GL_FRAGMENT_PROGRAM_ARB, current_fp );
			} else {
				qglDisable( GL_FRAGMENT_PROGRAM_ARB );
			}
		}
		programEnabled = 1;
	}
}


static void ARB_ProgramEnable( programNum vp, programNum fp )
{
	ARB_ProgramEnableExt( programs[ vp ], programs[ fp ] );
}


void GL_ProgramEnable( void )
{
	ARB_ProgramEnable( DUMMY_VERTEX, SPRITE_FRAGMENT );
}


#ifdef USE_PMLIGHT
static void ARB_Lighting( const shaderStage_t* pStage )
{
	const dlight_t* dl;
	byte clipBits[ SHADER_MAX_VERTEXES ];
	unsigned hitIndexes[ SHADER_MAX_INDEXES ];
	int numIndexes;
	int clip;
	int i;
	
	backEnd.pc.c_lit_vertices_lateculltest += tess.numVertexes;

	dl = tess.light;

	for ( i = 0; i < tess.numVertexes; ++i ) {
		vec3_t dist;
		VectorSubtract( dl->transformed, tess.xyz[i], dist );

		if ( tess.surfType != SF_GRID && DotProduct( dist, tess.normal[i] ) <= 0.0f ) {
			clipBits[ i ] = 63;
			continue;
		}

		clip = 0;
		if ( dist[0] > dl->radius ) {
			clip |= 1;
		} else if ( dist[0] < -dl->radius ) {
			clip |= 2;
		}
		if ( dist[1] > dl->radius ) {
			clip |= 4;
		} else if ( dist[1] < -dl->radius ) {
			clip |= 8;
		}
		if ( dist[2] > dl->radius ) {
			clip |= 16;
		} else if ( dist[2] < -dl->radius ) {
			clip |= 32;
		}

		clipBits[i] = clip;
	}

	// build a list of triangles that need light
	numIndexes = 0;

	for ( i = 0 ; i < tess.numIndexes ; i += 3 ) {
		int		a, b, c;

		a = tess.indexes[i];
		b = tess.indexes[i+1];
		c = tess.indexes[i+2];
		if ( clipBits[a] & clipBits[b] & clipBits[c] ) {
			continue;	// not lighted
		}
		hitIndexes[numIndexes] = a;
		hitIndexes[numIndexes+1] = b;
		hitIndexes[numIndexes+2] = c;
		numIndexes += 3;
	}

	backEnd.pc.c_lit_indices_latecull_in += numIndexes;
	backEnd.pc.c_lit_indices_latecull_out += tess.numIndexes - numIndexes;

	if ( !numIndexes )
		return;

	if ( tess.shader->sort < SS_OPAQUE ) {
		GL_State( GLS_SRCBLEND_DST_COLOR | GLS_DSTBLEND_ONE | GLS_DEPTHFUNC_EQUAL );
	} else {
		GL_State( GLS_SRCBLEND_ONE | GLS_DSTBLEND_ONE | GLS_DEPTHFUNC_EQUAL );
	}

	GL_SelectTexture( 0 );

	R_BindAnimatedImage( &pStage->bundle[ tess.shader->lightingBundle ] );
	
	R_DrawElements( numIndexes, hitIndexes );
}


static void ARB_Lighting_Fast( const shaderStage_t* pStage )
{
	if ( !tess.numIndexes )
		return;

	if ( tess.shader->sort < SS_OPAQUE ) {
		GL_State( GLS_SRCBLEND_DST_COLOR | GLS_DSTBLEND_ONE | GLS_DEPTHFUNC_EQUAL );
	} else {
		GL_State( GLS_SRCBLEND_ONE | GLS_DSTBLEND_ONE | GLS_DEPTHFUNC_EQUAL );
	}

	GL_SelectTexture( 0 );

	R_BindAnimatedImage( &pStage->bundle[ tess.shader->lightingBundle ] );
	
	R_DrawElements( tess.numIndexes, tess.indexes );
}


void ARB_SetupLightParams( void )
{
	programNum vertexProgram;
	programNum fragmentProgram;
	const fogProgramParms_t *fp;
	qboolean fogPass;
	const dlight_t *dl;
	vec3_t lightRGB;
	float radius;

	tess.dlightUpdateParams = qfalse;
	tess.cullType = tess.shader->cullType;

	if ( !programCompiled )
		return;

	dl = tess.light;

	if ( !glConfig.deviceSupportsGamma && !fboEnabled )
		VectorScale( dl->color, 2 * powf( r_intensity->value, r_gamma->value ), lightRGB );
	else
		VectorCopy( dl->color, lightRGB );

	radius = dl->radius;

	fogPass = ( tess.fogNum && tess.shader->fogPass );
	fp = NULL;

	vertexProgram = DLIGHT_VERTEX;

	if ( dl->linear ) {
		fragmentProgram = (tess.shader->cullType == CT_TWO_SIDED) ? DLIGHT_LINEAR_ABS_FRAGMENT : DLIGHT_LINEAR_FRAGMENT;
	} else {
		fragmentProgram = (tess.shader->cullType == CT_TWO_SIDED) ? DLIGHT_ABS_FRAGMENT : DLIGHT_FRAGMENT;
	}

	if ( fogPass ) {
		fp = RB_CalcFogProgramParms();
		// switch to fog programs
		if ( fp->eyeOutside ) {
			vertexProgram += 2;
		} else {
			vertexProgram += 1;
		}
		++fragmentProgram;
	}

	ARB_ProgramEnable( vertexProgram, fragmentProgram );

	qglProgramLocalParameter4fARB( GL_FRAGMENT_PROGRAM_ARB, 0, lightRGB[0], lightRGB[1], lightRGB[2], 1.0f / Square( radius ) );

	if ( dl->linear )
	{
		vec3_t ab;
		VectorSubtract( dl->transformed2, dl->transformed, ab );
		//qglProgramLocalParameter4fARB( GL_FRAGMENT_PROGRAM_ARB, 2, dl->transformed[0], dl->transformed[1], dl->transformed[2], 0 );
		//qglProgramLocalParameter4fARB( GL_FRAGMENT_PROGRAM_ARB, 3, dl->transformed2[0], dl->transformed2[1], dl->transformed2[2], 0 );
		qglProgramLocalParameter4fARB( GL_FRAGMENT_PROGRAM_ARB, 4, ab[0], ab[1], ab[2], 1.0f / DotProduct( ab, ab ) );
	}

	qglProgramLocalParameter4fARB( GL_VERTEX_PROGRAM_ARB, 0, backEnd.or.viewOrigin[0], backEnd.or.viewOrigin[1], backEnd.or.viewOrigin[2], 0 );
	qglProgramLocalParameter4fARB( GL_VERTEX_PROGRAM_ARB, 1, dl->transformed[0], dl->transformed[1], dl->transformed[2], 0 );

	if ( fogPass )
	{
		GL_BindTexture( 1, tr.fogImage->texnum );
		//qglProgramLocalParameter4fvARB( GL_FRAGMENT_PROGRAM_ARB, 5, fp->fogColor );
		qglProgramLocalParameter4fvARB( GL_VERTEX_PROGRAM_ARB, 2, fp->fogDistanceVector );
		qglProgramLocalParameter4fvARB( GL_VERTEX_PROGRAM_ARB, 3, fp->fogDepthVector );
		qglProgramLocalParameter4fARB( GL_VERTEX_PROGRAM_ARB, 4, fp->eyeT, 0.0f, 0.0f, 0.0f );
		GL_SelectTexture( 0 );
	}
}


void ARB_LightingPass( void )
{
	const shaderStage_t* pStage;

	if ( tess.shader->lightingStage < 0 )
		return;

	// we may need to update programs for fog transitions
	if ( tess.dlightUpdateParams )
		ARB_SetupLightParams();

	RB_DeformTessGeometry();

	GL_Cull( tess.shader->cullType );

	// set polygon offset if necessary
	if ( tess.shader->polygonOffset )
	{
		qglEnable( GL_POLYGON_OFFSET_FILL );
		qglPolygonOffset( r_offsetFactor->value, r_offsetUnits->value );
	}

	pStage = tess.xstages[ tess.shader->lightingStage ];

	R_ComputeTexCoords( 0, &pStage->bundle[ tess.shader->lightingBundle ] );

	GL_ClientState( 1, CLS_NONE );
	GL_ClientState( 0, CLS_TEXCOORD_ARRAY | CLS_NORMAL_ARRAY );

	// since this is guaranteed to be a single pass, fill and lock all the arrays

	qglTexCoordPointer( 2, GL_FLOAT, 0, tess.svars.texcoordPtr[0] );
	qglNormalPointer( GL_FLOAT, sizeof( tess.normal[0] ), tess.normal );
	qglVertexPointer( 3, GL_FLOAT, sizeof( tess.xyz[0] ), tess.xyz );

	if ( qglLockArraysEXT )
		qglLockArraysEXT( 0, tess.numVertexes );

	// CPU may limit performance in following cases
	if ( tess.light->linear || gl_version >= 40 )
		ARB_Lighting_Fast( pStage );
	else
		ARB_Lighting( pStage );

	if ( qglUnlockArraysEXT )
		qglUnlockArraysEXT();

	// reset polygon offset
	if ( tess.shader->polygonOffset ) 
	{
		qglDisable( GL_POLYGON_OFFSET_FILL );
	}
}
#endif // USE_PMLIGHT


const char *fogOutVPCode = {
	"PARAM fogDistanceVector = program.local[2]; \n"
	"PARAM fogDepthVector = program.local[3]; \n"
	"PARAM eyeT = program.local[4]; \n"
	"PARAM _01_32 = 0.03125; \n"
	"PARAM _30_32 = 0.93750; \n"
	"TEMP st; \n"
	
	// s = DotProduct( v, fogDistanceVector ) + fogDistanceVector[3];
	"DP3 st.x, fogDistanceVector, vertex.position; \n"	
	"ADD st.x, st.x, fogDistanceVector.w; \n"

	// t = DotProduct( v, fogDepthVector ) + fogDepthVector[3];
	"DP3 st.y, fogDepthVector, vertex.position; \n"	
	"ADD st.y, st.y, fogDepthVector.w; \n"

	// if ( t < 1.0 ) { t = 1.0/32; } else { t = 1.0/32 + 30.0/32 * t / ( t - eyeT ); }
	"SGE st.w, st.y, 1.0; \n"
	"SUB st.z, st.y, eyeT.x; \n"
	"RCP st.z, st.z; \n"
	"MUL st.z, st.z, st.y; \n"
	"MUL st.z, st.z, _30_32; \n"
	"MAD st.y, st.z, st.w, _01_32; \n"
	
	//"MOV st.z, {1.0}; \n"
	"MOV st.w, {1.0}; \n"

	"MOV result.texcoord[4], st; \n"
};


const char *fogInVPCode = {

	"PARAM fogDistanceVector = program.local[2]; \n"
	"PARAM fogDepthVector = program.local[3]; \n"
	"PARAM eyeT = program.local[4]; \n"
	"PARAM _01_32 = 0.03125; \n"
	"PARAM _30_32 = 0.93750; \n"
	"TEMP st; \n"
	
	// s = DotProduct( v, fogDistanceVector ) + fogDistanceVector[3];
	"DP3 st.x, fogDistanceVector, vertex.position; \n"	
	"ADD st.x, st.x, fogDistanceVector.w; \n"

	// t = DotProduct( v, fogDepthVector ) + fogDepthVector[3];
	"DP3 st.y, fogDepthVector, vertex.position; \n"
	"ADD st.y, st.y, fogDepthVector.w; \n"

	//if ( t < 0 ) { t = 1.0/32; } else { t = 31.0/32; }
	"SGE st.w, st.y, 0.0; \n"
	"MAD st.y, st.w, _30_32, _01_32; \n"

	//"MOV st.z, {1.0}; \n"
	"MOV st.w, {1.0}; \n"

	"MOV result.texcoord[4], st; \n"
};


#ifdef USE_PMLIGHT
static const char *dlightVP = {
	"!!ARBvp1.0 \n"
	"OPTION ARB_position_invariant; \n"
	"PARAM posEye = program.local[0]; \n"
	"PARAM posLight = program.local[1]; \n"
	"OUTPUT lv = result.texcoord[1]; \n" // 1
	"OUTPUT ev = result.texcoord[2]; \n" // 2
	"OUTPUT n = result.texcoord[3]; \n"  // 3
	"MOV result.texcoord[0], vertex.texcoord; \n" // 0
	"SUB lv, posLight, vertex.position; \n"
	"SUB ev, posEye, vertex.position; \n"
	"MOV n, vertex.normal; \n"
	"%s" // fog shader if needed
	"END \n"
};


static const char *ARB_BuildDlightFP( char *program, int programIndex )
{
	qboolean fog = qfalse;
	qboolean linear = qfalse;
	qboolean abslight = qfalse;

	program[0] = '\0';

	switch ( programIndex ) {
		case DLIGHT_FRAGMENT_FOG:
		case DLIGHT_ABS_FRAGMENT_FOG:
		case DLIGHT_LINEAR_FRAGMENT_FOG:
		case DLIGHT_LINEAR_ABS_FRAGMENT_FOG:
			fog = qtrue;
			break;
	}

	switch ( programIndex ) {
		case DLIGHT_LINEAR_FRAGMENT:
		case DLIGHT_LINEAR_FRAGMENT_FOG:
		case DLIGHT_LINEAR_ABS_FRAGMENT:
		case DLIGHT_LINEAR_ABS_FRAGMENT_FOG:
			linear = qtrue;
			break;
	}

	switch ( programIndex ) {
		case DLIGHT_ABS_FRAGMENT:
		case DLIGHT_ABS_FRAGMENT_FOG:
		case DLIGHT_LINEAR_ABS_FRAGMENT:
		case DLIGHT_LINEAR_ABS_FRAGMENT_FOG:
			abslight = qtrue;
			break;
	}

	strcat( program,
	"!!ARBfp1.0 \n"
	"OPTION ARB_precision_hint_fastest; \n"
	"PARAM lightRGB = program.local[0]; \n"
	//"PARAM lightRange2recip = program.local[1]; \n"
	//"PARAM fogColor = program.local[5]; \n" // fogColor
	"TEMP base, tmp; \n"
	"TEX base, fragment.texcoord[0], texture[0], 2D; \n" );

	if ( linear ) {
		strcat( program,
		"PARAM lightVector = program.local[4]; \n"
		"ATTRIB LV = fragment.texcoord[1]; \n"
		"TEMP dnLV; \n"
		// project fragment on light vector
		"DP3 tmp.w, -LV, lightVector; \n"
		"MUL_SAT tmp.x, tmp.w, lightVector.w; \n"
		// calculate light vector from projection point
		"MAD dnLV, lightVector, tmp.x, LV; \n"
		);
	} else {
		strcat( program, "ATTRIB dnLV = fragment.texcoord[1]; \n" );
	}

	strcat( program,
	"ATTRIB dnEV = fragment.texcoord[2]; \n" // 2
	"ATTRIB n = fragment.texcoord[3]; \n"    // 3
	
	// normalize light vector
	"TEMP lv; \n"
	"DP3 tmp.w, dnLV, dnLV; \n"
	"RSQ lv.w, tmp.w; \n"
	"MUL lv.xyz, dnLV, lv.w; \n"

	// calculate light intensity
	"TEMP light; \n"
	"MUL tmp.x, tmp.w, lightRGB.w; \n"
	"SUB tmp.x, {1.0}, tmp.x; \n"
	// discard blank fragments
	"KIL tmp.x; \n"

	"MUL light, lightRGB, tmp.x; \n" ); // light.rgb

	if ( r_dlightSpecColor->value > 0 )
		strcat( program, va( "PARAM specRGB = %1.2f; \n", r_dlightSpecColor->value ) );

	strcat( program, va( "PARAM specEXP = %1.2f; \n", r_dlightSpecPower->value ) );

	strcat( program,
	// normalize eye vector
	"TEMP ev; \n"
	"DP3 ev.w, dnEV, dnEV; \n"
	"RSQ ev.w, ev.w; \n"
	"MUL ev.xyz, dnEV, ev.w; \n"

	// normalize (eye + light) vector
	"ADD tmp, lv, ev; \n"
	"DP3 tmp.w, tmp, tmp; \n"
	"RSQ tmp.w, tmp.w; \n"
	"MUL tmp.xyz, tmp, tmp.w; \n" );

	// modulate specular strength
	if ( abslight ) {
		strcat( program,
		"DP3 tmp.w, n, tmp; \n"
		"ABS tmp.w, tmp.w; \n" );
	} else {
		strcat( program,
		"DP3_SAT tmp.w, n, tmp; \n" );
	}

	strcat( program,
	"POW tmp.w, tmp.w, specEXP.w; \n"
	"TEMP spec; \n" );

	if ( r_dlightSpecColor->value > 0 ) {
		// by constant
		strcat( program, "MUL spec, specRGB, tmp.w; \n" );
	} else {
		// by texture
		strcat( program, va( "MUL tmp.w, tmp.w, %1.2f; \n", -r_dlightSpecColor->value ) );
		strcat( program, "MUL spec, base, tmp.w; \n" );
	}

	// diffuse
	if ( abslight ) {
		strcat( program,
		"TEMP bump; \n"
		"DP3 bump.w, n, lv; \n"
		// make sure that light and eye vectors are on the same plane side
		"DP3 tmp.w, n, ev; \n"
		"MUL tmp.w, tmp.w, bump.w; \n"
		"KIL tmp.w; \n"
		"ABS bump.w, bump.w; \n" );
	} else {
		strcat( program,
		"TEMP bump; \n"
		"DP3_SAT bump.w, n, lv; \n" );
	}

	strcat( program, "MAD base, base, bump.w, spec; \n" );

	if ( fog ) {
		strcat( program,
		"TEMP fog; \n"
		"TEX fog, fragment.texcoord[4], texture[1], 2D; \n" // fog texture
		//"MUL fog, fog, fogColor; \n"
		// blend with fog
		//"LRP_SAT base, fog.a, fog, base; \n"
		// modulate by inverted fog alpha
		"SUB fog.a, {1.0}, fog.a; \n"
		"MUL base, base, fog.a; \n" );
	}

	strcat( program,
	"MUL_SAT result.color, base, light; \n"
	"END \n" );
	
	r_dlightSpecColor->modified = qfalse;
	r_dlightSpecPower->modified = qfalse;

	return program;
}

#endif // USE_PMLIGHT


static const char *dummyVP = {
	"!!ARBvp1.0 \n"
	"OPTION ARB_position_invariant; \n"
	"MOV result.texcoord[0], vertex.texcoord; \n"
	"END \n" 
};


static const char *spriteFP = {
	"!!ARBfp1.0 \n"
	"OPTION ARB_precision_hint_fastest; \n"
	"TEMP base; \n"
	"TEX base, fragment.texcoord[0], texture[0], 2D; \n"
	"TEMP test; \n"
	"SUB test.a, base.a, 0.85; \n"
	"KIL test.a; \n"
	"MOV base, 0.0; \n"
	"MOV result.color, base; \n"
	"MOV result.depth, fragment.position.z; \n"
	"END \n"
};


static char *ARB_BuildGreyscaleProgram( char *buf ) {
	char *s;

	if ( r_greyscale->value == 0 ) {
		*buf = '\0';
		return buf;
	}

	s = Q_stradd( buf, "PARAM sRGB = { 0.2126, 0.7152, 0.0722, 1.0 }; \n" );

	if ( r_greyscale->value == 1.0 ) {
		Q_stradd( s, "DP3 base.xyz, base, sRGB; \n"  );
	} else {
		s = Q_stradd( s, "TEMP luma; \n" );
		s = Q_stradd( s, "DP3 luma, base, sRGB; \n" );
		/*s +=*/ sprintf( s, "LRP base.xyz, %1.2f, luma, base; \n", r_greyscale->value );
	}

	return buf;
}


static const char *gammaFP = {
	"!!ARBfp1.0 \n"
	"OPTION ARB_precision_hint_fastest; \n"
	"PARAM gamma = program.local[0]; \n"
	"TEMP base; \n"
	"TEX base, fragment.texcoord[0], texture[0], 2D; \n"
	"POW base.x, base.x, gamma.x; \n"
	"POW base.y, base.y, gamma.y; \n"
	"POW base.z, base.z, gamma.z; \n"
	"MUL base.xyz, base, gamma.w; \n"
	"%s" // for greyscale shader if needed
	"MOV base.w, 1.0; \n"
	"MOV_SAT result.color, base; \n"
	"END \n"
};

static char *ARB_BuildBloomProgram( char *buf ) {
	qboolean intensityCalculated;
	char *s = buf;

	intensityCalculated = qfalse;
	s = Q_stradd( s,
		"!!ARBfp1.0 \n"
		"OPTION ARB_precision_hint_fastest; \n"
		"PARAM thres = program.local[0]; \n"
		"TEMP base; \n"
		"TEX base, fragment.texcoord[0], texture[0], 2D; \n" );

	if ( r_bloom_threshold_mode->integer == 0 ) {
		// (r|g|b) >= threshold
		s = Q_stradd( s,
			"TEMP minv; \n"
			"SGE minv, base, thres; \n"
			"DP3_SAT minv.w, minv, minv; \n"
			"MUL base.rgb, base, minv.w; \n" );
	} else if ( r_bloom_threshold_mode->integer == 1 ) {
		// (r+g+b)/3 >= threshold
		s = Q_stradd( s,
			"PARAM scale = { 0.3333, 0.3334, 0.3333, 1.0 }; \n"
			"TEMP avg; \n"
			"DP3_SAT avg, base, scale; \n"
			"SGE avg.w, avg.x, thres.x; \n"
			"MUL base.rgb, base, avg.w; \n" );
	} else {
		// luma(r,g,b) >= threshold
		s = Q_stradd( s,
			"PARAM luma = { 0.2126, 0.7152, 0.0722, 1.0 }; \n"
			"TEMP intensity; \n"
			"DP3_SAT intensity, base, luma; \n"
			"SGE intensity.w, intensity.x, thres.x; \n"
			"MUL base.rgb, base, intensity.w; \n" );
		intensityCalculated = qtrue;
	}

	// modulation
	if ( r_bloom_modulate->integer ) {
		if ( r_bloom_modulate->integer == 1 ) {
			// by itself
			s = Q_stradd( s, "MUL base, base, base; \n" );
		} else {
			// by intensity
			if ( !intensityCalculated ) {
				s = Q_stradd( s,
					"PARAM luma = { 0.2126, 0.7152, 0.0722, 1.0 }; \n"
					"TEMP intensity; \n"
					"DP3_SAT intensity, base, luma; \n" );
			}
			s = Q_stradd( s, "MUL base, base, intensity; \n" );
		}
	}

	/*s = */ Q_stradd( s,
		"MOV base.w, 1.0; \n"
		"MOV result.color, base; \n"
		"END \n" );

	return buf;
}


// Gaussian blur shader
static char *ARB_BuildBlurProgram( char *buf, int taps ) {
	int i;
	char *s = buf;

	*s = '\0';

	s = Q_stradd( s,
		"!!ARBfp1.0 \n"
		"OPTION ARB_precision_hint_fastest; \n"
		"ATTRIB tc = fragment.texcoord[0]; \n" );

	for ( i = 0; i < taps; i++ ) {
		s = Q_stradd( s, va( "PARAM p%i = program.local[%i]; \n", i, i ) ); // tex_offset_x, tex_offset_y, 0.0, weight
	}

	s = Q_stradd( s, "TEMP cc; \n"
		"MOV cc, {0.0, 0.0, 0.0, 1.0};\n" ); // initialize final color

	for ( i = 0; i < taps; i++ ) {
		s = Q_stradd( s, va( "TEMP c%i, tc%i; \n", i, i ) );
	}

	for ( i = 0; i < taps; i++ ) {
		s = Q_stradd( s, va( "ADD tc%i.xy, tc, p%i; \n", i, i ) );
	}

	for ( i = 0; i < taps; i++ ) {
		s = Q_stradd( s, va( "TEX c%i, tc%i, texture[0], 2D; \n", i, i ) );
		s = Q_stradd( s, va( "MAD cc, c%i, p%i.w, cc; \n", i, i ) ); // cc = cc + cN + pN.w
	}

	/*s = */ Q_stradd( s,
		"MOV cc.a, 1.0; \n"
		"MOV_SAT result.color, cc; \n"
		"END \n" );

	return buf;
}


static char *ARB_BuildBlendProgram( char *buf, int count ) {
	int i;
	char *s = buf;

	*s = '\0';
	s = Q_stradd( s, 
		"!!ARBfp1.0 \n"
		"OPTION ARB_precision_hint_fastest; \n"
		"ATTRIB tc = fragment.texcoord[0]; \n"
		"TEMP cx, cc;\n"
		"MOV cc, {0.0, 0.0, 0.0, 1.0}; \n" );

	for ( i = 0; i < count; i++ ) {
		s = Q_stradd( s, va( "TEX cx, fragment.texcoord[0], texture[%i], 2D; \n"
			"ADD cc, cx, cc; \n", i ) );
	}

	/*s = */ Q_stradd( s,
		"MOV cc.a, 1.0; \n"
		"MOV_SAT result.color, cc; \n"
		"END \n" );

	return buf;
}


// blend 2 texture together
static const char *blend2FP = {
	"!!ARBfp1.0 \n"
	"OPTION ARB_precision_hint_fastest; \n"
	"PARAM factor = program.local[1]; \n"
	"TEMP base; \n"
	"TEMP post; \n"
	"TEX base, fragment.texcoord[0], texture[0], 2D; \n"
	"TEX post, fragment.texcoord[0], texture[1], 2D; \n"
	"MAD base, post, factor.x, base; \n"
	//"ADD base, base, post; \n"
	"MOV base.w, 1.0; \n"
	"MOV_SAT result.color, base; \n"
	"END \n"
};

// combined blend + gamma correction pass
static const char *blend2gammaFP = {
	"!!ARBfp1.0 \n"
	"OPTION ARB_precision_hint_fastest; \n"
	"PARAM gamma = program.local[0]; \n"
	"PARAM factor = program.local[1]; \n"
	"TEMP base; \n"
	"TEMP post; \n"
	"TEX base, fragment.texcoord[0], texture[0], 2D; \n"
	"TEX post, fragment.texcoord[0], texture[1], 2D; \n"
	//"ADD base, base, post; \n"
	"MAD base, post, factor.x, base; \n"
	"POW base.x, base.x, gamma.x; \n"
	"POW base.y, base.y, gamma.y; \n"
	"POW base.z, base.z, gamma.z; \n"
	"MUL base.xyz, base, gamma.w; \n"
	"%s" // for greyscale shader if needed
	"MOV base.w, 1.0; \n"
	"MOV_SAT result.color, base; \n"
	"END \n" 
};


static void RenderQuad( int w, int h )
{
	static const vec2_t t[4] = { {0.0, 1.0}, {1.0, 1.0}, {0.0, 0.0}, {1.0, 0.0} };
	static vec3_t v[4] = { { 0 } };
	
	v[1][0] = w;
	v[2][1] = h;
	v[3][0] = w;
	v[3][1] = h;

	GL_ClientState( 0, CLS_TEXCOORD_ARRAY );

	qglVertexPointer( 3, GL_FLOAT, 0, v );
	qglTexCoordPointer( 2, GL_FLOAT, 0, t );

	qglDrawArrays( GL_TRIANGLE_STRIP, 0, 4 );
}


static void ARB_BlurParams( int width, int height, int ksize, qboolean horizontal )
{
	static float weight[ MAX_FILTER_SIZE ];
	static int old_ksize = -1;

	static const float x_k[ MAX_FILTER_SIZE+1 ][ MAX_FILTER_SIZE + 1 ] = {
		// [1/weight], coeff.1, coeff.2, [...]
		{ 0 },
		{ 1.0/1, 1 },
		{ 1.0/2, 1, 1 },
	//	{ 1/4,   1, 2, 1 },
		{ 1.0/16,  5, 6, 5 },
		{ 1.0/8,   1, 3, 3, 1 },
		{ 1.0/16,  1, 4, 6, 4, 1 },
		{ 1.0/32,  1, 5, 10, 10, 5, 1 },
		{ 1.0/64,  1, 6, 15, 20, 15, 6, 1 },
		{ 1.0/128, 1, 7, 21, 35, 35, 21, 7, 1 },
		{ 1.0/256, 1, 8, 28, 56, 70, 56, 28, 8, 1 },
		{ 1.0/512, 1, 9, 36, 84, 126, 126, 84, 36, 9, 1 },
		{ 1.0/1024, 1, 10, 45, 120, 210, 252, 210, 120, 45, 10, 1 },
		{ 1.0/2048, 1, 11, 55, 165, 330, 462, 462, 330, 165, 55, 11, 1 },
		{ 1.0/4096, 1, 12, 66, 220, 495, 792, 924, 792, 495, 220, 66, 12, 1 },
		{ 1.0/8192, 1, 13, 78, 286, 715, 1287, 1716, 1716, 1287, 715, 286, 78, 13, 1 },
		{ 1.0/16384, 1, 14, 91, 364, 1001, 2002, 3003, 3432, 3003, 2002, 1001, 364, 91, 14, 1 },
		{ 1.0/32768, 1, 15, 105, 455, 1365, 3003, 5005, 6435, 6435, 5005, 3003, 1365, 455, 105, 15, 1 },
		{ 1.0/65536, 1, 16, 120, 560, 1820, 4368, 8008, 11440, 12870, 11440, 8008, 4368, 1820, 560, 120, 16, 1 },
		{ 1.0/131072, 1, 17, 136, 680, 2380, 6188, 12376, 19448, 24310, 24310, 19448, 12376, 6188, 2380, 680, 136, 17, 1 },
		{ 1.0/262144, 1, 18, 153, 816, 3060, 8568, 18564, 31824, 43758, 48620, 43758, 31824, 18564, 8568, 3060, 816, 153, 18, 1 },
		{ 1.0/524288, 1, 19, 171, 969, 3876, 11628, 27132, 50388, 75582, 92378, 92378, 75582, 50388, 27132, 11628, 3876, 969, 171, 19, 1 },

	};

	static const float x_o[ MAX_FILTER_SIZE+1 ][ MAX_FILTER_SIZE ] = {
		{ 0 },
		{ 0.0 },
		{ -0.5, 0.5 },
	//	{ -1.0, 0.0, 1.0 },
		{ -1.2f, 0.0, 1.2f },
		{ -1.5, -0.5, 0.5, 1.5 },
		{ -2.0, -1.0, 0.0, 1.0, 2.0 },
		{ -2.5, -1.5, -0.5, 0.5, 1.5, 2.5 },
		{ -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0 },
		{ -3.5, -2.5, -1.5, -0.5, 0.5, 1.5, 2.5, 3.5 },
		{ -4.0, -3.0, -2.0, -1.0, 0.0, 1.0,	2.0, 3.0, 4.0 },
		{ -4.5, -3.5, -2.5, -1.5, -0.5, 0.5, 1.5, 2.5, 3.5, 4.5 },
		{ -5.0, -4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0 },
		{ -5.5, -4.5, -3.5, -2.5, -1.5, -0.5, 0.5, 1.5, 2.5, 3.5, 4.5, 5.5 },
		{ -6.0, -5.0, -4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0 },
		{ -6.5, -5.5, -4.5, -3.5, -2.5, -1.5, -0.5, 0.5, 1.5, 2.5, 3.5, 4.5, 5.5, 6.5 },
		{ -7.0, -6.0, -5.0, -4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0 },
		{ -7.5, -6.5, -5.5, -4.5, -3.5, -2.5, -1.5, -0.5, 0.5, 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5 },
		{ -8.0, -7.0, -6.0, -5.0, -4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0 },
		{ -8.5, -7.5, -6.5, -5.5, -4.5, -3.5, -2.5, -1.5, -0.5, 0.5, 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5 },
		{ -9.0, -8.0, -7.0, -6.0, -5.0, -4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0 },
		{ -9.5, -8.5, -7.5, -6.5, -5.5, -4.5, -3.5, -2.5, -1.5, -0.5, 0.5, 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5 },
	};

	const float *coeffs = x_k[ ksize ] + 1;
	const float *off = x_o[ ksize ];

	int i;
	float rsum;
	float texel_size_x;
	float texel_size_y;
	float offset[ MAX_FILTER_SIZE ][ 2 ]; // xy

	// texel size
	texel_size_x = 1.0 / (float) width;
	texel_size_y = 1.0 / (float) height;
	rsum = x_k[ ksize ][ 0 ];

	if ( old_ksize != ksize ) {
		old_ksize = ksize;
		for ( i = 0; i < ksize; i++ ) {
			weight[i] = coeffs[i] * rsum;
		}
	}

	// calculate texture offsets for lookup
	for ( i = 0; i < ksize; i++ ) {
		offset[i][0] = texel_size_x * off[i];
		offset[i][1] = texel_size_y * off[i];
	}

	if ( horizontal ) {
		// horizontal pass
		for (  i = 0; i < ksize; i++ )
			qglProgramLocalParameter4fARB( GL_FRAGMENT_PROGRAM_ARB, i, offset[i][0], 0.0, 0.0, weight[i] );
	} else {
		// vertical pass
		for (  i = 0; i < ksize; i++ )
			qglProgramLocalParameter4fARB( GL_FRAGMENT_PROGRAM_ARB, i, 0.0, offset[i][1], 0.0, weight[i] );
	}
}


static void ARB_DeletePrograms( void )
{
	qglDeleteProgramsARB( ARRAY_LEN( programs ) - PROGRAM_BASE, programs + PROGRAM_BASE );
	Com_Memset( programs, 0, sizeof( programs ) );
	programCompiled = 0;
}


qboolean ARB_CompileProgram( programType ptype, const char *text, GLuint program )
{
	GLint errorPos;
	unsigned int errCode;
	int kind;

	if ( ptype == Fragment )
		kind = GL_FRAGMENT_PROGRAM_ARB;
	else
		kind = GL_VERTEX_PROGRAM_ARB;

	qglBindProgramARB( kind, program );
	qglProgramStringARB( kind, GL_PROGRAM_FORMAT_ASCII_ARB, strlen( text ), text );
	qglGetIntegerv( GL_PROGRAM_ERROR_POSITION_ARB, &errorPos );
	if ( (errCode = qglGetError()) != GL_NO_ERROR || errorPos != -1 )
	{
		// we may receive error with active FBO but compiled programs will continue to work properly
		if ( (errCode == GL_INVALID_OPERATION && !fboEnabled) || errorPos != -1 )
		{
			ri.Printf( PRINT_ALL, S_COLOR_YELLOW "%s Compile Error(%i,%i): %s\n" S_COLOR_CYAN "%s\n", (ptype == Fragment) ? "FP" : "VP",
				errCode, errorPos, qglGetString( GL_PROGRAM_ERROR_STRING_ARB ), text );
			qglBindProgramARB( kind, 0 );
			ARB_DeletePrograms();
			return qfalse;
		}
	}

	return qtrue;
}


qboolean ARB_UpdatePrograms( void )
{
#ifdef USE_PMLIGHT
	const char *program;
	int i;
#endif
	char buf[4096];

	if ( !qglGenProgramsARB )
		return qfalse;

	if ( programCompiled ) // delete old programs
	{
		ARB_ProgramDisable();
		ARB_DeletePrograms();
	}

	qglGenProgramsARB( ARRAY_LEN( programs ) - PROGRAM_BASE, programs + PROGRAM_BASE );

#ifdef USE_PMLIGHT
	if ( !ARB_CompileProgram( Vertex, va( dlightVP, "" ), programs[ DLIGHT_VERTEX ] ) )
		return qfalse;
	if ( !ARB_CompileProgram( Vertex, va( dlightVP, fogInVPCode ), programs[ DLIGHT_VERTEX_FOG_IN ] ) )
		return qfalse;
	if ( !ARB_CompileProgram( Vertex, va( dlightVP, fogOutVPCode ), programs[ DLIGHT_VERTEX_FOG_OUT ] ) )
		return qfalse;

	for ( i = DLIGHT_FRAGMENT; i <= DLIGHT_LINEAR_ABS_FRAGMENT_FOG; i++ ) {
		program = ARB_BuildDlightFP( buf, i );
		if ( !ARB_CompileProgram( Fragment, program, programs[ i ] ) ) {
			return qfalse;
		}
	}
#endif // USE_PMLIGHT

	if ( !ARB_CompileProgram( Vertex, dummyVP, programs[ DUMMY_VERTEX ] ) )
		return qfalse;

	if ( !ARB_CompileProgram( Fragment, spriteFP, programs[ SPRITE_FRAGMENT ] ) )
		return qfalse;

	if ( !ARB_CompileProgram( Fragment, va( gammaFP, ARB_BuildGreyscaleProgram( buf ) ), programs[ GAMMA_FRAGMENT ] ) )
		return qfalse;

	if ( !ARB_CompileProgram( Fragment, ARB_BuildBloomProgram( buf ), programs[ BLOOM_EXTRACT_FRAGMENT ] ) )
		return qfalse;
	
	// only 1, 2, 3, 6, 8, 10, 12, 14, 16, 18 and 20 produces real visual difference
	fboBloomFilterSize = r_bloom_filter_size->integer;
	if ( !ARB_CompileProgram( Fragment, ARB_BuildBlurProgram( buf, fboBloomFilterSize ), programs[ BLUR_FRAGMENT ] ) )
		return qfalse;

	if ( !ARB_CompileProgram( Fragment, ARB_BuildBlurProgram( buf, 6 ), programs[ BLUR2_FRAGMENT ] ) )
		return qfalse;

	fboBloomBlendBase = r_bloom_blend_base->integer;
	if ( !ARB_CompileProgram( Fragment, ARB_BuildBlendProgram( buf, r_bloom_passes->integer - fboBloomBlendBase ), programs[ BLENDX_FRAGMENT ] ) )
		return qfalse;

	if ( !ARB_CompileProgram( Fragment, blend2FP, programs[ BLEND2_FRAGMENT ] ) )
		return qfalse;

	if ( !ARB_CompileProgram( Fragment, va( blend2gammaFP, ARB_BuildGreyscaleProgram( buf ) ), programs[ BLEND2_GAMMA_FRAGMENT ] ) )
		return qfalse;

	programCompiled = 1;

	return qtrue;
}

static void FBO_Bind( GLuint target, GLuint buffer );

void FBO_Clean( frameBuffer_t *fb )
{
	if ( fb->fbo )
	{
		FBO_Bind( GL_FRAMEBUFFER, fb->fbo );
		if ( fb->multiSampled )
		{
			qglBindRenderbuffer( GL_RENDERBUFFER, 0 );
			if ( fb->color )
			{
				qglDeleteRenderbuffers( 1, &fb->color );
				fb->color = 0;
			}
			if ( fb->depthStencil )
			{
				qglDeleteRenderbuffers( 1, &fb->depthStencil );
				fb->depthStencil = 0;
			}
		}
		else
		{
			GL_BindTexture( 0, 0 );
			if ( fb->color )
			{
				qglFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0 );
				qglDeleteTextures( 1, &fb->color );
				fb->color = 0;
			}
			if ( fb->depthStencil )
			{
#ifdef DEPTH_RENDER_BUFFER
				if ( fb->depthStencil && fb->depthStencil != commonDepthStencil )
				{
					qglDeleteRenderbuffers( 1, &fb->depthStencil );
					fb->depthStencil = 0;
				}
#else
				if ( r_stencilbits->integer == 0 )
					qglFramebufferTexture2D( GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, 0, 0 );
				else
					qglFramebufferTexture2D( GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0 );

				if ( fb->depthStencil && fb->depthStencil != commonDepthStencil )
				{
					qglDeleteTextures( 1, &fb->depthStencil );
					fb->depthStencil = 0;
				}
#endif
			}
		}
		FBO_Bind( GL_FRAMEBUFFER, 0 );
		qglDeleteFramebuffers( 1, &fb->fbo );
		fb->fbo = 0;
	}
	Com_Memset( fb, 0, sizeof( *fb ) );
}


static void FBO_CleanBloom( void )
{
	int i;
	for ( i = 0; i < MAX_BLUR_PASSES; i++ )
	{
		FBO_Clean( &frameBuffers[ i * 2 + BLOOM_BASE + 0 ] );
		FBO_Clean( &frameBuffers[ i * 2 + BLOOM_BASE + 1 ] );
	}
}


static void FBO_CleanDepth( void )
{
#ifdef COMMON_DEPTH_STENCIL
	if ( commonDepthStencil )
	{
#ifdef DEPTH_RENDER_BUFFER
		qglDeleteRenderbuffers( 1, &commonDepthStencil );
#else
		GL_BindTexture( 0, 0 );
		qglDeleteTextures( 1, &commonDepthStencil );
#endif
		commonDepthStencil = 0;
	}
#endif
}


static GLuint FBO_CreateDepthTextureOrBuffer( GLsizei width, GLsizei height )
{
#ifdef DEPTH_RENDER_BUFFER
	GLuint buffer;
	qglGenRenderbuffers( 1, &buffer );
	qglBindRenderbuffer( GL_RENDERBUFFER, buffer );
	if ( r_stencilbits->integer == 0 )
		qglRenderbufferStorage( GL_RENDERBUFFER, GL_DEPTH_COMPONENT32, width, height );
	else
		qglRenderbufferStorage( GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width, height );
	return buffer;
#else
	GLuint tex;
	qglGenTextures( 1, &tex );
	GL_BindTexture( 0, tex );
	qglTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );
	qglTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST );
	if ( r_stencilbits->integer == 0 )
		qglTexImage2D( GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32, width, height, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL );
	else
		qglTexImage2D( GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, width, height, 0, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, NULL );
	return tex;
#endif
}


static const char *glDefToStr( GLint define )
{
	#define CASE_STR(x) case (x): return #x
	static int index;
	static char buf[8][32];
	char *s;

	switch ( define )
	{
		// texture formats
		CASE_STR(GL_BGR);
		CASE_STR(GL_BGRA);
		CASE_STR(GL_RGB);
		CASE_STR(GL_RGBA);
		CASE_STR(GL_RGBA4);
		CASE_STR(GL_RGBA8);
		CASE_STR(GL_RGBA12);
		CASE_STR(GL_RGBA16);
		CASE_STR(GL_RGB10_A2);
		CASE_STR(GL_R11F_G11F_B10F);
		// data types
		CASE_STR(GL_BYTE);
		CASE_STR(GL_UNSIGNED_BYTE);
		CASE_STR(GL_SHORT);
		CASE_STR(GL_UNSIGNED_SHORT);
		CASE_STR(GL_INT);
		CASE_STR(GL_UNSIGNED_INT);
		CASE_STR(GL_FLOAT);
		CASE_STR(GL_DOUBLE);
		CASE_STR(GL_UNSIGNED_SHORT_4_4_4_4);
		CASE_STR(GL_UNSIGNED_INT_8_8_8_8);
		CASE_STR(GL_UNSIGNED_INT_10_10_10_2);
		CASE_STR(GL_UNSIGNED_SHORT_4_4_4_4_REV);
		CASE_STR(GL_UNSIGNED_INT_8_8_8_8_REV);
		CASE_STR(GL_UNSIGNED_INT_2_10_10_10_REV);
		CASE_STR(GL_UNSIGNED_NORMALIZED);
		// error codes
		CASE_STR(GL_NO_ERROR);
		CASE_STR(GL_INVALID_ENUM);
		CASE_STR(GL_INVALID_VALUE);
		CASE_STR(GL_INVALID_OPERATION);
		CASE_STR(GL_STACK_OVERFLOW);
		CASE_STR(GL_STACK_UNDERFLOW);
		CASE_STR(GL_OUT_OF_MEMORY);
		// fbo error codes
		CASE_STR(GL_FRAMEBUFFER_COMPLETE);
		CASE_STR(GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT);
		CASE_STR(GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT);
		CASE_STR(GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER);
		CASE_STR(GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER);
		CASE_STR(GL_FRAMEBUFFER_UNSUPPORTED);
	}
	s = buf[ index ]; // to handle multiple invocations as function parameters
	sprintf( s, "0x%04x", define );
	index = ( index + 1 ) & 7;
	return s;
}


static void getPreferredFormatAndType( GLint format, GLint *pFormat, GLint *pType )
{
	GLint preferredFormat;
	GLint preferredType;

	if ( qglGetInternalformativ && gl_version >= 43 ) {
		qglGetInternalformativ( GL_TEXTURE_2D, /*GL_RGBA8*/ format, GL_TEXTURE_IMAGE_FORMAT, 1, &preferredFormat );
		if ( qglGetError() != GL_NO_ERROR ) {
			goto __fallback;
		}
		qglGetInternalformativ( GL_TEXTURE_2D, /*GL_RGBA8*/ format, GL_TEXTURE_IMAGE_TYPE, 1, &preferredType );
		if ( qglGetError() != GL_NO_ERROR ) {
			goto __fallback;
		}
		if ( preferredFormat == 0 ) // nVidia ION drivers can do that
			preferredFormat = GL_RGBA;
		if ( preferredType == GL_UNSIGNED_NORMALIZED ) { // Intel HD 530 drivers can do that as well
			if ( format == GL_RGBA12 || format == GL_RGBA16 )
				preferredType = GL_UNSIGNED_SHORT;
			else
				preferredType = GL_UNSIGNED_BYTE;
		}
	} else {
__fallback:
		if ( format == GL_RGBA12 || format == GL_RGBA16 ) {
			preferredFormat = GL_RGBA;
			preferredType = GL_UNSIGNED_SHORT;
		} else {
			preferredFormat = GL_RGBA;
			preferredType = GL_UNSIGNED_BYTE;
		}
	}

	*pFormat = preferredFormat;
	*pType = preferredType;
}


static qboolean FBO_Create( frameBuffer_t *fb, GLsizei width, GLsizei height, qboolean depthStencil, GLint *outFormat, GLint *outType )
{
	int fboStatus;
	GLint internalFormat;
	GLint textureFormat;
	GLint textureType;

	fb->multiSampled = qfalse;
	fb->depthStencil = 0;

	// color texture
	qglGenTextures( 1, &fb->color );
	GL_BindTexture( 0, fb->color );
	qglTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
	qglTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );

	qglTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, gl_clamp_mode );
	qglTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, gl_clamp_mode );

	// always use GL_RGB10_A2 for bloom textures which is fast as usual GL_RGBA8
	// (GL_R11F_G11F_B10F is a bit slower at least on AMD GPUs)
	// but can provide better precision for blurring, also we barely need more than 10 bits for that,
	// texture formats that doesn't fit into 32bits are just performance-killers for bloom
	if ( fb - frameBuffers >= BLOOM_BASE )
		internalFormat = GL_RGB10_A2;
	else
		internalFormat = fboInternalFormat;

	getPreferredFormatAndType( internalFormat, &textureFormat, &textureType );

	qglTexImage2D( GL_TEXTURE_2D, 0, internalFormat, width, height, 0, textureFormat, textureType, NULL );
	// TODO: handle GL_INVALID_OPERATION in case of unsupported internalFormat/textureFormat
	
	if ( outFormat )
		*outFormat = textureFormat;
	if ( outType )
		*outType = textureType;

	qglGenFramebuffers( 1, &fb->fbo );
	FBO_Bind( GL_FRAMEBUFFER, fb->fbo );
	
	qglFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fb->color, 0 );

	if ( depthStencil )
	{
#ifdef COMMON_DEPTH_STENCIL
		if ( !commonDepthStencil )
			commonDepthStencil = FBO_CreateDepthTextureOrBuffer( width, height );

		fb->depthStencil = commonDepthStencil;
#else
		fb->depthStencil = FBO_CreateDepthTextureOrBuffer( width, height );
#endif
#ifdef DEPTH_RENDER_BUFFER
		if ( r_stencilbits->integer == 0 )
			qglFramebufferRenderbuffer( GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, fb->depthStencil );
		else
			qglFramebufferRenderbuffer( GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, fb->depthStencil );
#else
		if ( r_stencilbits->integer == 0 )
			qglFramebufferTexture2D( GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, fb->depthStencil, 0 );
		else
			qglFramebufferTexture2D( GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, fb->depthStencil, 0 );
#endif
	}

	GL_BindTexture( 0, 0 );

	fboStatus = qglCheckFramebufferStatus( GL_FRAMEBUFFER );
	if ( fboStatus != GL_FRAMEBUFFER_COMPLETE )
	{
		ri.Printf( PRINT_ALL, "Failed to create %s (%s:%s) FBO (status %s, error %s)\n",
			glDefToStr( internalFormat ), glDefToStr( textureFormat ), glDefToStr( textureType ),
			glDefToStr( fboStatus ), glDefToStr( (int)qglGetError() ) );
		FBO_Clean( fb );
		return qfalse;
	}

	fb->width = width;
	fb->height = height;

	qglClearColor( 0.0, 0.0, 0.0, 1.0 );
	if ( depthStencil )
		qglClear( GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT );
	else
		qglClear( GL_COLOR_BUFFER_BIT );

	FBO_Bind( GL_FRAMEBUFFER, 0 );

	return qtrue;
}


static qboolean FBO_CreateMS( frameBuffer_t *fb, int width, int height )
{
	GLsizei nSamples = r_ext_multisample->integer;
	int fboStatus;
	
	fb->multiSampled = qtrue;

	if ( nSamples <= 0 || !qglRenderbufferStorageMultisample )
	{
		return qfalse;
	}
	nSamples = PAD( nSamples, 2 );

	qglGenFramebuffers( 1, &fb->fbo );
	FBO_Bind( GL_FRAMEBUFFER, fb->fbo );

	qglGenRenderbuffers( 1, &fb->color );
	qglBindRenderbuffer( GL_RENDERBUFFER, fb->color );
	while ( nSamples > 0 ) {
		qglRenderbufferStorageMultisample( GL_RENDERBUFFER, nSamples, fboInternalFormat, width, height );
		if ( (int)qglGetError() == GL_INVALID_VALUE/* != GL_NO_ERROR */ ) {
			ri.Printf( PRINT_ALL, "...%ix MSAA is not available\n", nSamples );
			nSamples -= 2;
		} else {
			ri.Printf( PRINT_ALL, "...using %ix MSAA\n", nSamples );
			break;
		}
	}

	if ( nSamples <= 0 )
	{
		FBO_Clean( fb );
		return qfalse;
	}
	qglFramebufferRenderbuffer( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, fb->color );

	qglGenRenderbuffers( 1, &fb->depthStencil );
	qglBindRenderbuffer( GL_RENDERBUFFER, fb->depthStencil );
	if ( r_stencilbits->integer == 0 )
		qglRenderbufferStorageMultisample( GL_RENDERBUFFER, nSamples, GL_DEPTH_COMPONENT32, width, height );
	else
		qglRenderbufferStorageMultisample( GL_RENDERBUFFER, nSamples, GL_DEPTH24_STENCIL8, width, height );

	if ( (int)qglGetError() != GL_NO_ERROR )
	{
		FBO_Clean( fb );
		return qfalse;
	}

	if ( r_stencilbits->integer == 0 )
		qglFramebufferRenderbuffer( GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, fb->depthStencil );
	else
		qglFramebufferRenderbuffer( GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, fb->depthStencil );

	fboStatus = qglCheckFramebufferStatus( GL_FRAMEBUFFER );
	if ( fboStatus != GL_FRAMEBUFFER_COMPLETE )
	{
		ri.Printf( PRINT_WARNING, "Failed to create MS FBO (status %s, error %s)\n", glDefToStr( fboStatus ), glDefToStr( (int)qglGetError() ) );
		FBO_Clean( fb );
		return qfalse;
	}

	fb->width = width;
	fb->height = height;

	qglClearColor( 0.0, 0.0, 0.0, 1.0 );
	qglClear( GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT );

	FBO_Bind( GL_FRAMEBUFFER, 0 );

	return qtrue;
}


static qboolean FBO_CreateBloom( void )
{
	int width = glConfig.vidWidth;
	int height = glConfig.vidHeight;
	int i;

	fboBloomPasses = 0;

	if ( glConfig.numTextureUnits < r_bloom_passes->integer )
	{
		ri.Printf( PRINT_WARNING, "...not enough texture units (%i) for %i-pass bloom\n",
			glConfig.numTextureUnits, r_bloom_passes->integer );
		return qfalse;
	}

	for ( i = 0; i < r_bloom_passes->integer; i++ )
	{
		// we may need depth/stencil buffers for first bloom buffer in \r_bloom 2 mode
		if ( !FBO_Create( &frameBuffers[ i*2 + BLOOM_BASE + 0 ], width, height, i == 0 ? qtrue : qfalse, NULL, NULL ) ||
			!FBO_Create( &frameBuffers[ i*2 + BLOOM_BASE + 1 ], width, height, qfalse, NULL, NULL ) ) {
			return qfalse;
		}
		width = width / 2;
		height = height / 2;
		fboBloomPasses++;
		if ( width < 2 || height < 2 )
			break;
	}

	ri.Printf( PRINT_ALL, "...%i bloom passes\n", fboBloomPasses );

	return qtrue;
}


GLuint FBO_ScreenTexture( void )
{
	return frameBuffers[ 2 ].color;
}


static void FBO_Bind( GLuint target, GLuint buffer )
{
#if 1
	static GLuint draw_buffer = (GLuint)-1;
	static GLuint read_buffer = (GLuint)-1;
	if ( target == GL_FRAMEBUFFER ) {
		if ( draw_buffer != buffer || read_buffer != buffer )
			qglBindFramebuffer( GL_FRAMEBUFFER, buffer );
		draw_buffer = buffer;
		read_buffer = buffer;
	} else {
		if ( target == GL_READ_FRAMEBUFFER ) {
			if ( read_buffer != buffer )
				qglBindFramebuffer( GL_READ_FRAMEBUFFER, buffer );
			read_buffer = buffer;
		} else {
			if ( draw_buffer != buffer )
				qglBindFramebuffer( GL_DRAW_FRAMEBUFFER, buffer );
			draw_buffer = buffer;
		}
	}
#else
	qglBindFramebuffer( target, buffer );
#endif
}


void FBO_BindMain( void )
{
	if ( fboEnabled )
	{
		const frameBuffer_t *fb;
		if ( frameBufferMultiSampling )
		{
			blitMSfbo = qtrue;
			fb = &frameBufferMS;
		}
		else
		{
			blitMSfbo = qfalse;
			fb = &frameBuffers[ 0 ];
		}
		FBO_Bind( GL_FRAMEBUFFER, fb->fbo );
		fboReadIndex = 0;
	}
}


static void FBO_BlitToBackBuffer( int index )
{
	const frameBuffer_t *src = &frameBuffers[ index ];

	FBO_Bind( GL_READ_FRAMEBUFFER, src->fbo );
	FBO_Bind( GL_DRAW_FRAMEBUFFER, 0 );
	//qglReadBuffer( GL_COLOR_ATTACHMENT0 );
	qglDrawBuffer( GL_BACK );

	if ( windowAdjusted )
	{
		if ( blitClear > 0 )
		{
			blitClear--;
			qglClearColor( 0.0, 0.0, 0.0, 1.0 );
			qglClear( GL_COLOR_BUFFER_BIT );
		}
		qglViewport( blitX0, blitY0, blitX1 - blitX0, blitY1 - blitY0 );
		qglScissor( blitX0, blitY0, blitX1 - blitX0, blitY1 - blitY0 );
	}

	qglBlitFramebuffer( 0, 0, src->width, src->height, blitX0, blitY0, blitX1, blitY1, GL_COLOR_BUFFER_BIT, blitFilter );
	fboReadIndex = index;
}


void FBO_BlitSS( void )
{
	const frameBuffer_t *src = &frameBuffers[ fboReadIndex ];
	const frameBuffer_t *dst = &frameBuffers[ 4 ];

	FBO_Bind( GL_DRAW_FRAMEBUFFER, dst->fbo );
	
	qglBlitFramebuffer( 0, 0, src->width, src->height, 0, 0, dst->width, dst->height, GL_COLOR_BUFFER_BIT, GL_LINEAR );

	FBO_Bind( GL_READ_FRAMEBUFFER, dst->fbo );
}


void FBO_BlitMS( qboolean depthOnly )
{
	//if ( blitMSfbo )
	//{
	const int w = glConfig.vidWidth;
	const int h = glConfig.vidHeight;

	const frameBuffer_t *r = &frameBufferMS;
	const frameBuffer_t *d = &frameBuffers[ 0 ];

	fboReadIndex = 0;

	FBO_Bind( GL_READ_FRAMEBUFFER, r->fbo );
	FBO_Bind( GL_DRAW_FRAMEBUFFER, d->fbo );

	if ( depthOnly )
	{
		qglBlitFramebuffer( 0, 0, w, h, 0, 0, w, h, GL_DEPTH_BUFFER_BIT, GL_NEAREST );
		FBO_Bind( GL_READ_FRAMEBUFFER, d->fbo );
		return;
	}

	qglBlitFramebuffer( 0, 0, w, h, 0, 0, w, h, GL_COLOR_BUFFER_BIT, GL_NEAREST );
	// bind all further reads to main buffer
	FBO_Bind( GL_READ_FRAMEBUFFER, d->fbo );
}


static void FBO_Blur( const frameBuffer_t *fb1, const frameBuffer_t *fb2,  const frameBuffer_t *fb3 )
{
	const int w = glConfig.vidWidth;
	const int h = glConfig.vidHeight;

	qglViewport( 0, 0, fb1->width, fb1->height );

	// apply horizontal blur - render from FBO1 to FBO2
	FBO_Bind( GL_DRAW_FRAMEBUFFER, fb2->fbo );
	GL_BindTexture( 0, fb1->color );
	ARB_ProgramEnable( DUMMY_VERTEX, BLUR_FRAGMENT );
	ARB_BlurParams( fb1->width, fb1->height, fboBloomFilterSize, qtrue );
	RenderQuad( w, h );

	// apply vectical blur - render from FBO2 to FBO3
	FBO_Bind( GL_DRAW_FRAMEBUFFER, fb3->fbo );
	GL_BindTexture( 0, fb2->color );
	ARB_BlurParams( fb1->width, fb1->height, fboBloomFilterSize, qfalse );
	RenderQuad( w, h );
}


static void FBO_Blur2( const frameBuffer_t *fb1, const frameBuffer_t *fb2,  const frameBuffer_t *fb3 )
{
	const int w = glConfig.vidWidth;
	const int h = glConfig.vidHeight;

	qglViewport( 0, 0, fb1->width, fb1->height );

	// apply horizontal blur - render from FBO1 to FBO2
	FBO_Bind( GL_DRAW_FRAMEBUFFER, fb2->fbo );
	GL_BindTexture( 0, fb1->color );
	ARB_ProgramEnable( DUMMY_VERTEX, BLUR2_FRAGMENT );
	ARB_BlurParams( fb1->width, fb1->height, 6, qtrue );
	RenderQuad( w, h );

	// apply vectical blur - render from FBO2 to FBO3
	FBO_Bind( GL_DRAW_FRAMEBUFFER, fb3->fbo );
	GL_BindTexture( 0, fb2->color );
	ARB_BlurParams( fb1->width, fb1->height, 6, qfalse );
	RenderQuad( w, h );
}


void FBO_CopyScreen( void )
{
	const frameBuffer_t *dst;
	const frameBuffer_t *src;
	int yCrop;

	qglViewport( 0, 0, glConfig.vidWidth, glConfig.vidHeight );
	qglScissor( 0, 0, glConfig.vidWidth, glConfig.vidHeight );

	// resolve multisample buffer first
	if ( blitMSfbo )
	{
		src = &frameBufferMS;
		dst = &frameBuffers[ 0 ];
		FBO_Bind( GL_READ_FRAMEBUFFER, src->fbo );
		FBO_Bind( GL_DRAW_FRAMEBUFFER, dst->fbo );
		qglBlitFramebuffer( 0, 0, src->width, src->height, 0, 0, dst->width, dst->height, GL_COLOR_BUFFER_BIT, GL_NEAREST );
	}

	src = &frameBuffers[ 0 ];
	dst = &frameBuffers[ 2 ];
	FBO_Bind( GL_READ_FRAMEBUFFER, src->fbo );
	FBO_Bind( GL_DRAW_FRAMEBUFFER, dst->fbo );

	yCrop = backEnd.viewParms.viewportHeight / 4;

	qglBlitFramebuffer( backEnd.viewParms.viewportX, backEnd.viewParms.viewportY + yCrop,
		backEnd.viewParms.viewportWidth + backEnd.viewParms.viewportX,
		backEnd.viewParms.viewportHeight + backEnd.viewParms.viewportY,
		0, 0, dst->width, dst->height, GL_COLOR_BUFFER_BIT, GL_LINEAR );

	//if ( !backEnd.projection2D )
	{
		qglMatrixMode( GL_PROJECTION );
		qglLoadMatrixf( GL_Ortho( 0, glConfig.vidWidth, glConfig.vidHeight, 0, 0, 1 ) );
		qglMatrixMode( GL_MODELVIEW );
		qglLoadIdentity();
		GL_Cull( CT_TWO_SIDED );
		qglDisable( GL_CLIP_PLANE0 );
	}

	qglColor4f( 1, 1, 1, 1 );
	GL_State( GLS_DEPTHTEST_DISABLE | GLS_SRCBLEND_ONE | GLS_DSTBLEND_ZERO );
	FBO_Blur2( dst, dst+1, dst );
	ARB_ProgramDisable();

	//restore viewport and scissor
	qglViewport( backEnd.viewParms.viewportX, backEnd.viewParms.viewportY,
		backEnd.viewParms.viewportWidth, backEnd.viewParms.viewportHeight ); 
	qglScissor( backEnd.viewParms.scissorX, backEnd.viewParms.scissorY,
		backEnd.viewParms.scissorWidth, backEnd.viewParms.scissorHeight ); 

	FBO_BindMain();
}


static void R_Setup_Quad_Lens( float offset, vec4_t color, vec3_t *verts, vec2_t *coords, vec4_t *colors )
{
	static const vec2_t t[6] = { {1.0, 0.0}, {0.0, 0.0}, {0.0, 1.0}, {0.0, 1.0}, {1.0, 1.0}, {1.0, 0.0} };

	const float width = (float)glConfig.vidWidth;
	const float height = (float)glConfig.vidHeight;
	int i;
	
	for ( i = 0; i < 6; i++ ) {
		coords[i][0] = t[i][0];
		coords[i][1] = t[i][1];
		Vector4Copy( color, colors[i] );
		verts[i][2] = 0.0;
	}

	verts[0][0] = -offset;
	verts[0][1] = -offset;

	verts[1][0] = width + offset;
	verts[1][1] = -offset;

	verts[2][0] = width + offset;
	verts[2][1] = height + offset;

	verts[3][0] = width + offset;
	verts[3][1] = height + offset;

	verts[4][0] = -offset;
	verts[4][1] = height + offset;

	verts[5][0] = -offset;
	verts[5][1] = -offset;
}


static void R_Bloom_LensEffect( float alpha )
{
	// lens rainbow colors
	static const GLfloat lc[][3] = {
		{ 0.78f, 0.23f, 0.34f },
		{ 0.78f, 0.39f, 0.21f },
		{ 0.78f, 0.59f, 0.21f },
		{ 0.71f, 0.75f, 0.21f },
		{ 0.52f, 0.78f, 0.21f },
		{ 0.32f, 0.78f, 0.21f },
		{ 0.21f, 0.78f, 0.28f },
		{ 0.21f, 0.78f, 0.47f },
		{ 0.21f, 0.77f, 0.66f },
		{ 0.21f, 0.67f, 0.78f },
		{ 0.21f, 0.47f, 0.78f },
		{ 0.21f, 0.28f, 0.78f },
		{ 0.35f, 0.21f, 0.78f },
		{ 0.53f, 0.21f, 0.78f },
		{ 0.72f, 0.21f, 0.75f },
		{ 0.78f, 0.21f, 0.59f },
	};
	int i;

	vec3_t verts[ ARRAY_LEN(lc) * 6 ];
	vec2_t coords[ ARRAY_LEN(lc) * 6 ];
	vec4_t colors[ ARRAY_LEN(lc) * 6 ];
	vec4_t color;

	alpha /= (float)ARRAY_LEN( lc );
	for ( i = 0; i < ARRAY_LEN( lc ); i++ ) {
		VectorCopy( lc[i], color ); color[3] = alpha;
		R_Setup_Quad_Lens( (i+1)*144, color, &verts[i*6], &coords[i*6], &colors[i*6] );
	}

	GL_ClientState( 0, CLS_TEXCOORD_ARRAY | CLS_COLOR_ARRAY );

	qglVertexPointer( 3, GL_FLOAT, 0, verts );
	qglTexCoordPointer( 2, GL_FLOAT, 0, coords );
	qglColorPointer( 4, GL_FLOAT, 0, colors );

	qglDrawArrays( GL_TRIANGLES, 0, ARRAY_LEN( verts ) );
}


qboolean FBO_Bloom( const float gamma, const float obScale, qboolean finalStage )
{
	const int w = glConfig.vidWidth;
	const int h = glConfig.vidHeight;

	frameBuffer_t *src, *dst;
	int finalBloomFBO;
	int i;

	if ( backEnd.doneBloom || !backEnd.doneSurfaces )
	{
		return qfalse;
	}

	backEnd.doneBloom = qtrue;

	if ( !fboBloomInited )
	{
		if ( (fboBloomInited = FBO_CreateBloom() ) == qfalse )
		{
			ri.Printf( PRINT_WARNING, "...error creating framebuffers for bloom\n" );
			ri.Cvar_Set( "r_bloom", "0" );
			FBO_CleanBloom();
			return qfalse;
		}
		else
		{
			ri.Printf( PRINT_ALL, "...bloom framebuffers created\n" );
		}
	}

	if ( blitMSfbo )
	{
		FBO_BlitMS( qfalse );
		blitMSfbo = qfalse;
	}
	
	// extract intensity from main FBO to BLOOM_BASE
	src = &frameBuffers[ 0 ];
	dst = &frameBuffers[ BLOOM_BASE ];
	FBO_Bind( GL_FRAMEBUFFER, dst->fbo );
	GL_BindTexture( 0, src->color );
	qglViewport( 0, 0, dst->width, dst->height );
	ARB_ProgramEnable( DUMMY_VERTEX, BLOOM_EXTRACT_FRAGMENT );
	qglProgramLocalParameter4fARB( GL_FRAGMENT_PROGRAM_ARB, 0, r_bloom_threshold->value, r_bloom_threshold->value,
		r_bloom_threshold->value, 1.0 );
	RenderQuad( w, h );

	// downscale and blur
	src = frameBuffers + BLOOM_BASE;
	for ( i = 1; i < fboBloomPasses; i++, src+=2 ) {
		dst = src + 2;
		// copy image to next level
#ifdef USE_FBO_BLIT
		FBO_Bind( GL_READ_FRAMEBUFFER, src->fbo );
		FBO_Bind( GL_DRAW_FRAMEBUFFER, dst->fbo );
		qglBlitFramebuffer( 0, 0, src->width, src->height, 0, 0, dst->width, dst->height, GL_COLOR_BUFFER_BIT, GL_LINEAR );
#else
		ARB_ProgramDisable();
		FBO_Bind( GL_FRAMEBUFFER, dst->fbo );
		GL_BindTexture( 0, src->color );
		GL_State( GLS_DEPTHTEST_DISABLE | GLS_SRCBLEND_ONE | GLS_DSTBLEND_ZERO );
		qglViewport( 0, 0, dst->width, dst->height );
		RenderQuad( w, h );
#endif
		FBO_Blur( dst, dst+1, dst );
	}

	// restore viewport
	qglViewport( 0, 0, w, h );

	// blend all bloom buffers to BLOOM_BASE+1 texture
	finalBloomFBO = BLOOM_BASE+1;
	GL_State( GLS_DEPTHTEST_DISABLE | GLS_SRCBLEND_ONE | GLS_DSTBLEND_ZERO );
	FBO_Bind( GL_FRAMEBUFFER, frameBuffers[ finalBloomFBO ].fbo );
	ARB_ProgramEnable( DUMMY_VERTEX, BLENDX_FRAGMENT );
	// setup all texture units
	for ( i = 0; i < fboBloomPasses - fboBloomBlendBase; i++ ) {
		GL_BindTexture( i, frameBuffers[ (i+fboBloomBlendBase)*2 + BLOOM_BASE ].color );
	}
	RenderQuad( w, h );

	if ( r_bloom_reflection->value )
	{
		ARB_ProgramDisable();

		// copy final bloom image to some downscaled buffer
		src = &frameBuffers[ finalBloomFBO ];
		dst = &frameBuffers[ BLOOM_BASE + 2 + 2 ]; // 4x downscale
		FBO_Bind( GL_DRAW_FRAMEBUFFER, dst->fbo );
		FBO_Bind( GL_READ_FRAMEBUFFER, src->fbo );
		qglBlitFramebuffer( 0, 0, src->width, src->height, 0, 0, dst->width, dst->height, GL_COLOR_BUFFER_BIT, GL_LINEAR );
		
		// set render target to paired destination buffer and draw reflections
		FBO_Bind( GL_DRAW_FRAMEBUFFER, (dst+1)->fbo );
		GL_BindTexture( 0, dst->color );
		GL_State( GLS_DEPTHTEST_DISABLE | GLS_SRCBLEND_SRC_ALPHA | GLS_DSTBLEND_ONE );
		qglViewport( 0, 0, dst->width, dst->height );
		R_Bloom_LensEffect( fabs( r_bloom_reflection->value ) );
		
		// restore color and blend mode
		qglColor4f( 1.0f, 1.0f, 1.0f, 1.0f );
		GL_State( GLS_DEPTHTEST_DISABLE | GLS_SRCBLEND_ONE | GLS_DSTBLEND_ZERO );
		
		// blur lens effect in paired buffer
		FBO_Blur( dst+1, dst, dst+1 );
		ARB_ProgramDisable();

		// add lens effect to final bloom buffer
		FBO_Bind( GL_FRAMEBUFFER, src->fbo );
		if ( r_bloom_reflection->value > 0 ) {
			GL_State( GLS_DEPTHTEST_DISABLE | GLS_SRCBLEND_ONE | GLS_DSTBLEND_ONE );
		} else {
			// negative reflection values will replace bloom texture with just lens effect
		}
		qglViewport( 0, 0, w, h );
		GL_BindTexture( 0, (dst+1)->color );
		RenderQuad( w, h );

		// restore blend mode
		GL_State( GLS_DEPTHTEST_DISABLE | GLS_SRCBLEND_ONE | GLS_DSTBLEND_ZERO );
	}

	if ( windowAdjusted || backEnd.screenshotMask ) {
		finalStage = qfalse; // can't blit directly into back buffer in this case
	}

	// if we don't need to read pixels later - blend directly to back buffer
	if ( finalStage ) {
		if ( backEnd.screenshotMask ) {
			FBO_Bind( GL_FRAMEBUFFER, frameBuffers[ BLOOM_BASE ].fbo );
		} else {
			FBO_Bind( GL_FRAMEBUFFER, 0 );
		}
	} else {
		FBO_Bind( GL_FRAMEBUFFER, frameBuffers[ BLOOM_BASE ].fbo );
	}

	GL_BindTexture( 1, frameBuffers[ finalBloomFBO ].color ); // final bloom texture
	GL_BindTexture( 0, frameBuffers[ 0 ].color ); // original image
	if ( finalStage ) {
		// blend & apply gamma in one pass
		ARB_ProgramEnable( DUMMY_VERTEX, BLEND2_GAMMA_FRAGMENT );
		qglProgramLocalParameter4fARB( GL_FRAGMENT_PROGRAM_ARB, 0, gamma, gamma, gamma, obScale );
		qglProgramLocalParameter4fARB( GL_FRAGMENT_PROGRAM_ARB, 1, r_bloom_intensity->value, 0, 0, 0 );
	} else {
		// just blend
		ARB_ProgramEnable( DUMMY_VERTEX, BLEND2_FRAGMENT );
		qglProgramLocalParameter4fARB( GL_FRAGMENT_PROGRAM_ARB, 1, r_bloom_intensity->value, 0, 0, 0 );
	}
	RenderQuad( w, h );
	ARB_ProgramDisable();

	if ( finalStage ) {
		if ( backEnd.screenshotMask ) {
			FBO_BlitToBackBuffer( BLOOM_BASE ); // so any further qglReadPixels() will read from BLOOM_BASE
			 // fboReadIndex = 0;
		} else {
			//	already in back buffer
			fboReadIndex = 0;
		}
	} else {
		// we need depth/stencil buffers there
		fboReadIndex = BLOOM_BASE;
	}

	return finalStage;
}


void R_BloomScreen( void )
{
	if ( r_bloom->integer == 1 && fboEnabled && qglActiveTextureARB )
	{
		if ( !backEnd.doneBloom && backEnd.doneSurfaces )
		{
			if ( !backEnd.projection2D )
				RB_SetGL2D();
			qglColor4f( 1, 1, 1, 1 );
			FBO_Bloom( 0, 0, qfalse );
		}
	}
}


void FBO_PostProcess( void )
{
	const float obScale = 1 << tr.overbrightBits;
	const float gamma = 1.0f / r_gamma->value;
	const float w = glConfig.vidWidth;
	const float h = glConfig.vidHeight;
	qboolean minimized;

	ARB_ProgramDisable();

	if ( !backEnd.projection2D )
	{
		qglViewport( 0, 0, glConfig.vidWidth, glConfig.vidHeight );
		qglScissor( 0, 0, glConfig.vidWidth, glConfig.vidHeight );
		qglMatrixMode( GL_PROJECTION );
		qglLoadMatrixf( GL_Ortho( 0, glConfig.vidWidth, glConfig.vidHeight, 0, 0, 1 ) );
		qglMatrixMode( GL_MODELVIEW );
		qglLoadIdentity();
		backEnd.projection2D = qtrue;
	}

	if ( blitMSfbo )
	{
		FBO_BlitMS( qfalse );
		blitMSfbo = qfalse;
	}

	qglColor4f( 1.0f, 1.0f, 1.0f, 1.0f );
	GL_State( GLS_DEPTHTEST_DISABLE | GLS_SRCBLEND_ONE | GLS_DSTBLEND_ZERO );
	GL_Cull( CT_TWO_SIDED );
	if ( r_anaglyphMode->integer )
		qglColorMask( GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE );

	minimized = ri.CL_IsMinimized();

	if ( r_bloom->integer && programCompiled && qglActiveTextureARB ) {
		if ( FBO_Bloom( gamma, obScale, !minimized ) ) {
			return;
		}
	}

	// check if we can perform final draw directly into back buffer
	if ( backEnd.screenshotMask == 0 && !windowAdjusted && !minimized ) {
		FBO_Bind( GL_FRAMEBUFFER, 0 );
		GL_BindTexture( 0, frameBuffers[ fboReadIndex ].color );
		ARB_ProgramEnable( DUMMY_VERTEX, GAMMA_FRAGMENT );
		qglProgramLocalParameter4fARB( GL_FRAGMENT_PROGRAM_ARB, 0, gamma, gamma, gamma, obScale );
		RenderQuad( w, h );
		ARB_ProgramDisable();
		return;
	}

	// apply gamma shader
	FBO_Bind( GL_FRAMEBUFFER, frameBuffers[ 1 ].fbo ); // destination - secondary buffer
	GL_BindTexture( 0, frameBuffers[ fboReadIndex ].color );  // source - main color buffer
	ARB_ProgramEnable( DUMMY_VERTEX, GAMMA_FRAGMENT );
	qglProgramLocalParameter4fARB( GL_FRAGMENT_PROGRAM_ARB, 0, gamma, gamma, gamma, obScale );
	RenderQuad( w, h );
	ARB_ProgramDisable();

	if ( !minimized ) {
		FBO_BlitToBackBuffer( 1 );
	}
}



void QGL_SetRenderScale( qboolean verbose )
{
	windowAdjusted = qfalse;

	blitX0 = blitY0 = 0;
	blitX1 = gls.windowWidth;
	blitY1 = gls.windowHeight;

	blitFilter = GL_NEAREST;

	superSampled = qfalse;

	if ( !qglGenProgramsARB || !qglGenFramebuffers )
		return;

	if ( !r_fbo->integer )
	{
		if ( verbose && r_renderScale->integer )
		{
			ri.Printf( PRINT_ALL, "...ignoring \r_renderScale due to disabled FBO\n" );
		}
		return;
	}

	if ( r_ext_supersample->integer )
	{
		superSampled = qtrue;
		blitFilter = GL_LINEAR; // default value for (r_renderScale==0) case
	}

	if ( gls.windowWidth != glConfig.vidWidth || gls.windowHeight != glConfig.vidHeight )
	{
		if ( r_renderScale->integer > 0 )
		{
			int scaleMode = r_renderScale->integer - 1;
			if ( scaleMode & 1 )
			{
				// preserve aspect ratio (black bars on sides)
				float windowAspect = (float) gls.windowWidth / (float) gls.windowHeight;
				float renderAspect = (float) glConfig.vidWidth / (float) glConfig.vidHeight;
				if ( windowAspect >= renderAspect ) 
				{
					float scale = (float) gls.windowHeight / ( float ) glConfig.vidHeight;
					int bias = ( gls.windowWidth - scale * (float) glConfig.vidWidth ) / 2;
					blitX0 += bias;
					blitX1 -= bias;
				}
				else
				{
					float scale = (float) gls.windowWidth / ( float ) glConfig.vidWidth;
					int bias = ( gls.windowHeight - scale * (float) glConfig.vidHeight ) / 2;
					blitY0 += bias;
					blitY1 -= bias;
				}
			}
			// linear filtering
			if ( scaleMode & 2 )
				blitFilter = GL_LINEAR;
			else
				blitFilter = GL_NEAREST;
		}

		windowAdjusted = qtrue;
	}
}


void QGL_DoneFBO( void )
{
	if ( qglGenFramebuffers )
	{
		FBO_Bind(GL_FRAMEBUFFER, 0);
		FBO_Clean(&frameBufferMS);
		FBO_Clean(&frameBuffers[0]);
		FBO_Clean(&frameBuffers[1]);
		FBO_Clean(&frameBuffers[2]);
		FBO_Clean(&frameBuffers[3]);
		FBO_Clean(&frameBuffers[4]);
		FBO_CleanBloom();
		FBO_CleanDepth();
		fboEnabled = qfalse;
		fboBloomInited = qfalse;
	}
}


void QGL_InitFBO( void )
{
	int w, h;
	qboolean depthStencil;
	qboolean result = qfalse;

	QGL_DoneFBO();

	w = glConfig.vidWidth;
	h = glConfig.vidHeight;
	
	fboEnabled = qfalse;
	frameBufferMultiSampling = qfalse;

	if ( r_fbo->integer && ( !qglGenProgramsARB || !qglGenFramebuffers ) )
		ri.Printf( PRINT_WARNING, "...FBO is not available\n" );

	if ( !r_fbo->integer || !qglGenProgramsARB || !qglGenFramebuffers )
		return;

	qglGetError(); // reset error code

	if ( windowAdjusted )
		blitClear = 2; // front & back buffers
	else
		blitClear = 0;

	switch ( r_hdr->integer )
	{
		case -1: fboInternalFormat = GL_RGBA4; break;
		case 0: fboInternalFormat = GL_RGBA8; break;
		default: fboInternalFormat = GL_RGBA16; break;
	}

	if ( FBO_CreateMS( &frameBufferMS, w, h ) )
	{
		frameBufferMultiSampling = qtrue;
		if ( r_flares->integer )
			depthStencil = qtrue;
		else
			depthStencil = qfalse;
		result = FBO_Create( &frameBuffers[ 0 ], w, h, depthStencil, &fboTextureFormat, &fboTextureType )
			&& FBO_Create( &frameBuffers[ 1 ], w, h, depthStencil, NULL, NULL )
			&& FBO_Create( &frameBuffers[ 2 ], SCR_WIDTH, SCR_HEIGHT, qfalse, NULL, NULL )
			&& FBO_Create( &frameBuffers[ 3 ], SCR_WIDTH, SCR_HEIGHT, qfalse, NULL, NULL );
		frameBufferMultiSampling = result;
	}
	else
	{
		result = FBO_Create( &frameBuffers[ 0 ], w, h, qtrue, &fboTextureFormat, &fboTextureType )
			&& FBO_Create( &frameBuffers[ 1 ], w, h, qtrue, NULL, NULL )
			&& FBO_Create( &frameBuffers[ 2 ], SCR_WIDTH, SCR_HEIGHT, qfalse, NULL, NULL )
			&& FBO_Create( &frameBuffers[ 3 ], SCR_WIDTH, SCR_HEIGHT, qfalse, NULL, NULL );
	}

	if ( result && superSampled )
	{
		result &= FBO_Create( &frameBuffers[ 4 ], gls.captureWidth, gls.captureHeight, qfalse, NULL, NULL );
	}

	if ( result )
	{
		fboEnabled = qtrue;
		FBO_BindMain();
		ri.Printf( PRINT_ALL, "...using %s (%s:%s) FBO\n", glDefToStr( fboInternalFormat ),
			glDefToStr( fboTextureFormat ), glDefToStr( fboTextureType ) );
	}
	else
	{
		QGL_DoneFBO();
	}
}


void QGL_InitARB( void )
{
	ARB_UpdatePrograms();
	QGL_SetRenderScale( qtrue );
	QGL_InitFBO();
	ri.Cvar_ResetGroup( CVG_RENDERER, qtrue );
}


void QGL_DoneARB( void )
{
	QGL_DoneFBO();

	if ( programCompiled )
	{
		ARB_ProgramDisable();
		ARB_DeletePrograms();
	}
}
