code:
/* MD4C.C - RSA Data Security, Inc., MD4 message-digest algorithm
   Modified from original version published in RFC1320 by
   Jonathon Fowler (jf@jonof.id.au)
 */

/* Copyright (C) 1990-2, RSA Data Security, Inc. All rights reserved.

   License to copy and use this software is granted provided that it
   is identified as the "RSA Data Security, Inc. MD4 Message-Digest
   Algorithm" in all material mentioning or referencing this software
   or this function.

   License is also granted to make and use derivative works provided
   that such works are identified as "derived from the RSA Data
   Security, Inc. MD4 Message-Digest Algorithm" in all material
   mentioning or referencing the derived work.

   RSA Data Security, Inc. makes no representations concerning either
   the merchantability of this software or the suitability of this
   software for any particular purpose. It is provided "as is"
   without express or implied warranty of any kind.

   These notices must be retained in any copies of any part of this
   documentation and/or software.
 */

typedef unsigned char *POINTER;
typedef unsigned char const *RPOINTER;
typedef unsigned short UINT2;
typedef unsigned int  UINT4;

#include "md4.h"
#include "compat.h"

/* Constants for MD4Transform routine.
 */
#define S11 3
#define S12 7
#define S13 11
#define S14 19
#define S21 3
#define S22 5
#define S23 9
#define S24 13
#define S31 3
#define S32 9
#define S33 11
#define S34 15

static void MD4Transform(UINT4 [4], const unsigned char [64]);
static void Encode(unsigned char *, const UINT4 *, unsigned int);
static void Decode(UINT4 *, const unsigned char *, unsigned int);
#define MD4_memcpy Bmemcpy
#define MD4_memset memset

static unsigned char PADDING[64] =
{
    0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
};

/* F, G and H are basic MD4 functions.
 */
#define F(x, y, z) (((x) & (y)) | ((~x) & (z)))
#define G(x, y, z) (((x) & (y)) | ((x) & (z)) | ((y) & (z)))
#define H(x, y, z) ((x) ^ (y) ^ (z))

/* ROTATE_LEFT rotates x left n bits.
 */
#define ROTATE_LEFT(x, n) (((x) << (n)) | ((x) >> (32-(n))))

/* FF, GG and HH are transformations for rounds 1, 2 and 3 */
/* Rotation is separate from addition to prevent recomputation */
#define FF(a, b, c, d, x, s) { \
    (a) += F ((b), (c), (d)) + (x); \
    (a) = ROTATE_LEFT ((a), (s)); \
  }
#define GG(a, b, c, d, x, s) { \
    (a) += G ((b), (c), (d)) + (x) + (UINT4)0x5a827999; \
    (a) = ROTATE_LEFT ((a), (s)); \
  }
#define HH(a, b, c, d, x, s) { \
    (a) += H ((b), (c), (d)) + (x) + (UINT4)0x6ed9eba1; \
    (a) = ROTATE_LEFT ((a), (s)); \
  }

void md4once(const unsigned char *block, unsigned int len, unsigned char digest[16])
{
    MD4_CTX ctx;

    md4init(&ctx);
    md4block(&ctx, block, len);
    md4finish(digest, &ctx);
}

/* MD4 initialization. Begins an MD4 operation, writing a new context.
 */
void md4init(MD4_CTX *context)
{
    context->count[0] = context->count[1] = 0;

    /* Load magic initialization constants.
     */
    context->state[0] = 0x67452301;
    context->state[1] = 0xefcdab89;
    context->state[2] = 0x98badcfe;
    context->state[3] = 0x10325476;
}

/* MD4 block update operation. Continues an MD4 message-digest
     operation, processing another message block, and updating the
     context.
 */
void md4block(MD4_CTX *context, const unsigned char *input, unsigned int inputLen)
{
    unsigned int i, index, partLen;

    /* Compute number of bytes mod 64 */
    index = (unsigned int)((context->count[0] >> 3) & 0x3F);
    /* Update number of bits */
    if ((context->count[0] += ((UINT4)inputLen << 3))
            < ((UINT4)inputLen << 3))
        context->count[1]++;
    context->count[1] += ((UINT4)inputLen >> 29);

    partLen = 64 - index;

    /* Transform as many times as possible.
     */
    if (inputLen >= partLen)
    {
        MD4_memcpy
        ((POINTER)&context->buffer[index], (RPOINTER)input, partLen);
        MD4Transform(context->state, context->buffer);

        for (i = partLen; i + 63 < inputLen; i += 64)
            MD4Transform(context->state, &input[i]);

        index = 0;
    }
    else
        i = 0;

    /* Buffer remaining input */
    MD4_memcpy
    ((POINTER)&context->buffer[index], (RPOINTER)&input[i],
     inputLen-i);
}

/* MD4 finalization. Ends an MD4 message-digest operation, writing the
     the message digest and zeroizing the context.
 */
void md4finish(unsigned char digest[16], MD4_CTX *context)
{
    unsigned char bits[8];
    unsigned int index, padLen;

    /* Save number of bits */
    Encode(bits, context->count, 8);

    /* Pad out to 56 mod 64.
     */
    index = (unsigned int)((context->count[0] >> 3) & 0x3f);
    padLen = (index < 56) ? (56 - index) : (120 - index);
    md4block(context, PADDING, padLen);

    /* Append length (before padding) */
    md4block(context, bits, 8);
    /* Store state in digest */
    Encode(digest, context->state, 16);

    /* Zeroize sensitive information.
     */
    MD4_memset((POINTER)context, 0, sizeof(*context));
}

/* MD4 basic transformation. Transforms state based on block.
 */
static void MD4Transform(UINT4 state[4], const unsigned char block[64])
{
    UINT4 a = state[0], b = state[1], c = state[2], d = state[3], x[16];

    Decode(x, block, 64);

    /* Round 1 */
    FF(a, b, c, d, x[ 0], S11);  /* 1 */
    FF(d, a, b, c, x[ 1], S12);  /* 2 */
    FF(c, d, a, b, x[ 2], S13);  /* 3 */
    FF(b, c, d, a, x[ 3], S14);  /* 4 */
    FF(a, b, c, d, x[ 4], S11);  /* 5 */
    FF(d, a, b, c, x[ 5], S12);  /* 6 */
    FF(c, d, a, b, x[ 6], S13);  /* 7 */
    FF(b, c, d, a, x[ 7], S14);  /* 8 */
    FF(a, b, c, d, x[ 8], S11);  /* 9 */
    FF(d, a, b, c, x[ 9], S12);  /* 10 */
    FF(c, d, a, b, x[10], S13);  /* 11 */
    FF(b, c, d, a, x[11], S14);  /* 12 */
    FF(a, b, c, d, x[12], S11);  /* 13 */
    FF(d, a, b, c, x[13], S12);  /* 14 */
    FF(c, d, a, b, x[14], S13);  /* 15 */
    FF(b, c, d, a, x[15], S14);  /* 16 */

    /* Round 2 */
    GG(a, b, c, d, x[ 0], S21);  /* 17 */
    GG(d, a, b, c, x[ 4], S22);  /* 18 */
    GG(c, d, a, b, x[ 8], S23);  /* 19 */
    GG(b, c, d, a, x[12], S24);  /* 20 */
    GG(a, b, c, d, x[ 1], S21);  /* 21 */
    GG(d, a, b, c, x[ 5], S22);  /* 22 */
    GG(c, d, a, b, x[ 9], S23);  /* 23 */
    GG(b, c, d, a, x[13], S24);  /* 24 */
    GG(a, b, c, d, x[ 2], S21);  /* 25 */
    GG(d, a, b, c, x[ 6], S22);  /* 26 */
    GG(c, d, a, b, x[10], S23);  /* 27 */
    GG(b, c, d, a, x[14], S24);  /* 28 */
    GG(a, b, c, d, x[ 3], S21);  /* 29 */
    GG(d, a, b, c, x[ 7], S22);  /* 30 */
    GG(c, d, a, b, x[11], S23);  /* 31 */
    GG(b, c, d, a, x[15], S24);  /* 32 */

    /* Round 3 */
    HH(a, b, c, d, x[ 0], S31);  /* 33 */
    HH(d, a, b, c, x[ 8], S32);  /* 34 */
    HH(c, d, a, b, x[ 4], S33);  /* 35 */
    HH(b, c, d, a, x[12], S34);  /* 36 */
    HH(a, b, c, d, x[ 2], S31);  /* 37 */
    HH(d, a, b, c, x[10], S32);  /* 38 */
    HH(c, d, a, b, x[ 6], S33);  /* 39 */
    HH(b, c, d, a, x[14], S34);  /* 40 */
    HH(a, b, c, d, x[ 1], S31);  /* 41 */
    HH(d, a, b, c, x[ 9], S32);  /* 42 */
    HH(c, d, a, b, x[ 5], S33);  /* 43 */
    HH(b, c, d, a, x[13], S34);  /* 44 */
    HH(a, b, c, d, x[ 3], S31);  /* 45 */
    HH(d, a, b, c, x[11], S32);  /* 46 */
    HH(c, d, a, b, x[ 7], S33);  /* 47 */
    HH(b, c, d, a, x[15], S34);  /* 48 */

    state[0] += a;
    state[1] += b;
    state[2] += c;
    state[3] += d;

    /* Zeroize sensitive information.
     */
    MD4_memset((POINTER)x, 0, sizeof(x));
}

/* Encodes input (UINT4) into output (unsigned char). Assumes len is
     a multiple of 4.
 */
static void Encode(unsigned char *output, const UINT4 *input, unsigned int len)
{
    unsigned int i, j;

    for (i = 0, j = 0; j < len; i++, j += 4)
    {
        output[j] = (unsigned char)(input[i] & 0xff);
        output[j+1] = (unsigned char)((input[i] >> 8) & 0xff);
        output[j+2] = (unsigned char)((input[i] >> 16) & 0xff);
        output[j+3] = (unsigned char)((input[i] >> 24) & 0xff);
    }
}

/* Decodes input (unsigned char) into output (UINT4). Assumes len is
     a multiple of 4.
 */
static void Decode(UINT4 *output, const unsigned char *input, unsigned int len)
{
    unsigned int i, j;

    for (i = 0, j = 0; j < len; i++, j += 4)
        output[i] = ((UINT4)input[j]) | (((UINT4)input[j+1]) << 8) |
                    (((UINT4)input[j+2]) << 16) | (((UINT4)input[j+3]) << 24);
}

repo_name: orochi/nblood

license: mit


code:
//------------------------------------- MD2/MD3 LIBRARY BEGINS -------------------------------------

#ifdef USE_OPENGL

#include "build.h"
#include "mdsprite.h"
#include "hightile.h"
#include "texcache.h"
#include "kplib.h"
#include "engine_priv.h"
#include "common.h"
#include "polymost.h"

//#include "baselayer.h"
//#include "cache1d.h"
//#include "compat.h"
//#include "glad/glad.h"
#include "glbuild.h"
//#include "palette.h"
//#include "pragmas.h"
//#include "vfs.h"

static int32_t curextra=MAXTILES;

#define MIN_CACHETIME_PRINT 10

static int32_t addtileP(int32_t model,int32_t tile,int32_t pallet)
{
    // tile >= 0 && tile < MAXTILES

    UNREFERENCED_PARAMETER(model);
    if (curextra==MAXTILES+EXTRATILES-1)
    {
        initprintf("warning: max EXTRATILES reached\n");
        return curextra;
    }

    if (tile2model[tile].modelid==-1)
    {
        tile2model[tile].pal=pallet;
        return tile;
    }

    if (tile2model[tile].pal==pallet)
        return tile;

    while (tile2model[tile].nexttile!=-1)
    {
        tile=tile2model[tile].nexttile;
        if (tile2model[tile].pal==pallet)
            return tile;
    }

    tile2model[tile].nexttile=curextra;
    tile2model[curextra].pal=pallet;

    return curextra++;
}

int32_t Ptile2tile(int32_t tile,int32_t palette)
{
    int t = tile;
    while ((tile = tile2model[tile].nexttile) != -1)
        if (tile2model[tile].pal == palette)
        {
            t = tile;
            break;
        }
    return t;
}

#define MODELALLOCGROUP 256
static int32_t nummodelsalloced = 0;

static int32_t maxmodelverts = 0, allocmodelverts = 0;
static int32_t maxmodeltris = 0, allocmodeltris = 0;
static vec3f_t *vertlist = NULL; //temp array to store interpolated vertices for drawing

#ifdef USE_GLEXT
static int32_t allocvbos = 0, curvbo = 0;
static GLuint *vertvbos = NULL;
static GLuint *indexvbos = NULL;
#endif

#ifdef POLYMER
static int32_t *tribuf = NULL;
static int32_t tribufverts = 0;
#endif

static mdmodel_t *mdload(const char *);
static void mdfree(mdmodel_t *);

#ifdef USE_GLEXT
void md_freevbos()
{
    int32_t i;

    for (i=0; i<nextmodelid; i++)
        if (models[i]->mdnum == 3)
        {
            md3model_t *m = (md3model_t *)models[i];
            if (m->vbos)
            {
                //            OSD_Printf("freeing model %d vbo\n",i);
                glDeleteBuffers(m->head.numsurfs, m->vbos);
                DO_FREE_AND_NULL(m->vbos);
            }
        }

    if (allocvbos)
    {
        glDeleteBuffers(allocvbos, indexvbos);
        glDeleteBuffers(allocvbos, vertvbos);
        allocvbos = 0;
    }
}
#endif

void freeallmodels()
{
    int32_t i;

    if (models)
    {
        for (i=0; i<nextmodelid; i++) mdfree(models[i]);
        DO_FREE_AND_NULL(models);
        nummodelsalloced = 0;
        nextmodelid = 0;
    }

    Bmemset(tile2model,-1,sizeof(tile2model));
    for (i=0; i<MAXTILES; i++)
        Bmemset(tile2model[i].hudmem, 0, sizeof(tile2model[i].hudmem));

    curextra=MAXTILES;

    if (vertlist)
    {
        DO_FREE_AND_NULL(vertlist);
        allocmodelverts = maxmodelverts = 0;
        allocmodeltris = maxmodeltris = 0;
    }

#ifdef USE_GLEXT
    md_freevbos();
#endif
#ifdef POLYMER
    DO_FREE_AND_NULL(tribuf);
#endif
}

// Skin texture names can be aliased! This is ugly, but at least correct.
static void nullskintexids(GLuint texid)
{
    int32_t i, j;

    for (i=0; i<nextmodelid; i++)
    {
        mdmodel_t *m = models[i];

        if (m->mdnum == 2 || m->mdnum == 3)
        {
            mdskinmap_t *sk;
            md2model_t *m2 = (md2model_t *)m;

            for (j=0; j < m2->numskins * HICTINT_MEMORY_COMBINATIONS; j++)
                if (m2->texid[j] == texid)
                    m2->texid[j] = 0;

            for (sk=m2->skinmap; sk; sk=sk->next)
                for (j=0; j < HICTINT_MEMORY_COMBINATIONS; j++)
                    if (sk->texid[j] == texid)
                        sk->texid[j] = 0;
        }
    }
}

void clearskins(int32_t type)
{
    int32_t i, j;

    for (i=0; i<nextmodelid; i++)
    {
        mdmodel_t *m = models[i];

        if (m->mdnum == 1)
        {
            voxmodel_t *v = (voxmodel_t *)m;

            for (j=0; j<MAXPALOOKUPS; j++)
                if (v->texid[j])
                {
                    glDeleteTextures(1, &v->texid[j]);
                    v->texid[j] = 0;
                }
        }
        else if ((m->mdnum == 2 || m->mdnum == 3) && type == INVALIDATE_ALL)
        {
            mdskinmap_t *sk;
            md2model_t *m2 = (md2model_t *)m;

            for (j=0; j < m2->numskins * HICTINT_MEMORY_COMBINATIONS; j++)
                if (m2->texid[j])
                {
                    GLuint otexid = m2->texid[j];

                    glDeleteTextures(1, &m2->texid[j]);
                    m2->texid[j] = 0;

                    nullskintexids(otexid);
                }

            for (sk=m2->skinmap; sk; sk=sk->next)
                for (j=0; j < HICTINT_MEMORY_COMBINATIONS; j++)
                    if (sk->texid[j])
                    {
                        GLuint otexid = sk->texid[j];

                        glDeleteTextures(1, &sk->texid[j]);
                        sk->texid[j] = 0;

                        nullskintexids(otexid);
                    }
        }
    }

    for (i=0; i<MAXVOXELS; i++)
    {
        voxmodel_t *v = voxmodels[i];
        if (!v) continue;

        for (j=0; j<MAXPALOOKUPS; j++)
            if (v->texid[j])
            {
                glDeleteTextures(1, &v->texid[j]);
                v->texid[j] = 0;
            }
        if (v->texid8bit)
        {
            glDeleteTextures(1, &v->texid8bit);
            v->texid8bit = 0;
        }
    }
}

void mdinit()
{
    freeallmodels();
    mdinited = 1;
}

int32_t md_loadmodel(const char *fn)
{
    mdmodel_t *vm, **ml;

    if (!mdinited) mdinit();

    if (nextmodelid >= nummodelsalloced)
    {
        ml = (mdmodel_t **)Xrealloc(models,(nummodelsalloced+MODELALLOCGROUP)*sizeof(void *));
        models = ml; nummodelsalloced += MODELALLOCGROUP;
    }

    vm = mdload(fn); if (!vm) return -1;
    models[nextmodelid++] = vm;
    return nextmodelid-1;
}

int32_t md_setmisc(int32_t modelid, float scale, int32_t shadeoff, float zadd, float yoffset, int32_t flags)
{
    mdmodel_t *m;

    if (!mdinited) mdinit();

    if ((uint32_t)modelid >= (uint32_t)nextmodelid) return -1;
    m = models[modelid];
    m->bscale = scale;
    m->shadeoff = shadeoff;
    m->zadd = zadd;
    m->yoffset = yoffset;
    m->flags = flags;

    return 0;
}

static int32_t framename2index(mdmodel_t *vm, const char *nam)
{
    int32_t i = 0;

    switch (vm->mdnum)
    {
    case 2:
    {
        md2model_t *m = (md2model_t *)vm;
        md2frame_t *fr;
        for (i=0; i<m->numframes; i++)
        {
            fr = (md2frame_t *)&m->frames[i*m->framebytes];
            if (!Bstrcmp(fr->name, nam)) break;
        }
    }
    break;
    case 3:
    {
        md3model_t *m = (md3model_t *)vm;
        for (i=0; i<m->numframes; i++)
            if (!Bstrcmp(m->head.frames[i].nam,nam)) break;
    }
    break;
    }
    return i;
}

int32_t md_defineframe(int32_t modelid, const char *framename, int32_t tilenume, int32_t skinnum, float smoothduration, int32_t pal)
{
    md2model_t *m;
    int32_t i;

    if (!mdinited) mdinit();

    if ((uint32_t)modelid >= (uint32_t)nextmodelid) return -1;
    if ((uint32_t)tilenume >= (uint32_t)MAXTILES) return -2;
    if (!framename) return -3;

    tilenume=addtileP(modelid,tilenume,pal);
    m = (md2model_t *)models[modelid];
    if (m->mdnum == 1)
    {
        tile2model[tilenume].modelid = modelid;
        tile2model[tilenume].framenum = tile2model[tilenume].skinnum = 0;
        return 0;
    }

    i = framename2index((mdmodel_t *)m,framename);
    if (i == m->numframes) return -3;   // frame name invalid

    tile2model[tilenume].modelid = modelid;
    tile2model[tilenume].framenum = i;
    tile2model[tilenume].skinnum = skinnum;
    tile2model[tilenume].smoothduration = Blrintf((float)UINT16_MAX * smoothduration);

    return i;
}

int32_t md_defineanimation(int32_t modelid, const char *framestart, const char *frameend, int32_t fpssc, int32_t flags)
{
    md2model_t *m;
    mdanim_t ma, *map;
    int32_t i;

    if (!mdinited) mdinit();

    if ((uint32_t)modelid >= (uint32_t)nextmodelid) return -1;

    Bmemset(&ma, 0, sizeof(ma));
    m = (md2model_t *)models[modelid];
    if (m->mdnum < 2) return 0;

    //find index of start frame
    i = framename2index((mdmodel_t *)m,framestart);
    if (i == m->numframes) return -2;
    ma.startframe = i;

    //find index of finish frame which must trail start frame
    i = framename2index((mdmodel_t *)m,frameend);
    if (i == m->numframes) return -3;
    ma.endframe = i;

    ma.fpssc = fpssc;
    ma.flags = flags;

    map = (mdanim_t *)Xmalloc(sizeof(mdanim_t));

    Bmemcpy(map, &ma, sizeof(ma));

    map->next = m->animations;
    m->animations = map;

    return 0;
}

#if 0
// FIXME: CURRENTLY DISABLED: interpolation may access frames we consider 'unused'?
int32_t md_thinoutmodel(int32_t modelid, uint8_t *usedframebitmap)
{
    md3model_t *m;
    md3surf_t *s;
    mdanim_t *anm;
    int32_t i, surfi, sub, usedframes;
    static int16_t otonframe[1024];

    if ((uint32_t)modelid >= (uint32_t)nextmodelid) return -1;
    m = (md3model_t *)models[modelid];
    if (m->mdnum != 3) return -2;

    for (anm=m->animations; anm; anm=anm->next)
    {
        if (anm->endframe <= anm->startframe)
        {
//            initprintf("backward anim %d-%d\n", anm->startframe, anm->endframe);
            return -3;
        }

        for (i=anm->startframe; i<anm->endframe; i++)
            usedframebitmap[i>>3] |= pow2char[i&7];
    }

    sub = 0;
    for (i=0; i<m->numframes; i++)
    {
        if (!(usedframebitmap[i>>3]&pow2char[i&7]))
        {
            sub++;
            otonframe[i] = -1;
            continue;
        }

        otonframe[i] = i-sub;
    }

    usedframes = m->numframes - sub;
    if (usedframes==0 || usedframes==m->numframes)
        return usedframes;

    //// THIN OUT! ////

    for (i=0; i<m->numframes; i++)
    {
        if (otonframe[i]>=0 && otonframe[i] != i)
        {
            if (m->muladdframes)
                Bmemcpy(&m->muladdframes[2*otonframe[i]], &m->muladdframes[2*i], 2*sizeof(vec3f_t));
            Bmemcpy(&m->head.frames[otonframe[i]], &m->head.frames[i], sizeof(md3frame_t));
        }
    }

    for (surfi=0; surfi < m->head.numsurfs; surfi++)
    {
        s = &m->head.surfs[surfi];

        for (i=0; i<m->numframes; i++)
            if (otonframe[i]>=0 && otonframe[i] != i)
                Bmemcpy(&s->xyzn[otonframe[i]*s->numverts], &s->xyzn[i*s->numverts], s->numverts*sizeof(md3xyzn_t));
    }

    ////// tweak frame indices in various places

    for (anm=m->animations; anm; anm=anm->next)
    {
        if (otonframe[anm->startframe]==-1 || otonframe[anm->endframe-1]==-1)
            initprintf("md %d WTF: anm %d %d\n", modelid, anm->startframe, anm->endframe);

        anm->startframe = otonframe[anm->startframe];
        anm->endframe = otonframe[anm->endframe-1];
    }

    for (i=0; i<MAXTILES+EXTRATILES; i++)
        if (tile2model[i].modelid == modelid)
        {
            if (otonframe[tile2model[i].framenum]==-1)
                initprintf("md %d WTF: tile %d, fr %d\n", modelid, i, tile2model[i].framenum);
            tile2model[i].framenum = otonframe[tile2model[i].framenum];
        }

    ////// realloc & change "numframes" everywhere

    if (m->muladdframes)
        m->muladdframes = Xrealloc(m->muladdframes, 2*sizeof(vec3f_t)*usedframes);
    m->head.frames = Xrealloc(m->head.frames, sizeof(md3frame_t)*usedframes);

    for (surfi=0; surfi < m->head.numsurfs; surfi++)
    {
        m->head.surfs[surfi].numframes = usedframes;
        // CAN'T do that because xyzn is offset from a larger block when loaded from md3:
//        m->head.surfs[surfi].xyzn = Xrealloc(m->head.surfs[surfi].xyzn, s->numverts*usedframes*sizeof(md3xyzn_t));
    }

    m->head.numframes = usedframes;
    m->numframes = usedframes;

    ////////////
    return usedframes;
}
#endif

int32_t md_defineskin(int32_t modelid, const char *skinfn, int32_t palnum, int32_t skinnum, int32_t surfnum, float param, float specpower, float specfactor, int32_t flags)
{
    mdskinmap_t *sk, *skl;
    md2model_t *m;

    if (!mdinited) mdinit();

    if ((uint32_t)modelid >= (uint32_t)nextmodelid) return -1;
    if (!skinfn) return -2;
    if ((unsigned)palnum >= (unsigned)MAXPALOOKUPS) return -3;

    m = (md2model_t *)models[modelid];
    if (m->mdnum < 2) return 0;
    if (m->mdnum == 2) surfnum = 0;

    skl = NULL;
    for (sk = m->skinmap; sk; skl = sk, sk = sk->next)
        if (sk->palette == (uint8_t)palnum && skinnum == sk->skinnum && surfnum == sk->surfnum)
            break;
    if (!sk)
    {
        sk = (mdskinmap_t *)Xcalloc(1,sizeof(mdskinmap_t));

        if (!skl) m->skinmap = sk;
        else skl->next = sk;
    }
    else Xfree(sk->fn);

    sk->palette = (uint8_t)palnum;
    sk->flags = (uint8_t)flags;
    sk->skinnum = skinnum;
    sk->surfnum = surfnum;
    sk->param = param;
    sk->specpower = specpower;
    sk->specfactor = specfactor;
    sk->fn = Xstrdup(skinfn);

    return 0;
}

int32_t md_definehud(int32_t modelid, int32_t tilex, vec3f_t add, int32_t angadd, int32_t flags, int32_t fov)
{
    if (!mdinited) mdinit();

    if ((uint32_t)modelid >= (uint32_t)nextmodelid) return -1;
    if ((uint32_t)tilex >= (uint32_t)MAXTILES) return -2;

    tile2model[tilex].hudmem[(flags>>2)&1] = (hudtyp *)Xmalloc(sizeof(hudtyp));

    hudtyp * const hud = tile2model[tilex].hudmem[(flags>>2)&1];

    hud->add = add;
    hud->angadd = ((int16_t)angadd)|2048;
    hud->flags = (int16_t)flags;
    hud->fov = (int16_t)fov;

    return 0;
}

int32_t md_undefinetile(int32_t tile)
{
    if (!mdinited) return 0;
    if ((unsigned)tile >= (unsigned)MAXTILES) return -1;

    tile2model[tile].modelid = -1;
    tile2model[tile].nexttile = -1;
    DO_FREE_AND_NULL(tile2model[tile].hudmem[0]);
    DO_FREE_AND_NULL(tile2model[tile].hudmem[1]);
    return 0;
}

/* this function is problematic, it leaves NULL holes in model[]
 * (which runs from 0 to nextmodelid-1) */
int32_t md_undefinemodel(int32_t modelid)
{
    int32_t i;
    if (!mdinited) return 0;
    if ((uint32_t)modelid >= (uint32_t)nextmodelid) return -1;

    for (i=MAXTILES+EXTRATILES-1; i>=0; i--)
        if (tile2model[i].modelid == modelid)
        {
            tile2model[i].modelid = -1;
            DO_FREE_AND_NULL(tile2model[i].hudmem[0]);
            DO_FREE_AND_NULL(tile2model[i].hudmem[1]);
        }

    if (models)
    {
        mdfree(models[modelid]);
        models[modelid] = NULL;
    }

    return 0;
}

static int32_t mdloadskin_notfound(char * const skinfile, char const * const fn)
{
    OSD_Printf("Skin \"%s\" not found.\n", fn);

    skinfile[0] = 0;
    return 0;
}

static int32_t mdloadskin_failed(char * const skinfile, char const * const fn)
{
    OSD_Printf("Failed loading skin file \"%s\".\n", fn);

    skinfile[0] = 0;
    return 0;
}

//Note: even though it says md2model, it works for both md2model&md3model
int32_t mdloadskin(md2model_t *m, int32_t number, int32_t pal, int32_t surf)
{
    int32_t i;
    char *skinfile = NULL, fn[BMAX_PATH];
    GLuint *texidx = NULL;
    mdskinmap_t *sk, *skzero = NULL;
    int32_t doalloc = 1;

    if (m->mdnum == 2)
        surf = 0;

    if ((unsigned)pal >= (unsigned)MAXPALOOKUPS)
        return 0;

    i = -1;
    for (sk = m->skinmap; sk; sk = sk->next)
    {
        if (sk->palette == pal && sk->skinnum == number && sk->surfnum == surf)
        {
            skinfile = sk->fn;
            texidx = &sk->texid[hicfxid(pal)];
            Bstrncpyz(fn, skinfile, BMAX_PATH);
            //OSD_Printf("Using exact match skin (pal=%d,skinnum=%d,surfnum=%d) %s\n",pal,number,surf,skinfile);
            break;
        }
        //If no match, give highest priority to number, then pal.. (Parkar's request, 02/27/2005)
        else if ((sk->palette ==   0) && (sk->skinnum == number) && (sk->surfnum == surf) && (i < 5)) { i = 5; skzero = sk; }
        else if ((sk->palette == pal) && (sk->skinnum ==      0) && (sk->surfnum == surf) && (i < 4)) { i = 4; skzero = sk; }
        else if ((sk->palette ==   0) && (sk->skinnum ==      0) && (sk->surfnum == surf) && (i < 3)) { i = 3; skzero = sk; }
        else if ((sk->palette ==   0) && (sk->skinnum == number) && (i < 2)) { i = 2; skzero = sk; }
        else if ((sk->palette == pal) && (sk->skinnum ==      0) && (i < 1)) { i = 1; skzero = sk; }
        else if ((sk->palette ==   0) && (sk->skinnum ==      0) && (i < 0)) { i = 0; skzero = sk; }
    }

    if (!sk)
    {
        if (pal >= (MAXPALOOKUPS - RESERVEDPALS))
            return 0;

        if (skzero)
        {
            skinfile = skzero->fn;
            texidx = &skzero->texid[hicfxid(pal)];
            Bstrncpyz(fn, skinfile, BMAX_PATH);
            //OSD_Printf("Using def skin 0,0 as fallback, pal=%d\n", pal);
        }
        else
            return 0;
#if 0
        {
            // fall back to the model-defined texture
            if ((unsigned)number >= (unsigned)m->numskins)
                number = 0;

            // m->skinfn is undefined when md3model_t is cast to md2model_t --> crash
            skinfile = m->skinfn + number*64;
            texidx = &m->texid[number * HICTINT_MEMORY_COMBINATIONS + hicfxid(pal)];
            Bstrncpyz(fn, m->basepath, BMAX_PATH);
            if ((Bstrlen(fn) + Bstrlen(skinfile)) < BMAX_PATH)
                Bstrcat(fn,skinfile);
            //OSD_Printf("Using MD2/MD3 skin (%d) %s, pal=%d\n",number,skinfile,pal);
        }
#endif
    }

    if (skinfile == NULL || !skinfile[0])
        return 0;

    if (texidx && *texidx)
        return *texidx;

    // possibly fetch an already loaded multitexture :_)
    if (pal >= (MAXPALOOKUPS - RESERVEDPALS))
        for (i=0; i<nextmodelid; i++)
            for (skzero = ((md2model_t *)models[i])->skinmap; skzero && sk; skzero = skzero->next)
                if (!Bstrcasecmp(skzero->fn, sk->fn) && skzero->texid[hicfxid(pal)])
                {
                    size_t f = hicfxid(pal);

                    sk->texid[f] = skzero->texid[f];
                    return sk->texid[f];
                }

    // for sk->flags below
    if (!sk)
        sk = skzero;

    *texidx = 0;

    buildvfs_kfd filh;
    if ((filh = kopen4load(fn, 0)) == buildvfs_kfd_invalid)
        return mdloadskin_notfound(skinfile, fn);

    int32_t picfillen = kfilelength(filh);
    kclose(filh);	// FIXME: shouldn't have to do this. bug in cache1d.c

    int32_t startticks = timerGetTicks();
    extern int gloadtile_willprint;

    char hasalpha;
    texcacheheader cachead;
    char texcacheid[BMAX_PATH];
    texcache_calcid(texcacheid, fn, picfillen, pal<<8, hicfxmask(pal));
    int32_t gotcache = texcache_readtexheader(texcacheid, &cachead, 1);
    vec2_t siz = { 0, 0 }, tsiz = { 0, 0 };

    if (gotcache && !texcache_loadskin(&cachead, &doalloc, texidx, &siz))
    {
        tsiz.x = cachead.xdim;
        tsiz.y = cachead.ydim;
        hasalpha = !!(cachead.flags & CACHEAD_HASALPHA);

        if (pal < (MAXPALOOKUPS - RESERVEDPALS))
            m->usesalpha = hasalpha;
    }
    else
    {
        // CODEDUP: gloadtile_hi
        gotcache = 0;	// the compressed version will be saved to disk

        char al = 255;
        char onebitalpha = 1;

        auto pic = gloadtruecolortile_mdloadskin_shared(fn, picfillen, &tsiz, &siz, &onebitalpha, hicfxmask(pal), pal, &al);
        if (!pic) return mdloadskin_failed(skinfile, fn);
        hasalpha = (al != 255);

        // mdloadskin doesn't duplicate npow2 texture pixels

        if (!glinfo.bgra)
        {
            for (bssize_t j = siz.x*siz.y - 1; j >= 0; j--)
                swapchar(&pic[j].r, &pic[j].b);
        }

        if (pal < (MAXPALOOKUPS - RESERVEDPALS))
            m->usesalpha = hasalpha;
        if ((doalloc&3)==1)
            glGenTextures(1, texidx);

        buildgl_bindTexture(GL_TEXTURE_2D, *texidx);

        //gluBuild2DMipmaps(GL_TEXTURE_2D,GL_RGBA,xsiz,ysiz,GL_BGRA_EXT,GL_UNSIGNED_BYTE,(char *)fptr);

        int32_t const texfmt = glinfo.bgra ? GL_BGRA : GL_RGBA;

        uploadtexture((doalloc&1), siz, texfmt, pic, tsiz,
                      DAMETH_HI | DAMETH_MASK |
                      TO_DAMETH_NODOWNSIZE(sk->flags) |
                      TO_DAMETH_NOTEXCOMPRESS(sk->flags) |
                      TO_DAMETH_ARTIMMUNITY(sk->flags) |
                      (onebitalpha ? DAMETH_ONEBITALPHA : 0) |
                      (hasalpha ? DAMETH_HASALPHA : 0));

        Xfree(pic);
    }

    if (!m->skinloaded)
    {
        if (siz.x != tsiz.x || siz.y != tsiz.y)
        {
            float fx, fy;
            fx = ((float)tsiz.x)/((float)siz.x);
            fy = ((float)tsiz.y)/((float)siz.y);
            if (m->mdnum == 2)
            {
                int32_t *lptr;
                for (lptr=m->glcmds; (i=*lptr++);)
                    for (i=labs(i); i>0; i--,lptr+=3)
                    {
                        ((float *)lptr)[0] *= fx;
                        ((float *)lptr)[1] *= fy;
                    }
            }
            else if (m->mdnum == 3)
            {
                md3model_t *m3 = (md3model_t *)m;
                md3surf_t *s;
                int32_t surfi;
                for (surfi=0; surfi<m3->head.numsurfs; surfi++)
                {
                    s = &m3->head.surfs[surfi];
                    for (i=s->numverts-1; i>=0; i--)
                    {
                        s->uv[i].u *= fx;
                        s->uv[i].v *= fy;
                    }
                }
            }
        }
        m->skinloaded = 1+number;
    }

    int32_t const filter = (sk->flags & HICR_FORCEFILTER) ? TEXFILTER_ON : gltexfiltermode;

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,glfiltermodes[filter].mag);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,glfiltermodes[filter].min);
    glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_MAX_ANISOTROPY_EXT, polymost_getanisotropy(filter));
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_REPEAT);

#if defined USE_GLEXT && !defined EDUKE32_GLES
    if (!gotcache && glinfo.texcompr && glusetexcache && !(sk->flags & HICR_NOTEXCOMPRESS) &&
        (glusetexcompr == 2 || (glusetexcompr && !(sk->flags & HICR_ARTIMMUNITY))))
        {
            const int32_t nonpow2 = check_nonpow2(siz.x) || check_nonpow2(siz.y);

            // save off the compressed version
            cachead.quality = (sk->flags & (HICR_NODOWNSIZE|HICR_ARTIMMUNITY)) ? 0 : r_downsize;
            cachead.xdim = tsiz.x>>cachead.quality;
            cachead.ydim = tsiz.y>>cachead.quality;

            cachead.flags = nonpow2*CACHEAD_NONPOW2 | (hasalpha ? CACHEAD_HASALPHA : 0) |
                            ((sk->flags & (HICR_NODOWNSIZE|HICR_ARTIMMUNITY)) ? CACHEAD_NODOWNSIZE : 0);

///            OSD_Printf("Caching \"%s\"\n",fn);
            texcache_writetex_fromdriver(texcacheid, &cachead);

            if (gloadtile_willprint)
            {
                int32_t etime = timerGetTicks()-startticks;
                if (etime>=MIN_CACHETIME_PRINT)
                    OSD_Printf("Load skin: p%d-e%d \"%s\"... cached... %d ms\n", pal, hicfxmask(pal), fn, etime);
                gloadtile_willprint = 0;
            }
            else
                OSD_Printf("Cached skin \"%s\"\n", fn);
        }
#endif

    if (gloadtile_willprint)
    {
        int32_t etime = timerGetTicks()-startticks;
        if (etime>=MIN_CACHETIME_PRINT)
            OSD_Printf("Load skin: p%d-e%d \"%s\"... %d ms\n", pal, hicfxmask(pal), fn, etime);
    }

    return (*texidx);
}

//Note: even though it says md2model, it works for both md2model&md3model
void updateanimation(md2model_t *m, tspriteptr_t tspr, uint8_t lpal)
{
    if (m->numframes < 2)
    {
        m->interpol = 0;
        return;
    }

    int32_t const tile = Ptile2tile(tspr->picnum,lpal);
    m->cframe = m->nframe = tile2model[tile].framenum;

#ifdef DEBUGGINGAIDS
    if (m->cframe >= m->numframes)
        OSD_Printf("1: c > n\n");
#endif

    int32_t const smoothdurationp = (r_animsmoothing && (tile2model[tile].smoothduration != 0));
    spritesmooth_t * const smooth = &spritesmooth[((unsigned)tspr->owner < MAXSPRITES+MAXUNIQHUDID) ? tspr->owner : MAXSPRITES+MAXUNIQHUDID-1];
    spriteext_t * const sprext = &spriteext[((unsigned)tspr->owner < MAXSPRITES+MAXUNIQHUDID) ? tspr->owner : MAXSPRITES+MAXUNIQHUDID-1];

    const mdanim_t *anim;
    for (anim = m->animations; anim && anim->startframe != m->cframe; anim = anim->next)
    {
        /* do nothing */;
    }

    int32_t i, j, k;
    int32_t fps;

    if (!anim)
    {
        if (!smoothdurationp || ((smooth->mdoldframe == m->cframe) && (smooth->mdcurframe == m->cframe)))
        {
            m->interpol = 0;
            return;
        }

        // assert(smoothdurationp && ((smooth->mdoldframe != m->cframe) || (smooth->mdcurframe != m->cframe)))

        if (smooth->mdoldframe != m->cframe)
        {
            if (smooth->mdsmooth == 0)
            {
                sprext->mdanimtims = mdtims;
                m->interpol = 0;
                smooth->mdsmooth = 1;
                smooth->mdcurframe = m->cframe;
            }
            else if (smooth->mdcurframe != m->cframe)
            {
                sprext->mdanimtims = mdtims;
                m->interpol = 0;
                smooth->mdsmooth = 1;
                smooth->mdoldframe = smooth->mdcurframe;
                smooth->mdcurframe = m->cframe;
            }
        }
        else  // if (smooth->mdcurframe != m->cframe)
        {
            sprext->mdanimtims = mdtims;
            m->interpol = 0;
            smooth->mdsmooth = 1;
            smooth->mdoldframe = smooth->mdcurframe;
            smooth->mdcurframe = m->cframe;
        }
    }
    else if (/* anim && */ sprext->mdanimcur != anim->startframe)
    {
        //if (sprext->flags & SPREXT_NOMDANIM) OSD_Printf("SPREXT_NOMDANIM\n");
        //OSD_Printf("smooth launched ! oldanim %i new anim %i\n", sprext->mdanimcur, anim->startframe);
        sprext->mdanimcur = (int16_t)anim->startframe;
        sprext->mdanimtims = mdtims;
        m->interpol = 0;

        if (!smoothdurationp)
        {
            m->cframe = m->nframe = anim->startframe;
#ifdef DEBUGGINGAIDS
            if (m->cframe >= m->numframes)
                OSD_Printf("2: c > n\n");
#endif
            goto prep_return;
        }

        m->nframe = anim->startframe;
        m->cframe = smooth->mdoldframe;
#ifdef DEBUGGINGAIDS
        if (m->cframe >= m->numframes)
            OSD_Printf("3: c > n\n");
#endif
        smooth->mdsmooth = 1;
        goto prep_return;
    }

    fps = smooth->mdsmooth ? Blrintf((1.0f / ((float)tile2model[tile].smoothduration * (1.f / (float)UINT16_MAX))) * 66.f)
                                   : anim ? anim->fpssc : 1;

    i = (mdtims - sprext->mdanimtims) * ((fps * timerGetClockRate()) / 120);

    j = (smooth->mdsmooth || !anim) ? 65536 : ((anim->endframe + 1 - anim->startframe) << 16);

    // XXX: Just in case you play the game for a VERY long time...
    if (i < 0) { i = 0; sprext->mdanimtims = mdtims; }
    //compare with j*2 instead of j to ensure i stays > j-65536 for MDANIM_ONESHOT
    if (anim && (i >= j+j) && (fps) && !mdpause) //Keep mdanimtims close to mdtims to avoid the use of MOD
        sprext->mdanimtims += j/((fps*timerGetClockRate())/120);

    k = i;

    if (anim && (anim->flags&MDANIM_ONESHOT))
        { if (i > j-65536) i = j-65536; }
    else { if (i >= j) { i -= j; if (i >= j) i %= j; } }

    if (r_animsmoothing && smooth->mdsmooth)
    {
        m->nframe = anim ? anim->startframe : smooth->mdcurframe;
        m->cframe = smooth->mdoldframe;
#ifdef DEBUGGINGAIDS
        if (m->cframe >= m->numframes)
            OSD_Printf("4: c > n\n");
#endif
        //OSD_Printf("smoothing... cframe %i nframe %i\n", m->cframe, m->nframe);
        if (k > 65535)
        {
            sprext->mdanimtims = mdtims;
            m->interpol = 0;
            smooth->mdsmooth = 0;
            m->cframe = m->nframe; // = anim ? anim->startframe : smooth->mdcurframe;
#ifdef DEBUGGINGAIDS
            if (m->cframe >= m->numframes)
                OSD_Printf("5: c > n\n");
#endif
            smooth->mdoldframe = m->cframe;
            //OSD_Printf("smooth stopped !\n");
            goto prep_return;
        }
    }
    else
    {
        if (anim)
            m->cframe = (i>>16)+anim->startframe;

#ifdef DEBUGGINGAIDS
        if (m->cframe >= m->numframes)
            OSD_Printf("6: c > n\n");
#endif
        m->nframe = m->cframe+1;

        if (anim && m->nframe > anim->endframe)  // VERIFY: (!(r_animsmoothing && smooth->mdsmooth)) implies (anim!=NULL) ?
            m->nframe = anim->startframe;

        smooth->mdoldframe = m->cframe;
        //OSD_Printf("not smoothing... cframe %i nframe %i\n", m->cframe, m->nframe);
    }

    m->interpol = ((float)(i&65535))/65536.f;
    //OSD_Printf("interpol %f\n", m->interpol);

prep_return:
    if (m->cframe >= m->numframes)
        m->cframe = 0;
    if (m->nframe >= m->numframes)
        m->nframe = 0;
}

#ifdef USE_GLEXT
// VBO generation and allocation
static void mdloadvbos(md3model_t *m)
{
    int32_t     i;

    m->vbos = (GLuint *)Xmalloc(m->head.numsurfs * sizeof(GLuint));
    glGenBuffers(m->head.numsurfs, m->vbos);

    i = 0;
    while (i < m->head.numsurfs)
    {
        buildgl_bindBuffer(GL_ARRAY_BUFFER, m->vbos[i]);
        glBufferData(GL_ARRAY_BUFFER, m->head.surfs[i].numverts * sizeof(md3uv_t), m->head.surfs[i].uv, GL_STATIC_DRAW);
        i++;
    }
    buildgl_bindBuffer(GL_ARRAY_BUFFER, 0);
}
#endif

//--------------------------------------- MD2 LIBRARY BEGINS ---------------------------------------
static void md2free(md2model_t *m)
{
    if (!m)
        return;

    Xfree(m->texid);
    Xfree(m->skinfn);
    Xfree(m->basepath);
    Xfree(m->uv);
    Xfree(m->tris);
    Xfree(m->glcmds);
    Xfree(m->frames);
    Xfree(m);
}

static md2model_t *md2load(buildvfs_kfd fil, const char *filnam)
{
    md2model_t *m;
    md3model_t *m3;
    md3surf_t *s;
    md2frame_t *f;
    md2head_t head;
    char st[BMAX_PATH];
    int32_t i, j, k;

    int32_t ournumskins, ournumglcmds;

    m = (md2model_t *)Xcalloc(1,sizeof(md2model_t));
    m->mdnum = 2; m->scale = .01f;

    if (kread_and_test(fil,(char *)&head,sizeof(md2head_t))) { Xfree(m); return nullptr; }

#if B_BIG_ENDIAN != 0
    head.id = B_LITTLE32(head.id);                 head.vers = B_LITTLE32(head.vers);
    head.skinxsiz = B_LITTLE32(head.skinxsiz);     head.skinysiz = B_LITTLE32(head.skinysiz);
    head.framebytes = B_LITTLE32(head.framebytes); head.numskins = B_LITTLE32(head.numskins);
    head.numverts = B_LITTLE32(head.numverts);     head.numuv = B_LITTLE32(head.numuv);
    head.numtris = B_LITTLE32(head.numtris);       head.numglcmds = B_LITTLE32(head.numglcmds);
    head.numframes = B_LITTLE32(head.numframes);   head.ofsskins = B_LITTLE32(head.ofsskins);
    head.ofsuv = B_LITTLE32(head.ofsuv);           head.ofstris = B_LITTLE32(head.ofstris);
    head.ofsframes = B_LITTLE32(head.ofsframes);   head.ofsglcmds = B_LITTLE32(head.ofsglcmds);
    head.ofseof = B_LITTLE32(head.ofseof);
#endif

    if ((head.id != IDP2_MAGIC) || (head.vers != 8)) { Xfree(m); return 0; } //"IDP2"

    ournumskins = head.numskins ? head.numskins : 1;
    ournumglcmds = head.numglcmds ? head.numglcmds : 1;

    m->numskins = head.numskins;
    m->numframes = head.numframes;
    m->numverts = head.numverts;
    m->numglcmds = head.numglcmds;
    m->framebytes = head.framebytes;

    m->frames = (char *)Xmalloc(m->numframes*m->framebytes);
    m->glcmds = (int32_t *)Xmalloc(ournumglcmds*sizeof(int32_t));
    m->tris = (md2tri_t *)Xmalloc(head.numtris*sizeof(md2tri_t));
    m->uv = (md2uv_t *)Xmalloc(head.numuv*sizeof(md2uv_t));

    klseek(fil,head.ofsframes,SEEK_SET);
    if (kread_and_test(fil,(char *)m->frames,m->numframes*m->framebytes)) { md2free(m); return nullptr; }
    if (m->numglcmds > 0)
    {
        klseek(fil, head.ofsglcmds, SEEK_SET);
        if (kread_and_test(fil, (char *)m->glcmds, m->numglcmds * sizeof(int32_t))) { md2free(m); return nullptr; }
    }

    klseek(fil, head.ofstris, SEEK_SET);
    if (kread_and_test(fil, (char *)m->tris, head.numtris * sizeof(md2tri_t))) { md2free(m); return nullptr; }
    klseek(fil, head.ofsuv, SEEK_SET);
    if (kread_and_test(fil, (char *)m->uv, head.numuv * sizeof(md2uv_t))) { md2free(m); return nullptr; }
#if B_BIG_ENDIAN != 0
    {
        char *f = (char *)m->frames;
        int32_t *l,j;
        md2frame_t *fr;

        for (i = m->numframes-1; i>=0; i--)
        {
            fr = (md2frame_t *)f;
            l = (int32_t *)&fr->mul;
            for (j=5; j>=0; j--) l[j] = B_LITTLE32(l[j]);
            f += m->framebytes;
        }

        for (i = m->numglcmds-1; i>=0; i--)
        {
            m->glcmds[i] = B_LITTLE32(m->glcmds[i]);
        }
        for (i = head.numtris-1; i>=0; i--)
        {
            m->tris[i].v[0] = B_LITTLE16(m->tris[i].v[0]);
            m->tris[i].v[1] = B_LITTLE16(m->tris[i].v[1]);
            m->tris[i].v[2] = B_LITTLE16(m->tris[i].v[2]);
            m->tris[i].u[0] = B_LITTLE16(m->tris[i].u[0]);
            m->tris[i].u[1] = B_LITTLE16(m->tris[i].u[1]);
            m->tris[i].u[2] = B_LITTLE16(m->tris[i].u[2]);
        }
        for (i = head.numuv-1; i>=0; i--)
        {
            m->uv[i].u = B_LITTLE16(m->uv[i].u);
            m->uv[i].v = B_LITTLE16(m->uv[i].v);
        }
    }
#endif

    Bstrcpy(st,filnam);
    for (i=strlen(st)-1; i>0; i--)
        if ((st[i] == '/') || (st[i] == '\\')) { i++; break; }
    if (i<0) i=0;
    st[i] = 0;
    m->basepath = (char *)Xmalloc(i+1);
    Bstrcpy(m->basepath, st);

    m->skinfn = (char *)Xmalloc(ournumskins*64);
    if (m->numskins > 0)
    {
        klseek(fil,head.ofsskins,SEEK_SET);
        if (kread_and_test(fil,m->skinfn,64*m->numskins)) { md2free(m); return nullptr; }
    }

    m->texid = (GLuint *)Xcalloc(ournumskins, sizeof(GLuint) * HICTINT_MEMORY_COMBINATIONS);

    maxmodelverts = max(maxmodelverts, m->numverts);
    maxmodeltris = max(maxmodeltris, head.numtris);

    //return m;

    // the MD2 is now loaded internally - let's begin the MD3 conversion process
    //OSD_Printf("Beginning md3 conversion.\n");
    m3 = (md3model_t *)Xcalloc(1, sizeof(md3model_t));
    m3->mdnum = 3; m3->texid = 0; m3->scale = m->scale;
    m3->head.id = IDP3_MAGIC; m3->head.vers = 15;

    m3->head.flags = 0;

    m3->head.numframes = m->numframes;
    m3->head.numtags = 0; m3->head.numsurfs = 1;
    m3->head.numskins = 0;

    m3->numskins = m3->head.numskins;
    m3->numframes = m3->head.numframes;

    m3->head.frames = (md3frame_t *)Xcalloc(m3->head.numframes, sizeof(md3frame_t));
    m3->muladdframes = (vec3f_t *)Xcalloc(m->numframes * 2, sizeof(vec3f_t));

    f = (md2frame_t *)(m->frames);

    // frames converting
    i = 0;
    while (i < m->numframes)
    {
        f = (md2frame_t *)&m->frames[i*m->framebytes];
        Bstrcpy(m3->head.frames[i].nam, f->name);
        //OSD_Printf("Copied frame %s.\n", m3->head.frames[i].nam);
        m3->muladdframes[i*2] = f->mul;
        m3->muladdframes[i*2+1] = f->add;
        i++;
    }

    m3->head.tags = NULL;

    m3->head.surfs = (md3surf_t *)Xcalloc(1, sizeof(md3surf_t));
    s = m3->head.surfs;

    // model converting
    s->id = IDP3_MAGIC; s->flags = 0;
    s->numframes = m->numframes; s->numshaders = 0;
    s->numtris = head.numtris;
    s->numverts = head.numtris * 3; // oh man talk about memory effectiveness :((((
    // MD2 is actually more accurate than MD3 in term of uv-mapping, because each triangle has a triangle counterpart on the UV-map.
    // In MD3, each vertex unique UV coordinates, meaning that you have to duplicate vertices if you need non-seamless UV-mapping.

    maxmodelverts = max(maxmodelverts, s->numverts);

    Bstrcpy(s->nam, "Dummy surface from MD2");

    s->shaders = NULL;

    s->tris = (md3tri_t *)Xcalloc(head.numtris, sizeof(md3tri_t));
    s->uv = (md3uv_t *)Xcalloc(s->numverts, sizeof(md3uv_t));
    s->xyzn = (md3xyzn_t *)Xcalloc(s->numverts * m->numframes, sizeof(md3xyzn_t));

    //memoryusage += (s->numverts * m->numframes * sizeof(md3xyzn_t));
    //OSD_Printf("Current model geometry memory usage : %i.\n", memoryusage);

    //OSD_Printf("Number of frames : %i\n", m->numframes);
    //OSD_Printf("Number of triangles : %i\n", head.numtris);
    //OSD_Printf("Number of vertices : %i\n", s->numverts);

    // triangle converting
    i = 0;
    while (i < head.numtris)
    {
        j = 0;
        //OSD_Printf("Triangle : %i\n", i);
        while (j < 3)
        {
            // triangle vertex indexes
            s->tris[i].i[j] = i*3 + j;

            // uv coords
            s->uv[i*3+j].u = (float)(m->uv[m->tris[i].u[j]].u) / (float)(head.skinxsiz);
            s->uv[i*3+j].v = (float)(m->uv[m->tris[i].u[j]].v) / (float)(head.skinysiz);

            // vertices for each frame
            k = 0;
            while (k < m->numframes)
            {
                f = (md2frame_t *)&m->frames[k*m->framebytes];
                s->xyzn[(k*s->numverts) + (i*3) + j].x = (int16_t) (((f->verts[m->tris[i].v[j]].v[0] * f->mul.x) + f->add.x) * 64.f);
                s->xyzn[(k*s->numverts) + (i*3) + j].y = (int16_t) (((f->verts[m->tris[i].v[j]].v[1] * f->mul.y) + f->add.y) * 64.f);
                s->xyzn[(k*s->numverts) + (i*3) + j].z = (int16_t) (((f->verts[m->tris[i].v[j]].v[2] * f->mul.z) + f->add.z) * 64.f);

                k++;
            }
            j++;
        }
        //OSD_Printf("End triangle.\n");
        i++;
    }
    //OSD_Printf("Finished md3 conversion.\n");

    {
        mdskinmap_t *sk;

        sk = (mdskinmap_t *)Xcalloc(1,sizeof(mdskinmap_t));
        sk->palette = 0;
        sk->skinnum = 0;
        sk->surfnum = 0;

        if (m->numskins > 0)
        {
            sk->fn = (char *)Xmalloc(strlen(m->basepath)+strlen(m->skinfn)+1);
            Bstrcpy(sk->fn, m->basepath);
            Bstrcat(sk->fn, m->skinfn);
        }
        m3->skinmap = sk;
    }

    m3->indexes = (uint16_t *)Xmalloc(sizeof(uint16_t) * s->numtris);
    m3->vindexes = (uint16_t *)Xmalloc(sizeof(uint16_t) * s->numtris * 3);
    m3->maxdepths = (float *)Xmalloc(sizeof(float) * s->numtris);

    m3->vbos = NULL;

    // die MD2 ! DIE !
    md2free(m);

    return ((md2model_t *)m3);
}
//---------------------------------------- MD2 LIBRARY ENDS ----------------------------------------

// DICHOTOMIC RECURSIVE SORTING - USED BY MD3DRAW
int32_t partition(uint16_t *indexes, float *depths, int32_t f, int32_t l)
{
    int32_t up = f, down = l;
    float piv = depths[f];
    uint16_t piv2 = indexes[f];
    do
    {
        while ((up < l) && (depths[up] <= piv))
            up++;
        while ((depths[down] > piv) && (down > f))
            down--;
        if (up < down)
        {
            swapfloat(&depths[up], &depths[down]);
            swapshort(&indexes[up], &indexes[down]);
        }
    }
    while (down > up);
    depths[f] = depths[down];
    depths[down] = piv;
    indexes[f] = indexes[down];
    indexes[down] = piv2;
    return down;
}

static inline void quicksort(uint16_t *indexes, float *depths, int32_t first, int32_t last)
{
    int32_t pivIndex;
    if (first >= last) return;
    pivIndex = partition(indexes, depths, first, last);
    if (first < (pivIndex-1)) quicksort(indexes, depths, first, (pivIndex-1));
    if ((pivIndex+1) >= last) return;
    quicksort(indexes, depths, (pivIndex+1), last);
}
// END OF QUICKSORT LIB

//--------------------------------------- MD3 LIBRARY BEGINS ---------------------------------------
static void md3free(md3model_t *m);

static md3model_t *md3load(buildvfs_kfd fil)
{
    int32_t i, surfi, ofsurf, offs[4], leng[4];
    int32_t maxtrispersurf;
    md3model_t *m;
    md3surf_t *s;

    m = (md3model_t *)Xcalloc(1,sizeof(md3model_t));
    m->mdnum = 3; m->texid = 0; m->scale = .01f;

    m->muladdframes = NULL;

    if (kread_and_test(fil, &m->head, SIZEOF_MD3HEAD_T)) { md3free(m); return nullptr; }

#if B_BIG_ENDIAN != 0
    m->head.id = B_LITTLE32(m->head.id);             m->head.vers = B_LITTLE32(m->head.vers);
    m->head.flags = B_LITTLE32(m->head.flags);       m->head.numframes = B_LITTLE32(m->head.numframes);
    m->head.numtags = B_LITTLE32(m->head.numtags);   m->head.numsurfs = B_LITTLE32(m->head.numsurfs);
    m->head.numskins = B_LITTLE32(m->head.numskins); m->head.ofsframes = B_LITTLE32(m->head.ofsframes);
    m->head.ofstags = B_LITTLE32(m->head.ofstags); m->head.ofssurfs = B_LITTLE32(m->head.ofssurfs);
    m->head.eof = B_LITTLE32(m->head.eof);
#endif

    if ((m->head.id != IDP3_MAGIC) && (m->head.vers != 15)) { md3free(m); return nullptr; }

    m->numskins = m->head.numskins; //<- dead code?
    m->numframes = m->head.numframes;

    ofsurf = m->head.ofssurfs;

    klseek(fil,m->head.ofsframes,SEEK_SET); i = m->head.numframes*sizeof(md3frame_t);
    m->head.frames = (md3frame_t *)Xmalloc(i);
    if (kread_and_test(fil,m->head.frames,i)) { md3free(m); return nullptr; }

    if (m->head.numtags == 0) m->head.tags = NULL;
    else
    {
        klseek(fil,m->head.ofstags,SEEK_SET); i = m->head.numtags*sizeof(md3tag_t);
        m->head.tags = (md3tag_t *)Xmalloc(i);
        if (kread_and_test(fil,m->head.tags,i)) { md3free(m); return nullptr; }
    }

    klseek(fil,m->head.ofssurfs,SEEK_SET);
    m->head.surfs = (md3surf_t *)Xcalloc(m->head.numsurfs, sizeof(md3surf_t));
    // NOTE: We assume that NULL is represented by all-zeros.
    // surfs[0].geometry is for POLYMER_MD_PROCESS_CHECK (else: crashes).
    // surfs[i].geometry is for FREE_SURFS_GEOMETRY.
    Bassert(m->head.surfs[0].geometry == NULL);

#if B_BIG_ENDIAN != 0
    {
        int32_t j, *l;

        for (i = m->head.numframes-1; i>=0; i--)
        {
            l = (int32_t *)&m->head.frames[i].min;
            for (j=3+3+3+1-1; j>=0; j--) l[j] = B_LITTLE32(l[j]);
        }

        for (i = m->head.numtags-1; i>=0; i--)
        {
            l = (int32_t *)&m->head.tags[i].p;
            for (j=3+3+3+3-1; j>=0; j--) l[j] = B_LITTLE32(l[j]);
        }
    }
#endif

    maxtrispersurf = 0;

    for (surfi=0; surfi<m->head.numsurfs; surfi++)
    {
        s = &m->head.surfs[surfi];
        klseek(fil,ofsurf,SEEK_SET);
        if (kread_and_test(fil,s,SIZEOF_MD3SURF_T)) { md3free(m); return nullptr; }

#if B_BIG_ENDIAN != 0
        {
            int32_t j, *l;
            s->id = B_LITTLE32(s->id);
            l =	(int32_t *)&s->flags;
            for	(j=1+1+1+1+1+1+1+1+1+1-1; j>=0; j--) l[j] = B_LITTLE32(l[j]);
        }
#endif

        offs[0] = ofsurf+s->ofstris;
        offs[1] = ofsurf+s->ofsshaders;
        offs[2] = ofsurf+s->ofsuv;
        offs[3] = ofsurf+s->ofsxyzn;

        leng[0] = s->numtris*sizeof(md3tri_t);
        leng[1] = s->numshaders*sizeof(md3shader_t);
        leng[2] = s->numverts*sizeof(md3uv_t);
        leng[3] = s->numframes*s->numverts*sizeof(md3xyzn_t);

        //memoryusage += (s->numverts * s->numframes * sizeof(md3xyzn_t));
        //OSD_Printf("Current model geometry memory usage : %i.\n", memoryusage);

        s->tris = (md3tri_t *)Xmalloc((leng[0] + leng[1]) + (leng[2] + leng[3]));

        s->shaders = (md3shader_t *)(((intptr_t)s->tris)+leng[0]);
        s->uv      = (md3uv_t *)(((intptr_t)s->shaders)+leng[1]);
        s->xyzn    = (md3xyzn_t *)(((intptr_t)s->uv)+leng[2]);

        klseek(fil,offs[0],SEEK_SET); if (kread_and_test(fil,s->tris   ,leng[0])) { md3free(m); return nullptr; }
        klseek(fil,offs[1],SEEK_SET); if (kread_and_test(fil,s->shaders,leng[1])) { md3free(m); return nullptr; }
        klseek(fil,offs[2],SEEK_SET); if (kread_and_test(fil,s->uv     ,leng[2])) { md3free(m); return nullptr; }
        klseek(fil,offs[3],SEEK_SET); if (kread_and_test(fil,s->xyzn   ,leng[3])) { md3free(m); return nullptr; }

#if B_BIG_ENDIAN != 0
        {
            int32_t j, *l;

            for (i=s->numtris-1; i>=0; i--)
            {
                for (j=2; j>=0; j--) s->tris[i].i[j] = B_LITTLE32(s->tris[i].i[j]);
            }
            for (i=s->numshaders-1; i>=0; i--)
            {
                s->shaders[i].i = B_LITTLE32(s->shaders[i].i);
            }
            for (i=s->numverts-1; i>=0; i--)
            {
                l = (int32_t *)&s->uv[i].u;
                l[0] = B_LITTLE32(l[0]);
                l[1] = B_LITTLE32(l[1]);
            }
            for (i=s->numframes*s->numverts-1; i>=0; i--)
            {
                s->xyzn[i].x = (int16_t)B_LITTLE16((uint16_t)s->xyzn[i].x);
                s->xyzn[i].y = (int16_t)B_LITTLE16((uint16_t)s->xyzn[i].y);
                s->xyzn[i].z = (int16_t)B_LITTLE16((uint16_t)s->xyzn[i].z);
            }
        }
#endif
        maxmodelverts = max(maxmodelverts, s->numverts);
        maxmodeltris = max(maxmodeltris, s->numtris);
        maxtrispersurf = max(maxtrispersurf, s->numtris);
        ofsurf += s->ofsend;
    }

    m->indexes = (uint16_t *)Xmalloc(sizeof(uint16_t) * maxtrispersurf);
    m->vindexes = (uint16_t *)Xmalloc(sizeof(uint16_t) * maxtrispersurf * 3);
    m->maxdepths = (float *)Xmalloc(sizeof(float) * maxtrispersurf);

    m->vbos = NULL;

    return m;
}

#ifdef POLYMER
static inline void  invertmatrix(const float *m, float *out)
{
    float det;

    det  = (m[0] * (m[4]*m[8] - m[5] * m[7]))
         - (m[1] * (m[3]*m[8] - m[5] * m[6]))
         + (m[2] * (m[3]*m[7] - m[4] * m[6]));

    if (det == 0.0f)
    {
        Bmemset(out, 0, sizeof(float) * 9);
        out[8] = out[4] = out[0] = 1.f;
        return;
    }

    det = 1.0f / det;

    out[0] = det * (m[4] * m[8] - m[5] * m[7]);
    out[1] = det * (m[2] * m[7] - m[1] * m[8]);
    out[2] = det * (m[1] * m[5] - m[2] * m[4]);
    out[3] = det * (m[5] * m[6] - m[3] * m[8]);
    out[4] = det * (m[0] * m[8] - m[2] * m[6]);
    out[5] = det * (m[2] * m[3] - m[0] * m[5]);
    out[6] = det * (m[3] * m[7] - m[1] * m[6]);
    out[7] = det * (m[1] * m[6] - m[0] * m[7]);
    out[8] = det * (m[0] * m[4] - m[1] * m[3]);
}

static inline void  normalize(float *vec)
{
    float norm;

    if ((norm = vec[0] * vec[0] + vec[1] * vec[1] + vec[2] * vec[2]) == 0.f)
        return;

    norm = 1.f / Bsqrtf(norm);
    vec[0] *= norm;
    vec[1] *= norm;
    vec[2] *= norm;
}
#endif

static void      md3postload_common(md3model_t *m)
{
    int         framei, surfi, verti;
    md3frame_t  *frame;
    md3xyzn_t   *frameverts;
    float       dist, vec1[3];

    // apparently we can't trust loaded models bounding box/sphere information,
    // so let's compute it ourselves

    framei = 0;

    while (framei < m->head.numframes)
    {
        frame = &m->head.frames[framei];

        Bmemset(&frame->min, 0, sizeof(vec3f_t));
        Bmemset(&frame->max, 0, sizeof(vec3f_t));

        frame->r        = 0.0f;

        surfi = 0;
        while (surfi < m->head.numsurfs)
        {
            frameverts = &m->head.surfs[surfi].xyzn[framei * m->head.surfs[surfi].numverts];

            verti = 0;
            while (verti < m->head.surfs[surfi].numverts)
            {
                if (!verti && !surfi)
                {
                    md3xyzn_t const & framevert = frameverts[0];

                    frame->min.x    = framevert.x;
                    frame->min.y    = framevert.y;
                    frame->min.z    = framevert.z;

                    frame->max      = frame->min;
                }
                else
                {
                    md3xyzn_t const & framevert = frameverts[verti];

                    if (frame->min.x > framevert.x)
                        frame->min.x = framevert.x;
                    if (frame->max.x < framevert.x)
                        frame->max.x = framevert.x;

                    if (frame->min.y > framevert.y)
                        frame->min.y = framevert.y;
                    if (frame->max.y < framevert.y)
                        frame->max.y = framevert.y;

                    if (frame->min.z > framevert.z)
                        frame->min.z = framevert.z;
                    if (frame->max.z < framevert.z)
                        frame->max.z = framevert.z;
                }

                ++verti;
            }

            ++surfi;
        }

        frame->cen.x = (frame->min.x + frame->max.x) * .5f;
        frame->cen.y = (frame->min.y + frame->max.y) * .5f;
        frame->cen.z = (frame->min.z + frame->max.z) * .5f;

        surfi = 0;
        while (surfi < m->head.numsurfs)
        {
            md3surf_t const & surf = m->head.surfs[surfi];

            frameverts = &surf.xyzn[framei * surf.numverts];

            verti = 0;
            while (verti < surf.numverts)
            {
                md3xyzn_t const & framevert = frameverts[verti];

                vec1[0] = framevert.x - frame->cen.x;
                vec1[1] = framevert.y - frame->cen.y;
                vec1[2] = framevert.z - frame->cen.z;

                dist = vec1[0] * vec1[0] + vec1[1] * vec1[1] + vec1[2] * vec1[2];

                if (dist > frame->r)
                    frame->r = dist;

                ++verti;
            }

            ++surfi;
        }

        frame->r = Bsqrtf(frame->r);

        ++framei;
    }
}

#ifdef POLYMER
// pre-check success of conversion since it must not fail later.
// keep in sync with md3postload_polymer!
static int md3postload_polymer_check(md3model_t *m)
{
    ssize_t surfi, trii;
    md3surf_t   *s;

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

        uint32_t const numverts = s->numverts;

        trii = 0;
        while (trii < s->numtris)
        {
            uint32_t const * const u = (uint32_t const *)s->tris[trii].i;

            // let the vertices know they're being referenced by a triangle
            if (u[0] >= numverts || u[1] >= numverts || u[2] >= numverts)
            {
                // corrupt model
                OSD_Printf("%s: Triangle index out of bounds!\n", m->head.nam);
                return 1;
            }

            ++trii;
        }

        ++surfi;
    }

    return 0;
}

// Precalculated cos/sin arrays.
static float g_mdcos[256], g_mdsin[256];
static int32_t mdtrig_init = 0;

static void init_mdtrig_arrays(void)
{
    int32_t i;

    for (i=0; i<256; i++)
    {
        float ang = i * (2.f * fPI) * (1.f/255.f);
        g_mdcos[i] = cosf(ang);
        g_mdsin[i] = sinf(ang);
    }

    mdtrig_init = 1;
}
#endif

int      md3postload_polymer(md3model_t *m)
{
#ifdef POLYMER
    int         framei, surfi, verti, trii;
    float       vec1[5], vec2[5], mat[9], r;

    // POLYMER_MD_PROCESS_CHECK
    if (m->head.surfs[0].geometry)
        return -1;  // already postprocessed

    if (!mdtrig_init)
        init_mdtrig_arrays();

    // let's also repack the geometry to more usable formats

    surfi = 0;
    while (surfi < m->head.numsurfs)
    {
        handleevents();

        md3surf_t *const s = &m->head.surfs[surfi];
#ifdef DEBUG_MODEL_MEM
        i = (m->head.numframes * s->numverts * sizeof(float) * 15);
        if (i > 1<<20)
            initprintf("size %d (%d fr, %d v): md %s surf %d/%d\n", i, m->head.numframes, s->numverts,
                       m->head.nam, surfi, m->head.numsurfs);
#endif
        s->geometry = (float *)Xcalloc(m->head.numframes * s->numverts * 15, sizeof(float));

        if (s->numverts > tribufverts)
        {
            tribuf = (int32_t *) Xrealloc(tribuf, s->numverts * sizeof(int32_t));
            tribufverts = s->numverts;
        }

        Bmemset(tribuf, 0, s->numverts * sizeof(int32_t));

        verti = 0;
        while (verti < (m->head.numframes * s->numverts))
        {
            md3xyzn_t const & xyzn = s->xyzn[verti];

            // normal extraction from packed spherical coordinates
            // FIXME: swapping lat and lng because of npherno's compiler
            uint8_t lat = xyzn.nlng;
            uint8_t lng = xyzn.nlat;
            size_t verti15 = (size_t)verti * 15;

            s->geometry[verti15 + 0] = xyzn.x;
            s->geometry[verti15 + 1] = xyzn.y;
            s->geometry[verti15 + 2] = xyzn.z;

            s->geometry[verti15 + 3] = g_mdcos[lat] * g_mdsin[lng];
            s->geometry[verti15 + 4] = g_mdsin[lat] * g_mdsin[lng];
            s->geometry[verti15 + 5] = g_mdcos[lng];

            ++verti;
        }

        uint32_t numverts = s->numverts;

        trii = 0;
        while (trii < s->numtris)
        {
            int32_t const * const i = s->tris[trii].i;
            uint32_t const * const u = (uint32_t const *)i;

            // let the vertices know they're being referenced by a triangle
            if (u[0] >= numverts ||u[1] >= numverts || u[2] >= numverts)
            {
                // corrupt model
                return 0;
            }
            tribuf[u[0]]++;
            tribuf[u[1]]++;
            tribuf[u[2]]++;

            uint32_t const tris15[] = { u[0] * 15, u[1] * 15, u[2] * 15 };

            framei = 0;
            while (framei < m->head.numframes)
            {
                const uint32_t verti15 = framei * s->numverts * 15;

                vec1[0] = s->geometry[verti15 + tris15[1]]     - s->geometry[verti15 + tris15[0]];
                vec1[1] = s->geometry[verti15 + tris15[1] + 1] - s->geometry[verti15 + tris15[0] + 1];
                vec1[2] = s->geometry[verti15 + tris15[1] + 2] - s->geometry[verti15 + tris15[0] + 2];
                vec1[3] = s->uv[u[1]].u - s->uv[u[0]].u;
                vec1[4] = s->uv[u[1]].v - s->uv[u[0]].v;

                vec2[0] = s->geometry[verti15 + tris15[2]]     - s->geometry[verti15 + tris15[1]];
                vec2[1] = s->geometry[verti15 + tris15[2] + 1] - s->geometry[verti15 + tris15[1] + 1];
                vec2[2] = s->geometry[verti15 + tris15[2] + 2] - s->geometry[verti15 + tris15[1] + 2];
                vec2[3] = s->uv[u[2]].u - s->uv[u[1]].u;
                vec2[4] = s->uv[u[2]].v - s->uv[u[1]].v;

                r = (vec1[3] * vec2[4] - vec2[3] * vec1[4]);
                if (r != 0.0f)
                {
                    r = 1.f/r;

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

                    normalize(&mat[0]);

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

                    normalize(&mat[3]);
                }
                else
                    Bmemset(mat, 0, sizeof(float) * 6);

                // T and B are shared for the three vertices in that triangle
                size_t const offs = (framei * numverts * 15) + 6;
                size_t j = 0;
                do
                {
                    size_t const offsi = offs + j;
                    s->geometry[offsi + tris15[0]] += mat[j];
                    s->geometry[offsi + tris15[1]] += mat[j];
                    s->geometry[offsi + tris15[2]] += mat[j];
                }
                while (++j < 6);

                ++framei;
            }

            ++trii;
        }

        // now that we accumulated the TBNs, average and invert them for each vertex
        int verti_end = m->head.numframes * s->numverts;

        verti = 0;
        while (verti < verti_end)
        {
            const int32_t curnumtris = tribuf[verti % s->numverts];
            uint32_t const verti15 = verti * 15;

            if (curnumtris > 0)
            {
                const float rfcurnumtris = 1.f/(float)curnumtris;
                size_t i = 6;
                do
                    s->geometry[i + verti15] *= rfcurnumtris;
                while (++i < 12);
            }
#ifdef DEBUG_MODEL_MEM
            else if (verti == verti%s->numverts)
            {
                OSD_Printf("%s: vert %d is unused\n", m->head.nam, verti);
            }
#endif
            // copy N over
            Bmemcpy(&s->geometry[verti15 + 12], &s->geometry[verti15 + 3], sizeof(float) * 3);
            invertmatrix(&s->geometry[verti15 + 6], mat);
            Bmemcpy(&s->geometry[verti15 + 6], mat, sizeof(float) * 9);

            ++verti;
        }

        ++surfi;
    }

#else
    UNREFERENCED_PARAMETER(m);
#endif

    return 1;
}

void md3_vox_calcmat_common(tspriteptr_t tspr, const vec3f_t *a0, float f, float mat[16])
{
    float g;
    float k0, k1, k2, k3, k4, k5, k6, k7;

    k0 = ((float)(tspr->x+spriteext[tspr->owner].mdposition_offset.x-globalposx))*f*(1.f/1024.f);
    k1 = ((float)(tspr->y+spriteext[tspr->owner].mdposition_offset.y-globalposy))*f*(1.f/1024.f);
    f = gcosang2*gshang/gvrcorrection;
    g = gsinang2*gshang/gvrcorrection;

    if (tspriteptr[maxspritesonscreen] == tspr)
    {
        k4 = sinf(fix16_to_float((fix16_from_int((tspr->ang - globalang + spriteext[tspr->owner].mdangoff + 1024) & 2047) + qglobalang) & 0x7FFFFFF) * BANG2RAD);
        k5 = sinf(fix16_to_float((fix16_from_int((tspr->ang - globalang + spriteext[tspr->owner].mdangoff + 512) & 2047) + qglobalang) & 0x7FFFFFF) * BANG2RAD);
    }
    else
    {
        k4 = (float)sintable[(tspr->ang+spriteext[tspr->owner].mdangoff+1024)&2047] * (1.f/16384.f);
        k5 = (float)sintable[(tspr->ang+spriteext[tspr->owner].mdangoff+ 512)&2047] * (1.f/16384.f);
    }

    k2 = k0*(1-k4)+k1*k5;
    k3 = k1*(1-k4)-k0*k5;
    k6 = f*gstang - gsinang*gctang; k7 = g*gstang + gcosang*gctang;
    mat[0] = k4*k6 + k5*k7; mat[4] = gchang*gstang; mat[ 8] = k4*k7 - k5*k6; mat[12] = k2*k6 + k3*k7;
    k6 = f*gctang + gsinang*gstang; k7 = g*gctang - gcosang*gstang;
    mat[1] = k4*k6 + k5*k7; mat[5] = gchang*gctang; mat[ 9] = k4*k7 - k5*k6; mat[13] = k2*k6 + k3*k7;
    k6 =           gcosang2*gchang; k7 =           gsinang2*gchang;
    mat[2] = k4*k6 + k5*k7; mat[6] =-gshang*gvrcorrection; mat[10] = k4*k7 - k5*k6; mat[14] = k2*k6 + k3*k7;

    mat[12] = (mat[12] + a0->y*mat[0]) + (a0->z*mat[4] + a0->x*mat[ 8]);
    mat[13] = (mat[13] + a0->y*mat[1]) + (a0->z*mat[5] + a0->x*mat[ 9]);
    mat[14] = (mat[14] + a0->y*mat[2]) + (a0->z*mat[6] + a0->x*mat[10]);
}

static void md3draw_handle_triangles(const md3surf_t *s, uint16_t *indexhandle,
                                            int32_t texunits, const md3model_t *M)
{
    int32_t i;

    if (r_vertexarrays)
    {
        int32_t k = 0;

        if (M == NULL)
        {
            for (i=s->numtris-1; i>=0; i--, k+=3)
            {
                indexhandle[k]   = s->tris[i].i[0];
                indexhandle[k+1] = s->tris[i].i[1];
                indexhandle[k+2] = s->tris[i].i[2];
            }
            return;
        }

        for (i=s->numtris-1; i>=0; i--, k+=3)
        {
            uint16_t tri = M->indexes[i];

            indexhandle[k]   = s->tris[tri].i[0];
            indexhandle[k+1] = s->tris[tri].i[1];
            indexhandle[k+2] = s->tris[tri].i[2];
        }
        return;
    }

    glBegin(GL_TRIANGLES);
    for (i=s->numtris-1; i>=0; i--)
    {
        uint16_t tri = M ? M->indexes[i] : i;
        int32_t j;

        for (j=0; j<3; j++)
        {
            int32_t k = s->tris[tri].i[j];

#ifdef USE_GLEXT
            if (texunits > GL_TEXTURE0)
            {
                int32_t l = GL_TEXTURE0;
                while (l <= texunits)
                    glMultiTexCoord2f(l++, s->uv[k].u, s->uv[k].v);
            }
            else
#endif
                glTexCoord2f(s->uv[k].u, s->uv[k].v);

            glVertex3fv((float *) &vertlist[k]);
        }
    }
    glEnd();

#ifndef USE_GLEXT
    UNREFERENCED_PARAMETER(texunits);
#endif
}

static inline mdskinmap_t* mdgetskinmap(md3model_t* m, uint8_t pal, int number, int surface)
{
    mdskinmap_t *sk, *skzero = NULL;

    for (sk = m->skinmap; sk; sk = sk->next)
    {
        if ((int32_t)sk->palette == pal && sk->skinnum == number && sk->surfnum == surface)
            return sk;
    }

    // Reserved pals (like detail/glow/spec maps) shouldn't fall back.
    if (pal >= (MAXPALOOKUPS - RESERVEDPALS))
        return NULL;

    //If no match, give highest priority to number, then pal...
    int priority = 0;
    for (sk = m->skinmap; sk; sk = sk->next)
    {
        if ((sk->palette == 0) && (sk->skinnum == number) && (sk->surfnum == surface) && (priority < 5))    { priority = 5; skzero = sk; }
        else if ((sk->palette == pal) && (sk->skinnum == 0) && (sk->surfnum == surface) && (priority < 4))  { priority = 4; skzero = sk; }
        else if ((sk->palette == 0) && (sk->skinnum == 0) && (sk->surfnum == surface) && (priority < 3))    { priority = 3; skzero = sk; }
        else if ((sk->palette == 0) && (sk->skinnum == number) && (priority < 2))                           { priority = 2; skzero = sk; }
        else if ((sk->palette == pal) && (sk->skinnum == 0) && (priority < 1))                              { priority = 1; skzero = sk; }
        else if ((sk->palette == 0) && (sk->skinnum == 0) && (priority < 0))                                { priority = 0; skzero = sk; }
    }

    if (skzero)
        return skzero;

    return NULL;
}

static int32_t polymost_md3draw(md3model_t *m, tspriteptr_t tspr)
{
    vec3f_t m0, m1, a0;
    md3xyzn_t *v0, *v1;
    int32_t i, surfi;
    float f, g, k0, k1, k2=0, k3=0, mat[16];  // inits: compiler-happy
    GLfloat pc[4];
    int32_t texunits = GL_TEXTURE0;

    const int32_t owner = tspr->owner;
    const spriteext_t *const sext = &spriteext[((unsigned)owner < MAXSPRITES+MAXUNIQHUDID) ? owner : MAXSPRITES+MAXUNIQHUDID-1];
    const uint8_t lpal = ((unsigned)owner < MAXSPRITES) ? sprite[tspr->owner].pal : tspr->pal;
    const int32_t sizyrep = tilesiz[tspr->picnum].y*tspr->yrepeat;

    buildgl_outputDebugMessage(3, "polymost_md3draw(m:%p, tspr:%p)", m, tspr);

    if (m->vbos == NULL)
        mdloadvbos(m);

    //    if ((tspr->cstat&48) == 32) return 0;

    updateanimation((md2model_t *)m, tspr, lpal);

    //create current&next frame's vertex list from whole list

    f = m->interpol; g = 1.f - f;

    if (m->interpol < 0.f || m->interpol > 1.f ||
        (unsigned)m->cframe >= (unsigned)m->numframes ||
            (unsigned)m->nframe >= (unsigned)m->numframes)
    {
#ifdef DEBUGGINGAIDS
        OSD_Printf("%s: mdframe oob: c:%d n:%d total:%d interpol:%.02f\n",
                   m->head.nam, m->cframe, m->nframe, m->numframes, m->interpol);
#endif

        m->interpol = fclamp(m->interpol, 0.f, 1.f);
        m->cframe = clamp(m->cframe, 0, m->numframes-1);
        m->nframe = clamp(m->nframe, 0, m->numframes-1);
    }

    m0.z = m0.y = m0.x = g *= m->scale * (1.f/64.f);
    m1.z = m1.y = m1.x = f *= m->scale * (1.f/64.f);

    a0.x = a0.y = 0;
    a0.z = m->zadd * m->scale;

    // Parkar: Moved up to be able to use k0 for the y-flipping code
    k0 = (float)tspr->z+spriteext[tspr->owner].mdposition_offset.z;
    f = ((globalorientation&8) && (sprite[tspr->owner].cstat&48)!=0) ? -4.f : 4.f;
    k0 -= (tspr->yoffset*tspr->yrepeat)*f;
    if ((globalorientation&128) && !((globalorientation&48)==32))
        k0 += (float)(sizyrep<<1);

    // Parkar: Changed to use the same method as centeroriented sprites
    if (globalorientation&8) //y-flipping
    {
        m0.z = -m0.z; m1.z = -m1.z; a0.z = -a0.z;
        k0 -= (float)(sizyrep<<2);
    }
    if (globalorientation&4) { m0.y = -m0.y; m1.y = -m1.y; a0.y = -a0.y; } //x-flipping

    // yoffset differs from zadd in that it does not follow cstat&8 y-flipping
    a0.z += m->yoffset*m->scale;

    f = ((float)tspr->xrepeat) * (1.f/64.f) * m->bscale;
    m0.x *= f; m0.y *= -f;
    m1.x *= f; m1.y *= -f;
    a0.x *= f; a0.y *= -f;
    f = ((float)tspr->yrepeat) * (1.f/64.f) * m->bscale;
    m0.z *= f; m1.z *= f; a0.z *= f;

    // floor aligned
    k1 = (float)tspr->y+spriteext[tspr->owner].mdposition_offset.y;
    if ((globalorientation&48)==32)
    {
        m0.z = -m0.z; m1.z = -m1.z; a0.z = -a0.z;
        m0.y = -m0.y; m1.y = -m1.y; a0.y = -a0.y;
        f = a0.x; a0.x = a0.z; a0.z = f;
        k1 += (float)(sizyrep>>3);
    }

    // Note: These SCREEN_FACTORS will be neutralized in axes offset
    // calculations below again, but are needed for the base offsets.
    f = (65536.f*512.f)/(fxdimen*fviewingrange);
    g = 32.f/(fxdimen*gxyaspect);
    m0.y *= f; m1.y *= f; a0.y = (((float)(tspr->x+spriteext[tspr->owner].mdposition_offset.x-globalposx))*  (1.f/1024.f) + a0.y)*f;
    m0.x *=-f; m1.x *=-f; a0.x = ((k1     -fglobalposy) * -(1.f/1024.f) + a0.x)*-f;
    m0.z *= g; m1.z *= g; a0.z = ((k0     -fglobalposz) * -(1.f/16384.f) + a0.z)*g;

    md3_vox_calcmat_common(tspr, &a0, f, mat);

    // floor aligned
    if ((globalorientation&48)==32)
    {
        f = mat[4]; mat[4] = mat[8]*16.f; mat[8] = -f*(1.f/16.f);
        f = mat[5]; mat[5] = mat[9]*16.f; mat[9] = -f*(1.f/16.f);
        f = mat[6]; mat[6] = mat[10]*16.f; mat[10] = -f*(1.f/16.f);
    }

    //Mirrors
    if (grhalfxdown10x < 0) { mat[0] = -mat[0]; mat[4] = -mat[4]; mat[8] = -mat[8]; mat[12] = -mat[12]; }

    //------------
    // TSPR_FLAGS_MDHACK is an ugly hack in game.c:G_DoSpriteAnimations() telling md2sprite
    // to use Z-buffer hacks to hide overdraw problems with the flat-tsprite-on-floor shadows,
    // also disabling detail, glow, normal, and specular maps.

    if (tspr->clipdist & TSPR_FLAGS_MDHACK)
    {
#ifdef __arm__ // GL ES has a glDepthRangef and the loss of precision is OK there
        float f = (float) (tspr->owner + 1) * (std::numeric_limits<float>::epsilon() * 8.0);
        if (f != 0.0) f *= 1.f/(float) (sepldist(globalposx - tspr->x, globalposy - tspr->y)>>5);
#else
        double f = (double) (tspr->owner + 1) * (std::numeric_limits<double>::epsilon() * 8.0);
        if (f != 0.0) f *= 1.0/(double) (sepldist(globalposx - tspr->x, globalposy - tspr->y)>>5);
//        glBlendFunc(GL_SRC_ALPHA, GL_DST_COLOR);
#endif
        buildgl_setDepthFunc(GL_LEQUAL);
//        glDepthRange(0.0 - f, 1.0 - f);
    }

//    glPushAttrib(GL_POLYGON_BIT);
    if ((grhalfxdown10x >= 0) ^((globalorientation&8) != 0) ^((globalorientation&4) != 0)) glFrontFace(GL_CW); else glFrontFace(GL_CCW);
    buildgl_setEnabled(GL_CULL_FACE);
    glCullFace(GL_BACK);

    // tinting
    pc[0] = pc[1] = pc[2] = ((float)numshades - min(max((globalshade * shadescale) + m->shadeoff, 0.f), (float)numshades)) / (float)numshades;
    polytintflags_t const tintflags = hictinting[globalpal].f;
    if (!(tintflags & HICTINT_PRECOMPUTED))
    {
        if (!(m->flags&1))
            hictinting_apply(pc, globalpal);
        else globalnoeffect=1;
    }

    // global tinting
    if (have_basepal_tint())
        hictinting_apply(pc, MAXPALOOKUPS-1);

    pc[3] = (tspr->cstat&2) ? glblend[tspr->blend].def[!!(tspr->cstat&512)].alpha : 1.0f;
    pc[3] *= 1.0f - sext->alpha;

    handle_blend(!!(tspr->cstat & 2), tspr->blend, !!(tspr->cstat & 512));

    if (m->usesalpha) //Sprites with alpha in texture
    {
        //      buildgl_setEnabled(GL_BLEND);// glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
        //      buildgl_setEnabled(GL_ALPHA_TEST); buildgl_setAlphaFunc(GL_GREATER,0.32);
        //      float al = 0.32;
        // PLAG : default cutoff removed
        float al = 0.0;
        if (alphahackarray[globalpicnum] != 0)
            al=alphahackarray[globalpicnum] * (1.f/255.f);
        buildgl_setEnabled(GL_BLEND);
        // buildgl_setEnabled(GL_ALPHA_TEST);
        buildgl_setAlphaFunc(GL_GREATER,al);
    }
    else
    {
        if ((tspr->cstat&2) || sext->alpha > 0.f || pc[3] < 1.0f)
            buildgl_setEnabled(GL_BLEND); //else glDisable(GL_BLEND);
    }
    glColor4f(pc[0],pc[1],pc[2],pc[3]);
    //if (MFLAGS_NOCONV(m))
    //    glColor4f(0.0f, 0.0f, 1.0f, 1.0f);
    //------------

    // PLAG: Cleaner model rotation code
    if (sext->mdpitch || sext->mdroll)
    {
        float f = 1.f/((fxdimen * fviewingrange) * (256.f/(65536.f*128.f)) * (m0.x+m1.x));
        Bmemset(&a0, 0, sizeof(a0));

        if (sext->mdpivot_offset.x)
            a0.x = (float) sext->mdpivot_offset.x * f;

        if (sext->mdpivot_offset.y)  // Compare with SCREEN_FACTORS above
            a0.y = (float) sext->mdpivot_offset.y * f;

        if ((sext->mdpivot_offset.z) && !(tspr->clipdist & TSPR_FLAGS_MDHACK))  // Compare with SCREEN_FACTORS above
            a0.z = (float)(((tspr->cstat & CSTAT_SPRITE_ALIGNMENT) == CSTAT_SPRITE_ALIGNMENT_FLOOR) ? -sext->mdpivot_offset.z : sext->mdpivot_offset.z) / (gxyaspect * fxdimen * (65536.f/128.f) * (m0.z+m1.z));

        k0 = (float)sintable[(sext->mdpitch+512)&2047] * (1.f/16384.f);
        k1 = (float)sintable[sext->mdpitch&2047] * (1.f/16384.f);
        k2 = (float)sintable[(sext->mdroll+512)&2047] * (1.f/16384.f);
        k3 = (float)sintable[sext->mdroll&2047] * (1.f/16384.f);
    }

    float const xpanning = (float)sext->xpanning * (1.f/256.f);
    float const ypanning = (float)sext->ypanning * (1.f/256.f);

    char prevClamp = polymost_getClamp();
    polymost_setClamp(0);
    polymost_usePaletteIndexing(false);
    polymost_setTexturePosSize({ 0.f, 0.f, 1.f, 1.f });

    for (surfi=0; surfi<m->head.numsurfs; surfi++)
    {
        //PLAG : sorting stuff
#ifdef USE_GLEXT
        void               *vbotemp;
        vec3f_t            *vertexhandle = NULL;
#endif
        uint16_t           *indexhandle;
        vec3f_t fp;

        const md3surf_t *const s = &m->head.surfs[surfi];

        v0 = &s->xyzn[m->cframe*s->numverts];
        v1 = &s->xyzn[m->nframe*s->numverts];

#ifdef USE_GLEXT
        if (r_vertexarrays)
        {
            if (++curvbo >= r_vbocount)
                curvbo = 0;

            buildgl_bindBuffer(GL_ARRAY_BUFFER, vertvbos[curvbo]);
            vbotemp = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
            vertexhandle = (vec3f_t *)vbotemp;
        }
#endif

        if (sext->mdpitch || sext->mdroll)
        {
            vec3f_t fp1, fp2;

            for (i=s->numverts-1; i>=0; i--)
            {
                fp.z = v0[i].x + a0.x;
                fp.x = v0[i].y + a0.y;
                fp.y = v0[i].z + a0.z;

                fp1.x = fp.x*k2 +       fp.y*k3;
                fp1.y = fp.x*k0*(-k3) + fp.y*k0*k2 + fp.z*(-k1);
                fp1.z = fp.x*k1*(-k3) + fp.y*k1*k2 + fp.z*k0;

                fp.z = v1[i].x + a0.x;
                fp.x = v1[i].y + a0.y;
                fp.y = v1[i].z + a0.z;

                fp2.x = fp.x*k2 +       fp.y*k3;
                fp2.y = fp.x*k0*(-k3) + fp.y*k0*k2 + fp.z*(-k1);
                fp2.z = fp.x*k1*(-k3) + fp.y*k1*k2 + fp.z*k0;
                fp.z = (fp1.z - a0.x)*m0.x + (fp2.z - a0.x)*m1.x;
                fp.x = (fp1.x - a0.y)*m0.y + (fp2.x - a0.y)*m1.y;
                fp.y = (fp1.y - a0.z)*m0.z + (fp2.y - a0.z)*m1.z;

#ifdef USE_GLEXT
                if (r_vertexarrays)
                    vertexhandle[i] = fp;
#endif

                vertlist[i] = fp;
            }
        }
        else
        {
            for (i=s->numverts-1; i>=0; i--)
            {
                fp.z = v0[i].x*m0.x + v1[i].x*m1.x;
                fp.y = v0[i].z*m0.z + v1[i].z*m1.z;
                fp.x = v0[i].y*m0.y + v1[i].y*m1.y;

#ifdef USE_GLEXT
                if (r_vertexarrays)
                    vertexhandle[i] = fp;
#endif

                vertlist[i] = fp;
            }
        }

#ifdef USE_GLEXT
        if (r_vertexarrays)
        {
            glUnmapBuffer(GL_ARRAY_BUFFER);
            buildgl_bindBuffer(GL_ARRAY_BUFFER, 0);
        }
#endif

        glMatrixMode(GL_MODELVIEW); //Let OpenGL (and perhaps hardware :) handle the matrix rotation
        mat[3] = mat[7] = mat[11] = 0.f; mat[15] = 1.f; glLoadMatrixf(mat);
        // PLAG: End

        auto skinNum = tile2model[Ptile2tile(tspr->picnum, lpal)].skinnum;
        i = mdloadskin((md2model_t *)m, skinNum, globalpal, surfi);
        if (!i)
            continue;
        //i = mdloadskin((md2model *)m,tile2model[Ptile2tile(tspr->picnum,lpal)].skinnum,surfi); //hack for testing multiple surfaces per MD3

        auto sk = mdgetskinmap(m, globalpal, skinNum, surfi);

        if (sk)
            buildgl_bindSamplerObject(0, (sk->flags & HICR_FORCEFILTER) ? (PTH_HIGHTILE | PTH_FORCEFILTER) : PTH_HIGHTILE);

        buildgl_bindTexture(GL_TEXTURE_2D, i);

        glMatrixMode(GL_TEXTURE);
        glLoadIdentity();
        glTranslatef(xpanning, ypanning, 1.0f);
        glMatrixMode(GL_MODELVIEW);

        if (!(tspr->clipdist & TSPR_FLAGS_MDHACK))
        {
#ifdef USE_GLEXT
            //POGOTODO: if we add support for palette indexing on model skins, the texture for the palswap could be setup here
            texunits += 4;

            if ((i = r_detailmapping ? mdloadskin((md2model_t *) m, skinNum, DETAILPAL, surfi) : 0))
            {
                auto sk = mdgetskinmap(m, DETAILPAL, skinNum, surfi);

                if (sk != nullptr)
                {
                    polymost_useDetailMapping(true);
                    polymost_setupdetailtexture(GL_TEXTURE3, i, (sk->flags & HICR_FORCEFILTER) ? (PTH_HIGHTILE | PTH_FORCEFILTER) : PTH_HIGHTILE);

                    f = sk->param;

                    glMatrixMode(GL_TEXTURE);
                    glLoadIdentity();
                    glTranslatef(xpanning, ypanning, 1.0f);
                    glScalef(f, f, 1.0f);
                    glMatrixMode(GL_MODELVIEW);
                }
                else VLOG_F(LOG_ENGINE, "polymost_md3draw: detail skin %d has no skinmap", skinNum);
            }

            if ((i = r_glowmapping ? mdloadskin((md2model_t *) m, skinNum, GLOWPAL, surfi) : 0))
            {
                auto sk = mdgetskinmap(m, GLOWPAL, skinNum, surfi);

                if (sk != nullptr)
                {
                    polymost_useGlowMapping(true);
                    polymost_setupglowtexture(GL_TEXTURE4, i, (sk->flags & HICR_FORCEFILTER) ? (PTH_HIGHTILE | PTH_FORCEFILTER) : PTH_HIGHTILE);
                    glMatrixMode(GL_TEXTURE);
                    glLoadIdentity();
                    glTranslatef(xpanning, ypanning, 1.0f);
                    glMatrixMode(GL_MODELVIEW);
                }
                else VLOG_F(LOG_ENGINE, "polymost_md3draw: glow skin %d has no skinmap", skinNum);
            }

            if (r_vertexarrays)
            {
                buildgl_bindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexvbos[curvbo]);
                vbotemp = glMapBuffer(GL_ELEMENT_ARRAY_BUFFER, GL_WRITE_ONLY);
                indexhandle = (uint16_t *) vbotemp;
            }
            else
#endif
                indexhandle = m->vindexes;

            //PLAG: delayed polygon-level sorted rendering

            if (m->usesalpha)
            {
                for (i=0; i<=s->numtris-1; ++i)
                {
                    vec3f_t const vlt[3] = { vertlist[s->tris[i].i[0]], vertlist[s->tris[i].i[1]], vertlist[s->tris[i].i[2]] };

                    // Matrix multiplication - ugly but clear
                    vec3f_t const fp[3] = { { (vlt[0].x * mat[0]) + (vlt[0].y * mat[4]) + (vlt[0].z * mat[8]) + mat[12],
                                              (vlt[0].x * mat[1]) + (vlt[0].y * mat[5]) + (vlt[0].z * mat[9]) + mat[13],
                                              (vlt[0].x * mat[2]) + (vlt[0].y * mat[6]) + (vlt[0].z * mat[10]) + mat[14] },

                                            { (vlt[1].x * mat[0]) + (vlt[1].y * mat[4]) + (vlt[1].z * mat[8]) + mat[12],
                                              (vlt[1].x * mat[1]) + (vlt[1].y * mat[5]) + (vlt[1].z * mat[9]) + mat[13],
                                              (vlt[1].x * mat[2]) + (vlt[1].y * mat[6]) + (vlt[1].z * mat[10]) + mat[14] },

                                            { (vlt[2].x * mat[0]) + (vlt[2].y * mat[4]) + (vlt[2].z * mat[8]) + mat[12],
                                              (vlt[2].x * mat[1]) + (vlt[2].y * mat[5]) + (vlt[2].z * mat[9]) + mat[13],
                                              (vlt[2].x * mat[2]) + (vlt[2].y * mat[6]) + (vlt[2].z * mat[10]) + mat[14] } };

                    f = (fp[0].x * fp[0].x) + (fp[0].y * fp[0].y) + (fp[0].z * fp[0].z);
                    g = (fp[1].x * fp[1].x) + (fp[1].y * fp[1].y) + (fp[1].z * fp[1].z);

                    if (f > g)
                        f = g;

                    g = (fp[2].x * fp[2].x) + (fp[2].y * fp[2].y) + (fp[2].z * fp[2].z);

                    if (f > g)
                        f = g;

                    m->maxdepths[i] = f;
                    m->indexes[i]   = i;
                }

                // dichotomic recursive sorting - about 100x less iterations than bubblesort
                quicksort(m->indexes, m->maxdepths, 0, s->numtris - 1);
            }

            md3draw_handle_triangles(s, indexhandle, texunits, m->usesalpha ? m : NULL);
        }
        else
        {
#ifdef USE_GLEXT
            if (r_vertexarrays)
            {
                buildgl_bindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexvbos[curvbo]);
                vbotemp = glMapBuffer(GL_ELEMENT_ARRAY_BUFFER, GL_WRITE_ONLY);
                indexhandle = (uint16_t *) vbotemp;
            }
            else
#endif
                indexhandle = m->vindexes;

            md3draw_handle_triangles(s, indexhandle, texunits, NULL);
        }

        if (r_vertexarrays)
        {
#ifdef USE_GLEXT
            int32_t l;

            glUnmapBuffer(GL_ELEMENT_ARRAY_BUFFER);
            buildgl_bindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
            buildgl_bindBuffer(GL_ARRAY_BUFFER, m->vbos[surfi]);

            l = GL_TEXTURE0;
            do
            {
                glClientActiveTexture(l++);
                glEnableClientState(GL_TEXTURE_COORD_ARRAY);
                glTexCoordPointer(2, GL_FLOAT, 0, 0);
            } while (l <= texunits);

            buildgl_bindBuffer(GL_ARRAY_BUFFER, vertvbos[curvbo]);
            glVertexPointer(3, GL_FLOAT, 0, 0);

            buildgl_bindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexvbos[curvbo]);
            glDrawElements(GL_TRIANGLES, s->numtris * 3, GL_UNSIGNED_SHORT, 0);

            buildgl_bindBuffer(GL_ARRAY_BUFFER, 0);
            buildgl_bindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
            while (texunits > GL_TEXTURE0)
            {
                glMatrixMode(GL_TEXTURE);
                glLoadIdentity();
                glMatrixMode(GL_MODELVIEW);
                glTexEnvf(GL_TEXTURE_ENV, GL_RGB_SCALE, 1.0f);
                buildgl_setDisabled(GL_TEXTURE_2D);
                glDisableClientState(GL_TEXTURE_COORD_ARRAY);
                glClientActiveTexture(texunits - 1);
                buildgl_activeTexture(--texunits);
            }
#else
            glEnableClientState(GL_TEXTURE_COORD_ARRAY);
            glTexCoordPointer(2, GL_FLOAT, 0, &(s->uv[0].u));

            glVertexPointer(3, GL_FLOAT, 0, &(vertlist[0].x));

            glDrawElements(GL_TRIANGLES, s->numtris * 3, GL_UNSIGNED_SHORT, m->vindexes);
#endif
        }
#ifdef USE_GLEXT
        else // r_vertexarrays
        {
            while (texunits > GL_TEXTURE0)
            {
                glMatrixMode(GL_TEXTURE);
                glLoadIdentity();
                glMatrixMode(GL_MODELVIEW);
                glTexEnvf(GL_TEXTURE_ENV, GL_RGB_SCALE, 1.0f);
                buildgl_setDisabled(GL_TEXTURE_2D);
                buildgl_activeTexture(--texunits);
            }
        } // r_vertexarrays

        polymost_useDetailMapping(false);
        polymost_useGlowMapping(false);
#endif
    }
    //------------

    // if (m->usesalpha) glDisable(GL_ALPHA_TEST);

    buildgl_setDisabled(GL_CULL_FACE);
//    glPopAttrib();

    glMatrixMode(GL_TEXTURE);
    glLoadIdentity();
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

    polymost_setClamp(prevClamp);
    polymost_usePaletteIndexing(true);
    polymost_resetVertexPointers();

    globalnoeffect=0;
    return 1;
}

static void md3free(md3model_t *m)
{
    mdanim_t *anim, *nanim = NULL;
    mdskinmap_t *sk, *nsk = NULL;

    if (!m) return;

    for (anim=m->animations; anim; anim=nanim)
    {
        nanim = anim->next;
        Xfree(anim);
    }
    for (sk=m->skinmap; sk; sk=nsk)
    {
        nsk = sk->next;
        Xfree(sk->fn);
        Xfree(sk);
    }

    if (m->head.surfs)
    {
        for (bssize_t surfi=m->head.numsurfs-1; surfi>=0; surfi--)
        {
            md3surf_t *s = &m->head.surfs[surfi];
            Xfree(s->tris);
            Xfree(s->geometry);  // FREE_SURFS_GEOMETRY
        }
        Xfree(m->head.surfs);
    }
    Xfree(m->head.tags);
    Xfree(m->head.frames);

    Xfree(m->texid);

    Xfree(m->muladdframes);

    Xfree(m->indexes);
    Xfree(m->vindexes);
    Xfree(m->maxdepths);

#ifdef USE_GLEXT
    if (m->vbos)
    {
        glDeleteBuffers(m->head.numsurfs, m->vbos);
        DO_FREE_AND_NULL(m->vbos);
    }
#endif

    Xfree(m);
}

//---------------------------------------- MD3 LIBRARY ENDS ----------------------------------------
//--------------------------------------- MD LIBRARY BEGINS  ---------------------------------------

mdmodel_t *mdload(const char *filnam)
{
    mdmodel_t *vm;
    int32_t i;

    vm = (mdmodel_t *)voxload(filnam);
    if (vm) return vm;

    buildvfs_kfd fil = kopen4load(filnam,0);

    if (fil == buildvfs_kfd_invalid)
        return NULL;

    kread(fil,&i,4);
    klseek(fil,0,SEEK_SET);

    switch (B_LITTLE32(i))
    {
    case IDP2_MAGIC:
//        initprintf("Warning: model \"%s\" is version IDP2; wanted version IDP3\n",filnam);
        vm = (mdmodel_t *)md2load(fil,filnam);
        break; //IDP2
    case IDP3_MAGIC:
        vm = (mdmodel_t *)md3load(fil);
        break; //IDP3
    default:
        vm = NULL;
        break;
    }

    kclose(fil);

    if (vm)
    {
        md3model_t *vm3 = (md3model_t *)vm;

        // smuggle the file name into the model struct.
        // head.nam is unused as far as I can tell
        Bstrncpyz(vm3->head.nam, filnam, sizeof(vm3->head.nam)-1);

        md3postload_common(vm3);

#ifdef POLYMER
        if (glrendmode != REND_POLYMER)
            if (md3postload_polymer_check(vm3))
            {
                mdfree(vm);
                vm = NULL;
            }
#endif
    }

    return vm;
}

#ifdef USE_GLEXT
void md_allocvbos(void)
{
    int32_t i;

    indexvbos = (GLuint *) Xrealloc(indexvbos, sizeof(GLuint) * r_vbocount);
    vertvbos = (GLuint *) Xrealloc(vertvbos, sizeof(GLuint) * r_vbocount);

    if (r_vbocount != allocvbos)
    {
        glGenBuffers(r_vbocount - allocvbos, &(indexvbos[allocvbos]));
        glGenBuffers(r_vbocount - allocvbos, &(vertvbos[allocvbos]));

        i = allocvbos;
        while (i < r_vbocount)
        {
            buildgl_bindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexvbos[i]);
            glBufferData(GL_ELEMENT_ARRAY_BUFFER, maxmodeltris * 3 * sizeof(uint16_t), NULL, GL_STREAM_DRAW);
            buildgl_bindBuffer(GL_ARRAY_BUFFER, vertvbos[i]);
            glBufferData(GL_ARRAY_BUFFER, maxmodelverts * sizeof(vec3f_t), NULL, GL_STREAM_DRAW);
            i++;
        }

        buildgl_bindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
        buildgl_bindBuffer(GL_ARRAY_BUFFER, 0);

        allocvbos = r_vbocount;
    }
}
#endif

int32_t polymost_mddraw(tspriteptr_t tspr)
{
#ifdef USE_GLEXT
    if (r_vbocount > allocvbos)
        md_allocvbos();
#endif

    if (maxmodelverts > allocmodelverts)
    {
        vertlist = (vec3f_t *) Xrealloc(vertlist, sizeof(vec3f_t)*maxmodelverts);
        allocmodelverts = maxmodelverts;
    }

    mdmodel_t *const vm = models[tile2model[Ptile2tile(tspr->picnum,
    (tspr->owner >= MAXSPRITES) ? tspr->pal : sprite[tspr->owner].pal)].modelid];
    if (vm->mdnum == 1)
        return polymost_voxdraw((voxmodel_t *)vm,tspr);
    else if (vm->mdnum == 3)
        return polymost_md3draw((md3model_t *)vm,tspr);
    return 0;
}

void mdfree(mdmodel_t *vm)
{
    if (vm->mdnum == 1) { voxfree((voxmodel_t *)vm); return; }
    if (vm->mdnum == 2 || vm->mdnum == 3) { md3free((md3model_t *)vm); return; }
}

#endif

//---------------------------------------- MD LIBRARY ENDS  ----------------------------------------