rt: refactor loading surface lights, fix #417

Do not entangle brush model loading with loading surface lights.
Do a separate pass over brush model surfaces for the sole purpose of
finding light sources. Enables consistent live-reloading light data
after patching entities/surface/rad files.
This commit is contained in:
Ivan Avdeev 2023-03-02 22:32:08 -08:00
parent 58c9a9920e
commit a284567002
9 changed files with 210 additions and 169 deletions

View File

@ -459,8 +459,6 @@ typedef struct {
int num_surfaces, num_vertices, num_indices;
int max_texture_id;
int water_surfaces;
//int sky_surfaces;
int emissive_surfaces;
} model_sizes_t;
static model_sizes_t computeSizes( const model_t *mod ) {
@ -482,39 +480,11 @@ static model_sizes_t computeSizes( const model_t *mod ) {
sizes.num_indices += 3 * (surf->numedges - 1);
if (tex_id > sizes.max_texture_id)
sizes.max_texture_id = tex_id;
{
const xvk_patch_surface_t *const psurf = R_VkPatchGetSurface(i);
vec3_t emissive;
if ((psurf && (psurf->flags & Patch_Surface_Emissive)) || (RT_GetEmissiveForTexture(emissive, tex_id)))
++sizes.emissive_surfaces;
}
}
return sizes;
}
static rt_light_add_polygon_t loadPolyLight(const model_t *mod, const int surface_index, const msurface_t *surf, const vec3_t emissive) {
rt_light_add_polygon_t lpoly = {0};
lpoly.num_vertices = Q_min(7, surf->numedges);
// TODO split, don't clip
if (surf->numedges > 7)
gEngine.Con_Printf(S_WARN "emissive surface %d has %d vertices; clipping to 7\n", surface_index, surf->numedges);
VectorCopy(emissive, lpoly.emissive);
for (int i = 0; i < lpoly.num_vertices; ++i) {
const int iedge = mod->surfedges[surf->firstedge + i];
const medge_t *edge = mod->edges + (iedge >= 0 ? iedge : -iedge);
const mvertex_t *vertex = mod->vertexes + (iedge >= 0 ? edge->v[0] : edge->v[1]);
VectorCopy(vertex->position, lpoly.vertices[i]);
}
lpoly.surface = surf;
return lpoly;
}
static qboolean loadBrushSurfaces( model_sizes_t sizes, const model_t *mod ) {
vk_brush_model_t *bmodel = mod->cache.data;
uint32_t vertex_offset = 0;
@ -556,30 +526,6 @@ static qboolean loadBrushSurfaces( model_sizes_t sizes, const model_t *mod ) {
if (t != tex_id)
continue;
// FIXME move this to rt_light_bsp and static loading
{
qboolean is_emissive = false;
vec3_t emissive = {0};
rt_light_add_polygon_t polylight;
if (psurf && (psurf->flags & Patch_Surface_Emissive)) {
is_emissive = true;
VectorCopy(psurf->emissive, emissive);
} else if (RT_GetEmissiveForTexture(emissive, tex_id)) {
is_emissive = true;
}
if (is_emissive) {
if (bmodel->render_model.polylights) {
ASSERT(bmodel->render_model.polylights_count < sizes.emissive_surfaces);
bmodel->render_model.polylights[bmodel->render_model.polylights_count++] = loadPolyLight(mod, surface_index, surf, emissive);
} else {
polylight = loadPolyLight(mod, surface_index, surf, emissive);
RT_LightAddPolygon(&polylight);
}
}
}
++num_geometries;
//gEngine.Con_Reportf( "surface %d: numverts=%d numedges=%d\n", i, surf->polys ? surf->polys->numverts : -1, surf->numedges );
@ -680,10 +626,8 @@ static qboolean loadBrushSurfaces( model_sizes_t sizes, const model_t *mod ) {
R_GeometryBufferUnlock( &buffer );
if (bmodel->render_model.polylights) {
gEngine.Con_Reportf("Dynamic polylights %d %d \n", sizes.emissive_surfaces, bmodel->render_model.polylights_count);
ASSERT(sizes.emissive_surfaces == bmodel->render_model.polylights_count);
}
bmodel->render_model.dynamic_polylights = NULL;
bmodel->render_model.dynamic_polylights_count = 0;
ASSERT(sizes.num_surfaces == num_geometries);
bmodel->render_model.num_geometries = num_geometries;
@ -691,10 +635,8 @@ static qboolean loadBrushSurfaces( model_sizes_t sizes, const model_t *mod ) {
return true;
}
qboolean VK_BrushModelLoad( model_t *mod, qboolean map )
{
if (mod->cache.data)
{
qboolean VK_BrushModelLoad( model_t *mod ) {
if (mod->cache.data) {
gEngine.Con_Reportf( S_WARN "Model %s was already loaded\n", mod->name );
return true;
}
@ -711,7 +653,6 @@ qboolean VK_BrushModelLoad( model_t *mod, qboolean map )
mod->cache.data = bmodel;
Q_strncpy(bmodel->render_model.debug_name, mod->name, sizeof(bmodel->render_model.debug_name));
bmodel->render_model.render_type = kVkRenderTypeSolid;
bmodel->render_model.static_map = map;
bmodel->num_water_surfaces = sizes.water_surfaces;
Vector4Set(bmodel->render_model.color, 1, 1, 1, 1);
@ -719,9 +660,6 @@ qboolean VK_BrushModelLoad( model_t *mod, qboolean map )
if (sizes.num_surfaces != 0) {
bmodel->render_model.geometries = (vk_render_geometry_t*)((char*)(bmodel + 1));
if (!map && sizes.emissive_surfaces)
bmodel->render_model.polylights = Mem_Malloc(vk_core.pool, sizeof(bmodel->render_model.polylights[0]) * sizes.emissive_surfaces);
if (!loadBrushSurfaces(sizes, mod) || !VK_RenderModelInit(&bmodel->render_model)) {
gEngine.Con_Printf(S_ERROR "Could not load model %s\n", mod->name);
Mem_Free(bmodel);
@ -745,8 +683,6 @@ void VK_BrushModelDestroy( model_t *mod ) {
return;
VK_RenderModelDestroy(&bmodel->render_model);
if (bmodel->render_model.polylights)
Mem_Free(bmodel->render_model.polylights);
Mem_Free(bmodel);
mod->cache.data = NULL;
}
@ -757,3 +693,83 @@ void VK_BrushStatsClear( void )
g_brush.stat.num_vertices = 0;
g_brush.stat.num_indices = 0;
}
static rt_light_add_polygon_t loadPolyLight(const model_t *mod, const int surface_index, const msurface_t *surf, const vec3_t emissive) {
rt_light_add_polygon_t lpoly = {0};
lpoly.num_vertices = Q_min(7, surf->numedges);
// TODO split, don't clip
if (surf->numedges > 7)
gEngine.Con_Printf(S_WARN "emissive surface %d has %d vertices; clipping to 7\n", surface_index, surf->numedges);
VectorCopy(emissive, lpoly.emissive);
for (int i = 0; i < lpoly.num_vertices; ++i) {
const int iedge = mod->surfedges[surf->firstedge + i];
const medge_t *edge = mod->edges + (iedge >= 0 ? iedge : -iedge);
const mvertex_t *vertex = mod->vertexes + (iedge >= 0 ? edge->v[0] : edge->v[1]);
VectorCopy(vertex->position, lpoly.vertices[i]);
}
lpoly.surface = surf;
return lpoly;
}
void R_VkBrushModelCollectEmissiveSurfaces( const struct model_s *mod, qboolean is_worldmodel ) {
typedef struct {
int model_surface_index;
int surface_index;
const msurface_t *surf;
vec3_t emissive;
} emissive_surface_t;
emissive_surface_t emissive_surfaces[MAX_SURFACE_LIGHTS];
int emissive_surfaces_count = 0;
for( int i = 0; i < mod->nummodelsurfaces; ++i) {
const int surface_index = mod->firstmodelsurface + i;
const msurface_t *surf = mod->surfaces + surface_index;
if (!renderableSurface(surf, surface_index))
continue;
const int tex_id = surf->texinfo->texture->gl_texturenum; // TODO animation?
const xvk_patch_surface_t *const psurf = R_VkPatchGetSurface(surface_index);
vec3_t emissive;
if ((psurf && (psurf->flags & Patch_Surface_Emissive)) || (RT_GetEmissiveForTexture(emissive, tex_id))) {
if (emissive_surfaces_count == MAX_SURFACE_LIGHTS) {
gEngine.Con_Printf(S_ERROR "Too many emissive surfaces for model %s: max=%d\n", mod->name, MAX_SURFACE_LIGHTS);
break;
}
emissive_surface_t* const surface = &emissive_surfaces[emissive_surfaces_count++];
surface->model_surface_index = i;
surface->surface_index = surface_index;
surface->surf = surf;
VectorCopy(emissive, surface->emissive);
}
}
vk_brush_model_t *const bmodel = mod->cache.data;
ASSERT(bmodel);
if (!is_worldmodel) {
if (bmodel->render_model.dynamic_polylights)
Mem_Free(bmodel->render_model.dynamic_polylights);
bmodel->render_model.dynamic_polylights_count = emissive_surfaces_count;
bmodel->render_model.dynamic_polylights = Mem_Malloc(vk_core.pool, sizeof(bmodel->render_model.dynamic_polylights[0]) * emissive_surfaces_count);
}
for (int i = 0; i < emissive_surfaces_count; ++i) {
const emissive_surface_t* const s = emissive_surfaces + i;
const rt_light_add_polygon_t polylight = loadPolyLight(mod, s->surface_index, s->surf, s->emissive);
if (!is_worldmodel) {
bmodel->render_model.dynamic_polylights[i] = polylight;
} else {
RT_LightAddPolygon(&polylight);
}
}
gEngine.Con_Reportf("Loaded %d polylights for %smodel %s\n", emissive_surfaces_count, is_worldmodel ? "world" : "movable ", mod->name);
}

View File

@ -11,10 +11,12 @@ struct cl_entity_s;
qboolean VK_BrushInit( void );
void VK_BrushShutdown( void );
qboolean VK_BrushModelLoad(struct model_s *mod, qboolean map);
qboolean VK_BrushModelLoad(struct model_s *mod);
void VK_BrushModelDestroy(struct model_s *mod);
void VK_BrushModelDraw( const cl_entity_t *ent, int render_mode, float blend, const matrix4x4 model );
void VK_BrushStatsClear( void );
const texture_t *R_TextureAnimation( const cl_entity_t *ent, const msurface_t *s, const struct texture_s *base_override );
void R_VkBrushModelCollectEmissiveSurfaces( const struct model_s *mod, qboolean is_worldmodel );

View File

@ -467,7 +467,7 @@ static void prepareSurfacesLeafVisibilityCache( const struct model_s *map ) {
g_lights_bsp.surfaces[i].potentially_visible_leafs = NULL;
}
void RT_LightsNewMapBegin( const struct model_s *map ) {
void RT_LightsNewMap( const struct model_s *map ) {
// 1. Determine map bounding box (and optimal grid size?)
// map->mins, maxs
vec3_t map_size, min_cell, max_cell;
@ -504,48 +504,6 @@ void RT_LightsNewMapBegin( const struct model_s *map ) {
g_lights_.visited_cells = bitArrayCreate(g_lights.map.grid_cells);
prepareSurfacesLeafVisibilityCache( map );
// Load RAD data based on map name
memset(g_lights_.map.emissive_textures, 0, sizeof(g_lights_.map.emissive_textures));
loadRadData( map, "maps/lights.rad" );
{
int name_len = Q_strlen(map->name);
// Strip ".bsp" suffix
if (name_len > 4 && 0 == Q_stricmp(map->name + name_len - 4, ".bsp"))
name_len -= 4;
loadRadData( map, "%.*s.rad", name_len, map->name );
}
// Clear static lights counts
{
g_lights_.num_polygons = g_lights_.num_static.polygons = 0;
g_lights_.num_point_lights = g_lights_.num_static.point_lights = 0;
g_lights_.num_polygon_vertices = g_lights_.num_static.polygon_vertices = 0;
for (int i = 0; i < g_lights.map.grid_cells; ++i) {
vk_lights_cell_t *const cell = g_lights.cells + i;
cell->num_point_lights = cell->num_static.point_lights = 0;
cell->num_polygons = cell->num_static.polygons = 0;
cell->frame_sequence = g_lights_.frame_sequence;
}
}
}
void RT_LightsFrameBegin( void ) {
g_lights_.num_polygons = g_lights_.num_static.polygons;
g_lights_.num_point_lights = g_lights_.num_static.point_lights;
g_lights_.num_polygon_vertices = g_lights_.num_static.polygon_vertices;
g_lights.stats.dirty_cells = 0;
for (int i = 0; i < g_lights.map.grid_cells; ++i) {
vk_lights_cell_t *const cell = g_lights.cells + i;
cell->num_polygons = cell->num_static.polygons;
cell->num_point_lights = cell->num_static.point_lights;
}
}
static qboolean addSurfaceLightToCell( int cell_index, int polygon_light_index ) {
@ -931,10 +889,39 @@ static void processStaticPointLights( void ) {
APROF_SCOPE_END(static_lights);
}
void RT_LightsNewMapEnd( const struct model_s *map ) {
//debug_dump_lights.enabled = true;
void RT_LightsLoadBegin( const struct model_s *map ) {
// Load RAD data based on map name
{
int name_len = Q_strlen(map->name);
// Strip ".bsp" suffix
if (name_len > 4 && 0 == Q_stricmp(map->name + name_len - 4, ".bsp"))
name_len -= 4;
memset(g_lights_.map.emissive_textures, 0, sizeof(g_lights_.map.emissive_textures));
loadRadData( map, "maps/lights.rad" );
loadRadData( map, "%.*s.rad", name_len, map->name );
}
// Clear static lights counts
{
g_lights_.num_polygons = g_lights_.num_static.polygons = 0;
g_lights_.num_point_lights = g_lights_.num_static.point_lights = 0;
g_lights_.num_polygon_vertices = g_lights_.num_static.polygon_vertices = 0;
for (int i = 0; i < g_lights.map.grid_cells; ++i) {
vk_lights_cell_t *const cell = g_lights.cells + i;
cell->num_point_lights = cell->num_static.point_lights = 0;
cell->num_polygons = cell->num_static.polygons = 0;
cell->frame_sequence = g_lights_.frame_sequence;
}
}
processStaticPointLights();
}
void RT_LightsLoadEnd( void ) {
//debug_dump_lights.enabled = true;
// Fix static counts
{
@ -1131,6 +1118,20 @@ int RT_LightAddPolygon(const rt_light_add_polygon_t *addpoly) {
}
}
void RT_LightsFrameBegin( void ) {
g_lights_.num_polygons = g_lights_.num_static.polygons;
g_lights_.num_point_lights = g_lights_.num_static.point_lights;
g_lights_.num_polygon_vertices = g_lights_.num_static.polygon_vertices;
g_lights.stats.dirty_cells = 0;
for (int i = 0; i < g_lights.map.grid_cells; ++i) {
vk_lights_cell_t *const cell = g_lights.cells + i;
cell->num_polygons = cell->num_static.polygons;
cell->num_point_lights = cell->num_static.point_lights;
}
}
static void uploadGridRange( int begin, int end ) {
const int count = end - begin;
ASSERT( count > 0 );

View File

@ -71,9 +71,15 @@ extern vk_lights_t g_lights;
qboolean VK_LightsInit( void );
void VK_LightsShutdown( void );
// Allocate clusters and vis data for the new map
struct model_s;
void RT_LightsNewMapBegin( const struct model_s *map );
void RT_LightsNewMapEnd( const struct model_s *map );
void RT_LightsNewMap( const struct model_s *map );
// Clear light data and prepare for loading
// RT_LightsNewMap should have been already called for current map
void RT_LightsLoadBegin( const struct model_s *map );
// Finalize loading light data, i.e. mark everything loaded so far as static light data
void RT_LightsLoadEnd( void );
void RT_LightsFrameBegin( void );
void RT_LightsFrameEnd( void );

View File

@ -267,6 +267,12 @@ vk_ray_model_t* VK_RayModelCreate( vk_ray_model_init_t args ) {
case kVkRenderType_1_1_R: // blend: scr + dst, depth test
HACK_additive_emissive = true;
break;
case kVkRenderTypeSolid:
case kVkRenderType_AT:
break;
case kVkRenderType_COUNT:
ASSERT(!"Invalid model render_type");
break;
}
for (int i = 0; i < args.model->num_geometries; ++i) {
@ -539,8 +545,8 @@ void VK_RayFrameAddModel( vk_ray_model_t *model, const vk_render_model_t *render
model->kusochki_updated_this_frame = true;
}
for (int i = 0; i < render_model->polylights_count; ++i) {
rt_light_add_polygon_t *const polylight = render_model->polylights + i;
for (int i = 0; i < render_model->dynamic_polylights_count; ++i) {
rt_light_add_polygon_t *const polylight = render_model->dynamic_polylights + i;
polylight->transform_row = (const matrix3x4*)transform_row;
polylight->dynamic = true;
RT_LightAddPolygon(polylight);
@ -553,8 +559,7 @@ void RT_RayModel_Clear(void) {
R_DEBuffer_Init(&g_ray_model_state.kusochki_alloc, MAX_KUSOCHKI / 2, MAX_KUSOCHKI / 2);
}
void XVK_RayModel_ClearForNextFrame( void )
{
void XVK_RayModel_ClearForNextFrame( void ) {
// FIXME we depend on the fact that only a single frame can be in flight
// currently framectl waits for the queue to complete before returning
// so we can be sure here that previous frame is complete and we're free to

View File

@ -662,6 +662,8 @@ qboolean VK_RenderModelInit( vk_render_model_t *model ) {
.model = model,
};
model->ray_model = VK_RayModelCreate(args);
model->dynamic_polylights = NULL;
model->dynamic_polylights_count = 0;
return !!model->ray_model;
}
@ -670,8 +672,11 @@ qboolean VK_RenderModelInit( vk_render_model_t *model ) {
}
void VK_RenderModelDestroy( vk_render_model_t* model ) {
// FIXME why the condition? we should do the cleanup anyway
if (vk_core.rtx && (g_render_state.current_frame_is_ray_traced || !model->dynamic)) {
VK_RayModelDestroy(model->ray_model);
if (model->dynamic_polylights)
Mem_Free(model->dynamic_polylights);
}
}

View File

@ -59,10 +59,6 @@ typedef struct vk_render_geometry_s {
vec3_t emissive;
} vk_render_geometry_t;
struct vk_ray_model_s;
#define MAX_MODEL_NAME_LENGTH 64
typedef enum {
kVkRenderTypeSolid, // no blending, depth RW
kVkRenderType_A_1mA_RW, // blend: src*a + dst*(1-a), depth: RW
@ -73,11 +69,14 @@ typedef enum {
kVkRenderType_1_1_R, // blend: src + dst, depth test
kVkRenderType_COUNT
} vk_render_type_e;
struct rt_light_add_polygon_s;
struct vk_ray_model_s;
typedef struct vk_render_model_s {
#define MAX_MODEL_NAME_LENGTH 64
char debug_name[MAX_MODEL_NAME_LENGTH];
// FIXME: brushes, sprites, studio models, etc all treat render_mode differently
vk_render_type_e render_type;
vec4_t color;
int lightmap; // <= 0 if no lightmap
@ -88,13 +87,13 @@ typedef struct vk_render_model_s {
// This model will be one-frame only, its buffers are not preserved between frames
qboolean dynamic;
// FIXME ...
qboolean static_map;
// Non-NULL only for ray tracing
struct vk_ray_model_s *ray_model;
struct rt_light_add_polygon_s *polylights;
int polylights_count;
// Polylights which need to be added per-frame dynamically
// Used for non-worldmodel brush models which are not static
struct rt_light_add_polygon_s *dynamic_polylights;
int dynamic_polylights_count;
// previous frame ObjectToWorld (model) matrix
matrix4x4 prev_transform;

View File

@ -82,7 +82,6 @@ static struct {
rt_resource_t res[MAX_RESOURCES];
qboolean reload_pipeline;
qboolean reload_lighting;
matrix4x4 prev_inv_proj, prev_inv_view;
} g_rtx = {0};
@ -140,12 +139,6 @@ void VK_RayFrameBegin( void ) {
R_PrevFrame_StartFrame();
// TODO: move all lighting update to scene?
if (g_rtx.reload_lighting) {
g_rtx.reload_lighting = false;
// FIXME temporarily not supported VK_LightsLoadMapStaticLights();
}
// TODO shouldn't we do this in freeze models mode anyway?
RT_LightsFrameBegin();
}
@ -601,10 +594,6 @@ static void reloadPipeline( void ) {
g_rtx.reload_pipeline = true;
}
static void reloadLighting( void ) {
g_rtx.reload_lighting = true;
}
static void freezeModels( void ) {
g_ray_model_state.freeze_models = !g_ray_model_state.freeze_models;
}
@ -653,7 +642,6 @@ qboolean VK_RayInit( void )
RT_RayModel_Clear();
gEngine.Cmd_AddCommand("vk_rtx_reload", reloadPipeline, "Reload RTX shader");
gEngine.Cmd_AddCommand("vk_rtx_reload_rad", reloadLighting, "Reload RAD files for static lights");
gEngine.Cmd_AddCommand("vk_rtx_freeze", freezeModels, "Freeze models, do not update/add/delete models from to-draw list");
return true;

View File

@ -61,6 +61,44 @@ static struct {
draw_list_t *draw_list;
} g_lists;
static void loadLights( const model_t *const map ) {
RT_LightsLoadBegin(map);
const int num_models = gEngine.EngineGetParm( PARM_NUMMODELS, 0 );
for( int i = 0; i < num_models; i++ ) {
const model_t *const mod = gEngine.pfnGetModelByIndex( i + 1 );
if (!mod)
continue;
if( mod->type != mod_brush )
continue;
const qboolean is_worldmodel = i == 0;
R_VkBrushModelCollectEmissiveSurfaces(mod, is_worldmodel);
}
// Load static map lights
// Reads surfaces from loaded brush models (must happen after all brushes are loaded)
RT_LightsLoadEnd();
}
static void reloadMaterials( void ) {
// Materials do affect patching, as new materials can be referenced in patch data
// So we must do the full sequence
XVK_ParseMapEntities();
XVK_ReloadMaterials();
XVK_ParseMapPatches();
// Assumes that the map has been loaded
// Might have loaded new patch data, need to reload lighting data just in case
const model_t *const map = gEngine.pfnGetModelByIndex( 1 );
loadLights(map);
}
// Same as the above, but avoids reloading heavy materials data
// Only reloads light entities, patches, rad files
static void reloadPatches( void ) {
// Must re-parse map entities to initialize initial values before patching
XVK_ParseMapEntities();
@ -70,23 +108,8 @@ static void reloadPatches( void ) {
// Assumes that the map brush model has been loaded
// Patching does disturb light sources, reinitialize
// FIXME loading patches will be broken now ;_;
// ?? VK_LightsLoadMapStaticLights();
}
static void reloadMaterials( void ) {
// Materials do affect patching, as new materials can be referenced in patch data
// So we must do the full sequencce
XVK_ParseMapEntities();
XVK_ReloadMaterials();
XVK_ParseMapPatches();
// Assumes that the map has been loaded
// Might have loaded new patch data, need to reload lighting data just in case
// FIXME loading patches will be broken now ;_;
// ??? VK_LightsLoadMapStaticLights();
const model_t *const map = gEngine.pfnGetModelByIndex( 1 );
loadLights(map);
}
void VK_SceneInit( void )
@ -95,9 +118,11 @@ void VK_SceneInit( void )
g_lists.draw_list = g_lists.draw_stack;
g_lists.draw_stack_pos = 0;
if (vk_core.rtx) {
gEngine.Cmd_AddCommand("vk_rtx_reload_materials", reloadMaterials, "Reload PBR materials");
gEngine.Cmd_AddCommand("vk_rtx_reload_patches", reloadPatches, "Reload patches (does not update surface deletion)");
gEngine.Cmd_AddCommand("vk_rtx_reload_rad", reloadPatches, "Reload RAD files for static lights");
}
}
@ -178,9 +203,6 @@ void R_NewMap( void ) {
// Depends on loaded materials. Must preceed loading brush models.
XVK_ParseMapPatches();
// Need parsed map entities, and also should happen before brush model loading
RT_LightsNewMapBegin(map);
// Load all models at once
gEngine.Con_Reportf( "Num models: %d:\n", num_models );
for( int i = 0; i < num_models; i++ )
@ -194,15 +216,12 @@ void R_NewMap( void ) {
if( m->type != mod_brush )
continue;
if (!VK_BrushModelLoad(m, i == 0))
{
gEngine.Con_Printf( S_ERROR "Couldn't load model %s\n", m->name );
}
if (!VK_BrushModelLoad(m))
gEngine.Host_Error( "Couldn't load model %s\n", m->name );
}
// Load static map lights
// Reads surfaces from loaded brush models (must happen after all brushes are loaded)
RT_LightsNewMapEnd(map);
RT_LightsNewMap(map);
loadLights(map);
// TODO should we do something like VK_BrushEndLoad?
VK_UploadLightmap();