//------------------------------------- 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)
    {
        LOG_F(WARNING, "max EXTRATILES reached");
        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)
        {
            // LOG_F(WARNING, "backward anim %d-%d", anm->startframe, anm->endframe);
            return -3;
        }

        for (i=anm->startframe; i<anm->endframe; i++)
            bitmap_set(usedframebitmap, i);
    }

    sub = 0;
    for (i=0; i<m->numframes; i++)
    {
        if (!bitmap_test(usedframebitmap, i))
        {
            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)
            LOG_F(WARNING, "md %d WTF: anm %d %d", 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)
                LOG_F(WARNING, "md %d WTF: tile %d, fr %d", 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)
            LOG_F(INFO, "size %d (%d fr, %d v): md %s surf %d/%d", 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);

#ifdef USE_GLEXT
    if (m->vbos == NULL)
        mdloadvbos(m);
#endif

    // if (tspr->cstat & CSTAT_SPRITE_ALIGNMENT_FLOOR) return 0;

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

    auto const tsprflags = tspr->clipdist;

    //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 & CSTAT_SPRITE_ALIGNMENT) != CSTAT_SPRITE_ALIGNMENT_FACING) ? -4.f : 4.f;
    k0 -= (tspr->yoffset*tspr->yrepeat)*f;
    if ((globalorientation&128) && !(globalorientation & CSTAT_SPRITE_ALIGNMENT_FLOOR))
        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 & CSTAT_SPRITE_ALIGNMENT_FLOOR)
    {
        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 & CSTAT_SPRITE_ALIGNMENT_FLOOR)
    {
        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 (tsprflags & 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) && !(tsprflags & TSPR_FLAGS_MDHACK))  // Compare with SCREEN_FACTORS above
            a0.z = (float)((tspr->cstat & 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 (!(tsprflags & 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 & !(tspr->clipdist & TSPR_FLAGS_NO_GLOW)) ? 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:
        // LOG_F(WARNING, "model \"%s\" is version IDP2; wanted version IDP3", 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  ----------------------------------------
