From e820efce1e58371b566099340ad4d4941204a15c Mon Sep 17 00:00:00 2001 From: Thomas Debesse Date: Fri, 4 Jun 2021 23:18:56 +0200 Subject: [PATCH] sdl_glimp,tr_init: rewrite the original GL selection code, improve gfxinfo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit squashes multiple commits by illwieckz and slipher. See https://github.com/DaemonEngine/Daemon/pull/478 Co-authored-by: Thomas “illwieckz” Debesse Co-authored-by: slipher == Squashed commits by illwieckz :: sdl_glimp: rewrite the original GL selection code - Detect best configuration possible. - Try custom configuration if exists. - If no custom configuration or if it fails, load the recommended configuration if possible (OpenGL 3.2 core) or the best one available. - Reuse window and context when possible. - Display meaningful popup telling user OpenGL version is too low or required extensions are missing when that happens. - Rely on more return codes for GLimp_SetMode(). - Test for negative SDL_Init return value, not just -1. :: sdl_glimp,tr_init: do not test all OpenGL versions unless r_glExtendedValidation is set When r_glExtendedValidation is enabled, the engine tests if OpenGL versions higher than 3.2 core are supported for logging and diagnostic purpose, but still requestes 3.2 core anyway. Some drivers may provide more than 3.2 when requesting 3.2, this is not our fault. :: sdl_glimp,tr_init: rewrite logging, improve gfxinfo - Move GL query for logging purpose from tr_init to sdl_glimp. - Do not split MODE log message. - Unify some log. - Add more debug log when building GL extension list. - Also increase the extensions_string length to not truncate the string, 4096 is not enough, there can be more than 6000 characters on an OpenGL 4.6 driver. - Also log missing extensions to make gfxinfo more useful. - Rewrite gfxinfo in more useful way. - List enabled and missing GL extensions. :: sdl_glimp: silence the GL error when querying if context is core on non-core implementation - Silence the error that may happen when querying if the OpenGL context uses core profile when core profile is not supported by the OpenGL implementation to begin with. For example this may happen on implementations not supporting higher than OpenGL 2.1, while forcing OpenGL 2.1 on implementations supporting higher versions including core profiles may not raise an error. :: sdl_glimp: make GLimp_StartDriverAndSetMode only return true on RSERR_OK - Only return true on OK, don't return true on unknown errors. :: sdl_glimp: catch errors from GLimp_DetectAvailableModes to prevent further segfault It may be possible to create a valid context that is unusable. For example the 3840×2160 resolution is too large for the Radeon 9700 and the Mesa r300 driver may print this error when the requested resolution is higher than what is supported by hardware: > r300: Implementation error: Render targets are too big in r300_set_framebuffer_state, refusing to bind framebuffer state! It will unfortunately return a valid but unusable context that will make the engine segfault when calling GL_SetDefaultState(). :: sdl_glimp: flag fullscreen window as borderless when borderless is enabled Flag fullscreen window as borderless when borderless is enabled otherwise the window will be bordered when leaving fullscreen while the borderless option would be enabled. :: sdl_glimp,tr_init: remove unused depthBits :: sdl_glimp: remove SDL_INIT_NOPARACHUTE In GLimp_StartDriverAndSetMod() the SDL_INIT_NOPARACHUTE flag was removed from SDL_Init( SDL_INIT_VIDEO ) as this flag is now ignored, see https://wiki.libsdl.org/SDL_Init == Squashed commits by slipher :: Better type safety in GL detection code :: Simplify GL_ValidateBestContext duplicate loop :: GLimp_SetMode - simplify error handling :: Rework GL initialization Have GLimp_ValidateBestContext() validate both the highest-numbered available context (if r_glExtendedValidation is enabled), and the highest context that we actually want to use (at most 3.2). This means most of the code in GLimp_ApplyPreferredOptions can be removed because it was duplicating the knowledge about version preferences and the code for instantiating them in GLimp_ValidateBestContext. :: Also cut down on other code duplication. :: Remove dead cvar r_stencilBits :: Fix glConfig.colorBits logging - show actual not requested - don't log it twice at notice level --- src/engine/client/cl_main.cpp | 6 +- src/engine/renderer/tr_cmds.cpp | 21 +- src/engine/renderer/tr_init.cpp | 135 ++-- src/engine/renderer/tr_local.h | 2 +- src/engine/renderer/tr_public.h | 15 + src/engine/renderer/tr_types.h | 16 +- src/engine/sys/sdl_glimp.cpp | 1132 ++++++++++++++++++++++++------- 7 files changed, 1026 insertions(+), 301 deletions(-) diff --git a/src/engine/client/cl_main.cpp b/src/engine/client/cl_main.cpp index a3d5fd9cd2..499f159862 100644 --- a/src/engine/client/cl_main.cpp +++ b/src/engine/client/cl_main.cpp @@ -2662,7 +2662,7 @@ void CL_StartHunkUsers() if ( !cls.rendererStarted ) { CL_ShutdownRef(); - Sys::Error( "Couldn't load a renderer" ); + Sys::Error( "Couldn't load a renderer." ); } if ( !Audio::Init() ) { @@ -3023,7 +3023,9 @@ void CL_Shutdown() recursive = false; - memset( &cls, 0, sizeof( cls ) ); + // do not leak. + cls.~clientStatic_t(); + new(&cls) clientStatic_t{}; Log::Debug( "-----------------------" ); diff --git a/src/engine/renderer/tr_cmds.cpp b/src/engine/renderer/tr_cmds.cpp index 44122201b3..383031d700 100644 --- a/src/engine/renderer/tr_cmds.cpp +++ b/src/engine/renderer/tr_cmds.cpp @@ -773,21 +773,12 @@ void RE_BeginFrame() // do overdraw measurement if ( r_measureOverdraw->integer ) { - if ( glConfig.stencilBits < 4 ) - { - Log::Warn("not enough stencil bits to measure overdraw: %d", glConfig.stencilBits ); - ri.Cvar_Set( "r_measureOverdraw", "0" ); - } - else - { - R_SyncRenderThread(); - glEnable( GL_STENCIL_TEST ); - glStencilMask( ~0U ); - GL_ClearStencil( 0U ); - glStencilFunc( GL_ALWAYS, 0U, ~0U ); - glStencilOp( GL_KEEP, GL_INCR, GL_INCR ); - } - + R_SyncRenderThread(); + glEnable( GL_STENCIL_TEST ); + glStencilMask( ~0U ); + GL_ClearStencil( 0U ); + glStencilFunc( GL_ALWAYS, 0U, ~0U ); + glStencilOp( GL_KEEP, GL_INCR, GL_INCR ); r_measureOverdraw->modified = false; } else diff --git a/src/engine/renderer/tr_init.cpp b/src/engine/renderer/tr_init.cpp index 3e62bd4ff6..d59bf17c8b 100644 --- a/src/engine/renderer/tr_init.cpp +++ b/src/engine/renderer/tr_init.cpp @@ -38,6 +38,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA cvar_t *r_glDebugProfile; cvar_t *r_glDebugMode; cvar_t *r_glAllowSoftware; + cvar_t *r_glExtendedValidation; cvar_t *r_verbose; cvar_t *r_ignore; @@ -101,7 +102,6 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA cvar_t *r_checkGLErrors; cvar_t *r_logFile; - cvar_t *r_stencilbits; cvar_t *r_depthbits; cvar_t *r_colorbits; cvar_t *r_alphabits; @@ -801,11 +801,7 @@ ScreenshotCmd screenshotPNGRegistration("screenshotPNG", ssFormat_t::SSF_PNG, "p GLimp_LogComment( "--- GL_SetDefaultState ---\n" ); GL_ClearDepth( 1.0f ); - - if ( glConfig.stencilBits >= 4 ) - { - GL_ClearStencil( 0 ); - } + GL_ClearStencil( 0 ); GL_FrontFace( GL_CCW ); GL_CullFace( GL_FRONT ); @@ -905,108 +901,150 @@ ScreenshotCmd screenshotPNGRegistration("screenshotPNG", ssFormat_t::SSF_PNG, "p Log::Notice("GL_VENDOR: %s", glConfig.vendor_string ); Log::Notice("GL_RENDERER: %s", glConfig.renderer_string ); Log::Notice("GL_VERSION: %s", glConfig.version_string ); - Log::Debug("GL_EXTENSIONS: %s", glConfig.extensions_string ); - Log::Debug("GL_MAX_TEXTURE_SIZE: %d", glConfig.maxTextureSize ); + Log::Debug("GL_EXTENSIONS: %s", glConfig2.glExtensionsString ); + Log::Notice("GL_MAX_TEXTURE_SIZE: %d", glConfig.maxTextureSize ); Log::Notice("GL_SHADING_LANGUAGE_VERSION: %s", glConfig2.shadingLanguageVersionString ); Log::Notice("GL_MAX_VERTEX_UNIFORM_COMPONENTS %d", glConfig2.maxVertexUniforms ); - Log::Debug("GL_MAX_VERTEX_ATTRIBS %d", glConfig2.maxVertexAttribs ); + Log::Notice("GL_MAX_VERTEX_ATTRIBS %d", glConfig2.maxVertexAttribs ); if ( glConfig2.occlusionQueryAvailable ) { - Log::Debug("%d occlusion query bits", glConfig2.occlusionQueryBits ); + Log::Notice("Occlusion query bits: %d", glConfig2.occlusionQueryBits ); } if ( glConfig2.drawBuffersAvailable ) { - Log::Debug("GL_MAX_DRAW_BUFFERS: %d", glConfig2.maxDrawBuffers ); + Log::Notice("GL_MAX_DRAW_BUFFERS: %d", glConfig2.maxDrawBuffers ); } if ( glConfig2.textureAnisotropyAvailable ) { - Log::Debug("GL_TEXTURE_MAX_ANISOTROPY_EXT: %f", glConfig2.maxTextureAnisotropy ); + Log::Notice("GL_TEXTURE_MAX_ANISOTROPY_EXT: %f", glConfig2.maxTextureAnisotropy ); } - Log::Debug("GL_MAX_RENDERBUFFER_SIZE: %d", glConfig2.maxRenderbufferSize ); - Log::Debug("GL_MAX_COLOR_ATTACHMENTS: %d", glConfig2.maxColorAttachments ); + Log::Notice("GL_MAX_RENDERBUFFER_SIZE: %d", glConfig2.maxRenderbufferSize ); + Log::Notice("GL_MAX_COLOR_ATTACHMENTS: %d", glConfig2.maxColorAttachments ); + + Log::Notice("PIXELFORMAT: color(%d-bits)", glConfig.colorBits ); + + { + std::string out; + if ( glConfig.displayFrequency ) + { + out = Str::Format("%d", glConfig.displayFrequency ); + } + else + { + out = "N/A"; + } - Log::Debug("\nPIXELFORMAT: color(%d-bits) Z(%d-bit) stencil(%d-bits)", glConfig.colorBits, - glConfig.depthBits, glConfig.stencilBits ); - Log::Debug("MODE: %d, %d x %d %s hz:", r_mode->integer, glConfig.vidWidth, glConfig.vidHeight, - fsstrings[ r_fullscreen->integer == 1 ] ); + Log::Notice("MODE: %d, %d x %d %s hz: %s", + r_mode->integer, + glConfig.vidWidth, glConfig.vidHeight, + fsstrings[ r_fullscreen->integer == 1 ], + out + ); + } - if ( glConfig.displayFrequency ) + if ( !!r_glExtendedValidation->integer ) { - Log::Debug("%d", glConfig.displayFrequency ); + Log::Notice("Using OpenGL version %d.%d, requested: %d.%d, highest: %d.%d", + glConfig2.glMajor, glConfig2.glMinor, glConfig2.glRequestedMajor, glConfig2.glRequestedMinor, + glConfig2.glHighestMajor, glConfig2.glHighestMinor ); } else { - Log::Debug("N/A" ); + Log::Notice("Using OpenGL version %d.%d, requested: %d.%d", glConfig2.glMajor, glConfig2.glMinor, glConfig2.glRequestedMajor, glConfig2.glRequestedMinor ); } - Log::Debug("texturemode: %s", r_textureMode->string ); - Log::Debug("picmip: %d", r_picMip->integer ); - Log::Debug("imageMaxDimension: %d", r_imageMaxDimension->integer ); - Log::Debug("ignoreMaterialMinDimension: %d", r_ignoreMaterialMinDimension->integer ); - Log::Debug("ignoreMaterialMaxDimension: %d", r_ignoreMaterialMaxDimension->integer ); - Log::Debug("replaceMaterialMinDimensionIfPresentWithMaxDimension: %d", r_replaceMaterialMinDimensionIfPresentWithMaxDimension->integer ); - if ( glConfig.driverType == glDriverType_t::GLDRV_OPENGL3 ) { - int contextFlags, profile; - - Log::Notice("%sUsing OpenGL 3.x context", Color::ToString( Color::Green ) ); + Log::Notice("%sUsing OpenGL 3.x context.", Color::ToString( Color::Green ) ); - // check if we have a core-profile - glGetIntegerv( GL_CONTEXT_PROFILE_MASK, &profile ); + /* See https://www.khronos.org/opengl/wiki/OpenGL_Context + for information about core, compatibility and forward context. */ - if ( profile == GL_CONTEXT_CORE_PROFILE_BIT ) + if ( glConfig2.glCoreProfile ) { - Log::Debug("%sHaving a core profile", Color::ToString( Color::Green ) ); + Log::Notice("%sUsing an OpenGL core profile.", Color::ToString( Color::Green ) ); } else { - Log::Debug("%sHaving a compatibility profile", Color::ToString( Color::Red ) ); + Log::Notice("%sUsing an OpenGL compatibility profile.", Color::ToString( Color::Red ) ); } - // check if context is forward compatible - glGetIntegerv( GL_CONTEXT_FLAGS, &contextFlags ); - - if ( contextFlags & GL_CONTEXT_FLAG_FORWARD_COMPATIBLE_BIT ) + if ( glConfig2.glForwardCompatibleContext ) { - Log::Debug("%sContext is forward compatible", Color::ToString( Color::Green ) ); + Log::Notice("OpenGL 3.x context is forward compatible."); } else { - Log::Debug("%sContext is NOT forward compatible", Color::ToString( Color::Red )); + Log::Notice("OpenGL 3.x context is not forward compatible."); } } + else + { + Log::Notice("%sUsing OpenGL 2.x context.", Color::ToString( Color::Red ) ); + } + + if ( glConfig2.glEnabledExtensionsString.length() != 0 ) + { + Log::Notice("%sUsing OpenGL extensions: %s", Color::ToString( Color::Green ), glConfig2.glEnabledExtensionsString ); + } + + if ( glConfig2.glMissingExtensionsString.length() != 0 ) + { + Log::Notice("%sMissing OpenGL extensions: %s", Color::ToString( Color::Red ), glConfig2.glMissingExtensionsString ); + } if ( glConfig.hardwareType == glHardwareType_t::GLHW_R300 ) { - Log::Debug("HACK: ATI R300 approximations" ); + Log::Notice("%sUsing ATI R300 approximations.", Color::ToString( Color::Red )); } if ( glConfig.textureCompression != textureCompression_t::TC_NONE ) { - Log::Debug("Using S3TC (DXTC) texture compression" ); + Log::Notice("%sUsing S3TC (DXTC) texture compression.", Color::ToString( Color::Green ) ); } if ( glConfig2.vboVertexSkinningAvailable ) { - Log::Notice("Using GPU vertex skinning with max %i bones in a single pass", glConfig2.maxVertexSkinningBones ); + /* Mesa drivers usually support 256 bones, Nvidia proprietary drivers + usually support 233 bones, OpenGL 2.1 hardware usually supports no more + than 41 bones which may not be enough to use hardware acceleration on + models from games like Unvanquished. */ + if ( glConfig2.maxVertexSkinningBones < 233 ) + { + Log::Notice("%sUsing GPU vertex skinning with max %i bones in a single pass, some models may not be hardware accelerated.", Color::ToString( Color::Red ), glConfig2.maxVertexSkinningBones ); + } + else + { + Log::Notice("%sUsing GPU vertex skinning with max %i bones in a single pass, models are hardware accelerated.", Color::ToString( Color::Green ), glConfig2.maxVertexSkinningBones ); + } + } + else + { + Log::Notice("%sMissing GPU vertex skinning, models are not hardware-accelerated.", Color::ToString( Color::Red ) ); } if ( glConfig.smpActive ) { - Log::Debug("Using dual processor acceleration" ); + Log::Notice("Using dual processor acceleration." ); } if ( r_finish->integer ) { - Log::Debug("Forcing glFinish" ); + Log::Notice("Forcing glFinish." ); } + + Log::Debug("texturemode: %s", r_textureMode->string ); + Log::Debug("picmip: %d", r_picMip->integer ); + Log::Debug("imageMaxDimension: %d", r_imageMaxDimension->integer ); + Log::Debug("ignoreMaterialMinDimension: %d", r_ignoreMaterialMinDimension->integer ); + Log::Debug("ignoreMaterialMaxDimension: %d", r_ignoreMaterialMaxDimension->integer ); + Log::Debug("replaceMaterialMinDimensionIfPresentWithMaxDimension: %d", r_replaceMaterialMinDimensionIfPresentWithMaxDimension->integer ); } static void GLSL_restart_f() @@ -1032,6 +1070,7 @@ ScreenshotCmd screenshotPNGRegistration("screenshotPNG", ssFormat_t::SSF_PNG, "p r_glDebugProfile = ri.Cvar_Get( "r_glDebugProfile", "", CVAR_LATCH ); r_glDebugMode = ri.Cvar_Get( "r_glDebugMode", "0", CVAR_CHEAT ); r_glAllowSoftware = ri.Cvar_Get( "r_glAllowSoftware", "0", CVAR_LATCH ); + r_glExtendedValidation = ri.Cvar_Get( "r_glExtendedValidation", "0", CVAR_LATCH ); // latched and archived variables r_ext_occlusion_query = ri.Cvar_Get( "r_ext_occlusion_query", "1", CVAR_CHEAT | CVAR_LATCH ); @@ -1059,7 +1098,6 @@ ScreenshotCmd screenshotPNGRegistration("screenshotPNG", ssFormat_t::SSF_PNG, "p r_colorMipLevels = ri.Cvar_Get( "r_colorMipLevels", "0", CVAR_LATCH ); r_colorbits = ri.Cvar_Get( "r_colorbits", "0", CVAR_LATCH ); r_alphabits = ri.Cvar_Get( "r_alphabits", "0", CVAR_LATCH ); - r_stencilbits = ri.Cvar_Get( "r_stencilbits", "8", CVAR_LATCH ); r_depthbits = ri.Cvar_Get( "r_depthbits", "0", CVAR_LATCH ); r_ext_multisample = ri.Cvar_Get( "r_ext_multisample", "0", CVAR_LATCH | CVAR_ARCHIVE ); r_mode = ri.Cvar_Get( "r_mode", "-2", CVAR_LATCH | CVAR_ARCHIVE ); @@ -1428,7 +1466,6 @@ ScreenshotCmd screenshotPNGRegistration("screenshotPNG", ssFormat_t::SSF_PNG, "p // print info GfxInfo_f(); - GL_CheckErrors(); Log::Debug("----- finished R_Init -----" ); diff --git a/src/engine/renderer/tr_local.h b/src/engine/renderer/tr_local.h index 3a819c4e30..5b18812956 100644 --- a/src/engine/renderer/tr_local.h +++ b/src/engine/renderer/tr_local.h @@ -2811,6 +2811,7 @@ static inline void halfToFloat( const f16vec4_t in, vec4_t out ) extern cvar_t *r_glDebugProfile; extern cvar_t *r_glDebugMode; extern cvar_t *r_glAllowSoftware; + extern cvar_t *r_glExtendedValidation; extern cvar_t *r_ignore; // used for debugging anything extern cvar_t *r_verbose; // used for verbose debug spew @@ -2820,7 +2821,6 @@ static inline void halfToFloat( const f16vec4_t in, vec4_t out ) extern cvar_t *r_znear; // near Z clip plane extern cvar_t *r_zfar; - extern cvar_t *r_stencilbits; // number of desired stencil bits extern cvar_t *r_depthbits; // number of desired depth bits extern cvar_t *r_colorbits; // number of desired color bits, only relevant for fullscreen extern cvar_t *r_alphabits; // number of desired depth bits diff --git a/src/engine/renderer/tr_public.h b/src/engine/renderer/tr_public.h index 08443677e9..38c6a7242a 100644 --- a/src/engine/renderer/tr_public.h +++ b/src/engine/renderer/tr_public.h @@ -37,6 +37,7 @@ Maryland 20850 USA. #define __TR_PUBLIC_H #include "tr_types.h" +#include #define REF_API_VERSION 10 @@ -44,7 +45,21 @@ struct glconfig2_t { bool textureCompressionRGTCAvailable; + int glHighestMajor; + int glHighestMinor; + + int glRequestedMajor; + int glRequestedMinor; + + int glMajor; + int glMinor; + bool glCoreProfile; + bool glForwardCompatibleContext; + + std::string glExtensionsString; + std::string glEnabledExtensionsString; + std::string glMissingExtensionsString; int maxCubeMapTextureSize; diff --git a/src/engine/renderer/tr_types.h b/src/engine/renderer/tr_types.h index 7c4aeb672f..93bd7ea073 100644 --- a/src/engine/renderer/tr_types.h +++ b/src/engine/renderer/tr_types.h @@ -335,12 +335,22 @@ struct glconfig_t char renderer_string[ MAX_STRING_CHARS ]; char vendor_string[ MAX_STRING_CHARS ]; char version_string[ MAX_STRING_CHARS ]; - char extensions_string[ MAX_STRING_CHARS * 4 ]; // TTimo - bumping, some cards have a big extension string + + // TODO(0.53): Delete, moved to glconfig2_t. + char extensions_string[ MAX_STRING_CHARS * 4 ]; // TTimo - bumping, some cards have a big extension string int maxTextureSize; // queried from GL - int unused; - int colorBits, depthBits, stencilBits; + // TODO(0.53): Delete, unused. + int unused; + + int colorBits; + + // TODO(0.53): Delete, unused. + int stencilBits; + + // TODO(0.53): Delete, unused. + int depthBits; glDriverType_t driverType; glHardwareType_t hardwareType; diff --git a/src/engine/sys/sdl_glimp.cpp b/src/engine/sys/sdl_glimp.cpp index 03539a68cc..bb3de9f174 100644 --- a/src/engine/sys/sdl_glimp.cpp +++ b/src/engine/sys/sdl_glimp.cpp @@ -36,9 +36,8 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA static Log::Logger logger("glconfig", "", Log::Level::NOTICE); -SDL_Window *window = nullptr; +SDL_Window *window = nullptr; static SDL_GLContext glContext = nullptr; -static int colorBits = 0; #ifdef USE_SMP static void GLimp_SetCurrentContext( bool enable ) @@ -316,6 +315,7 @@ enum class rserr_t RSERR_INVALID_FULLSCREEN, RSERR_INVALID_MODE, + RSERR_MISSING_GL, RSERR_OLD_GL, RSERR_UNKNOWN @@ -326,6 +326,9 @@ cvar_t *r_centerWindow; cvar_t *r_displayIndex; cvar_t *r_sdlDriver; +static void GLimp_DestroyContextIfExists(); +static void GLimp_DestroyWindowIfExists(); + /* =============== GLimp_Shutdown @@ -347,17 +350,8 @@ void GLimp_Shutdown() #endif - if ( glContext ) - { - SDL_GL_DeleteContext( glContext ); - glContext = nullptr; - } - - if ( window ) - { - SDL_DestroyWindow( window ); - window = nullptr; - } + GLimp_DestroyContextIfExists(); + GLimp_DestroyWindowIfExists(); SDL_QuitSubSystem( SDL_INIT_VIDEO ); @@ -407,7 +401,7 @@ static int GLimp_CompareModes( const void *a, const void *b ) GLimp_DetectAvailableModes =============== */ -static void GLimp_DetectAvailableModes() +static bool GLimp_DetectAvailableModes() { char buf[ MAX_STRING_CHARS ] = { 0 }; SDL_Rect modes[ 128 ]; @@ -421,7 +415,7 @@ static void GLimp_DetectAvailableModes() if ( SDL_GetWindowDisplayMode( window, &windowMode ) < 0 ) { logger.Warn("Couldn't get window display mode: %s", SDL_GetError() ); - return; + return false; } for ( i = 0; i < SDL_GetNumDisplayModes( display ); i++ ) @@ -436,7 +430,7 @@ static void GLimp_DetectAvailableModes() if ( !mode.w || !mode.h ) { logger.Notice("Display supports any resolution" ); - return; + return true; } if ( windowMode.format != mode.format || windowMode.refresh_rate != mode.refresh_rate ) @@ -473,35 +467,122 @@ static void GLimp_DetectAvailableModes() logger.Notice("Available modes: '%s'", buf ); ri.Cvar_Set( "r_availableModes", buf ); } + + return true; } -/* -=============== -GLimp_SetMode -=============== -*/ -static rserr_t GLimp_SetMode( int mode, bool fullscreen, bool noborder ) +enum class glProfile { + UNDEFINED = 0, + COMPATIBILITY = 1, + CORE = 2, +}; + +struct glConfiguration { + int major; + int minor; + glProfile profile; + int colorBits; +}; + +static bool operator!=(const glConfiguration& c1, const glConfiguration& c2) { + return c1.major != c2.major + || c1.minor != c2.minor + || c1.profile != c2.profile + || c1.colorBits != c2.colorBits; +} + +static const char* GLimp_getProfileName( glProfile profile ) { - const char *glstring; - int perChannelColorBits; - int alphaBits, depthBits, stencilBits; - int samples; - int i = 0; - SDL_Surface *icon = nullptr; - SDL_DisplayMode desktopMode; - Uint32 flags = SDL_WINDOW_SHOWN | SDL_WINDOW_OPENGL; - int x, y; - GLenum glewResult; - int GLmajor, GLminor; - int GLEWmajor, GLEWminor, GLEWmicro; + ASSERT(profile != glProfile::UNDEFINED); + return profile == glProfile::CORE ? "core" : "compatibility"; +} - logger.Notice("Initializing OpenGL display" ); +static void GLimp_SetAttributes( const glConfiguration &configuration ) +{ + int perChannelColorBits = configuration.colorBits == 24 ? 8 : 4; + + SDL_GL_SetAttribute( SDL_GL_RED_SIZE, perChannelColorBits ); + SDL_GL_SetAttribute( SDL_GL_GREEN_SIZE, perChannelColorBits ); + SDL_GL_SetAttribute( SDL_GL_BLUE_SIZE, perChannelColorBits ); + SDL_GL_SetAttribute( SDL_GL_DOUBLEBUFFER, 1 ); + + if ( !r_glAllowSoftware->integer ) + { + SDL_GL_SetAttribute( SDL_GL_ACCELERATED_VISUAL, 1 ); + } + + SDL_GL_SetAttribute( SDL_GL_CONTEXT_MAJOR_VERSION, configuration.major ); + SDL_GL_SetAttribute( SDL_GL_CONTEXT_MINOR_VERSION, configuration.minor ); + + if ( configuration.profile == glProfile::CORE ) + { + SDL_GL_SetAttribute( SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE ); + } + else + { + SDL_GL_SetAttribute( SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_COMPATIBILITY ); + } + + if ( r_glDebugProfile->integer ) + { + SDL_GL_SetAttribute( SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_DEBUG_FLAG ); + } +} + +static bool GLimp_CreateWindow( bool fullscreen, bool bordered, const glConfiguration &configuration ) +{ + /* The requested attributes should be set before creating + an OpenGL window. + + -- http://wiki.libsdl.org/SDL_GL_SetAttribute */ + GLimp_SetAttributes( configuration ); + + Uint32 flags = SDL_WINDOW_SHOWN | SDL_WINDOW_OPENGL; if ( r_allowResize->integer ) { flags |= SDL_WINDOW_RESIZABLE; } + SDL_Surface *icon = nullptr; + + icon = SDL_CreateRGBSurfaceFrom( ( void * ) CLIENT_WINDOW_ICON.pixel_data, + CLIENT_WINDOW_ICON.width, + CLIENT_WINDOW_ICON.height, + CLIENT_WINDOW_ICON.bytes_per_pixel * 8, + CLIENT_WINDOW_ICON.bytes_per_pixel * CLIENT_WINDOW_ICON.width, +#ifdef Q3_LITTLE_ENDIAN + 0x000000FF, 0x0000FF00, 0x00FF0000, 0xFF000000 +#else + 0xFF000000, 0x00FF0000, 0x0000FF00, 0x000000FF +#endif + ); + + const char *windowType = nullptr; + + if ( fullscreen ) + { + flags |= SDL_WINDOW_FULLSCREEN; + windowType = "fullscreen"; + } + + /* We need to set borderless flag even when fullscreen + because otherwise when disabling fullscreen the window + will be bordered while the borderless option is enabled. */ + + if ( !bordered ) + { + flags |= SDL_WINDOW_BORDERLESS; + + /* Don't tell fullscreen window is borderless, + it's meaningless. */ + if ( ! fullscreen ) + { + windowType = "borderless"; + } + } + + int x, y; if ( r_centerWindow->integer ) { // center window on specified display @@ -514,32 +595,168 @@ static rserr_t GLimp_SetMode( int mode, bool fullscreen, bool noborder ) y = SDL_WINDOWPOS_UNDEFINED_DISPLAY( r_displayIndex->integer ); } - icon = SDL_CreateRGBSurfaceFrom( ( void * ) CLIENT_WINDOW_ICON.pixel_data, - CLIENT_WINDOW_ICON.width, - CLIENT_WINDOW_ICON.height, - CLIENT_WINDOW_ICON.bytes_per_pixel * 8, - CLIENT_WINDOW_ICON.bytes_per_pixel * CLIENT_WINDOW_ICON.width, -#ifdef Q3_LITTLE_ENDIAN - 0x000000FF, 0x0000FF00, 0x00FF0000, 0xFF000000 -#else - 0xFF000000, 0x00FF0000, 0x0000FF00, 0x000000FF -#endif - ); + window = SDL_CreateWindow( CLIENT_WINDOW_TITLE, x, y, glConfig.vidWidth, glConfig.vidHeight, flags ); + + if ( window ) + { + int w, h; + SDL_GetWindowPosition( window, &x, &y ); + SDL_GetWindowSize( window, &w, &h ); + logger.Debug( "SDL %s%swindow created at %d,%d with %d×%d size", + windowType ? windowType : "", + windowType ? " ": "", + x, y, w, h ); + } + else + { + logger.Warn( "SDL %d×%d %s%swindow not created", + glConfig.vidWidth, glConfig.vidHeight, + windowType ? windowType : "", + windowType ? " ": "" ); + logger.Warn("SDL_CreateWindow failed: %s", SDL_GetError() ); + return false; + } + + SDL_SetWindowIcon( window, icon ); + + SDL_FreeSurface( icon ); + + return true; +} + +static void GLimp_DestroyContextIfExists() +{ + if ( glContext != nullptr ) + { + SDL_GL_DeleteContext( glContext ); + glContext = nullptr; + } +} + +static void GLimp_DestroyWindowIfExists() +{ + // Do not let orphaned context alive. + GLimp_DestroyContextIfExists(); + + if ( window != nullptr ) + { + int x, y, w, h; + SDL_GetWindowPosition( window, &x, &y ); + SDL_GetWindowSize( window, &w, &h ); + logger.Debug("Destroying %d×%d SDL window at %d,%d", w, h, x, y ); + SDL_DestroyWindow( window ); + window = nullptr; + } +} + +static bool GLimp_CreateContext( const glConfiguration &configuration ) +{ + GLimp_DestroyContextIfExists(); + glContext = SDL_GL_CreateContext( window ); + + const char* profileName = GLimp_getProfileName( configuration.profile ); + if ( glContext != nullptr ) + { + logger.Debug( "Valid context: %d-bit OpenGL %d.%d %s", + configuration.colorBits, + configuration.major, + configuration.minor, + profileName ); + } + else + { + logger.Debug( "Invalid context: %d-bit OpenGL %d.%d %s", + configuration.colorBits, + configuration.major, + configuration.minor, + profileName ); + } + + return glContext != nullptr; +} + +/* GLimp_DestroyWindowIfExists checks if window exists before +destroying it so we can call GLimp_RecreateWindowWhenChange even +if no window is created yet. + +It is assumed width, height and other things like that are unchanged, +given vid_restart is called when changing those and then destroying +the window before calling this. */ +static bool GLimp_RecreateWindowWhenChange( const bool fullscreen, const bool bordered, const glConfiguration &configuration ) +{ + static bool currentFullscreen = false; + static bool currentBordered = false; + static glConfiguration currentConfiguration = {}; + + if ( window == nullptr + || configuration != currentConfiguration ) + { + currentConfiguration = configuration; + + GLimp_DestroyWindowIfExists(); + + if ( !GLimp_CreateWindow( fullscreen, bordered, configuration ) ) + { + return false; + } + } + + if ( fullscreen != currentFullscreen ) + { + Uint32 flags = fullscreen ? SDL_WINDOW_FULLSCREEN : 0; + int sdlToggled = SDL_SetWindowFullscreen( window, flags ); + + if ( sdlToggled < 0 ) + { + GLimp_DestroyWindowIfExists(); + + if ( !GLimp_CreateWindow( fullscreen, bordered, configuration ) ) + { + return false; + } + + const char* windowType = fullscreen ? "fullscreen" : "windowed"; + logger.Debug( "SDL window recreated as %s.", windowType ); + } + else + { + const char* windowType = fullscreen ? "fullscreen" : "windowed"; + logger.Debug( "SDL window set as %s.", windowType ); + } + } + + if ( bordered != currentBordered ) + { + SDL_bool sdlBordered = bordered ? SDL_TRUE : SDL_FALSE; + + SDL_SetWindowBordered( window, sdlBordered ); + + const char* windowType = bordered ? "bordered" : "borderless"; + logger.Debug( "SDL window set as %s.", windowType ); + } + + currentFullscreen = fullscreen; + currentBordered = bordered; + + return true; +} + +static rserr_t GLimp_SetModeAndResolution( const int mode ) +{ + SDL_DisplayMode desktopMode; if ( SDL_GetDesktopDisplayMode( r_displayIndex->integer, &desktopMode ) == 0 ) { displayAspect = ( float ) desktopMode.w / ( float ) desktopMode.h; - - logger.Notice("Display aspect: %.3f", displayAspect ); } else { memset( &desktopMode, 0, sizeof( SDL_DisplayMode ) ); - - logger.Notice("Cannot determine display aspect (%s), assuming 1.333", SDL_GetError() ); + displayAspect = 1.333f; + logger.Warn("Cannot determine display aspect, assuming %.3f: %s", displayAspect, SDL_GetError() ); } - logger.Notice("...setting mode %d:", mode ); + logger.Notice("Display aspect: %.3f", displayAspect ); if ( mode == -2 ) { @@ -553,201 +770,432 @@ static rserr_t GLimp_SetMode( int mode, bool fullscreen, bool noborder ) { glConfig.vidWidth = 640; glConfig.vidHeight = 480; - logger.Notice("Cannot determine display resolution, assuming 640x480" ); + logger.Warn("Cannot determine display resolution, assuming %dx%d", glConfig.vidWidth, glConfig.vidHeight ); } + + logger.Notice("Display resolution: %dx%d", glConfig.vidWidth, glConfig.vidHeight); } else if ( !R_GetModeInfo( &glConfig.vidWidth, &glConfig.vidHeight, mode ) ) { - logger.Notice(" invalid mode" ); + logger.Notice("Invalid mode %d", mode ); return rserr_t::RSERR_INVALID_MODE; } - logger.Notice(" %d %d", glConfig.vidWidth, glConfig.vidHeight ); - // HACK: We want to set the current value, not the latched value - Cvar::ClearFlags("r_customwidth", CVAR_LATCH); - Cvar::ClearFlags("r_customheight", CVAR_LATCH); - Cvar_Set( "r_customwidth", va("%d", glConfig.vidWidth ) ); - Cvar_Set( "r_customheight", va("%d", glConfig.vidHeight ) ); - Cvar::AddFlags("r_customwidth", CVAR_LATCH); - Cvar::AddFlags("r_customheight", CVAR_LATCH); + logger.Notice("...setting mode %d: %d×%d", mode, glConfig.vidWidth, glConfig.vidHeight ); - sscanf( ( const char * ) glewGetString( GLEW_VERSION ), "%d.%d.%d", - &GLEWmajor, &GLEWminor, &GLEWmicro ); - if( GLEWmajor < 2 ) { - logger.Warn( "GLEW version < 2.0.0 doesn't support GL core profiles" ); - } + return rserr_t::RSERR_OK; +} + +static rserr_t GLimp_ValidateBestContext( + const int GLEWmajor, glConfiguration &bestValidatedConfiguration, glConfiguration& extendedValidationConfiguration ) +{ + /* We iterate known OpenGL versions from highest to lowest, + iterating core profiles first, then compatibility profile, + iterating 24-bit display first, then 16-bit. + + Once we get a core profile working, we stop and attempt + to use 3.2 core whatever the highest version we validated. + The 3.2 is the oldest version with core profile, meaning + every extension not in 3.2 code are expected to be loaded + explicitely. This may make extension loading more predictable. + + For debugging and knowledge purpose we log the best supported + version even if we're not gonna use it. + + User can request explicit version or profile with related cvars. + + We test down to the lowest known OpenGL version so we can provide + useful and precise error message when an OpenGL version is too low. + + The idea of going from high to low is to make the engine load fast + on actual hardware likely to support highest core profile version, + then spend more time on more rarest configuration. The highest + loading time affects hardware and related drivers that can't run + then engine anyway. + + For known OpenGL version, + see https://en.wikipedia.org/wiki/OpenGL#Version_history + + For information about core, compatibility and forward profiles, + see https://www.khronos.org/opengl/wiki/OpenGL_Context */ + + struct { + int major; + int minor; + glProfile profile; + bool testByDefault; + } glSupportArray[] { + { 4, 6, glProfile::CORE, false }, + { 4, 5, glProfile::CORE, false }, + { 4, 4, glProfile::CORE, false }, + { 4, 3, glProfile::CORE, false }, + { 4, 2, glProfile::CORE, false }, + { 4, 1, glProfile::CORE, false }, + { 4, 0, glProfile::CORE, false }, + { 3, 3, glProfile::CORE, false }, + { 3, 2, glProfile::CORE, true }, + { 3, 1, glProfile::COMPATIBILITY, false }, + { 3, 0, glProfile::COMPATIBILITY, false }, + { 2, 1, glProfile::COMPATIBILITY, true }, + { 2, 0, glProfile::COMPATIBILITY, true }, + { 1, 5, glProfile::COMPATIBILITY, true }, + { 1, 4, glProfile::COMPATIBILITY, true }, + { 1, 3, glProfile::COMPATIBILITY, true }, + { 1, 2, glProfile::COMPATIBILITY, true }, + { 1, 1, glProfile::COMPATIBILITY, true }, + { 1, 0, glProfile::COMPATIBILITY, true }, + }; + + logger.Debug( "Validating best OpenGL context." ); - do + bool needHighestExtended = !!r_glExtendedValidation->integer; + for ( int colorBits : {24, 16} ) { - if ( glContext != nullptr ) + for ( auto& row : glSupportArray ) { - SDL_GL_DeleteContext( glContext ); - glContext = nullptr; - } + if ( GLEWmajor < 2 && row.profile == glProfile::CORE ) + { + // GLEW version < 2.0.0 doesn't support OpenGL core profiles. + continue; + } - if ( window != nullptr ) - { - SDL_GetWindowPosition( window, &x, &y ); - logger.Debug("Existing window at %dx%d before being destroyed", x, y ); - SDL_DestroyWindow( window ); - window = nullptr; + if ( !needHighestExtended && !row.testByDefault ) + { + continue; + } + + glConfiguration testConfiguration; + testConfiguration.major = row.major; + testConfiguration.minor = row.minor; + testConfiguration.profile = row.profile; + testConfiguration.colorBits = colorBits; + + if ( !GLimp_RecreateWindowWhenChange( false, false, testConfiguration ) ) + { + return rserr_t::RSERR_INVALID_MODE; + } + + if ( GLimp_CreateContext( testConfiguration ) ) + { + if ( needHighestExtended ) + { + needHighestExtended = false; + extendedValidationConfiguration = testConfiguration; + } + + if ( row.testByDefault ) + { + bestValidatedConfiguration = testConfiguration; + return rserr_t::RSERR_OK; + } + } } - // we come back here if we couldn't get a visual and there's - // something we can switch off + } + + if ( bestValidatedConfiguration.major == 0 ) + { + + return rserr_t::RSERR_MISSING_GL; + } + + return rserr_t::RSERR_OLD_GL; +} - if ( fullscreen ) +static glConfiguration GLimp_ApplyCustomOptions( const int GLEWmajor, const glConfiguration &bestConfiguration ) +{ + glConfiguration customConfiguration = {}; + + if ( bestConfiguration.profile == glProfile::CORE && !Q_stricmp( r_glProfile->string, "compat" ) ) + { + logger.Debug( "Compatibility profile is forced by r_glProfile" ); + + customConfiguration.profile = glProfile::COMPATIBILITY; + } + + if ( bestConfiguration.profile == glProfile::COMPATIBILITY && !Q_stricmp( r_glProfile->string, "core" ) ) + { + if ( GLEWmajor < 2 ) { - flags |= SDL_WINDOW_FULLSCREEN; + // GLEW version < 2.0.0 doesn't support OpenGL core profiles. + logger.Debug( "Core profile is ignored from r_glProfile" ); } - - if ( noborder ) + else { - flags |= SDL_WINDOW_BORDERLESS; + logger.Debug( "Core profile is forced by r_glProfile" ); + + customConfiguration.profile = glProfile::CORE; } + } - colorBits = r_colorbits->integer; + customConfiguration.major = std::max( 0, r_glMajorVersion->integer ); + customConfiguration.minor = std::max( 0, r_glMinorVersion->integer ); + + if ( customConfiguration.major == 0 ) + { + customConfiguration.major = bestConfiguration.major; + customConfiguration.minor = bestConfiguration.minor; + } + else if ( customConfiguration.major == 1 ) + { + logger.Warn( "OpenGL %d.%d is not supported, trying %d.%d instead", + customConfiguration.major, + customConfiguration.minor, + bestConfiguration.major, + bestConfiguration.minor ); - if ( ( !colorBits ) || ( colorBits >= 32 ) ) + customConfiguration.major = bestConfiguration.major; + customConfiguration.minor = bestConfiguration.minor; + } + else + { + if ( customConfiguration.major == 3 + && customConfiguration.minor < 2 + && customConfiguration.profile == glProfile::UNDEFINED ) { - colorBits = 24; + customConfiguration.profile = glProfile::COMPATIBILITY; } + else if ( customConfiguration.major == 2 ) + { + if ( customConfiguration.profile == glProfile::UNDEFINED ) + { + customConfiguration.profile = glProfile::COMPATIBILITY; + } - alphaBits = r_alphabits->integer; + if ( customConfiguration.minor == 0 ) + { + logger.Warn( "OpenGL 2.0 is not supported, trying 2.1 instead" ); - if ( alphaBits < 0 ) - { - alphaBits = 0; + customConfiguration.minor = 1; + } } - depthBits = r_depthbits->integer; + logger.Debug( "GL version %d.%d is forced by r_glMajorVersion and r_glMinorVersion", + customConfiguration.major, + customConfiguration.minor ); + } - if ( !depthBits ) - { - depthBits = 24; - } + if ( customConfiguration.profile == glProfile::UNDEFINED ) + { + customConfiguration.profile = bestConfiguration.profile; + } - stencilBits = r_stencilbits->integer; - samples = r_ext_multisample->integer; + customConfiguration.colorBits = std::max( 0, r_colorbits->integer ); - for ( i = 0; i < 4; i++ ) + if ( customConfiguration.colorBits == 0 ) + { + customConfiguration.colorBits = bestConfiguration.colorBits; + } + else + { + if ( customConfiguration.colorBits != bestConfiguration.colorBits ) { - int testColorBits, testCore; - int major = r_glMajorVersion->integer; - int minor = r_glMinorVersion->integer; - - // 0 - 24 bit color, core - // 1 - 24 bit color, compat - // 2 - 16 bit color, core - // 3 - 16 bit color, compat - testColorBits = (i >= 2) ? 16 : 24; - testCore = ((i & 1) == 0); - - if( testCore && !Q_stricmp(r_glProfile->string, "compat") ) - continue; + logger.Debug( "Color framebuffer bitness %d is forced by r_colorbits", + customConfiguration.colorBits ); + } + } - if( testCore && GLEWmajor < 2 ) - continue; + return customConfiguration; +} - if( !testCore && !Q_stricmp(r_glProfile->string, "core") ) - continue; +static std::string ContextDescription( const glConfiguration& configuration ) +{ + return Str::Format( "%d-bit OpenGL %d.%d %s", + configuration.colorBits, + configuration.major, + configuration.minor, + GLimp_getProfileName( configuration.profile ) ); +} - if( testColorBits > colorBits ) - continue; +static bool CreateWindowAndContext( + bool fullscreen, bool bordered, + Str::StringRef contextAdjective, + const glConfiguration& customConfiguration) +{ + if ( !GLimp_RecreateWindowWhenChange( fullscreen, bordered, customConfiguration ) ) + { + logger.Warn( "Failed to create window for %s context - %s", + contextAdjective, + ContextDescription( customConfiguration ) ); + return false; + } - if ( testColorBits == 24 ) - { - perChannelColorBits = 8; - } - else - { - perChannelColorBits = 4; - } + if ( !GLimp_CreateContext( customConfiguration ) ) + { + logger.Warn( "Failed to initialize %s context - %s", + contextAdjective, + ContextDescription( customConfiguration ) ); + logger.Warn( "SDL_GL_CreateContext failed: %s", SDL_GetError() ); + return false; + } - SDL_GL_SetAttribute( SDL_GL_RED_SIZE, perChannelColorBits ); - SDL_GL_SetAttribute( SDL_GL_GREEN_SIZE, perChannelColorBits ); - SDL_GL_SetAttribute( SDL_GL_BLUE_SIZE, perChannelColorBits ); - SDL_GL_SetAttribute( SDL_GL_DOUBLEBUFFER, 1 ); + logger.Notice( "Using %s context - %s", + contextAdjective, + ContextDescription( customConfiguration ) ); + return true; +} - if ( !r_glAllowSoftware->integer ) - { - SDL_GL_SetAttribute( SDL_GL_ACCELERATED_VISUAL, 1 ); - } +static void GLimp_RegisterConfiguration( const glConfiguration& highestConfiguration, const glConfiguration &requestedConfiguration ) +{ + glConfig2.glHighestMajor = highestConfiguration.major; + glConfig2.glHighestMinor = highestConfiguration.minor; - if( testCore && (major < 3 || (major == 3 && minor < 2)) ) { - major = 3; - minor = 2; - } + glConfig2.glRequestedMajor = requestedConfiguration.major; + glConfig2.glRequestedMinor = requestedConfiguration.minor; - if( major < 2 || (major == 2 && minor < 1)) { - major = 2; - minor = 1; - } + SDL_GL_SetSwapInterval( r_swapInterval->integer ); - SDL_GL_SetAttribute( SDL_GL_CONTEXT_MAJOR_VERSION, major ); - SDL_GL_SetAttribute( SDL_GL_CONTEXT_MINOR_VERSION, minor ); + { + /* Make sure we don't silence any useful error that would + already have happened. */ + GL_CheckErrors(); - if ( testCore ) - { - SDL_GL_SetAttribute( SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE ); - } - else - { - SDL_GL_SetAttribute( SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_COMPATIBILITY ); - } + // Check if we have a core profile. + int profileBit; + glGetIntegerv( GL_CONTEXT_PROFILE_MASK, &profileBit ); - if ( r_glDebugProfile->integer ) - { - SDL_GL_SetAttribute( SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_DEBUG_FLAG ); - } + /* OpenGL implementations not supporting core profile like the ones only + implementing OpenGL version older than 3.2 may raise a GL_INVALID_ENUM + error while implementations supporting core profile may not raise the + error when forcing an OpenGL version older than 3.2, so we better want + to catch and silence this expected error. */ - if ( !window ) - { - window = SDL_CreateWindow( CLIENT_WINDOW_TITLE, x, y, glConfig.vidWidth, glConfig.vidHeight, flags ); + if ( glGetError() != GL_NO_ERROR ) + { + glConfig2.glCoreProfile = false; + } + else + { + glConfig2.glCoreProfile = ( profileBit == GL_CONTEXT_CORE_PROFILE_BIT ); + } - if ( !window ) - { - logger.Warn("SDL_CreateWindow failed: %s\n", SDL_GetError() ); - continue; - } - } + glProfile providedProfile = glConfig2.glCoreProfile ? glProfile::CORE : glProfile::COMPATIBILITY ; + const char *providedProfileName = GLimp_getProfileName( providedProfile ); - SDL_SetWindowIcon( window, icon ); + if ( providedProfile != requestedConfiguration.profile ) + { + const char *requestedProfileName = GLimp_getProfileName( requestedConfiguration.profile ); - glContext = SDL_GL_CreateContext( window ); + logger.Warn( "Provided OpenGL %s profile is not the same as requested %s profile.", + providedProfileName, + requestedProfileName ); + } + else + { + logger.Debug( "Provided OpenGL context uses %s profile.", + providedProfileName ); + } + } - if ( !glContext ) - { - logger.Warn("SDL_GL_CreateContext failed: %s\n", SDL_GetError() ); - continue; - } - SDL_GL_SetSwapInterval( r_swapInterval->integer ); + { + int providedRedChannelColorBits; + SDL_GL_GetAttribute( SDL_GL_RED_SIZE, &providedRedChannelColorBits ); + + glConfig.colorBits = providedRedChannelColorBits == 8 ? 24 : 16; - // Fill window with a dark grey (#141414) background. - glClearColor( 0.08f, 0.08f, 0.08f, 1.0f ); - glClear( GL_COLOR_BUFFER_BIT ); - GLimp_EndFrame(); + if ( requestedConfiguration.colorBits != glConfig.colorBits ) + { + logger.Warn( "Provided OpenGL %d-bit channel depth is not the same as requested %d-bit depth.", + glConfig.colorBits, requestedConfiguration.colorBits ); + } + else + { + logger.Debug( "Provided OpenGL context uses %d-bit channel depth.", glConfig.colorBits ); + } + } - glConfig.colorBits = testColorBits; - glConfig.depthBits = depthBits; - glConfig.stencilBits = stencilBits; - glConfig2.glCoreProfile = testCore; + if ( requestedConfiguration.profile == glProfile::CORE ) + { + // Check if context is forward compatible. + int contextFlags; + glGetIntegerv( GL_CONTEXT_FLAGS, &contextFlags ); - logger.Notice("Using %d Color bits, %d depth, %d stencil display.", - glConfig.colorBits, glConfig.depthBits, glConfig.stencilBits ); + glConfig2.glForwardCompatibleContext = contextFlags & GL_CONTEXT_FLAG_FORWARD_COMPATIBLE_BIT; - break; + if ( glConfig2.glForwardCompatibleContext ) + { + logger.Debug( "Provided OpenGL core context is forward compatible." ); } - - if ( samples && ( !glContext || !window ) ) + else { - r_ext_multisample->integer = 0; + logger.Debug( "Provided OpenGL core context is not forward compatible." ); } + } + else + { + glConfig2.glForwardCompatibleContext = false; + } - } while ( ( !glContext || !window ) && samples ); + // alphaBits was used by legacy renderer, do we need it for anything? + // int alphaBits = std::max( 0, r_alphabits->integer ); - SDL_FreeSurface( icon ); + /* FIXME: It looks like MSAA was only implemented in legacy renderer. + int samples = std::max( 0, r_ext_multisample->integer ); */ - glewResult = glewInit(); + { + int GLmajor, GLminor; + sscanf( ( const char * ) glGetString( GL_VERSION ), "%d.%d", &GLmajor, &GLminor ); + + glConfig2.glMajor = GLmajor; + glConfig2.glMinor = GLminor; + } + + /* FIXME: a duplicate of this is done in GLimp_Init, also storing it + for gfxinfo command. */ + const char *glstring = ( char * ) glGetString( GL_RENDERER ); + logger.Notice("OpenGL Renderer: %s", glstring ); +} + +static void GLimp_DrawWindowContent() +{ + // Fill window with a dark grey (#141414) background. + glClearColor( 0.08f, 0.08f, 0.08f, 1.0f ); + glClear( GL_COLOR_BUFFER_BIT ); + GLimp_EndFrame(); +} + +static rserr_t GLimp_CheckOpenGLVersion( const glConfiguration &requestedConfiguration ) +{ + if ( glConfig2.glMajor != requestedConfiguration.major + || glConfig2.glMinor != requestedConfiguration.minor ) + { + logger.Warn( "Provided OpenGL %d.%d is not the same as requested %d.%d version", + glConfig2.glMajor, + glConfig2.glMinor, + requestedConfiguration.major, + requestedConfiguration.minor ); + } + else + { + logger.Debug( "Provided OpenGL %d.%d version.", + glConfig2.glMajor, + glConfig2.glMinor ); + } + + if ( glConfig2.glMajor < 2 || ( glConfig2.glMajor == 2 && glConfig2.glMinor < 1 ) ) + { + GLimp_DestroyWindowIfExists(); + + // Missing shader support, there is no OpenGL 1.x renderer anymore. + return rserr_t::RSERR_OLD_GL; + } + + if ( glConfig2.glMajor < 3 || ( glConfig2.glMajor == 3 && glConfig2.glMinor < 2 ) ) + { + // Shaders are supported, but not all OpenGL 3.x features + logger.Notice("Using GL3 Renderer in OpenGL 2.x mode..." ); + } + else + { + logger.Notice("Using GL3 Renderer in OpenGL 3.x mode..." ); + glConfig.driverType = glDriverType_t::GLDRV_OPENGL3; + } + + return rserr_t::RSERR_OK; +} + +static void GLimp_CheckGLEW( const glConfiguration &requestedConfiguration ) +{ + GLenum glewResult = glewInit(); #ifdef GLEW_ERROR_NO_GLX_DISPLAY if ( glewResult != GLEW_OK && glewResult != GLEW_ERROR_NO_GLX_DISPLAY ) @@ -756,36 +1204,165 @@ static rserr_t GLimp_SetMode( int mode, bool fullscreen, bool noborder ) #endif { // glewInit failed, something is seriously wrong - Sys::Error( "GLW_StartOpenGL() - could not load OpenGL subsystem: %s", glewGetErrorString( glewResult ) ); + + GLimp_DestroyWindowIfExists(); + + const char* requestedProfileName = GLimp_getProfileName( requestedConfiguration.profile ); + + Sys::Error( "GLEW initialization failed: %s.\n\n" + "Engine successfully created\n" + "%d-bit OpenGL %d.%d %d context,\n" + "This is a GLEW issue.", + glewGetErrorString( glewResult ), + requestedConfiguration.colorBits, + requestedConfiguration.major, + requestedConfiguration.minor, + requestedProfileName ); } - else +} + +/* +=============== +GLimp_SetMode +=============== +*/ +static rserr_t GLimp_SetMode( const int mode, const bool fullscreen, const bool bordered ) +{ + logger.Notice("Initializing OpenGL display" ); + + int GLEWmajor; + { - logger.Notice("Using GLEW %s", glewGetString( GLEW_VERSION ) ); + const GLubyte * glewVersion = glewGetString( GLEW_VERSION ); + + logger.Notice("Using GLEW version %s", glewVersion ); + + int GLEWminor, GLEWmicro; + + sscanf( ( const char * ) glewVersion, "%d.%d.%d", + &GLEWmajor, &GLEWminor, &GLEWmicro ); + + if ( GLEWmajor < 2 ) + { + logger.Warn( "GLEW version < 2.0.0 doesn't support OpenGL core profiles." ); + } } - sscanf( ( const char * ) glGetString( GL_VERSION ), "%d.%d", &GLmajor, &GLminor ); - if ( GLmajor < 2 || ( GLmajor == 2 && GLminor < 1 ) ) { - // missing shader support, there is no 1.x renderer anymore - return rserr_t::RSERR_OLD_GL; + rserr_t err = GLimp_SetModeAndResolution( mode ); + + if ( err != rserr_t::RSERR_OK ) + { + return err; + } } - if ( GLmajor < 3 || ( GLmajor == 3 && GLminor < 2 ) ) + // HACK: We want to set the current value, not the latched value + Cvar::ClearFlags("r_customwidth", CVAR_LATCH); + Cvar::ClearFlags("r_customheight", CVAR_LATCH); + Cvar_Set( "r_customwidth", va("%d", glConfig.vidWidth ) ); + Cvar_Set( "r_customheight", va("%d", glConfig.vidHeight ) ); + Cvar::AddFlags("r_customwidth", CVAR_LATCH); + Cvar::AddFlags("r_customheight", CVAR_LATCH); + + // Reuse best configuration on vid_restart + // unless glExtendedValidation is modified. + static glConfiguration bestValidatedConfiguration = {}; // considering only up to OpenGL 3.2 + static glConfiguration extendedValidationResult = {}; // max available OpenGL version for diagnostic purposes + + if ( r_glExtendedValidation->integer && extendedValidationResult.major != 0 ) { - // shaders are supported, but not all GL3.x features - logger.Notice("Using enhanced (GL3) Renderer in GL 2.x mode..." ); + logger.Debug( "Previously best validated context: %s", ContextDescription( extendedValidationResult ) ); } - else + else if ( bestValidatedConfiguration.major == 0 || r_glExtendedValidation->integer ) { - logger.Notice("Using enhanced (GL3) Renderer in GL 3.x mode..." ); - glConfig.driverType = glDriverType_t::GLDRV_OPENGL3; + // Detect best configuration. + rserr_t err = GLimp_ValidateBestContext( GLEWmajor, bestValidatedConfiguration, extendedValidationResult ); + + if ( err != rserr_t::RSERR_OK ) + { + if ( err == rserr_t::RSERR_OLD_GL ) + { + // Used by error message. + glConfig2.glMajor = bestValidatedConfiguration.major; + glConfig2.glMinor = bestValidatedConfiguration.minor; + } + + GLimp_DestroyWindowIfExists(); + return err; + } } - GLimp_DetectAvailableModes(); - glstring = ( char * ) glGetString( GL_RENDERER ); - logger.Notice("GL_RENDERER: %s", glstring ); + if ( r_glExtendedValidation->integer ) + { + logger.Notice( "Highest available context: %s", ContextDescription( extendedValidationResult ) ); + } - return rserr_t::RSERR_OK; + glConfiguration requestedConfiguration = {}; // The one we end up using in CreateContext calls etc. + + // Attempt to apply custom configuration if exists. + glConfiguration customConfiguration = GLimp_ApplyCustomOptions( GLEWmajor, bestValidatedConfiguration ); + if ( customConfiguration != bestValidatedConfiguration && + CreateWindowAndContext( fullscreen, bordered, "custom", customConfiguration ) ) + { + requestedConfiguration = customConfiguration; + } + + if ( requestedConfiguration.major == 0 ) + { + if ( !CreateWindowAndContext(fullscreen, bordered, "preferred", bestValidatedConfiguration ) ) + { + GLimp_DestroyWindowIfExists(); + return rserr_t::RSERR_INVALID_MODE; + } + requestedConfiguration = bestValidatedConfiguration; + } + + GLimp_DrawWindowContent(); + + GLimp_RegisterConfiguration( extendedValidationResult, requestedConfiguration ); + + { + rserr_t err = GLimp_CheckOpenGLVersion( requestedConfiguration ); + + if ( err != rserr_t::RSERR_OK ) + { + return err; + } + } + + /* GLimp_CheckGLEW() calls Sys::Error directly so it does not return + in case of error. */ + + GLimp_CheckGLEW( requestedConfiguration ); + + /* When calling GLimp_CreateContext() some drivers may provide a valid + context that is unusable while GL_CheckErrors() catches nothing. + + For example 3840×2160 is too large for the Radeon 9700 and + the Mesa r300 driver may print this error when the requested + resolution is higher than what is supported by hardware: + + > r300: Implementation error: Render targets are too big in r300_set_framebuffer_state, refusing to bind framebuffer state! + + The engine would later make a segfault when calling GL_SetDefaultState + from tr_init.cpp if we don't do anything. + + Hopefully it looks like SDL_GetWindowDisplayMode can raise this error: + + > Couldn't find display mode match + + so we catch this SDL error when calling GLimp_DetectAvailableModes() + and force a safe lower resolution before we start to draw to prevent + an engine segfault when calling GL_SetDefaultState(). */ + + if ( GLimp_DetectAvailableModes() ) + { + return rserr_t::RSERR_OK; + } + + // The engine will retry with a default mode. + return rserr_t::RSERR_UNKNOWN; } /* @@ -793,7 +1370,7 @@ static rserr_t GLimp_SetMode( int mode, bool fullscreen, bool noborder ) GLimp_StartDriverAndSetMode =============== */ -static bool GLimp_StartDriverAndSetMode( int mode, bool fullscreen, bool noborder ) +static bool GLimp_StartDriverAndSetMode( int mode, bool fullscreen, bool bordered ) { int numDisplays; @@ -804,11 +1381,21 @@ static bool GLimp_StartDriverAndSetMode( int mode, bool fullscreen, bool noborde SDL_GetVersion( &v ); logger.Notice("SDL_Init( SDL_INIT_VIDEO )... " ); - logger.Notice("Using SDL Version %u.%u.%u", v.major, v.minor, v.patch ); + logger.Notice("Using SDL version %u.%u.%u", v.major, v.minor, v.patch ); + + /* It is recommended to test for negative value and not just -1. + + > Returns 0 on success or a negative error code on failure; + > call SDL_GetError() for more information. + > -- https://wiki.libsdl.org/SDL_Init - if ( SDL_Init( SDL_INIT_VIDEO | SDL_INIT_NOPARACHUTE ) == -1 ) + the SDL_GetError page also gives a sample of code testing for < 0 + > if (SDL_Init(SDL_INIT_EVERYTHING) < 0) + > -- https://wiki.libsdl.org/SDL_GetError */ + + if ( SDL_Init( SDL_INIT_VIDEO ) < 0 ) { - logger.Notice("SDL_Init(SDL_INIT_VIDEO | SDL_INIT_NOPARACHUTE) FAILED (%s)", SDL_GetError() ); + logger.Notice("SDL_Init( SDL_INIT_VIDEO ) failed: %s", SDL_GetError() ); return false; } @@ -827,7 +1414,7 @@ static bool GLimp_StartDriverAndSetMode( int mode, bool fullscreen, bool noborde if ( numDisplays <= 0 ) { - Sys::Error( "SDL_GetNumVideoDisplays FAILED (%s)\n", SDL_GetError() ); + Sys::Error( "SDL_GetNumVideoDisplays failed: %s\n", SDL_GetError() ); } AssertCvarRange( r_displayIndex, 0, numDisplays - 1, true ); @@ -840,27 +1427,48 @@ static bool GLimp_StartDriverAndSetMode( int mode, bool fullscreen, bool noborde fullscreen = false; } - rserr_t err = GLimp_SetMode(mode, fullscreen, noborder); + rserr_t err = GLimp_SetMode(mode, fullscreen, bordered); switch ( err ) { + case rserr_t::RSERR_OK: + return true; + case rserr_t::RSERR_INVALID_FULLSCREEN: logger.Warn("GLimp: Fullscreen unavailable in this mode" ); - return false; + break; case rserr_t::RSERR_INVALID_MODE: - logger.Warn("GLimp: Could not set the given mode (%d)", mode ); - return false; + logger.Warn("GLimp: Could not set mode %d", mode ); + break; + + case rserr_t::RSERR_MISSING_GL: + Sys::Error( + "OpenGL is not available.\n\n" + "You need a graphic card with drivers supporting\n" + "at least OpenGL 3.2 or OpenGL 2.1 with\n" + "ARB_half_float_vertex and ARB_framebuffer_object." ); + + // Sys:Error calls OSExit() so the break and the return is unreachable. + break; case rserr_t::RSERR_OLD_GL: - logger.Warn("GLimp: OpenGL too old" ); - return false; + Sys::Error( + "OpenGL %d.%d is too old.\n\n" + "You need a graphic card with drivers supporting\n" + "at least OpenGL 3.2 or OpenGL 2.1 with\n" + "ARB_half_float_vertex and ARB_framebuffer_object." ); + // Sys:Error calls OSExit() so the break and the return is unreachable. + break; + + case rserr_t::RSERR_UNKNOWN: default: + logger.Warn("GLimp: Unknown error for mode %d", mode ); break; } - return true; + return false; } static GLenum debugTypes[] = @@ -966,6 +1574,15 @@ static bool LoadExt( int flags, bool hasExt, const char* name, bool test = true if ( test ) { logger.WithoutSuppression().Notice( "...using GL_%s", name ); + + if ( glConfig2.glEnabledExtensionsString.length() != 0 ) + { + glConfig2.glEnabledExtensionsString += " "; + } + + glConfig2.glEnabledExtensionsString += "GL_"; + glConfig2.glEnabledExtensionsString += name; + return true; } else @@ -980,11 +1597,19 @@ static bool LoadExt( int flags, bool hasExt, const char* name, bool test = true { if ( flags & ExtFlag_REQUIRED ) { - Sys::Error( "Required extension GL_%s is missing", name ); + Sys::Error( "Required extension GL_%s is missing.", name ); } else { - logger.WithoutSuppression().Notice( "...GL_%s not found", name ); + logger.WithoutSuppression().Notice( "...GL_%s not found.", name ); + + if ( glConfig2.glMissingExtensionsString.length() != 0 ) + { + glConfig2.glMissingExtensionsString += " "; + } + + glConfig2.glMissingExtensionsString += "GL_"; + glConfig2.glMissingExtensionsString += name; } } return false; @@ -998,6 +1623,9 @@ static void GLimp_InitExtensions() { logger.Notice("Initializing OpenGL extensions" ); + glConfig2.glEnabledExtensionsString = std::string(); + glConfig2.glMissingExtensionsString = std::string(); + if ( LOAD_EXTENSION_WITH_TEST( ExtFlag_NONE, ARB_debug_output, r_glDebugProfile->value ) ) { glDebugMessageCallbackARB( (GLDEBUGPROCARB)GLimp_DebugCallback, nullptr ); @@ -1213,7 +1841,7 @@ bool GLimp_Init() } // Create the window and set up the context - if ( GLimp_StartDriverAndSetMode( r_mode->integer, r_fullscreen->integer, r_noBorder->value ) ) + if ( GLimp_StartDriverAndSetMode( r_mode->integer, r_fullscreen->integer, !r_noBorder->value ) ) { goto success; } @@ -1223,7 +1851,7 @@ bool GLimp_Init() { logger.Notice("Setting r_mode %d failed, falling back on r_mode %d", r_mode->integer, R_MODE_FALLBACK ); - if ( GLimp_StartDriverAndSetMode( R_MODE_FALLBACK, false, false ) ) + if ( GLimp_StartDriverAndSetMode( R_MODE_FALLBACK, false, true ) ) { goto success; } @@ -1248,25 +1876,67 @@ bool GLimp_Init() Q_strncpyz( glConfig.version_string, ( char * ) glGetString( GL_VERSION ), sizeof( glConfig.version_string ) ); + glConfig2.glExtensionsString = std::string(); + if ( glConfig.driverType == glDriverType_t::GLDRV_OPENGL3 ) { GLint numExts, i; glGetIntegerv( GL_NUM_EXTENSIONS, &numExts ); - glConfig.extensions_string[ 0 ] = '\0'; + logger.Debug( "Found %d OpenGL extensions.", numExts ); + + std::string glExtensionsString = std::string(); + for ( i = 0; i < numExts; ++i ) { - if ( i != 0 ) + char* s = ( char * ) glGetStringi( GL_EXTENSIONS, i ); + + /* Check for errors when fetching string. + + > If an error is generated, glGetString returns 0. + > -- https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glGetString.xhtml */ + if ( s == nullptr ) { - Q_strcat( glConfig.extensions_string, sizeof( glConfig.extensions_string ), ( char * ) " " ); + logger.Warn( "Error when fetching OpenGL extension list." ); + } + else + { + std::string extensionName = s; + + if ( i != 0 ) + { + glExtensionsString += " "; + } + + glExtensionsString += extensionName; } - Q_strcat( glConfig.extensions_string, sizeof( glConfig.extensions_string ), ( char * ) glGetStringi( GL_EXTENSIONS, i ) ); } + + logger.Debug( "OpenGL extensions found: %s", glExtensionsString ); + + glConfig2.glExtensionsString = glExtensionsString; } else { - Q_strncpyz( glConfig.extensions_string, ( char * ) glGetString( GL_EXTENSIONS ), sizeof( glConfig.extensions_string ) ); + char* extensions_string = ( char * ) glGetString( GL_EXTENSIONS ); + + if ( extensions_string == nullptr ) + { + logger.Warn( "Error when fetching OpenGL extension list." ); + } + else + { + std::string glExtensionsString = extensions_string; + + int numExts = std::count(glExtensionsString.begin(), glExtensionsString.end(), ' '); + + logger.Debug( "Found %d OpenGL extensions.", numExts ); + + logger.Debug( "OpenGL extensions found: %s", glExtensionsString ); + + glConfig2.glExtensionsString = glExtensionsString; + } } if ( Q_stristr( glConfig.renderer_string, "amd " ) || @@ -1354,15 +2024,15 @@ void GLimp_EndFrame() =============== GLimp_HandleCvars -Responsible for handling cvars that change the window or GL state -Should only be called by the main thread +Responsible for handling cvars that change the window or OpenGL state, +should only be called by the main thread. =============== */ void GLimp_HandleCvars() { if ( r_swapInterval->modified ) { - /* Set the swap interval for the GL context. + /* Set the swap interval for the OpenGL context. * -1 : adaptive sync * 0 : immediate update