/* =========================================================================== Copyright (C) 1999-2005 Id Software, Inc. This file is part of Quake III Arena source code. Quake III Arena source code is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. Quake III Arena source code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Quake III Arena source code; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA =========================================================================== */ #include "tr_local.h" backEndData_t *backEndData[SMP_FRAMES]; backEndState_t backEnd; void GL_Bind( const image_t* image ) { int texnum; if ( !image ) { ri.Printf( PRINT_WARNING, "GL_Bind: NULL image\n" ); texnum = tr.defaultImage->texnum; } else { texnum = image->texnum; } if ( r_nobind->integer && tr.defaultImage && !backEnd.projection2D ) { texnum = tr.defaultImage->texnum; } if ( glState.texID[glState.currenttmu] != texnum ) { glState.texID[glState.currenttmu] = texnum; qglBindTexture( GL_TEXTURE_2D, texnum ); } } void GL_SelectTexture( int unit ) { if ( glState.currenttmu == unit ) return; if ( unit >= MAX_TMUS ) ri.Error( ERR_DROP, "GL_SelectTexture: unit = %i", unit ); qglActiveTextureARB( GL_TEXTURE0_ARB + unit ); qglClientActiveTextureARB( GL_TEXTURE0_ARB + unit ); glState.currenttmu = unit; } /* ** GL_Cull */ void GL_Cull( int cullType ) { if ( glState.faceCulling == cullType ) { return; } glState.faceCulling = cullType; if ( cullType == CT_TWO_SIDED ) { qglDisable( GL_CULL_FACE ); } else { qglEnable( GL_CULL_FACE ); if ( cullType == CT_BACK_SIDED ) { if ( backEnd.viewParms.isMirror ) { qglCullFace( GL_FRONT ); } else { qglCullFace( GL_BACK ); } } else { if ( backEnd.viewParms.isMirror ) { qglCullFace( GL_BACK ); } else { qglCullFace( GL_FRONT ); } } } } /* ** GL_TexEnv */ void GL_TexEnv( int env ) { if ( env == glState.texEnv[glState.currenttmu] ) { return; } glState.texEnv[glState.currenttmu] = env; switch ( env ) { case GL_MODULATE: case GL_REPLACE: case GL_DECAL: case GL_ADD: qglTexEnvf( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, env ); break; default: ri.Error( ERR_DROP, "GL_TexEnv: invalid env '%d' passed\n", env ); break; } } /* ** GL_State ** ** This routine is responsible for setting the most commonly changed state ** in Q3. */ void GL_State( unsigned long stateBits ) { unsigned long diff = stateBits ^ glState.glStateBits; if ( !diff ) { return; } // // check depthFunc bits // if ( diff & GLS_DEPTHFUNC_EQUAL ) { if ( stateBits & GLS_DEPTHFUNC_EQUAL ) { qglDepthFunc( GL_EQUAL ); } else { qglDepthFunc( GL_LEQUAL ); } } // // check blend bits // if ( diff & ( GLS_SRCBLEND_BITS | GLS_DSTBLEND_BITS ) ) { GLenum srcFactor, dstFactor; if ( stateBits & ( GLS_SRCBLEND_BITS | GLS_DSTBLEND_BITS ) ) { switch ( stateBits & GLS_SRCBLEND_BITS ) { case GLS_SRCBLEND_ZERO: srcFactor = GL_ZERO; break; case GLS_SRCBLEND_ONE: srcFactor = GL_ONE; break; case GLS_SRCBLEND_DST_COLOR: srcFactor = GL_DST_COLOR; break; case GLS_SRCBLEND_ONE_MINUS_DST_COLOR: srcFactor = GL_ONE_MINUS_DST_COLOR; break; case GLS_SRCBLEND_SRC_ALPHA: srcFactor = GL_SRC_ALPHA; break; case GLS_SRCBLEND_ONE_MINUS_SRC_ALPHA: srcFactor = GL_ONE_MINUS_SRC_ALPHA; break; case GLS_SRCBLEND_DST_ALPHA: srcFactor = GL_DST_ALPHA; break; case GLS_SRCBLEND_ONE_MINUS_DST_ALPHA: srcFactor = GL_ONE_MINUS_DST_ALPHA; break; case GLS_SRCBLEND_ALPHA_SATURATE: srcFactor = GL_SRC_ALPHA_SATURATE; break; default: srcFactor = GL_ONE; // to get warning to shut up ri.Error( ERR_DROP, "GL_State: invalid src blend state bits\n" ); break; } switch ( stateBits & GLS_DSTBLEND_BITS ) { case GLS_DSTBLEND_ZERO: dstFactor = GL_ZERO; break; case GLS_DSTBLEND_ONE: dstFactor = GL_ONE; break; case GLS_DSTBLEND_SRC_COLOR: dstFactor = GL_SRC_COLOR; break; case GLS_DSTBLEND_ONE_MINUS_SRC_COLOR: dstFactor = GL_ONE_MINUS_SRC_COLOR; break; case GLS_DSTBLEND_SRC_ALPHA: dstFactor = GL_SRC_ALPHA; break; case GLS_DSTBLEND_ONE_MINUS_SRC_ALPHA: dstFactor = GL_ONE_MINUS_SRC_ALPHA; break; case GLS_DSTBLEND_DST_ALPHA: dstFactor = GL_DST_ALPHA; break; case GLS_DSTBLEND_ONE_MINUS_DST_ALPHA: dstFactor = GL_ONE_MINUS_DST_ALPHA; break; default: dstFactor = GL_ONE; // to get warning to shut up ri.Error( ERR_DROP, "GL_State: invalid dst blend state bits\n" ); break; } qglEnable( GL_BLEND ); qglBlendFunc( srcFactor, dstFactor ); } else { qglDisable( GL_BLEND ); } } // // check depthmask // if ( diff & GLS_DEPTHMASK_TRUE ) { if ( stateBits & GLS_DEPTHMASK_TRUE ) { qglDepthMask( GL_TRUE ); } else { qglDepthMask( GL_FALSE ); } } // // fill/line mode // if ( diff & GLS_POLYMODE_LINE ) { if ( stateBits & GLS_POLYMODE_LINE ) { qglPolygonMode( GL_FRONT_AND_BACK, GL_LINE ); } else { qglPolygonMode( GL_FRONT_AND_BACK, GL_FILL ); } } // // depthtest // if ( diff & GLS_DEPTHTEST_DISABLE ) { if ( stateBits & GLS_DEPTHTEST_DISABLE ) { qglDisable( GL_DEPTH_TEST ); } else { qglEnable( GL_DEPTH_TEST ); } } // // alpha test // if ( diff & GLS_ATEST_BITS ) { switch ( stateBits & GLS_ATEST_BITS ) { case 0: qglDisable( GL_ALPHA_TEST ); break; case GLS_ATEST_GT_0: qglEnable( GL_ALPHA_TEST ); qglAlphaFunc( GL_GREATER, 0.0f ); break; case GLS_ATEST_LT_80: qglEnable( GL_ALPHA_TEST ); qglAlphaFunc( GL_LESS, 0.5f ); break; case GLS_ATEST_GE_80: qglEnable( GL_ALPHA_TEST ); qglAlphaFunc( GL_GEQUAL, 0.5f ); break; default: assert( 0 ); break; } } glState.glStateBits = stateBits; } // a player has predicted a teleport, but hasn't arrived yet static void RB_Hyperspace() { float c = 0.25 + 0.5 * sin( M_PI * (backEnd.refdef.time & 0x01FF) / 0x0200 ); qglClearColor( c, c, c, 1 ); qglClear( GL_COLOR_BUFFER_BIT ); } static void SetViewportAndScissor() { qglMatrixMode(GL_PROJECTION); qglLoadMatrixf( backEnd.viewParms.projectionMatrix ); qglMatrixMode(GL_MODELVIEW); // set the window clipping qglViewport( backEnd.viewParms.viewportX, backEnd.viewParms.viewportY, backEnd.viewParms.viewportWidth, backEnd.viewParms.viewportHeight ); qglScissor( backEnd.viewParms.viewportX, backEnd.viewParms.viewportY, backEnd.viewParms.viewportWidth, backEnd.viewParms.viewportHeight ); } // any mirrored or portaled views have already been drawn // so prepare to actually render the visible surfaces for this view static void RB_BeginDrawingView() { int clearBits = 0; // sync with gl if needed if ( r_finish->integer == 1 && !glState.finishCalled ) { qglFinish(); glState.finishCalled = qtrue; } if ( r_finish->integer == 0 ) { glState.finishCalled = qtrue; } // we will need to change the projection matrix before drawing 2D images again backEnd.projection2D = qfalse; backEnd.pc = backEnd.pc3D; // // set the modelview matrix for the viewer // SetViewportAndScissor(); // ensures that depth writes are enabled for the depth clear GL_State( GLS_DEFAULT ); // clear relevant buffers clearBits = GL_DEPTH_BUFFER_BIT; if ( r_measureOverdraw->integer ) { clearBits |= GL_STENCIL_BUFFER_BIT; } if ( r_fastsky->integer && !( backEnd.refdef.rdflags & RDF_NOWORLDMODEL ) ) { clearBits |= GL_COLOR_BUFFER_BIT; // tr.sunLight could have colored fastsky properly for the last 9 years, // ... if the code had actually been right >:( but, it's a bad idea to trust mappers anyway qglClearColor( 0.0f, 0.0f, 0.0f, 1.0f ); } qglClear( clearBits ); if ( backEnd.refdef.rdflags & RDF_HYPERSPACE ) { RB_Hyperspace(); return; } glState.faceCulling = -1; // force face culling to set next time // clip to the plane of the portal if ( backEnd.viewParms.isPortal ) { float plane[4]; double plane2[4]; plane[0] = backEnd.viewParms.portalPlane.normal[0]; plane[1] = backEnd.viewParms.portalPlane.normal[1]; plane[2] = backEnd.viewParms.portalPlane.normal[2]; plane[3] = backEnd.viewParms.portalPlane.dist; plane2[0] = DotProduct (backEnd.viewParms.orient.axis[0], plane); plane2[1] = DotProduct (backEnd.viewParms.orient.axis[1], plane); plane2[2] = DotProduct (backEnd.viewParms.orient.axis[2], plane); plane2[3] = DotProduct (plane, backEnd.viewParms.orient.origin) - plane[3]; qglLoadMatrixf( s_flipMatrix ); qglClipPlane (GL_CLIP_PLANE0, plane2); qglEnable (GL_CLIP_PLANE0); } else { qglDisable (GL_CLIP_PLANE0); } } static void RB_RenderDrawSurfList( const drawSurf_t* drawSurfs, int numDrawSurfs ) { int i; const shader_t* shader = NULL; unsigned int sort = (unsigned int)-1; // save original time for entity shader offsets double originalTime = backEnd.refdef.floatTime; // clear the z buffer, set the modelview, etc RB_BeginDrawingView(); // draw everything int oldEntityNum = -1; backEnd.currentEntity = &tr.worldEntity; qbool oldDepthRange = qfalse; qbool depthRange = qfalse; backEnd.pc[RB_SURFACES] += numDrawSurfs; const drawSurf_t* drawSurf; for ( i = 0, drawSurf = drawSurfs; i < numDrawSurfs; ++i, ++drawSurf ) { if ( drawSurf->sort == sort ) { // fast path, same as previous sort rb_surfaceTable[ *drawSurf->surface ]( drawSurf->surface ); continue; } int fogNum; const shader_t* shaderPrev = shader; int entityNum; R_DecomposeSort( drawSurf->sort, &entityNum, &shader, &fogNum ); // "entityMergable" shaders can have surfaces from multiple refentities // merged into a single batch, like (CONCEPTUALLY) smoke and blood puff sprites // only legacy code still uses them though, because refents are so heavyweight: // modern code just billboards in cgame and submits raw polys, all of which are // ENTITYNUM_WORLD and thus automatically take the "same sort" fast path if ( !shader->entityMergable || ((sort ^ drawSurf->sort) & ~QSORT_ENTITYNUM_MASK) ) { if (shaderPrev) RB_EndSurface(); RB_BeginSurface( shader, fogNum ); } sort = drawSurf->sort; // // change the modelview matrix if needed // if ( entityNum != oldEntityNum ) { depthRange = qfalse; if ( entityNum != ENTITYNUM_WORLD ) { backEnd.currentEntity = &backEnd.refdef.entities[entityNum]; if (backEnd.currentEntity->intShaderTime) backEnd.refdef.floatTime = originalTime - (double)(backEnd.currentEntity->e.shaderTime.iShaderTime) / 1000.0; else backEnd.refdef.floatTime = originalTime - backEnd.currentEntity->e.shaderTime.fShaderTime; // we have to reset the shaderTime as well otherwise image animations start // from the wrong frame tess.shaderTime = backEnd.refdef.floatTime - tess.shader->timeOffset; // set up the transformation matrix R_RotateForEntity( backEnd.currentEntity, &backEnd.viewParms, &backEnd.orient ); if ( backEnd.currentEntity->e.renderfx & RF_DEPTHHACK ) { // hack the depth range to prevent view model from poking into walls depthRange = qtrue; } } else { backEnd.currentEntity = &tr.worldEntity; backEnd.refdef.floatTime = originalTime; backEnd.orient = backEnd.viewParms.world; // we have to reset the shaderTime as well otherwise image animations on // the world (like water) continue with the wrong frame tess.shaderTime = backEnd.refdef.floatTime - tess.shader->timeOffset; } qglLoadMatrixf( backEnd.orient.modelMatrix ); // // change depthrange if needed // if ( oldDepthRange != depthRange ) { if ( depthRange ) { qglDepthRange( 0, 0.3 ); } else { qglDepthRange( 0, 1 ); } oldDepthRange = depthRange; } oldEntityNum = entityNum; } // add the triangles for this surface rb_surfaceTable[ *drawSurf->surface ]( drawSurf->surface ); } backEnd.refdef.floatTime = originalTime; // draw the contents of the last shader batch if (shader) { RB_EndSurface(); } // go back to the world modelview matrix qglLoadMatrixf( backEnd.viewParms.world.modelMatrix ); if ( depthRange ) { qglDepthRange( 0, 1 ); } // add light flares on lights that aren't obscured RB_RenderFlares(); } static void RB_RenderLitSurfList( dlight_t* dl ) { const shader_t* shader = NULL; int entityNum, oldEntityNum; qbool depthRange, oldDepthRange; unsigned int sort = (unsigned int)-1; // save original time for entity shader offsets double originalTime = backEnd.refdef.floatTime; //int litsurfs = backEnd.pc[RB_LIT_SURFACES]; // draw everything oldEntityNum = -1; backEnd.currentEntity = &tr.worldEntity; oldDepthRange = qfalse; depthRange = qfalse; for ( litSurf_t* litSurf = dl->head; litSurf; litSurf = litSurf->next ) { ++backEnd.pc[RB_LIT_SURFACES]; if ( litSurf->sort == sort ) { // fast path, same as previous sort rb_surfaceTable[ *litSurf->surface ]( litSurf->surface ); continue; } int fogNum; const shader_t* shaderPrev = shader; R_DecomposeSort( litSurf->sort, &entityNum, &shader, &fogNum ); // anything BEFORE opaque is sky/portal, anything AFTER it should never have been added //assert( shader->sort == SS_OPAQUE ); // !!! but MIRRORS can trip that assert, so just do this for now if ( shader->sort < SS_OPAQUE ) continue; if (shaderPrev) RB_EndSurface(); RB_BeginSurface( shader, fogNum ); sort = litSurf->sort; // // change the modelview matrix if needed // if ( entityNum != oldEntityNum ) { depthRange = qfalse; if ( entityNum != ENTITYNUM_WORLD ) { backEnd.currentEntity = &backEnd.refdef.entities[entityNum]; if (backEnd.currentEntity->intShaderTime) backEnd.refdef.floatTime = originalTime - (double)backEnd.currentEntity->e.shaderTime.iShaderTime; else backEnd.refdef.floatTime = originalTime - backEnd.currentEntity->e.shaderTime.fShaderTime; // we have to reset the shaderTime as well otherwise image animations start // from the wrong frame tess.shaderTime = backEnd.refdef.floatTime - tess.shader->timeOffset; // set up the transformation matrix R_RotateForEntity( backEnd.currentEntity, &backEnd.viewParms, &backEnd.orient ); R_TransformDlights( 1, dl, &backEnd.orient ); GL2_DynLights_SetupLight(); if ( backEnd.currentEntity->e.renderfx & RF_DEPTHHACK ) { // hack the depth range to prevent view model from poking into walls depthRange = qtrue; } } else { backEnd.currentEntity = &tr.worldEntity; backEnd.refdef.floatTime = originalTime; backEnd.orient = backEnd.viewParms.world; // we have to reset the shaderTime as well otherwise image animations on // the world (like water) continue with the wrong frame tess.shaderTime = backEnd.refdef.floatTime - tess.shader->timeOffset; R_TransformDlights( 1, dl, &backEnd.orient ); GL2_DynLights_SetupLight(); } qglLoadMatrixf( backEnd.orient.modelMatrix ); // // change depthrange if needed // if ( oldDepthRange != depthRange ) { if ( depthRange ) { qglDepthRange( 0, 0.3 ); } else { qglDepthRange( 0, 1 ); } oldDepthRange = depthRange; } oldEntityNum = entityNum; } // add the triangles for this surface rb_surfaceTable[ *litSurf->surface ]( litSurf->surface ); } backEnd.refdef.floatTime = originalTime; // draw the contents of the last shader batch if (shader) { RB_EndSurface(); } // go back to the world modelview matrix qglLoadMatrixf( backEnd.viewParms.world.modelMatrix ); if ( depthRange ) { qglDepthRange( 0, 1 ); } /* if ( r_speeds->integer ) { ri.Printf( PRINT_ALL, "light %d: %d surfaces\n", dl - backEnd.refdef.dlights, backEnd.pc[RB_LIT_SURFACES] - litsurfs ); } */ } /* ============================================================================ RENDER BACK END THREAD FUNCTIONS ============================================================================ */ static void RB_SetGL2D() { backEnd.projection2D = qtrue; backEnd.pc = backEnd.pc2D; // set 2D virtual screen size qglViewport( 0, 0, glConfig.vidWidth, glConfig.vidHeight ); qglScissor( 0, 0, glConfig.vidWidth, glConfig.vidHeight ); qglMatrixMode(GL_PROJECTION); qglLoadIdentity(); qglOrtho( 0, glConfig.vidWidth, glConfig.vidHeight, 0, 0, 1 ); qglMatrixMode(GL_MODELVIEW); qglLoadIdentity(); GL_State( GLS_DEPTHTEST_DISABLE | GLS_SRCBLEND_SRC_ALPHA | GLS_DSTBLEND_ONE_MINUS_SRC_ALPHA ); qglDisable( GL_CULL_FACE ); qglDisable( GL_CLIP_PLANE0 ); // set time for 2D shaders backEnd.refdef.time = ri.Milliseconds(); backEnd.refdef.floatTime = (double)backEnd.refdef.time / 1000.0; } /* ============= RE_StretchRaw FIXME: not exactly backend Stretches a raw 32 bit power of 2 bitmap image over the given screen rectangle. Used for cinematics. ============= */ void RE_StretchRaw (int x, int y, int w, int h, int cols, int rows, const byte *data, int client, qbool dirty) { int i, j; int start, end; if ( !tr.registered ) { return; } R_SyncRenderThread(); // we definately want to sync every frame for the cinematics qglFinish(); start = end = 0; if ( r_speeds->integer ) { start = ri.Milliseconds(); } // make sure rows and cols are powers of 2 for ( i = 0 ; ( 1 << i ) < cols ; i++ ) { } for ( j = 0 ; ( 1 << j ) < rows ; j++ ) { } if ( ( 1 << i ) != cols || ( 1 << j ) != rows) { ri.Error (ERR_DROP, "Draw_StretchRaw: size not a power of 2: %i by %i", cols, rows); } GL_Bind( tr.scratchImage[client] ); // if the scratchImage isn't in the format we want, specify it as a new texture if ( cols != tr.scratchImage[client]->width || rows != tr.scratchImage[client]->height ) { tr.scratchImage[client]->width = cols; tr.scratchImage[client]->height = rows; qglTexImage2D( GL_TEXTURE_2D, 0, GL_RGB8, cols, rows, 0, GL_RGBA, GL_UNSIGNED_BYTE, data ); qglTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR ); qglTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR ); qglTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP ); qglTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP ); } else { if (dirty) { // otherwise, just subimage upload it so that drivers can tell we are going to be changing // it and don't try and do a texture compression qglTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, cols, rows, GL_RGBA, GL_UNSIGNED_BYTE, data ); } } if ( r_speeds->integer ) { end = ri.Milliseconds(); ri.Printf( PRINT_ALL, "qglTexSubImage2D %i, %i: %i msec\n", cols, rows, end - start ); } RB_SetGL2D(); qglColor3f( tr.identityLight, tr.identityLight, tr.identityLight ); qglBegin (GL_QUADS); qglTexCoord2f ( 0.5f / cols, 0.5f / rows ); qglVertex2f (x, y); qglTexCoord2f ( ( cols - 0.5f ) / cols , 0.5f / rows ); qglVertex2f (x+w, y); qglTexCoord2f ( ( cols - 0.5f ) / cols, ( rows - 0.5f ) / rows ); qglVertex2f (x+w, y+h); qglTexCoord2f ( 0.5f / cols, ( rows - 0.5f ) / rows ); qglVertex2f (x, y+h); qglEnd (); } void RE_UploadCinematic (int w, int h, int cols, int rows, const byte *data, int client, qbool dirty) { GL_Bind( tr.scratchImage[client] ); // if the scratchImage isn't in the format we want, specify it as a new texture if ( cols != tr.scratchImage[client]->width || rows != tr.scratchImage[client]->height ) { tr.scratchImage[client]->width = cols; tr.scratchImage[client]->height = rows; qglTexImage2D( GL_TEXTURE_2D, 0, GL_RGB8, cols, rows, 0, GL_RGBA, GL_UNSIGNED_BYTE, data ); qglTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR ); qglTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR ); qglTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP ); qglTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP ); } else { if (dirty) { // otherwise, just subimage upload it so that drivers can tell we are going to be changing // it and don't try and do a texture compression qglTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, cols, rows, GL_RGBA, GL_UNSIGNED_BYTE, data ); } } } static const void* RB_SetColor( const void* data ) { const setColorCommand_t* cmd = (const setColorCommand_t*)data; backEnd.color2D[0] = (byte)(cmd->color[0] * 255); backEnd.color2D[1] = (byte)(cmd->color[1] * 255); backEnd.color2D[2] = (byte)(cmd->color[2] * 255); backEnd.color2D[3] = (byte)(cmd->color[3] * 255); return (const void*)(cmd + 1); } static const void* RB_StretchPic( const void* data ) { const stretchPicCommand_t* cmd = (const stretchPicCommand_t*)data; if ( !backEnd.projection2D ) RB_SetGL2D(); const shader_t* shader = cmd->shader; if ( shader != tess.shader ) { if ( tess.numIndexes ) { RB_EndSurface(); } backEnd.currentEntity = &backEnd.entity2D; RB_BeginSurface( shader, 0 ); } RB_CHECKOVERFLOW( 4, 6 ); int numVerts = tess.numVertexes; int numIndexes = tess.numIndexes; tess.numVertexes += 4; tess.numIndexes += 6; tess.indexes[ numIndexes ] = numVerts + 3; tess.indexes[ numIndexes + 1 ] = numVerts + 0; tess.indexes[ numIndexes + 2 ] = numVerts + 2; tess.indexes[ numIndexes + 3 ] = numVerts + 2; tess.indexes[ numIndexes + 4 ] = numVerts + 0; tess.indexes[ numIndexes + 5 ] = numVerts + 1; *(int *)tess.vertexColors[ numVerts ] = *(int *)tess.vertexColors[ numVerts + 1 ] = *(int *)tess.vertexColors[ numVerts + 2 ] = *(int *)tess.vertexColors[ numVerts + 3 ] = *(int *)backEnd.color2D; tess.xyz[ numVerts ][0] = cmd->x; tess.xyz[ numVerts ][1] = cmd->y; tess.xyz[ numVerts ][2] = 0; tess.texCoords[ numVerts ][0][0] = cmd->s1; tess.texCoords[ numVerts ][0][1] = cmd->t1; tess.xyz[ numVerts + 1 ][0] = cmd->x + cmd->w; tess.xyz[ numVerts + 1 ][1] = cmd->y; tess.xyz[ numVerts + 1 ][2] = 0; tess.texCoords[ numVerts + 1 ][0][0] = cmd->s2; tess.texCoords[ numVerts + 1 ][0][1] = cmd->t1; tess.xyz[ numVerts + 2 ][0] = cmd->x + cmd->w; tess.xyz[ numVerts + 2 ][1] = cmd->y + cmd->h; tess.xyz[ numVerts + 2 ][2] = 0; tess.texCoords[ numVerts + 2 ][0][0] = cmd->s2; tess.texCoords[ numVerts + 2 ][0][1] = cmd->t2; tess.xyz[ numVerts + 3 ][0] = cmd->x; tess.xyz[ numVerts + 3 ][1] = cmd->y + cmd->h; tess.xyz[ numVerts + 3 ][2] = 0; tess.texCoords[ numVerts + 3 ][0][0] = cmd->s1; tess.texCoords[ numVerts + 3 ][0][1] = cmd->t2; return (const void*)(cmd + 1); } static const void* RB_DrawSurfs( const void* data ) { const drawSurfsCommand_t* cmd = (const drawSurfsCommand_t*)data; // finish any 2D drawing if needed if ( tess.numIndexes ) RB_EndSurface(); backEnd.refdef = cmd->refdef; backEnd.viewParms = cmd->viewParms; tess.pass = shaderCommands_t::TP_BASE; RB_RenderDrawSurfList( cmd->drawSurfs, cmd->numDrawSurfs ); tess.pass = shaderCommands_t::TP_LIGHT; if ( r_measureOverdraw->integer == 2 ) { // see exactly how many fragments are touched by lighting qglClear( GL_STENCIL_BUFFER_BIT ); qglStencilOp( GL_KEEP, GL_KEEP, GL_INCR ); } for ( int i = 0; i < backEnd.refdef.num_dlights; ++i ) { dlight_t* dl = &backEnd.refdef.dlights[i]; if (!dl->head) continue; tess.light = dl; RB_RenderLitSurfList( dl ); } tess.pass = shaderCommands_t::TP_BASE; return (const void*)(cmd + 1); } static const void* RB_BeginFrame( const void* data ) { const beginFrameCommand_t* cmd = (const beginFrameCommand_t*)data; GL2_BeginFrame(); // clear screen for debugging if ( r_clear->integer ) { qglClearColor( 1, 0, 0.5, 1 ); qglClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT ); } return (const void*)(cmd + 1); } /* draw all the images to the screen, on top of whatever was there this was used to test for texture thrashing in 1999, but is useless now also called by RE_EndRegistration to touch them for residency */ void RB_ShowImages() { if ( !backEnd.projection2D ) RB_SetGL2D(); qglClear( GL_COLOR_BUFFER_BIT ); qglFinish(); int start = ri.Milliseconds(); for (int i = 0; i < tr.numImages; ++i) { const image_t* image = tr.images[i]; float w = glConfig.vidWidth / 20; float h = glConfig.vidHeight / 15; float x = i % 20 * w; float y = i / 20 * h; // show in proportional size in mode 2 if ( r_showImages->integer == 2 ) { w *= image->width / 512.0f; h *= image->height / 512.0f; } GL_Bind( image ); qglBegin( GL_QUADS ); qglTexCoord2f( 0, 0 ); qglVertex2f( x, y ); qglTexCoord2f( 1, 0 ); qglVertex2f( x + w, y ); qglTexCoord2f( 1, 1 ); qglVertex2f( x + w, y + h ); qglTexCoord2f( 0, 1 ); qglVertex2f( x, y + h ); qglEnd(); } qglFinish(); ri.Printf( PRINT_ALL, "%i msec to draw all images\n", ri.Milliseconds() - start ); } static const void* RB_SwapBuffers( const void* data ) { // finish any 2D drawing if needed if ( tess.numIndexes ) { RB_EndSurface(); } // texture swapping test if ( r_showImages->integer ) { RB_ShowImages(); } const swapBuffersCommand_t* cmd = (const swapBuffersCommand_t*)data; // we measure overdraw by reading back the stencil buffer and // counting up the number of increments that have happened if ( r_measureOverdraw->integer ) { RI_AutoPtr stencilReadback( glConfig.vidWidth * glConfig.vidHeight ); qglReadPixels( 0, 0, glConfig.vidWidth, glConfig.vidHeight, GL_STENCIL_INDEX, GL_UNSIGNED_BYTE, stencilReadback ); /* long sum = 0; for (int i = 0; i < glConfig.vidWidth * glConfig.vidHeight; ++i) sum += stencilReadback[i]; backEnd.pc.c_overDraw += sum; */ // convert the stencil data into human-comprehensible colors RI_AutoPtr stencilRGBA( glConfig.vidWidth * glConfig.vidHeight * 4 ); for (int i = 0; i < glConfig.vidWidth * glConfig.vidHeight; ++i) { unsigned* p = stencilRGBA.Get() + i; switch ( stencilReadback[i] ) { default: *p = 0xFFFFFFFF; break; case 0: *p = LittleLong( 0xFF000000 ); break; case 1: *p = LittleLong( 0xFF0000FF ); break; case 2: *p = LittleLong( 0xFF00FF00 ); break; case 3: *p = LittleLong( 0xFF00FFFF ); break; case 4: *p = LittleLong( 0xFFFF0000 ); break; case 5: *p = LittleLong( 0xFFFF00FF ); break; case 6: *p = LittleLong( 0xFFFFFF00 ); break; } } RB_SetGL2D(); qglDisable( GL_TEXTURE_2D ); qglRasterPos3f( 0, glConfig.vidHeight, 0 ); qglDrawPixels( glConfig.vidWidth, glConfig.vidHeight, GL_RGBA, GL_UNSIGNED_BYTE, stencilRGBA ); qglEnable( GL_TEXTURE_2D ); } if ( !backEnd.projection2D ) RB_SetGL2D(); GL2_EndFrame(); if ( !glState.finishCalled ) { qglFinish(); } GLimp_EndFrame(); backEnd.projection2D = qfalse; backEnd.pc = backEnd.pc3D; return (const void*)(cmd + 1); } /* ==================== RB_ExecuteRenderCommands This function will be called synchronously if running without smp extensions, or asynchronously by another thread. ==================== */ void RB_ExecuteRenderCommands( const void *data ) { int ms = ri.Milliseconds(); #ifdef USE_R_SMP if ( !r_smp->integer || data == backEndData[0]->commands.cmds ) { backEnd.smpFrame = 0; } else { backEnd.smpFrame = 1; } #else backEnd.smpFrame = 0; #endif while ( 1 ) { data = PADP(data, sizeof(void *)); switch ( *(const int *)data ) { case RC_SET_COLOR: data = RB_SetColor( data ); break; case RC_STRETCH_PIC: data = RB_StretchPic( data ); break; case RC_DRAW_SURFS: data = RB_DrawSurfs( data ); break; case RC_BEGIN_FRAME: data = RB_BeginFrame( data ); break; case RC_SWAP_BUFFERS: data = RB_SwapBuffers( data ); break; case RC_SCREENSHOT: data = RB_TakeScreenshotCmd( (const screenshotCommand_t*)data ); break; case RC_VIDEOFRAME: data = RB_TakeVideoFrameCmd( data ); break; case RC_END_OF_LIST: default: // stop rendering on this thread // we can't really "charge" 2D/3D properly, so it all counts as 3D backEnd.pc3D[RB_MSEC] = ri.Milliseconds() - ms; return; } } } /* ================ RB_RenderThread ================ */ void RB_RenderThread( void ) { const void *data; // wait for either a rendering command or a quit command while ( 1 ) { // sleep until we have work to do data = GLimp_RendererSleep(); if ( !data ) { return; // all done, renderer is shutting down } renderThreadActive = qtrue; RB_ExecuteRenderCommands( data ); renderThreadActive = qfalse; } }