// blah

#if defined USE_OPENGL && defined POLYMER

#include "compat.h"

#define POLYMER_C
#include "polymer.h"
#include "engine_priv.h"
#include "xxhash.h"
#include "texcache.h"

// CVARS
int32_t         pr_lighting = 1;
int32_t         pr_normalmapping = 1;
int32_t         pr_specularmapping = 1;
int32_t         pr_shadows = 1;
int32_t         pr_shadowcount = 5;
int32_t         pr_shadowdetail = 4;
int32_t         pr_shadowfiltering = 1;
int32_t         pr_maxlightpasses = 10;
int32_t         pr_maxlightpriority = PR_MAXLIGHTPRIORITY;
int32_t         pr_fov = 512;
double          pr_customaspect = 0.0f;
int32_t         pr_billboardingmode = 1;
int32_t         pr_verbosity = 1;       // 0: silent, 1: errors and one-times, 2: multiple-times, 3: flood
int32_t         pr_wireframe = 0;
int32_t         pr_vbos = 2;
int32_t         pr_buckets = 0;
int32_t         pr_gpusmoothing = 1;
int32_t         pr_overrideparallax = 0;
float           pr_parallaxscale = 0.1f;
float           pr_parallaxbias = 0.0f;
int32_t         pr_overridespecular = 0;
float           pr_specularpower = 15.0f;
float           pr_specularfactor = 1.0f;
int32_t         pr_highpalookups = 1;
int32_t         pr_artmapping = 1;
int32_t         pr_overridehud = 0;
float           pr_hudxadd = 0.0f;
float           pr_hudyadd = 0.0f;
float           pr_hudzadd = 0.0f;
int32_t         pr_hudangadd = 0;
int32_t         pr_hudfov = 512;
float           pr_overridemodelscale = 0.0f;
int32_t         pr_ati_fboworkaround = 0;
int32_t         pr_ati_nodepthoffset = 0;
#ifdef __APPLE__
int32_t         pr_ati_textureformat_one = 0;
#endif
int32_t         pr_nullrender = 0; // 1: no draw, 2: no draw or updates

int32_t         r_pr_maxlightpasses = 5; // value of the cvar (not live value), used to detect changes

GLenum          mapvbousage = GL_STREAM_DRAW;
GLenum          modelvbousage = GL_STATIC_DRAW;

// BUILD DATA
_prsector       *prsectors[MAXSECTORS];
_prwall         *prwalls[MAXWALLS];
_prsprite       *prsprites[MAXSPRITES];
_prmaterial     mdspritematerial;
_prhighpalookup prhighpalookups[MAXBASEPALS][MAXPALOOKUPS];

// One U8 texture per tile
GLuint          prartmaps[MAXTILES];
// 256 U8U8U8 values per basepal
GLuint          prbasepalmaps[MAXBASEPALS];
// numshades full indirections (32*256) per lookup
GLuint          prlookups[MAXPALOOKUPS];

GLuint          prmapvbo;
const GLsizeiptr proneplanesize = sizeof(_prvert) * 4;
const GLintptr prwalldatasize = sizeof(_prvert)* 4 * 3; // wall, over and mask planes for every wall
GLintptr prwalldataoffset;

GLuint          prindexringvbo;
GLuint          *prindexring;
const GLsizeiptr prindexringsize = 65535;
GLintptr prindexringoffset;

const GLbitfield prindexringmapflags = GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT;

_prbucket       *prbuckethead;
int32_t         prcanbucket;

static const _prvert  vertsprite[4] =
{
    {
        -0.5f, 0.0f, 0.0f,
        0.0f, 1.0f,
        0xff, 0xff, 0xff, 0xff,
    },
    {
        0.5f, 0.0f, 0.0f,
        1.0f, 1.0f,
        0xff, 0xff, 0xff, 0xff,
    },
    {
        0.5f, 1.0f, 0.0f,
        1.0f, 0.0f,
        0xff, 0xff, 0xff, 0xff,
    },
    {
        -0.5f, 1.0f, 0.0f,
        0.0f, 0.0f,
        0xff, 0xff, 0xff, 0xff,
    },
};

static const _prvert  horizsprite[4] =
{
    {
        -0.5f, 0.0f, 0.5f,
        0.0f, 0.0f,
        0xff, 0xff, 0xff, 0xff,
    },
    {
        0.5f, 0.0f, 0.5f,
        1.0f, 0.0f,
        0xff, 0xff, 0xff, 0xff,
    },
    {
        0.5f, 0.0f, -0.5f,
        1.0f, 1.0f,
        0xff, 0xff, 0xff, 0xff,
    },
    {
        -0.5f, 0.0f, -0.5f,
        0.0f, 1.0f,
        0xff, 0xff, 0xff, 0xff,
    },
};

static const GLfloat  skyboxdata[4 * 5 * 6] =
{
    // -ZY
    -0.5f, -0.5f, 0.5f,
    0.0f, 1.0f,
    -0.5f, -0.5f, -0.5f,
    1.0f, 1.0f,
    -0.5f, 0.5f, -0.5f,
    1.0f, 0.0f,
    -0.5f, 0.5f, 0.5f,
    0.0f, 0.0f,

    // XY
    -0.5f, -0.5f, -0.5f,
    0.0f, 1.0f,
    0.5f, -0.5f, -0.5f,
    1.0f, 1.0f,
    0.5f, 0.5f, -0.5f,
    1.0f, 0.0f,
    -0.5f, 0.5f, -0.5f,
    0.0f, 0.0f,

    // ZY
    0.5f, -0.5f, -0.5f,
    0.0f, 1.0f,
    0.5f, -0.5f, 0.5f,
    1.0f, 1.0f,
    0.5f, 0.5f, 0.5f,
    1.0f, 0.0f,
    0.5f, 0.5f, -0.5f,
    0.0f, 0.0f,

    // -XY
    0.5f, -0.5f, 0.5f,
    0.0f, 1.0f,
    -0.5f, -0.5f, 0.5f,
    1.0f, 1.0f,
    -0.5f, 0.5f, 0.5f,
    1.0f, 0.0f,
    0.5f, 0.5f, 0.5f,
    0.0f, 0.0f,

    // XZ
    -0.5f, 0.5f, -0.5f,
    1.0f, 1.0f,
    0.5f, 0.5f, -0.5f,
    1.0f, 0.0f,
    0.5f, 0.5f, 0.5f,
    0.0f, 0.0f,
    -0.5f, 0.5f, 0.5f,
    0.0f, 1.0f,

    // X-Z
    -0.5f, -0.5f, 0.5f,
    0.0f, 0.0f,
    0.5f, -0.5f, 0.5f,
    0.0f, 1.0f,
    0.5f, -0.5f, -0.5f,
    1.0f, 1.0f,
    -0.5f, -0.5f, -0.5f,
    1.0f, 0.0f,
};

GLuint          skyboxdatavbo;

GLfloat         artskydata[PSKYOFF_MAX*2];

// LIGHTS
static _prplanelist *plpool;
#pragma pack(push,1)
_prlight        prlights[PR_MAXLIGHTS];
int32_t         lightcount;
int32_t         curlight;
#pragma pack(pop)

static const GLfloat  shadowBias[] =
{
    0.5, 0.0, 0.0, 0.0,
    0.0, 0.5, 0.0, 0.0,
    0.0, 0.0, 0.5, 0.0,
    0.5, 0.5, 0.5, 1.0
};

// MATERIALS
static const _prprogrambit   prprogrambits[PR_BIT_COUNT] = {
    {
        1 << PR_BIT_HEADER,
        // vert_def
        "#version 120\n"
        "#extension GL_ARB_texture_rectangle : enable\n"
        "\n",
        // vert_prog
        "",
        // frag_def
        "#version 120\n"
        "#extension GL_ARB_texture_rectangle : enable\n"
        "\n",
        // frag_prog
        "",
    },
    {
        1 << PR_BIT_ANIM_INTERPOLATION,
        // vert_def
        "attribute vec4 nextFrameData;\n"
        "attribute vec4 nextFrameNormal;\n"
        "uniform float frameProgress;\n"
        "\n",
        // vert_prog
        "  vec4 currentFramePosition;\n"
        "  vec4 nextFramePosition;\n"
        "\n"
        "  currentFramePosition = curVertex * (1.0 - frameProgress);\n"
        "  nextFramePosition = nextFrameData * frameProgress;\n"
        "  curVertex = currentFramePosition + nextFramePosition;\n"
        "\n"
        "  currentFramePosition = vec4(curNormal, 1.0) * (1.0 - frameProgress);\n"
        "  nextFramePosition = nextFrameNormal * frameProgress;\n"
        "  curNormal = vec3(currentFramePosition + nextFramePosition);\n"
        "\n",
        // frag_def
        "",
        // frag_prog
        "",
    },
    {
        1 << PR_BIT_LIGHTING_PASS,
        // vert_def
        "",
        // vert_prog
        "",
        // frag_def
        "",
        // frag_prog
        "  isLightingPass = 1;\n"
        "  result = vec4(0.0, 0.0, 0.0, 1.0);\n"
        "\n",
    },
    {
        1 << PR_BIT_NORMAL_MAP,
        // vert_def
        "attribute vec3 T;\n"
        "attribute vec3 B;\n"
        "attribute vec3 N;\n"
        "uniform vec3 eyePosition;\n"
        "varying vec3 tangentSpaceEyeVec;\n"
        "\n",
        // vert_prog
        "  TBN = mat3(T, B, N);\n"
        "  tangentSpaceEyeVec = eyePosition - vec3(curVertex);\n"
        "  tangentSpaceEyeVec = TBN * tangentSpaceEyeVec;\n"
        "\n"
        "  isNormalMapped = 1;\n"
        "\n",
        // frag_def
        "uniform sampler2D normalMap;\n"
        "uniform vec2 normalBias;\n"
        "varying vec3 tangentSpaceEyeVec;\n"
        "\n",
        // frag_prog
        "  vec4 normalStep;\n"
        "  float biasedHeight;\n"
        "\n"
        "  eyeVec = normalize(tangentSpaceEyeVec);\n"
        "\n"
        "  for (int i = 0; i < 4; i++) {\n"
        "    normalStep = texture2D(normalMap, commonTexCoord.st);\n"
        "    biasedHeight = normalStep.a * normalBias.x - normalBias.y;\n"
        "    commonTexCoord += (biasedHeight - commonTexCoord.z) * normalStep.z * eyeVec;\n"
        "  }\n"
        "\n"
        "  normalTexel = texture2D(normalMap, commonTexCoord.st);\n"
        "\n"
        "  isNormalMapped = 1;\n"
        "\n",
    },
    {
        1 << PR_BIT_ART_MAP,
        // vert_def
        "varying vec3 horizDistance;\n"
        "\n",
        // vert_prog
        "  gl_TexCoord[0] = gl_MultiTexCoord0;\n"
        "  horizDistance = vec3(gl_ModelViewMatrix * curVertex);\n"
        "\n",
        // frag_def
        "uniform sampler2D artMap;\n"
        "uniform sampler2D basePalMap;\n"
        "uniform sampler2DRect lookupMap;\n"
        "uniform float shadeOffset;\n"
        "uniform float visibility;\n"
        "varying vec3 horizDistance;\n"
        "\n",
        // frag_prog

        "  float shadeLookup = length(horizDistance) / 1.024 * visibility;\n"
        "  shadeLookup = shadeLookup + shadeOffset;\n"
        "\n"
        "  float colorIndex = texture2D(artMap, commonTexCoord.st).r * 256.0;\n"
        "  float colorIndexNear = texture2DRect(lookupMap, vec2(colorIndex, floor(shadeLookup))).r;\n"
        "  float colorIndexFar = texture2DRect(lookupMap, vec2(colorIndex, floor(shadeLookup + 1.0))).r;\n"
        "  float colorIndexFullbright = texture2DRect(lookupMap, vec2(colorIndex, 0.0)).r;\n"
        "\n"
        "  vec3 texelNear = texture2D(basePalMap, vec2(colorIndexNear, 0.5)).rgb;\n"
        "  vec3 texelFar = texture2D(basePalMap, vec2(colorIndexFar, 0.5)).rgb;\n"
        "  diffuseTexel.rgb = texture2D(basePalMap, vec2(colorIndexFullbright, 0.5)).rgb;\n"
        "\n"
        "  if (isLightingPass == 0) {\n"
        "    result.rgb = mix(texelNear, texelFar, fract(shadeLookup));\n"
        "    result.a = 1.0;\n"
        "    if (colorIndex == 256.0)\n"
        "      result.a = 0.0;\n"
        "  }\n"
        "\n",
    },
    {
        1 << PR_BIT_DIFFUSE_MAP,
        // vert_def
        "uniform vec2 diffuseScale;\n"
        "\n",
        // vert_prog
        "  gl_TexCoord[0] = vec4(diffuseScale, 1.0, 1.0) * gl_MultiTexCoord0;\n"
        "\n",
        // frag_def
        "uniform sampler2D diffuseMap;\n"
        "\n",
        // frag_prog
        "  diffuseTexel = texture2D(diffuseMap, commonTexCoord.st);\n"
        "\n",
    },
    {
        1 << PR_BIT_DIFFUSE_DETAIL_MAP,
        // vert_def
        "uniform vec2 detailScale;\n"
        "varying vec2 fragDetailScale;\n"
        "\n",
        // vert_prog
        "  fragDetailScale = detailScale;\n"
        "  if (isNormalMapped == 0)\n"
        "    gl_TexCoord[1] = vec4(detailScale, 1.0, 1.0) * gl_MultiTexCoord0;\n"
        "\n",
        // frag_def
        "uniform sampler2D detailMap;\n"
        "varying vec2 fragDetailScale;\n"
        "\n",
        // frag_prog
        "  if (isNormalMapped == 0)\n"
        "    diffuseTexel *= texture2D(detailMap, gl_TexCoord[1].st);\n"
        "  else\n"
        "    diffuseTexel *= texture2D(detailMap, commonTexCoord.st * fragDetailScale);\n"
        "  diffuseTexel.rgb *= 2.0;\n"
        "\n",
    },
    {
        1 << PR_BIT_DIFFUSE_MODULATION,
        // vert_def
        "",
        // vert_prog
        "  gl_FrontColor = gl_Color;\n"
        "\n",
        // frag_def
        "",
        // frag_prog
        "  if (isLightingPass == 0)\n"
        "    result *= vec4(gl_Color);\n"
        "\n",
    },
    {
        1 << PR_BIT_DIFFUSE_MAP2,
        // vert_def
        "",
        // vert_prog
        "",
        // frag_def
        "",
        // frag_prog
        "  if (isLightingPass == 0)\n"
        "    result *= diffuseTexel;\n"
        "\n",
    },
    {
        1 << PR_BIT_HIGHPALOOKUP_MAP,
        // vert_def
        "",
        // vert_prog
        "",
        // frag_def
        "uniform sampler3D highPalookupMap;\n"
        "\n",
        // frag_prog
        "  float highPalScale = 0.9921875; // for 6 bits\n"
        "  float highPalBias = 0.00390625;\n"
        "\n"
        "  if (isLightingPass == 0)\n"
        "    result.rgb = texture3D(highPalookupMap, result.rgb * highPalScale + highPalBias).rgb;\n"
        "  diffuseTexel.rgb = texture3D(highPalookupMap, diffuseTexel.rgb * highPalScale + highPalBias).rgb;\n"
        "\n",
    },
    {
        1 << PR_BIT_SPECULAR_MAP,
        // vert_def
        "",
        // vert_prog
        "",
        // frag_def
        "uniform sampler2D specMap;\n"
        "\n",
        // frag_prog
        "  specTexel = texture2D(specMap, commonTexCoord.st);\n"
        "\n"
        "  isSpecularMapped = 1;\n"
        "\n",
    },
    {
        1 << PR_BIT_SPECULAR_MATERIAL,
        // vert_def
        "",
        // vert_prog
        "",
        // frag_def
        "uniform vec2 specMaterial;\n"
        "\n",
        // frag_prog
        "  specularMaterial = specMaterial;\n"
        "\n",
    },
    {
        1 << PR_BIT_MIRROR_MAP,
        // vert_def
        "",
        // vert_prog
        "",
        // frag_def
        "uniform sampler2DRect mirrorMap;\n"
        "\n",
        // frag_prog
        "  vec4 mirrorTexel;\n"
        "  vec2 mirrorCoords;\n"
        "\n"
        "  mirrorCoords = gl_FragCoord.st;\n"
        "  if (isNormalMapped == 1) {\n"
        "    mirrorCoords += 100.0 * (normalTexel.rg - 0.5);\n"
        "  }\n"
        "  mirrorTexel = texture2DRect(mirrorMap, mirrorCoords);\n"
        "  result = vec4((result.rgb * (1.0 - specTexel.a)) + (mirrorTexel.rgb * specTexel.rgb * specTexel.a), result.a);\n"
        "\n",
    },
    {
        1 << PR_BIT_FOG,
        // vert_def
        "",
        // vert_prog
        "",
        // frag_def
#ifdef PR_LINEAR_FOG
        "uniform bool linearFog;\n"
#endif
        "",
        // frag_prog
        "  float fragDepth;\n"
        "  float fogFactor;\n"
        "\n"
        "  fragDepth = gl_FragCoord.z / gl_FragCoord.w;\n"
#ifdef PR_LINEAR_FOG
        "  if (!linearFog) {\n"
#endif
        "    fragDepth *= fragDepth;\n"
        "    fogFactor = exp2(-gl_Fog.density * gl_Fog.density * fragDepth * 1.442695);\n"
#ifdef PR_LINEAR_FOG
        "  } else {\n"
        "    fogFactor = gl_Fog.scale * (gl_Fog.end - fragDepth);\n"
        "    fogFactor = clamp(fogFactor, 0.0, 1.0);"
        "  }\n"
#endif
        "  result.rgb = mix(gl_Fog.color.rgb, result.rgb, fogFactor);\n"
        "\n",
    },
    {
        1 << PR_BIT_GLOW_MAP,
        // vert_def
        "",
        // vert_prog
        "",
        // frag_def
        "uniform sampler2D glowMap;\n"
        "\n",
        // frag_prog
        "  vec4 glowTexel;\n"
        "\n"
        "  glowTexel = texture2D(glowMap, commonTexCoord.st);\n"
        "  result = vec4((result.rgb * (1.0 - glowTexel.a)) + (glowTexel.rgb * glowTexel.a), result.a);\n"
        "\n",
    },
    {
        1 << PR_BIT_PROJECTION_MAP,
        // vert_def
        "uniform mat4 shadowProjMatrix;\n"
        "\n",
        // vert_prog
        "  gl_TexCoord[2] = shadowProjMatrix * curVertex;\n"
        "\n",
        // frag_def
        "",
        // frag_prog
        "",
    },
    {
        1 << PR_BIT_SHADOW_MAP,
        // vert_def
        "",
        // vert_prog
        "",
        // frag_def
        "uniform sampler2DShadow shadowMap;\n"
        "\n",
        // frag_prog
        "  shadowResult = shadow2DProj(shadowMap, gl_TexCoord[2]).a;\n"
        "\n",
    },
    {
        1 << PR_BIT_LIGHT_MAP,
        // vert_def
        "",
        // vert_prog
        "",
        // frag_def
        "uniform sampler2D lightMap;\n"
        "\n",
        // frag_prog
        "  lightTexel = texture2D(lightMap, vec2(gl_TexCoord[2].s, -gl_TexCoord[2].t) / gl_TexCoord[2].q).rgb;\n"
        "\n",
    },
    {
        1 << PR_BIT_SPOT_LIGHT,
        // vert_def
        "",
        // vert_prog
        "",
        // frag_def
        "uniform vec3 spotDir;\n"
        "uniform vec2 spotRadius;\n"
        "\n",
        // frag_prog
        "  spotVector = spotDir;\n"
        "  spotCosRadius = spotRadius;\n"
        "  isSpotLight = 1;\n"
        "\n",
    },
    {
        1 << PR_BIT_POINT_LIGHT,
        // vert_def
        "varying vec3 vertexNormal;\n"
        "varying vec3 eyeVector;\n"
        "varying vec3 lightVector;\n"
        "varying vec3 tangentSpaceLightVector;\n"
        "\n",
        // vert_prog
        "  vec3 vertexPos;\n"
        "\n"
        "  vertexPos = vec3(gl_ModelViewMatrix * curVertex);\n"
        "  eyeVector = -vertexPos;\n"
        "  lightVector = gl_LightSource[0].ambient.rgb - vertexPos;\n"
        "\n"
        "  if (isNormalMapped == 1) {\n"
        "    tangentSpaceLightVector = gl_LightSource[0].specular.rgb - vec3(curVertex);\n"
        "    tangentSpaceLightVector = TBN * tangentSpaceLightVector;\n"
        "  } else\n"
        "    vertexNormal = normalize(gl_NormalMatrix * curNormal);\n"
        "\n",
        // frag_def
        "varying vec3 vertexNormal;\n"
        "varying vec3 eyeVector;\n"
        "varying vec3 lightVector;\n"
        "varying vec3 tangentSpaceLightVector;\n"
        "\n",
        // frag_prog
        "  float pointLightDistance;\n"
        "  float lightAttenuation;\n"
        "  float spotAttenuation;\n"
        "  vec3 N, L, E, R, D;\n"
        "  vec3 lightDiffuse;\n"
        "  float lightSpecular;\n"
        "  float NdotL;\n"
        "  float spotCosAngle;\n"
        "\n"
        "  L = normalize(lightVector);\n"
        "\n"
        "  pointLightDistance = dot(lightVector,lightVector);\n"
        "  lightAttenuation = clamp(1.0 - pointLightDistance * gl_LightSource[0].linearAttenuation, 0.0, 1.0);\n"
        "  spotAttenuation = 1.0;\n"
        "\n"
        "  if (isSpotLight == 1) {\n"
        "    D = normalize(spotVector);\n"
        "    spotCosAngle = dot(-L, D);\n"
        "    spotAttenuation = clamp((spotCosAngle - spotCosRadius.x) * spotCosRadius.y, 0.0, 1.0);\n"
        "  }\n"
        "\n"
        "  if (isNormalMapped == 1) {\n"
        "    E = eyeVec;\n"
        "    N = normalize(2.0 * (normalTexel.rgb - 0.5));\n"
        "    L = normalize(tangentSpaceLightVector);\n"
        "  } else {\n"
        "    E = normalize(eyeVector);\n"
        "    N = normalize(vertexNormal);\n"
        "  }\n"
        "  NdotL = max(dot(N, L), 0.0);\n"
        "\n"
        "  R = reflect(-L, N);\n"
        "\n"
        "  lightDiffuse = gl_Color.a * shadowResult * lightTexel *\n"
        "                 gl_LightSource[0].diffuse.rgb * lightAttenuation * spotAttenuation;\n"
        "  result += vec4(lightDiffuse * diffuseTexel.a * diffuseTexel.rgb * NdotL, 0.0);\n"
        "\n"
        "  if (isSpecularMapped == 0)\n"
        "    specTexel.rgb = diffuseTexel.rgb * diffuseTexel.a;\n"
        "\n"
        "  lightSpecular = pow( max(dot(R, E), 0.0), specularMaterial.x * specTexel.a) * specularMaterial.y;\n"
        "  result += vec4(lightDiffuse * specTexel.rgb * lightSpecular, 0.0);\n"
        "\n",
    },
    {
        1 << PR_BIT_FOOTER,
        // vert_def
        "void main(void)\n"
        "{\n"
        "  vec4 curVertex = gl_Vertex;\n"
        "  vec3 curNormal = gl_Normal;\n"
        "  int isNormalMapped = 0;\n"
        "  mat3 TBN;\n"
        "\n"
        "  gl_TexCoord[0] = gl_MultiTexCoord0;\n"
        "\n",
        // vert_prog
        "  gl_Position = gl_ModelViewProjectionMatrix * curVertex;\n"
        "}\n",
        // frag_def
        "void main(void)\n"
        "{\n"
        "  vec3 commonTexCoord = vec3(gl_TexCoord[0].st, 0.0);\n"
        "  vec4 result = vec4(1.0, 1.0, 1.0, 1.0);\n"
        "  vec4 diffuseTexel = vec4(1.0, 1.0, 1.0, 1.0);\n"
        "  vec4 specTexel = vec4(1.0, 1.0, 1.0, 1.0);\n"
        "  vec4 normalTexel;\n"
        "  int isLightingPass = 0;\n"
        "  int isNormalMapped = 0;\n"
        "  int isSpecularMapped = 0;\n"
        "  vec3 eyeVec;\n"
        "  int isSpotLight = 0;\n"
        "  vec3 spotVector;\n"
        "  vec2 spotCosRadius;\n"
        "  float shadowResult = 1.0;\n"
        "  vec2 specularMaterial = vec2(15.0, 1.0);\n"
        "  vec3 lightTexel = vec3(1.0, 1.0, 1.0);\n"
        "\n",
        // frag_prog
        "  gl_FragColor = result;\n"
        "}\n",
    }
};

_prprograminfo  prprograms[1 << PR_BIT_COUNT];

int32_t         overridematerial;
int32_t         globaloldoverridematerial;

int32_t         rotatespritematerialbits;

// RENDER TARGETS
_prrt           *prrts;

// CONTROL
GLfloat         spritemodelview[16];
GLfloat         mdspritespace[4][4];
GLfloat         rootmodelviewmatrix[16];
GLfloat         *curmodelviewmatrix;
GLfloat         rootskymodelviewmatrix[16];
GLfloat         *curskymodelviewmatrix;

static int16_t  sectorqueue[MAXSECTORS];
static int16_t  querydelay[MAXSECTORS];
static GLuint   queryid[MAXWALLS];
static int16_t  drawingstate[MAXSECTORS];

int16_t         *cursectormasks;
int16_t         *cursectormaskcount;

float           horizang;
fix16_t         viewangle;

int32_t         depth;
_prmirror       mirrors[10];

#if defined __clang__ && defined __APPLE__
// XXX: OS X 10.9 deprecated GLUtesselator.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#endif
GLUtesselator*  prtess;
#if defined __clang__ && defined __APPLE__
#pragma clang diagnostic pop
#endif

static int16_t  cursky;
static char     curskypal;
static int8_t   curskyshade;
static float    curskyangmul = 1;

_pranimatespritesinfo asi;

int32_t         polymersearching;

int32_t         culledface;

// EXTERNAL FUNCTIONS
int32_t             polymer_init(void)
{
    int32_t         i, j, t = timerGetTicks();

    if (pr_verbosity >= 1) OSD_Printf("Initializing Polymer subsystem...\n");

    if (!glinfo.texnpot ||
        !glinfo.depthtex ||
        !glinfo.shadow ||
        !glinfo.fbos ||
        !glinfo.rect ||
        !glinfo.multitex ||
        !glinfo.vbos ||
        !glinfo.occlusionqueries ||
        !glinfo.glsl)
    {
        OSD_Printf("PR : Your video card driver/combo doesn't support the necessary features!\n");
        OSD_Printf("PR : Disabling Polymer...\n");
        return 0;
    }

    // clean up existing stuff since it will be initialized again if we're re-entering here
    polymer_uninit();

    Bmemset(&prsectors[0], 0, sizeof(prsectors[0]) * MAXSECTORS);
    Bmemset(&prwalls[0], 0, sizeof(prwalls[0]) * MAXWALLS);

    prtess = bgluNewTess();
    if (prtess == 0)
    {
        OSD_Printf("PR : Tessellation object initialization failed!\n");
        return 0;
    }

    polymer_loadboard();

    polymer_initartsky();
    skyboxdatavbo = 0;

    i = 0;
    while (i < nextmodelid)
    {
        if (models[i])
        {
            md3model_t* m;

            m = (md3model_t*)models[i];
            m->indices = NULL;
        }
        i++;
    }

    i = 0;
    while (i < (1 << PR_BIT_COUNT))
    {
        prprograms[i].handle = 0;
        i++;
    }

    overridematerial = 0xFFFFFFFF;

    polymersearching = FALSE;

    polymer_initrendertargets(pr_shadowcount + 1);

    // Prime highpalookup maps
    i = 0;
    while (i < MAXBASEPALS)
    {
        j = 0;
        while (j < MAXPALOOKUPS)
        {
            if (prhighpalookups[i][j].data)
            {
                glGenTextures(1, &prhighpalookups[i][j].map);
                polymost_bindTexture(GL_TEXTURE_3D, prhighpalookups[i][j].map);
                glTexImage3D(GL_TEXTURE_3D,                // target
                              0,                            // mip level
                              GL_RGBA,                      // internalFormat
                              PR_HIGHPALOOKUP_DIM,          // width
                              PR_HIGHPALOOKUP_DIM,          // height
                              PR_HIGHPALOOKUP_DIM,          // depth
                              0,                            // border
                              GL_BGRA,                      // upload format
                              GL_UNSIGNED_BYTE,             // upload component type
                              prhighpalookups[i][j].data);     // data pointer
                glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
                glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
                glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, glinfo.clamptoedge?GL_CLAMP_TO_EDGE:GL_CLAMP);
                glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, glinfo.clamptoedge?GL_CLAMP_TO_EDGE:GL_CLAMP);
                glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, glinfo.clamptoedge?GL_CLAMP_TO_EDGE:GL_CLAMP);
                polymost_bindTexture(GL_TEXTURE_3D, 0);
            }
            j++;
        }
        i++;
    }

#ifndef __APPLE__
    if (glinfo.debugoutput) {
        // Enable everything.
        glDebugMessageControlARB(GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, NULL, GL_TRUE);
        glDebugMessageCallbackARB((GLDEBUGPROCARB)polymer_debugoutputcallback, NULL);
        glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS_ARB);
    }
#endif

    if (pr_verbosity >= 1) OSD_Printf("PR : Initialization complete in %d ms.\n", timerGetTicks()-t);

    return 1;
}

void                polymer_uninit(void)
{
    int32_t         i, j;

    if (prtess)
    {
        bgluDeleteTess(prtess);
        prtess = NULL;
    }

    polymer_freeboard();

    polymer_initrendertargets(0);

    i = 0;
    while (i < MAXBASEPALS)
    {
        j = 0;
        while (j < MAXPALOOKUPS)
        {
//            if (prhighpalookups[i][j].data) {
//                DO_FREE_AND_NULL(prhighpalookups[i][j].data);
//            }
            if (prhighpalookups[i][j].map) {
                glDeleteTextures(1, &prhighpalookups[i][j].map);
                prhighpalookups[i][j].map = 0;
            }
            j++;
        }
        i++;
    }

    i = 0;
    while (plpool)
    {
        _prplanelist*   next = plpool->n;

        Xfree(plpool);
        plpool = next;
        i++;
    }

    if (pr_verbosity >= 3)
        OSD_Printf("PR: freed %d planelists\n", i);
}

void                polymer_setaspect(int32_t ang)
{
    float           aspect;
    float fang = (float)ang * atanf(fviewingrange*(1.f/65536.f)) * (4.f/fPI);

    // use horizontal fov instead of vertical
    fang = atanf(tanf(fang * (fPI / 2048.f)) * float(windowxy2.y - windowxy1.y + 1) / float(windowxy2.x - windowxy1.x + 1) *
                      float(xdim) / float(ydim) * (3.f / 4.f)) * (2048.f / fPI);

    if (pr_customaspect != 0.0f)
        aspect = pr_customaspect;
    else
        aspect = (float)(windowxy2.x-windowxy1.x+1) /
                 (float)(windowxy2.y-windowxy1.y+1);

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    bgluPerspective(fang * (360.f/2048.f), aspect, 0.01f, 100.0f);
}

void                polymer_glinit(void)
{
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    glClearStencil(0);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
    glViewport(windowxy1.x, ydim-(windowxy2.y+1),windowxy2.x-windowxy1.x+1, windowxy2.y-windowxy1.y+1);

    glEnable(GL_DEPTH_TEST);
    glDepthFunc(GL_LEQUAL);

    glDisable(GL_BLEND);
    glDisable(GL_ALPHA_TEST);

    if (pr_wireframe)
        glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
    else
        glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);

    polymer_setaspect(pr_fov);

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);

    glDisable(GL_FOG);

    culledface = GL_BACK;
    glCullFace(GL_BACK);
    glFrontFace(GL_CCW);

    glEnable(GL_CULL_FACE);
}

void                polymer_resetlights(void)
{
    int32_t         i;
    _prsector       *s;
    _prwall         *w;

    i = 0;
    while (i < numsectors)
    {
        s = prsectors[i];

        if (!s) {
            i++;
            continue;
        }

        polymer_resetplanelights(&s->floor);
        polymer_resetplanelights(&s->ceil);

        i++;
    }

    i = 0;
    while (i < numwalls)
    {
        w = prwalls[i];

        if (!w) {
            i++;
            continue;
        }

        polymer_resetplanelights(&w->wall);
        polymer_resetplanelights(&w->over);
        polymer_resetplanelights(&w->mask);

        i++;
    }

    i = 0;
    while (i < PR_MAXLIGHTS)
    {
        prlights[i].flags.active = 0;
        i++;
    }

    lightcount = 0;

    if (!engineLoadMHK(NULL))
        OSD_Printf("polymer_resetlights: reloaded maphack\n");
}

void                polymer_loadboard(void)
{
    int32_t         i;

    polymer_freeboard();

    // in the big map buffer, sectors have floor and ceiling vertices for each wall first, then walls
    prwalldataoffset = numwalls * 2 * sizeof(_prvert);

    glGenBuffers(1, &prmapvbo);
    glBindBuffer(GL_ARRAY_BUFFER, prmapvbo);
    glBufferData(GL_ARRAY_BUFFER, prwalldataoffset + (numwalls * prwalldatasize), NULL, mapvbousage);
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    glGenBuffers(1, &prindexringvbo);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, prindexringvbo);

    if (pr_buckets)
    {
        glBufferStorage(GL_ELEMENT_ARRAY_BUFFER, prindexringsize * sizeof(GLuint), NULL, prindexringmapflags | GL_DYNAMIC_STORAGE_BIT);
        prindexring = (GLuint*)glMapBufferRange(GL_ELEMENT_ARRAY_BUFFER, 0, prindexringsize * sizeof(GLuint), prindexringmapflags);
    }

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

    i = 0;
    while (i < numsectors)
    {
        polymer_initsector(i);
        polymer_updatesector(i);
        i++;
    }

    i = 0;
    while (i < numwalls)
    {
        polymer_initwall(i);
        polymer_updatewall(i);
        i++;
    }

    polymer_getsky();

    polymer_resetlights();

    if (pr_verbosity >= 1 && numsectors) OSD_Printf("PR : Board loaded.\n");
}

int32_t polymer_printtext256(int32_t xpos, int32_t ypos, int16_t col, int16_t backcol, const char *name, char fontsize)
{
    //POGOTODO: Polymer should implement this so it's no longer coupled with Polymost & reliant on the fixed-function pipeline
    glEnable(GL_TEXTURE_2D);
    int32_t returnVal = polymost_printtext256(xpos, ypos, col, backcol, name, fontsize);
    glDisable(GL_TEXTURE_2D);
    return returnVal;
}

void polymer_fillpolygon(int32_t npoints)
{
    //POGOTODO: Polymer should implement this so it's no longer coupled with Polymost & reliant on the fixed-function pipeline
    glEnable(GL_TEXTURE_2D);
    polymost_fillpolygon(npoints);
    glDisable(GL_TEXTURE_2D);
}

// The parallaxed ART sky angle divisor corresponding to a horizfrac of 32768.
#define DEFAULT_ARTSKY_ANGDIV 4.3027f

void polymer_drawrooms(int32_t daposx, int32_t daposy, int32_t daposz, fix16_t daang, fix16_t dahoriz, int16_t dacursectnum)
{
    int16_t         cursectnum;
    int32_t         i, cursectflorz, cursectceilz;
    float           skyhoriz, ang, tiltang;
    float           pos[3];
    pthtyp*         pth;

    if (videoGetRenderMode() == REND_CLASSIC) return;

    videoBeginDrawing();

    // TODO: support for screen resizing
    // frameoffset = frameplace + windowxy1.y*bytesperline + windowxy1.x;

    if (pr_verbosity >= 3) OSD_Printf("PR : Drawing rooms...\n");

    // fogcalc_old needs this
    gvisibility = ((float)globalvisibility)*FOGSCALE;

    ang = fix16_to_float(daang) * (360.f/2048.f);
    horizang = -(float)atan2f(fix16_to_float(dahoriz) - 100.f, 128.f) * (180.f * (float)M_1_PI);
    tiltang = (gtang * 90.0f);

    pos[0] = (float)daposy;
    pos[1] = -(float)(daposz) / 16.0f;
    pos[2] = -(float)daposx;

    polymer_updatelights();

//     polymer_resetlights();
//     if (pr_lighting)
//         polymer_applylights();

    depth = 0;

    if (pr_shadows && lightcount && (pr_shadowcount > 0))
        polymer_prepareshadows();

    // hack for parallax skies
    skyhoriz = horizang;
    if (skyhoriz < -180.0f)
        skyhoriz += 360.0f;

    drawingskybox = 1;
    pth = texcache_fetch(cursky, 0, 0, DAMETH_NOMASK);
    drawingskybox = 0;

    // if it's not a skybox, make the sky parallax
    // DEFAULT_ARTSKY_ANGDIV is computed from eyeballed values
    // need to recompute it if we ever change the max horiz amplitude
    if (!pth || !(pth->flags & PTH_SKYBOX))
        skyhoriz *= curskyangmul;

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

    glRotatef(tiltang, 0.0f, 0.0f, -1.0f);
    glRotatef(skyhoriz, 1.0f, 0.0f, 0.0f);
    glRotatef(ang, 0.0f, 1.0f, 0.0f);

    glScalef(1.0f / 1000.0f, 1.0f / 1000.0f, 1.0f / 1000.0f);
    glTranslatef(-pos[0], -pos[1], -pos[2]);

    glGetFloatv(GL_MODELVIEW_MATRIX, rootskymodelviewmatrix);

    curskymodelviewmatrix = rootskymodelviewmatrix;

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

    glRotatef(tiltang, 0.0f, 0.0f, -1.0f);
    glRotatef(horizang, 1.0f, 0.0f, 0.0f);
    glRotatef(ang, 0.0f, 1.0f, 0.0f);

    glScalef(1.0f / 1000.0f, 1.0f / 1000.0f, 1.0f / 1000.0f);
    glTranslatef(-pos[0], -pos[1], -pos[2]);

    glGetFloatv(GL_MODELVIEW_MATRIX, rootmodelviewmatrix);

    cursectnum = dacursectnum;
    updatesector(daposx, daposy, &cursectnum);

    if (cursectnum >= 0 && cursectnum < numsectors)
        dacursectnum = cursectnum;
    else if (pr_verbosity>=2)
        OSD_Printf("PR : got sector %d after update!\n", cursectnum);

    // unflag all sectors
    i = numsectors-1;
    while (i >= 0)
    {
        prsectors[i]->flags.uptodate = 0;
        prsectors[i]->wallsproffset = 0.0f;
        prsectors[i]->floorsproffset = 0.0f;
        i--;
    }
    i = numwalls-1;
    while (i >= 0)
    {
        prwalls[i]->flags.uptodate = 0;
        i--;
    }

    if (searchit == 2 && !polymersearching)
    {
        globaloldoverridematerial = overridematerial;
        overridematerial = prprogrambits[PR_BIT_DIFFUSE_MODULATION].bit;
        overridematerial |= prprogrambits[PR_BIT_DIFFUSE_MAP2].bit;
        polymersearching = TRUE;
    }
    if (!searchit && polymersearching) {
        overridematerial = globaloldoverridematerial;
        polymersearching = FALSE;
    }

    if (dacursectnum > -1 && dacursectnum < numsectors)
        getzsofslope(dacursectnum, daposx, daposy, &cursectceilz, &cursectflorz);

    // external view (editor)
    if ((dacursectnum < 0) || (dacursectnum >= numsectors) ||
            (daposz > cursectflorz) ||
            (daposz < cursectceilz))
    {
        prcanbucket = 1;

        if (!editstatus && pr_verbosity>=1)
        {
            if ((unsigned)dacursectnum < (unsigned)numsectors)
                OSD_Printf("PR : EXT sec=%d  z=%d (%d, %d)\n", dacursectnum, daposz, cursectflorz, cursectceilz);
            else
                OSD_Printf("PR : EXT sec=%d  z=%d\n", dacursectnum, daposz);
        }

        curmodelviewmatrix = rootmodelviewmatrix;
        i = numsectors-1;
        while (i >= 0)
        {
            polymer_updatesector(i);
            polymer_drawsector(i, FALSE);
            polymer_scansprites(i, tsprite, &spritesortcnt);
            i--;
        }

        i = numwalls-1;
        while (i >= 0)
        {
            polymer_updatewall(i);
            polymer_drawwall(sectorofwall(i), i);
            i--;
        }

        polymer_emptybuckets();

        viewangle = daang;
        videoEndDrawing();
        return;
    }

    // GO!
    polymer_displayrooms(dacursectnum);

    curmodelviewmatrix = rootmodelviewmatrix;

    // build globals used by rotatesprite
    viewangle = daang;
    set_globalang(daang);

    // polymost globals used by polymost_dorotatesprite
    gcosang = fcosglobalang*(1./262144.f);
    gsinang = fsinglobalang*(1./262144.f);
    gcosang2 = gcosang*fviewingrange*(1./65536.f);
    gsinang2 = gsinang*fviewingrange*(1./65536.f);

    if (pr_verbosity >= 3) OSD_Printf("PR : Rooms drawn.\n");
    videoEndDrawing();
}

void                polymer_drawmasks(void)
{
    glEnable(GL_ALPHA_TEST);
    glEnable(GL_BLEND);
//     glEnable(GL_POLYGON_OFFSET_FILL);

//     while (--spritesortcnt)
//     {
//         tspriteptr[spritesortcnt] = &tsprite[spritesortcnt];
//         polymer_drawsprite(spritesortcnt);
//     }

    glEnable(GL_CULL_FACE);

    if (cursectormaskcount) {
        // We (kind of) queue sector masks near to far, so drawing them in reverse
        // order is the sane approach here. Of course impossible cases will arise.
        while (*cursectormaskcount) {
            polymer_drawsector(cursectormasks[--(*cursectormaskcount)], TRUE);
        }

        // This should _always_ be called after a corresponding pr_displayrooms()
        // unless we're in "external view" mode, which was checked above.
        // Both the top-level game drawrooms and the recursive internal passes
        // should be accounted for here. If these free cause corruption, there's
        // an accounting bug somewhere.
        DO_FREE_AND_NULL(cursectormaskcount);
        DO_FREE_AND_NULL(cursectormasks);
    }

    glDisable(GL_CULL_FACE);

//     glDisable(GL_POLYGON_OFFSET_FILL);
    glDisable(GL_BLEND);
    glDisable(GL_ALPHA_TEST);
}

void                polymer_editorpick(void)
{
    GLubyte         picked[3];
    int16_t         num;

    glReadPixels(searchx, ydim - searchy, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, picked);

    num = B_UNBUF16(&picked[1]);

    searchstat = picked[0];

    switch (searchstat) {
    case 0: // wall
    case 5: // botomwall
    case 4: // 1-way/masked wall
        searchsector = sectorofwall(num);
        searchbottomwall = searchwall = num;
        searchisbottom = (searchstat==5);
        if (searchstat == 5) {
            searchstat = 0;
            if (wall[num].nextwall >= 0 && (wall[num].cstat & 2)) {
                searchbottomwall = wall[num].nextwall;
            }
        }
        break;
    case 1: // floor
    case 2: // ceiling
        searchsector = num;

        // Apologies to Plagman for littering here, but this feature is quite essential
        {
            GLdouble model[16];
            GLdouble proj[16];
            GLint view[4];

            GLdouble x,y,z;
            GLfloat scr[3], scrv[3];
            GLdouble scrx,scry,scrz;
            GLfloat dadepth;

            int16_t k, bestk=0;
            GLfloat bestwdistsq = (GLfloat)3.4e38, wdistsq;
            GLfloat w1[2], w2[2], w21[2], pw1[2], pw2[2];
            GLfloat ptonline[2];
            GLfloat scrvxz[2];
            GLfloat scrvxznorm, scrvxzn[2], scrpxz[2];
            GLfloat w1d, w2d;
            walltype *wal = &wall[sector[searchsector].wallptr];

            GLfloat t, svcoeff, p[2];
            GLfloat *pl;

            glGetDoublev(GL_MODELVIEW_MATRIX, model);
            glGetDoublev(GL_PROJECTION_MATRIX, proj);
            glGetIntegerv(GL_VIEWPORT, view);

            glReadPixels(searchx, ydim-searchy, 1,1, GL_DEPTH_COMPONENT, GL_FLOAT, &dadepth);
            bgluUnProject(searchx, ydim-searchy, dadepth,  model, proj, view,  &x, &y, &z);
            bgluUnProject(searchx, ydim-searchy, 0.0,  model, proj, view,  &scrx, &scry, &scrz);

            scr[0]=scrx, scr[1]=scry, scr[2]=scrz;

            scrv[0] = x-scrx;
            scrv[1] = y-scry;
            scrv[2] = z-scrz;

            scrvxz[0] = x-scrx;
            scrvxz[1] = z-scrz;

            if (prsectors[searchsector]==NULL)
            {
                //OSD_Printf("polymer_editorpick: prsectors[searchsector]==NULL !!!\n");
                searchwall = sector[num].wallptr;
            }
            else
            {
                if (searchstat==1)
                    pl = prsectors[searchsector]->ceil.plane;
                else
                    pl = prsectors[searchsector]->floor.plane;

                if (pl == NULL)
                {
                    searchwall = sector[num].wallptr;
                    return;
                }

                t = dot3f(pl,scrv);
                svcoeff = -(dot3f(pl,scr)+pl[3])/t;

                // point on plane (x and z)
                p[0] = scrx + svcoeff*scrv[0];
                p[1] = scrz + svcoeff*scrv[2];

                for (k=0; k<sector[searchsector].wallnum; k++)
                {
                    w1[1] = -(float)wal[k].x;
                    w1[0] = (float)wal[k].y;
                    w2[1] = -(float)wall[wal[k].point2].x;
                    w2[0] = (float)wall[wal[k].point2].y;

                    scrvxznorm = sqrt(dot2f(scrvxz,scrvxz));
                    scrvxzn[0] = scrvxz[1]/scrvxznorm;
                    scrvxzn[1] = -scrvxz[0]/scrvxznorm;

                    relvec2f(p,w1, pw1);
                    relvec2f(p,w2, pw2);
                    relvec2f(w2,w1, w21);

                    w1d = dot2f(scrvxzn,pw1);
                    w2d = dot2f(scrvxzn,pw2);
                    w2d = -w2d;
                    if (w1d <= 0 || w2d <= 0)
                        continue;

                    ptonline[0] = w2[0]+(w2d/(w1d+w2d))*w21[0];
                    ptonline[1] = w2[1]+(w2d/(w1d+w2d))*w21[1];
                    relvec2f(p,ptonline, scrpxz);
                    if (dot2f(scrvxz,scrpxz)<0)
                        continue;

                    wdistsq = dot2f(scrpxz,scrpxz);
                    if (wdistsq < bestwdistsq)
                    {
                        bestk = k;
                        bestwdistsq = wdistsq;
                    }
                }

                searchwall = sector[searchsector].wallptr + bestk;
            }
        }
        // :P

//        searchwall = sector[num].wallptr;
        break;
    case 3:
        // sprite
        searchsector = sprite[num].sectnum;
        searchwall = num;
        break;
    }

    searchit = 0;
}

void                polymer_inb4rotatesprite(int16_t tilenum, char pal, int8_t shade, int32_t method)
{
    _prmaterial     rotatespritematerial;

    polymer_getbuildmaterial(&rotatespritematerial, tilenum, pal, shade, 0, method);

    rotatespritematerialbits = polymer_bindmaterial(&rotatespritematerial, NULL, 0);
}

void                polymer_postrotatesprite(void)
{
    polymer_unbindmaterial(rotatespritematerialbits);
}

static void         polymer_setupdiffusemodulation(_prplane *plane, GLubyte modulation, const GLubyte *data)
{
    plane->material.diffusemodulation[0] = modulation;
    plane->material.diffusemodulation[1] = ((GLubyte const *) data)[0];
    plane->material.diffusemodulation[2] = ((GLubyte const *) data)[1];
    plane->material.diffusemodulation[3] = 0xFF;
}

static void         polymer_drawsearchplane(_prplane *plane, GLubyte *oldcolor, GLubyte modulation, GLubyte *data)
{
    Bmemcpy(oldcolor, plane->material.diffusemodulation, sizeof(GLubyte) * 4);

    polymer_setupdiffusemodulation(plane, modulation, data);

    polymer_drawplane(plane);

    Bmemcpy(plane->material.diffusemodulation, oldcolor, sizeof(GLubyte) * 4);
}

void                polymer_drawmaskwall(int32_t damaskwallcnt)
{
    usectorptr_t      sec;
    walltype        *wal;
    _prwall         *w;
    GLubyte         oldcolor[4];

    if (pr_verbosity >= 3) OSD_Printf("PR : Masked wall %i...\n", damaskwallcnt);

    sec = (usectorptr_t)&sector[sectorofwall(maskwall[damaskwallcnt])];
    wal = &wall[maskwall[damaskwallcnt]];
    w = prwalls[maskwall[damaskwallcnt]];

    glEnable(GL_CULL_FACE);

    if (searchit == 2) {
        polymer_drawsearchplane(&w->mask, oldcolor, 0x04, (GLubyte *)&maskwall[damaskwallcnt]);
    } else {
        calc_and_apply_fog(fogshade(wal->shade, wal->pal), sec->visibility, get_floor_fogpal(sec));
        polymer_drawplane(&w->mask);
    }

    glDisable(GL_CULL_FACE);
}

void                polymer_drawsprite(int32_t snum)
{
    int32_t         i, j, cs;
    _prsprite       *s;

    auto const tspr = tspriteptr[snum];
    usectorptr_t sec;

    if (pr_verbosity >= 3) OSD_Printf("PR : Sprite %i...\n", snum);

    if (bad_tspr(tspr))
        return;

    if ((tspr->clipdist & TSPR_FLAGS_NO_SHADOW) && (depth && !mirrors[depth-1].plane))
        return;

    if ((tspr->clipdist & TSPR_FLAGS_INVISIBLE_WITH_SHADOW) && (!depth || mirrors[depth-1].plane))
        return;

    int const spritenum = tspr->owner;
    Bassert(spritenum < MAXSPRITES+MAXUNIQHUDID);

    tileUpdatePicnum(&tspr->picnum, (unsigned)spritenum < MAXSPRITES ? spritenum+32768 : 0);

    sec = (usectorptr_t)&sector[tspr->sectnum];
    calc_and_apply_fog(fogshade(tspr->shade, tspr->pal), sec->visibility, get_floor_fogpal((usectorptr_t)&sector[tspr->sectnum]));

    if (usemodels && tile2model[Ptile2tile(tspr->picnum,tspr->pal)].modelid >= 0 &&
        tile2model[Ptile2tile(tspr->picnum,tspr->pal)].framenum >= 0 &&
        !(spriteext[spritenum].flags & SPREXT_NOTMD))
    {
        glEnable(GL_CULL_FACE);
        SWITCH_CULL_DIRECTION;
        polymer_drawmdsprite(tspr);
        SWITCH_CULL_DIRECTION;
        glDisable(GL_CULL_FACE);
        return;
    }

    cs = tspr->cstat;

    // I think messing with the tspr is safe at this point?
    // If not, change that to modify a temp position in updatesprite itself.
    // I don't think this flags are meant to change on the fly so it'd possibly
    // be safe to cache a plane that has them applied.
    if (spriteext[spritenum].flags & SPREXT_AWAY1)
    {
        tspr->x += sintable[(tspr->ang + 512) & 2047] >> 13;
        tspr->y += sintable[tspr->ang & 2047] >> 13;
    }
    else if (spriteext[spritenum].flags & SPREXT_AWAY2)
    {
        tspr->x -= sintable[(tspr->ang + 512) & 2047] >> 13;
        tspr->y -= sintable[tspr->ang & 2047] >> 13;
    }

    polymer_updatesprite(snum);

    Bassert(spritenum < MAXSPRITES);
    s = prsprites[spritenum];

    if (s == NULL)
        return;

    switch ((tspr->cstat>>4) & 3)
    {
    case 1:
        prsectors[tspr->sectnum]->wallsproffset += 0.5f;
        if (!depth || mirrors[depth-1].plane)
            glPolygonOffset(-1.0f, -1.0f);
        break;
    case 2:
        prsectors[tspr->sectnum]->floorsproffset += 0.5f;
        if (!depth || mirrors[depth-1].plane)
            glPolygonOffset(-1.0f, -1.0f);
        break;
    }

    if ((cs & 48) == 0)
    {
        int32_t curpriority = 0;

        s->plane.lightcount = 0;

        while ((curpriority < pr_maxlightpriority) && (!depth || mirrors[depth-1].plane))
        {
            i = j = 0;
            while (j < lightcount)
            {
                while (!prlights[i].flags.active)
                    i++;

                if (prlights[i].priority != curpriority)
                {
                    i++;
                    j++;
                    continue;
                }

                if (polymer_planeinlight(&s->plane, &prlights[i]))
                    s->plane.lights[s->plane.lightcount++] = i;

                i++;
                j++;
            }
            curpriority++;
        }
    }

    if (automapping == 1)
        show2dsprite[spritenum>>3] |= pow2char[spritenum&7];

    if ((tspr->cstat & 64) && (tspr->cstat & SPR_ALIGN_MASK))
    {
        if ((tspr->cstat & SPR_ALIGN_MASK)==SPR_FLOOR && (tspr->cstat & SPR_YFLIP))
            SWITCH_CULL_DIRECTION;
        glEnable(GL_CULL_FACE);
    }

    if ((!depth || mirrors[depth-1].plane) && !pr_ati_nodepthoffset)
        glEnable(GL_POLYGON_OFFSET_FILL);

    polymer_drawplane(&s->plane);

    if ((!depth || mirrors[depth-1].plane) && !pr_ati_nodepthoffset)
        glDisable(GL_POLYGON_OFFSET_FILL);

    if ((tspr->cstat & 64) && (tspr->cstat & SPR_ALIGN_MASK))
    {
        if ((tspr->cstat & SPR_ALIGN_MASK)==SPR_FLOOR && (tspr->cstat & SPR_YFLIP))
            SWITCH_CULL_DIRECTION;
        glDisable(GL_CULL_FACE);
    }
}

void                polymer_setanimatesprites(animatespritesptr animatesprites, int32_t x, int32_t y, int32_t z, int32_t a, int32_t smoothratio)
{
    asi.animatesprites = animatesprites;
    asi.x = x;
    asi.y = y;
    asi.z = z;
    asi.a = a;
    asi.smoothratio = smoothratio;
}

int16_t             polymer_addlight(_prlight* light)
{
    int32_t         lighti;

    if (lightcount >= PR_MAXLIGHTS || light->priority > pr_maxlightpriority || !pr_lighting)
        return -1;

    if ((light->sector == -1) || (light->sector >= numsectors))
        return -1;

    lighti = 0;
    while ((lighti < PR_MAXLIGHTS) && (prlights[lighti].flags.active))
        lighti++;

    if (lighti == PR_MAXLIGHTS)
        return -1;
#if 0
    // Spot lights disabled on ATI cards because they cause crashes with
    // Catalyst 12.8 drivers.
    // See: http://forums.duke4.net/topic/5723-hrp-polymer-crash/
    if (pr_ati_fboworkaround && light->radius)
        return -1;
#endif
    Bmemcpy(&prlights[lighti], light, sizeof(_prlight));

    if (light->radius) {
        polymer_processspotlight(&prlights[lighti]);

        // get the texture handle for the lightmap
        if (light->tilenum > 0) {
            int16_t     picnum = light->tilenum;
            pthtyp*     pth;

            tileUpdatePicnum(&picnum, 0);

            if (!waloff[picnum])
                tileLoad(picnum);

            pth = NULL;
            pth = texcache_fetch(picnum, 0, 0, DAMETH_NOMASK);

            if (pth)
                light->lightmap = pth->glpic;
        }
    }

    prlights[lighti].flags.isinview = 0;
    prlights[lighti].flags.active = 1;

    prlights[lighti].flags.invalidate = 0;

    prlights[lighti].planecount = 0;
    prlights[lighti].planelist = NULL;

    polymer_culllight(lighti);

    lightcount++;

    return lighti;
}

void                polymer_deletelight(int16_t lighti)
{
    if (!prlights[lighti].flags.active)
    {
#ifdef DEBUGGINGAIDS
        if (pr_verbosity >= 2)
            OSD_Printf("PR : Called polymer_deletelight on inactive light\n");
        // currently known cases: when reloading maphack lights (didn't set maphacklightcnt=0
        // but did loadmaphack()->delete_maphack_lights() after polymer_resetlights())
#endif
        return;
    }

    polymer_removelight(lighti);

    prlights[lighti].flags.active = 0;

    lightcount--;
}

void                polymer_invalidatelights(void)
{
    int32_t         i = PR_MAXLIGHTS-1;

    do
        prlights[i].flags.invalidate = prlights[i].flags.active;
    while (i--);
}

void                polymer_texinvalidate(void)
{
    int32_t         i;

    i = 0;

    while (i < MAXSPRITES) {
        polymer_invalidatesprite(i);
        i++;
    }

    i = numsectors - 1;

    if (!numsectors || !prsectors[i])
        return;

    do
        prsectors[i--]->flags.invalidtex = 1;
    while (i >= 0);

    i = numwalls - 1;
    do
        prwalls[i--]->flags.invalidtex = 1;
    while (i >= 0);
}

void                polymer_definehighpalookup(char basepalnum, char palnum, char *data)
{
    prhighpalookups[basepalnum][palnum].data = (char *)Xmalloc(PR_HIGHPALOOKUP_DATA_SIZE);

    Bmemcpy(prhighpalookups[basepalnum][palnum].data, data, PR_HIGHPALOOKUP_DATA_SIZE);
}

int32_t             polymer_havehighpalookup(int32_t basepalnum, int32_t palnum)
{
    if ((uint32_t)basepalnum >= MAXBASEPALS || (uint32_t)palnum >= MAXPALOOKUPS)
        return 0;

    return (prhighpalookups[basepalnum][palnum].data != NULL);
}


// CORE
static void         polymer_displayrooms(const int16_t dacursectnum)
{
    usectorptr_t      sec;
    int32_t         i;
    int16_t         bunchnum;
    int16_t         ns;
    GLint           result;
    int16_t         doquery;
    int32_t         front;
    int32_t         back;
    GLfloat         localskymodelviewmatrix[16];
    GLfloat         localmodelviewmatrix[16];
    GLfloat         localprojectionmatrix[16];
    float           frustum[5 * 4];
    int32_t         localspritesortcnt;
    tspritetype     localtsprite[MAXSPRITESONSCREEN];
    int16_t         localmaskwall[MAXWALLSB];
    int16_t         localmaskwallcnt;
    _prmirror       mirrorlist[10];
    int             mirrorcount;
    int16_t         *localsectormasks;
    int16_t         *localsectormaskcount;
    int32_t         gx, gy, gz, px, py, pz;
    GLdouble        plane[4];
    float           coeff;

    curmodelviewmatrix = localmodelviewmatrix;
    glGetFloatv(GL_MODELVIEW_MATRIX, localmodelviewmatrix);
    glGetFloatv(GL_PROJECTION_MATRIX, localprojectionmatrix);

    polymer_extractfrustum(localmodelviewmatrix, localprojectionmatrix, frustum);

    Bmemset(querydelay, 0, sizeof(int16_t) * numsectors);
    Bmemset(queryid, 0, sizeof(GLuint) * numwalls);
    Bmemset(drawingstate, 0, sizeof(int16_t) * numsectors);

    front = 0;
    back = 1;
    sectorqueue[0] = dacursectnum;
    drawingstate[dacursectnum] = 1;

    localspritesortcnt = localmaskwallcnt = 0;

    mirrorcount = 0;

    localsectormasks = (int16_t *)Xmalloc(sizeof(int16_t) * numsectors);
    localsectormaskcount = (int16_t *)Xcalloc(sizeof(int16_t), 1);
    cursectormasks = localsectormasks;
    cursectormaskcount = localsectormaskcount;

    glDisable(GL_DEPTH_TEST);
    glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
    polymer_drawsky(cursky, curskypal, curskyshade);
    glEnable(GL_DEPTH_TEST);

    // depth-only occlusion testing pass
//     overridematerial = 0;

    prcanbucket = 1;

    while (front != back)
    {
        sec = (usectorptr_t)&sector[sectorqueue[front]];

        polymer_pokesector(sectorqueue[front]);
        polymer_drawsector(sectorqueue[front], FALSE);
        polymer_scansprites(sectorqueue[front], localtsprite, &localspritesortcnt);

        doquery = 0;

        i = sec->wallnum-1;
        do
        {
            // if we have a level boundary somewhere in the sector,
            // consider these walls as visportals
            if (wall[sec->wallptr + i].nextsector < 0 && pr_buckets == 0)
                doquery = 1;
        }
        while (--i >= 0);

        i = sec->wallnum-1;
        while (i >= 0)
        {
            if ((wall[sec->wallptr + i].nextsector >= 0) &&
                (wallvisible(globalposx, globalposy, sec->wallptr + i)) &&
                (polymer_planeinfrustum(&prwalls[sec->wallptr + i]->mask, frustum)))
            {
                if ((prwalls[sec->wallptr + i]->mask.vertcount == 4) &&
                    !(prwalls[sec->wallptr + i]->underover & 4) &&
                    !(prwalls[sec->wallptr + i]->underover & 8))
                {
                    // early exit for closed sectors
                    _prwall         *w;

                    w = prwalls[sec->wallptr + i];

                    if ((w->mask.buffer[0].y >= w->mask.buffer[3].y) &&
                        (w->mask.buffer[1].y >= w->mask.buffer[2].y))
                    {
                        i--;
                        continue;
                    }
                }

                if ((wall[sec->wallptr + i].cstat & 48) == 16)
                {
                    int pic = wall[sec->wallptr + i].overpicnum;

                    if (tilesiz[pic].x > 0 && tilesiz[pic].y > 0)
                        localmaskwall[localmaskwallcnt++] = sec->wallptr + i;
                }

                if (!depth && (overridematerial & prprogrambits[PR_BIT_MIRROR_MAP].bit) &&
                     wall[sec->wallptr + i].overpicnum == 560 &&
                     wall[sec->wallptr + i].cstat & 32)
                {
                    mirrorlist[mirrorcount].plane = &prwalls[sec->wallptr + i]->mask;
                    mirrorlist[mirrorcount].sectnum = sectorqueue[front];
                    mirrorlist[mirrorcount].wallnum = sec->wallptr + i;
                    mirrorcount++;
                }

                if (!(wall[sec->wallptr + i].cstat & 32)) {
                    if (doquery && (!drawingstate[wall[sec->wallptr + i].nextsector]))
                    {
                        float pos[3], sqdist;
                        int32_t oldoverridematerial;

                        pos[0] = fglobalposy;
                        pos[1] = fglobalposz * (-1.f/16.f);
                        pos[2] = -fglobalposx;

                        sqdist = prwalls[sec->wallptr + i]->mask.plane[0] * pos[0] +
                                 prwalls[sec->wallptr + i]->mask.plane[1] * pos[1] +
                                 prwalls[sec->wallptr + i]->mask.plane[2] * pos[2] +
                                 prwalls[sec->wallptr + i]->mask.plane[3];

                        // hack to avoid occlusion querying portals that are too close to the viewpoint
                        // this is needed because of the near z-clipping plane;
                        if (sqdist < 100)
                            queryid[sec->wallptr + i] = 0xFFFFFFFF;
                        else {
                            _prwall         *w;

                            w = prwalls[sec->wallptr + i];

                            glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
                            glDepthMask(GL_FALSE);

                            glGenQueries(1, &queryid[sec->wallptr + i]);
                            glBeginQuery(GL_SAMPLES_PASSED, queryid[sec->wallptr + i]);

                            oldoverridematerial = overridematerial;
                            overridematerial = 0;

                            if ((w->underover & 4) && (w->underover & 1))
                                polymer_drawplane(&w->wall);
                            polymer_drawplane(&w->mask);
                            if ((w->underover & 8) && (w->underover & 2))
                                polymer_drawplane(&w->over);

                            overridematerial = oldoverridematerial;

                            glEndQuery(GL_SAMPLES_PASSED);

                            glDepthMask(GL_TRUE);
                            glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
                        }
                    } else
                        queryid[sec->wallptr + i] = 0xFFFFFFFF;
                }
            }

            i--;
        }

        // Cram as much CPU or GPU work as we can between queuing the
        // occlusion queries and reaping them.
        i = sec->wallnum-1;
        do
        {
            if (wallvisible(globalposx, globalposy, sec->wallptr + i))
                polymer_drawwall(sectorqueue[front], sec->wallptr + i);
        }
        while (--i >= 0);
#ifdef YAX_ENABLE
        // queue ROR neighbors
        if ((bunchnum = yax_getbunch(sectorqueue[front], YAX_FLOOR)) >= 0) {

            for (SECTORS_OF_BUNCH(bunchnum, YAX_CEILING, ns)) {

                if (ns >= 0 && !drawingstate[ns] &&
                    polymer_planeinfrustum(&prsectors[ns]->ceil, frustum)) {

                    sectorqueue[back++] = ns;
                    drawingstate[ns] = 1;
                }
            }
        }

        if ((bunchnum = yax_getbunch(sectorqueue[front], YAX_CEILING)) >= 0) {

            for (SECTORS_OF_BUNCH(bunchnum, YAX_FLOOR, ns)) {

                if (ns >= 0 && !drawingstate[ns] &&
                    polymer_planeinfrustum(&prsectors[ns]->floor, frustum)) {

                    sectorqueue[back++] = ns;
                    drawingstate[ns] = 1;
                }
            }
        }
#endif
        i = sec->wallnum-1;
        do
        {
            if ((queryid[sec->wallptr + i]) &&
                (!drawingstate[wall[sec->wallptr + i].nextsector]))
            {
                // REAP
                result = 0;
                if (doquery && (queryid[sec->wallptr + i] != 0xFFFFFFFF))
                {
                    glGetQueryObjectiv(queryid[sec->wallptr + i],
                                           GL_QUERY_RESULT,
                                           &result);
                    glDeleteQueries(1, &queryid[sec->wallptr + i]);
                } else if (queryid[sec->wallptr + i] == 0xFFFFFFFF)
                    result = 1;

                queryid[sec->wallptr + i] = 0;

                if (result || !doquery)
                {
                    sectorqueue[back++] = wall[sec->wallptr + i].nextsector;
                    drawingstate[wall[sec->wallptr + i].nextsector] = 1;
                }
            } else if (queryid[sec->wallptr + i] &&
                       queryid[sec->wallptr + i] != 0xFFFFFFFF)
            {
                glDeleteQueries(1, &queryid[sec->wallptr + i]);
                queryid[sec->wallptr + i] = 0;
            }
        }
        while (--i >= 0);

        front++;
    }

    polymer_emptybuckets();

    // do the actual shaded drawing
//     overridematerial = 0xFFFFFFFF;

    // go through the sector queue again
//     front = 0;
//     while (front < back)
//     {
//         sec = &sector[sectorqueue[front]];
//
//         polymer_drawsector(sectorqueue[front]);
//
//         i = 0;
//         while (i < sec->wallnum)
//         {
//             polymer_drawwall(sectorqueue[front], sec->wallptr + i);
//
//             i++;
//         }
//
//         front++;
//     }

    i = mirrorcount-1;
    while (i >= 0)
    {
        glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, prrts[0].fbo);
        glPushAttrib(GL_VIEWPORT_BIT);
        glViewport(windowxy1.x, ydim-(windowxy2.y+1),windowxy2.x-windowxy1.x+1, windowxy2.y-windowxy1.y+1);

        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        Bmemcpy(localskymodelviewmatrix, curskymodelviewmatrix, sizeof(GLfloat) * 16);
        curskymodelviewmatrix = localskymodelviewmatrix;

        glMatrixMode(GL_MODELVIEW);
        glPushMatrix();

        plane[0] = mirrorlist[i].plane->plane[0];
        plane[1] = mirrorlist[i].plane->plane[1];
        plane[2] = mirrorlist[i].plane->plane[2];
        plane[3] = mirrorlist[i].plane->plane[3];

        glClipPlane(GL_CLIP_PLANE0, plane);
        polymer_inb4mirror(mirrorlist[i].plane->buffer, mirrorlist[i].plane->plane);
        SWITCH_CULL_DIRECTION;
        //glEnable(GL_CLIP_PLANE0);

        if (mirrorlist[i].wallnum >= 0)
            renderPrepareMirror(globalposx, globalposy, globalposz, qglobalang, qglobalhoriz,
                                mirrorlist[i].wallnum, &gx, &gy, &viewangle);

        gx = globalposx;
        gy = globalposy;
        gz = globalposz;

        // map the player pos from build to polymer
        px = globalposy;
        py = -globalposz / 16;
        pz = -globalposx;

        // calculate new player position on the other side of the mirror
        // this way the basic build visibility shit can be used (wallvisible)
        coeff = mirrorlist[i].plane->plane[0] * px +
                mirrorlist[i].plane->plane[1] * py +
                mirrorlist[i].plane->plane[2] * pz +
                mirrorlist[i].plane->plane[3];

        coeff /= (float)(mirrorlist[i].plane->plane[0] * mirrorlist[i].plane->plane[0] +
                         mirrorlist[i].plane->plane[1] * mirrorlist[i].plane->plane[1] +
                         mirrorlist[i].plane->plane[2] * mirrorlist[i].plane->plane[2]);

        px = (int32_t)(-coeff*mirrorlist[i].plane->plane[0]*2 + px);
        py = (int32_t)(-coeff*mirrorlist[i].plane->plane[1]*2 + py);
        pz = (int32_t)(-coeff*mirrorlist[i].plane->plane[2]*2 + pz);

        // map back from polymer to build
        set_globalpos(-pz, px, -py * 16);

        mirrors[depth++] = mirrorlist[i];
        polymer_displayrooms(mirrorlist[i].sectnum);
        depth--;

        cursectormasks = localsectormasks;
        cursectormaskcount = localsectormaskcount;

        set_globalpos(gx, gy, gz);

        glDisable(GL_CLIP_PLANE0);
        SWITCH_CULL_DIRECTION;
        glMatrixMode(GL_MODELVIEW);
        glPopMatrix();

        glPopAttrib();
        glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);

        mirrorlist[i].plane->material.mirrormap = prrts[0].color;
        polymer_drawplane(mirrorlist[i].plane);
        mirrorlist[i].plane->material.mirrormap = 0;

        i--;
    }

    spritesortcnt = localspritesortcnt;
    Bmemcpy(tsprite, localtsprite, sizeof(tspritetype) * spritesortcnt);
    maskwallcnt = localmaskwallcnt;
    Bmemcpy(maskwall, localmaskwall, sizeof(int16_t) * maskwallcnt);

    if (depth)
    {
        set_globalang(viewangle);

        if (mirrors[depth - 1].plane)
            display_mirror = 1;
        polymer_animatesprites();
        if (mirrors[depth - 1].plane)
            display_mirror = 0;

        glDisable(GL_CULL_FACE);
        renderDrawMasks();
        glEnable(GL_CULL_FACE);
    }
    }

static void         polymer_emptybuckets(void)
{
    _prbucket *bucket = prbuckethead;

    if (pr_buckets == 0)
        return;

    glBindBuffer(GL_ARRAY_BUFFER, prmapvbo);
    glVertexPointer(3, GL_FLOAT, sizeof(_prvert), NULL);
    glTexCoordPointer(2, GL_FLOAT, sizeof(_prvert), (GLvoid *)(3 * sizeof(GLfloat)));

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, prindexringvbo);

    uint32_t indexcount = 0;
    while (bucket != NULL)
    {
        indexcount += bucket->count;

        bucket = bucket->next;
    }

    // ensure space in index ring, wrap otherwise
    if (indexcount + prindexringoffset >= (unsigned)prindexringsize)
    {
        glUnmapBuffer(GL_ELEMENT_ARRAY_BUFFER);
        prindexring = (GLuint *)glMapBufferRange(GL_ELEMENT_ARRAY_BUFFER, 0, prindexringsize * sizeof(GLuint), GL_MAP_INVALIDATE_BUFFER_BIT | prindexringmapflags);
        prindexringoffset = 0;
    }

    // put indices in the ring, all at once
    bucket = prbuckethead;

    while (bucket != NULL)
    {
        if (bucket->count == 0)
        {
            bucket = bucket->next;
            continue;
        }

        memcpy(&prindexring[prindexringoffset], bucket->indices, bucket->count * sizeof(GLuint));

        bucket->indiceoffset = (GLuint*)(prindexringoffset * sizeof(GLuint));

        prindexringoffset += bucket->count;

        bucket = bucket->next;
    }

    bucket = prbuckethead;

    while (bucket != NULL)
    {
        if (bucket->count == 0)
        {
            bucket = bucket->next;
            continue;
        }

        int32_t materialbits = polymer_bindmaterial(&bucket->material, NULL, 0);

        glDrawElements(GL_TRIANGLES, bucket->count, GL_UNSIGNED_INT, bucket->indiceoffset);

        polymer_unbindmaterial(materialbits);

        bucket->count = 0;

        bucket = bucket->next;
    }

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    prcanbucket = 0;
}

static hashtable_t h_buckets      = { 2048, NULL };

static _prbucket*   polymer_findbucket(int16_t tilenum, char pal)
{
    char propstr[16];

    Bsprintf(propstr, "%d_%d", tilenum, pal);

    _prbucket *bucketptr = prbuckethead ? (_prbucket *)hash_find(&h_buckets, propstr) : NULL;

    // find bucket

    // no buckets or no bucket found, create one
    if (bucketptr == NULL || (intptr_t)bucketptr == -1)
    {
        bucketptr = (_prbucket *)Xmalloc(sizeof (_prbucket));

        if (h_buckets.items == NULL)
            hash_init(&h_buckets);

        // insert, since most likely to use same pattern next frame
        // will need to reorder by MRU first every once in a while
        // or move to hashing lookup
        bucketptr->next = prbuckethead;
        prbuckethead = bucketptr;

        bucketptr->tilenum = tilenum;
        bucketptr->pal = pal;

        bucketptr->invalidmaterial = 1;

        bucketptr->count = 0;
        bucketptr->buffersize = 1024;
        bucketptr->indices = (GLuint *)Xmalloc(bucketptr->buffersize * sizeof(GLuint));

        hash_add(&h_buckets, propstr, (intptr_t)bucketptr, 1);
    }

    return bucketptr;
}

static void         polymer_bucketplane(_prplane* plane)
{
    _prbucket *bucketptr = plane->bucket;
    uint32_t neededindicecount;
    int32_t i;

    // we don't keep buffers for quads
    neededindicecount = (plane->indicescount == 0) ? 6 : plane->indicescount;

    // ensure size
    while (bucketptr->count + neededindicecount >= bucketptr->buffersize)
    {
        bucketptr->buffersize *= 2;
        bucketptr->indices = (GLuint *)Xrealloc(bucketptr->indices, bucketptr->buffersize * sizeof(GLuint));
    }

    // queue indices
    i = 0;

    if (plane->indicescount > 0)
    {
        while (i < plane->indicescount)
        {
            bucketptr->indices[bucketptr->count] = plane->indices[i] + plane->mapvbo_vertoffset;
            bucketptr->count++;
            i++;
        }
    }
    else
    {
        static const uint32_t quadindices[6] = { 0, 1, 2, 0, 2, 3 };

        while (i < 6)
        {
            bucketptr->indices[bucketptr->count] = quadindices[i] + plane->mapvbo_vertoffset;
            bucketptr->count++;
            i++;
        }
    }
}

static void         polymer_drawplane(_prplane* plane)
{
    int32_t         materialbits;

    if (pr_nullrender >= 1) return;

    // debug code for drawing plane inverse TBN
//     glDisable(GL_TEXTURE_2D);
//     glBegin(GL_LINES);
//     glColor4f(1.0, 0.0, 0.0, 1.0);
//     glVertex3f(plane->buffer[0],
//                 plane->buffer[1],
//                 plane->buffer[2]);
//     glVertex3f(plane->buffer[0] + plane->t[0] * 50,
//                 plane->buffer[1] + plane->t[1] * 50,
//                 plane->buffer[2] + plane->t[2] * 50);
//     glColor4f(0.0, 1.0, 0.0, 1.0);
//     glVertex3f(plane->buffer[0],
//                 plane->buffer[1],
//                 plane->buffer[2]);
//     glVertex3f(plane->buffer[0] + plane->b[0] * 50,
//                 plane->buffer[1] + plane->b[1] * 50,
//                 plane->buffer[2] + plane->b[2] * 50);
//     glColor4f(0.0, 0.0, 1.0, 1.0);
//     glVertex3f(plane->buffer[0],
//                 plane->buffer[1],
//                 plane->buffer[2]);
//     glVertex3f(plane->buffer[0] + plane->n[0] * 50,
//                 plane->buffer[1] + plane->n[1] * 50,
//                 plane->buffer[2] + plane->n[2] * 50);
//     glEnd();
//     glEnable(GL_TEXTURE_2D);

    // debug code for drawing plane normals
//     glDisable(GL_TEXTURE_2D);
//     glBegin(GL_LINES);
//     glColor4f(1.0, 1.0, 1.0, 1.0);
//     glVertex3f(plane->buffer[0],
//                 plane->buffer[1],
//                 plane->buffer[2]);
//     glVertex3f(plane->buffer[0] + plane->plane[0] * 50,
//                 plane->buffer[1] + plane->plane[1] * 50,
//                 plane->buffer[2] + plane->plane[2] * 50);
//     glEnd();
//     glEnable(GL_TEXTURE_2D);

    if (pr_buckets && pr_vbos > 0 && prcanbucket && plane->bucket)
    {
        polymer_bucketplane(plane);
        return;
    }

    glNormal3f((float)(plane->plane[0]), (float)(plane->plane[1]), (float)(plane->plane[2]));

    GLuint planevbo;
    GLintptr geomfbooffset;

    if (plane->mapvbo_vertoffset != (uint32_t)-1)
    {
        planevbo = prmapvbo;
        geomfbooffset = plane->mapvbo_vertoffset * sizeof(_prvert);
    }
    else
    {
        planevbo = plane->vbo;
        geomfbooffset = 0;
    }

    if (planevbo && (pr_vbos > 0))
    {
        glBindBuffer(GL_ARRAY_BUFFER, planevbo);
        glVertexPointer(3, GL_FLOAT, sizeof(_prvert), (GLvoid *)(geomfbooffset));
        glTexCoordPointer(2, GL_FLOAT, sizeof(_prvert), (GLvoid *)(geomfbooffset + (3 * sizeof(GLfloat))));
        if (plane->indices)
            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, plane->ivbo);
    } else {
        glVertexPointer(3, GL_FLOAT, sizeof(_prvert), &plane->buffer->x);
        glTexCoordPointer(2, GL_FLOAT, sizeof(_prvert), &plane->buffer->u);
    }

    curlight = 0;
    do {
        materialbits = polymer_bindmaterial(&plane->material, plane->lights, plane->lightcount);

        if (materialbits & prprogrambits[PR_BIT_NORMAL_MAP].bit)
        {
            glVertexAttrib3fv(prprograms[materialbits].attrib_T, &plane->tbn[0][0]);
            glVertexAttrib3fv(prprograms[materialbits].attrib_B, &plane->tbn[1][0]);
            glVertexAttrib3fv(prprograms[materialbits].attrib_N, &plane->tbn[2][0]);
        }

        if (plane->indices)
        {
            if (planevbo && (pr_vbos > 0))
                glDrawElements(GL_TRIANGLES, plane->indicescount, GL_UNSIGNED_SHORT, NULL);
            else
                glDrawElements(GL_TRIANGLES, plane->indicescount, GL_UNSIGNED_SHORT, plane->indices);
        } else
            glDrawArrays(GL_QUADS, 0, 4);

        polymer_unbindmaterial(materialbits);

        if (plane->lightcount && (!depth || mirrors[depth-1].plane))
            prlights[plane->lights[curlight]].flags.isinview = 1;

        curlight++;
    } while ((curlight < plane->lightcount) && (curlight < pr_maxlightpasses) && (!depth || mirrors[depth-1].plane));

    if (planevbo && (pr_vbos > 0))
    {
        glBindBuffer(GL_ARRAY_BUFFER, 0);
        if (plane->indices)
            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    }
}

static inline void  polymer_inb4mirror(_prvert* buffer, const GLfloat* plane)
{
    float           pv;
    float           reflectionmatrix[16];

    pv = buffer->x * plane[0] +
         buffer->y * plane[1] +
         buffer->z * plane[2];

    reflectionmatrix[0] = 1 - (2 * plane[0] * plane[0]);
    reflectionmatrix[1] = -2 * plane[0] * plane[1];
    reflectionmatrix[2] = -2 * plane[0] * plane[2];
    reflectionmatrix[3] = 0;

    reflectionmatrix[4] = -2 * plane[0] * plane[1];
    reflectionmatrix[5] = 1 - (2 * plane[1] * plane[1]);
    reflectionmatrix[6] = -2 * plane[1] * plane[2];
    reflectionmatrix[7] = 0;

    reflectionmatrix[8] = -2 * plane[0] * plane[2];
    reflectionmatrix[9] = -2 * plane[1] * plane[2];
    reflectionmatrix[10] = 1 - (2 * plane[2] * plane[2]);
    reflectionmatrix[11] = 0;

    reflectionmatrix[12] = 2 * pv * plane[0];
    reflectionmatrix[13] = 2 * pv * plane[1];
    reflectionmatrix[14] = 2 * pv * plane[2];
    reflectionmatrix[15] = 1;

    glMultMatrixf(reflectionmatrix);

    glPushMatrix();
    glLoadMatrixf(curskymodelviewmatrix);
    glMultMatrixf(reflectionmatrix);
    glGetFloatv(GL_MODELVIEW_MATRIX, curskymodelviewmatrix);
    glPopMatrix();
}

static void         polymer_animatesprites(void)
{
    if (asi.animatesprites)
        asi.animatesprites(globalposx, globalposy, globalposz, fix16_to_int(viewangle), asi.smoothratio);
}

static void         polymer_freeboard(void)
{
    int32_t         i;

    i = 0;
    while (i < MAXSECTORS)
    {
        if (prsectors[i])
        {
            Xfree(prsectors[i]->verts);
            Xfree(prsectors[i]->floor.buffer);
            Xfree(prsectors[i]->ceil.buffer);
            Xfree(prsectors[i]->floor.indices);
            Xfree(prsectors[i]->ceil.indices);
            if (prsectors[i]->ceil.vbo) glDeleteBuffers(1, &prsectors[i]->ceil.vbo);
            if (prsectors[i]->ceil.ivbo) glDeleteBuffers(1, &prsectors[i]->ceil.ivbo);
            if (prsectors[i]->floor.vbo) glDeleteBuffers(1, &prsectors[i]->floor.vbo);
            if (prsectors[i]->floor.ivbo) glDeleteBuffers(1, &prsectors[i]->floor.ivbo);

            DO_FREE_AND_NULL(prsectors[i]);
        }

        i++;
    }

    i = 0;
    while (i < MAXWALLS)
    {
        if (prwalls[i])
        {
            Xfree(prwalls[i]->bigportal);
            Xfree(prwalls[i]->mask.buffer);
            Xfree(prwalls[i]->over.buffer);
            // Xfree(prwalls[i]->cap);
            Xfree(prwalls[i]->wall.buffer);
            if (prwalls[i]->wall.vbo) glDeleteBuffers(1, &prwalls[i]->wall.vbo);
            if (prwalls[i]->over.vbo) glDeleteBuffers(1, &prwalls[i]->over.vbo);
            if (prwalls[i]->mask.vbo) glDeleteBuffers(1, &prwalls[i]->mask.vbo);
            if (prwalls[i]->stuffvbo) glDeleteBuffers(1, &prwalls[i]->stuffvbo);

            DO_FREE_AND_NULL(prwalls[i]);
        }

        i++;
    }

    i = 0;
    while (i < MAXSPRITES)
    {
        if (prsprites[i])
        {
            Xfree(prsprites[i]->plane.buffer);
            if (prsprites[i]->plane.vbo) glDeleteBuffers(1, &prsprites[i]->plane.vbo);
            DO_FREE_AND_NULL(prsprites[i]);
        }

        i++;
    }

    i = 0;
    while (i < MAXTILES)
    {
        polymer_invalidateartmap(i);
        i++;
    }

    i = 0;
    while (i < MAXBASEPALS)
    {
        if (prbasepalmaps[i])
        {
            glDeleteTextures(1, &prbasepalmaps[i]);
            prbasepalmaps[i] = 0;
        }

        i++;
    }

    i = 0;
    while (i < MAXPALOOKUPS)
    {
        if (prlookups[i])
        {
            glDeleteTextures(1, &prlookups[i]);
            prlookups[i] = 0;
        }

        i++;
    }
}

// SECTORS
static int32_t      polymer_initsector(int16_t sectnum)
{
    usectorptr_t sec;
    _prsector*      s;

    if (pr_verbosity >= 2) OSD_Printf("PR : Initializing sector %i...\n", sectnum);

    sec = (usectorptr_t)&sector[sectnum];
    s = (_prsector *)Xcalloc(1, sizeof(_prsector));

    s->verts = (GLdouble *)Xcalloc(sec->wallnum, sizeof(GLdouble) * 3);
    s->floor.buffer = (_prvert *)Xcalloc(sec->wallnum, sizeof(_prvert));
    s->floor.vertcount = sec->wallnum;
    s->ceil.buffer = (_prvert *)Xcalloc(sec->wallnum, sizeof(_prvert));
    s->ceil.vertcount = sec->wallnum;

    glGenBuffers(1, &s->floor.vbo);
    glGenBuffers(1, &s->ceil.vbo);
    glGenBuffers(1, &s->floor.ivbo);
    glGenBuffers(1, &s->ceil.ivbo);

    glBindBuffer(GL_ARRAY_BUFFER, s->floor.vbo);
    glBufferData(GL_ARRAY_BUFFER, sec->wallnum * sizeof(GLfloat) * 5, NULL, mapvbousage);

    glBindBuffer(GL_ARRAY_BUFFER, s->ceil.vbo);
    glBufferData(GL_ARRAY_BUFFER, sec->wallnum * sizeof(GLfloat) * 5, NULL, mapvbousage);

    glBindBuffer(GL_ARRAY_BUFFER, 0);

    s->flags.empty = 1; // let updatesector know that everything needs to go

    prsectors[sectnum] = s;

    if (pr_verbosity >= 2) OSD_Printf("PR : Initialized sector %i.\n", sectnum);

    return 1;
}

static int32_t      polymer_updatesector(int16_t sectnum)
{
    _prsector*      s;
    usectorptr_t sec;
    walltype        *wal;
    int32_t         i, j;
    int32_t         ceilz, florz;
    int32_t         heidiff;
    float           tex, tey;
    float           secangcos, secangsin, scalecoef, xpancoef, ypancoef;
    int32_t         needfloor, wallinvalidate;
    int16_t         curstat, curpicnum, floorpicnum, ceilingpicnum;
    char            curxpanning, curypanning;
    _prvert*        curbuffer;
    float           relscalefactorfloor = 0.0, relscalefactorceil = 0.0, currelscalefactor;

    if (pr_nullrender >= 3) return 0;

    s = prsectors[sectnum];
    sec = (usectorptr_t)&sector[sectnum];

    secangcos = secangsin = 2;

    if (s == NULL)
    {
        if (pr_verbosity >= 1) OSD_Printf("PR : Can't update uninitialized sector %i.\n", sectnum);
        return -1;
    }

    needfloor = wallinvalidate = 0;

    // geometry
    wal = &wall[sec->wallptr];
    i = 0;
    while (i < sec->wallnum)
    {
        if ((-wal->x != s->verts[(i*3)+2]))
        {
            s->verts[(i*3)+2] = s->floor.buffer[i].z = s->ceil.buffer[i].z = -(float)wal->x;
            needfloor = wallinvalidate = 1;
        }
        if ((wal->y != s->verts[i*3]))
        {
            s->verts[i*3] = s->floor.buffer[i].x = s->ceil.buffer[i].x = (float)wal->y;
            needfloor = wallinvalidate = 1;
        }

        i++;
        wal = &wall[sec->wallptr + i];
    }

    if ((s->flags.empty) ||
            needfloor ||
            (sec->floorz != s->floorz) ||
            (sec->ceilingz != s->ceilingz) ||
            (sec->floorheinum != s->floorheinum) ||
            (sec->ceilingheinum != s->ceilingheinum))
    {
        wallinvalidate = 1;

        wal = &wall[sec->wallptr];
        i = 0;
        while (i < sec->wallnum)
        {
            getzsofslope(sectnum, wal->x, wal->y, &ceilz, &florz);
            s->floor.buffer[i].y = -(float)(florz) / 16.0f;
            s->ceil.buffer[i].y = -(float)(ceilz) / 16.0f;

            i++;
            wal = &wall[sec->wallptr + i];
        }

        s->floorz = sec->floorz;
        s->ceilingz = sec->ceilingz;
        s->floorheinum = sec->floorheinum;
        s->ceilingheinum = sec->ceilingheinum;
    }
    else if (sec->visibility != s->visibility)
        wallinvalidate = 1;

    floorpicnum = sec->floorpicnum;
    tileUpdatePicnum(&floorpicnum, sectnum);
    ceilingpicnum = sec->ceilingpicnum;
    tileUpdatePicnum(&ceilingpicnum, sectnum);

    if ((!s->flags.empty) && (!needfloor) &&
            (floorpicnum == s->floorpicnum_anim) &&
            (ceilingpicnum == s->ceilingpicnum_anim) &&
#ifdef USE_STRUCT_TRACKERS
            (s->trackedrev == sectorchanged[sectnum]))
#else
            !Bmemcmp(&s->ceilingstat, &sec->ceilingstat, offsetof(sectortype, visibility) - offsetof(sectortype, ceilingstat)))
#endif
        goto attributes;

    if (((sec->floorstat & 64) || (sec->ceilingstat & 64)) &&
        ((secangcos == 2) && (secangsin == 2)))
    {
        // relative-aligned floors apparently have a scaling bug in classic related to length of firstwall
        // lifting polymost code to replicate here
        vec2_t const xy = { wall[wall[sec->wallptr].point2].x - wall[sec->wallptr].x,
                            wall[wall[sec->wallptr].point2].y - wall[sec->wallptr].y };

        float len = Bsqrtf(xy.x * xy.x + xy.y * xy.y);

        if (sec->floorstat & 2 || sec->ceilingstat & 2)
        {
            int i = krecipasm(nsqrtasm(uhypsq(xy.x,xy.y)));
            float relscalefactor = i * (1.f/1073741824.f) * len;

            if (sec->floorstat & 2)
                relscalefactorfloor = relscalefactor;

            if (sec->ceilingstat & 2)
                relscalefactorceil = relscalefactor;
        }

        if (!(sec->floorstat & 2) || !(sec->ceilingstat & 2))
        {
            int i = nsqrtasm(uhypsq(xy.x,xy.y)); if (i == 0) i = 1024; else i = tabledivide32(1048576, i);
            float relscalefactor = i * (1.f/1048576.f) * len;

            if (!(sec->floorstat & 2))
                relscalefactorfloor = relscalefactor;

            if (!(sec->ceilingstat & 2))
                relscalefactorceil = relscalefactor;
        }

        // now sanely compute relative firstwall angle
        double arctan = atan2( xy.y, xy.x ) + M_PI / 2.0;
        secangcos = cos( arctan );
        secangsin = sin( arctan );
    }

    wal = &wall[sec->wallptr];
    i = 0;
    while (i < sec->wallnum)
    {
        j = 2;
        curstat = sec->floorstat;
        curbuffer = s->floor.buffer;
        curpicnum = floorpicnum;
        curxpanning = sec->floorxpanning;
        curypanning = sec->floorypanning;
        currelscalefactor = relscalefactorfloor;

        while (j)
        {
            if (j == 1)
            {
                curstat = sec->ceilingstat;
                curbuffer = s->ceil.buffer;
                curpicnum = ceilingpicnum;
                curxpanning = sec->ceilingxpanning;
                curypanning = sec->ceilingypanning;
                currelscalefactor = relscalefactorceil;
            }

            if (!waloff[curpicnum])
                tileLoad(curpicnum);

            // relative texturing
            if (curstat & 64)
            {
                xpancoef = (float)(wal->x - wall[sec->wallptr].x) * currelscalefactor;
                ypancoef = (float)(wall[sec->wallptr].y - wal->y) * currelscalefactor;

                tex = xpancoef * secangsin + ypancoef * secangcos;
                tey = xpancoef * secangcos - ypancoef * secangsin;
            } else {
                tex = wal->x;
                tey = -wal->y;
            }

            if ((curstat & (2+64)) == (2+64))
            {
                heidiff = (int32_t)(curbuffer[i].y - curbuffer[0].y);
                // don't forget the sign, tey could be negative with concave sectors
                if (tey >= 0)
                    tey = sqrt((double)((tey * tey) + (heidiff * heidiff)));
                else
                    tey = -sqrt((double)((tey * tey) + (heidiff * heidiff)));
            }

            if (curstat & 4)
                swapfloat(&tex, &tey);

            if (curstat & 16) tex = -tex;
            if (curstat & 32) tey = -tey;

            scalecoef = (curstat & 8) ? 8.0f : 16.0f;

            if (curxpanning)
            {
                xpancoef = (float)(pow2long[picsiz[curpicnum] & 15]);
                xpancoef *= (float)(curxpanning) / (256.0f * (float)(tilesiz[curpicnum].x));
            }
            else
                xpancoef = 0;

            if (curypanning)
            {
                ypancoef = (float)(pow2long[picsiz[curpicnum] >> 4]);
                ypancoef *= (float)(curypanning) / (256.0f * (float)(tilesiz[curpicnum].y));
            }
            else
                ypancoef = 0;

            curbuffer[i].u = (tex / (scalecoef * tilesiz[curpicnum].x)) + xpancoef;
            curbuffer[i].v = (tey / (scalecoef * tilesiz[curpicnum].y)) + ypancoef;

            j--;
        }
        i++;
        wal = &wall[sec->wallptr + i];
    }

    s->floorxpanning = sec->floorxpanning;
    s->ceilingxpanning = sec->ceilingxpanning;
    s->floorypanning = sec->floorypanning;
    s->ceilingypanning = sec->ceilingypanning;
#ifdef USE_STRUCT_TRACKERS
    s->trackedrev = sectorchanged[sectnum];
#endif

    i = -1;

attributes:
    if ((pr_vbos > 0) && ((i == -1) || (wallinvalidate)))
    {
        if (pr_vbos > 0)
        {
            if (pr_nullrender < 2)
            {
                /*glBindBuffer(GL_ARRAY_BUFFER, s->floor.vbo);
                glBufferSubData(GL_ARRAY_BUFFER, 0, sec->wallnum * sizeof(GLfloat)* 5, s->floor.buffer);
                glBindBuffer(GL_ARRAY_BUFFER, s->ceil.vbo);
                glBufferSubData(GL_ARRAY_BUFFER, 0, sec->wallnum * sizeof(GLfloat)* 5, s->ceil.buffer);
                */

                s->floor.mapvbo_vertoffset = sec->wallptr * 2;
                s->ceil.mapvbo_vertoffset = s->floor.mapvbo_vertoffset + sec->wallnum;

                GLintptr sector_offset = s->floor.mapvbo_vertoffset * sizeof(_prvert);
                GLsizeiptr cur_sector_size = sec->wallnum * sizeof(_prvert);
                glBindBuffer(GL_ARRAY_BUFFER, prmapvbo);
                // floor
                glBufferSubData(GL_ARRAY_BUFFER, sector_offset, cur_sector_size, s->floor.buffer);
                // ceiling
                glBufferSubData(GL_ARRAY_BUFFER, sector_offset + cur_sector_size, cur_sector_size, s->ceil.buffer);
                glBindBuffer(GL_ARRAY_BUFFER, 0);
            }
        }
        else
        {
            s->floor.mapvbo_vertoffset = -1;
            s->ceil.mapvbo_vertoffset = -1;
        }
    }

    if ((!s->flags.empty) && (!s->flags.invalidtex) &&
            (floorpicnum == s->floorpicnum_anim) &&
            (ceilingpicnum == s->ceilingpicnum_anim) &&
            !Bmemcmp(&s->ceilingstat, &sec->ceilingstat, offsetof(sectortype, fogpal) - offsetof(sectortype, ceilingstat)))
        goto finish;

    s->floor.bucket = polymer_getbuildmaterial(&s->floor.material, floorpicnum, sec->floorpal, sec->floorshade, sec->visibility, (sec->floorstat & 384) ? DAMETH_MASK : DAMETH_NOMASK);

    if (sec->floorstat & 256) {
        if (sec->floorstat & 128) {
            s->floor.material.diffusemodulation[3] = 0x55;
        } else {
            s->floor.material.diffusemodulation[3] = 0xAA;
        }
    }

    s->ceil.bucket = polymer_getbuildmaterial(&s->ceil.material, ceilingpicnum, sec->ceilingpal, sec->ceilingshade, sec->visibility, (sec->ceilingstat & 384) ? DAMETH_MASK : DAMETH_NOMASK);

    if (sec->ceilingstat & 256) {
        if (sec->ceilingstat & 128) {
            s->ceil.material.diffusemodulation[3] = 0x55;
        } else {
            s->ceil.material.diffusemodulation[3] = 0xAA;
        }
    }

    s->flags.invalidtex = 0;

    // copy ceilingstat through visibility members
    Bmemcpy(&s->ceilingstat, &sec->ceilingstat, offsetof(sectortype, fogpal) - offsetof(sectortype, ceilingstat));
    s->floorpicnum_anim = floorpicnum;
    s->ceilingpicnum_anim = ceilingpicnum;

finish:

    if (needfloor)
    {
        polymer_buildfloor(sectnum);
        if ((pr_vbos > 0))
        {
            if (pr_nullrender < 2)
            {
                if (s->oldindicescount < s->indicescount)
                {
                    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, s->floor.ivbo);
                    glBufferData(GL_ELEMENT_ARRAY_BUFFER, s->indicescount * sizeof(GLushort), NULL, mapvbousage);
                    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, s->ceil.ivbo);
                    glBufferData(GL_ELEMENT_ARRAY_BUFFER, s->indicescount * sizeof(GLushort), NULL, mapvbousage);
                    s->oldindicescount = s->indicescount;
                }
                glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, s->floor.ivbo);
                glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, s->indicescount * sizeof(GLushort), s->floor.indices);
                glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, s->ceil.ivbo);
                glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, s->indicescount * sizeof(GLushort), s->ceil.indices);
                glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
            }
        }
    }

    if (wallinvalidate)
    {
        s->invalidid++;
        polymer_invalidatesectorlights(sectnum);
        polymer_computeplane(&s->floor);
        polymer_computeplane(&s->ceil);
    }

    s->flags.empty = 0;
    s->flags.uptodate = 1;

    if (pr_verbosity >= 3) OSD_Printf("PR : Updated sector %i.\n", sectnum);

    return 0;
}

void PR_CALLBACK    polymer_tesserror(GLenum error)
{
    /* This callback is called by the tesselator whenever it raises an error.
       GLU_TESS_ERROR6 is the "no error"/"null" error spam in e1l1 and others. */

    if (pr_verbosity >= 1 && error != GLU_TESS_ERROR6) OSD_Printf("PR : Tesselation error number %i reported : %s.\n", error, bgluErrorString(errno));
}

void PR_CALLBACK    polymer_tessedgeflag(GLenum error)
{
    // Passing an edgeflag callback forces the tesselator to output a triangle list
    UNREFERENCED_PARAMETER(error);
}

void PR_CALLBACK    polymer_tessvertex(void* vertex, void* sector)
{
    _prsector*      s;

    s = (_prsector*)sector;

    if (s->curindice >= s->indicescount)
    {
        if (pr_verbosity >= 2) OSD_Printf("PR : Indice overflow, extending the indices list... !\n");
        s->indicescount++;
        s->floor.indices = (GLushort *)Xrealloc(s->floor.indices, s->indicescount * sizeof(GLushort));
        s->ceil.indices = (GLushort *)Xrealloc(s->ceil.indices, s->indicescount * sizeof(GLushort));
    }
    s->ceil.indices[s->curindice] = (intptr_t)vertex;
    s->curindice++;
}

static int32_t      polymer_buildfloor(int16_t sectnum)
{
    // This function tesselates the floor/ceiling of a sector and stores the triangles in a display list.
    _prsector*      s;
    usectorptr_t sec;
    intptr_t        i;

    if (pr_verbosity >= 2) OSD_Printf("PR : Tesselating floor of sector %i...\n", sectnum);

    s = prsectors[sectnum];
    sec = (usectorptr_t)&sector[sectnum];

    if (s == NULL)
        return -1;

    if (s->floor.indices == NULL)
    {
        s->indicescount = (max<int16_t>(3, sec->wallnum) - 2) * 3;
        s->floor.indices = (GLushort *)Xcalloc(s->indicescount, sizeof(GLushort));
        s->ceil.indices = (GLushort *)Xcalloc(s->indicescount, sizeof(GLushort));
    }

    s->curindice = 0;

    bgluTessCallback(prtess, GLU_TESS_VERTEX_DATA, (void (PR_CALLBACK *)(void))polymer_tessvertex);
    bgluTessCallback(prtess, GLU_TESS_EDGE_FLAG, (void (PR_CALLBACK *)(void))polymer_tessedgeflag);
    bgluTessCallback(prtess, GLU_TESS_ERROR, (void (PR_CALLBACK *)(void))polymer_tesserror);

    bgluTessProperty(prtess, GLU_TESS_WINDING_RULE, GLU_TESS_WINDING_POSITIVE);

    bgluTessBeginPolygon(prtess, s);
    bgluTessBeginContour(prtess);

    i = 0;
    while (i < sec->wallnum)
    {
        bgluTessVertex(prtess, s->verts + (3 * i), (void *)i);
        if ((i != (sec->wallnum - 1)) && ((sec->wallptr + i) > wall[sec->wallptr + i].point2))
        {
            bgluTessEndContour(prtess);
            bgluTessBeginContour(prtess);
        }
        i++;
    }
    bgluTessEndContour(prtess);
    bgluTessEndPolygon(prtess);

    i = 0;
    while (i < s->indicescount)
    {
        s->floor.indices[s->indicescount - i - 1] = s->ceil.indices[i];

        i++;
    }
    s->floor.indicescount = s->ceil.indicescount = s->indicescount;

    if (pr_verbosity >= 2) OSD_Printf("PR : Tesselated floor of sector %i.\n", sectnum);

    return 1;
}

static void         polymer_drawsector(int16_t sectnum, int32_t domasks)
{
    usectorptr_t sec;
    _prsector*      s;
    GLubyte         oldcolor[4];
    int32_t         draw;
    int32_t         queuedmask;

    if (pr_verbosity >= 3) OSD_Printf("PR : Drawing sector %i...\n", sectnum);

    if (automapping)
        show2dsector[sectnum>>3] |= pow2char[sectnum&7];

    gotsector[sectnum>>3] |= pow2char[sectnum&7];

    sec = (usectorptr_t)&sector[sectnum];
    s = prsectors[sectnum];

    queuedmask = FALSE;

    // If you're thinking of 'optimizing' the following logic, you'd better
    // provide compelling evidence that the generated code is more efficient
    // than what GCC can come up with on its own.

    draw = TRUE;
    // Draw masks regardless; avoid all non-masks TROR links
    if (sec->floorstat & 384) {
        draw = domasks;
    } else if (yax_getbunch(sectnum, YAX_FLOOR) >= 0) {
        draw = FALSE;
    }
    // Parallaxed
    if (sec->floorstat & 1) {
        draw = FALSE;
    }

    if (draw || (searchit == 2)) {
        if (searchit == 2) {
            polymer_drawsearchplane(&s->floor, oldcolor, 0x02, (GLubyte *) &sectnum);
        }
        else {
            calc_and_apply_fog(fogshade(sec->floorshade, sec->floorpal), sec->visibility,
                get_floor_fogpal(sec));
            polymer_drawplane(&s->floor);
        }
    } else if (!domasks && cursectormaskcount && sec->floorstat & 384) {
        // If we just skipped a mask, queue it for later
        cursectormasks[(*cursectormaskcount)++] = sectnum;
        // Don't queue it twice if the ceiling is also a mask, though.
        queuedmask = TRUE;
    }

    draw = TRUE;
    // Draw masks regardless; avoid all non-masks TROR links
    if (sec->ceilingstat & 384) {
        draw = domasks;
    } else if (yax_getbunch(sectnum, YAX_CEILING) >= 0) {
        draw = FALSE;
    }
    // Parallaxed
    if (sec->ceilingstat & 1) {
        draw = FALSE;
    }

    if (draw || (searchit == 2)) {
        if (searchit == 2) {
            polymer_drawsearchplane(&s->ceil, oldcolor, 0x01, (GLubyte *) &sectnum);
        }
        else {
            calc_and_apply_fog(fogshade(sec->ceilingshade, sec->ceilingpal), sec->visibility,
                               get_ceiling_fogpal(sec));
            polymer_drawplane(&s->ceil);
        }
    } else if (!domasks && !queuedmask && cursectormaskcount &&
               (sec->ceilingstat & 384)) {
        // If we just skipped a mask, queue it for later
        cursectormasks[(*cursectormaskcount)++] = sectnum;
    }

    if (pr_verbosity >= 3) OSD_Printf("PR : Finished drawing sector %i...\n", sectnum);
}

// WALLS
static int32_t      polymer_initwall(int16_t wallnum)
{
    _prwall         *w;

    if (pr_verbosity >= 2) OSD_Printf("PR : Initializing wall %i...\n", wallnum);

    w = (_prwall *)Xcalloc(1, sizeof(_prwall));

    if (w->mask.buffer == NULL) {
        w->mask.buffer = (_prvert *)Xmalloc(4 * sizeof(_prvert));
        w->mask.vertcount = 4;
    }
    if (w->bigportal == NULL)
        w->bigportal = (GLfloat *)Xmalloc(4 * sizeof(GLfloat) * 5);
    //if (w->cap == NULL)
    //    w->cap = (GLfloat *)Xmalloc(4 * sizeof(GLfloat) * 3);

    glGenBuffers(1, &w->wall.vbo);
    glGenBuffers(1, &w->over.vbo);
    glGenBuffers(1, &w->mask.vbo);
    glGenBuffers(1, &w->stuffvbo);

    glBindBuffer(GL_ARRAY_BUFFER, w->wall.vbo);
    glBufferData(GL_ARRAY_BUFFER, 4 * sizeof(GLfloat) * 5, NULL, mapvbousage);

    glBindBuffer(GL_ARRAY_BUFFER, w->over.vbo);
    glBufferData(GL_ARRAY_BUFFER, 4 * sizeof(GLfloat) * 5, NULL, mapvbousage);

    glBindBuffer(GL_ARRAY_BUFFER, w->mask.vbo);
    glBufferData(GL_ARRAY_BUFFER, 4 * sizeof(GLfloat) * 5, NULL, mapvbousage);

    glBindBuffer(GL_ARRAY_BUFFER, w->stuffvbo);
    glBufferData(GL_ARRAY_BUFFER, 8 * sizeof(GLfloat) * 5, NULL, mapvbousage);

    glBindBuffer(GL_ARRAY_BUFFER, 0);

    w->flags.empty = 1;

    prwalls[wallnum] = w;

    if (pr_verbosity >= 2) OSD_Printf("PR : Initialized wall %i.\n", wallnum);

    return 1;
}

// TODO: r_npotwallmode. Needs polymost_is_npotmode() handling among others.
#define DAMETH_WALL 0

static float calc_ypancoef(char curypanning, int16_t curpicnum, int32_t dopancor)
{
#ifdef NEW_MAP_FORMAT
    if (g_loadedMapVersion >= 10)
        return curypanning / 256.0f;
#endif
    {
        float ypancoef = (float)(pow2long[picsiz[curpicnum] >> 4]);

        if (ypancoef < tilesiz[curpicnum].y)
            ypancoef *= 2;

        if (dopancor)
        {
            int32_t yoffs = Blrintf((ypancoef - tilesiz[curpicnum].y) * (255.0f / ypancoef));
            if (curypanning > 256 - yoffs)
                curypanning -= yoffs;
        }

        ypancoef *= (float)curypanning / (256.0f * (float)tilesiz[curpicnum].y);

        return ypancoef;
    }
}

#define NBYTES_WALL_CSTAT_THROUGH_YPANNING \
    (offsetof(walltype, ypanning)+sizeof(wall[0].ypanning) - offsetof(walltype, cstat))

static void         polymer_updatewall(int16_t wallnum)
{
    int16_t         nwallnum, nnwallnum, curpicnum, wallpicnum, walloverpicnum, nwallpicnum;
    char            curxpanning, curypanning, underwall, overwall, curpal;
    int8_t          curshade;
    walltype        *wal;
    sectortype      *sec, *nsec;
    _prwall         *w;
    _prsector       *s, *ns;
    int32_t         xref, yref;
    float           ypancoef, dist;
    int32_t         i;
    uint32_t        invalid;
    int32_t         sectofwall = sectorofwall(wallnum);

    if (pr_nullrender >= 3) return;

    // yes, this function is messy and unefficient
    // it also works, bitches
    sec = &sector[sectofwall];

    if (sectofwall < 0 || sectofwall >= numsectors ||
        wallnum < 0 || wallnum > numwalls ||
        sec->wallptr > wallnum || wallnum >= (sec->wallptr + sec->wallnum))
        return; // yay, corrupt map

    wal = &wall[wallnum];
    nwallnum = wal->nextwall;

    w = prwalls[wallnum];
    s = prsectors[sectofwall];
    invalid = s->invalidid;
    if (nwallnum >= 0 && nwallnum < numwalls && wal->nextsector >= 0 && wal->nextsector < numsectors)
    {
        ns = prsectors[wal->nextsector];
        invalid += ns->invalidid;
        nsec = &sector[wal->nextsector];
    }
    else
    {
        ns = NULL;
        nsec = NULL;
    }

    if (w->wall.buffer == NULL) {
        w->wall.buffer = (_prvert *)Xcalloc(4, sizeof(_prvert));  // XXX
        w->wall.vertcount = 4;
    }

    wallpicnum = wal->picnum;
    tileUpdatePicnum(&wallpicnum, wallnum+16384);

    walloverpicnum = wal->overpicnum;
    if (walloverpicnum>=0)
        tileUpdatePicnum(&walloverpicnum, wallnum+16384);

    if (nwallnum >= 0 && nwallnum < numwalls)
    {
        nwallpicnum = wall[nwallnum].picnum;
        tileUpdatePicnum(&nwallpicnum, wallnum+16384);
    }
    else
        nwallpicnum = 0;

    if ((!w->flags.empty) && (!w->flags.invalidtex) &&
            (w->invalidid == invalid) &&
            (wallpicnum == w->picnum_anim) &&
            (walloverpicnum == w->overpicnum_anim) &&
#ifdef USE_STRUCT_TRACKERS
            (w->trackedrev == wallchanged[wallnum]) &&
#else
            !Bmemcmp(&wal->cstat, &w->cstat, NBYTES_WALL_CSTAT_THROUGH_YPANNING) &&
#endif
            ((nwallnum < 0 || nwallnum > numwalls) ||
             ((nwallpicnum == w->nwallpicnum) &&
              (wall[nwallnum].xpanning == w->nwallxpanning) &&
              (wall[nwallnum].ypanning == w->nwallypanning) &&
              (wall[nwallnum].cstat == w->nwallcstat) &&
              (wall[nwallnum].shade == w->nwallshade))))
    {
        w->flags.uptodate = 1;
        return; // screw you guys I'm going home
    }
    else
    {
        w->invalidid = invalid;

        Bmemcpy(&w->cstat, &wal->cstat, NBYTES_WALL_CSTAT_THROUGH_YPANNING);

        w->picnum_anim = wallpicnum;
        w->overpicnum_anim = walloverpicnum;
#ifdef USE_STRUCT_TRACKERS
        w->trackedrev = wallchanged[wallnum];
#endif
        if (nwallnum >= 0 && nwallnum < numwalls)
        {
            w->nwallpicnum = nwallpicnum;
            w->nwallxpanning = wall[nwallnum].xpanning;
            w->nwallypanning = wall[nwallnum].ypanning;
            w->nwallcstat = wall[nwallnum].cstat;
            w->nwallshade = wall[nwallnum].shade;
        }
    }

    w->underover = underwall = overwall = 0;

    if (wal->cstat & 8)
        xref = 1;
    else
        xref = 0;

    if ((unsigned)wal->nextsector >= (unsigned)numsectors || !ns)
    {
        Bmemcpy(w->wall.buffer, &s->floor.buffer[wallnum - sec->wallptr], sizeof(GLfloat) * 3);
        Bmemcpy(&w->wall.buffer[1], &s->floor.buffer[wal->point2 - sec->wallptr], sizeof(GLfloat) * 3);
        Bmemcpy(&w->wall.buffer[2], &s->ceil.buffer[wal->point2 - sec->wallptr], sizeof(GLfloat) * 3);
        Bmemcpy(&w->wall.buffer[3], &s->ceil.buffer[wallnum - sec->wallptr], sizeof(GLfloat) * 3);

        if (wal->nextsector < 0)
            curpicnum = wallpicnum;
        else
            curpicnum = walloverpicnum;

        w->wall.bucket = polymer_getbuildmaterial(&w->wall.material, curpicnum, wal->pal, wal->shade, sec->visibility, DAMETH_WALL);

        if (wal->cstat & 4)
            yref = sec->floorz;
        else
            yref = sec->ceilingz;

        if ((wal->cstat & 32) && (wal->nextsector >= 0))
        {
            if ((!(wal->cstat & 2) && (wal->cstat & 4)) || ((wal->cstat & 2) && (wall[nwallnum].cstat & 4)))
                yref = sec->ceilingz;
            else
                yref = nsec->floorz;
        }

        if (wal->ypanning)
            // white (but not 1-way)
            ypancoef = calc_ypancoef(wal->ypanning, curpicnum, !(wal->cstat & 4));
        else
            ypancoef = 0;

        i = 0;
        while (i < 4)
        {
            if ((i == 0) || (i == 3))
                dist = (float)xref;
            else
                dist = (float)(xref == 0);

            w->wall.buffer[i].u = ((dist * 8.0f * wal->xrepeat) + wal->xpanning) / (float)(tilesiz[curpicnum].x);
            w->wall.buffer[i].v = (-(float)(yref + (w->wall.buffer[i].y * 16)) / ((tilesiz[curpicnum].y * 2048.0f) / (float)(wal->yrepeat))) + ypancoef;

            if (wal->cstat & 256) w->wall.buffer[i].v = -w->wall.buffer[i].v;

            i++;
        }

        w->underover |= 1;
    }
    else
    {
        nnwallnum = wall[nwallnum].point2;

        if ((s->floor.buffer[wallnum - sec->wallptr].y < ns->floor.buffer[nnwallnum - nsec->wallptr].y) ||
            (s->floor.buffer[wal->point2 - sec->wallptr].y < ns->floor.buffer[nwallnum - nsec->wallptr].y))
            underwall = 1;

        if ((underwall) || (wal->cstat & 16) || (wal->cstat & 32))
        {
            int32_t refwall;

            if (s->floor.buffer[wallnum - sec->wallptr].y < ns->floor.buffer[nnwallnum - nsec->wallptr].y)
                Bmemcpy(w->wall.buffer, &s->floor.buffer[wallnum - sec->wallptr], sizeof(GLfloat) * 3);
            else
                Bmemcpy(w->wall.buffer, &ns->floor.buffer[nnwallnum - nsec->wallptr], sizeof(GLfloat) * 3);
            Bmemcpy(&w->wall.buffer[1], &s->floor.buffer[wal->point2 - sec->wallptr], sizeof(GLfloat) * 3);
            Bmemcpy(&w->wall.buffer[2], &ns->floor.buffer[nwallnum - nsec->wallptr], sizeof(GLfloat) * 3);
            Bmemcpy(&w->wall.buffer[3], &ns->floor.buffer[nnwallnum - nsec->wallptr], sizeof(GLfloat) * 3);

            if (wal->cstat & 2)
                refwall = nwallnum;
            else
                refwall = wallnum;

            curpicnum = (wal->cstat & 2) ? nwallpicnum : wallpicnum;
            curpal = wall[refwall].pal;
            curshade = wall[refwall].shade;
            curxpanning = wall[refwall].xpanning;
            curypanning = wall[refwall].ypanning;

            w->wall.bucket = polymer_getbuildmaterial(&w->wall.material, curpicnum, curpal, curshade, sec->visibility, DAMETH_WALL);

            if (!(wall[refwall].cstat&4))
                yref = nsec->floorz;
            else
                yref = sec->ceilingz;

            if (curypanning)
                // under
                ypancoef = calc_ypancoef(curypanning, curpicnum, !(wall[refwall].cstat & 4));
            else
                ypancoef = 0;

            i = 0;
            while (i < 4)
            {
                if ((i == 0) || (i == 3))
                    dist = (float)xref;
                else
                    dist = (float)(xref == 0);

                w->wall.buffer[i].u = ((dist * 8.0f * wal->xrepeat) + curxpanning) / (float)(tilesiz[curpicnum].x);
                w->wall.buffer[i].v = (-(float)(yref + (w->wall.buffer[i].y * 16)) / ((tilesiz[curpicnum].y * 2048.0f) / (float)(wal->yrepeat))) + ypancoef;

                if ((!(wal->cstat & 2) && (wal->cstat & 256)) ||
                    ((wal->cstat & 2) && (wall[nwallnum].cstat & 256)))
                    w->wall.buffer[i].v = -w->wall.buffer[i].v;

                i++;
            }

            if (underwall)
                w->underover |= 1;

            Bmemcpy(w->mask.buffer, &w->wall.buffer[3], sizeof(GLfloat) * 5);
            Bmemcpy(&w->mask.buffer[1], &w->wall.buffer[2], sizeof(GLfloat) * 5);
        }
        else
        {
            Bmemcpy(w->mask.buffer, &s->floor.buffer[wallnum - sec->wallptr], sizeof(GLfloat) * 5);
            Bmemcpy(&w->mask.buffer[1], &s->floor.buffer[wal->point2 - sec->wallptr], sizeof(GLfloat) * 5);
        }

        if ((s->ceil.buffer[wallnum - sec->wallptr].y > ns->ceil.buffer[nnwallnum - nsec->wallptr].y) ||
            (s->ceil.buffer[wal->point2 - sec->wallptr].y > ns->ceil.buffer[nwallnum - nsec->wallptr].y))
            overwall = 1;

        if ((overwall) || (wal->cstat & 48))
        {
            if (w->over.buffer == NULL) {
                w->over.buffer = (_prvert *)Xmalloc(4 * sizeof(_prvert));
                w->over.vertcount = 4;
            }

            Bmemcpy(w->over.buffer, &ns->ceil.buffer[nnwallnum - nsec->wallptr], sizeof(GLfloat) * 3);
            Bmemcpy(&w->over.buffer[1], &ns->ceil.buffer[nwallnum - nsec->wallptr], sizeof(GLfloat) * 3);
            if (s->ceil.buffer[wal->point2 - sec->wallptr].y > ns->ceil.buffer[nwallnum - nsec->wallptr].y)
                Bmemcpy(&w->over.buffer[2], &s->ceil.buffer[wal->point2 - sec->wallptr], sizeof(GLfloat) * 3);
            else
                Bmemcpy(&w->over.buffer[2], &ns->ceil.buffer[nwallnum - nsec->wallptr], sizeof(GLfloat) * 3);
            Bmemcpy(&w->over.buffer[3], &s->ceil.buffer[wallnum - sec->wallptr], sizeof(GLfloat) * 3);

            curpicnum = wallpicnum;

            w->over.bucket = polymer_getbuildmaterial(&w->over.material, curpicnum, wal->pal, wal->shade, sec->visibility, DAMETH_WALL);

            if (wal->cstat & 48)
            {
                // mask
                w->mask.bucket = polymer_getbuildmaterial(&w->mask.material, walloverpicnum, wal->pal, wal->shade, sec->visibility, DAMETH_WALL | ((wal->cstat & 48) == 48 ? DAMETH_NOMASK : DAMETH_MASK));

                if (wal->cstat & 128)
                {
                    if (wal->cstat & 512)
                        w->mask.material.diffusemodulation[3] = 0x55;
                    else
                        w->mask.material.diffusemodulation[3] = 0xAA;
                }
            }

            if (wal->cstat & 4)
                yref = sec->ceilingz;
            else
                yref = nsec->ceilingz;

            if (wal->ypanning)
                // over
                ypancoef = calc_ypancoef(wal->ypanning, curpicnum, wal->cstat & 4);
            else
                ypancoef = 0;

            i = 0;
            while (i < 4)
            {
                if ((i == 0) || (i == 3))
                    dist = (float)xref;
                else
                    dist = (float)(xref == 0);

                w->over.buffer[i].u = ((dist * 8.0f * wal->xrepeat) + wal->xpanning) / (float)(tilesiz[curpicnum].x);
                w->over.buffer[i].v = (-(float)(yref + (w->over.buffer[i].y * 16)) / ((tilesiz[curpicnum].y * 2048.0f) / (float)(wal->yrepeat))) + ypancoef;

                if (wal->cstat & 256) w->over.buffer[i].v = -w->over.buffer[i].v;

                i++;
            }

            if (overwall)
                w->underover |= 2;

            Bmemcpy(&w->mask.buffer[2], &w->over.buffer[1], sizeof(GLfloat) * 5);
            Bmemcpy(&w->mask.buffer[3], &w->over.buffer[0], sizeof(GLfloat) * 5);

            if ((wal->cstat & 16) || (wal->cstat & 32))
            {
                const int botSwap = (wal->cstat & 4);

                if (wal->cstat & 32)
                {
                    // 1-sided wall
                    if (nsec)
                        yref = botSwap ? sec->ceilingz : nsec->ceilingz;
                    else
                        yref = botSwap ? sec->floorz : sec->ceilingz;
                }
                else
                {
                    // masked wall
                    if (botSwap)
                        yref = min(sec->floorz, nsec->floorz);
                    else
                        yref = max(sec->ceilingz, nsec->ceilingz);
                }

                curpicnum = walloverpicnum;

                if (wal->ypanning)
                    // mask / 1-way
                    ypancoef = calc_ypancoef(wal->ypanning, curpicnum, 0);
                else
                    ypancoef = 0;

                i = 0;
                while (i < 4)
                {
                    if ((i == 0) || (i == 3))
                        dist = (float)xref;
                    else
                        dist = (float)(xref == 0);

                    w->mask.buffer[i].u = ((dist * 8.0f * wal->xrepeat) + wal->xpanning) / (float)(tilesiz[curpicnum].x);
                    w->mask.buffer[i].v = (-(float)(yref + (w->mask.buffer[i].y * 16)) / ((tilesiz[curpicnum].y * 2048.0f) / (float)(wal->yrepeat))) + ypancoef;

                    if (wal->cstat & 256) w->mask.buffer[i].v = -w->mask.buffer[i].v;

                    i++;
                }
            }
        }
        else
        {
            Bmemcpy(&w->mask.buffer[2], &s->ceil.buffer[wal->point2 - sec->wallptr], sizeof(GLfloat) * 5);
            Bmemcpy(&w->mask.buffer[3], &s->ceil.buffer[wallnum - sec->wallptr], sizeof(GLfloat) * 5);
        }
    }

    // make sure shade color handling is correct below XXX
    if (wal->nextsector < 0)
        Bmemcpy(w->mask.buffer, w->wall.buffer, sizeof(_prvert) * 4);

    Bmemcpy(w->bigportal, &s->floor.buffer[wallnum - sec->wallptr], sizeof(GLfloat) * 3);
    Bmemcpy(&w->bigportal[5], &s->floor.buffer[wal->point2 - sec->wallptr], sizeof(GLfloat) * 3);
    Bmemcpy(&w->bigportal[10], &s->ceil.buffer[wal->point2 - sec->wallptr], sizeof(GLfloat) * 3);
    Bmemcpy(&w->bigportal[15], &s->ceil.buffer[wallnum - sec->wallptr], sizeof(GLfloat) * 3);

    //Bmemcpy(&w->cap[0], &s->ceil.buffer[wallnum - sec->wallptr], sizeof(GLfloat) * 3);
    //Bmemcpy(&w->cap[3], &s->ceil.buffer[wal->point2 - sec->wallptr], sizeof(GLfloat) * 3);
    //Bmemcpy(&w->cap[6], &s->ceil.buffer[wal->point2 - sec->wallptr], sizeof(GLfloat) * 3);
    //Bmemcpy(&w->cap[9], &s->ceil.buffer[wallnum - sec->wallptr], sizeof(GLfloat) * 3);
    //w->cap[7] += 1048576; // this number is the result of 1048574 + 2
    //w->cap[10] += 1048576; // this one is arbitrary

    if (w->underover & 1)
        polymer_computeplane(&w->wall);
    if (w->underover & 2)
        polymer_computeplane(&w->over);
    polymer_computeplane(&w->mask);

    if ((pr_vbos > 0))
    {
        if (pr_nullrender < 2)
        {
            const GLintptr thiswalloffset = prwalldataoffset + (prwalldatasize * wallnum);
            const GLintptr thisoveroffset = thiswalloffset + proneplanesize;
            const GLintptr thismaskoffset = thisoveroffset + proneplanesize;
            glBindBuffer(GL_ARRAY_BUFFER, prmapvbo);
            glBufferSubData(GL_ARRAY_BUFFER, thiswalloffset, proneplanesize, w->wall.buffer);
            glBindBuffer(GL_ARRAY_BUFFER, prmapvbo);
            if (w->over.buffer)
                glBufferSubData(GL_ARRAY_BUFFER, thisoveroffset, proneplanesize, w->over.buffer);
            glBindBuffer(GL_ARRAY_BUFFER, prmapvbo);
            glBufferSubData(GL_ARRAY_BUFFER, thismaskoffset, proneplanesize, w->mask.buffer);
            glBindBuffer(GL_ARRAY_BUFFER, w->stuffvbo);
            glBufferSubData(GL_ARRAY_BUFFER, 0, 4 * sizeof(GLfloat)* 5, w->bigportal);
            //glBufferSubData(GL_ARRAY_BUFFER, 4 * sizeof(GLfloat)* 5, 4 * sizeof(GLfloat)* 3, w->cap);
            glBindBuffer(GL_ARRAY_BUFFER, 0);

            w->wall.mapvbo_vertoffset = thiswalloffset / sizeof(_prvert);
            w->over.mapvbo_vertoffset = thisoveroffset / sizeof(_prvert);
            w->mask.mapvbo_vertoffset = thismaskoffset / sizeof(_prvert);
        }
    }
    else
    {
        w->wall.mapvbo_vertoffset = -1;
        w->over.mapvbo_vertoffset = -1;
        w->mask.mapvbo_vertoffset = -1;
    }

    w->flags.empty = 0;
    w->flags.uptodate = 1;
    w->flags.invalidtex = 0;

    if (pr_verbosity >= 3) OSD_Printf("PR : Updated wall %i.\n", wallnum);
}

static void         polymer_drawwall(int16_t sectnum, int16_t wallnum)
{
    usectorptr_t sec;
    walltype        *wal;
    _prwall         *w;
    GLubyte         oldcolor[4];
    int32_t         parallaxedfloor = 0, parallaxedceiling = 0;

    if (pr_verbosity >= 3) OSD_Printf("PR : Drawing wall %i...\n", wallnum);

    sec = (usectorptr_t)&sector[sectnum];
    wal = &wall[wallnum];
    w = prwalls[wallnum];

    if ((sec->floorstat & 1) && (wal->nextsector >= 0) &&
        (sector[wal->nextsector].floorstat & 1))
        parallaxedfloor = 1;

    if ((sec->ceilingstat & 1) && (wal->nextsector >= 0) &&
        (sector[wal->nextsector].ceilingstat & 1))
        parallaxedceiling = 1;

    calc_and_apply_fog(fogshade(wal->shade, wal->pal), sec->visibility, get_floor_fogpal(sec));

    if ((w->underover & 1) && (!parallaxedfloor || (searchit == 2)))
    {
        if (searchit == 2) {
            polymer_drawsearchplane(&w->wall, oldcolor, 0x05, (GLubyte *) &wallnum);
        }
        else
            polymer_drawplane(&w->wall);
    }

    if ((w->underover & 2) && (!parallaxedceiling || (searchit == 2)))
    {
        if (searchit == 2) {
            polymer_drawsearchplane(&w->over, oldcolor, 0x00, (GLubyte *) &wallnum);
        }
        else
            polymer_drawplane(&w->over);
    }

    if ((wall[wallnum].cstat & 32) && (wall[wallnum].nextsector >= 0))
    {
        if (searchit == 2) {
            polymer_drawsearchplane(&w->mask, oldcolor, 0x04, (GLubyte *) &wallnum);
        }
        else
            polymer_drawplane(&w->mask);
    }

    //if (!searchit && (sector[sectnum].ceilingstat & 1) &&
    //    ((wall[wallnum].nextsector < 0) ||
    //    !(sector[wall[wallnum].nextsector].ceilingstat & 1)))
    //{
    //    glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);

    //    if (pr_vbos)
    //    {
    //        glBindBuffer(GL_ARRAY_BUFFER, w->stuffvbo);
    //        glVertexPointer(3, GL_FLOAT, 0, (const GLvoid*)(4 * sizeof(GLfloat) * 5));
    //    }
    //    else
    //        glVertexPointer(3, GL_FLOAT, 0, w->cap);

    //    glDrawArrays(GL_QUADS, 0, 4);

    //    if (pr_vbos)
    //        glBindBuffer(GL_ARRAY_BUFFER, 0);

    //    glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
    //}

    if (automapping)
        show2dwall[wallnum>>3] |= pow2char[wallnum&7];

    if (pr_verbosity >= 3) OSD_Printf("PR : Finished drawing wall %i...\n", wallnum);
}

// HSR
static void         polymer_computeplane(_prplane* p)
{
    GLfloat         vec1[5], vec2[5], norm, r;// BxN[3], NxT[3], TxB[3];
    int32_t         i;
    _prvert*        buffer;
    GLfloat*        plane;

    if (p->indices && (p->indicescount < 3))
        return; // corrupt sector (E3L4, I'm looking at you)

    buffer = p->buffer;
    plane = p->plane;

    i = 0;
    do
    {
        vec1[0] = buffer[(INDICE(1))].x - buffer[(INDICE(0))].x; //x1
        vec1[1] = buffer[(INDICE(1))].y - buffer[(INDICE(0))].y; //y1
        vec1[2] = buffer[(INDICE(1))].z - buffer[(INDICE(0))].z; //z1
        vec1[3] = buffer[(INDICE(1))].u - buffer[(INDICE(0))].u; //s1
        vec1[4] = buffer[(INDICE(1))].v - buffer[(INDICE(0))].v; //t1

        vec2[0] = buffer[(INDICE(2))].x - buffer[(INDICE(1))].x; //x2
        vec2[1] = buffer[(INDICE(2))].y - buffer[(INDICE(1))].y; //y2
        vec2[2] = buffer[(INDICE(2))].z - buffer[(INDICE(1))].z; //z2
        vec2[3] = buffer[(INDICE(2))].u - buffer[(INDICE(1))].u; //s2
        vec2[4] = buffer[(INDICE(2))].v - buffer[(INDICE(1))].v; //t2

        polymer_crossproduct(vec2, vec1, plane);

        norm = plane[0] * plane[0] + plane[1] * plane[1] + plane[2] * plane[2];

        // hack to work around a precision issue with slopes
        if (norm >= 15000)
        {
            float tangent[3][3];
            double det;

            // normalize the normal/plane equation and calculate its plane norm
            norm = -sqrt(norm);
            norm = 1.0 / norm;
            plane[0] *= norm;
            plane[1] *= norm;
            plane[2] *= norm;
            plane[3] = -(plane[0] * buffer->x + plane[1] * buffer->y + plane[2] * buffer->z);

            // calculate T and B
            r = 1.0 / (vec1[3] * vec2[4] - vec2[3] * vec1[4]);

            // tangent
            tangent[0][0] = (vec2[4] * vec1[0] - vec1[4] * vec2[0]) * r;
            tangent[0][1] = (vec2[4] * vec1[1] - vec1[4] * vec2[1]) * r;
            tangent[0][2] = (vec2[4] * vec1[2] - vec1[4] * vec2[2]) * r;

            polymer_normalize(&tangent[0][0]);

            // bitangent
            tangent[1][0] = (vec1[3] * vec2[0] - vec2[3] * vec1[0]) * r;
            tangent[1][1] = (vec1[3] * vec2[1] - vec2[3] * vec1[1]) * r;
            tangent[1][2] = (vec1[3] * vec2[2] - vec2[3] * vec1[2]) * r;

            polymer_normalize(&tangent[1][0]);

            // normal
            tangent[2][0] = plane[0];
            tangent[2][1] = plane[1];
            tangent[2][2] = plane[2];

            INVERT_3X3(p->tbn, det, tangent);

            break;
        }
        i+= (p->indices) ? 3 : 1;
    }
    while ((p->indices && i < p->indicescount) ||
          (!p->indices && i < p->vertcount));
}

static inline void  polymer_crossproduct(const GLfloat* in_a, const GLfloat* in_b, GLfloat* out)
{
    out[0] = in_a[1] * in_b[2] - in_a[2] * in_b[1];
    out[1] = in_a[2] * in_b[0] - in_a[0] * in_b[2];
    out[2] = in_a[0] * in_b[1] - in_a[1] * in_b[0];
}

static inline void  polymer_transformpoint(const float* inpos, float* pos, const float* matrix)
{
    pos[0] = inpos[0] * matrix[0] +
             inpos[1] * matrix[4] +
             inpos[2] * matrix[8] +
                      + matrix[12];
    pos[1] = inpos[0] * matrix[1] +
             inpos[1] * matrix[5] +
             inpos[2] * matrix[9] +
                      + matrix[13];
    pos[2] = inpos[0] * matrix[2] +
             inpos[1] * matrix[6] +
             inpos[2] * matrix[10] +
                      + matrix[14];
}

static inline void  polymer_normalize(float* vec)
{
    double norm;

    norm = vec[0] * vec[0] + vec[1] * vec[1] + vec[2] * vec[2];

    norm = sqrt(norm);
    norm = 1.0 / norm;
    vec[0] *= norm;
    vec[1] *= norm;
    vec[2] *= norm;
}

static inline void  polymer_pokesector(int16_t sectnum)
{
    sectortype      *sec = &sector[sectnum];
    _prsector       *s = prsectors[sectnum];
    walltype        *wal = &wall[sec->wallptr];
    int32_t         i = 0;

    if (!s->flags.uptodate)
        polymer_updatesector(sectnum);

    do
    {
        if ((wal->nextsector >= 0) && (!prsectors[wal->nextsector]->flags.uptodate))
            polymer_updatesector(wal->nextsector);
        if (!prwalls[sec->wallptr + i]->flags.uptodate)
            polymer_updatewall(sec->wallptr + i);

        i++;
        wal = &wall[sec->wallptr + i];
    }
    while (i < sec->wallnum);
}

static void         polymer_extractfrustum(GLfloat* modelview, GLfloat* projection, float* frustum)
{
    GLfloat         matrix[16];
    int32_t         i;

    glMatrixMode(GL_TEXTURE);
    glLoadMatrixf(projection);
    glMultMatrixf(modelview);
    glGetFloatv(GL_TEXTURE_MATRIX, matrix);
    glLoadIdentity();
    glMatrixMode(GL_MODELVIEW);

    i = 0;
    do
    {
        uint32_t ii = i<<2, iii = (i<<2) + 3;

        frustum[i] = matrix[iii] + matrix[ii];               // left
        frustum[i + 4] = matrix[iii] - matrix[ii];           // right
        frustum[i + 8] = matrix[iii] - matrix[ii + 1];     // top
        frustum[i + 12] = matrix[iii] + matrix[ii + 1];    // bottom
        frustum[i + 16] = matrix[iii] - matrix[ii + 2];    // far
    }
    while (++i < 4);

    if (pr_verbosity >= 3) OSD_Printf("PR : Frustum extracted.\n");
}

static inline int32_t polymer_planeinfrustum(_prplane *plane, const float* frustum)
{
    int32_t         i, j, k = -1;
    i = 4;


    do
    {
        int32_t ii = i * 4;
        j = k = plane->vertcount - 1;

        do
        {
            k -= ((frustum[ii + 0] * plane->buffer[j].x +
                   frustum[ii + 1] * plane->buffer[j].y +
                   frustum[ii + 2] * plane->buffer[j].z +
                   frustum[ii + 3]) < 0.f);
        }
        while (j--);

        if (k == -1)
            return 0; // OUT !
    }
    while (i--);

    return 1;
}

static inline void  polymer_scansprites(int16_t sectnum, tspriteptr_t localtsprite, int32_t* localspritesortcnt)
{
    int32_t         i;
    spritetype      *spr;

    for (i = headspritesect[sectnum];i >=0;i = nextspritesect[i])
    {
        spr = &sprite[i];
        if ((((spr->cstat&0x8000) == 0) || (showinvisibility)) &&
                (spr->xrepeat > 0) && (spr->yrepeat > 0) &&
                (*localspritesortcnt < maxspritesonscreen))
        {
            // this function's localtsprite is either the tsprite global or
            // polymer_drawroom's locattsprite, so no aliasing
            renderMakeTSpriteFromSprite(&localtsprite[(*localspritesortcnt)++], i);
        }
    }
}

void                polymer_updatesprite(int32_t snum)
{
    int32_t         xsize, ysize, i, j;
    int32_t         tilexoff, tileyoff, xoff, yoff, centeryoff=0, heinum;
    auto const      tspr = tspriteptr[snum];
    float           xratio, yratio, ang;
    float           spos[3];
    const _prvert   *inbuffer;
    uint8_t         flipu, flipv;
    _prsprite       *s;

    const uint32_t cs = tspr->cstat;
    const uint32_t alignmask = (cs & SPR_ALIGN_MASK);
    const uint8_t flooraligned = (alignmask==SPR_FLOOR);

    if (pr_nullrender >= 3) return;

    if (pr_verbosity >= 3) OSD_Printf("PR : Updating sprite %i...\n", snum);

    int32_t const curpicnum = tspr->picnum;

    if (tspr->owner < 0 || curpicnum < 0) return;

    s = prsprites[tspr->owner];

    if (s == NULL)
    {
        s = prsprites[tspr->owner] = (_prsprite *)Xcalloc(sizeof(_prsprite), 1);

        s->plane.buffer = (_prvert *)Xcalloc(4, sizeof(_prvert));  // XXX
        s->plane.vertcount = 4;
        s->plane.mapvbo_vertoffset = -1;
        s->hash = 0xDEADBEEF;
    }

    if ((tspr->cstat & 48) && (pr_vbos > 0) && !s->plane.vbo)
    {
        if (pr_nullrender < 2)
        {
            glGenBuffers(1, &s->plane.vbo);
            glBindBuffer(GL_ARRAY_BUFFER, s->plane.vbo);
            glBufferData(GL_ARRAY_BUFFER, 4 * sizeof(_prvert), NULL, mapvbousage);
        }
    }

    if (tspr->cstat & 48 && searchit != 2)
    {
        uint32_t const changed = XXH3_64bits((uint8_t *) tspr, offsetof(spritetype, owner));

        if (changed == s->hash)
            return;

        s->hash = changed;
    }

    polymer_getbuildmaterial(&s->plane.material, curpicnum, tspr->pal, tspr->shade,
                             sector[tspr->sectnum].visibility, DAMETH_MASK | DAMETH_CLAMPED);

    if (tspr->cstat & 2)
    {
        if (tspr->cstat & 512)
            s->plane.material.diffusemodulation[3] = 0x55;
        else
            s->plane.material.diffusemodulation[3] = 0xAA;
    }

    float f = s->plane.material.diffusemodulation[3] * (1.0f - spriteext[tspr->owner].alpha);
    s->plane.material.diffusemodulation[3] = (GLubyte)f;

    if (searchit == 2)
    {
        polymer_setupdiffusemodulation(&s->plane, 0x03, (GLubyte *) &tspr->owner);
        s->hash = 0xDEADBEEF;
    }

    if (((tspr->cstat>>4) & 3) == 0)
        xratio = (float)(tspr->xrepeat) * 0.20f; // 32 / 160
    else
        xratio = (float)(tspr->xrepeat) * 0.25f;

    yratio = (float)(tspr->yrepeat) * 0.25f;

    xsize = tilesiz[curpicnum].x;
    ysize = tilesiz[curpicnum].y;

    if (usehightile && h_xsize[curpicnum])
    {
        xsize = h_xsize[curpicnum];
        ysize = h_ysize[curpicnum];
    }

    xsize = (int32_t)(xsize * xratio);
    ysize = (int32_t)(ysize * yratio);

    tilexoff = (usehightile && h_xsize[curpicnum]) ? h_xoffs[curpicnum] : picanm[curpicnum].xofs;
    tileyoff = (usehightile && h_xsize[curpicnum]) ? h_yoffs[curpicnum] : picanm[curpicnum].yofs;
    
    heinum = tspriteGetSlope(tspr);
    
    if (heinum == 0)
    {
        tilexoff += (int32_t)tspr->xoffset;
        tileyoff += (int32_t)tspr->yoffset;
    }

    xoff = (int32_t)(tilexoff * xratio);
    yoff = (int32_t)(tileyoff * yratio);

    if ((tspr->cstat & 128) && !flooraligned)
    {
        if (alignmask == 0)
            yoff -= ysize / 2;
        else
            centeryoff = ysize / 2;
    }

    spos[0] = (float)tspr->y;
    spos[1] = -(float)(tspr->z) / 16.0f;
    spos[2] = -(float)tspr->x;

    glMatrixMode(GL_MODELVIEW);
    glPushMatrix();
    glLoadIdentity();

    inbuffer = vertsprite;

    {
        const uint8_t xflip = !!(cs & SPR_XFLIP);
        const uint8_t yflip = !!(cs & SPR_YFLIP);

        // Initially set flipu and flipv.
        flipu = (xflip ^ flooraligned);
        flipv = (yflip && !flooraligned);

        if (pr_billboardingmode && alignmask==0)
        {
            // do surgery on the face tspr to make it look like a wall sprite
            tspr->cstat |= 16;
            tspr->ang = (fix16_to_int(viewangle) + 1024) & 2047;
        }

        if (flipu)
            xoff = -xoff;

        if (yflip && alignmask!=0)
            yoff = -yoff;
    }

    switch (tspr->cstat & SPR_ALIGN_MASK)
    {
    case 0:
        ang = (float)((fix16_to_int(viewangle)) & 2047) * (360.f/2048.f);

        glTranslatef(spos[0], spos[1], spos[2]);
        glRotatef(-ang, 0.0f, 1.0f, 0.0f);
        glRotatef(-horizang, 1.0f, 0.0f, 0.0f);
        glTranslatef((float)(-xoff), (float)(yoff), 0.0f);
        glScalef((float)(xsize), (float)(ysize), 1.0f);
        break;
    case SPR_WALL:
        ang = (float)((tspr->ang + 1024) & 2047) * (360.f/2048.f);

        glTranslatef(spos[0], spos[1], spos[2]);
        glRotatef(-ang, 0.0f, 1.0f, 0.0f);
        glTranslatef((float)(-xoff), (float)(yoff-centeryoff), 0.0f);
        glScalef((float)(xsize), (float)(ysize), 1.0f);
        break;
    case SPR_FLOOR:
    {
        float const sang = atan2f(float(heinum), 4096.f) * (180.f * float(M_1_PI));
        ang = (float)((tspr->ang + 1024) & 2047) * (360.f/2048.f);

        glTranslatef(spos[0], spos[1], spos[2]);
        glRotatef(-ang, 0.0f, 1.0f, 0.0f);
        glRotatef(-sang, 1.0f, 0.0f, 0.0f);
        glTranslatef((float)(-xoff), 1.0f, (float)(yoff));
        glScalef((float)(xsize), 1.0f, (float)(ysize));

        inbuffer = horizsprite;
        break;
    }
    }

    glGetFloatv(GL_MODELVIEW_MATRIX, spritemodelview);
    glPopMatrix();

    Bmemcpy(s->plane.buffer, inbuffer, sizeof(_prvert) * 4);

    if (flipu || flipv)
    {
        i = 0;
        do
        {
            if (flipu)
                s->plane.buffer[i].u =
                (s->plane.buffer[i].u - 1.0f) * -1.0f;
            if (flipv)
                s->plane.buffer[i].v =
                (s->plane.buffer[i].v - 1.0f) * -1.0f;
        }
        while (++i < 4);
    }

    i = 0;
    do
        polymer_transformpoint(&inbuffer[i].x, &s->plane.buffer[i].x, spritemodelview);
    while (++i < 4);

    polymer_computeplane(&s->plane);

    if (pr_nullrender < 2)
    {
        if (alignmask && (pr_vbos > 0))
        {
            glBindBuffer(GL_ARRAY_BUFFER, s->plane.vbo);
            glBufferSubData(GL_ARRAY_BUFFER, 0, 4 * sizeof(_prvert), s->plane.buffer);
            glBindBuffer(GL_ARRAY_BUFFER, 0);
        }
        else if (s->plane.vbo) // clean up the vbo if a wall/floor sprite becomes a face sprite
        {
            glDeleteBuffers(1, &s->plane.vbo);
            s->plane.vbo = 0;
        }
    }

    if (alignmask)
    {
        int32_t curpriority = 0;

        polymer_resetplanelights(&s->plane);

        while (curpriority < pr_maxlightpriority)
        {
            i = j = 0;
            while (j < lightcount)
            {
                while (!prlights[i].flags.active)
                    i++;

                if (prlights[i].priority != curpriority)
                {
                    i++;
                    j++;
                    continue;
                }

                if (polymer_planeinlight(&s->plane, &prlights[i]))
                    polymer_addplanelight(&s->plane, i);
                i++;
                j++;
            }
            curpriority++;
        }
    }
}

// SKIES
static void         polymer_getsky(void)
{
    int32_t         i;

    i = 0;
    while (i < numsectors)
    {
        if (sector[i].ceilingstat & 1)
        {
            int32_t horizfrac;

            cursky = sector[i].ceilingpicnum;
            curskypal = sector[i].ceilingpal;
            curskyshade = sector[i].ceilingshade;

            getpsky(cursky, &horizfrac, NULL, NULL, NULL);

            switch (horizfrac)
            {
            case 0:
                // psky always at same level wrt screen
                curskyangmul = 0.f;
                break;
            case 65536:
                // psky horiz follows camera horiz
                curskyangmul = 1.f;
                break;
            default:
                // sky has hard-coded parallax
                curskyangmul = 1/DEFAULT_ARTSKY_ANGDIV;
                break;
            }

            return;
        }
        i++;
    }
}

void         polymer_drawsky(int16_t tilenum, char palnum, int8_t shade)
{
    float           pos[3];
    pthtyp*         pth;

    pos[0] = fglobalposy;
    pos[1] = fglobalposz * (-1.f/16.f);
    pos[2] = -fglobalposx;

    glPushMatrix();
    glLoadIdentity();

    glLoadMatrixf(curskymodelviewmatrix);

    glTranslatef(pos[0], pos[1], pos[2]);
    glScalef(1000.0f, 1000.0f, 1000.0f);

    drawingskybox = 1;
    pth = texcache_fetch(tilenum, 0, 0, DAMETH_NOMASK);
    drawingskybox = 0;

    if (pth && (pth->flags & PTH_SKYBOX))
        polymer_drawskybox(tilenum, palnum, shade);
    else
        polymer_drawartsky(tilenum, palnum, shade);

    glPopMatrix();
}

static void         polymer_initartsky(void)
{
    constexpr double factor = 2.0 * PI / (double)PSKYOFF_MAX;

    for (int i = 0; i < PSKYOFF_MAX; i++)
    {
        artskydata[i * 2 + 0] = -cos(i * factor);
        artskydata[i * 2 + 1] = sin(i * factor);
    }
}

static void         polymer_drawartsky(int16_t tilenum, char palnum, int8_t shade)
{
    pthtyp*         pth;
    GLuint          glpics[PSKYOFF_MAX];
    GLfloat         glcolors[PSKYOFF_MAX][3];
    int32_t         i, j;
    GLfloat         height = 2.45f / 2.0f;

    int32_t dapskybits;
    const int8_t *dapskyoff = getpsky(tilenum, NULL, &dapskybits, NULL, NULL);
    const int32_t numskytiles = 1<<dapskybits;
    const int32_t numskytilesm1 = numskytiles-1;

    i = 0;
    while (i < numskytiles)
    {
        int16_t picnum = tilenum + dapskyoff[i];
        // Prevent oob by bad user input:
        if (picnum >= MAXTILES)
            picnum = MAXTILES-1;

        tileUpdatePicnum(&picnum, 0);
        if (!waloff[picnum])
            tileLoad(picnum);
        pth = texcache_fetch(picnum, palnum, 0, DAMETH_NOMASK);
        glpics[i] = pth ? pth->glpic : 0;

        glcolors[i][0] = glcolors[i][1] = glcolors[i][2] = getshadefactor(shade);

        if (pth)
        {
            // tinting
            polytintflags_t const tintflags = hictinting[palnum].f;
            if (!(tintflags & HICTINT_PRECOMPUTED))
            {
                if (pth->flags & PTH_HIGHTILE)
                {
                    if (pth->palnum != palnum || (pth->effects & HICTINT_IN_MEMORY) || (tintflags & HICTINT_APPLYOVERALTPAL))
                        hictinting_apply(glcolors[i], palnum);
                }
                else if (tintflags & (HICTINT_USEONART|HICTINT_ALWAYSUSEART))
                    hictinting_apply(glcolors[i], palnum);
            }

            // global tinting
            if ((pth->flags & PTH_HIGHTILE) && have_basepal_tint())
                hictinting_apply(glcolors[i], MAXPALOOKUPS-1);

            globaltinting_apply(glcolors[i]);
        }

        i++;
    }

    glEnable(GL_TEXTURE_2D);
    i = 0;
    j = 0;
    int32_t const increment = PSKYOFF_MAX>>max(3, dapskybits);  // In Polymer, an ART sky has 8 or 16 sides...
    while (i < PSKYOFF_MAX)
    {
        GLint oldswrap;
        // ... but in case a multi-psky specifies less than 8, repeat cyclically:
        const int8_t tileofs = j&numskytilesm1;

        glColor4f(glcolors[tileofs][0], glcolors[tileofs][1], glcolors[tileofs][2], 1.0f);
        polymost_bindTexture(GL_TEXTURE_2D, glpics[tileofs]);

        glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, &oldswrap);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, glinfo.clamptoedge?GL_CLAMP_TO_EDGE:GL_CLAMP);

        polymer_drawartskyquad(i, (i + increment) & (PSKYOFF_MAX - 1), height);

        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, oldswrap);

        i += increment;
        ++j;
    }
    glDisable(GL_TEXTURE_2D);
}

static void         polymer_drawartskyquad(int32_t p1, int32_t p2, GLfloat height)
{
    glBegin(GL_QUADS);
    glTexCoord2f(0.0f, 0.0f);
    //OSD_Printf("PR: drawing %f %f %f\n", skybox[(p1 * 2) + 1], height, skybox[p1 * 2]);
    glVertex3f(artskydata[(p1 * 2) + 1], height, artskydata[p1 * 2]);
    glTexCoord2f(0.0f, 1.0f);
    //OSD_Printf("PR: drawing %f %f %f\n", skybox[(p1 * 2) + 1], -height, skybox[p1 * 2]);
    glVertex3f(artskydata[(p1 * 2) + 1], -height, artskydata[p1 * 2]);
    glTexCoord2f(1.0f, 1.0f);
    //OSD_Printf("PR: drawing %f %f %f\n", skybox[(p2 * 2) + 1], -height, skybox[p2 * 2]);
    glVertex3f(artskydata[(p2 * 2) + 1], -height, artskydata[p2 * 2]);
    glTexCoord2f(1.0f, 0.0f);
    //OSD_Printf("PR: drawing %f %f %f\n", skybox[(p2 * 2) + 1], height, skybox[p2 * 2]);
    glVertex3f(artskydata[(p2 * 2) + 1], height, artskydata[p2 * 2]);
    glEnd();
}

static void         polymer_drawskybox(int16_t tilenum, char palnum, int8_t shade)
{
    pthtyp*         pth;
    int32_t         i;
    GLfloat         color[3];

    if ((pr_vbos > 0) && (skyboxdatavbo == 0))
    {
        glGenBuffers(1, &skyboxdatavbo);

        glBindBuffer(GL_ARRAY_BUFFER, skyboxdatavbo);
        glBufferData(GL_ARRAY_BUFFER, 4 * sizeof(GLfloat) * 5 * 6, skyboxdata, modelvbousage);

        glBindBuffer(GL_ARRAY_BUFFER, 0);
    }

    if (pr_vbos > 0)
        glBindBuffer(GL_ARRAY_BUFFER, skyboxdatavbo);

    tileUpdatePicnum(&tilenum, 0);

    i = 0;
    while (i < 6)
    {
        drawingskybox = i + 1;
        pth = texcache_fetch(tilenum, palnum, 0, DAMETH_CLAMPED);

        color[0] = color[1] = color[2] = getshadefactor(shade);

        if (pth)
        {
            // tinting
            polytintflags_t const tintflags = hictinting[palnum].f;
            if (!(tintflags & HICTINT_PRECOMPUTED))
            {
                if (pth->flags & PTH_HIGHTILE)
                {
                    if (pth->palnum != palnum || (pth->effects & HICTINT_IN_MEMORY) || (tintflags & HICTINT_APPLYOVERALTPAL))
                        hictinting_apply(color, palnum);
                }
                else if (tintflags & (HICTINT_USEONART|HICTINT_ALWAYSUSEART))
                    hictinting_apply(color, palnum);
            }

            // global tinting
            if ((pth->flags & PTH_HIGHTILE) && have_basepal_tint())
                hictinting_apply(color, MAXPALOOKUPS-1);

            globaltinting_apply(color);
        }

        glColor4f(color[0], color[1], color[2], 1.0);
        glEnable(GL_TEXTURE_2D);
        polymost_bindTexture(GL_TEXTURE_2D, pth ? pth->glpic : 0);
        if (pr_vbos > 0)
        {
            glVertexPointer(3, GL_FLOAT, 5 * sizeof(GLfloat), (GLfloat*)(4 * 5 * i * sizeof(GLfloat)));
            glTexCoordPointer(2, GL_FLOAT, 5 * sizeof(GLfloat), (GLfloat*)(((4 * 5 * i) + 3) * sizeof(GLfloat)));
        } else {
            glVertexPointer(3, GL_FLOAT, 5 * sizeof(GLfloat), &skyboxdata[4 * 5 * i]);
            glTexCoordPointer(2, GL_FLOAT, 5 * sizeof(GLfloat), &skyboxdata[3 + (4 * 5 * i)]);
        }
        glDrawArrays(GL_QUADS, 0, 4);
        glDisable(GL_TEXTURE_2D);

        i++;
    }
    drawingskybox = 0;

    if (pr_vbos > 0)
        glBindBuffer(GL_ARRAY_BUFFER, 0);
}

// MDSPRITES
static void         polymer_drawmdsprite(tspriteptr_t tspr)
{
    md3model_t*     m;
    mdskinmap_t*    sk;
    float           *v0, *v1;
    md3surf_t       *s;
    char            targetpal, usinghighpal, foundpalskin;
    float           spos2[3], spos[3], tspos[3], lpos[3], tlpos[3], vec[3], mat[4][4];
    float           ang;
    float           scale;
    double          det;
    int32_t         surfi, i, j;
    GLubyte*        color;
    int32_t         materialbits;
    float           sradius, lradius;
    int16_t         modellights[PR_MAXLIGHTS];
    char            modellightcount;
    uint8_t         curpriority;

    uint8_t lpal = (tspr->owner >= MAXSPRITES) ? tspr->pal : sprite[tspr->owner].pal;

    m = (md3model_t*)models[tile2model[Ptile2tile(tspr->picnum,lpal)].modelid];
    updateanimation((md2model_t *)m,tspr,lpal);

    if ((pr_vbos > 1) && (m->indices == NULL))
        polymer_loadmodelvbos(m);

    // Hackish, but that means it's a model drawn by rotatesprite.
    if (tspriteptr[maxspritesonscreen] == tspr) {
        float       x, y, z;

        spos[0] = fglobalposy;
        spos[1] = fglobalposz * (-1.f/16.f);
        spos[2] = -fglobalposx;

        // The coordinates are actually floats disguised as int in this case
        memcpy(&x, &tspr->x, sizeof(float));
        memcpy(&y, &tspr->y, sizeof(float));
        memcpy(&z, &tspr->z, sizeof(float));

        spos2[0] = y - globalposy;
        spos2[1] = (z - fglobalposz) * (-1.f/16.f);
        spos2[2] = fglobalposx - x;
    } else {
        spos[0] = (float)tspr->y+spriteext[tspr->owner].mdposition_offset.y;
        spos[1] = -(float)(tspr->z+spriteext[tspr->owner].mdposition_offset.z) / 16.0f;
        spos[2] = -(float)(tspr->x+spriteext[tspr->owner].mdposition_offset.x);

        spos2[0] = spos2[1] = spos2[2] = 0.0f;
    }

    ang = (float)((tspr->ang+spriteext[tspr->owner].mdangoff) & 2047) * (360.f/2048.f);
    ang -= 90.0f;
    if (((tspr->cstat>>4) & 3) == 2)
        ang -= 90.0f;

    glMatrixMode(GL_MODELVIEW);
    glPushMatrix();
    glLoadIdentity();
    scale = (1.0/4.0);
    scale *= m->scale;
    if (pr_overridemodelscale) {
        scale *= pr_overridemodelscale;
    } else {
        scale *= m->bscale;
    }

    if (tspriteptr[maxspritesonscreen] == tspr) {
        float playerang, radplayerang, cosminusradplayerang, sinminusradplayerang, hudzoom;

        playerang = (globalang & 2047) * (360.f/2048.f) - 90.0f;
        radplayerang = (globalang & 2047) * (2.0f * fPI / 2048.0f);
        cosminusradplayerang = cos(-radplayerang);
        sinminusradplayerang = sin(-radplayerang);
        hudzoom = 65536.0 / spriteext[tspr->owner].mdpivot_offset.z;

        glTranslatef(spos[0], spos[1], spos[2]);
        glRotatef(horizang, -cosminusradplayerang, 0.0f, sinminusradplayerang);
        glRotatef(spriteext[tspr->owner].mdroll * (360.f/2048.f), sinminusradplayerang, 0.0f, cosminusradplayerang);
        glRotatef(-playerang, 0.0f, 1.0f, 0.0f);
        glScalef(hudzoom, 1.0f, 1.0f);
        glRotatef(playerang, 0.0f, 1.0f, 0.0f);
        glTranslatef(spos2[0], spos2[1], spos2[2]);
        glRotatef(-ang, 0.0f, 1.0f, 0.0f);
    } else {
        glTranslatef(spos[0], spos[1], spos[2]);
        glRotatef(-ang, 0.0f, 1.0f, 0.0f);
    }
    if (((tspr->cstat>>4) & 3) == 2)
    {
        glTranslatef(0.0f, 0.0, -(float)(tilesiz[tspr->picnum].y * tspr->yrepeat) / 8.0f);
        glRotatef(90.0f, 0.0f, 0.0f, 1.0f);
    }
    else
        glRotatef(-90.0f, 1.0f, 0.0f, 0.0f);

    if ((tspr->cstat & 128) && (((tspr->cstat>>4) & 3) != 2))
        glTranslatef(0.0f, 0.0, -(float)(tilesiz[tspr->picnum].y * tspr->yrepeat) / 8.0f);

    // yoffset differs from zadd in that it does not follow cstat&8 y-flipping
    glTranslatef(0.0f, 0.0, m->yoffset * 64 * scale * tspr->yrepeat);

    if (tspr->cstat & 8)
    {
        glTranslatef(0.0f, 0.0, (float)(tilesiz[tspr->picnum].y * tspr->yrepeat) / 4.0f);
        glScalef(1.0f, 1.0f, -1.0f);
    }

    if (tspr->cstat & 4)
        glScalef(1.0f, -1.0f, 1.0f);

    if (!(tspr->cstat & 4) != !(tspr->cstat & 8)) {
        // Only inverting one coordinate will reverse the winding order of
        // faces, so we need to account for that when culling.
        SWITCH_CULL_DIRECTION;
    }

    glScalef(scale * tspr->xrepeat, scale * tspr->xrepeat, scale * tspr->yrepeat);
    glTranslatef(0.0f, 0.0, m->zadd * 64);

    // scripted model rotation
    if (tspr->owner < MAXSPRITES &&
        (spriteext[tspr->owner].mdpitch || spriteext[tspr->owner].mdroll))
    {
        float       pitchang, rollang, offsets[3];

        pitchang = (float)(spriteext[tspr->owner].mdpitch) * (360.f/2048.f);
        rollang = (float)(spriteext[tspr->owner].mdroll) * (360.f/2048.f);

        offsets[0] = -spriteext[tspr->owner].mdpivot_offset.x / (scale * tspr->xrepeat);
        offsets[1] = -spriteext[tspr->owner].mdpivot_offset.y / (scale * tspr->xrepeat);
        offsets[2] = (float)(spriteext[tspr->owner].mdpivot_offset.z) / 16.0f / (scale * tspr->yrepeat);

        glTranslatef(-offsets[0], -offsets[1], -offsets[2]);

        glRotatef(pitchang, 0.0f, 1.0f, 0.0f);
        glRotatef(rollang, -1.0f, 0.0f, 0.0f);

        glTranslatef(offsets[0], offsets[1], offsets[2]);
    }

    glGetFloatv(GL_MODELVIEW_MATRIX, spritemodelview);

    glPopMatrix();
    glPushMatrix();
    glMultMatrixf(spritemodelview);

    // invert this matrix to get the polymer -> mdsprite space
    memcpy(mat, spritemodelview, sizeof(float) * 16);
    INVERT_4X4(mdspritespace, det, mat);

    // debug code for drawing the model bounding sphere
//     glDisable(GL_TEXTURE_2D);
//     glBegin(GL_LINES);
//     glColor4f(1.0, 0.0, 0.0, 1.0);
//     glVertex3f(m->head.frames[m->cframe].cen.x,
//                 m->head.frames[m->cframe].cen.y,
//                 m->head.frames[m->cframe].cen.z);
//     glVertex3f(m->head.frames[m->cframe].cen.x + m->head.frames[m->cframe].r,
//                 m->head.frames[m->cframe].cen.y,
//                 m->head.frames[m->cframe].cen.z);
//     glColor4f(0.0, 1.0, 0.0, 1.0);
//     glVertex3f(m->head.frames[m->cframe].cen.x,
//                 m->head.frames[m->cframe].cen.y,
//                 m->head.frames[m->cframe].cen.z);
//     glVertex3f(m->head.frames[m->cframe].cen.x,
//                 m->head.frames[m->cframe].cen.y + m->head.frames[m->cframe].r,
//                 m->head.frames[m->cframe].cen.z);
//     glColor4f(0.0, 0.0, 1.0, 1.0);
//     glVertex3f(m->head.frames[m->cframe].cen.x,
//                 m->head.frames[m->cframe].cen.y,
//                 m->head.frames[m->cframe].cen.z);
//     glVertex3f(m->head.frames[m->cframe].cen.x,
//                 m->head.frames[m->cframe].cen.y,
//                 m->head.frames[m->cframe].cen.z + m->head.frames[m->cframe].r);
//     glEnd();
//     glEnable(GL_TEXTURE_2D);

    polymer_getscratchmaterial(&mdspritematerial);

    color = mdspritematerial.diffusemodulation;

    color[0] = color[1] = color[2] =
        (GLubyte)(((float)(numshades-min(max((tspr->shade * shadescale)+m->shadeoff,0.f),(float)numshades)))/((float)numshades) * 0xFF);

    usinghighpal = (pr_highpalookups &&
                    prhighpalookups[curbasepal][tspr->pal].map);

    // tinting
    polytintflags_t const tintflags = hictinting[tspr->pal].f;
    if (!usinghighpal && !(tintflags & HICTINT_PRECOMPUTED))
    {
        if (!(m->flags&1))
            hictinting_apply_ub(color, tspr->pal);
        else globalnoeffect=1; //mdloadskin reads this
    }

    // global tinting
    if (!usinghighpal && have_basepal_tint())
        hictinting_apply_ub(color, MAXPALOOKUPS-1);

    globaltinting_apply_ub(color);

    if (tspr->cstat & 2)
    {
        if (!(tspr->cstat&512))
            color[3] = 0xAA;
        else
            color[3] = 0x55;
    } else
        color[3] = 0xFF;

    {
        double f = color[3] * (1.0f - spriteext[tspr->owner].alpha);
        color[3] = (GLubyte)f;
    }

    if (searchit == 2)
    {
        color[0] = 0x03;
        color[1] = ((GLubyte *)(&tspr->owner))[0];
        color[2] = ((GLubyte *)(&tspr->owner))[1];
        color[3] = 0xFF;
    }

    if (pr_gpusmoothing)
        mdspritematerial.frameprogress = m->interpol;

    mdspritematerial.mdspritespace = GL_TRUE;

    modellightcount = 0;
    curpriority = 0;

    // light culling
    if (lightcount && (!depth || mirrors[depth-1].plane))
    {
        sradius = (m->head.frames[m->cframe].r * (1 - m->interpol)) +
                  (m->head.frames[m->nframe].r * m->interpol);

        sradius *= max(scale * tspr->xrepeat, scale * tspr->yrepeat);
        sradius /= 1000.0f;

        spos[0] = (m->head.frames[m->cframe].cen.x * (1 - m->interpol)) +
                  (m->head.frames[m->nframe].cen.x * m->interpol);
        spos[1] = (m->head.frames[m->cframe].cen.y * (1 - m->interpol)) +
                  (m->head.frames[m->nframe].cen.y * m->interpol);
        spos[2] = (m->head.frames[m->cframe].cen.z * (1 - m->interpol)) +
                  (m->head.frames[m->nframe].cen.z * m->interpol);

        polymer_transformpoint(spos, tspos, spritemodelview);
        polymer_transformpoint(tspos, spos, rootmodelviewmatrix);

        while (curpriority < pr_maxlightpriority)
        {
            i = j = 0;
            while (j < lightcount)
            {
                while (!prlights[i].flags.active)
                    i++;

                if (prlights[i].priority != curpriority)
                {
                    i++;
                    j++;
                    continue;
                }

                lradius = prlights[i].range / 1000.0f;

                lpos[0] = (float)prlights[i].y;
                lpos[1] = -(float)prlights[i].z / 16.0f;
                lpos[2] = -(float)prlights[i].x;

                polymer_transformpoint(lpos, tlpos, rootmodelviewmatrix);

                vec[0] = tlpos[0] - spos[0];
                vec[0] *= vec[0];
                vec[1] = tlpos[1] - spos[1];
                vec[1] *= vec[1];
                vec[2] = tlpos[2] - spos[2];
                vec[2] *= vec[2];

                if ((vec[0] + vec[1] + vec[2]) <= ((sradius+lradius) * (sradius+lradius)))
                    modellights[modellightcount++] = i;

                i++;
                j++;
            }
            curpriority++;
        }
    }

    for (surfi=0;surfi<m->head.numsurfs;surfi++)
    {
        s = &m->head.surfs[surfi];
        v0 = &s->geometry[m->cframe*s->numverts*15];
        v1 = &s->geometry[m->nframe*s->numverts*15];

        // debug code for drawing model normals
//         glDisable(GL_TEXTURE_2D);
//         glBegin(GL_LINES);
//         glColor4f(1.0, 1.0, 1.0, 1.0);
//
//         int i = 0;
//         while (i < s->numverts)
//         {
//             glVertex3f(v0[(i * 6) + 0],
//                         v0[(i * 6) + 1],
//                         v0[(i * 6) + 2]);
//             glVertex3f(v0[(i * 6) + 0] + v0[(i * 6) + 3] * 100,
//                         v0[(i * 6) + 1] + v0[(i * 6) + 4] * 100,
//                         v0[(i * 6) + 2] + v0[(i * 6) + 5] * 100);
//             i++;
//         }
//         glEnd();
//         glEnable(GL_TEXTURE_2D);


        targetpal = tspr->pal;
        foundpalskin = 0;

        for (sk = m->skinmap; sk; sk = sk->next)
            if ((int32_t)sk->palette == tspr->pal &&
                 sk->skinnum == tile2model[Ptile2tile(tspr->picnum,lpal)].skinnum &&
                 sk->surfnum == surfi)
        {
            if (sk->specpower != 1.0)
                mdspritematerial.specmaterial[0] = sk->specpower;
            mdspritematerial.specmaterial[1] = sk->specfactor;
            foundpalskin = 1;
        }

        // If we have a global palette tint, the palskin won't do us any good
        if (curbasepal)
            foundpalskin = 0;

        if (!foundpalskin && usinghighpal) {
            // We don't have a specific skin defined for this palette
            // Use the base skin instead and plug in our highpalookup map
            targetpal = 0;
            mdspritematerial.highpalookupmap = prhighpalookups[curbasepal][tspr->pal].map;
        }

        mdspritematerial.diffusemap =
                mdloadskin((md2model_t *)m,tile2model[Ptile2tile(tspr->picnum,lpal)].skinnum,targetpal,surfi);
        if (!mdspritematerial.diffusemap)
            continue;

        if (!(tspr->clipdist & TSPR_FLAGS_MDHACK))
        {
            mdspritematerial.detailmap =
                    mdloadskin((md2model_t *)m,tile2model[Ptile2tile(tspr->picnum,lpal)].skinnum,DETAILPAL,surfi);

            for (sk = m->skinmap; sk; sk = sk->next)
                if ((int32_t)sk->palette == DETAILPAL &&
                    sk->skinnum == tile2model[Ptile2tile(tspr->picnum,lpal)].skinnum &&
                    sk->surfnum == surfi)
                    mdspritematerial.detailscale[0] = mdspritematerial.detailscale[1] = sk->param;

            mdspritematerial.specmap =
                    mdloadskin((md2model_t *)m,tile2model[Ptile2tile(tspr->picnum,lpal)].skinnum,SPECULARPAL,surfi);

            mdspritematerial.normalmap =
                    mdloadskin((md2model_t *)m,tile2model[Ptile2tile(tspr->picnum,lpal)].skinnum,NORMALPAL,surfi);

            for (sk = m->skinmap; sk; sk = sk->next)
                if ((int32_t)sk->palette == NORMALPAL &&
                    sk->skinnum == tile2model[Ptile2tile(tspr->picnum,lpal)].skinnum &&
                    sk->surfnum == surfi) {
                    mdspritematerial.normalbias[0] = sk->specpower;
                    mdspritematerial.normalbias[1] = sk->specfactor;
                }

            mdspritematerial.glowmap =
                    mdloadskin((md2model_t *)m,tile2model[Ptile2tile(tspr->picnum,lpal)].skinnum,GLOWPAL,surfi);
        }

        glEnableClientState(GL_NORMAL_ARRAY);

        if (pr_vbos > 1)
        {
            glBindBuffer(GL_ARRAY_BUFFER, m->texcoords[surfi]);
            glTexCoordPointer(2, GL_FLOAT, 0, 0);

            glBindBuffer(GL_ARRAY_BUFFER, m->geometry[surfi]);
            glVertexPointer(3, GL_FLOAT, sizeof(float) * 15, (GLfloat*)(m->cframe * s->numverts * sizeof(float) * 15));
            glNormalPointer(GL_FLOAT, sizeof(float) * 15, (GLfloat*)(m->cframe * s->numverts * sizeof(float) * 15) + 3);

            mdspritematerial.tbn = (GLfloat*)(m->cframe * s->numverts * sizeof(float) * 15) + 6;

            if (pr_gpusmoothing) {
                mdspritematerial.nextframedata = (GLfloat*)(m->nframe * s->numverts * sizeof(float) * 15);
            }

            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m->indices[surfi]);

            curlight = 0;
            do {
                materialbits = polymer_bindmaterial(&mdspritematerial, modellights, modellightcount);
                glDrawElements(GL_TRIANGLES, s->numtris * 3, GL_UNSIGNED_INT, 0);
                polymer_unbindmaterial(materialbits);
            } while ((++curlight < modellightcount) && (curlight < pr_maxlightpasses));

            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
            glBindBuffer(GL_ARRAY_BUFFER, 0);
        }
        else
        {
            glVertexPointer(3, GL_FLOAT, sizeof(float) * 15, v0);
            glNormalPointer(GL_FLOAT, sizeof(float) * 15, v0 + 3);
            glTexCoordPointer(2, GL_FLOAT, 0, s->uv);

            mdspritematerial.tbn = v0 + 6;

            if (pr_gpusmoothing) {
                mdspritematerial.nextframedata = (GLfloat*)(v1);
            }

            curlight = 0;
            do {
                materialbits = polymer_bindmaterial(&mdspritematerial, modellights, modellightcount);
                glDrawElements(GL_TRIANGLES, s->numtris * 3, GL_UNSIGNED_INT, s->tris);
                polymer_unbindmaterial(materialbits);
            } while ((++curlight < modellightcount) && (curlight < pr_maxlightpasses));
        }

        glDisableClientState(GL_NORMAL_ARRAY);
    }

    glPopMatrix();

    if (!(tspr->cstat & 4) != !(tspr->cstat & 8)) {
        SWITCH_CULL_DIRECTION;
    }

    globalnoeffect = 0;
}

static void         polymer_loadmodelvbos(md3model_t* m)
{
    int32_t         i;
    md3surf_t       *s;

    m->indices = (GLuint *)Xmalloc(m->head.numsurfs * sizeof(GLuint));
    m->texcoords = (GLuint *)Xmalloc(m->head.numsurfs * sizeof(GLuint));
    m->geometry = (GLuint *)Xmalloc(m->head.numsurfs * sizeof(GLuint));

    glGenBuffers(m->head.numsurfs, m->indices);
    glGenBuffers(m->head.numsurfs, m->texcoords);
    glGenBuffers(m->head.numsurfs, m->geometry);

    i = 0;
    while (i < m->head.numsurfs)
    {
        s = &m->head.surfs[i];

        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m->indices[i]);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, s->numtris * sizeof(md3tri_t), s->tris, modelvbousage);

        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

        glBindBuffer(GL_ARRAY_BUFFER, m->texcoords[i]);
        glBufferData(GL_ARRAY_BUFFER, s->numverts * sizeof(md3uv_t), s->uv, modelvbousage);

        glBindBuffer(GL_ARRAY_BUFFER, m->geometry[i]);
        glBufferData(GL_ARRAY_BUFFER, s->numframes * s->numverts * sizeof(float) * (15), s->geometry, modelvbousage);

        glBindBuffer(GL_ARRAY_BUFFER, 0);
        i++;
    }
}

// MATERIALS
static void         polymer_getscratchmaterial(_prmaterial* material)
{
    // this function returns a material that won't validate any bits
    // make sure to keep it up to date with the validation logic in bindmaterial

    // PR_BIT_ANIM_INTERPOLATION
    material->frameprogress = 0.0f;
    material->nextframedata = (float*)-1;
    // PR_BIT_NORMAL_MAP
    material->normalmap = 0;
    material->normalbias[0] = material->normalbias[1] = 0.0f;
    material->tbn = NULL;
    // PR_BIT_ART_MAP
    material->artmap = 0;
    material->basepalmap = 0;
    material->lookupmap = 0;
    // PR_BIT_DIFFUSE_MAP
    material->diffusemap = 0;
    material->diffusescale[0] = material->diffusescale[1] = 1.0f;
    // PR_BIT_HIGHPALOOKUP_MAP
    material->highpalookupmap = 0;
    // PR_BIT_DIFFUSE_DETAIL_MAP
    material->detailmap = 0;
    material->detailscale[0] = material->detailscale[1] = 1.0f;
    // PR_BIT_DIFFUSE_MODULATION
    material->diffusemodulation[0] =
            material->diffusemodulation[1] =
            material->diffusemodulation[2] =
            material->diffusemodulation[3] = 0xFF;
    // PR_BIT_SPECULAR_MAP
    material->specmap = 0;
    // PR_BIT_SPECULAR_MATERIAL
    material->specmaterial[0] = 15.0f;
    material->specmaterial[1] = 1.0f;
    // PR_BIT_MIRROR_MAP
    material->mirrormap = 0;
    // PR_BIT_GLOW_MAP
    material->glowmap = 0;
    // PR_BIT_PROJECTION_MAP
    material->mdspritespace = GL_FALSE;
}

static void         polymer_setupartmap(int16_t tilenum, char pal)
{
    if (!prartmaps[tilenum]) {
        char *tilebuffer = (char *) waloff[tilenum];
        char *tempbuffer = (char *) Xmalloc(tilesiz[tilenum].x * tilesiz[tilenum].y);
        int i, j, k;

        i = k = 0;
        while (i < tilesiz[tilenum].y) {
            j = 0;
            while (j < tilesiz[tilenum].x) {
                tempbuffer[k] = tilebuffer[(j * tilesiz[tilenum].y) + i];
                k++;
                j++;
            }
            i++;
        }

        glGenTextures(1, &prartmaps[tilenum]);
        polymost_bindTexture(GL_TEXTURE_2D, prartmaps[tilenum]);
        glTexImage2D(GL_TEXTURE_2D,
            0,
            GL_RED,
            tilesiz[tilenum].x,
            tilesiz[tilenum].y,
            0,
            GL_RED,
            GL_UNSIGNED_BYTE,
            tempbuffer);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
        polymost_bindTexture(GL_TEXTURE_2D, 0);
        Xfree(tempbuffer);
    }

    if (!prbasepalmaps[curbasepal]) {
        glGenTextures(1, &prbasepalmaps[curbasepal]);
        polymost_bindTexture(GL_TEXTURE_2D, prbasepalmaps[curbasepal]);
        glTexImage2D(GL_TEXTURE_2D,
            0,
            GL_RGB,
            256,
            1,
            0,
            GL_RGB,
            GL_UNSIGNED_BYTE,
            basepaltable[curbasepal]);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, glinfo.clamptoedge ? GL_CLAMP_TO_EDGE : GL_CLAMP);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, glinfo.clamptoedge ? GL_CLAMP_TO_EDGE : GL_CLAMP);
        polymost_bindTexture(GL_TEXTURE_2D, 0);
    }

    if (!prlookups[pal]) {
        glGenTextures(1, &prlookups[pal]);
        polymost_bindTexture(GL_TEXTURE_RECTANGLE_ARB, prlookups[pal]);
        glTexImage2D(GL_TEXTURE_RECTANGLE_ARB,
            0,
            GL_RED,
            256,
            numshades,
            0,
            GL_RED,
            GL_UNSIGNED_BYTE,
            palookup[pal]);
        glTexParameteri(GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
        glTexParameteri(GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
        glTexParameteri(GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_WRAP_S, glinfo.clamptoedge ? GL_CLAMP_TO_EDGE : GL_CLAMP);
        glTexParameteri(GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_WRAP_T, glinfo.clamptoedge ? GL_CLAMP_TO_EDGE : GL_CLAMP);
        polymost_bindTexture(GL_TEXTURE_RECTANGLE_ARB, 0);
    }
}

static _prbucket*   polymer_getbuildmaterial(_prmaterial* material, int16_t tilenum, char pal, int8_t shade, int8_t vis, int32_t cmeth)
{
    // find corresponding bucket; XXX key that with pr_buckets later, need to be tied to restartvid
    _prbucket *bucketptr = polymer_findbucket(tilenum, pal);

    polymer_getscratchmaterial(material);

    if (!waloff[tilenum])
        tileLoad(tilenum);

    // PR_BIT_DIFFUSE_MAP
    pthtyp *pth = texcache_fetch(tilenum, pal, 0, cmeth);

    if (pth)
    {
        material->diffusemap = pth->glpic;

        if (pth->hicr)
        {
            material->diffusescale[0] = pth->hicr->scale.x;
            material->diffusescale[1] = pth->hicr->scale.y;
        }
    }

    int32_t usinghighpal = 0;

    // Lazily fill in all the textures we need, move this to precaching later
    if (pr_artmapping && !(globalflags & GLOBAL_NO_GL_TILESHADES) && polymer_eligible_for_artmap(tilenum, pth))
    {
        polytintflags_t const tintflags = hictinting[pal].f;

        if (tintflags & (HICTINT_USEONART|HICTINT_ALWAYSUSEART))
        {
            if (!(tintflags & HICTINT_APPLYOVERPALSWAP))
                pal = 0;
        }

        if (!prartmaps[tilenum] || !prbasepalmaps[curbasepal] || !prlookups[pal])
            polymer_setupartmap(tilenum, pal);

        material->artmap = prartmaps[tilenum];
        material->basepalmap = prbasepalmaps[curbasepal];
        material->lookupmap = prlookups[pal];

        if (!material->basepalmap || !material->lookupmap) {
            material->artmap = 0;
        }

        material->shadeoffset = shade;
        material->visibility = (uint8_t)(vis+16);

        globaltinting_apply_ub(material->diffusemodulation);
        // all the stuff below is mutually exclusive with artmapping
        goto done;
    }

    // PR_BIT_HIGHPALOOKUP_MAP
    if (pr_highpalookups && prhighpalookups[curbasepal][pal].map &&
        hicfindsubst(tilenum, 0) &&
        (curbasepal || (hicfindsubst(tilenum, pal)->palnum != pal)))
    {
        material->highpalookupmap = prhighpalookups[curbasepal][pal].map;
        pal = 0;
        usinghighpal = 1;
    }

    if (pth)
    {
        if (pth->hicr)
        {
            // PR_BIT_SPECULAR_MATERIAL
            if (pth->hicr->specpower != 1.0f)
                material->specmaterial[0] = pth->hicr->specpower;
            material->specmaterial[1] = pth->hicr->specfactor;
        }

        // PR_BIT_DIFFUSE_MODULATION
        material->diffusemodulation[0] =
            material->diffusemodulation[1] =
            material->diffusemodulation[2] =
            (GLubyte)(getshadefactor(shade) * 0xFF);

        // tinting
        polytintflags_t const tintflags = hictinting[pal].f;
        if (!(tintflags & HICTINT_PRECOMPUTED))
        {
            if (pth->flags & PTH_HIGHTILE)
            {
                if (pth->palnum != pal || (pth->effects & HICTINT_IN_MEMORY) || (tintflags & HICTINT_APPLYOVERALTPAL))
                    hictinting_apply_ub(material->diffusemodulation, pal);
            }
            else if (tintflags & (HICTINT_USEONART|HICTINT_ALWAYSUSEART))
                hictinting_apply_ub(material->diffusemodulation, pal);
        }

        // global tinting
        if ((pth->flags & PTH_HIGHTILE) && !usinghighpal && have_basepal_tint())
            hictinting_apply_ub(material->diffusemodulation, MAXPALOOKUPS-1);

        globaltinting_apply_ub(material->diffusemodulation);

        // PR_BIT_GLOW_MAP
        if (r_fullbrights && pth->flags & PTH_HASFULLBRIGHT)
            material->glowmap = pth->ofb->glpic;
    }

    // PR_BIT_DIFFUSE_DETAIL_MAP
    if (hicfindsubst(tilenum, DETAILPAL, 1) && (pth = texcache_fetch(tilenum, DETAILPAL, 0, DAMETH_NOMASK)) &&
        pth->hicr && (pth->hicr->palnum == DETAILPAL))
    {
        material->detailmap = pth->glpic;
        material->detailscale[0] = pth->hicr->scale.x;
        material->detailscale[1] = pth->hicr->scale.y;
    }

    // PR_BIT_GLOW_MAP
    if (hicfindsubst(tilenum, GLOWPAL, 1) && (pth = texcache_fetch(tilenum, GLOWPAL, 0, DAMETH_MASK)) &&
        pth->hicr && (pth->hicr->palnum == GLOWPAL))
        material->glowmap = pth->glpic;

    // PR_BIT_SPECULAR_MAP
    if (hicfindsubst(tilenum, SPECULARPAL, 1) && (pth = texcache_fetch(tilenum, SPECULARPAL, 0, DAMETH_NOMASK)) &&
        pth->hicr && (pth->hicr->palnum == SPECULARPAL))
        material->specmap = pth->glpic;

    // PR_BIT_NORMAL_MAP
    if (hicfindsubst(tilenum, NORMALPAL, 1) && (pth = texcache_fetch(tilenum, NORMALPAL, 0, DAMETH_NOMASK)) &&
        pth->hicr && (pth->hicr->palnum == NORMALPAL))
    {
        material->normalmap = pth->glpic;
        material->normalbias[0] = pth->hicr->specpower;
        material->normalbias[1] = pth->hicr->specfactor;
    }

done:
    if (bucketptr->invalidmaterial != 0)
    {
        bucketptr->material = *material;
        bucketptr->invalidmaterial = 0;
    }

    return bucketptr;
}

static int32_t      polymer_bindmaterial(const _prmaterial *material, const int16_t* lights, int matlightcount)
{
    int32_t         programbits;
    int32_t         texunit;

    programbits = 0;

    // --------- bit validation

    // PR_BIT_ANIM_INTERPOLATION
    if (material->nextframedata != ((float*)-1))
        programbits |= prprogrambits[PR_BIT_ANIM_INTERPOLATION].bit;

    // PR_BIT_LIGHTING_PASS
    if (curlight && matlightcount)
        programbits |= prprogrambits[PR_BIT_LIGHTING_PASS].bit;

    // PR_BIT_NORMAL_MAP
    if (pr_normalmapping && material->normalmap)
        programbits |= prprogrambits[PR_BIT_NORMAL_MAP].bit;

    // PR_BIT_ART_MAP
    if (pr_artmapping && material->artmap &&
        !(globalflags & GLOBAL_NO_GL_TILESHADES) &&
        (overridematerial & prprogrambits[PR_BIT_ART_MAP].bit)) {
        programbits |= prprogrambits[PR_BIT_ART_MAP].bit;
    } else
    // PR_BIT_DIFFUSE_MAP
    if (material->diffusemap) {
        programbits |= prprogrambits[PR_BIT_DIFFUSE_MAP].bit;
        programbits |= prprogrambits[PR_BIT_DIFFUSE_MAP2].bit;
    }

    // PR_BIT_HIGHPALOOKUP_MAP
    if (material->highpalookupmap)
        programbits |= prprogrambits[PR_BIT_HIGHPALOOKUP_MAP].bit;

    // PR_BIT_DIFFUSE_DETAIL_MAP
    if (r_detailmapping && material->detailmap)
        programbits |= prprogrambits[PR_BIT_DIFFUSE_DETAIL_MAP].bit;

    // PR_BIT_DIFFUSE_MODULATION
    programbits |= prprogrambits[PR_BIT_DIFFUSE_MODULATION].bit;

    // PR_BIT_SPECULAR_MAP
    if (pr_specularmapping && material->specmap)
        programbits |= prprogrambits[PR_BIT_SPECULAR_MAP].bit;

    // PR_BIT_SPECULAR_MATERIAL
    if ((material->specmaterial[0] != 15.0) || (material->specmaterial[1] != 1.0) || pr_overridespecular)
        programbits |= prprogrambits[PR_BIT_SPECULAR_MATERIAL].bit;

    // PR_BIT_MIRROR_MAP
    if (!curlight && material->mirrormap)
        programbits |= prprogrambits[PR_BIT_MIRROR_MAP].bit;

    // PR_BIT_FOG
    if (!material->artmap && !curlight && !material->mirrormap)
        programbits |= prprogrambits[PR_BIT_FOG].bit;

    // PR_BIT_GLOW_MAP
    if (!curlight && r_glowmapping && material->glowmap)
        programbits |= prprogrambits[PR_BIT_GLOW_MAP].bit;

    // PR_BIT_POINT_LIGHT
    if (matlightcount) {
        programbits |= prprogrambits[PR_BIT_POINT_LIGHT].bit;
        // PR_BIT_SPOT_LIGHT
        if (prlights[lights[curlight]].radius) {
            programbits |= prprogrambits[PR_BIT_SPOT_LIGHT].bit;
            // PR_BIT_SHADOW_MAP
            if (prlights[lights[curlight]].rtindex != -1) {
                programbits |= prprogrambits[PR_BIT_SHADOW_MAP].bit;
                programbits |= prprogrambits[PR_BIT_PROJECTION_MAP].bit;
            }
            // PR_BIT_LIGHT_MAP
            if (prlights[lights[curlight]].lightmap) {
                programbits |= prprogrambits[PR_BIT_LIGHT_MAP].bit;
                programbits |= prprogrambits[PR_BIT_PROJECTION_MAP].bit;
            }
        }
    }

    // material override
    programbits &= overridematerial;

    programbits |= prprogrambits[PR_BIT_HEADER].bit;
    programbits |= prprogrambits[PR_BIT_FOOTER].bit;

    // --------- program compiling
    if (!prprograms[programbits].handle)
        polymer_compileprogram(programbits);

    polymost_useShaderProgram(prprograms[programbits].handle);

    // --------- bit setup

    texunit = 0;

    // PR_BIT_ANIM_INTERPOLATION
    if (programbits & prprogrambits[PR_BIT_ANIM_INTERPOLATION].bit)
    {
        glEnableVertexAttribArray(prprograms[programbits].attrib_nextFrameData);
        if (prprograms[programbits].attrib_nextFrameNormal != -1)
            glEnableVertexAttribArray(prprograms[programbits].attrib_nextFrameNormal);
        glVertexAttribPointer(prprograms[programbits].attrib_nextFrameData,
                               3, GL_FLOAT, GL_FALSE,
                               sizeof(float) * 15,
                               material->nextframedata);
        if (prprograms[programbits].attrib_nextFrameNormal != -1)
            glVertexAttribPointer(prprograms[programbits].attrib_nextFrameNormal,
                                   3, GL_FLOAT, GL_FALSE,
                                   sizeof(float) * 15,
                                   material->nextframedata + 3);

        glUniform1f(prprograms[programbits].uniform_frameProgress, material->frameprogress);
    }

    // PR_BIT_LIGHTING_PASS
    if (programbits & prprogrambits[PR_BIT_LIGHTING_PASS].bit)
    {
        glPushAttrib(GL_COLOR_BUFFER_BIT);
        glEnable(GL_BLEND);
        glBlendFunc(GL_ONE, GL_ONE);

        if (prlights[lights[curlight]].publicflags.negative) {
            glBlendEquation(GL_FUNC_REVERSE_SUBTRACT);
        }
    }

    // PR_BIT_NORMAL_MAP
    if (programbits & prprogrambits[PR_BIT_NORMAL_MAP].bit)
    {
        float pos[3], bias[2];

        pos[0] = fglobalposy;
        pos[1] = fglobalposz * (-1.f/16.f);
        pos[2] = -fglobalposx;

        polymost_activeTexture(texunit + GL_TEXTURE0);
        polymost_bindTexture(GL_TEXTURE_2D, material->normalmap);

        if (material->mdspritespace == GL_TRUE) {
            float mdspritespacepos[3];
            polymer_transformpoint(pos, mdspritespacepos, (float *)mdspritespace);
            glUniform3fv(prprograms[programbits].uniform_eyePosition, 1, mdspritespacepos);
        } else
            glUniform3fv(prprograms[programbits].uniform_eyePosition, 1, pos);
        glUniform1i(prprograms[programbits].uniform_normalMap, texunit);
        if (pr_overrideparallax) {
            bias[0] = pr_parallaxscale;
            bias[1] = pr_parallaxbias;
            glUniform2fv(prprograms[programbits].uniform_normalBias, 1, bias);
        } else
            glUniform2fv(prprograms[programbits].uniform_normalBias, 1, material->normalbias);

        if (material->tbn) {
            glEnableVertexAttribArray(prprograms[programbits].attrib_T);
            glEnableVertexAttribArray(prprograms[programbits].attrib_B);
            glEnableVertexAttribArray(prprograms[programbits].attrib_N);

            glVertexAttribPointer(prprograms[programbits].attrib_T,
                                      3, GL_FLOAT, GL_FALSE,
                                      sizeof(float) * 15,
                                      material->tbn);
            glVertexAttribPointer(prprograms[programbits].attrib_B,
                                      3, GL_FLOAT, GL_FALSE,
                                      sizeof(float) * 15,
                                      material->tbn + 3);
            glVertexAttribPointer(prprograms[programbits].attrib_N,
                                      3, GL_FLOAT, GL_FALSE,
                                      sizeof(float) * 15,
                                      material->tbn + 6);
        }

        texunit++;
    }

    // PR_BIT_ART_MAP
    if (programbits & prprogrambits[PR_BIT_ART_MAP].bit)
    {
        polymost_activeTexture(texunit + GL_TEXTURE0);
        polymost_bindTexture(GL_TEXTURE_2D, material->artmap);

        glUniform1i(prprograms[programbits].uniform_artMap, texunit);

        texunit++;

        polymost_activeTexture(texunit + GL_TEXTURE0);
        polymost_bindTexture(GL_TEXTURE_2D, material->basepalmap);

        glUniform1i(prprograms[programbits].uniform_basePalMap, texunit);

        texunit++;

        polymost_activeTexture(texunit + GL_TEXTURE0);
        polymost_bindTexture(GL_TEXTURE_RECTANGLE_ARB, material->lookupmap);

        glUniform1i(prprograms[programbits].uniform_lookupMap, texunit);

        texunit++;

        glUniform1f(prprograms[programbits].uniform_shadeOffset, (GLfloat)material->shadeoffset);
        if (r_usenewshading == 4)
        {
            // the fog in Polymer is a sphere insted of a plane, the furthest visible point should be the same as Polymost
            glUniform1f(prprograms[programbits].uniform_visibility, globalvisibility / 262144.f * material->visibility);
        }
        else
        {
            static constexpr float material_visibility_divisor = 16.f;

            // NOTE: the denominator was 1.024, but we increase it towards a bit
            // farther far clipoff distance to account for the fact that the
            // distance to the fragment is the common Euclidean one, as opposed to
            // the "ortho" distance of Build.
            static constexpr float factor_new = 1.f / ((2048.f * (1.07f / 1.024f) * (150.f / 230.f) / 35.f) * material_visibility_divisor);

            static constexpr float factor_old = 1.f / ((2048.f * (1.07f / 1.024f) / 35.f) * material_visibility_divisor);

            glUniform1f(prprograms[programbits].uniform_visibility, globalvisibility * material->visibility * r_usenewshading > 1 ? factor_new : factor_old);
        }
    }

    // PR_BIT_DIFFUSE_MAP
    if (programbits & prprogrambits[PR_BIT_DIFFUSE_MAP].bit)
    {
        polymost_activeTexture(texunit + GL_TEXTURE0);
        polymost_bindTexture(GL_TEXTURE_2D, material->diffusemap);

        glUniform1i(prprograms[programbits].uniform_diffuseMap, texunit);
        glUniform2fv(prprograms[programbits].uniform_diffuseScale, 1, material->diffusescale);

        texunit++;
    }

    // PR_BIT_HIGHPALOOKUP_MAP
    if (programbits & prprogrambits[PR_BIT_HIGHPALOOKUP_MAP].bit)
    {
        polymost_activeTexture(texunit + GL_TEXTURE0);
        polymost_bindTexture(GL_TEXTURE_3D, material->highpalookupmap);

        glUniform1i(prprograms[programbits].uniform_highPalookupMap, texunit);

        texunit++;
    }

    // PR_BIT_DIFFUSE_DETAIL_MAP
    if (programbits & prprogrambits[PR_BIT_DIFFUSE_DETAIL_MAP].bit)
    {
        float scale[2];

        // scale by the diffuse map scale if we're not doing normal mapping
        if (!(programbits & prprogrambits[PR_BIT_NORMAL_MAP].bit))
        {
            scale[0] = material->diffusescale[0] * material->detailscale[0];
            scale[1] = material->diffusescale[1] * material->detailscale[1];
        } else {
            scale[0] = material->detailscale[0];
            scale[1] = material->detailscale[1];
        }

        polymost_activeTexture(texunit + GL_TEXTURE0);
        polymost_bindTexture(GL_TEXTURE_2D, material->detailmap);

        glUniform1i(prprograms[programbits].uniform_detailMap, texunit);
        glUniform2fv(prprograms[programbits].uniform_detailScale, 1, scale);

        texunit++;
    }

    // PR_BIT_DIFFUSE_MODULATION
    if (programbits & prprogrambits[PR_BIT_DIFFUSE_MODULATION].bit)
    {
            glColor4ub(material->diffusemodulation[0],
                        material->diffusemodulation[1],
                        material->diffusemodulation[2],
                        material->diffusemodulation[3]);
    }

    // PR_BIT_SPECULAR_MAP
    if (programbits & prprogrambits[PR_BIT_SPECULAR_MAP].bit)
    {
        polymost_activeTexture(texunit + GL_TEXTURE0);
        polymost_bindTexture(GL_TEXTURE_2D, material->specmap);

        glUniform1i(prprograms[programbits].uniform_specMap, texunit);

        texunit++;
    }

    // PR_BIT_SPECULAR_MATERIAL
    if (programbits & prprogrambits[PR_BIT_SPECULAR_MATERIAL].bit)
    {
        float specmaterial[2];

        if (pr_overridespecular) {
            specmaterial[0] = pr_specularpower;
            specmaterial[1] = pr_specularfactor;
            glUniform2fv(prprograms[programbits].uniform_specMaterial, 1, specmaterial);
        } else
            glUniform2fv(prprograms[programbits].uniform_specMaterial, 1, material->specmaterial);
    }

    // PR_BIT_MIRROR_MAP
    if (programbits & prprogrambits[PR_BIT_MIRROR_MAP].bit)
    {
        polymost_activeTexture(texunit + GL_TEXTURE0);
        polymost_bindTexture(GL_TEXTURE_RECTANGLE_ARB, material->mirrormap);

        glUniform1i(prprograms[programbits].uniform_mirrorMap, texunit);

        texunit++;
    }
#ifdef PR_LINEAR_FOG
    if (programbits & prprogrambits[PR_BIT_FOG].bit)
    {
        glUniform1i(prprograms[programbits].uniform_linearFog, r_usenewshading >= 2);
    }
#endif
    // PR_BIT_GLOW_MAP
    if (programbits & prprogrambits[PR_BIT_GLOW_MAP].bit)
    {
        polymost_activeTexture(texunit + GL_TEXTURE0);
        polymost_bindTexture(GL_TEXTURE_2D, material->glowmap);

        glUniform1i(prprograms[programbits].uniform_glowMap, texunit);

        texunit++;
    }

    // PR_BIT_POINT_LIGHT
    if (programbits & prprogrambits[PR_BIT_POINT_LIGHT].bit)
    {
        float inpos[4], pos[4];
        float range[2];
        float color[4];

        inpos[0] = (float)prlights[lights[curlight]].y;
        inpos[1] = -(float)prlights[lights[curlight]].z / 16.0f;
        inpos[2] = -(float)prlights[lights[curlight]].x;

        polymer_transformpoint(inpos, pos, curmodelviewmatrix);

        // PR_BIT_SPOT_LIGHT
        if (programbits & prprogrambits[PR_BIT_SPOT_LIGHT].bit)
        {
            float sinang, cosang, sinhorizang, coshorizangs;
            float indir[3], dir[3];

            cosang = (float)(sintable[(-prlights[lights[curlight]].angle+1024)&2047]) / 16383.0f;
            sinang = (float)(sintable[(-prlights[lights[curlight]].angle+512)&2047]) / 16383.0f;
            coshorizangs = (float)(sintable[(getangle(128, prlights[lights[curlight]].horiz-100)+1024)&2047]) / 16383.0f;
            sinhorizang = (float)(sintable[(getangle(128, prlights[lights[curlight]].horiz-100)+512)&2047]) / 16383.0f;

            indir[0] = inpos[0] + sinhorizang * cosang;
            indir[1] = inpos[1] - coshorizangs;
            indir[2] = inpos[2] - sinhorizang * sinang;

            polymer_transformpoint(indir, dir, curmodelviewmatrix);

            dir[0] -= pos[0];
            dir[1] -= pos[1];
            dir[2] -= pos[2];

            indir[0] = (float)(sintable[(prlights[lights[curlight]].radius+512)&2047]) / 16383.0f;
            indir[1] = (float)(sintable[(prlights[lights[curlight]].faderadius+512)&2047]) / 16383.0f;
            indir[1] = 1.0 / (indir[1] - indir[0]);

            glUniform3fv(prprograms[programbits].uniform_spotDir, 1, dir);
            glUniform2fv(prprograms[programbits].uniform_spotRadius, 1, indir);

            // PR_BIT_PROJECTION_MAP
            if (programbits & prprogrambits[PR_BIT_PROJECTION_MAP].bit)
            {
                GLfloat matrix[16];

                glMatrixMode(GL_TEXTURE);
                glLoadMatrixf(shadowBias);
                glMultMatrixf(prlights[lights[curlight]].proj);
                glMultMatrixf(prlights[lights[curlight]].transform);
                if (material->mdspritespace == GL_TRUE)
                    glMultMatrixf(spritemodelview);
                glGetFloatv(GL_TEXTURE_MATRIX, matrix);
                glLoadIdentity();
                glMatrixMode(GL_MODELVIEW);

                glUniformMatrix4fv(prprograms[programbits].uniform_shadowProjMatrix, 1, GL_FALSE, matrix);

                // PR_BIT_SHADOW_MAP
                if (programbits & prprogrambits[PR_BIT_SHADOW_MAP].bit)
                {
                    polymost_activeTexture(texunit + GL_TEXTURE0);
                    polymost_bindTexture(prrts[prlights[lights[curlight]].rtindex].target, prrts[prlights[lights[curlight]].rtindex].z);

                    glUniform1i(prprograms[programbits].uniform_shadowMap, texunit);

                    texunit++;
                }

                // PR_BIT_LIGHT_MAP
                if (programbits & prprogrambits[PR_BIT_LIGHT_MAP].bit)
                {
                    polymost_activeTexture(texunit + GL_TEXTURE0);
                    polymost_bindTexture(GL_TEXTURE_2D, prlights[lights[curlight]].lightmap);

                    glUniform1i(prprograms[programbits].uniform_lightMap, texunit);

                    texunit++;
                }
            }
        }

        range[0] = prlights[lights[curlight]].range  / 1000.0f;
        range[1] = 1 / (range[0] * range[0]);

        color[0] = prlights[lights[curlight]].color[0]   / 255.0f;
        color[1] = prlights[lights[curlight]].color[1]   / 255.0f;
        color[2] = prlights[lights[curlight]].color[2]   / 255.0f;

        // If this isn't a lighting-only pass, just negate the components
        if (!curlight && prlights[lights[curlight]].publicflags.negative) {
            color[0] = -color[0];
            color[1] = -color[1];
            color[2] = -color[2];
        }

        glLightfv(GL_LIGHT0, GL_AMBIENT, pos);
        glLightfv(GL_LIGHT0, GL_DIFFUSE, color);
        if (material->mdspritespace == GL_TRUE) {
            float mdspritespacepos[3];
            polymer_transformpoint(inpos, mdspritespacepos, (float *)mdspritespace);
            glLightfv(GL_LIGHT0, GL_SPECULAR, mdspritespacepos);
        } else {
            glLightfv(GL_LIGHT0, GL_SPECULAR, inpos);
        }
        glLightfv(GL_LIGHT0, GL_LINEAR_ATTENUATION, &range[1]);
    }

    polymost_activeTexture(GL_TEXTURE0);

    return programbits;
}

static void         polymer_unbindmaterial(int32_t programbits)
{
    // repair any dirty GL state here

    // PR_BIT_ANIM_INTERPOLATION
    if (programbits & prprogrambits[PR_BIT_ANIM_INTERPOLATION].bit)
    {
        if (prprograms[programbits].attrib_nextFrameNormal != -1)
            glDisableVertexAttribArray(prprograms[programbits].attrib_nextFrameNormal);
        glDisableVertexAttribArray(prprograms[programbits].attrib_nextFrameData);
    }

    // PR_BIT_LIGHTING_PASS
    if (programbits & prprogrambits[PR_BIT_LIGHTING_PASS].bit)
    {
        glPopAttrib();
    }

    // PR_BIT_NORMAL_MAP
    if (programbits & prprogrambits[PR_BIT_NORMAL_MAP].bit)
    {
        glDisableVertexAttribArray(prprograms[programbits].attrib_T);
        glDisableVertexAttribArray(prprograms[programbits].attrib_B);
        glDisableVertexAttribArray(prprograms[programbits].attrib_N);
    }

    polymost_useShaderProgram(0);
}

static void         polymer_compileprogram(int32_t programbits)
{
    int32_t         i, enabledbits;
    GLuint          vert, frag, program;
    const GLchar*      source[PR_BIT_COUNT * 2];
    GLchar       infobuffer[PR_INFO_LOG_BUFFER_SIZE];
    GLint           linkstatus;

    // --------- VERTEX
    vert = glCreateShader(GL_VERTEX_SHADER);

    enabledbits = i = 0;
    while (i < PR_BIT_COUNT)
    {
        if (programbits & prprogrambits[i].bit)
            source[enabledbits++] = prprogrambits[i].vert_def;
        i++;
    }
    i = 0;
    while (i < PR_BIT_COUNT)
    {
        if (programbits & prprogrambits[i].bit)
            source[enabledbits++] = prprogrambits[i].vert_prog;
        i++;
    }

    glShaderSource(vert, enabledbits, source, NULL);

    glCompileShader(vert);

    // --------- FRAGMENT
    frag = glCreateShader(GL_FRAGMENT_SHADER);

    enabledbits = i = 0;
    while (i < PR_BIT_COUNT)
    {
        if (programbits & prprogrambits[i].bit)
            source[enabledbits++] = prprogrambits[i].frag_def;
        i++;
    }
    i = 0;
    while (i < PR_BIT_COUNT)
    {
        if (programbits & prprogrambits[i].bit)
            source[enabledbits++] = prprogrambits[i].frag_prog;
        i++;
    }

    glShaderSource(frag, enabledbits, (const GLchar**)source, NULL);

    glCompileShader(frag);

    // --------- PROGRAM
    program = glCreateProgram();

    glAttachShader(program, vert);
    glAttachShader(program, frag);

    glLinkProgram(program);

    glGetProgramiv(program, GL_LINK_STATUS, &linkstatus);

    glGetProgramInfoLog(program, PR_INFO_LOG_BUFFER_SIZE, NULL, infobuffer);

    prprograms[programbits].handle = program;

#ifdef DEBUGGINGAIDS
    if (pr_verbosity >= 1)
#else
    if (pr_verbosity >= 2)
#endif
        OSD_Printf("PR : Compiling GPU program with bits (octal) %o...\n", (unsigned)programbits);
    if (!linkstatus) {
        OSD_Printf("PR : Failed to compile GPU program with bits (octal) %o!\n", (unsigned)programbits);
        if (pr_verbosity >= 1) OSD_Printf("PR : Compilation log:\n%s\n", infobuffer);
        glGetShaderSource(vert, PR_INFO_LOG_BUFFER_SIZE, NULL, infobuffer);
        if (pr_verbosity >= 1) OSD_Printf("PR : Vertex source dump:\n%s\n", infobuffer);
        glGetShaderSource(frag, PR_INFO_LOG_BUFFER_SIZE, NULL, infobuffer);
        if (pr_verbosity >= 1) OSD_Printf("PR : Fragment source dump:\n%s\n", infobuffer);
    }

    // --------- ATTRIBUTE/UNIFORM LOCATIONS

    // PR_BIT_ANIM_INTERPOLATION
    if (programbits & prprogrambits[PR_BIT_ANIM_INTERPOLATION].bit)
    {
        prprograms[programbits].attrib_nextFrameData = glGetAttribLocation(program, "nextFrameData");
        prprograms[programbits].attrib_nextFrameNormal = glGetAttribLocation(program, "nextFrameNormal");
        prprograms[programbits].uniform_frameProgress = glGetUniformLocation(program, "frameProgress");
    }

    // PR_BIT_NORMAL_MAP
    if (programbits & prprogrambits[PR_BIT_NORMAL_MAP].bit)
    {
        prprograms[programbits].attrib_T = glGetAttribLocation(program, "T");
        prprograms[programbits].attrib_B = glGetAttribLocation(program, "B");
        prprograms[programbits].attrib_N = glGetAttribLocation(program, "N");
        prprograms[programbits].uniform_eyePosition = glGetUniformLocation(program, "eyePosition");
        prprograms[programbits].uniform_normalMap = glGetUniformLocation(program, "normalMap");
        prprograms[programbits].uniform_normalBias = glGetUniformLocation(program, "normalBias");
    }

    // PR_BIT_ART_MAP
    if (programbits & prprogrambits[PR_BIT_ART_MAP].bit)
    {
        prprograms[programbits].uniform_artMap = glGetUniformLocation(program, "artMap");
        prprograms[programbits].uniform_basePalMap = glGetUniformLocation(program, "basePalMap");
        prprograms[programbits].uniform_lookupMap = glGetUniformLocation(program, "lookupMap");
        prprograms[programbits].uniform_shadeOffset = glGetUniformLocation(program, "shadeOffset");
        prprograms[programbits].uniform_visibility = glGetUniformLocation(program, "visibility");
    }

    // PR_BIT_DIFFUSE_MAP
    if (programbits & prprogrambits[PR_BIT_DIFFUSE_MAP].bit)
    {
        prprograms[programbits].uniform_diffuseMap = glGetUniformLocation(program, "diffuseMap");
        prprograms[programbits].uniform_diffuseScale = glGetUniformLocation(program, "diffuseScale");
    }

    // PR_BIT_HIGHPALOOKUP_MAP
    if (programbits & prprogrambits[PR_BIT_HIGHPALOOKUP_MAP].bit)
    {
        prprograms[programbits].uniform_highPalookupMap = glGetUniformLocation(program, "highPalookupMap");
    }

    // PR_BIT_DIFFUSE_DETAIL_MAP
    if (programbits & prprogrambits[PR_BIT_DIFFUSE_DETAIL_MAP].bit)
    {
        prprograms[programbits].uniform_detailMap = glGetUniformLocation(program, "detailMap");
        prprograms[programbits].uniform_detailScale = glGetUniformLocation(program, "detailScale");
    }

    // PR_BIT_SPECULAR_MAP
    if (programbits & prprogrambits[PR_BIT_SPECULAR_MAP].bit)
    {
        prprograms[programbits].uniform_specMap = glGetUniformLocation(program, "specMap");
    }

    // PR_BIT_SPECULAR_MATERIAL
    if (programbits & prprogrambits[PR_BIT_SPECULAR_MATERIAL].bit)
    {
        prprograms[programbits].uniform_specMaterial = glGetUniformLocation(program, "specMaterial");
    }

    // PR_BIT_MIRROR_MAP
    if (programbits & prprogrambits[PR_BIT_MIRROR_MAP].bit)
    {
        prprograms[programbits].uniform_mirrorMap = glGetUniformLocation(program, "mirrorMap");
    }
#ifdef PR_LINEAR_FOG
    if (programbits & prprogrambits[PR_BIT_FOG].bit)
    {
        prprograms[programbits].uniform_linearFog = glGetUniformLocation(program, "linearFog");
    }
#endif
    // PR_BIT_GLOW_MAP
    if (programbits & prprogrambits[PR_BIT_GLOW_MAP].bit)
    {
        prprograms[programbits].uniform_glowMap = glGetUniformLocation(program, "glowMap");
    }

    // PR_BIT_PROJECTION_MAP
    if (programbits & prprogrambits[PR_BIT_PROJECTION_MAP].bit)
    {
        prprograms[programbits].uniform_shadowProjMatrix = glGetUniformLocation(program, "shadowProjMatrix");
    }

    // PR_BIT_SHADOW_MAP
    if (programbits & prprogrambits[PR_BIT_SHADOW_MAP].bit)
    {
        prprograms[programbits].uniform_shadowMap = glGetUniformLocation(program, "shadowMap");
    }

    // PR_BIT_LIGHT_MAP
    if (programbits & prprogrambits[PR_BIT_LIGHT_MAP].bit)
    {
        prprograms[programbits].uniform_lightMap = glGetUniformLocation(program, "lightMap");
    }

    // PR_BIT_SPOT_LIGHT
    if (programbits & prprogrambits[PR_BIT_SPOT_LIGHT].bit)
    {
        prprograms[programbits].uniform_spotDir = glGetUniformLocation(program, "spotDir");
        prprograms[programbits].uniform_spotRadius = glGetUniformLocation(program, "spotRadius");
    }
}

// LIGHTS
static void         polymer_removelight(int16_t lighti)
{
    _prplanelist*   oldhead;

    while (prlights[lighti].planelist)
    {
        polymer_deleteplanelight(prlights[lighti].planelist->plane, lighti);
        oldhead = prlights[lighti].planelist;
        prlights[lighti].planelist = prlights[lighti].planelist->n;
        oldhead->n = plpool;
        plpool = oldhead;
        plpool->plane = NULL;
    }
    prlights[lighti].planecount = 0;
    prlights[lighti].planelist = NULL;
}

static void         polymer_updatelights(void)
{
    int32_t         i = 0;

    do
    {
        _prlight* light = &prlights[i];

        if (light->flags.active && light->flags.invalidate) {
            // highly suboptimal
            polymer_removelight(i);

            if (light->radius)
                polymer_processspotlight(light);

            polymer_culllight(i);

            light->flags.invalidate = 0;
        }

        if (light->flags.active) {
            // get the texture handle for the lightmap
            if (light->radius && light->tilenum > 0)
            {
                int16_t     picnum = light->tilenum;
                pthtyp*     pth;

                tileUpdatePicnum(&picnum, 0);

                if (!waloff[picnum])
                    tileLoad(picnum);

                pth = NULL;
                pth = texcache_fetch(picnum, 0, 0, DAMETH_NOMASK);

                if (pth)
                    light->lightmap = pth->glpic;
            }

            light->rtindex = -1;
        }
    }
    while (++i < PR_MAXLIGHTS);
}

static inline void  polymer_resetplanelights(_prplane* plane)
{
    Bmemset(&plane->lights[0], -1, sizeof(plane->lights[0]) * plane->lightcount);
    plane->lightcount = 0;
}

static void         polymer_addplanelight(_prplane* plane, int16_t lighti)
{
    _prplanelist*   oldhead;
    int32_t         i = 0;

    if (plane->lightcount)
    {
        if (plane->lightcount == PR_MAXLIGHTS - 1)
            return;

        do
        {
            if (plane->lights[i++] == lighti)
                goto out;
        }
        while (i < plane->lightcount);

        i = 0;
        while (i < plane->lightcount && prlights[plane->lights[i]].priority < prlights[lighti].priority)
            i++;
        Bmemmove(&plane->lights[i+1], &plane->lights[i], sizeof(int16_t) * (plane->lightcount - i));
    }

    plane->lights[i] = lighti;
    plane->lightcount++;

out:
    oldhead = prlights[lighti].planelist;
    while (oldhead != NULL)
    {
        if (oldhead->plane == plane) return;
        oldhead = oldhead->n;
    }

    oldhead = prlights[lighti].planelist;
    if (plpool == NULL)
    {
        prlights[lighti].planelist = (_prplanelist *) Xmalloc(sizeof(_prplanelist));
        prlights[lighti].planelist->n = oldhead;
    }
    else
    {
        prlights[lighti].planelist = plpool;
        plpool = plpool->n;
        prlights[lighti].planelist->n = oldhead;
    }

    prlights[lighti].planelist->plane = plane;
    prlights[lighti].planecount++;
}

static inline void  polymer_deleteplanelight(_prplane* plane, int16_t lighti)
{
    int32_t         i = plane->lightcount-1;

    while (i >= 0)
    {
        if (plane->lights[i] == lighti)
        {
            Bmemmove(&plane->lights[i], &plane->lights[i+1], sizeof(int16_t) * (plane->lightcount - i));
            plane->lightcount--;
            return;
        }
        i--;
    }
}

static int32_t      polymer_planeinlight(_prplane* plane, _prlight* light)
{
    float           lightpos[3];
    int32_t         i, j, k, l;

    if (!plane->vertcount)
        return 0;

    if (light->radius)
        return polymer_planeinfrustum(plane, light->frustum);

    lightpos[0] = (float)light->y;
    lightpos[1] = -(float)light->z / 16.0f;
    lightpos[2] = -(float)light->x;

    i = 0;

    do
    {
        j = k = l = 0;

        do
        {
            if ((&plane->buffer[j].x)[i] > (lightpos[i] + light->range)) k++;
            if ((&plane->buffer[j].x)[i] < (lightpos[i] - light->range)) l++;
        }
        while (++j < plane->vertcount);

        if ((k == plane->vertcount) || (l == plane->vertcount))
            return 0;
    }
    while (++i < 3);

    return 1;
}

static void         polymer_invalidateplanelights(_prplane* plane)
{
    int32_t         i = plane->lightcount;

    while (i--)
    {
        if (((unsigned)plane->lights[i] < PR_MAXLIGHTS) && (prlights[plane->lights[i]].flags.active))
            prlights[plane->lights[i]].flags.invalidate = 1;
    }
}

static void         polymer_invalidatesectorlights(int16_t sectnum)
{
    int32_t         i;
    _prsector       *s = prsectors[sectnum];
    sectortype      *sec = &sector[sectnum];

    if (!s)
        return;

    polymer_invalidateplanelights(&s->floor);
    polymer_invalidateplanelights(&s->ceil);

    i = sec->wallnum;

    while (i--)
    {
        _prwall         *w;
        if (!(w = prwalls[sec->wallptr + i])) continue;

        polymer_invalidateplanelights(&w->wall);
        polymer_invalidateplanelights(&w->over);
        polymer_invalidateplanelights(&w->mask);
    }
}

static void         polymer_processspotlight(_prlight* light)
{
    float           radius, ang, horizang, lightpos[3];

    // hack to avoid lights beams perpendicular to walls
    if ((light->horiz <= 100) && (light->horiz > 90))
        light->horiz = 90;
    if ((light->horiz > 100) && (light->horiz < 110))
        light->horiz = 110;

    lightpos[0] = (float)light->y;
    lightpos[1] = -(float)light->z / 16.0f;
    lightpos[2] = -(float)light->x;

    // calculate the spot light transformations and matrices
    radius = (float)(light->radius) * (360.f/2048.f);
    ang = (float)(light->angle) * (360.f/2048.f);
    horizang = (float)(-getangle(128, light->horiz-100)) * (360.f/2048.f);

    glMatrixMode(GL_PROJECTION);
    glPushMatrix();
    glLoadIdentity();
    bgluPerspective(radius * 2, 1, 0.1f, light->range * (1.f/1000.f));
    glGetFloatv(GL_PROJECTION_MATRIX, light->proj);
    glPopMatrix();

    glMatrixMode(GL_MODELVIEW);
    glPushMatrix();
    glLoadIdentity();
    glRotatef(horizang, 1.0f, 0.0f, 0.0f);
    glRotatef(ang, 0.0f, 1.0f, 0.0f);
    glScalef(1.0f / 1000.0f, 1.0f / 1000.0f, 1.0f / 1000.0f);
    glTranslatef(-lightpos[0], -lightpos[1], -lightpos[2]);
    glGetFloatv(GL_MODELVIEW_MATRIX, light->transform);
    glPopMatrix();

    polymer_extractfrustum(light->transform, light->proj, light->frustum);

    light->rtindex = -1;
    light->lightmap = 0;
}

static inline void  polymer_culllight(int16_t lighti)
{
    _prlight*       light = &prlights[lighti];
    int32_t         front = 0;
    int32_t         back = 1;
    int32_t         i;
    int32_t         j;
    int32_t         zdiff;
    int32_t         checkror;
    int16_t         bunchnum;
    int16_t         ns;
    _prsector       *s;
    _prwall         *w;
    sectortype      *sec, *nsec;

    Bmemset(drawingstate, 0, sizeof(int16_t) * numsectors);
    drawingstate[light->sector] = 1;

    sectorqueue[0] = light->sector;

    do
    {
        s = prsectors[sectorqueue[front]];
        sec = &sector[sectorqueue[front]];

        polymer_pokesector(sectorqueue[front]);

        checkror = FALSE;

        if (!(sec->floorstat & 1))
        {
            zdiff = light->z - s->floorz;
            if (zdiff < 0)
                zdiff = -zdiff;
            zdiff >>= 4;

            if (!light->radius) {
                if (zdiff < light->range) {
                    polymer_addplanelight(&s->floor, lighti);
                    checkror = TRUE;
                }
            } else if (polymer_planeinlight(&s->floor, light)) {
                polymer_addplanelight(&s->floor, lighti);
                checkror = TRUE;
            }
        }

#ifdef YAX_ENABLE
        // queue ROR neighbors
        if (checkror &&
            (bunchnum = yax_getbunch(sectorqueue[front], YAX_FLOOR)) >= 0) {

            for (SECTORS_OF_BUNCH(bunchnum, YAX_CEILING, ns)) {

                if (ns >= 0 && !drawingstate[ns] &&
                    polymer_planeinlight(&prsectors[ns]->ceil, light)) {

                    sectorqueue[back++] = ns;
                    drawingstate[ns] = 1;
                }
            }
        }
#endif
        checkror = FALSE;

        if (!(sec->ceilingstat & 1))
        {
            zdiff = light->z - s->ceilingz;
            if (zdiff < 0)
                zdiff = -zdiff;
            zdiff >>= 4;

            if (!light->radius) {
                if (zdiff < light->range) {
                    polymer_addplanelight(&s->ceil, lighti);
                    checkror = TRUE;
                }
            } else if (polymer_planeinlight(&s->ceil, light)) {
                polymer_addplanelight(&s->ceil, lighti);
                checkror = TRUE;
            }
        }

#ifdef YAX_ENABLE
        // queue ROR neighbors
        if (checkror &&
            (bunchnum = yax_getbunch(sectorqueue[front], YAX_CEILING)) >= 0) {

            for (SECTORS_OF_BUNCH(bunchnum, YAX_FLOOR, ns)) {

                if (ns >= 0 && !drawingstate[ns] &&
                    polymer_planeinlight(&prsectors[ns]->floor, light)) {

                    sectorqueue[back++] = ns;
                    drawingstate[ns] = 1;
                }
            }
        }
#endif
        i = 0;
        while (i < sec->wallnum)
        {
            w = prwalls[sec->wallptr + i];
            walltype *wal = &wall[sec->wallptr + i];

            if (wal->nextsector >= 0 && wal->nextsector < numsectors)
            {
                nsec = &sector[wal->nextsector];
            }
            else
            {
                nsec = nullptr;
            }

            j = 0;

            if (!(sec->floorstat & 1 && nsec && nsec->floorstat & 1))
            {
                if (polymer_planeinlight(&w->wall, light)) {
                    polymer_addplanelight(&w->wall, lighti);
                    j++;
                }
            }

            if (!(sec->ceilingstat & 1 && nsec && nsec->ceilingstat & 1))
            {
                if (polymer_planeinlight(&w->over, light)) {
                    polymer_addplanelight(&w->over, lighti);
                    j++;
                }
            }

            // assume the light hits the middle section if it hits the top and bottom
            if (wallvisible(light->x, light->y, sec->wallptr + i) &&
                (j == 2 || polymer_planeinlight(&w->mask, light))) {
                if ((w->mask.vertcount == 4) &&
                    (w->mask.buffer[0].y >= w->mask.buffer[3].y) &&
                    (w->mask.buffer[1].y >= w->mask.buffer[2].y))
                {
                    i++;
                    continue;
                }

                polymer_addplanelight(&w->mask, lighti);

                if ((wall[sec->wallptr + i].nextsector >= 0) &&
                    (!drawingstate[wall[sec->wallptr + i].nextsector])) {
                    drawingstate[wall[sec->wallptr + i].nextsector] = 1;
                    sectorqueue[back] = wall[sec->wallptr + i].nextsector;
                    back++;
                }
            }

            i++;
        }
        front++;
    }
    while (front != back);

    i = MAXSPRITES-1;

    do
    {
        _prsprite *s = prsprites[i];

        if ((sprite[i].cstat & 48) == 0 || s == NULL || sprite[i].statnum == MAXSTATUS || sprite[i].sectnum == MAXSECTORS)
            continue;

        if (polymer_planeinlight(&s->plane, light))
            polymer_addplanelight(&s->plane, lighti);
    }
    while (i--);
}

static void         polymer_prepareshadows(void)
{
    fix16_t         oviewangle, oglobalang;
    int32_t         i, j, k;
    int32_t         gx, gy, gz;
    int32_t         oldoverridematerial;

    // for wallvisible()
    gx = globalposx;
    gy = globalposy;
    gz = globalposz;
    // build globals used by drawmasks
    oviewangle = viewangle;
    oglobalang = qglobalang;

    i = j = k = 0;

    while ((k < lightcount) && (j < pr_shadowcount))
    {
        while (!prlights[i].flags.active)
            i++;

        if (prlights[i].radius && prlights[i].publicflags.emitshadow &&
            prlights[i].flags.isinview)
        {
            prlights[i].flags.isinview = 0;
            prlights[i].rtindex = j + 1;
            if (pr_verbosity >= 3) OSD_Printf("PR : Drawing shadow %i...\n", i);

            glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, prrts[prlights[i].rtindex].fbo);
            glPushAttrib(GL_VIEWPORT_BIT);
            glViewport(0, 0, prrts[prlights[i].rtindex].xdim, prrts[prlights[i].rtindex].ydim);

            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

            glMatrixMode(GL_PROJECTION);
            glPushMatrix();
            glLoadMatrixf(prlights[i].proj);
            glMatrixMode(GL_MODELVIEW);
            glLoadMatrixf(prlights[i].transform);

            glEnable(GL_POLYGON_OFFSET_FILL);
            glPolygonOffset(5, SHADOW_DEPTH_OFFSET);

            set_globalpos(prlights[i].x, prlights[i].y, prlights[i].z);

            // build globals used by rotatesprite
            viewangle = fix16_from_int(prlights[i].angle);
            set_globalang(fix16_from_int(prlights[i].angle));

            oldoverridematerial = overridematerial;
            // smooth model shadows
            overridematerial = prprogrambits[PR_BIT_ANIM_INTERPOLATION].bit;
            // used by alpha-testing for sprite silhouette
            overridematerial |= prprogrambits[PR_BIT_DIFFUSE_MAP].bit;
            overridematerial |= prprogrambits[PR_BIT_DIFFUSE_MAP2].bit;

            // to force sprite drawing
            mirrors[depth++].plane = NULL;
            polymer_displayrooms(prlights[i].sector);
            depth--;

            overridematerial = oldoverridematerial;

            glDisable(GL_POLYGON_OFFSET_FILL);

            glMatrixMode(GL_PROJECTION);
            glPopMatrix();

            glPopAttrib();
            glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);

            j++;
        }
        i++;
        k++;
    }

    set_globalpos(gx, gy, gz);

    viewangle = oviewangle;
    set_globalang(oglobalang);
}

// RENDER TARGETS
static void         polymer_initrendertargets(int32_t count)
{
    int32_t         i;

    static int32_t ocount;

    if (count == 0)  // uninit
    {
        if (prrts)
        {
            for (i=0; i<ocount; i++)
            {
                if (prrts[i].color)
                {
                    glDeleteTextures(1, &prrts[i].color);
                    prrts[i].color = 0;
                }
                glDeleteTextures(1, &prrts[i].z);
                prrts[i].z = 0;

                glDeleteFramebuffersEXT(1, &prrts[i].fbo);
                prrts[i].fbo = 0;
            }
            DO_FREE_AND_NULL(prrts);
        }

        ocount = 0;
        return;
    }

    ocount = count;
    //////////

    prrts = (_prrt *)Xcalloc(count, sizeof(_prrt));

    i = 0;
    while (i < count)
    {
        if (!i) {
            prrts[i].target = GL_TEXTURE_RECTANGLE_ARB;
            prrts[i].xdim = xdim;
            prrts[i].ydim = ydim;

            glGenTextures(1, &prrts[i].color);
            polymost_bindTexture(prrts[i].target, prrts[i].color);

            glTexImage2D(prrts[i].target, 0, GL_RGB, prrts[i].xdim, prrts[i].ydim, 0, GL_RGB, GL_SHORT, NULL);
            glTexParameteri(prrts[i].target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
            glTexParameteri(prrts[i].target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
            glTexParameteri(prrts[i].target, GL_TEXTURE_WRAP_S, glinfo.clamptoedge?GL_CLAMP_TO_EDGE:GL_CLAMP);
            glTexParameteri(prrts[i].target, GL_TEXTURE_WRAP_T, glinfo.clamptoedge?GL_CLAMP_TO_EDGE:GL_CLAMP);
        } else {
            prrts[i].target = GL_TEXTURE_2D;
            prrts[i].xdim = 128 << pr_shadowdetail;
            prrts[i].ydim = 128 << pr_shadowdetail;
            prrts[i].color = 0;

            if (pr_ati_fboworkaround) {
                glGenTextures(1, &prrts[i].color);
                polymost_bindTexture(prrts[i].target, prrts[i].color);

                glTexImage2D(prrts[i].target, 0, GL_RGB, prrts[i].xdim, prrts[i].ydim, 0, GL_RGB, GL_SHORT, NULL);
                glTexParameteri(prrts[i].target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
                glTexParameteri(prrts[i].target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
                glTexParameteri(prrts[i].target, GL_TEXTURE_WRAP_S, glinfo.clamptoedge?GL_CLAMP_TO_EDGE:GL_CLAMP);
                glTexParameteri(prrts[i].target, GL_TEXTURE_WRAP_T, glinfo.clamptoedge?GL_CLAMP_TO_EDGE:GL_CLAMP);
            }
        }

        glGenTextures(1, &prrts[i].z);
        polymost_bindTexture(prrts[i].target, prrts[i].z);

        glTexImage2D(prrts[i].target, 0, GL_DEPTH_COMPONENT, prrts[i].xdim, prrts[i].ydim, 0, GL_DEPTH_COMPONENT, GL_SHORT, NULL);
        glTexParameteri(prrts[i].target, GL_TEXTURE_MIN_FILTER, pr_shadowfiltering ? GL_LINEAR : GL_NEAREST);
        glTexParameteri(prrts[i].target, GL_TEXTURE_MAG_FILTER, pr_shadowfiltering ? GL_LINEAR : GL_NEAREST);
        glTexParameteri(prrts[i].target, GL_TEXTURE_WRAP_S, glinfo.clamptoedge?GL_CLAMP_TO_EDGE:GL_CLAMP);
        glTexParameteri(prrts[i].target, GL_TEXTURE_WRAP_T, glinfo.clamptoedge?GL_CLAMP_TO_EDGE:GL_CLAMP);
        glTexParameteri(prrts[i].target, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE);
        glTexParameteri(prrts[i].target, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);
        glTexParameteri(prrts[i].target, GL_DEPTH_TEXTURE_MODE, GL_ALPHA);

        glGenFramebuffersEXT(1, &prrts[i].fbo);
        glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, prrts[i].fbo);

        if (prrts[i].color)
            glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT,
                                       prrts[i].target, prrts[i].color, 0);
        else {
            glDrawBuffer(GL_NONE);
            glReadBuffer(GL_NONE);
        }
        glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, prrts[i].target, prrts[i].z, 0);

        if (glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT) != GL_FRAMEBUFFER_COMPLETE_EXT)
        {
            OSD_Printf("PR : FBO #%d initialization failed.\n", i);
        }

        polymost_bindTexture(prrts[i].target, 0);
        glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);

        i++;
    }
}

// DEBUG OUTPUT
void PR_CALLBACK    polymer_debugoutputcallback(GLenum source,GLenum type,GLuint id,GLenum severity,GLsizei length,const GLchar *message,GLvoid *userParam)
{
    UNREFERENCED_PARAMETER(source);
    UNREFERENCED_PARAMETER(type);
    UNREFERENCED_PARAMETER(id);
    UNREFERENCED_PARAMETER(severity);
    UNREFERENCED_PARAMETER(length);
    UNREFERENCED_PARAMETER(userParam);

    if (type == GL_DEBUG_TYPE_ERROR_ARB)
    {
        OSD_Printf("PR : Received OpenGL debug message: %s\n", message);
    }
}

#endif
