Files
bearstonem--openmohaa-vr/code/client/cl_scrn.cpp
T
bearstonem d9968210aa fix(vr): correct visual glitches and reduce rendering overhead
Restore desktop HUD behavior and objective state, fix stereo text lifetimes, HUD placement and colors, sprite culling, and font shader passes. Handle swapchain timeouts and incomplete stereo frames safely, remove forced GPU waits, and correct rendering timing attribution.

Track client-game source additions in CMake. Validated desktop and Android GL1/GL2 builds plus targeted regression and sanitizer checks. Headset appearance and performance remain unverified.
2026-10-14 14:48:22 -07:00

663 lines
15 KiB
C++

/*
===========================================================================
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
===========================================================================
*/
// cl_scrn.c -- master for refresh, status bar, console, chat, notify, etc
#include "client.h"
#include "cl_ui.h"
#ifdef USE_OPENXR
#include "../vr/vr_common.h"
#endif
qboolean scr_initialized; // ready to draw
stereoFrame_t s_scr_stereoFrame;
vrDrawMode_t cl_vrDrawMode = VRDRAW_ALL;
cvar_t *cl_timegraph;
cvar_t *cl_debuggraph;
cvar_t *cl_graphheight;
cvar_t *cl_graphscale;
cvar_t *cl_graphshift;
/*
================
SCR_DrawNamedPic
Coordinates are 640*480 virtual values
=================
*/
void SCR_DrawNamedPic( float x, float y, float width, float height, const char *picname ) {
qhandle_t hShader;
assert( width != 0 );
hShader = re.RegisterShader( picname );
SCR_AdjustFrom640( &x, &y, &width, &height );
re.DrawStretchPic( x, y, width, height, 0, 0, 1, 1, hShader );
}
/*
================
SCR_AdjustFrom640
Adjusted for resolution and screen aspect ratio
================
*/
void SCR_AdjustFrom640( float *x, float *y, float *w, float *h ) {
float xscale;
float yscale;
#if 0
// adjust for wide screens
if ( cls.glconfig.vidWidth * 480 > cls.glconfig.vidHeight * 640 ) {
*x += 0.5 * ( cls.glconfig.vidWidth - ( cls.glconfig.vidHeight * 640 / 480 ) );
}
#endif
// scale for screen sizes
xscale = cls.glconfig.vidWidth / 640.0;
yscale = cls.glconfig.vidHeight / 480.0;
if ( x ) {
*x *= xscale;
}
if ( y ) {
*y *= yscale;
}
if ( w ) {
*w *= xscale;
}
if ( h ) {
*h *= yscale;
}
}
/*
================
SCR_FillRect
Coordinates are 640*480 virtual values
=================
*/
void SCR_FillRect( float x, float y, float width, float height, const float *color ) {
re.SetColor( color );
SCR_AdjustFrom640( &x, &y, &width, &height );
re.DrawStretchPic( x, y, width, height, 0, 0, 0, 0, cls.whiteShader );
re.SetColor( NULL );
}
/*
================
SCR_DrawPic
Coordinates are 640*480 virtual values
=================
*/
void SCR_DrawPic( float x, float y, float width, float height, qhandle_t hShader ) {
SCR_AdjustFrom640( &x, &y, &width, &height );
re.DrawStretchPic( x, y, width, height, 0, 0, 1, 1, hShader );
}
/*
** SCR_DrawChar
** chars are drawn at 640*480 virtual screen size
*/
static void SCR_DrawChar( int x, int y, float size, int ch ) {
int row, col;
float frow, fcol;
float ax, ay, aw, ah;
ch &= 255;
if ( ch == ' ' ) {
return;
}
if ( y < -size ) {
return;
}
ax = x;
ay = y;
aw = size;
ah = size;
SCR_AdjustFrom640( &ax, &ay, &aw, &ah );
row = ch>>4;
col = ch&15;
frow = row*0.0625;
fcol = col*0.0625;
size = 0.0625;
re.DrawStretchPic( ax, ay, aw, ah,
fcol, frow,
fcol + size, frow + size,
cls.charSetShader );
}
/*
** SCR_DrawSmallChar
** small chars are drawn at native screen resolution
*/
void SCR_DrawSmallChar( int x, int y, int ch ) {
int row, col;
float frow, fcol;
float size;
ch &= 255;
if ( ch == ' ' ) {
return;
}
if ( y < -SMALLCHAR_HEIGHT ) {
return;
}
row = ch>>4;
col = ch&15;
frow = row*0.0625;
fcol = col*0.0625;
size = 0.0625;
re.DrawStretchPic( x, y, SMALLCHAR_WIDTH, SMALLCHAR_HEIGHT,
fcol, frow,
fcol + size, frow + size,
cls.charSetShader );
}
/*
==================
SCR_DrawBigString[Color]
Draws a multi-colored string with a drop shadow, optionally forcing
to a fixed color.
Coordinates are at 640 by 480 virtual resolution
==================
*/
void SCR_DrawStringExt( int x, int y, float size, const char *string, float *setColor, qboolean forceColor,
qboolean noColorEscape ) {
vec4_t color;
const char *s;
int xx;
// draw the drop shadow
color[0] = color[1] = color[2] = 0;
color[3] = setColor[3];
re.SetColor( color );
s = string;
xx = x;
while ( *s ) {
if ( !noColorEscape && Q_IsColorString( s ) ) {
s += 2;
continue;
}
SCR_DrawChar( xx+2, y+2, size, *s );
xx += size;
s++;
}
// draw the colored text
s = string;
xx = x;
re.SetColor( setColor );
while ( *s ) {
if ( Q_IsColorString( s ) ) {
if ( !forceColor ) {
Com_Memcpy( color, g_color_table[ColorIndex(*(s+1))], sizeof( color ) );
color[3] = setColor[3];
re.SetColor( color );
}
if ( !noColorEscape ) {
s += 2;
continue;
}
}
SCR_DrawChar( xx, y, size, *s );
xx += size;
s++;
}
re.SetColor( NULL );
}
void SCR_DrawBigString( int x, int y, const char *s, float alpha, qboolean noColorEscape ) {
float color[4];
color[0] = color[1] = color[2] = 1.0;
color[3] = alpha;
SCR_DrawStringExt( x, y, BIGCHAR_WIDTH, s, color, qfalse, noColorEscape );
}
void SCR_DrawBigStringColor( int x, int y, const char *s, vec4_t color, qboolean noColorEscape ) {
SCR_DrawStringExt( x, y, BIGCHAR_WIDTH, s, color, qtrue, noColorEscape );
}
/*
==================
SCR_DrawSmallString[Color]
Draws a multi-colored string with a drop shadow, optionally forcing
to a fixed color.
==================
*/
void SCR_DrawSmallStringExt( int x, int y, const char *string, float *setColor, qboolean forceColor,
qboolean noColorEscape ) {
vec4_t color;
const char *s;
int xx;
// draw the colored text
s = string;
xx = x;
re.SetColor( setColor );
while ( *s ) {
if ( Q_IsColorString( s ) ) {
if ( !forceColor ) {
Com_Memcpy( color, g_color_table[ColorIndex(*(s+1))], sizeof( color ) );
color[3] = setColor[3];
re.SetColor( color );
}
if ( !noColorEscape ) {
s += 2;
continue;
}
}
SCR_DrawSmallChar( xx, y, *s );
xx += SMALLCHAR_WIDTH;
s++;
}
re.SetColor( NULL );
}
/*
** SCR_Strlen -- skips color escape codes
*/
static int SCR_Strlen( const char *str ) {
const char *s = str;
int count = 0;
while ( *s ) {
if ( Q_IsColorString( s ) ) {
s += 2;
} else {
count++;
s++;
}
}
return count;
}
/*
** SCR_GetBigStringWidth
*/
int SCR_GetBigStringWidth( const char *str ) {
return SCR_Strlen( str ) * BIGCHAR_WIDTH;
}
//===============================================================================
/*
=================
SCR_DrawDemoRecording
=================
*/
void SCR_DrawDemoRecording( void ) {
char string[1024];
int pos;
if ( !clc.demorecording ) {
return;
}
if ( clc.spDemoRecording ) {
return;
}
pos = FS_FTell( clc.demofile );
Com_sprintf( string, sizeof( string ), "RECORDING %s: %ik", clc.demoName, pos / 1024 );
SCR_DrawStringExt( 320 - strlen( string ) * 4, 20, 8, string, g_color_table[7], qtrue, qfalse );
}
/*
===============================================================================
DEBUG GRAPH
===============================================================================
*/
static int current;
static float values[1024];
/*
==============
SCR_DebugGraph
==============
*/
void SCR_DebugGraph (float value)
{
values[current] = value;
current = (current + 1) % ARRAY_LEN(values);
}
/*
==============
SCR_DrawDebugGraph
==============
*/
void SCR_DrawDebugGraph (void)
{
int a, x, y, w, i, h;
float v;
//
// draw the graph
//
w = cls.glconfig.vidWidth;
x = 0;
y = cls.glconfig.vidHeight;
re.SetColor( g_color_table[0] );
re.DrawStretchPic(x, y - cl_graphheight->integer,
w, cl_graphheight->integer, 0, 0, 0, 0, cls.whiteShader );
re.SetColor( NULL );
for (a=0 ; a<w ; a++)
{
i = (ARRAY_LEN(values)+current-1-(a % ARRAY_LEN(values))) % ARRAY_LEN(values);
v = values[i];
v = v * cl_graphscale->integer + cl_graphshift->integer;
if (v < 0)
v += cl_graphheight->integer * (1+(int)(-v / cl_graphheight->integer));
h = (int)v % cl_graphheight->integer;
re.DrawStretchPic( x+w-1-a, y - h, 1, h, 0, 0, 0, 0, cls.whiteShader );
}
}
//=============================================================================
/*
==================
SCR_Init
==================
*/
void SCR_Init( void ) {
cl_timegraph = Cvar_Get ("timegraph", "0", CVAR_CHEAT);
cl_debuggraph = Cvar_Get ("debuggraph", "0", CVAR_CHEAT);
cl_graphheight = Cvar_Get ("graphheight", "32", CVAR_CHEAT);
cl_graphscale = Cvar_Get ("graphscale", "1", CVAR_CHEAT);
cl_graphshift = Cvar_Get ("graphshift", "0", CVAR_CHEAT);
scr_initialized = qtrue;
}
//=======================================================
/*
==================
SCR_DrawScreenField
This will be called twice if rendering in stereo mode
==================
*/
void SCR_DrawScreenField( void ) {
// wide aspect ratio screens need to have the sides cleared
// unless they are displaying game renderings
if ( clc.state != CA_ACTIVE && clc.state != CA_CINEMATIC ) {
if ( cls.glconfig.vidWidth * 480 > cls.glconfig.vidHeight * 640 ) {
re.SetColor( g_color_table[0] );
//re.DrawStretchPic( 0, 0, cls.glconfig.vidWidth, cls.glconfig.vidHeight, 0, 0, 0, 0, cls.whiteShader );
re.DrawBox( 0, 0, cls.glconfig.vidHeight, cls.glconfig.vidWidth );
re.SetColor( NULL );
}
}
switch( clc.state ) {
case CA_UNINITIALIZED:
Com_Error( ERR_FATAL, "SCR_DrawScreenField: clc.state == CA_UNINITIALIZED" );
break;
case CA_LOADING:
case CA_PRIMED:
// draw the game information screen and loading progress
CL_CGameRendering(s_scr_stereoFrame);
// also draw the connection information, so it doesn't
// flash away too briefly on local or lan games
// refresh to update the time
case CA_CONNECTING:
case CA_CHALLENGING:
case CA_CONNECTED:
// connecting clients will only show the connection dialog
// refresh to update the time
UI_DrawConnect();
break;
case CA_CINEMATIC:
SCR_DrawCinematic();
break;
case CA_ACTIVE:
CL_CGameRendering(s_scr_stereoFrame);
break;
default:
break;
}
}
/*
==================
UpdateStereoSide
==================
*/
void UpdateStereoSide( stereoFrame_t s ) {
s_scr_stereoFrame = s;
re.BeginFrame( s );
if( clc.state == CA_CINEMATIC ) {
SCR_DrawCinematic();
}
UI_Update();
}
/*
==================
SCR_SimpleUpdateScreen
==================
*/
void SCR_SimpleUpdateScreen( void ) {
#ifdef USE_OPENXR
// In VR the frame is drawn once per eye, each into its own swapchain image,
// and handed to the compositor rather than to the window. Deliberately two
// passes and no more: a third, mono pass to produce a flat image would cost
// half the frame again.
if( VR_Enabled() ) {
if( VR_BeginFrame() ) {
if( VR_UseScreenLayer() ) {
// Flat content: draw it once onto a panel fixed in front of the
// viewer. Drawing it into both eyes instead would give it no
// stereo disparity while each eye still warps it through its own
// asymmetric frustum, and the two images would never fuse.
VR_PrepareScreenLayer();
UpdateStereoSide( STEREO_CENTER );
if( com_speeds->integer ) {
re.EndFrame( &time_frontend, &time_backend );
}
else {
re.EndFrame( NULL, NULL );
}
VR_FinishScreenLayer();
}
else {
int eye;
// Only worth splitting the frame when something else is going to
// draw the flat half. Otherwise the eye passes carry it, the way
// a flat frame does, and RB_SetGL2D converges it.
const qboolean split = VR_WristPanelEnabled();
cl_vrDrawMode = split ? VRDRAW_WORLD : VRDRAW_ALL;
Cvar_Set( "vr_hudPass", split ? "2" : "3" );
for( eye = 0; eye < VR_MAX_EYES; eye++ ) {
if( !VR_PrepareEye( eye ) ) {
continue;
}
// Deliberately STEREO_CENTER for both eyes.
// STEREO_LEFT/RIGHT select the GL_BACK_LEFT/RIGHT draw
// buffers of quad buffered stereo, a different mechanism
// entirely: here each eye already has its own framebuffer,
// and its viewpoint comes from the headset pose.
{
const int sceneStart = Sys_Milliseconds();
int issueStart;
UpdateStereoSide( STEREO_CENTER );
issueStart = Sys_Milliseconds();
// Always asked for here, not only under com_speeds: the
// frame timing report wants the split, and there is no
// console in a headset to turn com_speeds on with.
re.EndFrame( &time_frontend, &time_backend );
VR_TraceRenderTimes( time_frontend, time_backend );
VR_TraceSceneTimes( issueStart - sceneStart,
Sys_Milliseconds() - issueStart );
}
VR_FinishEye( eye );
}
// Then the HUD and any menus, once, onto the panel on the
// player's wrist. Once rather than per eye because it is flat:
// the compositor gives it the same disparity in both eyes by
// placing the quad in the room.
if( VR_WristPanelVisible() ) {
cl_vrDrawMode = VRDRAW_PANEL;
Cvar_Set( "vr_hudPass", "1" );
VR_PrepareWristPanel();
UpdateStereoSide( STEREO_CENTER );
if( com_speeds->integer ) {
re.EndFrame( &time_frontend, &time_backend );
}
else {
re.EndFrame( NULL, NULL );
}
VR_FinishWristPanel();
}
cl_vrDrawMode = VRDRAW_ALL;
Cvar_Set( "vr_hudPass", "0" );
}
}
VR_SubmitFrame();
return;
}
#endif
// if running in stereo, we need to draw the frame twice
if( cls.glconfig.stereoEnabled ) {
UpdateStereoSide( STEREO_LEFT );
UpdateStereoSide( STEREO_RIGHT );
}
else {
UpdateStereoSide( STEREO_CENTER );
}
if( com_speeds->integer ) {
re.EndFrame( &time_frontend, &time_backend );
}
else {
re.EndFrame( NULL, NULL );
}
}
/*
==================
SCR_UpdateScreen
This is called every frame, and can also be called explicitly to flush
text to the screen.
==================
*/
void SCR_UpdateScreen( void ) {
static qboolean screen_recursive;
if ( !scr_initialized ) {
return; // not initialized yet
}
if (screen_recursive) {
// already called
return;
}
screen_recursive = qtrue;
CL_StartHunkUsers(qfalse);
SCR_SimpleUpdateScreen();
// set the fps value
if( fps->integer && clc.state == CA_ACTIVE ) {
static int belowlastplaytime = 0;
static qboolean belowframerate = qfalse;
if( currentfps > 19.8 ) {
belowframerate = qfalse;
} else if( !belowframerate ) {
if( currentfps < 18.0 ) {
belowlastplaytime = 10000;
belowframerate = qtrue;
}
}
if( belowframerate && cls.realtime > belowlastplaytime ) {
belowlastplaytime = cls.realtime + 4000;
}
}
screen_recursive = qfalse;
}