#include "global.h" /* ----------------------------------------------------------------------------- Class: RageDisplay Desc: Methods common to all RageDisplays Copyright (c) 2001-2002 by the person(s) listed below. All rights reserved. Chris Danford Glenn Maynard ----------------------------------------------------------------------------- */ #include "RageDisplay.h" #include "RageTimer.h" #include "RageLog.h" #include "RageMath.h" #include "RageUtil.h" #include "GameConstantsAndTypes.h" #include "SDL_utils.h" // // Statistics stuff // RageTimer g_LastCheckTimer; int g_iNumVerts; int g_iFPS, g_iVPF, g_iCFPS; int RageDisplay::GetFPS() const { return g_iFPS; } int RageDisplay::GetVPF() const { return g_iVPF; } int RageDisplay::GetCumFPS() const { return g_iCFPS; } static int g_iFramesRenderedSinceLastCheck, g_iFramesRenderedSinceLastReset, g_iVertsRenderedSinceLastCheck, g_iNumChecksSinceLastReset; RageDisplay* DISPLAY = NULL; CString RageDisplay::PixelFormatToString( PixelFormat pixfmt ) { const CString s[NUM_PIX_FORMATS] = { "FMT_RGBA8", "FMT_RGBA4", "FMT_RGB5A1", "FMT_RGB5", "FMT_RGB8", "FMT_PAL" }; return s[pixfmt]; }; // Return true if device was re-created and we need to reload textures. bool RageDisplay::SetVideoMode( VideoModeParams p ) { /* Round to the nearest valid fullscreen resolution */ if( !p.windowed ) { if( p.width <= 320 ) p.width = 320; else if( p.width <= 400 ) p.width = 400; else if( p.width <= 512 ) p.width = 512; else if( p.width <= 640 ) p.width = 640; else if( p.width <= 800 ) p.width = 800; else if( p.width <= 1024 ) p.width = 1024; else if( p.width <= 1280 ) p.width = 1280; switch( p.width ) { case 320: p.height = 240; break; case 400: p.height = 300; break; case 512: p.height = 384; break; case 640: p.height = 480; break; case 800: p.height = 600; break; case 1024: p.height = 768; break; case 1280: p.height = 960; break; default: ASSERT(0); } } bool bNeedReloadTextures; CString err; err = this->TryVideoMode(p,bNeedReloadTextures); if( err == "" ) return bNeedReloadTextures; // fall back p.windowed = false; if( this->TryVideoMode(p,bNeedReloadTextures) == "" ) return bNeedReloadTextures; p.bpp = 16; if( this->TryVideoMode(p,bNeedReloadTextures) == "" ) return bNeedReloadTextures; p.width = 640; p.height = 480; if( this->TryVideoMode(p,bNeedReloadTextures) == "" ) return bNeedReloadTextures; RageException::ThrowNonfatal( "SetVideoMode failed: %s", err.c_str() ); } void RageDisplay::ProcessStatsOnFlip() { g_iFramesRenderedSinceLastCheck++; g_iFramesRenderedSinceLastReset++; if( g_LastCheckTimer.PeekDeltaTime() >= 1.0f ) // update stats every 1 sec. { g_LastCheckTimer.GetDeltaTime(); g_iNumChecksSinceLastReset++; g_iFPS = g_iFramesRenderedSinceLastCheck; g_iCFPS = g_iFramesRenderedSinceLastReset / g_iNumChecksSinceLastReset; g_iVPF = g_iVertsRenderedSinceLastCheck / g_iFPS; g_iFramesRenderedSinceLastCheck = g_iVertsRenderedSinceLastCheck = 0; LOG->Trace( "FPS: %d, CFPS %d, VPF: %d", g_iFPS, g_iCFPS, g_iVPF ); } } void RageDisplay::ResetStats() { g_iFPS = g_iVPF = 0; g_iFramesRenderedSinceLastCheck = g_iFramesRenderedSinceLastReset = 0; g_iNumChecksSinceLastReset = 0; g_iVertsRenderedSinceLastCheck = 0; g_LastCheckTimer.GetDeltaTime(); } void RageDisplay::StatsAddVerts( int iNumVertsRendered ) { g_iVertsRenderedSinceLastCheck += iNumVertsRendered; } /* Draw a line as a quad. GL_LINES with antialiasing off can draw line * ends at odd angles--they're forced to axis-alignment regardless of the * angle of the line. */ void RageDisplay::DrawPolyLine(const RageSpriteVertex &p1, const RageSpriteVertex &p2, float LineWidth ) { /* soh cah toa strikes strikes again! */ float opp = p2.p.x - p1.p.x; float adj = p2.p.y - p1.p.y; float hyp = powf(opp*opp + adj*adj, 0.5f); float lsin = opp/hyp; float lcos = adj/hyp; RageSpriteVertex v[4]; v[0] = v[1] = p1; v[2] = v[3] = p2; float ydist = lsin * LineWidth/2; float xdist = lcos * LineWidth/2; v[0].p.x += xdist; v[0].p.y -= ydist; v[1].p.x -= xdist; v[1].p.y += ydist; v[2].p.x -= xdist; v[2].p.y += ydist; v[3].p.x += xdist; v[3].p.y -= ydist; this->DrawQuad(v); } void RageDisplay::DrawLineStrip( const RageSpriteVertex v[], int iNumVerts, float LineWidth ) { ASSERT( iNumVerts >= 2 ); /* Draw a line strip with rounded corners using polys. This is used on * cards that have strange allergic reactions to antialiased points and * lines. */ int i; for(i = 0; i < iNumVerts-1; ++i) DrawPolyLine(v[i], v[i+1], LineWidth); /* Join the lines with circles so we get rounded corners. */ for(i = 0; i < iNumVerts; ++i) DrawCircle( v[i], LineWidth/2 ); } void RageDisplay::DrawCircle( const RageSpriteVertex &p, float radius ) { const int subdivisions = 32; RageSpriteVertex v[subdivisions+2]; v[0] = p; for(int i = 0; i < subdivisions+1; ++i) { const float fRotation = float(i) / subdivisions * 2*PI; const float fX = cosf(fRotation) * radius; const float fY = -sinf(fRotation) * radius; v[1+i] = v[0]; v[1+i].p.x += fX; v[1+i].p.y += fY; } this->DrawFan( v, subdivisions+2 ); } void RageDisplay::SetDefaultRenderStates() { SetLighting( false ); SetBackfaceCull( false ); SetZBuffer( false ); SetAlphaTest( true ); SetBlendMode( BLEND_NORMAL ); SetTextureFiltering( true ); LoadMenuPerspective(0, CENTER_X, CENTER_Y); // 0 FOV = ortho } // // Matrix stuff // class MatrixStack { vector stack; public: MatrixStack() { stack.resize(1); LoadIdentity(); } // Pops the top of the stack. void Pop() { stack.pop_back(); ASSERT( stack.size() > 0 ); // underflow } // Pushes the stack by one, duplicating the current matrix. void Push() { stack.push_back( stack.back() ); ASSERT( stack.size() < 100 ); // overflow } // Loads identity in the current matrix. void LoadIdentity() { RageMatrixIdentity( &stack.back() ); } // Loads the given matrix into the current matrix void LoadMatrix( const RageMatrix& m ) { stack.back() = m; } // Right-Multiplies the given matrix to the current matrix. // (transformation is about the current world origin) void MultMatrix( const RageMatrix& m ) { RageMatrixMultiply( &stack.back(), &m, &stack.back() ); } // Left-Multiplies the given matrix to the current matrix // (transformation is about the local origin of the object) void MultMatrixLocal( const RageMatrix& m ) { RageMatrixMultiply( &stack.back(), &stack.back(), &m ); } // Right multiply the current matrix with the computed rotation // matrix, counterclockwise about the given axis with the given angle. // (rotation is about the current world origin) void RotateX( float degrees ) { RageMatrix m; RageMatrixRotationX( &m, degrees ); MultMatrix( m ); } void RotateY( float degrees ) { RageMatrix m; RageMatrixRotationY( &m, degrees ); MultMatrix( m ); } void RotateZ( float degrees ) { RageMatrix m; RageMatrixRotationZ( &m, degrees ); MultMatrix( m ); } // Left multiply the current matrix with the computed rotation // matrix. All angles are counterclockwise. (rotation is about the // local origin of the object) void RotateXLocal( float degrees ) { RageMatrix m; RageMatrixRotationX( &m, degrees ); MultMatrixLocal( m ); } void RotateYLocal( float degrees ) { RageMatrix m; RageMatrixRotationY( &m, degrees ); MultMatrixLocal( m ); } void RotateZLocal( float degrees ) { RageMatrix m; RageMatrixRotationZ( &m, degrees ); MultMatrixLocal( m ); } // Right multiply the current matrix with the computed scale // matrix. (transformation is about the current world origin) void Scale( float x, float y, float z) { RageMatrix m; RageMatrixScaling( &m, x, y, z ); MultMatrix( m ); } // Left multiply the current matrix with the computed scale // matrix. (transformation is about the local origin of the object) void ScaleLocal( float x, float y, float z) { RageMatrix m; RageMatrixScaling( &m, x, y, z ); MultMatrixLocal( m ); } // Right multiply the current matrix with the computed translation // matrix. (transformation is about the current world origin) void Translate( float x, float y, float z) { RageMatrix m; RageMatrixTranslation( &m, x, y, z ); MultMatrix( m ); } // Left multiply the current matrix with the computed translation // matrix. (transformation is about the local origin of the object) void TranslateLocal( float x, float y, float z) { RageMatrix m; RageMatrixTranslation( &m, x, y, z ); MultMatrixLocal( m ); } // Obtain the current matrix at the top of the stack const RageMatrix* GetTop() { return &stack.back(); } }; MatrixStack g_ProjectionStack; MatrixStack g_ViewStack; MatrixStack g_WorldStack; const RageMatrix* RageDisplay::GetProjectionTop() { return g_ProjectionStack.GetTop(); } const RageMatrix* RageDisplay::GetViewTop() { return g_ViewStack.GetTop(); } const RageMatrix* RageDisplay::GetWorldTop() { return g_WorldStack.GetTop(); } void RageDisplay::PushMatrix() { g_WorldStack.Push(); } void RageDisplay::PopMatrix() { g_WorldStack.Pop(); } void RageDisplay::Translate( float x, float y, float z ) { g_WorldStack.TranslateLocal(x, y, z); } void RageDisplay::TranslateWorld( float x, float y, float z ) { g_WorldStack.Translate(x, y, z); } void RageDisplay::Scale( float x, float y, float z ) { g_WorldStack.ScaleLocal(x, y, z); } void RageDisplay::RotateX( float deg ) { g_WorldStack.RotateXLocal( deg ); } void RageDisplay::RotateY( float deg ) { g_WorldStack.RotateYLocal( deg ); } void RageDisplay::RotateZ( float deg ) { g_WorldStack.RotateZLocal( deg ); } void RageDisplay::PostMultMatrix( const RageMatrix &m ) { g_WorldStack.MultMatrix( m ); } void RageDisplay::PreMultMatrix( const RageMatrix &m ) { g_WorldStack.MultMatrixLocal( m ); } void RageDisplay::LoadIdentity() { g_WorldStack.LoadIdentity(); } void RageDisplay::LoadMenuPerspective( float fovDegrees, float fVanishPointX, float fVanishPointY ) { /* fovDegrees == 0 looks the same as an ortho projection. However, * we don't want to mess with the ModelView stack because * EnterPerspectiveMode's preserve location feature expectes there * not to be any camera transforms. So, do a true ortho projection * if fovDegrees == 0. Perhaps it would be more convenient to keep * separate model and view stacks like D3D? */ if( fovDegrees == 0 ) { float left = 0, right = SCREEN_WIDTH, bottom = SCREEN_HEIGHT, top = 0; g_ProjectionStack.LoadMatrix( GetOrthoMatrix(left, right, bottom, top, SCREEN_NEAR, SCREEN_FAR) ); g_ViewStack.LoadIdentity(); } else { CLAMP( fovDegrees, 0.1f, 179.9f ); float fovRadians = fovDegrees / 180.f * PI; float theta = fovRadians/2; float fDistCameraFromImage = SCREEN_WIDTH/2 / tanf( theta ); fVanishPointX = SCALE( fVanishPointX, SCREEN_LEFT, SCREEN_RIGHT, SCREEN_RIGHT, SCREEN_LEFT ); fVanishPointY = SCALE( fVanishPointY, SCREEN_TOP, SCREEN_BOTTOM, SCREEN_BOTTOM, SCREEN_TOP ); fVanishPointX -= CENTER_X; fVanishPointY -= CENTER_Y; /* It's the caller's responsibility to push first. */ g_ProjectionStack.LoadMatrix( GetFrustumMatrix( (fVanishPointX-SCREEN_WIDTH/2)/fDistCameraFromImage, (fVanishPointX+SCREEN_WIDTH/2)/fDistCameraFromImage, (fVanishPointY+SCREEN_HEIGHT/2)/fDistCameraFromImage, (fVanishPointY-SCREEN_HEIGHT/2)/fDistCameraFromImage, 1, fDistCameraFromImage+1000 ) ); g_ViewStack.LoadMatrix( RageLookAt( -fVanishPointX+CENTER_X, -fVanishPointY+CENTER_Y, fDistCameraFromImage, -fVanishPointX+CENTER_X, -fVanishPointY+CENTER_Y, 0, 0.0f, 1.0f, 0.0f) ); } } /* Switch from orthogonal to perspective view. * * Tricky: we want to maintain all of the zooms, rotations and translations * that have been applied already. They're in our internal screen space (that * is, 640x480 ortho). We can't simply leave them where they are, since they'll * be applied before the perspective transform, which means they'll be in the * wrong coordinate space. * * Handle translations (the right column of the transform matrix) to the viewport. * Move rotations and scaling (0,0 through 1,1) to after the perspective transform. * Actually, those might be able to stay where they are, I'm not sure; it's translations * that are annoying. So, XXX: see if rots and scales can be left on the modelview * matrix instead of messing with the projection matrix. * * When finished, the current position will be the "viewpoint" (at 0,0). negative * Z goes into the screen, positive X and Y is right and down. */ //void RageDisplay::EnterPerspective(float fov, bool preserve_loc, float near_clip, float far_clip) //{ // g_ProjectionStack.Push(); // g_WorldStack.Push(); // // float aspect = SCREEN_WIDTH/(float)SCREEN_HEIGHT; // g_ProjectionStack.LoadMatrix( GetPerspectiveMatrix(fov, aspect, near_clip, far_clip) ); // /* Flip the Y coordinate, so positive numbers go down. */ // g_ProjectionStack.Scale(1, -1, 1); // // if( preserve_loc ) // { // //RageMatrix matTop = *g_WorldStack.GetTop(); /* TODO: Come up with a more general way to handle this. * It looks kind of hacky. -Chris */ // { // /* Pull out the 2d translation. */ // float x = matTop.m[3][0], y = matTop.m[3][1]; // // /* These values are where the viewpoint should be. By default, it's in the // * center of the screen, and these values are from the top-left, so subtract // * the difference. */ // x -= SCREEN_WIDTH/2; // y -= SCREEN_HEIGHT/2; // SetViewport(int(x), int(y)); // } // // /* Pull out the 2d rotations and scales. */ // { // RageMatrix mat; // RageMatrixIdentity(&mat); // mat.m[0][0] = matTop.m[0][0]; // mat.m[0][1] = matTop.m[0][1]; // mat.m[1][0] = matTop.m[1][0]; // mat.m[1][1] = matTop.m[1][1]; // this->MultMatrix(mat); // } // // /* We can't cope with perspective matrices or things that touch Z. (We shouldn't // * have touched those while in 2d, anyway.) */ // ASSERT(matTop.m[0][2] == 0.f && matTop.m[0][3] == 0.f && matTop.m[1][2] == 0.f && // matTop.m[1][3] == 0.f && matTop.m[2][0] == 0.f && matTop.m[2][1] == 0.f && // matTop.m[2][2] == 1.f && matTop.m[3][2] == 0.f && matTop.m[3][3] == 1.f); // // /* Reset the matrix back to identity. */ // glMatrixMode( GL_MODELVIEW ); // glLoadIdentity(); // } //} void RageDisplay::CameraPushMatrix() { g_ProjectionStack.Push(); g_ViewStack.Push(); } void RageDisplay::CameraPopMatrix() { g_ProjectionStack.Pop(); g_ViewStack.Pop(); } /* gluLookAt. The result is pre-multiplied to the matrix (M = L * M) instead of * post-multiplied. */ void RageDisplay::LoadLookAt(float fov, const RageVector3 &Eye, const RageVector3 &At, const RageVector3 &Up) { float aspect = SCREEN_WIDTH/(float)SCREEN_HEIGHT; g_ProjectionStack.LoadMatrix( GetPerspectiveMatrix(fov, aspect, 1, 1000) ); /* Flip the Y coordinate, so positive numbers go down. */ g_ProjectionStack.Scale(1, -1, 1); g_ViewStack.LoadMatrix(RageLookAt(Eye.x, Eye.y, Eye.z, At.x, At.y, At.z, Up.x, Up.y, Up.z)); } RageMatrix RageDisplay::GetPerspectiveMatrix(float fovy, float aspect, float zNear, float zFar) { float ymax = zNear * tanf(fovy * PI / 360.0f); float ymin = -ymax; float xmin = ymin * aspect; float xmax = ymax * aspect; return GetFrustumMatrix(xmin, xmax, ymin, ymax, zNear, zFar); } SDL_Surface *RageDisplay::CreateSurfaceFromPixfmt( PixelFormat pixfmt, void *pixels, int width, int height, int pitch ) { const PixelFormatDesc *tpf = GetPixelFormatDesc(pixfmt); SDL_Surface *surf = SDL_CreateRGBSurfaceFrom( pixels, width, height, tpf->bpp, pitch, tpf->masks[0], tpf->masks[1], tpf->masks[2], tpf->masks[3]); return surf; } RageDisplay::PixelFormat RageDisplay::FindPixelFormat( int bpp, int Rmask, int Gmask, int Bmask, int Amask ) { PixelFormatDesc tmp = { bpp, Rmask, Gmask, Bmask, Amask }; for(int pixfmt = 0; pixfmt < NUM_PIX_FORMATS; ++pixfmt) { const PixelFormatDesc *pf = GetPixelFormatDesc(PixelFormat(pixfmt)); if(!SupportsTextureFormat( PixelFormat(pixfmt) )) continue; if(memcmp(pf, &tmp, sizeof(tmp))) continue; return PixelFormat(pixfmt); } return NUM_PIX_FORMATS; } RageMatrix RageDisplay::GetFrustumMatrix( float l, float r, float b, float t, float zn, float zf ) { // glFrustrum float A = (r+l) / (r-l); float B = (t+b) / (t-b); float C = -1 * (zf+zn) / (zf-zn); float D = -1 * (2*zf*zn) / (zf-zn); RageMatrix m( 2*zn/(r-l), 0, 0, 0, 0, 2*zn/(t-b), 0, 0, A, B, C, -1, 0, 0, D, 0 ); return m; }