/*
===========================================================================
Copyright (C) 1999-2005 Id Software, Inc.

This file is part of Quake III Arena source code.

Quake III Arena source code 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 2 of the License,
or (at your option) any later version.

Quake III Arena source code 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.

You should have received a copy of the GNU General Public License
along with Quake III Arena source code; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
===========================================================================
*/
// tr_map.c

#include "tr_local.h"
#ifdef USE_VULKAN
#include "vk.h"
#endif

/*

Loads and prepares a map file for scene rendering.

A single entry point:

void RE_LoadWorldMap( const char *name );

*/

static	world_t		s_worldData;
static	byte		*fileBase;

static int	c_gridVerts;

//===============================================================================

static void HSVtoRGB( float h, float s, float v, float rgb[3] )
{
	int i;
	float f;
	float p, q, t;

	h *= 5;

	i = floor( h );
	f = h - i;

	p = v * ( 1 - s );
	q = v * ( 1 - s * f );
	t = v * ( 1 - s * ( 1 - f ) );

	switch ( i )
	{
	case 0:
		rgb[0] = v;
		rgb[1] = t;
		rgb[2] = p;
		break;
	case 1:
		rgb[0] = q;
		rgb[1] = v;
		rgb[2] = p;
		break;
	case 2:
		rgb[0] = p;
		rgb[1] = v;
		rgb[2] = t;
		break;
	case 3:
		rgb[0] = p;
		rgb[1] = q;
		rgb[2] = v;
		break;
	case 4:
		rgb[0] = t;
		rgb[1] = p;
		rgb[2] = v;
		break;
	case 5:
		rgb[0] = v;
		rgb[1] = p;
		rgb[2] = q;
		break;
	}
}


/*
===============
R_ClampDenorm

Clamp fp values that may result in denormalization after further multiplication
===============
*/
float R_ClampDenorm( float v ) {
	if ( fabsf( v ) > 0.0f && fabsf( v ) < 1e-9f ) {
		return 0.0f;
	} else {
		return v;
	}
}


/*
===============
R_ColorShiftLightingBytes
===============
*/
void R_ColorShiftLightingBytes( const byte in[4], byte out[4], qboolean hasAlpha ) {
	int		shift, r, g, b;

	// shift the color data based on overbright range
	shift = r_mapOverBrightBits->integer - tr.overbrightBits;

	// shift the data based on overbright range
	if ( shift >= 0 ) {
		r = in[0] << shift;
		g = in[1] << shift;
		b = in[2] << shift;
		// normalize by color instead of saturating to white
		if ( ( r | g | b ) > 255 ) {
			int max = r > g ? r : g;
			max = max > b ? max : b;
			r = r * 255 / max;
			g = g * 255 / max;
			b = b * 255 / max;
		}
	} else {
		r = in[0] >> -shift;
		g = in[1] >> -shift;
		b = in[2] >> -shift;
	}

	if ( r_mapGreyScale->integer ) {
		const byte luma = LUMA( r, g, b );
		out[0] = luma;
		out[1] = luma;
		out[2] = luma;
	} else if( r_mapGreyScale->value ) {
		const float scale = fabs( r_mapGreyScale->value );
		const float luma = LUMA( r, g, b );
		out[0] = LERP( r, luma, scale );
		out[1] = LERP( g, luma, scale );
		out[2] = LERP( b, luma, scale );
	} else {
		out[0] = r;
		out[1] = g;
		out[2] = b;
	}

	if ( hasAlpha ) {
		out[3] = in[3];
	}
}


#define LIGHTMAP_SIZE 128
#define LIGHTMAP_BORDER 2
#define LIGHTMAP_LEN (LIGHTMAP_SIZE + LIGHTMAP_BORDER*2)

static const int lightmapFlags = IMGFLAG_NOLIGHTSCALE | IMGFLAG_NO_COMPRESSION | IMGFLAG_LIGHTMAP | IMGFLAG_NOSCALE;

static int lightmapWidth;
static int lightmapHeight;
static int lightmapCountX;
static int lightmapCountY;

static void FillBorders( byte *img )
{
#define PIX(xx,yy,offs) img[((yy)*LIGHTMAP_LEN + (xx))*4+(offs)]
	int x0, y0;
	int x1, y1;
	int n, len, i;

	for ( n = LIGHTMAP_BORDER; n > 0; n-- )
	{
		x0 = n - 1; x1 = LIGHTMAP_LEN - n;
		y0 = n - 1; y1 = LIGHTMAP_LEN - n;
		len = LIGHTMAP_SIZE + (LIGHTMAP_BORDER*2 - n);
		for ( i = n; i < len; i++ ) 
		{
			PIX( i, y0, 0 ) = PIX( i, y0+1, 0 );
			PIX( i, y0, 1 ) = PIX( i, y0+1, 1 );
			PIX( i, y0, 2 ) = PIX( i, y0+1, 2 );
			PIX( i, y0, 3 ) = PIX( i, y0+1, 3 );

			PIX( x0, i, 0 ) = PIX( x0+1, i, 0 );
			PIX( x0, i, 1 ) = PIX( x0+1, i, 1 );
			PIX( x0, i, 2 ) = PIX( x0+1, i, 2 );
			PIX( x0, i, 3 ) = PIX( x0+1, i, 3 );

			PIX( i, y1, 0 ) = PIX( i, y1-1, 0 );
			PIX( i, y1, 1 ) = PIX( i, y1-1, 1 );
			PIX( i, y1, 2 ) = PIX( i, y1-1, 2 );
			PIX( i, y1, 3 ) = PIX( i, y1-1, 3 );

			PIX( x1, i, 0 ) = PIX( x1-1, i, 0 );
			PIX( x1, i, 1 ) = PIX( x1-1, i, 1 );
			PIX( x1, i, 2 ) = PIX( x1-1, i, 2 );
			PIX( x1, i, 3 ) = PIX( x1-1, i, 3 );
		}

		// interpolate corners
		PIX( x0, y0, 0 ) = (int)(PIX( x0, y0+1, 0 ) + PIX( x0+1, y0, 0 )) >> 1;
		PIX( x0, y0, 1 ) = (int)(PIX( x0, y0+1, 1 ) + PIX( x0+1, y0, 1 )) >> 1;
		PIX( x0, y0, 2 ) = (int)(PIX( x0, y0+1, 2 ) + PIX( x0+1, y0, 2 )) >> 1;
		PIX( x0, y0, 3 ) = (int)(PIX( x0, y0+1, 3 ) + PIX( x0+1, y0, 3 )) >> 1;
		
		PIX( x1, y0, 0 ) = (int)(PIX( x1-1, y0, 0 ) + PIX( x1, y0+1, 0 )) >> 1;
		PIX( x1, y0, 1 ) = (int)(PIX( x1-1, y0, 1 ) + PIX( x1, y0+1, 1 )) >> 1;
		PIX( x1, y0, 2 ) = (int)(PIX( x1-1, y0, 2 ) + PIX( x1, y0+1, 2 )) >> 1;
		PIX( x1, y0, 3 ) = (int)(PIX( x1-1, y0, 3 ) + PIX( x1, y0+1, 3 )) >> 1;
	
		PIX( x0, y1, 0 ) = (int)(PIX( x0, y1-1, 0 ) + PIX( x0+1, y1, 0 )) >> 1;
		PIX( x0, y1, 1 ) = (int)(PIX( x0, y1-1, 1 ) + PIX( x0+1, y1, 1 )) >> 1;
		PIX( x0, y1, 2 ) = (int)(PIX( x0, y1-1, 2 ) + PIX( x0+1, y1, 2 )) >> 1;
		PIX( x0, y1, 3 ) = (int)(PIX( x0, y1-1, 3 ) + PIX( x0+1, y1, 3 )) >> 1;

		PIX( x1, y1, 0 ) = (int)(PIX( x1, y1-1, 0 ) + PIX( x1-1, y1, 0 )) >> 1;
		PIX( x1, y1, 1 ) = (int)(PIX( x1, y1-1, 1 ) + PIX( x1-1, y1, 1 )) >> 1;
		PIX( x1, y1, 2 ) = (int)(PIX( x1, y1-1, 2 ) + PIX( x1-1, y1, 2 )) >> 1;
		PIX( x1, y1, 3 ) = (int)(PIX( x1, y1-1, 3 ) + PIX( x1-1, y1, 3 )) >> 1;
	}
}


/*
===============
R_ProcessLightmap

expand the 24 bit on-disk to 32 bit and return max.intensity
===============
*/
static float R_ProcessLightmap( byte *image, const byte *buf_p, float maxIntensity )
{
	int x, y;

	if ( 0 && r_lightmap->integer == 2 ) {
		int j;
		// color code by intensity as development tool	(FIXME: check range)
		for ( j = 0; j < LIGHTMAP_SIZE * LIGHTMAP_SIZE; j++ )
		{
			float r = buf_p[j*3+0];
			float g = buf_p[j*3+1];
			float b = buf_p[j*3+2];
			float intensity;
			float out[3] = {0.0, 0.0, 0.0};

			intensity = 0.33f * r + 0.685f * g + 0.063f * b;

			if ( intensity > 255 )
				intensity = 1.0f;
			else
				intensity /= 255.0f;

			if ( intensity > maxIntensity )
				maxIntensity = intensity;

			HSVtoRGB( intensity, 1.00, 0.50, out );

			image[j*4+0] = out[0] * 255;
			image[j*4+1] = out[1] * 255;
			image[j*4+2] = out[2] * 255;
			image[j*4+3] = 255;
		}
	} else {
		if ( tr.mergeLightmaps ) {
			for ( y = 0 ; y < LIGHTMAP_SIZE; y++ ) {
				for ( x = 0 ; x < LIGHTMAP_SIZE; x++ ) {
					byte *dst = &image[((y + LIGHTMAP_BORDER) * LIGHTMAP_LEN + x + LIGHTMAP_BORDER) * 4];
					R_ColorShiftLightingBytes( buf_p, dst, qfalse );
					dst[3] = 255;
					buf_p += 3;
				}
			}
			FillBorders( image );
		} else {
			// legacy path
			for ( y = 0 ; y < LIGHTMAP_SIZE; y++ ) {
				for ( x = 0 ; x < LIGHTMAP_SIZE; x++ ) {
					byte *dst = &image[(y * LIGHTMAP_SIZE + x) * 4];
					R_ColorShiftLightingBytes( buf_p, dst, qfalse );
					dst[3] = 255;
					buf_p += 3;
				}
			}
		}
	}

	return maxIntensity;
}


static int SetLightmapParams( int numLightmaps, int maxTextureSize )
{
	lightmapWidth = log2pad( LIGHTMAP_LEN, 1 );
	lightmapHeight = log2pad( LIGHTMAP_LEN, 1 );

	lightmapCountX = 1;
	lightmapCountY = 1;

	while ( lightmapWidth < maxTextureSize && lightmapCountX * lightmapCountY < numLightmaps )
	{
		lightmapWidth = log2pad( lightmapWidth + LIGHTMAP_LEN, 1 );
		lightmapCountX = lightmapWidth / LIGHTMAP_LEN;
		if ( lightmapCountX * lightmapCountY >= numLightmaps )
			break;
		lightmapHeight = log2pad( lightmapHeight + LIGHTMAP_LEN, 1 );
		lightmapCountY = lightmapHeight / LIGHTMAP_LEN;
	}

	tr.lightmapMod = lightmapCountX * lightmapCountY;

	tr.lightmapScale[0] = (double)LIGHTMAP_SIZE / (double) lightmapWidth;
	tr.lightmapScale[1] = (double)LIGHTMAP_SIZE / (double) lightmapHeight;

	numLightmaps = ( numLightmaps + tr.lightmapMod - 1 ) / tr.lightmapMod;

	return numLightmaps;
}


int R_GetLightmapCoords( const int lightmapIndex, float *x, float *y )
{
	const int lightmapNum = lightmapIndex / tr.lightmapMod;
	const int cN = lightmapIndex % tr.lightmapMod;
	const int cX = cN % lightmapCountX;
	const int cY = cN / lightmapCountX;

	*x = (float)( LIGHTMAP_BORDER + cX * LIGHTMAP_LEN ) / (float) lightmapWidth;
	*y = (float)( LIGHTMAP_BORDER + cY * LIGHTMAP_LEN ) / (float) lightmapHeight;

	return lightmapNum;
}


/*
===============
R_LoadMergedLightmaps
===============
*/
static void R_LoadMergedLightmaps( const lump_t *l, byte *image )
{
	const byte	*buf;
	int			offs;
	int			i, x, y;
 	float		maxIntensity = 0;

	if ( l->filelen < LIGHTMAP_SIZE * LIGHTMAP_SIZE * 3 )
		return;

	buf = fileBase + l->fileofs;

	// create all the lightmaps
	tr.numLightmaps = l->filelen / (LIGHTMAP_SIZE * LIGHTMAP_SIZE * 3);

	tr.numLightmaps = SetLightmapParams( tr.numLightmaps, glConfig.maxTextureSize );

	tr.lightmaps = ri.Hunk_Alloc( tr.numLightmaps * sizeof(image_t *), h_low );

	for ( offs = 0, i = 0 ; i < tr.numLightmaps; i++ ) {

		tr.lightmaps[ i ] = R_CreateImage( va( "*mergedLightmap%d", i ), NULL, NULL,
			lightmapWidth, lightmapHeight, lightmapFlags | IMGFLAG_CLAMPTOBORDER );

		for ( y = 0; y < lightmapCountY; y++ ) {
			if ( offs >= l->filelen )
				break;

			for ( x = 0; x < lightmapCountX; x++ ) {
				if ( offs >= l->filelen )
					break;

				R_ProcessLightmap( image, buf + offs, maxIntensity );
				
#ifdef USE_VULKAN
				vk_upload_image_data( tr.lightmaps[ i ], x * LIGHTMAP_LEN, y * LIGHTMAP_LEN, LIGHTMAP_LEN, LIGHTMAP_LEN, 1, image, LIGHTMAP_LEN * LIGHTMAP_LEN * 4, qtrue );
#else
				R_UploadSubImage( image, x * LIGHTMAP_LEN, y * LIGHTMAP_LEN, LIGHTMAP_LEN, LIGHTMAP_LEN, tr.lightmaps[ i ] );
#endif

				offs += LIGHTMAP_SIZE * LIGHTMAP_SIZE * 3;
			}
		}
#ifdef USE_VULKAN
		//
#else
		ri.Printf( PRINT_DEVELOPER, "lightmaps[%i]=%i\n", i, tr.lightmaps[i]->texnum );
#endif
	}

	//if ( r_lightmap->integer == 2 )	{
	//	ri.Printf( PRINT_ALL, "Brightest lightmap value: %d\n", ( int ) ( maxIntensity * 255 ) );
	//}
}


/*
===============
R_LoadLightmaps
===============
*/
static void R_LoadLightmaps( const lump_t *l ) {
	const byte	*buf;
	byte		image[LIGHTMAP_LEN*LIGHTMAP_LEN*4];
	int			i, numLightmaps;
	float		maxIntensity = 0;

	tr.numLightmaps = 0;
	tr.mergeLightmaps = qfalse;
	tr.lightmapScale[0] = 1.0f;
	tr.lightmapScale[1] = 1.0f;
	tr.lightmapOffset[0] = 0.0f;
	tr.lightmapOffset[1] = 0.0f;
	tr.lightmapMod = MAX_QINT;
	lightmapWidth = LIGHTMAP_SIZE;
	lightmapHeight = LIGHTMAP_SIZE;
	lightmapCountX = 1;
	lightmapCountY = 1;

	if ( l->filelen < LIGHTMAP_SIZE * LIGHTMAP_SIZE * 3 ) {
		return;
	}

	// if we are in r_vertexLight mode, we don't need the lightmaps at all
	if ( r_vertexLight->integer || glConfig.hardwareType == GLHW_PERMEDIA2 ) {
		return;
	}

	numLightmaps = l->filelen / (LIGHTMAP_SIZE * LIGHTMAP_SIZE * 3);

	if ( r_mergeLightmaps->integer && numLightmaps > 1 ) {
		// check for low texture sizes
		if ( glConfig.maxTextureSize >= LIGHTMAP_LEN * 2 ) {
			tr.mergeLightmaps = qtrue;
			R_LoadMergedLightmaps( l, image ); // reuse stack space
			return;
		}
	}

	buf = fileBase + l->fileofs;

	// create all the lightmaps
	tr.numLightmaps = numLightmaps;

	tr.lightmaps = ri.Hunk_Alloc( tr.numLightmaps * sizeof(image_t *), h_low );

	for ( i = 0 ; i < tr.numLightmaps ; i++ ) {
		maxIntensity = R_ProcessLightmap( image, buf + i * LIGHTMAP_SIZE * LIGHTMAP_SIZE * 3, maxIntensity );
		tr.lightmaps[i] = R_CreateImage( va( "*lightmap%d", i ), NULL, image, LIGHTMAP_SIZE, LIGHTMAP_SIZE,
			lightmapFlags | IMGFLAG_CLAMPTOEDGE );
	}

	//if ( r_lightmap->integer == 2 )	{
	//	ri.Printf( PRINT_ALL, "Brightest lightmap value: %d\n", ( int ) ( maxIntensity * 255 ) );
	//}
}


/*
=================
RE_SetWorldVisData

This is called by the clipmodel subsystem so we can share the 1.8 megs of
space in big maps...
=================
*/
void RE_SetWorldVisData( const byte *vis ) {
	tr.externalVisData = vis;
}


/*
=================
R_LoadVisibility
=================
*/
static void R_LoadVisibility( const lump_t *l ) {
	unsigned numClusters, clusterBytes, len;
	byte	*buf;

	len = PAD( s_worldData.numClusters, 64 ) >> 3;
	s_worldData.novis = ri.Hunk_Alloc( len, h_low );
	Com_Memset( s_worldData.novis, 0xff, len );

	len = l->filelen;
	if ( !len ) {
		return;
	}

	if ( len < VIS_HEADER ) {
		Com_Error( ERR_DROP, "%s: lump too short", __func__ );
	}

	buf = fileBase + l->fileofs;
	numClusters = LittleLong( ((int *)buf)[0] );
	clusterBytes = LittleLong( ((int *)buf)[1] );

	buf += VIS_HEADER;
	len -= VIS_HEADER;

	if ( (uint64_t)numClusters * clusterBytes > len ) {
		Com_Error( ERR_DROP, "%s: lump too short", __func__ );
	}
	if ( numClusters < s_worldData.numClusters ) {
		Com_Error( ERR_DROP, "%s: bad numClusters", __func__ );
	}
	if ( clusterBytes < (numClusters + 7) >> 3 ) {
		Com_Error( ERR_DROP, "%s: bad clusterBytes", __func__ );
	}

	s_worldData.numClusters = numClusters;
	s_worldData.clusterBytes = clusterBytes;

	// CM_Load should have given us the vis data to share, so
	// we don't need to allocate another copy
	if ( tr.externalVisData ) {
		s_worldData.vis = tr.externalVisData;
	} else {
		byte	*dest;

		dest = ri.Hunk_Alloc( len, h_low );
		Com_Memcpy( dest, buf, len );
		s_worldData.vis = dest;
	}
}

//===============================================================================


/*
===============
ShaderForShaderNum
===============
*/
static shader_t *ShaderForShaderNum( const int shaderNum, int lightmapNum ) {
	shader_t	*shader;
	const dshader_t *dsh;

	if ( shaderNum < 0 || shaderNum >= s_worldData.numShaders ) {
		ri.Error( ERR_DROP, "ShaderForShaderNum: bad num %i", shaderNum );
	}

	dsh = &s_worldData.shaders[ shaderNum ];

	if ( ( r_vertexLight->integer && tr.vertexLightingAllowed ) || glConfig.hardwareType == GLHW_PERMEDIA2 ) {
		lightmapNum = LIGHTMAP_BY_VERTEX;
	}

	if ( r_fullbright->integer ) {
		lightmapNum = LIGHTMAP_WHITEIMAGE;
	}

	shader = R_FindShader( dsh->shader, lightmapNum, qtrue );

	// if the shader had errors, just use default shader
	if ( shader->defaultShader ) {
		return tr.defaultShader;
	}

	if ( r_singleShader->integer && !shader->isSky ) {
		return tr.defaultShader;
	}

	return shader;
}


#ifdef USE_PMLIGHT
static void GenerateNormals( srfSurfaceFace_t *face )
{
	vec3_t ba, ca, cross;
	float *v1, *v2, *v3, *n1, *n2, *n3;
	int i, *indices, i0, i1, i2;

	indices = ((int *)((byte *)face + face->ofsIndices));

	// store as vec4_t so we can simply use memcpy() during tesselation
	face->normals = ri.Hunk_Alloc( face->numPoints * sizeof( tess.normal[0] ), h_low );

	for ( i = 0; i < face->numIndices; i += 3 ) {
		i0 = indices[i+0];
		i1 = indices[i+1];
		i2 = indices[i+2];
		if ( i0 >= face->numPoints || i1 >= face->numPoints || i2 >= face->numPoints )
			continue;
		v1 = face->points[i0];
		v2 = face->points[i1];
		v3 = face->points[i2];
		VectorSubtract( v3, v1, ca );
		VectorSubtract( v2, v1, ba );
		CrossProduct( ca, ba, cross );
		n1 = face->normals + indices[i+0]*4;
		n2 = face->normals + indices[i+1]*4;
		n3 = face->normals + indices[i+2]*4;
		VectorAdd( n1, cross, n1 );
		VectorAdd( n2, cross, n2 );
		VectorAdd( n3, cross, n3 );
	}

	for ( i = 0; i < face->numPoints; i++ ) {
		n1 = face->normals + i*4;
		VectorNormalize2( n1, n1 );
		for ( i0 = 0; i0 < 3; i0++ ) {
			n1[i0] = R_ClampDenorm( n1[i0] );
		}
	}
}
#endif // USE_PMLIGHT


/*
=============
qsort_idx
=============
*/
void qsort_idx( int32_t *a, const int n ) {
	int32_t temp[3], m;
	int i, j, x;

	i = 0;
	j = n;
	x = (n >> 1)*3;
	m = a[ x + 0 ] + a[ x + 1 ] + a[ x + 2 ];

	do {
		while ( a[i*3+0]+a[i*3+1]+a[i*3+2] < m )
			i++;
		while ( a[j*3+0]+a[j*3+1]+a[j*3+2] > m )
			j--;
		if ( i <= j ) {
			memcpy( temp, &a[i*3], sizeof( temp ) );
			memcpy( &a[i*3], &a[j*3], sizeof( temp ) );
			memcpy( &a[j*3], temp, sizeof( temp ) );
			i++;
			j--;
		}
	} while ( i <= j );

	if ( j > 0 ) qsort_idx( a, j );
	if ( n > i ) qsort_idx( a+i*3, n-i );
}


/*
===============
ParseFace
===============
*/
static void ParseFace( const dsurface_t *ds, const drawVert_t *verts, int numPoints, msurface_t *surf, int *srcIndexes, int numIndexes ) {
	int			i, j;
	srfSurfaceFace_t	*cv;
	int			*indexes;
	int			lightmapNum;
	float		lightmapX, lightmapY;
	int			sfaceSize, ofsIndexes;
	//static const int idx_pattern[] = {2, 3, 4, 3, 5, 4};
	//static const int idx_pattern2[] = {5, 4, 3, 2, 3, 4};

	lightmapNum = LittleLong( ds->lightmapNum );
	if ( lightmapNum >= 0 && tr.mergeLightmaps ) {
		lightmapNum = R_GetLightmapCoords( lightmapNum, &lightmapX, &lightmapY );
	} else {
		lightmapX = lightmapY = 0.0f;
	}

	tr.lightmapOffset[0] = lightmapX;
	tr.lightmapOffset[1] = lightmapY;

	// get shader value
	surf->shader = ShaderForShaderNum( LittleLong( ds->shaderNum ), lightmapNum );

	if (numPoints > MAX_FACE_POINTS) {
		ri.Printf( PRINT_WARNING, "WARNING: MAX_FACE_POINTS exceeded: %i\n", numPoints);
		numPoints = MAX_FACE_POINTS;
		surf->shader = tr.defaultShader;
	}

	// create the srfSurfaceFace_t
	sfaceSize = sizeof( *cv ) - sizeof( cv->points ) + sizeof( cv->points[0] ) * numPoints;
	ofsIndexes = sfaceSize;
	sfaceSize += sizeof( int ) * numIndexes;

	cv = ri.Hunk_Alloc( sfaceSize, h_low );
	cv->surfaceType = SF_FACE;
	cv->numPoints = numPoints;
	cv->numIndices = numIndexes;
	cv->ofsIndices = ofsIndexes;

	for ( i = 0 ; i < numPoints ; i++ ) {
		for ( j = 0 ; j < 3 ; j++ ) {
			cv->points[i][j] = LittleFloat( verts[i].xyz[j] );
		}
		for ( j = 0 ; j < 2 ; j++ ) {
			cv->points[i][3+j] = LittleFloat( verts[i].st[j] );
			cv->points[i][5+j] = LittleFloat( verts[i].lightmap[j] );
		}
		R_ColorShiftLightingBytes( verts[i].color.rgba, (byte *)&cv->points[i][7], qtrue );
		if ( lightmapNum >= 0 && tr.mergeLightmaps ) {
			// adjust lightmap coords
			cv->points[i][5] = cv->points[i][5] * tr.lightmapScale[0] + lightmapX;
			cv->points[i][6] = cv->points[i][6] * tr.lightmapScale[1] + lightmapY;
		}
	}

	indexes = (int*)((byte *) cv + cv->ofsIndices);

	for ( i = 0 ; i < numIndexes ; i++ ) {
		unsigned num = LittleLong( srcIndexes[ i ] );
		if ( num >= numPoints )
			ri.Error( ERR_DROP, "%s: bad index", __func__ );
		indexes[i] = num;
	}

	// reorder certain indexes to avoid bug on intel gen 9.5 hardware/vulkan driver
	// can be observed on lun3dm5 map
	//if ( numIndexes >=6 && memcmp( indexes, idx_pattern, sizeof( idx_pattern ) ) == 0 ) {
	//	memcpy( indexes, idx_pattern2, sizeof( idx_pattern2 ) );
	//}

	if ( numIndexes >= 6 ) {
		qsort_idx( indexes, (numIndexes / 3) - 1 );
	}

	// take the plane information from the lightmap vector
	for ( i = 0 ; i < 3 ; i++ ) {
		cv->plane.normal[i] = LittleFloat( ds->lightmapVecs[2][i] );
	}

#ifdef USE_PMLIGHT
	if ( surf->shader->numUnfoggedPasses && surf->shader->lightingStage >= 0 ) {
		if ( fabsf( cv->plane.normal[0] ) < 0.01f && fabsf( cv->plane.normal[1] ) < 0.01f && fabsf( cv->plane.normal[2] ) < 0.01f ) {
			// Zero-normals case:
			// might happen if surface contains multiple non-coplanar faces for terrain simulation
			// like in 'Pyramid of the Magician', 'tvy-bench' or 'terrast' maps
			// which results in non-working new per-pixel dynamic lighting.
			// So we will try to regenerate normals and apply smooth shading
			// for normals that is shared between multiple faces.
			// It is not a big problem for incorrectly (negative) generated normals
			// because it is unlikely for shared ones and will result in the same non-working lighting.
			// Also we will NOT update existing face->plane.normal to avoid potential surface culling issues
			GenerateNormals( cv );
		}
	}
#endif

	for ( i = 0; i < 3; i++ ) {
		cv->plane.normal[i] = R_ClampDenorm( cv->plane.normal[i] );
	}

	cv->plane.dist = DotProduct( cv->points[0], cv->plane.normal );
	SetPlaneSignbits( &cv->plane );
	cv->plane.type = PlaneTypeForNormal( cv->plane.normal );

	surf->data = (surfaceType_t *)cv;
}


/*
===============
ParseMesh
===============
*/
static void ParseMesh( const dsurface_t *ds, const drawVert_t *verts, int numVerts, msurface_t *surf ) {
	srfGridMesh_t	*grid;
	int				i, j;
	unsigned		width, height, numPoints;
	drawVert_t points[MAX_PATCH_SIZE*MAX_PATCH_SIZE];
	int				lightmapNum;
	float			lightmapX, lightmapY;
	vec3_t			bounds[2];
	vec3_t			tmpVec;
	static surfaceType_t	skipData = SF_SKIP;

	lightmapNum = LittleLong( ds->lightmapNum );
	if ( lightmapNum >= 0 && tr.mergeLightmaps ) {
		lightmapNum = R_GetLightmapCoords( lightmapNum, &lightmapX, &lightmapY );
	} else {
		lightmapX = lightmapY = 0.0f;
	}

	tr.lightmapOffset[0] = lightmapX;
	tr.lightmapOffset[1] = lightmapY;

	// get shader value
	surf->shader = ShaderForShaderNum( LittleLong( ds->shaderNum ), lightmapNum );

	// we may have a nodraw surface, because they might still need to
	// be around for movement clipping
	if ( s_worldData.shaders[ LittleLong( ds->shaderNum ) ].surfaceFlags & SURF_NODRAW ) {
		surf->data = &skipData;
		return;
	}

	width = LittleLong( ds->patchWidth );
	height = LittleLong( ds->patchHeight );

	// mirror CM_GeneratePatchCollide() checks
	if (width <= 2 || height <= 2 || !(width & 1) || !(height & 1) ||
		width > MAX_GRID_SIZE || height > MAX_GRID_SIZE ||
		width * height > ARRAY_LEN(points))
		ri.Error( ERR_DROP, "%s: bad patch size", __func__ );

	numPoints = width * height;
	if (numPoints > numVerts)
		ri.Error( ERR_DROP, "%s: verts out of range", __func__ );

	for ( i = 0 ; i < numPoints ; i++ ) {
		for ( j = 0 ; j < 3 ; j++ ) {
			points[i].xyz[j] = LittleFloat( verts[i].xyz[j] );
			points[i].normal[j] = R_ClampDenorm( LittleFloat( verts[i].normal[j] ) );
		}
		for ( j = 0 ; j < 2 ; j++ ) {
			points[i].st[j] = LittleFloat( verts[i].st[j] );
			points[i].lightmap[j] = LittleFloat( verts[i].lightmap[j] );
		}
		R_ColorShiftLightingBytes( verts[i].color.rgba, points[i].color.rgba, qtrue );
		if ( lightmapNum >= 0 && tr.mergeLightmaps ) {
			// adjust lightmap coords
			points[i].lightmap[0] = points[i].lightmap[0] * tr.lightmapScale[0] + lightmapX;
			points[i].lightmap[1] = points[i].lightmap[1] * tr.lightmapScale[1] + lightmapY;
		}
	}

	// pre-tesseleate
	grid = R_SubdividePatchToGrid( width, height, points );
	surf->data = (surfaceType_t *)grid;

	// copy the level of detail origin, which is the center
	// of the group of all curves that must subdivide the same
	// to avoid cracking
	for ( i = 0 ; i < 3 ; i++ ) {
		bounds[0][i] = LittleFloat( ds->lightmapVecs[0][i] );
		bounds[1][i] = LittleFloat( ds->lightmapVecs[1][i] );
	}
	VectorAdd( bounds[0], bounds[1], bounds[1] );
	VectorScale( bounds[1], 0.5f, grid->lodOrigin );
	VectorSubtract( bounds[0], grid->lodOrigin, tmpVec );
	grid->lodRadius = VectorLength( tmpVec );
}


/*
===============
ParseTriSurf
===============
*/
static void ParseTriSurf( const dsurface_t *ds, const drawVert_t *verts, int numVerts, msurface_t *surf, int *indexes, int numIndexes ) {
	srfTriangles_t	*tri;
	int				i, j;
	int				lightmapNum;
	float			lightmapX, lightmapY;

	lightmapNum = LittleLong( ds->lightmapNum );
	if ( lightmapNum >= 0 && tr.mergeLightmaps ) {
		lightmapNum = R_GetLightmapCoords( lightmapNum, &lightmapX, &lightmapY );
	} else {
		lightmapX = lightmapY = 0.0f;
	}

	tr.lightmapOffset[0] = lightmapX;
	tr.lightmapOffset[1] = lightmapY;

	// get shader
	surf->shader = ShaderForShaderNum( LittleLong( ds->shaderNum ), LIGHTMAP_BY_VERTEX );

	tri = ri.Hunk_Alloc( sizeof( *tri ) + numVerts * sizeof( tri->verts[0] ) 
		+ numIndexes * sizeof( tri->indexes[0] ), h_low );
	tri->surfaceType = SF_TRIANGLES;
	tri->numVerts = numVerts;
	tri->numIndexes = numIndexes;
	tri->verts = (drawVert_t *)(tri + 1);
	tri->indexes = (int *)(tri->verts + tri->numVerts );

	surf->data = (surfaceType_t *)tri;

	// copy vertexes
	ClearBounds( tri->bounds[0], tri->bounds[1] );
	for ( i = 0 ; i < numVerts ; i++ ) {
		for ( j = 0 ; j < 3 ; j++ ) {
			tri->verts[i].xyz[j] = LittleFloat( verts[i].xyz[j] );
			tri->verts[i].normal[j] = R_ClampDenorm( LittleFloat( verts[i].normal[j] ) );
		}
		AddPointToBounds( tri->verts[i].xyz, tri->bounds[0], tri->bounds[1] );
		for ( j = 0 ; j < 2 ; j++ ) {
			tri->verts[i].st[j] = LittleFloat( verts[i].st[j] );
			tri->verts[i].lightmap[j] = LittleFloat( verts[i].lightmap[j] );
		}

		R_ColorShiftLightingBytes( verts[i].color.rgba, tri->verts[i].color.rgba, qtrue );
		if ( lightmapNum >= 0 && tr.mergeLightmaps ) {
			// adjust lightmap coords
			tri->verts[i].lightmap[0] = tri->verts[i].lightmap[0] * tr.lightmapScale[0] + lightmapX;
			tri->verts[i].lightmap[1] = tri->verts[i].lightmap[1] * tr.lightmapScale[1] + lightmapY;
		}
	}

	// copy indexes
	for ( i = 0 ; i < numIndexes ; i++ ) {
		tri->indexes[i] = LittleLong( indexes[i] );
		if ( tri->indexes[i] < 0 || tri->indexes[i] >= numVerts ) {
			ri.Error( ERR_DROP, "Bad index in triangle surface" );
		}
	}
}


/*
===============
ParseFlare
===============
*/
static void ParseFlare( const dsurface_t *ds, msurface_t *surf ) {
	srfFlare_t		*flare;
	int				i;

	// get shader
	surf->shader = ShaderForShaderNum( LittleLong( ds->shaderNum ), LIGHTMAP_BY_VERTEX );

	flare = ri.Hunk_Alloc( sizeof( *flare ), h_low );
	flare->surfaceType = SF_FLARE;

	surf->data = (surfaceType_t *)flare;

	for ( i = 0 ; i < 3 ; i++ ) {
		flare->origin[i] = LittleFloat( ds->lightmapOrigin[i] );
		flare->color[i] = LittleFloat( ds->lightmapVecs[0][i] );
		flare->normal[i] = R_ClampDenorm( LittleFloat( ds->lightmapVecs[2][i] ) );
	}
}


/*
=================
R_MergedWidthPoints

returns qtrue if there are grid points merged on a width edge
=================
*/
static qboolean R_MergedWidthPoints( const srfGridMesh_t *grid, int offset ) {
	int i, j;

	for (i = 1; i < grid->width-1; i++) {
		for (j = i + 1; j < grid->width-1; j++) {
			if ( fabs(grid->verts[i + offset].xyz[0] - grid->verts[j + offset].xyz[0]) > .1) continue;
			if ( fabs(grid->verts[i + offset].xyz[1] - grid->verts[j + offset].xyz[1]) > .1) continue;
			if ( fabs(grid->verts[i + offset].xyz[2] - grid->verts[j + offset].xyz[2]) > .1) continue;
			return qtrue;
		}
	}
	return qfalse;
}


/*
=================
R_MergedHeightPoints

returns qtrue if there are grid points merged on a height edge
=================
*/
static qboolean R_MergedHeightPoints( const srfGridMesh_t *grid, int offset ) {
	int i, j;

	for (i = 1; i < grid->height-1; i++) {
		for (j = i + 1; j < grid->height-1; j++) {
			if ( fabs(grid->verts[grid->width * i + offset].xyz[0] - grid->verts[grid->width * j + offset].xyz[0]) > .1) continue;
			if ( fabs(grid->verts[grid->width * i + offset].xyz[1] - grid->verts[grid->width * j + offset].xyz[1]) > .1) continue;
			if ( fabs(grid->verts[grid->width * i + offset].xyz[2] - grid->verts[grid->width * j + offset].xyz[2]) > .1) continue;
			return qtrue;
		}
	}
	return qfalse;
}


/*
=================
R_FixSharedVertexLodError_r

NOTE: never sync LoD through grid edges with merged points!

FIXME: write generalized version that also avoids cracks between a patch and one that meets half way?
=================
*/
static void R_FixSharedVertexLodError_r( int start, srfGridMesh_t *grid1 ) {
	int j, k, l, m, n, offset1, offset2, touch;
	srfGridMesh_t *grid2;

	for ( j = start; j < s_worldData.numsurfaces; j++ ) {
		//
		grid2 = (srfGridMesh_t *) s_worldData.surfaces[j].data;
		// if this surface is not a grid
		if ( grid2->surfaceType != SF_GRID ) continue;
		// if the LOD errors are already fixed for this patch
		if ( grid2->lodFixed == 2 ) continue;
		// grids in the same LOD group should have the exact same lod radius
		if ( grid1->lodRadius != grid2->lodRadius ) continue;
		// grids in the same LOD group should have the exact same lod origin
		if ( grid1->lodOrigin[0] != grid2->lodOrigin[0] ) continue;
		if ( grid1->lodOrigin[1] != grid2->lodOrigin[1] ) continue;
		if ( grid1->lodOrigin[2] != grid2->lodOrigin[2] ) continue;
		//
		touch = qfalse;
		for (n = 0; n < 2; n++) {
			//
			if (n) offset1 = (grid1->height-1) * grid1->width;
			else offset1 = 0;
			if (R_MergedWidthPoints(grid1, offset1)) continue;
			for (k = 1; k < grid1->width-1; k++) {
				for (m = 0; m < 2; m++) {

					if (m) offset2 = (grid2->height-1) * grid2->width;
					else offset2 = 0;
					if (R_MergedWidthPoints(grid2, offset2)) continue;
					for ( l = 1; l < grid2->width-1; l++) {
					//
						if ( fabs(grid1->verts[k + offset1].xyz[0] - grid2->verts[l + offset2].xyz[0]) > .1) continue;
						if ( fabs(grid1->verts[k + offset1].xyz[1] - grid2->verts[l + offset2].xyz[1]) > .1) continue;
						if ( fabs(grid1->verts[k + offset1].xyz[2] - grid2->verts[l + offset2].xyz[2]) > .1) continue;
						// ok the points are equal and should have the same lod error
						grid2->widthLodError[l] = grid1->widthLodError[k];
						touch = qtrue;
					}
				}
				for (m = 0; m < 2; m++) {

					if (m) offset2 = grid2->width-1;
					else offset2 = 0;
					if (R_MergedHeightPoints(grid2, offset2)) continue;
					for ( l = 1; l < grid2->height-1; l++) {
					//
						if ( fabs(grid1->verts[k + offset1].xyz[0] - grid2->verts[grid2->width * l + offset2].xyz[0]) > .1) continue;
						if ( fabs(grid1->verts[k + offset1].xyz[1] - grid2->verts[grid2->width * l + offset2].xyz[1]) > .1) continue;
						if ( fabs(grid1->verts[k + offset1].xyz[2] - grid2->verts[grid2->width * l + offset2].xyz[2]) > .1) continue;
						// ok the points are equal and should have the same lod error
						grid2->heightLodError[l] = grid1->widthLodError[k];
						touch = qtrue;
					}
				}
			}
		}
		for (n = 0; n < 2; n++) {
			//
			if (n) offset1 = grid1->width-1;
			else offset1 = 0;
			if (R_MergedHeightPoints(grid1, offset1)) continue;
			for (k = 1; k < grid1->height-1; k++) {
				for (m = 0; m < 2; m++) {

					if (m) offset2 = (grid2->height-1) * grid2->width;
					else offset2 = 0;
					if (R_MergedWidthPoints(grid2, offset2)) continue;
					for ( l = 1; l < grid2->width-1; l++) {
					//
						if ( fabs(grid1->verts[grid1->width * k + offset1].xyz[0] - grid2->verts[l + offset2].xyz[0]) > .1) continue;
						if ( fabs(grid1->verts[grid1->width * k + offset1].xyz[1] - grid2->verts[l + offset2].xyz[1]) > .1) continue;
						if ( fabs(grid1->verts[grid1->width * k + offset1].xyz[2] - grid2->verts[l + offset2].xyz[2]) > .1) continue;
						// ok the points are equal and should have the same lod error
						grid2->widthLodError[l] = grid1->heightLodError[k];
						touch = qtrue;
					}
				}
				for (m = 0; m < 2; m++) {

					if (m) offset2 = grid2->width-1;
					else offset2 = 0;
					if (R_MergedHeightPoints(grid2, offset2)) continue;
					for ( l = 1; l < grid2->height-1; l++) {
					//
						if ( fabs(grid1->verts[grid1->width * k + offset1].xyz[0] - grid2->verts[grid2->width * l + offset2].xyz[0]) > .1) continue;
						if ( fabs(grid1->verts[grid1->width * k + offset1].xyz[1] - grid2->verts[grid2->width * l + offset2].xyz[1]) > .1) continue;
						if ( fabs(grid1->verts[grid1->width * k + offset1].xyz[2] - grid2->verts[grid2->width * l + offset2].xyz[2]) > .1) continue;
						// ok the points are equal and should have the same lod error
						grid2->heightLodError[l] = grid1->heightLodError[k];
						touch = qtrue;
					}
				}
			}
		}
		if (touch) {
			grid2->lodFixed = 2;
			R_FixSharedVertexLodError_r ( start, grid2 );
			//NOTE: this would be correct but makes things really slow
			//grid2->lodFixed = 1;
		}
	}
}


/*
=================
R_FixSharedVertexLodError

This function assumes that all patches in one group are nicely stitched together for the highest LoD.
If this is not the case this function will still do its job but won't fix the highest LoD cracks.
=================
*/
static void R_FixSharedVertexLodError( void ) {
	int i;
	srfGridMesh_t *grid1;

	for ( i = 0; i < s_worldData.numsurfaces; i++ ) {
		//
		grid1 = (srfGridMesh_t *) s_worldData.surfaces[i].data;
		// if this surface is not a grid
		if ( grid1->surfaceType != SF_GRID )
			continue;
		//
		if ( grid1->lodFixed )
			continue;
		//
		grid1->lodFixed = 2;
		// recursively fix other patches in the same LOD group
		R_FixSharedVertexLodError_r( i + 1, grid1);
	}
}


/*
===============
R_StitchPatches
===============
*/
static int R_StitchPatches( int grid1num, int grid2num ) {
	float *v1, *v2;
	srfGridMesh_t *grid1, *grid2;
	int k, l, m, n, offset1, offset2, row, column;

	grid1 = (srfGridMesh_t *) s_worldData.surfaces[grid1num].data;
	grid2 = (srfGridMesh_t *) s_worldData.surfaces[grid2num].data;
	for (n = 0; n < 2; n++) {
		//
		if (n) offset1 = (grid1->height-1) * grid1->width;
		else offset1 = 0;
		if (R_MergedWidthPoints(grid1, offset1))
			continue;
		for (k = 0; k < grid1->width-2; k += 2) {

			for (m = 0; m < 2; m++) {

				if ( grid2->width >= MAX_GRID_SIZE )
					break;
				if (m) offset2 = (grid2->height-1) * grid2->width;
				else offset2 = 0;
				for ( l = 0; l < grid2->width-1; l++) {
				//
					v1 = grid1->verts[k + offset1].xyz;
					v2 = grid2->verts[l + offset2].xyz;
					if ( fabs(v1[0] - v2[0]) > .1)
						continue;
					if ( fabs(v1[1] - v2[1]) > .1)
						continue;
					if ( fabs(v1[2] - v2[2]) > .1)
						continue;

					v1 = grid1->verts[k + 2 + offset1].xyz;
					v2 = grid2->verts[l + 1 + offset2].xyz;
					if ( fabs(v1[0] - v2[0]) > .1)
						continue;
					if ( fabs(v1[1] - v2[1]) > .1)
						continue;
					if ( fabs(v1[2] - v2[2]) > .1)
						continue;
					//
					v1 = grid2->verts[l + offset2].xyz;
					v2 = grid2->verts[l + 1 + offset2].xyz;
					if ( fabs(v1[0] - v2[0]) < .01 &&
							fabs(v1[1] - v2[1]) < .01 &&
							fabs(v1[2] - v2[2]) < .01)
						continue;
					//
					//ri.Printf( PRINT_ALL, "found highest LoD crack between two patches\n" );
					// insert column into grid2 right after column l
					if (m) row = grid2->height-1;
					else row = 0;
					grid2 = R_GridInsertColumn( grid2, l+1, row,
									grid1->verts[k + 1 + offset1].xyz, grid1->widthLodError[k+1]);
					grid2->lodStitched = qfalse;
					s_worldData.surfaces[grid2num].data = (void *) grid2;
					return qtrue;
				}
			}
			for (m = 0; m < 2; m++) {

				if (grid2->height >= MAX_GRID_SIZE)
					break;
				if (m) offset2 = grid2->width-1;
				else offset2 = 0;
				for ( l = 0; l < grid2->height-1; l++) {
					//
					v1 = grid1->verts[k + offset1].xyz;
					v2 = grid2->verts[grid2->width * l + offset2].xyz;
					if ( fabs(v1[0] - v2[0]) > .1)
						continue;
					if ( fabs(v1[1] - v2[1]) > .1)
						continue;
					if ( fabs(v1[2] - v2[2]) > .1)
						continue;

					v1 = grid1->verts[k + 2 + offset1].xyz;
					v2 = grid2->verts[grid2->width * (l + 1) + offset2].xyz;
					if ( fabs(v1[0] - v2[0]) > .1)
						continue;
					if ( fabs(v1[1] - v2[1]) > .1)
						continue;
					if ( fabs(v1[2] - v2[2]) > .1)
						continue;
					//
					v1 = grid2->verts[grid2->width * l + offset2].xyz;
					v2 = grid2->verts[grid2->width * (l + 1) + offset2].xyz;
					if ( fabs(v1[0] - v2[0]) < .01 &&
							fabs(v1[1] - v2[1]) < .01 &&
							fabs(v1[2] - v2[2]) < .01)
						continue;
					//
					//ri.Printf( PRINT_ALL, "found highest LoD crack between two patches\n" );
					// insert row into grid2 right after row l
					if (m) column = grid2->width-1;
					else column = 0;
					grid2 = R_GridInsertRow( grid2, l+1, column,
										grid1->verts[k + 1 + offset1].xyz, grid1->widthLodError[k+1]);
					grid2->lodStitched = qfalse;
					s_worldData.surfaces[grid2num].data = (void *) grid2;
					return qtrue;
				}
			}
		}
	}
	for (n = 0; n < 2; n++) {
		//
		if (n) offset1 = grid1->width-1;
		else offset1 = 0;
		if (R_MergedHeightPoints(grid1, offset1))
			continue;
		for (k = 0; k < grid1->height-2; k += 2) {
			for (m = 0; m < 2; m++) {

				if ( grid2->width >= MAX_GRID_SIZE )
					break;
				if (m) offset2 = (grid2->height-1) * grid2->width;
				else offset2 = 0;
				for ( l = 0; l < grid2->width-1; l++) {
				//
					v1 = grid1->verts[grid1->width * k + offset1].xyz;
					v2 = grid2->verts[l + offset2].xyz;
					if ( fabs(v1[0] - v2[0]) > .1)
						continue;
					if ( fabs(v1[1] - v2[1]) > .1)
						continue;
					if ( fabs(v1[2] - v2[2]) > .1)
						continue;

					v1 = grid1->verts[grid1->width * (k + 2) + offset1].xyz;
					v2 = grid2->verts[l + 1 + offset2].xyz;
					if ( fabs(v1[0] - v2[0]) > .1)
						continue;
					if ( fabs(v1[1] - v2[1]) > .1)
						continue;
					if ( fabs(v1[2] - v2[2]) > .1)
						continue;
					//
					v1 = grid2->verts[l + offset2].xyz;
					v2 = grid2->verts[(l + 1) + offset2].xyz;
					if ( fabs(v1[0] - v2[0]) < .01 &&
							fabs(v1[1] - v2[1]) < .01 &&
							fabs(v1[2] - v2[2]) < .01)
						continue;
					//
					//ri.Printf( PRINT_ALL, "found highest LoD crack between two patches\n" );
					// insert column into grid2 right after column l
					if (m) row = grid2->height-1;
					else row = 0;
					grid2 = R_GridInsertColumn( grid2, l+1, row,
									grid1->verts[grid1->width * (k + 1) + offset1].xyz, grid1->heightLodError[k+1]);
					grid2->lodStitched = qfalse;
					s_worldData.surfaces[grid2num].data = (void *) grid2;
					return qtrue;
				}
			}
			for (m = 0; m < 2; m++) {

				if (grid2->height >= MAX_GRID_SIZE)
					break;
				if (m) offset2 = grid2->width-1;
				else offset2 = 0;
				for ( l = 0; l < grid2->height-1; l++) {
				//
					v1 = grid1->verts[grid1->width * k + offset1].xyz;
					v2 = grid2->verts[grid2->width * l + offset2].xyz;
					if ( fabs(v1[0] - v2[0]) > .1)
						continue;
					if ( fabs(v1[1] - v2[1]) > .1)
						continue;
					if ( fabs(v1[2] - v2[2]) > .1)
						continue;

					v1 = grid1->verts[grid1->width * (k + 2) + offset1].xyz;
					v2 = grid2->verts[grid2->width * (l + 1) + offset2].xyz;
					if ( fabs(v1[0] - v2[0]) > .1)
						continue;
					if ( fabs(v1[1] - v2[1]) > .1)
						continue;
					if ( fabs(v1[2] - v2[2]) > .1)
						continue;
					//
					v1 = grid2->verts[grid2->width * l + offset2].xyz;
					v2 = grid2->verts[grid2->width * (l + 1) + offset2].xyz;
					if ( fabs(v1[0] - v2[0]) < .01 &&
							fabs(v1[1] - v2[1]) < .01 &&
							fabs(v1[2] - v2[2]) < .01)
						continue;
					//
					//ri.Printf( PRINT_ALL, "found highest LoD crack between two patches\n" );
					// insert row into grid2 right after row l
					if (m) column = grid2->width-1;
					else column = 0;
					grid2 = R_GridInsertRow( grid2, l+1, column,
									grid1->verts[grid1->width * (k + 1) + offset1].xyz, grid1->heightLodError[k+1]);
					grid2->lodStitched = qfalse;
					s_worldData.surfaces[grid2num].data = (void *) grid2;
					return qtrue;
				}
			}
		}
	}
	for (n = 0; n < 2; n++) {
		//
		if (n) offset1 = (grid1->height-1) * grid1->width;
		else offset1 = 0;
		if (R_MergedWidthPoints(grid1, offset1))
			continue;
		for (k = grid1->width-1; k > 1; k -= 2) {

			for (m = 0; m < 2; m++) {

				if ( !grid2 || grid2->width >= MAX_GRID_SIZE )
					break;
				if (m) offset2 = (grid2->height-1) * grid2->width;
				else offset2 = 0;
				for ( l = 0; l < grid2->width-1; l++) {
				//
					v1 = grid1->verts[k + offset1].xyz;
					v2 = grid2->verts[l + offset2].xyz;
					if ( fabs(v1[0] - v2[0]) > .1)
						continue;
					if ( fabs(v1[1] - v2[1]) > .1)
						continue;
					if ( fabs(v1[2] - v2[2]) > .1)
						continue;

					v1 = grid1->verts[k - 2 + offset1].xyz;
					v2 = grid2->verts[l + 1 + offset2].xyz;
					if ( fabs(v1[0] - v2[0]) > .1)
						continue;
					if ( fabs(v1[1] - v2[1]) > .1)
						continue;
					if ( fabs(v1[2] - v2[2]) > .1)
						continue;
					//
					v1 = grid2->verts[l + offset2].xyz;
					v2 = grid2->verts[(l + 1) + offset2].xyz;
					if ( fabs(v1[0] - v2[0]) < .01 &&
							fabs(v1[1] - v2[1]) < .01 &&
							fabs(v1[2] - v2[2]) < .01)
						continue;
					//
					//ri.Printf( PRINT_ALL, "found highest LoD crack between two patches\n" );
					// insert column into grid2 right after column l
					if (m) row = grid2->height-1;
					else row = 0;
					grid2 = R_GridInsertColumn( grid2, l+1, row,
										grid1->verts[k - 1 + offset1].xyz, grid1->widthLodError[k+1]);
					grid2->lodStitched = qfalse;
					s_worldData.surfaces[grid2num].data = (void *) grid2;
					return qtrue;
				}
			}
			for (m = 0; m < 2; m++) {

				if (!grid2 || grid2->height >= MAX_GRID_SIZE)
					break;
				if (m) offset2 = grid2->width-1;
				else offset2 = 0;
				for ( l = 0; l < grid2->height-1; l++) {
				//
					v1 = grid1->verts[k + offset1].xyz;
					v2 = grid2->verts[grid2->width * l + offset2].xyz;
					if ( fabs(v1[0] - v2[0]) > .1)
						continue;
					if ( fabs(v1[1] - v2[1]) > .1)
						continue;
					if ( fabs(v1[2] - v2[2]) > .1)
						continue;

					v1 = grid1->verts[k - 2 + offset1].xyz;
					v2 = grid2->verts[grid2->width * (l + 1) + offset2].xyz;
					if ( fabs(v1[0] - v2[0]) > .1)
						continue;
					if ( fabs(v1[1] - v2[1]) > .1)
						continue;
					if ( fabs(v1[2] - v2[2]) > .1)
						continue;
					//
					v1 = grid2->verts[grid2->width * l + offset2].xyz;
					v2 = grid2->verts[grid2->width * (l + 1) + offset2].xyz;
					if ( fabs(v1[0] - v2[0]) < .01 &&
							fabs(v1[1] - v2[1]) < .01 &&
							fabs(v1[2] - v2[2]) < .01)
						continue;
					//
					//ri.Printf( PRINT_ALL, "found highest LoD crack between two patches\n" );
					// insert row into grid2 right after row l
					if (m) column = grid2->width-1;
					else column = 0;
					grid2 = R_GridInsertRow( grid2, l+1, column,
										grid1->verts[k - 1 + offset1].xyz, grid1->widthLodError[k+1]);
					if (!grid2)
						break;
					grid2->lodStitched = qfalse;
					s_worldData.surfaces[grid2num].data = (void *) grid2;
					return qtrue;
				}
			}
		}
	}
	for (n = 0; n < 2; n++) {
		//
		if (n) offset1 = grid1->width-1;
		else offset1 = 0;
		if (R_MergedHeightPoints(grid1, offset1))
			continue;
		for (k = grid1->height-1; k > 1; k -= 2) {
			for (m = 0; m < 2; m++) {

				if ( !grid2 || grid2->width >= MAX_GRID_SIZE )
					break;
				if (m) offset2 = (grid2->height-1) * grid2->width;
				else offset2 = 0;
				for ( l = 0; l < grid2->width-1; l++) {
				//
					v1 = grid1->verts[grid1->width * k + offset1].xyz;
					v2 = grid2->verts[l + offset2].xyz;
					if ( fabs(v1[0] - v2[0]) > .1)
						continue;
					if ( fabs(v1[1] - v2[1]) > .1)
						continue;
					if ( fabs(v1[2] - v2[2]) > .1)
						continue;

					v1 = grid1->verts[grid1->width * (k - 2) + offset1].xyz;
					v2 = grid2->verts[l + 1 + offset2].xyz;
					if ( fabs(v1[0] - v2[0]) > .1)
						continue;
					if ( fabs(v1[1] - v2[1]) > .1)
						continue;
					if ( fabs(v1[2] - v2[2]) > .1)
						continue;
					//
					v1 = grid2->verts[l + offset2].xyz;
					v2 = grid2->verts[(l + 1) + offset2].xyz;
					if ( fabs(v1[0] - v2[0]) < .01 &&
							fabs(v1[1] - v2[1]) < .01 &&
							fabs(v1[2] - v2[2]) < .01)
						continue;
					//
					//ri.Printf( PRINT_ALL, "found highest LoD crack between two patches\n" );
					// insert column into grid2 right after column l
					if (m) row = grid2->height-1;
					else row = 0;
					grid2 = R_GridInsertColumn( grid2, l+1, row,
										grid1->verts[grid1->width * (k - 1) + offset1].xyz, grid1->heightLodError[k+1]);
					grid2->lodStitched = qfalse;
					s_worldData.surfaces[grid2num].data = (void *) grid2;
					return qtrue;
				}
			}
			for (m = 0; m < 2; m++) {

				if (!grid2 || grid2->height >= MAX_GRID_SIZE)
					break;
				if (m) offset2 = grid2->width-1;
				else offset2 = 0;
				for ( l = 0; l < grid2->height-1; l++) {
				//
					v1 = grid1->verts[grid1->width * k + offset1].xyz;
					v2 = grid2->verts[grid2->width * l + offset2].xyz;
					if ( fabs(v1[0] - v2[0]) > .1)
						continue;
					if ( fabs(v1[1] - v2[1]) > .1)
						continue;
					if ( fabs(v1[2] - v2[2]) > .1)
						continue;

					v1 = grid1->verts[grid1->width * (k - 2) + offset1].xyz;
					v2 = grid2->verts[grid2->width * (l + 1) + offset2].xyz;
					if ( fabs(v1[0] - v2[0]) > .1)
						continue;
					if ( fabs(v1[1] - v2[1]) > .1)
						continue;
					if ( fabs(v1[2] - v2[2]) > .1)
						continue;
					//
					v1 = grid2->verts[grid2->width * l + offset2].xyz;
					v2 = grid2->verts[grid2->width * (l + 1) + offset2].xyz;
					if ( fabs(v1[0] - v2[0]) < .01 &&
							fabs(v1[1] - v2[1]) < .01 &&
							fabs(v1[2] - v2[2]) < .01)
						continue;
					//
					//ri.Printf( PRINT_ALL, "found highest LoD crack between two patches\n" );
					// insert row into grid2 right after row l
					if (m) column = grid2->width-1;
					else column = 0;
					grid2 = R_GridInsertRow( grid2, l+1, column,
										grid1->verts[grid1->width * (k - 1) + offset1].xyz, grid1->heightLodError[k+1]);
					grid2->lodStitched = qfalse;
					s_worldData.surfaces[grid2num].data = (void *) grid2;
					return qtrue;
				}
			}
		}
	}
	return qfalse;
}


/*
===============
R_TryStitchPatch

This function will try to stitch patches in the same LoD group together for the highest LoD.

Only single missing vertex cracks will be fixed.

Vertices will be joined at the patch side a crack is first found, at the other side
of the patch (on the same row or column) the vertices will not be joined and cracks
might still appear at that side.
===============
*/
static int R_TryStitchingPatch( int grid1num ) {
	int j, numstitches;
	srfGridMesh_t *grid1, *grid2;

	numstitches = 0;
	grid1 = (srfGridMesh_t *) s_worldData.surfaces[grid1num].data;
	for ( j = 0; j < s_worldData.numsurfaces; j++ ) {
		//
		grid2 = (srfGridMesh_t *) s_worldData.surfaces[j].data;
		// if this surface is not a grid
		if ( grid2->surfaceType != SF_GRID ) continue;
		// grids in the same LOD group should have the exact same lod radius
		if ( grid1->lodRadius != grid2->lodRadius ) continue;
		// grids in the same LOD group should have the exact same lod origin
		if ( grid1->lodOrigin[0] != grid2->lodOrigin[0] ) continue;
		if ( grid1->lodOrigin[1] != grid2->lodOrigin[1] ) continue;
		if ( grid1->lodOrigin[2] != grid2->lodOrigin[2] ) continue;
		//
		while (R_StitchPatches(grid1num, j))
		{
			numstitches++;
		}
	}
	return numstitches;
}


/*
===============
R_StitchAllPatches
===============
*/
static void R_StitchAllPatches( void ) {
	int i, stitched, numstitches;
	srfGridMesh_t *grid1;

	numstitches = 0;
	do
	{
		stitched = qfalse;
		for ( i = 0; i < s_worldData.numsurfaces; i++ ) {
			//
			grid1 = (srfGridMesh_t *) s_worldData.surfaces[i].data;
			// if this surface is not a grid
			if ( grid1->surfaceType != SF_GRID )
				continue;
			//
			if ( grid1->lodStitched )
				continue;
			//
			grid1->lodStitched = qtrue;
			stitched = qtrue;
			//
			numstitches += R_TryStitchingPatch( i );
		}
	}
	while (stitched);
	ri.Printf( PRINT_ALL, "stitched %d LoD cracks\n", numstitches );
}


/*
===============
R_MovePatchSurfacesToHunk
===============
*/
static void R_MovePatchSurfacesToHunk( void ) {
	int i, j, k, n, size;
	srfGridMesh_t *grid, *hunkgrid;

	for ( i = 0; i < s_worldData.numsurfaces; i++ ) {
		//
		grid = (srfGridMesh_t *) s_worldData.surfaces[i].data;
		// if this surface is not a grid
		if ( grid->surfaceType != SF_GRID )
			continue;
		//
		n = grid->width * grid->height - 1;
		size = n * sizeof( drawVert_t ) + sizeof( *grid );

		for (j = 0; j < n; j++) {
			for (k = 0; k < 3; k++) {
				grid->verts[j].normal[k] = R_ClampDenorm( grid->verts[j].normal[k] );
			}
		}

		hunkgrid = ri.Hunk_Alloc( size, h_low );
		Com_Memcpy(hunkgrid, grid, size);

		hunkgrid->widthLodError = ri.Hunk_Alloc( grid->width * 4, h_low );
		Com_Memcpy( hunkgrid->widthLodError, grid->widthLodError, grid->width * 4 );

		hunkgrid->heightLodError = ri.Hunk_Alloc( grid->height * 4, h_low );
		Com_Memcpy( hunkgrid->heightLodError, grid->heightLodError, grid->height * 4 );

		R_FreeSurfaceGridMesh( grid );

		s_worldData.surfaces[i].data = (void *) hunkgrid;
	}
}


/*
===============
R_LoadSurfaces
===============
*/
static void R_LoadSurfaces( const lump_t *surfs, const lump_t *verts, const lump_t *indexLump ) {
	const dsurface_t *in;
	msurface_t	*out;
	const drawVert_t *dv;
	int			*indexes;
	int			count, totalVerts, totalIndexes;
	int			numFaces, numMeshes, numTriSurfs, numFlares;
	unsigned	firstVert, numVerts, firstIndex, numIndexes;
	int			i;

	numFaces = 0;
	numMeshes = 0;
	numTriSurfs = 0;
	numFlares = 0;

	in = (void *)(fileBase + surfs->fileofs);
	if (surfs->filelen % sizeof(*in))
		ri.Error( ERR_DROP, "%s(): funny lump size in %s", __func__, s_worldData.name );
	count = surfs->filelen / sizeof(*in);

	dv = (void *)(fileBase + verts->fileofs);
	if (verts->filelen % sizeof(*dv))
		ri.Error( ERR_DROP, "%s(): funny lump size in %s", __func__, s_worldData.name );
	totalVerts = verts->filelen / sizeof(*dv);

	indexes = (void *)(fileBase + indexLump->fileofs);
	if ( indexLump->filelen % sizeof(*indexes))
		ri.Error( ERR_DROP, "%s(): funny lump size in %s", __func__, s_worldData.name );
	totalIndexes = indexLump->filelen / sizeof(*indexes);

	out = ri.Hunk_Alloc( count * sizeof(*out), h_low );

	s_worldData.surfaces = out;
	s_worldData.numsurfaces = count;

	// shut up compiler
	firstVert = numVerts = 0;
	firstIndex = numIndexes = 0;

	for ( i = 0 ; i < count ; i++, in++, out++ ) {
		unsigned type = LittleLong( in->surfaceType );
		unsigned fogIndex;

		if ( type != MST_FLARE ) {
			firstVert = LittleLong( in->firstVert );
			if ( type == MST_PATCH )
				numVerts = 0;	// use patch size
			else
				numVerts = LittleLong( in->numVerts );
			if ( (uint64_t)firstVert + numVerts > totalVerts )
				ri.Error( ERR_DROP, "%s: bad verts", __func__ );

			if ( type != MST_PATCH ) {
				firstIndex = LittleLong( in->firstIndex );
				numIndexes = LittleLong( in->numIndexes );
				if ( (uint64_t)firstIndex + numIndexes > totalIndexes )
					ri.Error( ERR_DROP, "%s: bad indexes", __func__ );

				// don't allow partial triangles
				if ( numIndexes % 3 ) {
					ri.Printf( PRINT_WARNING, "%s: odd number of indexes: %u\n", __func__, numIndexes );
					numIndexes -= numIndexes % 3;
				}
			}
		}

		// get fog volume
		fogIndex = LittleLong( in->fogNum ) + 1U;
		if ( fogIndex >= s_worldData.numfogs ) {
			if ( type != MST_FLARE )
				ri.Printf( PRINT_WARNING, "%s: bad fog index: %u\n", __func__, fogIndex );
			fogIndex = 0;
		}
		out->fogIndex = fogIndex;

		switch ( type ) {
		case MST_PATCH:
			ParseMesh( in, dv + firstVert, totalVerts - firstVert, out );
			numMeshes++;
			break;
		case MST_TRIANGLE_SOUP:
			ParseTriSurf( in, dv + firstVert, numVerts, out, indexes + firstIndex, numIndexes );
			numTriSurfs++;
			break;
		case MST_PLANAR:
			ParseFace( in, dv + firstVert, numVerts, out, indexes + firstIndex, numIndexes );
			numFaces++;
			break;
		case MST_FLARE:
			ParseFlare( in, out );
			numFlares++;
			break;
		default:
			ri.Error( ERR_DROP, "%s: bad surfaceType: %u", __func__, type );
		}
	}

#ifdef PATCH_STITCHING
	R_StitchAllPatches();
#endif

	R_FixSharedVertexLodError();

#ifdef PATCH_STITCHING
	R_MovePatchSurfacesToHunk();
#endif

	ri.Printf( PRINT_ALL, "...loaded %d faces, %i meshes, %i trisurfs, %i flares\n", 
		numFaces, numMeshes, numTriSurfs, numFlares );
}


/*
=================
R_LoadSubmodels
=================
*/
static void R_LoadSubmodels( const lump_t *l ) {
	const dmodel_t *in;
	bmodel_t	*out;
	int			i, j, count;
	unsigned	firstSurface, numSurfaces;

	in = (void *)(fileBase + l->fileofs);
	if (l->filelen % sizeof(*in))
		ri.Error( ERR_DROP, "%s(): funny lump size in %s", __func__, s_worldData.name );
	count = l->filelen / sizeof(*in);

	s_worldData.bmodels = out = ri.Hunk_Alloc( count * sizeof(*out), h_low );

	for ( i=0 ; i<count ; i++, in++, out++ ) {
		model_t *model;

		model = R_AllocModel();

		if ( model == NULL ) {
			ri.Error( ERR_DROP, "R_LoadSubmodels: R_AllocModel() failed" );
		}

		model->type = MOD_BRUSH;
		model->bmodel = out;
		Com_sprintf( model->name, sizeof( model->name ), "*%d", i );

		for (j=0 ; j<3 ; j++) {
			out->bounds[0][j] = LittleFloat (in->mins[j]);
			out->bounds[1][j] = LittleFloat (in->maxs[j]);
		}

		firstSurface = LittleLong( in->firstSurface );
		numSurfaces = LittleLong( in->numSurfaces );
		if ( (uint64_t)firstSurface + numSurfaces > s_worldData.numsurfaces ) {
			ri.Error( ERR_DROP, "%s: bad surfaces", __func__ );
		}

		out->firstSurface = s_worldData.surfaces + firstSurface;
		out->numSurfaces = numSurfaces;
	}
}



//==================================================================

/*
=================
R_SetParent
=================
*/
static void R_SetParent( mnode_t *node, mnode_t *parent )
{
	if ( node->parent )
		ri.Error( ERR_DROP, "%s: cycle encountered", __func__ );
	node->parent = parent;
	if ( node->contents != CONTENTS_NODE )
		return;
	R_SetParent( node->children[0], node );
	R_SetParent( node->children[1], node );
}


/*
=================
R_LoadNodesAndLeafs
=================
*/
static void R_LoadNodesAndLeafs( const lump_t *nodeLump, const lump_t *leafLump ) {
	int			i, j;
	unsigned	p, firstmarksurface, nummarksurfaces;
	const dnode_t		*in;
	dleaf_t		*inLeaf;
	mnode_t 	*out;
	int			numNodes, numLeafs;

	in = (void *)(fileBase + nodeLump->fileofs);
	if (nodeLump->filelen % sizeof(dnode_t) ||
		leafLump->filelen % sizeof(dleaf_t) ) {
		ri.Error( ERR_DROP, "%s(): funny lump size in %s", __func__, s_worldData.name );
	}
	numNodes = nodeLump->filelen / sizeof(dnode_t);
	numLeafs = leafLump->filelen / sizeof(dleaf_t);

	out = ri.Hunk_Alloc ( (numNodes + numLeafs) * sizeof(*out), h_low);	

	s_worldData.nodes = out;
	s_worldData.numnodes = numNodes + numLeafs;
	s_worldData.numDecisionNodes = numNodes;

	// load nodes
	for ( i=0 ; i<numNodes; i++, in++, out++)
	{
		for (j=0 ; j<3 ; j++)
		{
			out->mins[j] = LittleLong (in->mins[j]);
			out->maxs[j] = LittleLong (in->maxs[j]);
		}
	
		p = LittleLong(in->planeNum);
		if ( p >= s_worldData.numplanes ) {
			ri.Error( ERR_DROP, "%s: bad planeNum", __func__ );
		}
		out->plane = s_worldData.planes + p;

		out->contents = CONTENTS_NODE;	// differentiate from leafs

		for (j=0 ; j<2 ; j++)
		{
			p = LittleLong (in->children[j]);
			if (p & 0x80000000) {
				p = ~p;
				if ( p >= numLeafs ) {
					ri.Error( ERR_DROP, "%s: bad leaf", __func__ );
				}
				out->children[j] = s_worldData.nodes + numNodes + p;
			} else {
				if ( p >= numNodes ) {
					ri.Error( ERR_DROP, "%s: bad node", __func__ );
				}
				out->children[j] = s_worldData.nodes + p;
			}
		}
	}
	
	// load leafs
	inLeaf = (void *)(fileBase + leafLump->fileofs);
	for ( i=0 ; i<numLeafs ; i++, inLeaf++, out++)
	{
		for (j=0 ; j<3 ; j++)
		{
			out->mins[j] = LittleLong (inLeaf->mins[j]);
			out->maxs[j] = LittleLong (inLeaf->maxs[j]);
		}

		out->cluster = LittleLong(inLeaf->cluster);
		if ( out->cluster + 1U > INT_MAX - 63U )
			Com_Error( ERR_DROP, "%s: bad cluster", __func__ );

		out->area = LittleLong(inLeaf->area);
		if ( out->area + 1U > MAX_MAP_AREAS )
			Com_Error( ERR_DROP, "%s: bad area", __func__ );

		if ( out->cluster >= s_worldData.numClusters ) {
			s_worldData.numClusters = out->cluster + 1;
		}

		firstmarksurface = LittleLong(inLeaf->firstLeafSurface);
		nummarksurfaces = LittleLong(inLeaf->numLeafSurfaces);
		if ( (uint64_t)firstmarksurface + nummarksurfaces > s_worldData.nummarksurfaces ) {
			ri.Error( ERR_DROP, "%s: bad marksurfaces", __func__ );
		}

		out->firstmarksurface = s_worldData.marksurfaces + firstmarksurface;
		out->nummarksurfaces = nummarksurfaces;
	}	

	// chain descendants
	R_SetParent (s_worldData.nodes, NULL);
}

//=============================================================================


/*
=================
R_ReplaceShaders

replaces some buggy map shaders
=================
*/
static void R_ReplaceMapShaders( dshader_t *out, int count ) 
{
	if ( Q_stricmp( s_worldData.baseName, "mapel4b" ) == 0 && count == 86 ) {
		if ( crc32_buffer( (const byte*)out, count*sizeof(*out) ) == 0x1593623C ) {
			if ( strcmp( out[72].shader, "textures/mapel4/crate1_top3" ) == 0 ) {
				strcpy( out[72].shader, "textures/mapel4/crate1_top2" );
			}
		}
	}
}


/*
=================
R_LoadShaders
=================
*/
static void R_LoadShaders( const lump_t *l ) {
	int		i, count;
	dshader_t	*in, *out;
	
	in = (void *)(fileBase + l->fileofs);
	if (l->filelen % sizeof(*in))
		ri.Error( ERR_DROP, "%s(): funny lump size in %s", __func__, s_worldData.name );
	count = l->filelen / sizeof(*in);
	out = ri.Hunk_Alloc ( count*sizeof(*out), h_low );

	s_worldData.shaders = out;
	s_worldData.numShaders = count;

	Com_Memcpy( out, in, count*sizeof(*out) );

	R_ReplaceMapShaders( out, count );

	for ( i=0 ; i<count ; i++ ) {
		out[i].surfaceFlags = LittleLong( out[i].surfaceFlags );
		out[i].contentFlags = LittleLong( out[i].contentFlags );
	}
}


/*
=================
R_LoadMarksurfaces
=================
*/
static void R_LoadMarksurfaces( const lump_t *l )
{	
	int		i, count;
	unsigned	j;
	int		*in;
	msurface_t **out;
	
	in = (void *)(fileBase + l->fileofs);
	if (l->filelen % sizeof(*in))
		ri.Error( ERR_DROP, "%s(): funny lump size in %s", __func__, s_worldData.name );
	count = l->filelen / sizeof(*in);
	out = ri.Hunk_Alloc ( count*sizeof(*out), h_low);

	s_worldData.marksurfaces = out;
	s_worldData.nummarksurfaces = count;

	for ( i=0 ; i<count ; i++)
	{
		j = LittleLong(in[i]);
		if ( j >= s_worldData.numsurfaces ) {
			ri.Error( ERR_DROP, "%s: bad surface", __func__ );
		}
		out[i] = s_worldData.surfaces + j;
	}
}


/*
=================
R_LoadPlanes
=================
*/
static	void R_LoadPlanes( const lump_t *l ) {
	int			i, j;
	cplane_t	*out;
	const dplane_t 	*in;
	int			count;
	int			bits;

	in = (void *)(fileBase + l->fileofs);
	if (l->filelen % sizeof(*in))
		ri.Error( ERR_DROP, "%s(): funny lump size in %s", __func__, s_worldData.name );
	count = l->filelen / sizeof(*in);
	out = ri.Hunk_Alloc( count*2*sizeof(*out), h_low );

	s_worldData.planes = out;
	s_worldData.numplanes = count;

	for ( i=0 ; i<count ; i++, in++, out++) {
		bits = 0;
		for (j=0 ; j<3 ; j++) {
			out->normal[j] = LittleFloat (in->normal[j]);
			if (out->normal[j] < 0) {
				bits |= 1<<j;
			}
		}

		out->dist = LittleFloat (in->dist);
		out->type = PlaneTypeForNormal( out->normal );
		out->signbits = bits;
	}
}


/*
=================
R_PreLoadFogs
=================
*/
static void R_PreLoadFogs( const lump_t *l ) {
	if ( l->filelen % sizeof( dfog_t ) ) {
		tr.numFogs = 0;
	} else {
		tr.numFogs = l->filelen / sizeof( dfog_t );
	}
}


/*
=================
R_LoadFogs
=================
*/
static void R_LoadFogs( const lump_t *l, const lump_t *brushesLump, const lump_t *sidesLump ) {
	int			i, j, n;
	fog_t		*out;
	const dfog_t		*fogs;
	const dbrush_t 	*brushes, *brush;
	const dbrushside_t	*sides;
	int			count, brushesCount, sidesCount;
	unsigned	sideNum, planeNum, firstSide;
	shader_t	*shader;
	float		d;
	vec3_t		fogColor;

	fogs = (void *)(fileBase + l->fileofs);
	if (l->filelen % sizeof(*fogs)) {
		ri.Error( ERR_DROP, "%s(): funny lump size in %s", __func__, s_worldData.name );
	}
	count = l->filelen / sizeof(*fogs);

	// create fog structures for them
	s_worldData.numfogs = count + 1;
	s_worldData.fogs = ri.Hunk_Alloc( s_worldData.numfogs*sizeof(*out), h_low);
	out = s_worldData.fogs + 1;

	if ( !count ) {
		return;
	}

	brushes = (void *)(fileBase + brushesLump->fileofs);
	if (brushesLump->filelen % sizeof(*brushes)) {
		ri.Error( ERR_DROP, "%s(): funny lump size in %s", __func__, s_worldData.name );
	}
	brushesCount = brushesLump->filelen / sizeof(*brushes);

	sides = (void *)(fileBase + sidesLump->fileofs);
	if (sidesLump->filelen % sizeof(*sides)) {
		ri.Error( ERR_DROP, "%s(): funny lump size in %s", __func__, s_worldData.name );
	}
	sidesCount = sidesLump->filelen / sizeof(*sides);
	if (sidesCount < 6) {
		ri.Error( ERR_DROP, "%s(): sides lump too short in %s", __func__, s_worldData.name );
	}

	for ( i=0 ; i<count ; i++, fogs++) {
		out->originalBrushNumber = LittleLong( fogs->brushNum );

		if ( (unsigned)out->originalBrushNumber >= brushesCount ) {
			ri.Error( ERR_DROP, "fog brushNumber out of range" );
		}
		brush = brushes + out->originalBrushNumber;

		firstSide = LittleLong( brush->firstSide );

		if ( firstSide > sidesCount - 6 ) {
			ri.Error( ERR_DROP, "fog brush sideNumber out of range" );
		}

		// brushes are always sorted with the axial sides first
		for ( j = 0; j < 6; j++ ) {
			sideNum = firstSide + j;
			planeNum = LittleLong( sides[ sideNum ].planeNum );
			if ( planeNum >= s_worldData.numplanes ) {
				ri.Error( ERR_DROP, "fog brush planeNum out of range" );
			}
			d = s_worldData.planes[ planeNum ].dist;
			out->bounds[j & 1][j >> 1] = (j & 1) ? d : -d;
		}

		// get information from the shader for fog parameters
		shader = R_FindShader( fogs->shader, LIGHTMAP_NONE, qtrue );
	
		VectorCopy( shader->fogParms.color, fogColor );

		if ( r_mapGreyScale->value > 0 ) {
			float luminance;
			luminance = LUMA( fogColor[0], fogColor[1], fogColor[2] );
			fogColor[0] = LERP( fogColor[0], luminance, r_mapGreyScale->value );
			fogColor[1] = LERP( fogColor[1], luminance, r_mapGreyScale->value );
			fogColor[2] = LERP( fogColor[2], luminance, r_mapGreyScale->value );
		}

		out->parms = shader->fogParms;

		out->colorInt.rgba[0] = ( fogColor[0] * tr.identityLight ) * 255.0f;
		out->colorInt.rgba[1] = ( fogColor[1] * tr.identityLight ) * 255.0f;
		out->colorInt.rgba[2] = ( fogColor[2] * tr.identityLight ) * 255.0f;
		out->colorInt.rgba[3] = 255;

		for ( n = 0; n < 4; n++ )
			out->color[ n ] = (float) out->colorInt.rgba[ n ] / 255.0f;

		d = shader->fogParms.depthForOpaque < 1 ? 1 : shader->fogParms.depthForOpaque;
		out->tcScale = 1.0f / ( d * 8 );

		// set the gradient vector
		sideNum = LittleLong( fogs->visibleSide );

		if ( sideNum == -1 ) {
			out->hasSurface = qfalse;
		} else {
			if ( sideNum >= sidesCount - firstSide ) {
				ri.Printf( PRINT_WARNING, "bad fog side offset\n" );
				out->hasSurface = qfalse;
			} else {
				out->hasSurface = qtrue;
				planeNum = LittleLong( sides[ firstSide + sideNum ].planeNum );
				if ( planeNum >= s_worldData.numplanes ) {
					ri.Error( ERR_DROP, "fog brush planeNum out of range" );
				}
				VectorSubtract( vec3_origin, s_worldData.planes[ planeNum ].normal, out->surface );
				out->surface[3] = -s_worldData.planes[ planeNum ].dist;
			}
		}

		out++;
	}

}


/*
================
R_LoadLightGrid
================
*/
static void R_LoadLightGrid( const lump_t *l ) {
	int		i;
	vec3_t	maxs;
	unsigned bounds[3];
	unsigned numGridPoints;
	world_t	*w;
	float	*wMins, *wMaxs;

	w = &s_worldData;

	if ( !l->filelen ) {
		w->lightGridData = NULL;
		return;
	}

	w->lightGridInverseSize[0] = 1.0f / w->lightGridSize[0];
	w->lightGridInverseSize[1] = 1.0f / w->lightGridSize[1];
	w->lightGridInverseSize[2] = 1.0f / w->lightGridSize[2];

	wMins = w->bmodels[0].bounds[0];
	wMaxs = w->bmodels[0].bounds[1];

	for ( i = 0 ; i < 3 ; i++ ) {
		w->lightGridOrigin[i] = w->lightGridSize[i] * ceil( wMins[i] / w->lightGridSize[i] );
		maxs[i] = w->lightGridSize[i] * floor( wMaxs[i] / w->lightGridSize[i] );
		bounds[i] = (maxs[i] - w->lightGridOrigin[i])/w->lightGridSize[i] + 1;
	}

	if ( (uint64_t)bounds[0] * bounds[1] > INT_MAX ||
		 (uint64_t)bounds[0] * bounds[1] * bounds[2] > INT_MAX) {
		ri.Printf( PRINT_WARNING, "WARNING: bad light grid bounds\n" );
		w->lightGridData = NULL;
		return;
	}

	numGridPoints = bounds[0] * bounds[1] * bounds[2];

	if ( l->filelen % 8 || l->filelen / 8 != numGridPoints ) {
		ri.Printf( PRINT_WARNING, "WARNING: light grid mismatch\n" );
		w->lightGridData = NULL;
		return;
	}

	for ( i = 0 ; i < 3 ; i++ ) {
		w->lightGridBounds[i] = bounds[i];
	}

	w->lightGridData = ri.Hunk_Alloc( l->filelen, h_low );
	Com_Memcpy( w->lightGridData, (void *)(fileBase + l->fileofs), l->filelen );

	// deal with overbright bits
	for ( i = 0 ; i < numGridPoints ; i++ ) {
		R_ColorShiftLightingBytes( &w->lightGridData[i*8], &w->lightGridData[i*8], qfalse );
		R_ColorShiftLightingBytes( &w->lightGridData[i*8+3], &w->lightGridData[i*8+3], qfalse );
	}
}


/*
================
R_LoadEntities
================
*/
static void R_LoadEntities( const lump_t *l ) {
	const char *p, *token, *s;
	char keyname[MAX_TOKEN_CHARS];
	char value[MAX_TOKEN_CHARS], *v[3];
	world_t	*w;

	w = &s_worldData;
	w->lightGridSize[0] = 64;
	w->lightGridSize[1] = 64;
	w->lightGridSize[2] = 128;

	// store for reference by the cgame
	w->entityString = ri.Hunk_Alloc( l->filelen + 1, h_low );
	memcpy( w->entityString, fileBase + l->fileofs, l->filelen );
	w->entityString[l->filelen] = 0;
	w->entityParsePoint = w->entityString;

	p = w->entityString;
	token = COM_ParseExt( &p, qtrue );
	if (*token != '{') {
		return;
	}

	// only parse the world spawn
	while ( 1 ) {	
		// parse key
		token = COM_ParseExt( &p, qtrue );

		if ( !*token || *token == '}' ) {
			break;
		}
		Q_strncpyz(keyname, token, sizeof(keyname));

		// parse value
		token = COM_ParseExt( &p, qtrue );

		if ( !*token || *token == '}' ) {
			break;
		}
		Q_strncpyz(value, token, sizeof(value));

		// check for remapping of shaders for vertex lighting
		s = "vertexremapshader";
		if (!Q_strncmp(keyname, s, strlen(s)) ) {
			char *vs = strchr(value, ';');
			if (!vs) {
				ri.Printf( PRINT_WARNING, "WARNING: no semi colon in vertexshaderremap '%s'\n", value );
				break;
			}
			*vs++ = '\0';
			if ( r_vertexLight->integer && tr.vertexLightingAllowed ) {
				RE_RemapShader(value, s, "0");
			}
			continue;
		}
		// check for remapping of shaders
		s = "remapshader";
		if (!Q_strncmp(keyname, s, (int)strlen(s)) ) {
			char *vs = strchr(value, ';');
			if (!vs) {
				ri.Printf( PRINT_WARNING, "WARNING: no semi colon in shaderremap '%s'\n", value );
				break;
			}
			*vs++ = '\0';
			RE_RemapShader(value, s, "0");
			continue;
		}
		// check for a different grid size
		if (!Q_stricmp(keyname, "gridsize")) {
			//sscanf(value, "%f %f %f", &w->lightGridSize[0], &w->lightGridSize[1], &w->lightGridSize[2] );
			Com_Split( value, v, 3, ' ' );
			w->lightGridSize[0] = Q_atof( v[0] );
			w->lightGridSize[1] = Q_atof( v[1] );
			w->lightGridSize[2] = Q_atof( v[2] );
			continue;
		}
	}
}


/*
=================
RE_GetEntityToken
=================
*/
qboolean RE_GetEntityToken( char *buffer, int size ) {
	const char	*s;

	s = COM_Parse( &s_worldData.entityParsePoint );
	Q_strncpyz( buffer, s, size );
	if ( !s_worldData.entityParsePoint && !s[0] ) {
		s_worldData.entityParsePoint = s_worldData.entityString;
		return qfalse;
	} else {
		return qtrue;
	}
}


/*
=================
RE_LoadWorldMap

Called directly from cgame
=================
*/
void RE_LoadWorldMap( const char *name ) {
	int			i;
	int32_t		size;
	dheader_t	*header;
	union {
		byte *b;
		void *v;
	} buffer;
	byte		*startMarker;

	if ( tr.worldMapLoaded ) {
		ri.Error( ERR_DROP, "ERROR: attempted to redundantly load world map" );
	}

	// set default sun direction to be used if it isn't
	// overridden by a shader
	tr.sunDirection[0] = 0.45f;
	tr.sunDirection[1] = 0.3f;
	tr.sunDirection[2] = 0.9f;

	VectorNormalize( tr.sunDirection );

	tr.worldMapLoaded = qtrue;

	// load it
	size = ri.FS_ReadFile( name, &buffer.v );
	if ( !buffer.b ) {
		ri.Error( ERR_DROP, "%s: couldn't load %s", __func__, name );
	}
	if ( size < sizeof( dheader_t ) ) {
		ri.Error( ERR_DROP, "%s: %s has truncated header", __func__, name );
	}

	tr.mapLoading = qtrue;

	// clear tr.world so if the level fails to load, the next
	// try will not look at the partially loaded version
	tr.world = NULL;

	Com_Memset( &s_worldData, 0, sizeof( s_worldData ) );
	Q_strncpyz( s_worldData.name, name, sizeof( s_worldData.name ) );

	Q_strncpyz( s_worldData.baseName, COM_SkipPath( s_worldData.name ), sizeof( s_worldData.name ) );
	COM_StripExtension(s_worldData.baseName, s_worldData.baseName, sizeof(s_worldData.baseName));

	startMarker = ri.Hunk_Alloc(0, h_low);
	c_gridVerts = 0;

	header = (dheader_t *)buffer.b;
	fileBase = (byte *)header;

	// swap all the lumps
	for ( i = 0; i < sizeof( dheader_t ) / 4; i++ ) {
		( (int32_t *)header )[i] = LittleLong( ( (int32_t *)header )[i] );
	}

	if ( header->version != BSP_VERSION ) {
		ri.Error( ERR_DROP, "%s: %s has wrong version number (%i should be %i)", __func__, name, header->version, BSP_VERSION );
	}

	for ( i = 0; i < HEADER_LUMPS; i++ ) {
		uint32_t ofs = header->lumps[i].fileofs;
		uint32_t len = header->lumps[i].filelen;
		if ( (uint64_t)ofs + len > size ) {
			ri.Error( ERR_DROP, "%s: %s has wrong lump[%i] size/offset", __func__, name, i );
		}
	}

	// load into heap
	R_LoadLightmaps( &header->lumps[LUMP_LIGHTMAPS] );
	R_PreLoadFogs( &header->lumps[LUMP_FOGS] );
	R_LoadShaders( &header->lumps[LUMP_SHADERS] );
	R_LoadPlanes( &header->lumps[LUMP_PLANES] );
	R_LoadFogs( &header->lumps[LUMP_FOGS], &header->lumps[LUMP_BRUSHES], &header->lumps[LUMP_BRUSHSIDES] );
	R_LoadSurfaces( &header->lumps[LUMP_SURFACES], &header->lumps[LUMP_DRAWVERTS], &header->lumps[LUMP_DRAWINDEXES] );
	R_LoadMarksurfaces( &header->lumps[LUMP_LEAFSURFACES] );
	R_LoadNodesAndLeafs( &header->lumps[LUMP_NODES], &header->lumps[LUMP_LEAFS] );
	R_LoadSubmodels( &header->lumps[LUMP_MODELS] );
	R_LoadVisibility( &header->lumps[LUMP_VISIBILITY] );
	R_LoadEntities( &header->lumps[LUMP_ENTITIES] );
	R_LoadLightGrid( &header->lumps[LUMP_LIGHTGRID] );

#ifdef USE_VBO
	R_BuildWorldVBO( s_worldData.surfaces, s_worldData.numsurfaces );
#endif

	tr.mapLoading = qfalse;

	s_worldData.dataSize = (byte *)ri.Hunk_Alloc(0, h_low) - startMarker;

	// only set tr.world now that we know the entire level has loaded properly
	tr.world = &s_worldData;

	ri.FS_FreeFile( buffer.v );
}
