From b93d46cb3fceb5fe712bd76b79557c2267269160 Mon Sep 17 00:00:00 2001 From: Thomas Debesse Date: Mon, 4 Aug 2025 04:13:02 +0200 Subject: [PATCH 1/6] tr_shader: linearize fog colors --- src/engine/renderer/tr_shader.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/engine/renderer/tr_shader.cpp b/src/engine/renderer/tr_shader.cpp index c59bb64b89..fa9bbcfc09 100644 --- a/src/engine/renderer/tr_shader.cpp +++ b/src/engine/renderer/tr_shader.cpp @@ -4415,6 +4415,11 @@ static bool ParseShader( const char *_text ) return false; } + if ( tr.worldLinearizeTexture ) + { + convertFromSRGB( shader.fogParms.color ); + } + token = COM_ParseExt2( text, false ); if ( !token[ 0 ] ) From acdd787c8a4191fd708338aa3b08375b814282db Mon Sep 17 00:00:00 2001 From: Thomas Debesse Date: Wed, 6 Aug 2025 03:01:09 +0200 Subject: [PATCH 2/6] tr_image: create two fog images for naive and sRGB pipelines --- src/engine/renderer/tr_bsp.cpp | 2 ++ src/engine/renderer/tr_image.cpp | 47 ++++++++++++++++++-------------- src/engine/renderer/tr_local.h | 4 ++- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/src/engine/renderer/tr_bsp.cpp b/src/engine/renderer/tr_bsp.cpp index 5a4079173d..99bc9b6e99 100644 --- a/src/engine/renderer/tr_bsp.cpp +++ b/src/engine/renderer/tr_bsp.cpp @@ -3954,6 +3954,8 @@ void R_LoadEntities( lump_t *l, std::string &externalEntities ) tr.convertColorFromSRGB = convertColorFromSRGB_cheap; } } + + tr.fogImage = tr.worldLinearizeTexture ? tr.fogImageLinear : tr.fogImageNaive; } /* diff --git a/src/engine/renderer/tr_image.cpp b/src/engine/renderer/tr_image.cpp index 6a669a0556..131d35d86f 100644 --- a/src/engine/renderer/tr_image.cpp +++ b/src/engine/renderer/tr_image.cpp @@ -2405,26 +2405,28 @@ static void R_CreateFogImage() { // Fog image is always created because disabling fog is cheat. - int x, y; - byte *data, *ptr; - float d; - float borderColor[ 4 ]; - - constexpr int FOG_S = 256; - constexpr int FOG_T = 32; + constexpr size_t FOG_S = 256; + constexpr size_t FOG_T = 32; + constexpr channels = 4; - ptr = data = (byte*) ri.Hunk_AllocateTempMemory( FOG_S * FOG_T * 4 ); + byte *dataNaive, *ptrNaive; + byte *dataLinear, *ptrLinear; + ptrNaive = dataNaive = (byte*) ri.Hunk_AllocateTempMemory( FOG_S * FOG_T * channels ); + ptrLinear = dataLinear = (byte*) ri.Hunk_AllocateTempMemory( FOG_S * FOG_T * channels ); // S is distance, T is depth - for ( y = 0; y < FOG_T; y++ ) + for ( int y = 0; y < FOG_T; y++ ) { - for ( x = 0; x < FOG_S; x++ ) + for ( int x = 0; x < FOG_S; x++ ) { - d = R_FogFactor( ( x + 0.5f ) / FOG_S, ( y + 0.5f ) / FOG_T ); + float d = R_FogFactor( ( x + 0.5f ) / FOG_S, ( y + 0.5f ) / FOG_T ); - ptr[ 0 ] = ptr[ 1 ] = ptr[ 2 ] = 255; - ptr[ 3 ] = 255 * d; - ptr += 4; + ptrNaive[ 0 ] = ptrNaive[ 1 ] = ptrNaive[ 2 ] = 255; + ptrLinear[ 0 ] = ptrLinear[ 1 ] = ptrLinear[ 2 ] = 255; + ptrNaive[ 3 ] = 255 * d; + ptrLinear[ 3 ] = 255 * convertFromSRGB( d ); + ptrNaive += channels; + ptrLinear += channels; } } @@ -2436,13 +2438,18 @@ static void R_CreateFogImage() imageParams.filterType = filterType_t::FT_DEFAULT; imageParams.wrapType = wrapTypeEnum_t::WT_CLAMP; - tr.fogImage = R_CreateImage( "_fog", ( const byte ** ) &data, FOG_S, FOG_T, 1, imageParams ); - ri.Hunk_FreeTempMemory( data ); + tr.fogImageNaive = R_CreateImage( "_fogNaive", ( const byte ** ) &dataNaive, FOG_S, FOG_T, 1, imageParams ); + tr.fogImageLinear = R_CreateImage( "_fogLinear", ( const byte ** ) &dataLinear, FOG_S, FOG_T, 1, imageParams ); + + ri.Hunk_FreeTempMemory( dataNaive ); + ri.Hunk_FreeTempMemory( dataLinear ); + + /* Just to be safe and not leave a null pointer in the wild. + This is modified when a map is loaded. */ + tr.fogImage = tr.fogImageNaive; - borderColor[ 0 ] = 1.0; - borderColor[ 1 ] = 1.0; - borderColor[ 2 ] = 1.0; - borderColor[ 3 ] = 1; + vec4_t borderColor; + Vector4Set( borderColor, 1.0f, 1.0f, 1.0f, 1.0f ); glTexParameterfv( GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor ); } diff --git a/src/engine/renderer/tr_local.h b/src/engine/renderer/tr_local.h index c8d8759c90..12256c3603 100644 --- a/src/engine/renderer/tr_local.h +++ b/src/engine/renderer/tr_local.h @@ -2449,7 +2449,9 @@ enum image_t *defaultImage; image_t *cinematicImage[ MAX_IN_GAME_VIDEOS ]; - image_t *fogImage; + image_t *fogImage; // Will be set to either fogImageNaive or fogImageSrgb. + image_t *fogImageNaive; + image_t *fogImageLinear; image_t *whiteImage; // full of 0xff image_t *blackImage; // full of 0x0 image_t *redImage; From 68a08d3b8ec6d276138df3e476b7ba9bf364e3d4 Mon Sep 17 00:00:00 2001 From: Thomas Debesse Date: Wed, 6 Aug 2025 07:51:52 +0200 Subject: [PATCH 3/6] tr_image: store the fog alpha channel in red one so we can apply colorspace conversions on it --- .../renderer/glsl_source/fogGlobal_fp.glsl | 2 +- .../renderer/glsl_source/fogQuake3_fp.glsl | 2 +- src/engine/renderer/tr_image.cpp | 30 ++++++++----------- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/engine/renderer/glsl_source/fogGlobal_fp.glsl b/src/engine/renderer/glsl_source/fogGlobal_fp.glsl index 126fc12bcc..68a1705348 100644 --- a/src/engine/renderer/glsl_source/fogGlobal_fp.glsl +++ b/src/engine/renderer/glsl_source/fogGlobal_fp.glsl @@ -53,7 +53,7 @@ void main() // st.s = vertexDistanceToCamera; st.t = 1.0; - vec4 color = texture2D(u_ColorMap, st); + vec4 color = vec4( vec3( 1.0 ), texture2D( u_ColorMap, st ).r ); outputColor = UnpackColor( u_Color ) * color; } diff --git a/src/engine/renderer/glsl_source/fogQuake3_fp.glsl b/src/engine/renderer/glsl_source/fogQuake3_fp.glsl index ab31ca9fb3..7a9826c191 100644 --- a/src/engine/renderer/glsl_source/fogQuake3_fp.glsl +++ b/src/engine/renderer/glsl_source/fogQuake3_fp.glsl @@ -36,7 +36,7 @@ void main() { #insert material_fp - vec4 color = texture2D(u_FogMap, var_TexCoords); + vec4 color = vec4( vec3( 1.0 ), texture2D( u_FogMap, var_TexCoords ).r ); color *= var_Color; diff --git a/src/engine/renderer/tr_image.cpp b/src/engine/renderer/tr_image.cpp index 131d35d86f..c03f570ce9 100644 --- a/src/engine/renderer/tr_image.cpp +++ b/src/engine/renderer/tr_image.cpp @@ -2407,26 +2407,21 @@ static void R_CreateFogImage() constexpr size_t FOG_S = 256; constexpr size_t FOG_T = 32; - constexpr channels = 4; + constexpr size_t channels = 4; - byte *dataNaive, *ptrNaive; - byte *dataLinear, *ptrLinear; - ptrNaive = dataNaive = (byte*) ri.Hunk_AllocateTempMemory( FOG_S * FOG_T * channels ); - ptrLinear = dataLinear = (byte*) ri.Hunk_AllocateTempMemory( FOG_S * FOG_T * channels ); + byte *data, *ptr; + ptr = data = (byte*) ri.Hunk_AllocateTempMemory( FOG_S * FOG_T * channels ); // S is distance, T is depth - for ( int y = 0; y < FOG_T; y++ ) + for ( size_t y = 0; y < FOG_T; y++ ) { - for ( int x = 0; x < FOG_S; x++ ) + for ( size_t x = 0; x < FOG_S; x++ ) { float d = R_FogFactor( ( x + 0.5f ) / FOG_S, ( y + 0.5f ) / FOG_T ); - ptrNaive[ 0 ] = ptrNaive[ 1 ] = ptrNaive[ 2 ] = 255; - ptrLinear[ 0 ] = ptrLinear[ 1 ] = ptrLinear[ 2 ] = 255; - ptrNaive[ 3 ] = 255 * d; - ptrLinear[ 3 ] = 255 * convertFromSRGB( d ); - ptrNaive += channels; - ptrLinear += channels; + ptr[ 0 ] = 255 * d; + ptr[ 1 ] = ptr[ 2 ] = ptr[ 3 ] = 255; + ptr += channels; } } @@ -2438,11 +2433,12 @@ static void R_CreateFogImage() imageParams.filterType = filterType_t::FT_DEFAULT; imageParams.wrapType = wrapTypeEnum_t::WT_CLAMP; - tr.fogImageNaive = R_CreateImage( "_fogNaive", ( const byte ** ) &dataNaive, FOG_S, FOG_T, 1, imageParams ); - tr.fogImageLinear = R_CreateImage( "_fogLinear", ( const byte ** ) &dataLinear, FOG_S, FOG_T, 1, imageParams ); + tr.fogImageNaive = R_CreateImage( "_fogNaive", ( const byte ** ) &data, FOG_S, FOG_T, 1, imageParams ); + + imageParams.bits |= IF_SRGB; + tr.fogImageLinear = R_CreateImage( "_fogLinear", ( const byte ** ) &data, FOG_S, FOG_T, 1, imageParams ); - ri.Hunk_FreeTempMemory( dataNaive ); - ri.Hunk_FreeTempMemory( dataLinear ); + ri.Hunk_FreeTempMemory( data ); /* Just to be safe and not leave a null pointer in the wild. This is modified when a map is loaded. */ From f7892d2a3a91fdd18589a606d4e2cd9924fbbb2a Mon Sep 17 00:00:00 2001 From: Thomas Debesse Date: Wed, 6 Aug 2025 08:13:39 +0200 Subject: [PATCH 4/6] renderer: add support for the GL_RED image format --- src/engine/renderer/tr_backend.cpp | 3 +-- src/engine/renderer/tr_image.cpp | 18 ++++++++++++++++++ src/engine/renderer/tr_public.h | 1 + src/engine/sys/sdl_glimp.cpp | 5 +++++ 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/engine/renderer/tr_backend.cpp b/src/engine/renderer/tr_backend.cpp index a97f7347b0..877aa434ab 100644 --- a/src/engine/renderer/tr_backend.cpp +++ b/src/engine/renderer/tr_backend.cpp @@ -739,12 +739,11 @@ static GLint GL_ToSRGB( GLint internalFormat, bool isSRGB ) { switch ( format ) { -#if 0 // Not used in the code base. /* EXT_texture_sRGB_R8 extension. See: https://github.com/KhronosGroup/OpenGL-Registry/blob/main/extensions/EXT/EXT_texture_sRGB_R8.txt */ case GL_RED: + ASSERT( glConfig2.textureSrgbR8Available ); return GL_SR8_EXT; -#endif case GL_RGB: return GL_SRGB; case GL_RGBA: diff --git a/src/engine/renderer/tr_image.cpp b/src/engine/renderer/tr_image.cpp index c03f570ce9..c526323215 100644 --- a/src/engine/renderer/tr_image.cpp +++ b/src/engine/renderer/tr_image.cpp @@ -176,6 +176,7 @@ class ListImagesCmd : public Cmd::StaticCmd { GL_RGBA32UI, { "RGBA32UI", 16 } }, { GL_ALPHA16F_ARB, { "A16F", 2 } }, { GL_ALPHA32F_ARB, { "A32F", 4 } }, + { GL_RED, { "R8", 1 } }, { GL_R16F, { "R16F", 2 } }, { GL_R32F, { "R32F", 4 } }, { GL_LUMINANCE_ALPHA16F_ARB, { "LA16F", 4 } }, @@ -933,6 +934,18 @@ void R_UploadImage( const char *name, const byte **dataArray, int numLayers, int format = GL_DEPTH_STENCIL; internalFormat = GL_DEPTH24_STENCIL8; } + else if ( image->bits & IF_RED ) + { + if( isSRGB && !glConfig2.textureSrgbR8Available ) + { + Log::Warn("red image '%s' cannot be loaded as sRGB", image->name ); + internalFormat = GL_RGBA8; + } + else + { + internalFormat = GL_RED; + } + } else if ( image->bits & ( IF_RGBA16F | IF_RGBA32F | IF_TWOCOMP16F | IF_TWOCOMP32F | IF_ONECOMP16F | IF_ONECOMP32F ) ) { if( !glConfig2.textureFloatAvailable ) { @@ -2433,6 +2446,11 @@ static void R_CreateFogImage() imageParams.filterType = filterType_t::FT_DEFAULT; imageParams.wrapType = wrapTypeEnum_t::WT_CLAMP; + if ( glConfig2.textureSrgbR8Available ) + { + imageParams.bits |= IF_RED; + } + tr.fogImageNaive = R_CreateImage( "_fogNaive", ( const byte ** ) &data, FOG_S, FOG_T, 1, imageParams ); imageParams.bits |= IF_SRGB; diff --git a/src/engine/renderer/tr_public.h b/src/engine/renderer/tr_public.h index b879c3fe7a..c9f9877c35 100644 --- a/src/engine/renderer/tr_public.h +++ b/src/engine/renderer/tr_public.h @@ -121,6 +121,7 @@ struct glconfig2_t bool gpuShader4Available; bool gpuShader5Available; bool textureGatherAvailable; + bool textureSrgbR8Available; int maxDrawBuffers; float maxTextureAnisotropy; diff --git a/src/engine/sys/sdl_glimp.cpp b/src/engine/sys/sdl_glimp.cpp index 501de885f1..ecc7f62fc3 100644 --- a/src/engine/sys/sdl_glimp.cpp +++ b/src/engine/sys/sdl_glimp.cpp @@ -123,6 +123,8 @@ static Cvar::Cvar r_ext_texture_integer( "r_ext_texture_integer", "Use GL_EXT_texture_integer if available", Cvar::NONE, true ); static Cvar::Cvar r_ext_texture_rg( "r_ext_texture_rg", "Use GL_EXT_texture_rg if available", Cvar::NONE, true ); +static Cvar::Cvar r_ext_texture_srgb_r8( "r_ext_texture_srgb_r8", + "Use GL_EXT_texture_sRGB_R8 if available", Cvar::NONE, true ); static Cvar::Cvar r_khr_debug( "r_khr_debug", "Use GL_KHR_debug if available", Cvar::NONE, true ); static Cvar::Cvar r_khr_shader_subgroup( "r_khr_shader_subgroup", @@ -2019,6 +2021,7 @@ static void GLimp_InitExtensions() Cvar::Latch( r_ext_texture_float ); Cvar::Latch( r_ext_texture_integer ); Cvar::Latch( r_ext_texture_rg ); + Cvar::Latch( r_ext_texture_srgb_r8 ); Cvar::Latch( r_khr_debug ); Cvar::Latch( r_khr_shader_subgroup ); @@ -2198,6 +2201,8 @@ static void GLimp_InitExtensions() // made required in OpenGL 3.0 glConfig2.textureCompressionRGTCAvailable = LOAD_EXTENSION( ExtFlag_CORE, ARB_texture_compression_rgtc ); + glConfig2.textureSrgbR8Available = LOAD_EXTENSION_WITH_TEST( ExtFlag_NONE, EXT_texture_sRGB_R8, r_ext_texture_srgb_r8.Get() ); + // Texture - others glConfig2.textureAnisotropyAvailable = false; glConfig2.textureAnisotropy = 0.0f; From 48b66849f1334ec2c89da39c945b38101d346832 Mon Sep 17 00:00:00 2001 From: Thomas Debesse Date: Wed, 6 Aug 2025 08:15:06 +0200 Subject: [PATCH 5/6] tr_image: store the fogNaive image as GL_RED, and fogLinear as GL_SR8_ET when available --- src/engine/renderer/tr_image.cpp | 11 ++++------- src/engine/renderer/tr_local.h | 1 + 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/engine/renderer/tr_image.cpp b/src/engine/renderer/tr_image.cpp index c526323215..39addd0975 100644 --- a/src/engine/renderer/tr_image.cpp +++ b/src/engine/renderer/tr_image.cpp @@ -2442,18 +2442,15 @@ static void R_CreateFogImage() // the border color at the edges. OpenGL 1.2 has clamp-to-edge, which does // what we want. imageParams_t imageParams = {}; - imageParams.bits = IF_NOPICMIP; + imageParams.bits = IF_NOPICMIP | IF_RED; imageParams.filterType = filterType_t::FT_DEFAULT; imageParams.wrapType = wrapTypeEnum_t::WT_CLAMP; - if ( glConfig2.textureSrgbR8Available ) - { - imageParams.bits |= IF_RED; - } - tr.fogImageNaive = R_CreateImage( "_fogNaive", ( const byte ** ) &data, FOG_S, FOG_T, 1, imageParams ); - imageParams.bits |= IF_SRGB; + imageParams.bits = IF_NOPICMIP | IF_SRGB; + imageParams.bits |= glConfig2.textureSrgbR8Available ? IF_RED : 0; + tr.fogImageLinear = R_CreateImage( "_fogLinear", ( const byte ** ) &data, FOG_S, FOG_T, 1, imageParams ); ri.Hunk_FreeTempMemory( data ); diff --git a/src/engine/renderer/tr_local.h b/src/engine/renderer/tr_local.h index 12256c3603..c9ba9ebd1d 100644 --- a/src/engine/renderer/tr_local.h +++ b/src/engine/renderer/tr_local.h @@ -476,6 +476,7 @@ enum class ssaoMode { IF_DEPTH32 = BIT( 11 ), IF_PACKED_DEPTH24_STENCIL8 = BIT( 12 ), IF_LIGHTMAP = BIT( 13 ), + IF_RED = BIT( 14 ), IF_RGBE = BIT( 15 ), IF_ALPHATEST = BIT( 16 ), // FIXME: this is unused IF_ALPHA = BIT( 17 ), From 685a059fa3e4d2701f35282c33c5c93736c529f2 Mon Sep 17 00:00:00 2001 From: Thomas Debesse Date: Mon, 11 Aug 2025 16:44:32 +0200 Subject: [PATCH 6/6] tr_image: add a comment saying the current fog image implementation in linear pipeline is a hack --- src/engine/renderer/tr_image.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/engine/renderer/tr_image.cpp b/src/engine/renderer/tr_image.cpp index 39addd0975..052143f47b 100644 --- a/src/engine/renderer/tr_image.cpp +++ b/src/engine/renderer/tr_image.cpp @@ -2448,6 +2448,13 @@ static void R_CreateFogImage() tr.fogImageNaive = R_CreateImage( "_fogNaive", ( const byte ** ) &data, FOG_S, FOG_T, 1, imageParams ); + /* HACK: The previous fog image generator was calibrated for the + naive pipeline that was unaware of colorspaces. We need a new fog + image generator calibrated for the linear pipeline. It happens that + applying an sRGB-to-linear conversion of the alpha channel luckily + produces some good-enough results, and since we optimize the alpha + channel by storing it in a red-only image, this is very cheap to do. + A non-hacky implementation is welcome. */ imageParams.bits = IF_NOPICMIP | IF_SRGB; imageParams.bits |= glConfig2.textureSrgbR8Available ? IF_RED : 0;