public: move atlas packing code from renderer, add tests

This commit is contained in:
Alibek Omarov
2026-04-20 01:26:28 +05:00
parent 491ba9bc7d
commit a3dba64513
5 changed files with 241 additions and 49 deletions

62
public/atlas.c Normal file
View File

@@ -0,0 +1,62 @@
/*
atlas.c - generic 2D atlas packer (strip-based)
Copyright (C) 1997 id Software
Copyright (C) Xash3D and Xash3D FWGS developers
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
*/
#include "atlas.h"
qboolean Atlas_AllocBlock( atlas_t *atlas, int w, int h, int *x, int *y )
{
int i, j;
int best, best2;
int size = atlas->size;
best = size;
for( i = 0; i <= size - w; )
{
best2 = 0;
for( j = 0; j < w; j++ )
{
if( atlas->allocated[i + j] >= best )
break;
if( atlas->allocated[i + j] > best2 )
best2 = atlas->allocated[i + j];
}
if( j == w )
{
*x = i;
*y = best = best2;
if( best == 0 )
break;
i++;
}
else
{
i += j + 1;
}
}
if( best + h > size )
return false;
for( i = 0; i < w; i++ )
atlas->allocated[*x + i] = best + h;
if( best + h > atlas->max_height )
atlas->max_height = best + h;
return true;
}

41
public/atlas.h Normal file
View File

@@ -0,0 +1,41 @@
/*
atlas.h - generic 2D atlas packer (strip-based)
Copyright (C) 1997 id Software
Copyright (C) Xash3D and Xash3D FWGS developers
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
*/
#ifndef ATLAS_H
#define ATLAS_H
#include <string.h>
#include "xash3d_types.h"
// maximum atlas dimension
#define ATLAS_MAX_SIZE 1024
typedef struct atlas_s
{
int allocated[ATLAS_MAX_SIZE];
int size;
int max_height;
} atlas_t;
static inline void Atlas_Init( atlas_t *atlas, int size )
{
memset( atlas->allocated, 0, sizeof( atlas->allocated ));
atlas->size = size;
atlas->max_height = 0;
}
qboolean Atlas_AllocBlock( atlas_t *atlas, int w, int h, int *x, int *y );
#endif // ATLAS_H

131
public/tests/test_atlas.c Normal file
View File

@@ -0,0 +1,131 @@
#include "atlas.h"
static int Test_BasicAlloc( void )
{
atlas_t atlas;
int x, y;
Atlas_Init( &atlas, 128 );
if( !Atlas_AllocBlock( &atlas, 16, 16, &x, &y ))
return 1;
if( x != 0 || y != 0 )
return 2;
if( atlas.max_height != 16 )
return 3;
if( !Atlas_AllocBlock( &atlas, 16, 16, &x, &y ))
return 4;
if( x != 16 || y != 0 )
return 5;
return 0;
}
static int Test_Full( void )
{
atlas_t atlas;
int x, y;
Atlas_Init( &atlas, 64 );
if( !Atlas_AllocBlock( &atlas, 64, 64, &x, &y ))
return 1;
if( Atlas_AllocBlock( &atlas, 1, 1, &x, &y ))
return 2;
return 0;
}
static int Test_Packing( void )
{
atlas_t atlas;
int x, y;
Atlas_Init( &atlas, 64 );
if( !Atlas_AllocBlock( &atlas, 32, 16, &x, &y ))
return 1;
if( !Atlas_AllocBlock( &atlas, 32, 16, &x, &y ))
return 2;
if( x != 32 || y != 0 )
return 3;
if( !Atlas_AllocBlock( &atlas, 64, 16, &x, &y ))
return 4;
if( y != 16 )
return 5;
return 0;
}
static int Test_Reset( void )
{
atlas_t atlas;
int x, y;
Atlas_Init( &atlas, 64 );
if( !Atlas_AllocBlock( &atlas, 64, 64, &x, &y ))
return 1;
Atlas_Init( &atlas, 64 );
if( !Atlas_AllocBlock( &atlas, 64, 64, &x, &y ))
return 2;
if( x != 0 || y != 0 )
return 3;
return 0;
}
static int Test_TooLarge( void )
{
atlas_t atlas;
int x, y;
Atlas_Init( &atlas, 64 );
if( Atlas_AllocBlock( &atlas, 65, 1, &x, &y ))
return 1;
if( Atlas_AllocBlock( &atlas, 1, 65, &x, &y ))
return 2;
return 0;
}
int main( void )
{
int ret;
ret = Test_BasicAlloc();
if( ret > 0 )
return ret;
ret = Test_Full();
if( ret > 0 )
return ret + 16;
ret = Test_Packing();
if( ret > 0 )
return ret + 32;
ret = Test_Reset();
if( ret > 0 )
return ret + 48;
ret = Test_TooLarge();
if( ret > 0 )
return ret + 64;
return 0;
}

View File

@@ -157,6 +157,7 @@ def build(bld):
'efp': 'tests/test_efp.c',
'atoi': 'tests/test_atoi.c',
'parsefile': 'tests/test_parsefile.c',
'atlas': 'tests/test_atlas.c',
}
for i in tests:

View File

@@ -16,6 +16,7 @@ GNU General Public License for more details.
#include "gl_local.h"
#include "xash3d_mathlib.h"
#include "mod_local.h"
#include "atlas.h"
#define TURBSCALE ( 256.0f / ( M_PI2 ))
@@ -27,8 +28,7 @@ static const float r_turbsin[] =
typedef struct
{
int allocated[BLOCK_SIZE_MAX];
int max_height; // maximum height currently in use in the block
atlas_t atlas;
int current_lightmap_texture;
msurface_t *dynamic_surfaces;
msurface_t *lightmap_surfaces[MAX_LIGHTMAPS];
@@ -646,60 +646,17 @@ static void R_AddDynamicLights( const msurface_t *surf )
*/
static void LM_InitBlock( void )
{
memset( gl_lms.allocated, 0, sizeof( gl_lms.allocated ));
gl_lms.max_height = 0;
Atlas_Init( &gl_lms.atlas, BLOCK_SIZE );
}
static int LM_AllocBlock( int w, int h, int *x, int *y )
static qboolean LM_AllocBlock( int w, int h, int *x, int *y )
{
int i, j;
int best, best2;
best = BLOCK_SIZE;
for( i = 0; i < BLOCK_SIZE - w; )
{
best2 = 0;
for( j = 0; j < w; j++ )
{
if( gl_lms.allocated[i+j] >= best )
break;
if( gl_lms.allocated[i+j] > best2 )
best2 = gl_lms.allocated[i+j];
}
if( j == w )
{
// this is a valid spot
*x = i;
*y = best = best2;
if( best == 0 )
break; // height 0 is optimal, can't do better
i++;
}
else
{
// allocated[i+j] was too tall — no position in [i, i+j] can work
i += j + 1;
}
}
if( best + h > BLOCK_SIZE )
return false;
for( i = 0; i < w; i++ )
gl_lms.allocated[*x + i] = best + h;
if( best + h > gl_lms.max_height )
gl_lms.max_height = best + h;
return true;
return Atlas_AllocBlock( &gl_lms.atlas, w, h, x, y );
}
static void LM_UploadDynamicBlock( void )
{
pglTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, BLOCK_SIZE, gl_lms.max_height, GL_RGBA, GL_UNSIGNED_BYTE, gl_lms.lightmap_buffer );
pglTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, BLOCK_SIZE, gl_lms.atlas.max_height, GL_RGBA, GL_UNSIGNED_BYTE, gl_lms.lightmap_buffer );
}
static void LM_UploadBlock( qboolean dynamic )