diff --git a/Changelog.md b/Changelog.md index 47c336af..1584ebc9 100644 --- a/Changelog.md +++ b/Changelog.md @@ -8,7 +8,20 @@ Note: Numbers starting with a "#" like #330 refer to the bugreport with that num ------------------------------------------------------------------------ * Enable/disable Soft Particles when **loading** a graphics quality preset (only enabled in Ultra preset, - though you can still configure it independently as before; #604) + though you can still configure it independently like before; #604) +* Support BC7-compressed (BPTC) .dds textures. They offer better quality than the older S3TC/DXT/BC1-3 + texture compression standard that Doom3 always supported. Mostly relevant for high-res retexturing + packs, because they offer similar quality as uncompressed TGAs while being smaller, using only + a quarter of the VRAM (TGA: 4 bytes per pixel, BC7: 1 byte per pixel) and loading *significantly* + faster because mipmaps are contained and don't have to be generated on load. + If you have such DDS files and want to use them (instead of TGAs), you must set + `image_usePrecompressedTextures 1` and `image_useNormalCompression 1`. + If you want to *create* .dds files with BC7 texture data, you can use any common texture compression + tool, **except** for **normalmaps**, those must be created with my [**customized bc7enc**](https://github.com/DanielGibson/bc7enc_rdo) + with the `-r2a` flag! *(Because Doom3 requires that normalmaps have the red channel moved into the + alpha channel, id confusingly called that "RXGB", and AFAIK no other tool supports that for BC7.)* + Just like the old DXT .dds files, they must be in the `dds/` subdirectory of a mod (either directly + in the filesystem or in a .pk4). * Support SDL3 (SDL2 and, to some degree, SDL1.2 are also still supported) * Fix bugs on 64bit Big Endian platforms (#472, #625) * Fixes for high-poly models (use heap allocation instead of `alloca()` for big buffers; #528) diff --git a/neo/framework/Common.cpp b/neo/framework/Common.cpp index 85affed3..a3d11dea 100644 --- a/neo/framework/Common.cpp +++ b/neo/framework/Common.cpp @@ -1463,7 +1463,7 @@ void Com_ExecMachineSpec_f( const idCmdArgs &args ) { cvarSystem->SetCVarInteger( "s_maxSoundsPerShader", 0, CVAR_ARCHIVE ); cvarSystem->SetCVarInteger( "image_useNormalCompression", 0, CVAR_ARCHIVE ); if ( !nores ) // DG: added optional "nores" argument - cvarSystem->SetCVarInteger( "", 4, CVAR_ARCHIVE ); + cvarSystem->SetCVarInteger( "r_mode", 4, CVAR_ARCHIVE ); cvarSystem->SetCVarInteger( "r_multiSamples", 0, CVAR_ARCHIVE ); } else if ( com_machineSpec.GetInteger() == 1 ) { // medium cvarSystem->SetCVarString( "image_filter", "GL_LINEAR_MIPMAP_LINEAR", CVAR_ARCHIVE ); diff --git a/neo/framework/Dhewm3SettingsMenu.cpp b/neo/framework/Dhewm3SettingsMenu.cpp index 85d04d88..12394fbf 100644 --- a/neo/framework/Dhewm3SettingsMenu.cpp +++ b/neo/framework/Dhewm3SettingsMenu.cpp @@ -1706,6 +1706,8 @@ static int initialMode = 0; static int initialCustomVidRes[2]; static int initialMSAAmode = 0; static int qualityPreset = 0; +static bool initialUsePrecomprTextures = false; +static int initialUseNormalCompr = false; static void SetVideoStuffFromCVars() { @@ -1734,6 +1736,9 @@ static void SetVideoStuffFromCVars() if ( qualityPreset == -1 ) qualityPreset = 1; // default to medium Quality } + + initialUsePrecomprTextures = globalImages->image_usePrecompressedTextures.GetBool(); + initialUseNormalCompr = globalImages->image_useNormalCompression.GetInteger(); } static bool VideoHasResettableChanges() @@ -1754,6 +1759,12 @@ static bool VideoHasResettableChanges() if ( initialMSAAmode != r_multiSamples.GetInteger() ) { return true; } + if ( initialUsePrecomprTextures != globalImages->image_usePrecompressedTextures.GetBool() ) { + return true; + } + if ( initialUseNormalCompr != globalImages->image_useNormalCompression.GetInteger() ) { + return true; + } return false; } @@ -1775,13 +1786,29 @@ static bool VideoHasApplyableChanges() return true; } + if ( initialUsePrecomprTextures != globalImages->image_usePrecompressedTextures.GetBool() ) { + return true; + } + // Note: value of image_useNormalCompression is only relevant if image_usePrecompressedTextures is enabled + if ( initialUsePrecomprTextures + && initialUseNormalCompr != globalImages->image_useNormalCompression.GetInteger() ) { + return true; + } + return false; } static void ApplyVideoSettings() { - cmdSystem->BufferCommandText( CMD_EXEC_APPEND, "vid_restart partial\n" ); + const char* cmd = "vid_restart partial\n"; + if ( initialUsePrecomprTextures != globalImages->image_usePrecompressedTextures.GetBool() + || initialUseNormalCompr != globalImages->image_useNormalCompression.GetInteger() ) + { + // these need a full restart (=> textures must be reloaded) + cmd = "vid_restart\n"; + } + cmdSystem->BufferCommandText( CMD_EXEC_APPEND, cmd ); } static void VideoResetChanges() @@ -1794,6 +1821,8 @@ static void VideoResetChanges() r_fullscreenDesktop.SetBool( initialFullscreenDesktop ); r_multiSamples.SetInteger( initialMSAAmode ); + globalImages->image_usePrecompressedTextures.SetBool( initialUsePrecomprTextures ); + globalImages->image_useNormalCompression.SetInteger( initialUseNormalCompr ); } static void InitVideoOptionsMenu() @@ -1937,6 +1966,36 @@ static void DrawVideoOptionsMenu() } AddCVarOptionTooltips( r_multiSamples, "Note: Not all GPUs/drivers support all modes, esp. not 16x!" ); + bool usePreComprTex = globalImages->image_usePrecompressedTextures.GetBool(); + if ( ImGui::Checkbox( "Use precompressed textures", &usePreComprTex ) ) { + globalImages->image_usePrecompressedTextures.SetBool(usePreComprTex); + // by default I guess people also want compressed normal maps when using this + // especially relevant for retexturing packs that only ship BC7 DDS files + // (otherwise the lowres TGA normalmaps would be used) + if ( usePreComprTex ) { + cvarSystem->SetCVarInteger( "image_useNormalCompression", 2 ); + } + } + const char* descr = "Use precompressed (.dds) textures. Faster loading, use less VRAM, possibly worse image quality.\n" + "May also be used by highres retexturing packs for BC7-compressed textures (there image quality is not impaired)"; + AddCVarOptionTooltips( globalImages->image_usePrecompressedTextures, descr ); + + ImGui::BeginDisabled( !usePreComprTex ); + bool useNormalCompr = globalImages->image_useNormalCompression.GetBool(); + ImGui::Dummy( ImVec2(16, 0) ); + ImGui::SameLine(); + if ( ImGui::Checkbox( "Use precompressed normalmaps", &useNormalCompr ) ) { + // image_useNormalCompression 1 is not supported by modern GPUs + globalImages->image_useNormalCompression.SetInteger(useNormalCompr ? 2 : 0); + } + if ( usePreComprTex ) { + const char* descr = "Also use precompressed textures for normalmaps"; + AddCVarOptionTooltips( globalImages->image_useNormalCompression, descr ); + } else { + AddTooltip( "Can only be used if precompressed textures are enabled!" ); + } + ImGui::EndDisabled(); + // Apply Button if ( !VideoHasApplyableChanges() ) { ImGui::BeginDisabled(); diff --git a/neo/renderer/Image.h b/neo/renderer/Image.h index 5eac4531..ec7f5d0d 100644 --- a/neo/renderer/Image.h +++ b/neo/renderer/Image.h @@ -125,6 +125,23 @@ typedef struct unsigned int dwReserved2[3]; } ddsFileHeader_t; +// DG: additional header that's right behind the ddsFileHeader_t +// ONLY IF ddsHeader.ddspf.dwFourCC == 'DX10' +// https://learn.microsoft.com/en-us/windows/win32/direct3ddds/dds-header-dxt10 +typedef struct +{ + // https://learn.microsoft.com/en-us/windows/win32/api/dxgiformat/ne-dxgiformat-dxgi_format + unsigned int dxgiFormat; // we only support DXGI_FORMAT_BC7_UNORM = 98; + // we *could* probably support DXGI_FORMAT_BC1_UNORM = 71, DXGI_FORMAT_BC2_UNORM = 74, DXGI_FORMAT_BC3_UNORM = 77 + // and map that to the old S3TC stuff, but I hope that tools writing those formats + // stick to just DX9-style ddsFileHeader_t to be more compatible? + + unsigned int resourceDimension; // 0: unknown, 2: Texture1D, 3: Texture2D, 4: Texture3D + unsigned int miscFlag; // 4 if 2D texture is cubemap, else 0 + unsigned int arraySize; // number of elements in texture array + unsigned int miscFlags2; // must be 0 for DX10, for DX11 has info about alpha channel (in lower 3 bits) +} ddsDXT10addHeader_t; + // increasing numeric values imply more information is stored typedef enum { diff --git a/neo/renderer/Image_init.cpp b/neo/renderer/Image_init.cpp index 89d9c578..f591b030 100644 --- a/neo/renderer/Image_init.cpp +++ b/neo/renderer/Image_init.cpp @@ -1217,7 +1217,7 @@ void R_ListImages_f( const idCmdArgs &args ) { if ( uncompressedOnly ) { if ( ( image->internalFormat >= GL_COMPRESSED_RGB_S3TC_DXT1_EXT && image->internalFormat <= GL_COMPRESSED_RGBA_S3TC_DXT5_EXT ) - || image->internalFormat == GL_COLOR_INDEX8_EXT ) { + || image->internalFormat == GL_COLOR_INDEX8_EXT || image->internalFormat == GL_COMPRESSED_RGBA_BPTC_UNORM_ARB ) { continue; } } diff --git a/neo/renderer/Image_load.cpp b/neo/renderer/Image_load.cpp index 0702c6d5..06dfc30c 100644 --- a/neo/renderer/Image_load.cpp +++ b/neo/renderer/Image_load.cpp @@ -37,8 +37,9 @@ PROBLEM: compressed textures may break the zero clamp rule! */ static bool FormatIsDXT( int internalFormat ) { - if ( internalFormat < GL_COMPRESSED_RGB_S3TC_DXT1_EXT - || internalFormat > GL_COMPRESSED_RGBA_S3TC_DXT5_EXT ) { + if ( (internalFormat < GL_COMPRESSED_RGB_S3TC_DXT1_EXT + || internalFormat > GL_COMPRESSED_RGBA_S3TC_DXT5_EXT) + && internalFormat != GL_COMPRESSED_RGBA_BPTC_UNORM ) { return false; } return true; @@ -86,6 +87,8 @@ int idImage::BitsForInternalFormat( int internalFormat ) const { return 8; case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT: return 8; + case GL_COMPRESSED_RGBA_BPTC_UNORM: + return 8; case GL_RGBA4: return 16; case GL_RGB5: @@ -1368,7 +1371,7 @@ bool idImage::CheckPrecompressedImage( bool fullLoad ) { } int len = f->Length(); - if ( len < sizeof( ddsFileHeader_t ) ) { + if ( len < sizeof( ddsFileHeader_t ) + 4 ) { // +4 for the magic 'DDS ' fourcc at the beginning fileSystem->CloseFile( f ); return false; } @@ -1392,6 +1395,7 @@ bool idImage::CheckPrecompressedImage( bool fullLoad ) { unsigned int magic = LittleInt( *(unsigned int *)data ); ddsFileHeader_t *_header = (ddsFileHeader_t *)(data + 4); int ddspf_dwFlags = LittleInt( _header->ddspf.dwFlags ); + unsigned int ddspf_dwFourCC = LittleInt( _header->ddspf.dwFourCC ); if ( magic != DDS_MAKEFOURCC('D', 'D', 'S', ' ')) { common->Printf( "CheckPrecompressedImage( %s ): magic != 'DDS '\n", imgName.c_str() ); @@ -1401,11 +1405,27 @@ bool idImage::CheckPrecompressedImage( bool fullLoad ) { // if we don't support color index textures, we must load the full image // should we just expand the 256 color image to 32 bit for upload? - if ( ddspf_dwFlags & DDSF_ID_INDEXCOLOR && !glConfig.sharedTexturePaletteAvailable ) { + if ( (ddspf_dwFlags & DDSF_ID_INDEXCOLOR) && !glConfig.sharedTexturePaletteAvailable ) { R_StaticFree( data ); return false; } + // DG: same if this is a BC7 (BPTC) texture but the GPU doesn't support that + // or if it uses the additional DX10 header and is *not* a BC7 texture + if ( ddspf_dwFourCC == DDS_MAKEFOURCC( 'D', 'X', '1', '0' ) ) { + ddsDXT10addHeader_t *dx10Header = (ddsDXT10addHeader_t *)( data + 4 + sizeof(ddsFileHeader_t) ); + unsigned int dxgiFormat = LittleInt( dx10Header->dxgiFormat ); + if ( dxgiFormat != 98 // DXGI_FORMAT_BC7_UNORM + || !glConfig.bptcTextureCompressionAvailable ) { + if (dxgiFormat != 98) { + common->Warning( "Image file '%s' has unsupported dxgiFormat %d - dhewm3 only supports DXGI_FORMAT_BC7_UNORM (98)!", + filename, dxgiFormat); + } + R_StaticFree( data ); + return false; + } + } + // upload all the levels UploadPrecompressedImage( data, len ); @@ -1455,6 +1475,7 @@ void idImage::UploadPrecompressedImage( byte *data, int len ) { uploadWidth = header->dwWidth; uploadHeight = header->dwHeight; + size_t additionalHeaderOffset = 0; // used if the DDS has a DDS_HEADER_DXT10 if ( header->ddspf.dwFlags & DDSF_FOURCC ) { switch ( header->ddspf.dwFourCC ) { case DDS_MAKEFOURCC( 'D', 'X', 'T', '1' ): @@ -1473,6 +1494,12 @@ void idImage::UploadPrecompressedImage( byte *data, int len ) { case DDS_MAKEFOURCC( 'R', 'X', 'G', 'B' ): internalFormat = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT; break; + case DDS_MAKEFOURCC( 'D', 'X', '1', '0' ): // BC7 aka BPTC + additionalHeaderOffset = 20; + // Note: this is a bit hacky, but in CheckPrecompressedImage() we made sure + // that only BC7 UNORM is accepted if the FourCC is 'DX10' + internalFormat = GL_COMPRESSED_RGBA_BPTC_UNORM; + break; default: common->Warning( "Invalid compressed internal format\n" ); return; @@ -1510,12 +1537,13 @@ void idImage::UploadPrecompressedImage( byte *data, int len ) { int uw = uploadWidth; int uh = uploadHeight; + int lastUW = uw, lastUH = uh; // We may skip some mip maps if we are downsizing int skipMip = 0; GetDownsize( uploadWidth, uploadHeight ); - byte *imagedata = data + sizeof(ddsFileHeader_t) + 4; + byte *imagedata = data + sizeof(ddsFileHeader_t) + 4 + additionalHeaderOffset; for ( int i = 0 ; i < numMipmaps; i++ ) { int size = 0; @@ -1535,6 +1563,8 @@ void idImage::UploadPrecompressedImage( byte *data, int len ) { qglTexImage2D( GL_TEXTURE_2D, i - skipMip, internalFormat, uw, uh, 0, externalFormat, GL_UNSIGNED_BYTE, imagedata ); } } + lastUW = uw; + lastUH = uh; imagedata += size; uw /= 2; @@ -1546,6 +1576,19 @@ void idImage::UploadPrecompressedImage( byte *data, int len ) { uh = 1; } } + // in case the mipmap chain is incomplete (doesn't go down to 1x1 pixel) + // the texture may be shown as black unless GL_TEXTURE_MAX_LEVEL is set accordingly + if ( lastUW > 1 || lastUH > 1 ) { + numMipmaps -= skipMip; + if ( numMipmaps == 1 ) { + // if there is only one mipmap, just don't use mipmapping for this texture + if ( filter == TF_DEFAULT ) { + filter = TF_LINEAR; + } + } else { + qglTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, numMipmaps - 1 ); + } + } SetImageFilterAndRepeat(); } @@ -2140,6 +2183,9 @@ void idImage::Print() const { case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT: common->Printf( "DXT5 " ); break; + case GL_COMPRESSED_RGBA_BPTC_UNORM: + common->Printf( "BC7 " ); + break; case GL_RGBA4: common->Printf( "RGBA4 " ); break; diff --git a/neo/renderer/RenderSystem.h b/neo/renderer/RenderSystem.h index 1474ee4f..95d41fd8 100644 --- a/neo/renderer/RenderSystem.h +++ b/neo/renderer/RenderSystem.h @@ -63,6 +63,7 @@ typedef struct glconfig_s { bool multitextureAvailable; bool textureCompressionAvailable; + bool bptcTextureCompressionAvailable; // DG: for GL_ARB_texture_compression_bptc (BC7) bool anisotropicAvailable; bool textureLODBiasAvailable; bool textureEnvAddAvailable; diff --git a/neo/renderer/RenderSystem_init.cpp b/neo/renderer/RenderSystem_init.cpp index b7b66cdd..7914093e 100644 --- a/neo/renderer/RenderSystem_init.cpp +++ b/neo/renderer/RenderSystem_init.cpp @@ -431,8 +431,12 @@ static void R_CheckPortableExtensions( void ) { glConfig.textureCompressionAvailable = true; qglCompressedTexImage2DARB = (PFNGLCOMPRESSEDTEXIMAGE2DARBPROC)GLimp_ExtensionPointer( "glCompressedTexImage2DARB" ); qglGetCompressedTexImageARB = (PFNGLGETCOMPRESSEDTEXIMAGEARBPROC)GLimp_ExtensionPointer( "glGetCompressedTexImageARB" ); + if ( R_CheckExtension( "GL_ARB_texture_compression_bptc" ) ) { + glConfig.bptcTextureCompressionAvailable = true; + } } else { glConfig.textureCompressionAvailable = false; + glConfig.bptcTextureCompressionAvailable = false; } // GL_EXT_texture_filter_anisotropic diff --git a/neo/renderer/qgl.h b/neo/renderer/qgl.h index abce7a8e..6b3f6f20 100644 --- a/neo/renderer/qgl.h +++ b/neo/renderer/qgl.h @@ -110,6 +110,14 @@ extern PFNGLSTENCILOPSEPARATEPROC qglStencilOpSeparate; extern PFNGLCOMPRESSEDTEXIMAGE2DARBPROC qglCompressedTexImage2DARB; extern PFNGLGETCOMPRESSEDTEXIMAGEARBPROC qglGetCompressedTexImageARB; +// ARB_texture_compression_bptc - uses ARB_texture_compression, just adds new constants +// that might be missing in old OpenGL headers +#ifndef GL_COMPRESSED_RGBA_BPTC_UNORM_ARB + // currently the only one we use, there's also COMPRESSED_SRGB_ALPHA_BPTC_UNORM_ARB (0x8E8D) + // and COMPRESSED_RGB_BPTC_SIGNED_FLOAT_ARB (0x8E8E) and COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_ARB (0x8E8F) + #define GL_COMPRESSED_RGBA_BPTC_UNORM_ARB 0x8E8C +#endif + // ARB_vertex_program / ARB_fragment_program extern PFNGLVERTEXATTRIBPOINTERARBPROC qglVertexAttribPointerARB; extern PFNGLENABLEVERTEXATTRIBARRAYARBPROC qglEnableVertexAttribArrayARB;