Merge pull request #661 from DanielGibson/bc7

Support BPTC (BC7) -compressed .dds textures.
This commit is contained in:
Daniel Gibson 2025-03-23 05:33:54 +01:00 committed by GitHub
commit b0e3f59c07
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 157 additions and 9 deletions

View file

@ -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)

View file

@ -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 );

View file

@ -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();

View file

@ -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 {

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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

View file

@ -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;