Add BRX1 extension format and native in-BSP lightmap page sizes

q3map2 changes:
- BRX1 backward-compatible BSP extension (always v46, extra lumps
  after header with magic + overlap detection)
- -lightmapsize N stores pages natively in BSP via EXT_LUMP_LIGHTMAP_INFO
- -extlmhacksize N unchanged (legacy external TGA + custom shaders)
- SH grid moved from v47 lump 18 to BRX1 EXT_LUMP_LIGHTGRID_SH
- Removed v47 version logic
- MSVC fix: noexcept move ctor for entity_t (std::list move not noexcept)
This commit is contained in:
Sergei Shubin 2026-04-16 15:18:44 +08:00
parent 301959d0ae
commit aae67c1fb5
4 changed files with 182 additions and 107 deletions

View file

@ -61,16 +61,8 @@
#define LUMP_LIGHTMAPS 14
#define LUMP_LIGHTGRID 15
#define LUMP_VISIBILITY 16
#define LUMP_ADVERTISEMENTS 17
#define LUMP_LIGHTGRID_SH 18
#define HEADER_LUMPS_V46 17 /* standard Q3: lumps 0-16, header = 144 bytes */
#define HEADER_LUMPS_V47 19 /* extended: lumps 0-18, header = 160 bytes */
#define HEADER_LUMPS_MAX 19 /* max for internal use */
#define IBSP_VERSION_V46 46
#define IBSP_VERSION_V47 47
/* types */
struct ibspHeader_v46_t
@ -80,20 +72,8 @@ struct ibspHeader_v46_t
bspLump_t lumps[ HEADER_LUMPS_V46 ];
};
struct ibspHeader_v47_t
{
char ident[ 4 ];
int version;
bspLump_t lumps[ HEADER_LUMPS_V47 ];
};
/* internal header -- always uses max lump count for convenience */
struct ibspHeader_t
{
char ident[ 4 ];
int version;
bspLump_t lumps[ HEADER_LUMPS_MAX ];
};
/* internal header -- uses v46 lump count */
using ibspHeader_t = ibspHeader_v46_t;
@ -238,30 +218,21 @@ void LoadIBSPFile( const char *filename ){
/* load the file */
MemBuffer file = LoadFile( filename );
/* read ident and version to determine header size */
/* read ident and version */
const byte *data = (const byte *)file.data();
const size_t fileSize = file.size();
const int version = LittleLong( *(const int *)( data + 4 ) );
/* make sure it matches the format we're trying to load */
if ( !force && memcmp( data, g_game->bspIdent, 4 ) ) {
Error( "%s is not a %s file", filename, g_game->bspIdent );
}
if ( !force && version != IBSP_VERSION_V46 && version != IBSP_VERSION_V47 ) {
Error( "%s is version %d, expected %d or %d", filename, version, IBSP_VERSION_V46, IBSP_VERSION_V47 );
if ( !force && version != IBSP_VERSION_V46 ) {
Error( "%s is version %d, expected %d", filename, version, IBSP_VERSION_V46 );
}
/* determine lump count from version */
const int numLumps = ( version == IBSP_VERSION_V47 ) ? HEADER_LUMPS_V47 : HEADER_LUMPS_V46;
/* CopyLump uses (byte*)header + offset to access file data,
so we overlay a bspHeader_t at the file buffer start and
copy/swap the lump directory into it. bspHeader_t has lumps[100]
so it can hold all our lumps. */
bspHeader_t *header = (bspHeader_t *)file.data();
/* swap just the lump directory entries we have */
SwapBlock( (int *)( (byte *)header + 8 ), numLumps * sizeof( bspLump_t ) );
/* note: lumps beyond numLumps are not zeroed -- they overlap file data
in the buffer. We only access extended lumps under version checks. */
SwapBlock( (int *)( (byte *)header + 8 ), HEADER_LUMPS_V46 * sizeof( bspLump_t ) );
/* load/convert standard lumps (0-16) */
CopyLump( header, LUMP_SHADERS, bspShaders );
@ -282,33 +253,81 @@ void LoadIBSPFile( const char *filename ){
CopyLump( header, LUMP_ENTITIES, bspEntData );
CopyLump<bspGridPoint_t, ibspGridPoint_t>( header, LUMP_LIGHTGRID, bspGridPoints );
/* v47 extended lumps (17-18) */
if ( version == IBSP_VERSION_V47 ) {
/* advertisements */
CopyLump( header, LUMP_ADVERTISEMENTS, bspAds );
/* BRX1 extension detection -- two-pass minimum probe */
bspGridPointsSH.clear();
{
const int extStart = BSP_V46_HEADER_SIZE;
/* SH light grid */
const int length = header->lumps[LUMP_LIGHTGRID_SH].length;
const int offset = header->lumps[LUMP_LIGHTGRID_SH].offset;
if ( length > (int)sizeof( bspGridSHHeader_t ) ) {
const bspGridSHHeader_t *shHeader = (const bspGridSHHeader_t *)( data + offset );
gridMinsSH = shHeader->gridMins;
gridSizeSH = shHeader->gridSize;
gridBoundsSH[0] = shHeader->gridBounds[0];
gridBoundsSH[1] = shHeader->gridBounds[1];
gridBoundsSH[2] = shHeader->gridBounds[2];
const int numPoints = shHeader->numPoints;
const bspGridPointSH_t *points = (const bspGridPointSH_t *)( data + offset + sizeof( bspGridSHHeader_t ) );
bspGridPointsSH.assign( points, points + numPoints );
/* pass 1: check no standard lump overlaps [extStart, extStart+8) */
bool canProbe = ( fileSize >= (size_t)extStart + 8 );
if ( canProbe ) {
for ( int i = 0; i < HEADER_LUMPS_V46; ++i ) {
const int lo = header->lumps[i].offset;
const int llen = header->lumps[i].length;
if ( llen > 0 && lo < extStart + 8 && lo + llen > extStart ) {
canProbe = false;
break;
}
}
}
else {
bspGridPointsSH.clear();
if ( canProbe ) {
const int magic = LittleLong( *(const int *)( data + extStart ) );
const int extCount = LittleLong( *(const int *)( data + extStart + 4 ) );
if ( magic == BSP_EXT_MAGIC_INT && extCount > 0 && extCount <= BSP_EXT_MAX_LUMPS ) {
const int extEnd = extStart + 8 + extCount * (int)sizeof( bspLump_t );
/* pass 2: verify full extension block range */
bool extValid = ( fileSize >= (size_t)extEnd );
if ( extValid ) {
for ( int i = 0; i < HEADER_LUMPS_V46; ++i ) {
const int lo = header->lumps[i].offset;
const int llen = header->lumps[i].length;
if ( llen > 0 && lo < extEnd && lo + llen > extStart ) {
extValid = false;
break;
}
}
}
if ( extValid ) {
/* read extension lump directory */
const bspLump_t *extLumps = (const bspLump_t *)( data + extStart + 8 );
/* EXT_LUMP_LIGHTGRID_SH */
if ( extCount > EXT_LUMP_LIGHTGRID_SH ) {
const int off = LittleLong( extLumps[EXT_LUMP_LIGHTGRID_SH].offset );
const int len = LittleLong( extLumps[EXT_LUMP_LIGHTGRID_SH].length );
if ( len > (int)sizeof( bspGridSHHeader_t ) && off + len <= (int)fileSize ) {
const bspGridSHHeader_t *shHeader = (const bspGridSHHeader_t *)( data + off );
gridMinsSH = shHeader->gridMins;
gridSizeSH = shHeader->gridSize;
gridBoundsSH[0] = shHeader->gridBounds[0];
gridBoundsSH[1] = shHeader->gridBounds[1];
gridBoundsSH[2] = shHeader->gridBounds[2];
const int numPoints = shHeader->numPoints;
const bspGridPointSH_t *points = (const bspGridPointSH_t *)( data + off + sizeof( bspGridSHHeader_t ) );
bspGridPointsSH.assign( points, points + numPoints );
}
}
/* EXT_LUMP_LIGHTMAP_INFO */
if ( extCount > EXT_LUMP_LIGHTMAP_INFO ) {
const int off = LittleLong( extLumps[EXT_LUMP_LIGHTMAP_INFO].offset );
const int len = LittleLong( extLumps[EXT_LUMP_LIGHTMAP_INFO].length );
if ( len >= (int)sizeof( bspLightmapInfo_t ) && off + len <= (int)fileSize ) {
const bspLightmapInfo_t *lmInfo = (const bspLightmapInfo_t *)( data + off );
bspLightmapPageW = LittleLong( lmInfo->pageWidth );
bspLightmapPageH = LittleLong( lmInfo->pageHeight );
}
}
Sys_FPrintf( SYS_VRB, "BRX1 extension: %d lumps\n", extCount );
}
}
}
}
else {
bspAds.clear();
bspGridPointsSH.clear();
}
}
/*
@ -327,16 +346,12 @@ void LoadIBSPorRBSPFilePartially( const char *filename ){
if ( !force && memcmp( data, g_game->bspIdent, 4 ) ) {
Error( "%s is not a %s file", filename, g_game->bspIdent );
}
if ( !force && version != IBSP_VERSION_V46 && version != IBSP_VERSION_V47 ) {
Error( "%s is version %d, expected %d or %d", filename, version, IBSP_VERSION_V46, IBSP_VERSION_V47 );
if ( !force && version != IBSP_VERSION_V46 ) {
Error( "%s is version %d, expected %d", filename, version, IBSP_VERSION_V46 );
}
const int numLumps = ( version == IBSP_VERSION_V47 ) ? HEADER_LUMPS_V47 : HEADER_LUMPS_V46;
bspHeader_t *header = (bspHeader_t *)file.data();
SwapBlock( (int *)( (byte *)header + 8 ), numLumps * sizeof( bspLump_t ) );
for ( int i = numLumps; i < 100; ++i )
header->lumps[i] = {};
SwapBlock( (int *)( (byte *)header + 8 ), HEADER_LUMPS_V46 * sizeof( bspLump_t ) );
/* load/convert lumps */
CopyLump( header, LUMP_SHADERS, bspShaders );
@ -355,23 +370,33 @@ void LoadIBSPorRBSPFilePartially( const char *filename ){
*/
void WriteIBSPFile( const char *filename ){
/* determine version: v47 if SH data exists, v46 otherwise */
const bool extendedBSP = !bspGridPointsSH.empty();
const int bspVersion = extendedBSP ? IBSP_VERSION_V47 : IBSP_VERSION_V46;
const int numLumps = extendedBSP ? HEADER_LUMPS_V47 : HEADER_LUMPS_V46;
const int headerSize = 8 + numLumps * (int)sizeof( bspLump_t );
/* always write v46 -- extensions go in BRX1 block after the header */
const bool hasSHGrid = !bspGridPointsSH.empty();
const bool hasLmInfo = ( bspLightmapPageW != LIGHTMAP_WIDTH || bspLightmapPageH != LIGHTMAP_HEIGHT );
const bool hasExtension = hasSHGrid || hasLmInfo;
ibspHeader_t header{};
//% Swapfile();
/* set up header */
/* set up header -- always v46 */
memcpy( header.ident, g_game->bspIdent, 4 );
header.version = LittleLong( bspVersion );
header.version = LittleLong( IBSP_VERSION_V46 );
/* write initial header (placeholder, overwritten at the end) */
const int v46HeaderSize = (int)sizeof( ibspHeader_v46_t );
/* compute extension block size */
int extBlockSize = 0;
if ( hasExtension ) {
extBlockSize = 8 + EXT_LUMP_COUNT * (int)sizeof( bspLump_t ); /* magic + count + directory */
}
/* write initial header + extension placeholder (overwritten at the end) */
FILE *file = SafeOpenWrite( filename );
SafeWrite( file, &header, headerSize );
SafeWrite( file, &header, v46HeaderSize );
if ( hasExtension ) {
/* placeholder for extension block -- filled in at the end */
std::vector<byte> extPlaceholder( extBlockSize, 0 );
SafeWrite( file, extPlaceholder.data(), extBlockSize );
}
{ /* add marker lump */
time_t t;
@ -381,7 +406,7 @@ void WriteIBSPFile( const char *filename ){
AddLump( file, header.lumps[0], std::vector<char>( marker.cbegin(), marker.cend() + 1 ) );
}
/* add standard lumps (0-16) */
/* add standard lumps (0-16) -- data starts after header + extension block */
AddLump( file, header.lumps[LUMP_SHADERS], bspShaders );
AddLump( file, header.lumps[LUMP_PLANES], bspPlanes );
AddLump( file, header.lumps[LUMP_LEAFS], bspLeafs );
@ -400,34 +425,52 @@ void WriteIBSPFile( const char *filename ){
AddLump( file, header.lumps[LUMP_FOGS], bspFogs );
AddLump( file, header.lumps[LUMP_DRAWINDEXES], bspDrawIndexes );
/* v47 extended lumps (17-18) */
if ( extendedBSP ) {
/* advertisements */
AddLump( file, header.lumps[LUMP_ADVERTISEMENTS], bspAds );
/* BRX1 extension lumps -- data appended after standard lumps */
bspLump_t extLumps[EXT_LUMP_COUNT] = {};
if ( hasExtension ) {
/* SH light grid */
if ( hasSHGrid ) {
const size_t shHeaderSz = sizeof( bspGridSHHeader_t );
const size_t dataSz = bspGridPointsSH.size() * sizeof( bspGridPointSH_t );
std::vector<byte> shLump( shHeaderSz + dataSz );
bspGridSHHeader_t shHeader;
shHeader.gridMins = gridMinsSH;
shHeader.gridSize = gridSizeSH;
shHeader.gridBounds[0] = gridBoundsSH[0];
shHeader.gridBounds[1] = gridBoundsSH[1];
shHeader.gridBounds[2] = gridBoundsSH[2];
shHeader.numPoints = (int)bspGridPointsSH.size();
memcpy( shLump.data(), &shHeader, shHeaderSz );
memcpy( shLump.data() + shHeaderSz, bspGridPointsSH.data(), dataSz );
AddLump( file, extLumps[EXT_LUMP_LIGHTGRID_SH], shLump );
}
/* SH light grid -- header + points packed into a byte lump */
const size_t shHeaderSz = sizeof( bspGridSHHeader_t );
const size_t dataSz = bspGridPointsSH.size() * sizeof( bspGridPointSH_t );
std::vector<byte> shLump( shHeaderSz + dataSz );
bspGridSHHeader_t shHeader;
shHeader.gridMins = gridMinsSH;
shHeader.gridSize = gridSizeSH;
shHeader.gridBounds[0] = gridBoundsSH[0];
shHeader.gridBounds[1] = gridBoundsSH[1];
shHeader.gridBounds[2] = gridBoundsSH[2];
shHeader.numPoints = (int)bspGridPointsSH.size();
memcpy( shLump.data(), &shHeader, shHeaderSz );
memcpy( shLump.data() + shHeaderSz, bspGridPointsSH.data(), dataSz );
AddLump( file, header.lumps[LUMP_LIGHTGRID_SH], shLump );
/* lightmap info */
if ( hasLmInfo ) {
bspLightmapInfo_t lmInfo;
lmInfo.pageWidth = bspLightmapPageW;
lmInfo.pageHeight = bspLightmapPageH;
AddLump( file, extLumps[EXT_LUMP_LIGHTMAP_INFO], std::vector<bspLightmapInfo_t>( 1, lmInfo ) );
}
}
/* emit bsp size */
const int size = ftell( file );
Sys_Printf( "Wrote %.1f MB (%d bytes)\n", (float) size / ( 1024 * 1024 ), size );
/* write the completed header */
/* write the completed v46 header */
fseek( file, 0, SEEK_SET );
SafeWrite( file, &header, headerSize );
SafeWrite( file, &header, v46HeaderSize );
/* write the completed BRX1 extension block */
if ( hasExtension ) {
const int extMagic = LittleLong( BSP_EXT_MAGIC_INT );
const int extCount = LittleLong( EXT_LUMP_COUNT );
SafeWrite( file, &extMagic, 4 );
SafeWrite( file, &extCount, 4 );
SafeWrite( file, extLumps, EXT_LUMP_COUNT * sizeof( bspLump_t ) );
Sys_FPrintf( SYS_VRB, "BRX1 extension: %d lumps written\n", EXT_LUMP_COUNT );
}
/* close the file */
fclose( file );

View file

@ -2722,12 +2722,18 @@ int LightMain( Args& args ){
}
Sys_Printf( "Default lightmap size set to %d x %d pixels\n", lmCustomSizeW, lmCustomSizeH );
/* enable external lightmaps */
/* handle non-default lightmap size */
if ( lmCustomSizeW != g_game->lightmapSize || lmCustomSizeH != g_game->lightmapSize ) {
/* -lightmapsize might just require -external for native external lms, but it has already been used in existing batches alone,
so brand new switch here for external lms, referenced by shaders hack/behavior */
externalLightmaps = !extlmhack;
Sys_Printf( "Storing all lightmaps externally\n" );
if ( extlmhack ) {
/* -extlmhacksize: external TGA + custom shader hack (legacy, for vanilla Q3 engines) */
Sys_Printf( "External lightmap hack enabled\n" );
}
else {
/* -lightmapsize: store natively in BSP via BRX1 extension */
bspLightmapPageW = lmCustomSizeW;
bspLightmapPageH = lmCustomSizeH;
Sys_Printf( "Lightmap pages stored in BSP at %d x %d (BRX1 extension)\n", bspLightmapPageW, bspLightmapPageH );
}
}
}

View file

@ -1741,7 +1741,9 @@ static void SetupOutLightmap( rawLightmap_t *lm, outLightmap_t *olm ){
}
/* is this a "normal" bsp-stored lightmap? */
if ( ( lm->customWidth == g_game->lightmapSize && lm->customHeight == g_game->lightmapSize ) || externalLightmaps ) {
if ( ( lm->customWidth == g_game->lightmapSize && lm->customHeight == g_game->lightmapSize )
|| ( lm->customWidth == bspLightmapPageW && lm->customHeight == bspLightmapPageH )
|| externalLightmaps ) {
olm->lightmapNum = numBSPLightmaps;
numBSPLightmaps++;
@ -2887,7 +2889,7 @@ void StoreSurfaceLightmaps( bool fastAllocate, bool storeForReal ){
timer.start();
/* count the bsp lightmaps and allocate space */
const size_t gameLmSize = g_game->lightmapSize * g_game->lightmapSize * sizeof( Vector3b );
const size_t gameLmSize = (size_t)lmCustomSizeW * lmCustomSizeH * sizeof( Vector3b );
if ( numBSPLightmaps == 0 || externalLightmaps ) {
bspLightBytes.clear();
}
@ -3221,8 +3223,10 @@ void StoreSurfaceLightmaps( bool fastAllocate, bool storeForReal ){
}
/* devise a custom shader for this surface (fixme: make this work with light styles) */
/* skip this hack when using BRX1 native lightmap pages */
else if ( olm != nullptr && lm != nullptr && !externalLightmaps &&
( olm->customWidth != g_game->lightmapSize || olm->customHeight != g_game->lightmapSize ) ) {
( olm->customWidth != g_game->lightmapSize || olm->customHeight != g_game->lightmapSize ) &&
( olm->customWidth != bspLightmapPageW || olm->customHeight != bspLightmapPageH ) ) {
/* get output lightmap */
olm = &outLightmaps[ lm->outLightmapNums[ 0 ] ];

View file

@ -364,7 +364,27 @@ struct bspGridPoint_t
byte latLong[ 2 ];
};
/* SH light grid header -- stored at the start of LUMP_LIGHTGRID_SH */
/* BRX1 backward-compatible BSP extension format.
Extension block sits at offset 144 (right after v46 header).
Layout: magic(4) + count(4) + lump_dir(count*8) */
#define BSP_EXT_MAGIC "BRX1"
#define BSP_EXT_MAGIC_INT (('1'<<24)+('X'<<16)+('R'<<8)+'B') /* little-endian "BRX1" */
#define BSP_V46_HEADER_SIZE 144
#define BSP_EXT_MAX_LUMPS 32
/* Extension lump indices */
#define EXT_LUMP_LIGHTGRID_SH 0
#define EXT_LUMP_LIGHTMAP_INFO 1
#define EXT_LUMP_COUNT 2 /* total extension lumps we use */
/* Lightmap page size metadata -- stored in EXT_LUMP_LIGHTMAP_INFO */
struct bspLightmapInfo_t
{
int pageWidth;
int pageHeight;
};
/* SH light grid header -- stored at the start of EXT_LUMP_LIGHTGRID_SH */
struct bspGridSHHeader_t
{
Vector3 gridMins; /* world-space origin of the SH grid */
@ -2166,6 +2186,8 @@ inline bool exportLightmaps;
inline bool externalLightmaps;
inline int lmCustomSizeW = LIGHTMAP_WIDTH;
inline int lmCustomSizeH = LIGHTMAP_WIDTH;
inline int bspLightmapPageW = LIGHTMAP_WIDTH; /* actual page size written to BSP (for BRX1 extension) */
inline int bspLightmapPageH = LIGHTMAP_HEIGHT;
inline const char * lmCustomDir;
inline int lmLimitSize;