code:
#ifdef USE_OPENGL

#include "baselayer.h"
#include "build.h"
#include "lz4.h"
#include "hightile.h"
#include "polymost.h"
#include "texcache.h"
#include "dxtfilter.h"
#include "scriptfile.h"
#include "xxhash.h"
#include "kplib.h"

#include "vfs.h"

#include <fcntl.h>
#ifdef _WIN32
# include <io.h>
#else
# include <unistd.h>
#endif
#include <sys/stat.h>

#include "mio.hpp"

// mio uses OS file pointers on Windows and regular int file descriptors elsewhere
#ifdef _WIN32
# define MIO_HANDLE_FROM_FP(f) (mio::file_handle_type)(_get_osfhandle(_fileno(f)))
#else
# define MIO_HANDLE_FROM_FP(f) (mio::file_handle_type)(fileno(f))
#endif

#define CLEAR_GL_ERRORS() while(glGetError() != GL_NO_ERROR) { }
#define TEXCACHE_FREEBUFS() { Xfree(pic), Xfree(packbuf), Xfree(midbuf); }

globaltexcache texcache;

char TEXCACHEFILE[BMAX_PATH] = "texturecache";

static const char *texcache_errors[TEXCACHEERRORS] = {
    "no error",
    "out of memory",
    "read too few bytes from cache file",
    "dedxtfilter failed",
    "glCompressedTexImage2DARB failed",
    "glGetTexLevelParameteriv failed",
};

void (*gloadtile_n64)(int32_t dapic, int32_t dapal, int32_t tintpalnum, int32_t dashade, int32_t dameth, pthtyp *pth, int32_t doalloc) = nullptr;

static pthtyp *texcache_tryart(int32_t const dapicnum, int32_t const dapalnum, int32_t const dashade, int32_t dameth)
{
    const int32_t j = dapicnum&(GLTEXCACHEADSIZ-1);
    pthtyp *pth;
    int32_t tintpalnum = -1;
    int32_t searchpalnum = dapalnum;
    polytintflags_t const tintflags = hictinting[dapalnum].f;

    if (tintflags & (HICTINT_USEONART|HICTINT_ALWAYSUSEART))
    {
        tintpalnum = dapalnum;
        dameth &= ~DAMETH_INDEXED;
        if (!(tintflags & HICTINT_APPLYOVERPALSWAP))
            searchpalnum = 0;
    }

    if (dameth & DAMETH_N64)
    {
        // load from art
        for (pth=texcache.list[j]; pth; pth=pth->next)
            if (pth->picnum == dapicnum && (pth->flags & PTH_N64)
                && (pth->flags & (PTH_CLAMPED | PTH_N64_INTENSIVITY | PTH_N64_SCALED)) == (TO_PTH_CLAMPED(dameth) | TO_PTH_N64_INTENSIVITY(dameth) | TO_PTH_N64_SCALED(dameth)))
            {
                if (pth->flags & PTH_INVALIDATED)
                {
                    pth->flags &= ~PTH_INVALIDATED;
                    gloadtile_n64(dapicnum, searchpalnum, tintpalnum, dashade, dameth, pth, 0);
                    pth->palnum = dapalnum;
                }

                return pth;
            }

        pth = (pthtyp *)Xcalloc(1,sizeof(pthtyp));

        gloadtile_n64(dapicnum, searchpalnum, tintpalnum, dashade, dameth, pth, 1);

        pth->palnum = dapalnum;
        pth->next = texcache.list[j];
        texcache.list[j] = pth;

        return pth;
    }

    // load from art
    for (pth=texcache.list[j]; pth; pth=pth->next)
        if (pth->picnum == dapicnum &&
            (dameth & DAMETH_INDEXED ? (pth->flags & PTH_INDEXED) &&
                                       (pth->flags & PTH_CLAMPED) == TO_PTH_CLAMPED(dameth) :
                 (pth->palnum == dapalnum && pth->shade == dashade &&
                 !(pth->flags & PTH_INDEXED) &&
                 (pth->flags & (PTH_CLAMPED | PTH_HIGHTILE | PTH_NOTRANSFIX)) ==
                     (TO_PTH_CLAMPED(dameth) | TO_PTH_NOTRANSFIX(dameth)) &&
                 polymost_want_npotytex(dameth, tilesiz[dapicnum].y) == !!(pth->flags&PTH_NPOTWALL))))
        {
            if (pth->flags & PTH_INVALIDATED)
            {
                pth->flags &= ~PTH_INVALIDATED;
                gloadtile_art(dapicnum, searchpalnum, tintpalnum, dashade, dameth, pth, 0);
                pth->palnum = dapalnum;
            }

            return pth;
        }

    pth = (pthtyp *)Xcalloc(1,sizeof(pthtyp));

    gloadtile_art(dapicnum, searchpalnum, tintpalnum, dashade, dameth, pth, 1);

    pth->palnum = dapalnum;
    pth->next = texcache.list[j];
    texcache.list[j] = pth;

    return pth;
}

pthtyp *texcache_fetchmulti(pthtyp *pth, hicreplctyp *si, int32_t dapicnum, int32_t dameth)
{
    const int32_t j = dapicnum&(GLTEXCACHEADSIZ-1);
    int32_t i;

    for (i = 0; i <= (GLTEXCACHEADSIZ - 1); i++)
    {
        const pthtyp *pth2;

        for (pth2=texcache.list[i]; pth2; pth2=pth2->next)
        {
            if (pth2->hicr && pth2->hicr->filename && si->filename && filnamcmp(pth2->hicr->filename, si->filename) == 0)
            {
                Bmemcpy(pth, pth2, sizeof(pthtyp));
                pth->picnum = dapicnum;
                pth->flags = TO_PTH_CLAMPED(dameth) | TO_PTH_NOTRANSFIX(dameth) |
                             PTH_HIGHTILE | (drawingskybox>0)*PTH_SKYBOX;
                if (pth2->flags & PTH_HASALPHA)
                    pth->flags |= PTH_HASALPHA;
                pth->hicr = si;

                pth->next = texcache.list[j];
                texcache.list[j] = pth;

                return pth;
            }
        }
    }

    return NULL;
}

// <dashade>: ignored if not in Polymost+r_usetileshades
pthtyp *texcache_fetch(int32_t dapicnum, int32_t dapalnum, int32_t dashade, int32_t dameth)
{
    const int32_t j = dapicnum & (GLTEXCACHEADSIZ - 1);
    int indexed = 0;
    hicreplctyp *si = usehightile ? hicfindsubst(dapicnum, dapalnum, hictinting[dapalnum].f & HICTINT_ALWAYSUSEART) : NULL;
    if (usehightile && (dameth & DAMETH_INDEXED) && !(hictinting[dapalnum].f & HICTINT_ALWAYSUSEART))
    {
        hicreplctyp *pal0 = hicfindsubst(dapicnum, 0, 0);
        if (pal0 && pal0->flags & HICR_INDEXED)
        {
            indexed = 1;
            si = pal0;
        }
    }

    if (drawingskybox && usehightile)
        if ((si = hicfindskybox(dapicnum, dapalnum)) == NULL)
            return NULL;

    if (polymost_usetileshades() != TS_TEXTURE || videoGetRenderMode() != REND_POLYMOST)
        dashade = 0;

    if (!si)
    {
        return (dapalnum >= (MAXPALOOKUPS - RESERVEDPALS) || hicprecaching) ?
                NULL : texcache_tryart(dapicnum, dapalnum, dashade, dameth);
    }
    if (!indexed)
        dameth &= ~DAMETH_INDEXED;

    /* if palette > 0 && replacement found
     *    no effects are applied to the texture
     * else if palette > 0 && no replacement found
     *    effects are applied to the palette 0 texture if it exists
     */

    polytintflags_t const tintflags = hictinting[dapalnum].f;

    const int32_t checktintpal = (tintflags & HICTINT_APPLYOVERALTPAL) ? 0 : si->palnum;
    const int32_t checkcachepal = ((tintflags & HICTINT_IN_MEMORY) || ((tintflags & HICTINT_APPLYOVERALTPAL) && si->palnum > 0)) ? dapalnum : si->palnum;

    // load a replacement
    for (pthtyp *pth = texcache.list[j]; pth; pth = pth->next)
    {
        if (pth->picnum == dapicnum && pth->palnum == checkcachepal && (checktintpal > 0 ? 1 : (pth->effects == tintflags))
            && (pth->flags & (PTH_CLAMPED | PTH_HIGHTILE | PTH_SKYBOX | PTH_NOTRANSFIX | PTH_INDEXED))
               == (TO_PTH_CLAMPED(dameth) | TO_PTH_NOTRANSFIX(dameth) | PTH_HIGHTILE | (drawingskybox > 0) * PTH_SKYBOX | indexed * PTH_INDEXED)
            && (drawingskybox > 0 ? (pth->skyface == drawingskybox) : 1))
        {
            if (pth->flags & PTH_INVALIDATED)
            {
                pth->flags &= ~PTH_INVALIDATED;

                int32_t tilestat = gloadtile_hi(dapicnum, dapalnum, drawingskybox, si, dameth, pth, 0,
                                        (checktintpal > 0) ? 0 : tintflags);  // reload tile

                if (!tilestat)
                    continue;

                if (tilestat == -2)  // bad filename
                    hicclearsubst(dapicnum, dapalnum);

                return (drawingskybox || hicprecaching) ? NULL : texcache_tryart(dapicnum, dapalnum, dashade, dameth);
            }

            return pth;
        }
    }

    pthtyp *pth = (pthtyp *)Xcalloc(1, sizeof(pthtyp));

    // possibly fetch an already loaded multitexture :_)
    if (dapalnum == DETAILPAL && texcache_fetchmulti(pth, si, dapicnum, dameth))
        return pth;

    int32_t tilestat =
    gloadtile_hi(dapicnum, dapalnum, drawingskybox, si, dameth, pth, 1, (checktintpal > 0) ? 0 : tintflags);

    if (!tilestat)
    {
        pth->next = texcache.list[j];
        pth->palnum = checkcachepal;
        texcache.list[j] = pth;
        return pth;
    }

    if (tilestat == -2)  // bad filename
        hicclearsubst(dapicnum, dapalnum);

    Xfree(pth);

    return (drawingskybox || hicprecaching) ? NULL : texcache_tryart(dapicnum, dapalnum, dashade, dameth);
}

static void texcache_closefiles(void)
{
    MAYBE_FCLOSE_AND_NULL(texcache.dataFilePtr);
    MAYBE_FCLOSE_AND_NULL(texcache.indexFilePtr);

    texcache_freeptrs();
}

void texcache_freeptrs(void)
{
    texcache.entrybufsiz = 0;

    if (!texcache.entries)
        return;

    for (bssize_t i = 0; i < texcache.numentries; i++)
        if (texcache.entries[i])
        {
            for (bssize_t ii = texcache.numentries - 1; ii >= 0; ii--)
                if (i != ii && texcache.entries[ii] == texcache.entries[i])
                {
                    /*OSD_Printf("removing duplicate cacheptr %d\n",ii);*/
                    texcache.entries[ii] = NULL;
                }

            DO_FREE_AND_NULL(texcache.entries[i]->name);
            DO_FREE_AND_NULL(texcache.entries[i]);
        }

    DO_FREE_AND_NULL(texcache.entries);
}

static inline void texcache_clearmemcache(void)
{
    texcache.rw_mmap.unmap();
}

void texcache_syncmemcache(void)
{
    if (!texcache.dataFilePtr || buildvfs_flength(texcache.dataFilePtr) <= 0)
        return;

    std::error_code error;

    if (texcache.rw_mmap.is_mapped())
    {
        texcache.rw_mmap.sync(error);

        if (error)
        {
            LOG_F(ERROR, "Failed syncing mmap texcache: %s (%d).", error.message().c_str(), error.value());
            texcache.rw_mmap.unmap();
        }
    }
}

void texcache_init(void)
{
    if (!texcache.indexFilePtr)
    {
        if (texcache.dataFilePtr)
            Bfclose((FILE *)texcache.dataFilePtr);

        texcache.dataFilePtr = nullptr;
    }

    texcache_closefiles();
    texcache_clearmemcache();
    texcache_freeptrs();

    texcache.current = texcache.first = (texcacheindex *)Xcalloc(1, sizeof(texcacheindex));
    texcache.numentries = 0;

    //    Bmemset(&firstcacheindex, 0, sizeof(texcacheindex));
    //    Bmemset(&cacheptrs[0], 0, sizeof(cacheptrs));

    texcache.hashes.size = TEXCACHEHASHSIZE;
    hash_init(&texcache.hashes);
}

static void texcache_deletefiles(void)
{
    Bassert(!texcache.indexFilePtr && !texcache.dataFilePtr);

    unlink(TEXCACHEFILE);
    Bstrcpy(ptempbuf, TEXCACHEFILE);
    Bstrcat(ptempbuf, ".index");
    unlink(ptempbuf);
}

int32_t texcache_enabled(void)
{
#if defined EDUKE32_GLES || !defined USE_GLEXT
    return 0;
#else
    if (!glinfo.texcompr || !glusetexcompr || !glusetexcache)
        return 0;

    if (!texcache.indexFilePtr || !texcache.dataFilePtr)
    {
        LOG_F(WARNING, "No active texcache!");
        return 0;
    }

    return 1;
#endif
}

void texcache_openfiles(void)
{
    Bassert(!texcache.indexFilePtr && !texcache.dataFilePtr);

    Bstrcpy(ptempbuf, TEXCACHEFILE);
    Bstrcat(ptempbuf, ".index");

    bool const texcache_exists = buildvfs_exists(ptempbuf);

    texcache.indexFilePtr = buildvfs_fopen_append(ptempbuf);
    texcache.dataFilePtr  = buildvfs_fopen_append(TEXCACHEFILE);

    if (!texcache.indexFilePtr || !texcache.dataFilePtr)
    {
        LOG_F(ERROR, "Unable to open cache file %s or %s: %s.", TEXCACHEFILE, ptempbuf, strerror(errno));
        texcache_closefiles();
        glusetexcache = 0;
        return;
    }

    if (!texcache_exists)
    {
        buildvfs_fputstr(texcache.indexFilePtr, "// automatically generated by the engine, DO NOT MODIFY!\n");
    }

    LOG_F(INFO, "Opened %s as cache file.", TEXCACHEFILE);
}

void texcache_checkgarbage(void)
{
    if (!texcache_enabled())
        return;

    texcache.current = texcache.first;

    int32_t bytes = 0;

    while (texcache.current->next)
    {
        bytes += texcache.current->len;
        texcache.current = texcache.current->next;
    }

    buildvfs_fseek_end(texcache.dataFilePtr);
    bytes = buildvfs_ftell(texcache.dataFilePtr)-bytes;

    if (bytes)
        LOG_F(INFO, "Cache contains %d bytes of garbage data", bytes);
}

void texcache_invalidate(void)
{
    DVLOG_F(LOG_DEBUG, "Invalidating texcache...");

    r_downsizevar = r_downsize; // update the cvar representation when the menu changes r_downsize

    polymost_glreset();

    texcache_init();
    texcache_deletefiles();
    texcache_openfiles();
}

int texcache_loadoffsets(void)
{
    Bstrcpy(ptempbuf, TEXCACHEFILE);
    Bstrcat(ptempbuf, ".index");
    scriptfile *script = scriptfile_fromfile(ptempbuf);

    if (!script) return -1;

    int32_t foffset, fsize;
    char *fname;

    while (!scriptfile_eof(script))
    {
        if (scriptfile_getstring(script, &fname)) break;	// hashed filename
        if (scriptfile_getnumber(script, &foffset)) break;	// offset in cache
        if (scriptfile_getnumber(script, &fsize)) break;	// size

        int const i = hash_find(&texcache.hashes,fname);

        if (i > -1)
        {
            // update an existing entry
            texcacheindex *t = texcache.entries[i];
            t->offset = foffset;
            t->len = fsize;
            /*initprintf("%s %d got a match for %s offset %d\n",__FILE__, __LINE__, fname,foffset);*/
        }
        else
        {
            texcacheindex * const index = texcache.current;

            index->name   = Xstrdup(fname);
            index->offset = foffset;
            index->len    = fsize;
            index->next   = (texcacheindex *) Xcalloc(1, sizeof(texcacheindex));
            hash_add(&texcache.hashes, fname, texcache.numentries, 1);
            if (++texcache.numentries > texcache.entrybufsiz)
            {
                texcache.entrybufsiz += 512;
                texcache.entries = (texcacheindex **) Xrealloc(texcache.entries, sizeof(intptr_t) * texcache.entrybufsiz);
            }
            texcache.entries[texcache.numentries-1] = texcache.current;
            texcache.current = index->next;
        }
    }

    scriptfile_close(script);
    return 0;
}

// Read from on-disk texcache or its in-memory cache.
int texcache_readdata(void *outBuf, int32_t len)
{
    const bsize_t ofilepos = texcache.dataFilePos;

    texcache.dataFilePos += len;

    if (texcache.rw_mmap.is_mapped() && texcache.rw_mmap.length() >= ofilepos + len)
    {
        Bmemcpy(outBuf, texcache.rw_mmap.data() + ofilepos, len);
        return 0;
    }

    if (buildvfs_fseek_abs(texcache.dataFilePtr, ofilepos) || buildvfs_fread(outBuf, len,1,texcache.dataFilePtr) != 1)
        return 1;

    return 0;
}

char const * texcache_calcid(char *outbuf, const char *filename, const int32_t len, const int32_t dameth, const char effect)
{
    // Assert that BMAX_PATH is a multiple of 4 so that struct texcacheid_t
    // gets no padding inserted by the compiler.
    EDUKE32_STATIC_ASSERT((BMAX_PATH & 3) == 0);

    struct texcacheid_t {
        int32_t len, method;
        char effect, name[BMAX_PATH+3];  // +3: pad to a multiple of 4
    } id = { len, dameth, effect, "" };

    EDUKE32_STATIC_ASSERT((sizeof(struct texcacheid_t) & 3) == 0);

    size_t const fnlen = Bstrlen(filename);

    Bstrcpy(id.name, filename);
    Bsprintf(outbuf, "%08" PRIx64, XXH3_64bits_withSeed((uint8_t *)&id, offsetof(struct texcacheid_t, name) + fnlen, TEXCACHEMAGIC[3]));

    return outbuf;
}

#define FAIL(x) { err = x; goto failure; }

// returns 1 on success
int texcache_readtexheader(char const * cacheid, texcacheheader *head, int32_t modelp)
{
    if (!texcache_enabled())
        return 0;

    int32_t i = hash_find(&texcache.hashes, cacheid);

    if (i < 0 || !texcache.entries[i])
        return 0;  // didn't find it

    texcache.dataFilePos = texcache.entries[i]->offset;
//    initprintf("%s %d got a match for %s offset %d\n",__FILE__, __LINE__, cachefn,offset);

    int err = 0;

    if (texcache_readdata(head, sizeof(texcacheheader)))
        FAIL(0);

    if (Bmemcmp(head->magic, TEXCACHEMAGIC, 4))
        FAIL(1);

    // native (little-endian) -> internal
    head->xdim    = B_LITTLE32(head->xdim);
    head->ydim    = B_LITTLE32(head->ydim);
    head->flags   = B_LITTLE32(head->flags);
    head->quality = B_LITTLE32(head->quality);

    if (modelp && head->quality != r_downsize)
        FAIL(2);
    if ((head->flags & CACHEAD_COMPRESSED) && glusetexcache != 2)
        FAIL(3);
    if (!(head->flags & CACHEAD_COMPRESSED) && glusetexcache == 2)
        FAIL(4);

    // handle nodownsize
    if (!modelp && !(head->flags & CACHEAD_NODOWNSIZE) && head->quality != r_downsize)
        return 0;

    if (gltexmaxsize && (head->xdim > (1<<gltexmaxsize) || head->ydim > (1<<gltexmaxsize)))
        FAIL(5);
    if (!glinfo.texnpot && (head->flags & CACHEAD_NONPOW2))
        FAIL(6);

    return 1;

failure:
    {
        static const char *error_msgs[] = {
            "failed reading texture cache header",
            "header magic string doesn't match",
            "r_downsize doesn't match",  // (skins only)
            "compression doesn't match: cache contains compressed data",
            "compression doesn't match: cache contains uncompressed data",
            "texture in cache exceeds maximum supported size",
            "texture in cache has non-power-of-two size, unsupported",
        };

        LOG_F(ERROR, "Got bad %s data from cache: %s", modelp ? "skin" : "texture", error_msgs[err]);
    }

    return 0;
}

#undef READTEXHEADER_FAILURE

#if defined USE_GLEXT && !defined EDUKE32_GLES

void texcache_prewritetex(texcacheheader *head)
{
    Bmemcpy(head->magic, TEXCACHEMAGIC, 4);   // sizes are set by caller

    if (glusetexcache == 2)
        head->flags |= CACHEAD_COMPRESSED;

    // native -> external (little-endian)
    head->xdim    = B_LITTLE32(head->xdim);
    head->ydim    = B_LITTLE32(head->ydim);
    head->flags   = B_LITTLE32(head->flags);
    head->quality = B_LITTLE32(head->quality);
}

#define WRITEX_FAIL_ON_ERROR() if (glGetError() != GL_NO_ERROR) goto failure

void texcache_writetex_fromdriver(char const * const cacheid, texcacheheader *head)
{
    if (!texcache_enabled()) return;

    GLint gi = GL_FALSE;
    glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_COMPRESSED, &gi);
    if (gi != GL_TRUE)
    {
        static GLint glGetTexLevelParameterivOK = GL_TRUE;
        if (glGetTexLevelParameterivOK == GL_TRUE)
        {
            LOG_F(ERROR, "glGetTexLevelParameteriv returned GL_FALSE!");
            glGetTexLevelParameterivOK = GL_FALSE;
        }
        return;
    }

    texcache_prewritetex(head);
    buildvfs_fseek_end(texcache.dataFilePtr);
    size_t const offset = buildvfs_ftell(texcache.dataFilePtr);

    texcachepicture pict;

    char *pic     = nullptr;
    char *packbuf = nullptr;
    void *midbuf  = nullptr;
    size_t alloclen = 0;

    //    OSD_Printf("Caching %s, offset 0x%x\n", cachefn, offset);
    if (buildvfs_fwrite(head, sizeof(texcacheheader), 1, texcache.dataFilePtr) != 1) goto failure;

    CLEAR_GL_ERRORS();

    for (int level = 0, padx = 0, pady = 0; level == 0 || (padx > 1 || pady > 1); ++level)
    {
        glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_COMPRESSED, &gi);
        WRITEX_FAIL_ON_ERROR();
        if (gi != GL_TRUE)
            goto failure;  // an uncompressed mipmap

        glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_INTERNAL_FORMAT, &gi);
        WRITEX_FAIL_ON_ERROR();

#if defined __APPLE__ && defined POLYMER
        if (pr_ati_textureformat_one && gi == 1) gi = GL_COMPRESSED_RGBA_S3TC_DXT1_EXT;
#endif
        // native -> external (little endian)
        pict.format = B_LITTLE32(gi);

        glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_WIDTH, &gi);
        WRITEX_FAIL_ON_ERROR();
        padx      = gi;
        pict.xdim = B_LITTLE32(gi);

        glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_HEIGHT, &gi);
        WRITEX_FAIL_ON_ERROR();
        pady      = gi;
        pict.ydim = B_LITTLE32(gi);

        glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_BORDER, &gi);
        WRITEX_FAIL_ON_ERROR();
        pict.border = B_LITTLE32(gi);

        glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_DEPTH, &gi);
        WRITEX_FAIL_ON_ERROR();
        pict.depth = B_LITTLE32(gi);

        glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &gi);
        WRITEX_FAIL_ON_ERROR();
        uint32_t miplen = gi;
        pict.size       = B_LITTLE32(gi);

        if (alloclen < miplen)
        {
            alloclen = miplen;
            pic      = (char *)Xrealloc(pic, miplen);
            packbuf  = (char *)Xrealloc(packbuf, miplen);
            midbuf   = (void *)Xrealloc(midbuf, miplen);
        }

        glGetCompressedTexImage(GL_TEXTURE_2D, level, pic);
        WRITEX_FAIL_ON_ERROR();

        if (buildvfs_fwrite(&pict, sizeof(texcachepicture), 1, texcache.dataFilePtr) != 1) goto failure;
        if (dxtfilter(&pict, pic, midbuf, packbuf, miplen)) goto failure;
    }

    texcache_postwritetex(cacheid, offset);
    TEXCACHE_FREEBUFS();
    return;

failure:
    LOG_F(ERROR, "texcache mystery error");
    texcache.current->offset = 0;
    Xfree(texcache.current->name);
    TEXCACHE_FREEBUFS();
}

#undef WRITEX_FAIL_ON_ERROR

void texcache_postwritetex(char const * const cacheid, int32_t const offset)
{
    int32_t const i = hash_find(&texcache.hashes, cacheid);

    texcacheindex *t;

    if (i > -1)
    {
        // update an existing entry
        t = texcache.entries[i];

        t->offset = offset;
        buildvfs_fseek_end(texcache.dataFilePtr);
        t->len    = buildvfs_ftell(texcache.dataFilePtr) - t->offset;
        /*initprintf("%s %d got a match for %s offset %d\n",__FILE__, __LINE__, cachefn,offset);*/
    }
    else
    {
        t = texcache.current;

        Xfree(t->name);
        t->name   = Xstrdup(cacheid);
        t->offset = offset;
        buildvfs_fseek_end(texcache.dataFilePtr);
        t->len    = buildvfs_ftell(texcache.dataFilePtr) - t->offset;
        t->next   = (texcacheindex *)Xcalloc(1, sizeof(texcacheindex));

        hash_add(&texcache.hashes, cacheid, texcache.numentries, 0);

        if (++texcache.numentries > texcache.entrybufsiz)
        {
            texcache.entrybufsiz += 512;
            texcache.entries = (texcacheindex **)Xrealloc(texcache.entries, sizeof(intptr_t) * texcache.entrybufsiz);
        }

        texcache.entries[texcache.numentries - 1] = t;
        texcache.current = t->next;
    }

    if (texcache.indexFilePtr)
    {
        char buf[64];
        buildvfs_fputstrptr(texcache.indexFilePtr, t->name);
        snprintf(buf, sizeof(buf), " %d %d\n", t->offset, t->len);
        buildvfs_fputstrptr(texcache.indexFilePtr, buf);
    }
    else
        LOG_F(ERROR, "fatal error in texcache: no indexFilePtr");
}

#endif

static void texcache_setuptexture(int32_t *doalloc, GLuint *glpic)
{
    if (*doalloc&1)
    {
        glGenTextures(1, glpic);  //# of textures (make OpenGL allocate structure)
        *doalloc |= 2;	// prevents glGenTextures being called again if we fail in here
    }

    buildgl_bindTexture(GL_TEXTURE_2D, *glpic);
}

static char const* texcache_format_to_name(GLint format)
{
    switch (format)
    {
        case GL_COMPRESSED_RGB_S3TC_DXT1_EXT:
            return "GL_COMPRESSED_RGB_S3TC_DXT1";
        case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT:
            return "GL_COMPRESSED_RGBA_S3TC_DXT1";
        case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT:
            return "GL_COMPRESSED_RGBA_S3TC_DXT3";
        case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT:
            return "GL_COMPRESSED_RGBA_S3TC_DXT5";
        default:
            return "UNKNOWN/INVALID";
    }
}

static int32_t texcache_loadmips(const texcacheheader *head, GLenum *glerr)
{
    texcachepicture pict;

    char *pic     = nullptr;
    char *packbuf = nullptr;
    void *midbuf  = nullptr;

    int32_t alloclen=0;

#if !defined USE_GLEXT && defined EDUKE32_GLES
    UNREFERENCED_PARAMETER(glerr);
    UNREFERENCED_PARAMETER(head);
#endif

    for (bssize_t level = 0; level==0 || (pict.xdim > 1 || pict.ydim > 1); level++)
    {
        if (texcache_readdata(&pict, sizeof(texcachepicture)))
        {
            TEXCACHE_FREEBUFS();
            return TEXCACHERR_BUFFERUNDERRUN;
        }

        // external (little endian) -> native
        pict.size   = B_LITTLE32(pict.size);
        pict.format = B_LITTLE32(pict.format);
        pict.xdim   = B_LITTLE32(pict.xdim);
        pict.ydim   = B_LITTLE32(pict.ydim);
        pict.border = B_LITTLE32(pict.border);
        pict.depth  = B_LITTLE32(pict.depth);

        if (alloclen < pict.size)
        {
            alloclen = pict.size;
            pic      = (char *)Xrealloc(pic, pict.size);
            packbuf  = (char *)Xrealloc(packbuf, pict.size + 16);
            midbuf   = (void *)Xrealloc(midbuf, pict.size);
        }

#if defined USE_GLEXT && !defined EDUKE32_GLES
        if (dedxtfilter(&pict, pic, midbuf, packbuf, (head->flags & CACHEAD_COMPRESSED) != 0))
        {
            TEXCACHE_FREEBUFS();
            return TEXCACHERR_DEDXT;
        }

        glCompressedTexImage2D(GL_TEXTURE_2D, level, pict.format, pict.xdim, pict.ydim, pict.border, pict.size, pic);
        if ((*glerr=glGetError()) != GL_NO_ERROR)
        {
            TEXCACHE_FREEBUFS();
            return TEXCACHERR_COMPTEX;
        }

        GLint format;
        glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_INTERNAL_FORMAT, &format);
        if ((*glerr = glGetError()) != GL_NO_ERROR)
        {
            TEXCACHE_FREEBUFS();
            return TEXCACHERR_GETTEXLEVEL;
        }

        if (pict.format != format)
        {
            LOG_F(ERROR, "Invalid texcache format, have format %s but need format %s.",
                                    texcache_format_to_name(pict.format), texcache_format_to_name(format));
            TEXCACHE_FREEBUFS();
            return -1;
        }
#endif
    }

    TEXCACHE_FREEBUFS();
    return 0;
}

int32_t texcache_loadskin(const texcacheheader *head, int32_t *doalloc, GLuint *glpic, vec2_t *siz)
{
    int32_t err=0;
    GLenum glerr=GL_NO_ERROR;

    texcache_setuptexture(doalloc, glpic);

    siz->x = head->xdim;
    siz->y = head->ydim;

    CLEAR_GL_ERRORS();

    if ((err = texcache_loadmips(head, &glerr)))
    {
        if (err > 0)
            LOG_F(ERROR, "Unable to load cached data: %s (error %x)", texcache_errors[err], glerr);

        return -1;
    }

    return 0;
}

int32_t texcache_loadtile(const texcacheheader *head, int32_t *doalloc, pthtyp *pth)
{
    int32_t err   = 0;
    GLenum  glerr = GL_NO_ERROR;

    texcache_setuptexture(doalloc, &pth->glpic);

    pth->siz.x = head->xdim;
    pth->siz.y = head->ydim;

    CLEAR_GL_ERRORS();

    if ((err = texcache_loadmips(head, &glerr)))
    {
        if (err > 0)
            LOG_F(ERROR, "Unable to load cached data: %s (error %x)", texcache_errors[err], glerr);

        return -1;
    }

    return 0;
}

void texcache_setupmemcache(void)
{
    if (!glusememcache || !texcache_enabled())
        return;

    if (!texcache.dataFilePtr || buildvfs_flength(texcache.dataFilePtr) <= 0)
        return;

    std::error_code error;

    texcache.rw_mmap = mio::make_mmap_sink(MIO_HANDLE_FROM_FP(texcache.dataFilePtr), 0, mio::map_entire_file, error);

    if (error || !texcache.rw_mmap.is_mapped())
    {
        if (error)
            initprintf("Failed mapping texcache! Error %d (%s).\n", error.value(), error.message().c_str());

        texcache_clearmemcache();
        return;
    }
    else
    {
        initprintf("Mapped %d byte texcache\n", (int)texcache.rw_mmap.length());
    }
}

// ---------------------------------------
// Voxel 2 Poly Disk Caching
// ---------------------------------------

#define INITVARS_VOXSIZES(_vs, _is, _ts, _total) do {\
    _vs = 5 * 4 * ((size_t)vm->qcnt) * sizeof(GLfloat);\
    _is = 3 * 2 * ((size_t)vm->qcnt) * sizeof(GLuint);\
    _ts = ((size_t)vm->mytexx) * ((size_t)vm->mytexy) * sizeof(int32_t);\
    _total = _vs + _is + _ts;\
} while(0);

struct voxcachedat_t {
    int32_t qcnt, mytexx, mytexy;
    int32_t compressed_size;
};

voxmodel_t* voxcache_fetchvoxmodel(const char* const cacheid)
{
    if (!texcache_enabled()) return NULL;

    int32_t i = hash_find(&texcache.hashes, cacheid);
    if (i < 0 || !texcache.entries[i])
        return NULL;  // didn't find it

    voxcachedat_t voxd = {};
    size_t vertexsize, indexsize, mytexsize, totalsize;
    voxmodel_t* vm = (voxmodel_t*)Xcalloc(1, sizeof(voxmodel_t));

    texcache.dataFilePos = texcache.entries[i]->offset;
    texcache_readdata(&voxd, sizeof(voxd));
    vm->mytexx = voxd.mytexx;
    vm->mytexy = voxd.mytexy;
    vm->qcnt = voxd.qcnt;
    INITVARS_VOXSIZES(vertexsize, indexsize, mytexsize, totalsize);

    char* compressed_data = (char*)Xmalloc(voxd.compressed_size);
    texcache_readdata(compressed_data, voxd.compressed_size);

    char* decompressed_data = (char*)Xmalloc(totalsize);
    auto bytes = LZ4_decompress_safe(compressed_data, decompressed_data, voxd.compressed_size, totalsize);
    Xfree(compressed_data);

    UNREFERENCED_PARAMETER(bytes);
    Bassert(bytes > 0);

    vm->vertex = (GLfloat*)Xmalloc(vertexsize);
    Bmemcpy(vm->vertex, decompressed_data, vertexsize);

    vm->index = (GLuint*)Xmalloc(indexsize);
    Bmemcpy(vm->index, &decompressed_data[vertexsize], indexsize);

    vm->mytex = (int32_t*)Xmalloc(mytexsize);
    Bmemcpy(vm->mytex, &decompressed_data[vertexsize + indexsize], mytexsize);

    Xfree(decompressed_data);
    return vm;
}

void voxcache_writevoxmodel(const char* const cacheid, voxmodel_t* vm)
{
    if (!vm || !texcache_enabled()) return;

    size_t vertexsize, indexsize, mytexsize, totalsize;
    voxcachedat_t vxdat = { vm->qcnt, vm->mytexx, vm->mytexy, 0 };
    INITVARS_VOXSIZES(vertexsize, indexsize, mytexsize, totalsize);

    char* srcdata = (char*)Xmalloc(totalsize);
    Bmemcpy(srcdata, vm->vertex, vertexsize);
    Bmemcpy(&srcdata[vertexsize], vm->index, indexsize);
    Bmemcpy(&srcdata[vertexsize + indexsize], vm->mytex, mytexsize);

    int const max_compressed_size = LZ4_compressBound(totalsize);
    char* targetdata = (char*) Xmalloc(max_compressed_size);
    int32_t const actual_compressed_size = LZ4_compress_default((const char*)srcdata, targetdata, totalsize, max_compressed_size);
    Xfree(srcdata);

    Bassert(actual_compressed_size > 0);
    vxdat.compressed_size = actual_compressed_size;
    targetdata = (char*)Xrealloc(targetdata, actual_compressed_size);

    buildvfs_fseek_end(texcache.dataFilePtr);
    size_t const offset = buildvfs_ftell(texcache.dataFilePtr);
    if (buildvfs_fwrite(&vxdat, sizeof(voxcachedat_t), 1, texcache.dataFilePtr) != 1)
        goto vxstore_failure;
    if (buildvfs_fwrite(targetdata, actual_compressed_size, 1, texcache.dataFilePtr) != 1)
        goto vxstore_failure;

    texcache_postwritetex(cacheid, offset);
    Xfree(targetdata);
    return;

vxstore_failure:
    LOG_F(ERROR, "voxcache mystery error");
    texcache.current->offset = 0;
    Xfree(texcache.current->name);
    Xfree(targetdata);
}

#endif


code:
/*
 * tilepacker.cpp
 *  A k-d tree based bin packer that organizes rectangular tiles to fit
 *  neatly into one texture.
 *
 * Copyright � 2018, Alex Dawson. All rights reserved.
 */

#include "compat.h"
#include "build.h"
#include "tilepacker.h"

typedef struct TreeNode
{
    struct TreeNode *pParent, *pChild0, *pChild1;
    TileRect rect;
    uint32_t maxSide;
    uint32_t tileUID;
} TreeNode;

// allocate all the memory we could ever need up front to avoid dynamic allocation
#define NUM_NON_ROOT_NODES (MAXPACKEDTILES*2)
#define NUM_NODES (NUM_NON_ROOT_NODES+MAXTILESHEETS)
TreeNode nodes[NUM_NODES];
uint32_t heapNodes = 0;
uint32_t nextTreeNodeIndex = NUM_NON_ROOT_NODES-1;

Tile tiles[MAXPACKEDTILES];

// node rejection queue implemented using a circular buffer
#define MAX_REJECTS (MAXPACKEDTILES-1)
TreeNode rejectQueue[MAX_REJECTS];
uint32_t rejectQueueHeadIndex = 0;
uint32_t numRejected = 0;

#if 0
static void maxheap_bubbleUp(uint32_t nodeIndex)
{
    while (true)
    {
        if (nodeIndex == 0)
        {
            return;
        }

        uint32_t parentIndex = (nodeIndex-1)/2;
        if (nodes[parentIndex].maxSide >= nodes[nodeIndex].maxSide)
        {
            return;
        }

        // bubble up
        TreeNode temp = nodes[nodeIndex];
        nodes[nodeIndex] = nodes[parentIndex];
        nodes[parentIndex] = temp;

        nodeIndex = parentIndex;
    }
}
#endif

static void maxheap_bubbleDown(uint32_t nodeIndex)
{
    while (true)
    {
        uint32_t largestChildIndex = 2*nodeIndex + 1;
        if (largestChildIndex >= heapNodes)
        {
            return;
        }

        uint32_t rightChildIndex = largestChildIndex+1;
        if (rightChildIndex < heapNodes &&
            nodes[rightChildIndex].maxSide > nodes[largestChildIndex].maxSide)
        {
            largestChildIndex = rightChildIndex;
        }

        if (nodes[largestChildIndex].maxSide <= nodes[nodeIndex].maxSide)
        {
            return;
        }

        // bubble down
        TreeNode temp = nodes[nodeIndex];
        nodes[nodeIndex] = nodes[largestChildIndex];
        nodes[largestChildIndex] = temp;

        nodeIndex = largestChildIndex;
    }
}

static void maxheap_buildHeap()
{
    if (heapNodes)
    for (int i = (heapNodes-2)/2; i >= 0; --i)
    {
        maxheap_bubbleDown(i);
    }
}

static TreeNode* maxheap_pop()
{
    if (heapNodes == 0)
    {
        return NULL;
    }

    // swap the root and the last node
    TreeNode temp = nodes[0];
    nodes[0] = nodes[heapNodes-1];
    nodes[heapNodes-1] = temp;
    --heapNodes;

    maxheap_bubbleDown(0);

    return nodes+heapNodes;
}

static TreeNode* maxheap_reserveNode(TreeNode *pParent,
                                     TreeNode *pChild0,
                                     TreeNode *pChild1,
                                     TileRect rectangle,
                                     uint32_t tileUID)
{
    if (heapNodes == nextTreeNodeIndex+1)
    {
        // our tree and heap are going to collide
#ifdef DEBUGGINGAIDS
        OSD_Printf("tilepacker: maxheap_reserveNode(): out of nodes\n");
#endif
        return NULL;
    }

    nodes[heapNodes] = {(TreeNode*) pParent,
                        (TreeNode*) pChild0,
                        (TreeNode*) pChild1,
                        rectangle,
                        rectangle.width >= rectangle.height ? rectangle.width : rectangle.height,
                        tileUID};
    ++heapNodes;

    return nodes+heapNodes-1;
}

static TreeNode* kdtree_reserveNode(TreeNode *pParent,
                                    TreeNode *pChild0,
                                    TreeNode *pChild1,
                                    TileRect rectangle,
                                    uint32_t tileUID)
{
    if (nextTreeNodeIndex == heapNodes-1)
    {
        // our tree and heap are going to collide
#ifdef DEBUGGINGAIDS
        OSD_Printf("tilepacker: kdtree_reserveNode(): out of nodes\n");
#endif
        return NULL;
    }

    nodes[nextTreeNodeIndex] = {(TreeNode*) pParent,
                                (TreeNode*) pChild0,
                                (TreeNode*) pChild1,
                                rectangle,
                                rectangle.width >= rectangle.height ? rectangle.width : rectangle.height,
                                tileUID};
    --nextTreeNodeIndex;

    return nodes+nextTreeNodeIndex+1;
}

static char kdtree_add(uint32_t treeIndex, TreeNode *pNode)
{
    TreeNode *pCurrentNode = nodes+NUM_NODES-treeIndex-1;
    while (true)
    {
        // is this node large enough to contain the currentNode?
        if (pCurrentNode->rect.width >= pNode->rect.width &&
            pCurrentNode->rect.height >= pNode->rect.height)
        {
            if (pCurrentNode->tileUID != (uint32_t) -1)
            {
                // if we're not a leaf node, continue to tunnel down until we reach a leaf on the 0-side of the tree
                pCurrentNode = pCurrentNode->pChild0;
                continue;
            }

            // otherwise, we have the node we want to split to insert our new node
            break;
        }

        // climb out until we find an unexplored 1-side branch to tunnel down
        TreeNode *lastNode;
        do
        {
            lastNode = pCurrentNode;
            pCurrentNode = pCurrentNode->pParent;

            if (pCurrentNode == NULL)
            {
                // we've fully explored the tree and asked for the root's parent
                return false;
            }
        } while (pCurrentNode->pChild1 == lastNode || !pCurrentNode->pChild1);

        pCurrentNode = pCurrentNode->pChild1;
    }

    // assign the empty leaf node the tileUID we want to add, then create children to split the node's remaining space
    pCurrentNode->tileUID = pNode->tileUID;
    tiles[pNode->tileUID].tilesheetID = treeIndex;
    tiles[pNode->tileUID].rect = {pCurrentNode->rect.u,
                                  pCurrentNode->rect.v,
                                  pNode->rect.width,
                                  pNode->rect.height};

    uint32_t rightSideWidth = pCurrentNode->rect.width - pNode->rect.width;
    uint32_t bottomSideHeight = pCurrentNode->rect.height - pNode->rect.height;
    TileRect rect0 = {pCurrentNode->rect.u+pNode->rect.width, pCurrentNode->rect.v, rightSideWidth, pCurrentNode->rect.height};
    TileRect rect1 = {pCurrentNode->rect.u, pCurrentNode->rect.v+pNode->rect.height, pNode->rect.width, bottomSideHeight};
    // decide which way to split
    if (rightSideWidth < bottomSideHeight)
    {
        //POGOTODO: instead of creating two new children and having the tree contain filled nodes
        //          I should usurp the place of pCurrentNode with the largest child,
        //          since the smaller child can always be contained within the larger one.
        //          This requires an additional sort/bubbling step and always splitting width/height
        //          based on level rather than max in order for the organization to work.

        // we'll have a more width-confined space to our right, so chop it off horizontally
        // rather than create a tall, potentially narrow empty area to have to fill later
        rect0 = {pCurrentNode->rect.u, pCurrentNode->rect.v+pNode->rect.height, pCurrentNode->rect.width, bottomSideHeight};
        rect1 = {pCurrentNode->rect.u+pNode->rect.width, pCurrentNode->rect.v, rightSideWidth, pNode->rect.height};
    }
    // it's important that the larger area become child0
    pCurrentNode->pChild0 = kdtree_reserveNode(pCurrentNode,
                                               NULL,
                                               NULL,
                                               rect0,
                                               -1);
    pCurrentNode->pChild1 = kdtree_reserveNode(pCurrentNode,
                                               NULL,
                                               NULL,
                                               rect1,
                                               -1);

    return true;
}

static char rejectQueue_add(TreeNode *pNode)
{
    if (numRejected >= MAX_REJECTS)
    {
        return false;
    }

    rejectQueue[(rejectQueueHeadIndex+numRejected) % MAX_REJECTS] = *pNode;
    ++numRejected;
    return true;
}

static TreeNode* rejectQueue_remove()
{
    Bassert(numRejected);

    --numRejected;
    TreeNode* pNode = rejectQueue+rejectQueueHeadIndex;
    rejectQueueHeadIndex = (rejectQueueHeadIndex+1) % MAX_REJECTS;
    return pNode;
}

/*static void tilepacker_deleteTree()
{
    //POGOTODO: this
}*/

void tilepacker_initTilesheet(uint32_t tilesheetID, uint32_t tilesheetWidth, uint32_t tilesheetHeight)
{
    //POGOTODO: delete the tree if it's already been initialized

    nodes[NUM_NODES-tilesheetID-1] = {(TreeNode*) 0,
                                      (TreeNode*) 0,
                                      (TreeNode*) 0,
                                      {0, 0, tilesheetWidth, tilesheetHeight},
                                      tilesheetWidth >= tilesheetHeight ? tilesheetWidth : tilesheetHeight,
                                      (uint32_t) -1}; // use the maximum uint32_t to indicate this node is empty space
}

void tilepacker_addTile(uint32_t tileUID, uint32_t tileWidth, uint32_t tileHeight)
{
    if (nextTreeNodeIndex < heapNodes)
    {
        // cannot reserve any more tiles!
#ifdef DEBUGGINGAIDS
        OSD_Printf("tilepacker: tilepacker_addTile(): out of nodes\n");
#endif
        return;
    }
    if (tileWidth == 0 ||
        tileHeight == 0)
    {
        // don't allow adding tiles with a width or height of 0
        return;
    }

    maxheap_reserveNode((TreeNode*) 0,
                        (TreeNode*) 0,
                        (TreeNode*) 0,
                        {0, 0, tileWidth, tileHeight},
                        tileUID);
}

char tilepacker_pack(uint32_t tilesheetID)
{
    if (tilesheetID >= MAXTILESHEETS)
    {
        return false;
    }

    for (int numLeft = numRejected; numLeft > 0; --numLeft)
    {
        TreeNode *pNode = rejectQueue_remove();
        char success = kdtree_add(tilesheetID, pNode);
        if (!success)
        {
            rejectQueue_add(pNode);
        }
    }

    maxheap_buildHeap();
    for (TreeNode *pNode = maxheap_pop(); pNode != NULL; pNode = maxheap_pop())
    {
        char success = kdtree_add(tilesheetID, pNode);
        if (!success)
        {
            rejectQueue_add(pNode);
        }
    }

    return numRejected == 0;
}

void tilepacker_discardRejects()
{
    numRejected = 0;
}

char tilepacker_getTile(uint32_t tileUID, Tile *pOutput)
{
    if (tileUID >= MAXPACKEDTILES)
    {
        return false;
    }

    Tile tile = tiles[tileUID];
    if (tile.rect.width == 0)
    {
        // that tileUID has not been packed or didn't fit
        return false;
    }

    if (pOutput)
    {
        *pOutput = tile;
    }
    return true;
}

repo_name: orochi/nblood

license: mit


code:
// "Build Engine & Tools" Copyright (c) 1993-1997 Ken Silverman
// Ken Silverman's official web site: "http://www.advsys.net/ken"
// See the included license file "BUILDLIC.TXT" for license info.
//
// This file has been modified from Ken Silverman's original release
// by Jonathon Fowler (jf@jonof.id.au)
// by the EDuke32 team (development@voidpoint.com)

#include "baselayer.h"
#include "build.h"
#include "cache1d.h"
#include "compat.h"
#include "crc32.h"
#include "engine_priv.h"
#include "lz4.h"
#include "texcache.h"
#include "vfs.h"

static void *g_vm_data;

// The tile file number (tilesXXX <- this) of each tile:
// 0 <= . < MAXARTFILES_BASE: tile is in a "base" ART file
// MAXARTFILES_BASE <= . < MAXARTFILES_TOTAL: tile is in a map-specific ART file
uint8_t tilefilenum[MAXTILES];
EDUKE32_STATIC_ASSERT(MAXARTFILES_TOTAL <= 256);

static int32_t tilefileoffs[MAXTILES];

// Backup tilefilenum[] and tilefileoffs[]. These get allocated only when
// necessary (have per-map ART files).
static uint8_t *g_bakTileFileNum;
static int32_t *g_bakTileFileOffs;
static vec2_16_t *g_bakTileSiz;
static picanm_t *g_bakPicAnm;
static char * g_bakFakeTile;
static char ** g_bakFakeTileData;
static rottile_t *g_bakRottile;
// NOTE: picsiz[] is not backed up, but recalculated when necessary.

//static int32_t artsize = 0;
static int32_t g_vm_size = 0;

static char artfilename[BMAX_PATH];
static char artfilenameformat[BMAX_PATH];
static char mapartfilename[BMAX_PATH];  // map-specific ART file name
static int32_t mapartfnXXofs;  // byte offset to 'XX' (the number part) in the above
static int32_t artfilnum, artfilplc;
static buildvfs_kfd artfil;

////////// Per-map ART file loading //////////

// Some forward declarations.
static const char *artGetIndexedFileName(int32_t tilefilei);
static int32_t artReadIndexedFile(int32_t tilefilei);

static inline void artClearMapArtFilename(void)
{
    Bmemset(mapartfilename, 0, sizeof(mapartfilename));
    mapartfnXXofs = 0;
}

static inline void artUpdateManifest(void)
{
    for (bssize_t i=0; i<MAXTILES; i++)
        tileUpdatePicSiz(i);
}

template <typename origar_t, typename bakar_t>
static inline void RESTORE_MAPART_ARRAY(origar_t & origar, bakar_t & bakar)
{
    EDUKE32_STATIC_ASSERT(sizeof(origar[0]) == sizeof(bakar[0]));
    for (size_t i=0; i < ARRAY_SIZE(origar); i++)
        if (tilefilenum[i] >= MAXARTFILES_BASE)
            origar[i] = bakar[i];
    DO_FREE_AND_NULL(bakar);
}

template <typename origar_t, typename bakar_t>
static inline void ALLOC_MAPART_ARRAY(origar_t & origar, bakar_t & bakar)
{
    bakar = (bakar_t) Xmalloc(ARRAY_SIZE(origar) * sizeof(origar[0]));
    Bmemcpy(bakar, origar, ARRAY_SIZE(origar) * sizeof(origar[0]));
}

void artClearMapArt(void)
{
    if (g_bakTileFileNum == NULL)
        return;  // per-map ART N/A

    artClearMapArtFilename();

    if (artfilnum >= MAXARTFILES_BASE)
    {
        kclose(artfil);

        artfil = buildvfs_kfd_invalid;
        artfilnum = -1;
        artfilplc = 0L;
    }

    for (bssize_t i=0; i<MAXTILES; i++)
    {
        if (tilefilenum[i] >= MAXARTFILES_BASE)
        {
            // XXX: OK way to free it? Better: cache1d API. CACHE1D_FREE
            walock[i] = CACHE1D_FREE;
            waloff[i] = 0;
        }
    }

    // Restore original per-tile arrays
    RESTORE_MAPART_ARRAY(tilefileoffs, g_bakTileFileOffs);
    RESTORE_MAPART_ARRAY(tilesiz, g_bakTileSiz);
    RESTORE_MAPART_ARRAY(picanm, g_bakPicAnm);
    RESTORE_MAPART_ARRAY(rottile, g_bakRottile);

    // restore entire faketile array as it can cause problems otherwise
    EDUKE32_STATIC_ASSERT(sizeof(faketile[0]) == sizeof(g_bakFakeTile[0]));
    Bmemcpy(faketile, g_bakFakeTile, ARRAY_SIZE(faketile) * sizeof(faketile[0]));
    DO_FREE_AND_NULL(g_bakFakeTile);

    for (size_t i = 0; i < MAXUSERTILES; ++i)
    {
        if (tilefilenum[i] >= MAXARTFILES_BASE && faketiledata[i] != g_bakFakeTileData[i])
        {
            Xfree(faketiledata[i]);
            faketiledata[i] = g_bakFakeTileData[i];
        }
    }
    DO_FREE_AND_NULL(g_bakFakeTileData);

    // must be restored last
    RESTORE_MAPART_ARRAY(tilefilenum, g_bakTileFileNum);

    artUpdateManifest();
#ifdef USE_OPENGL
    //POGOTODO: review this to ensure we're not invalidating more than we have to
    gltexinvalidatetype(INVALIDATE_ART);
# ifdef POLYMER
    if (videoGetRenderMode() == REND_POLYMER)
        polymer_texinvalidate();
# endif
#endif
}

void artSetupMapArt(const char *filename)
{
    artClearMapArt();

    if (Bstrlen(filename) + 7 >= sizeof(mapartfilename))
        return;

    Bstrcpy(mapartfilename, filename);
    append_ext_UNSAFE(mapartfilename, "_XX.art");
    mapartfnXXofs = Bstrlen(mapartfilename) - 6;

    // Check for first per-map ART file: if that one doesn't exist, don't load any.
    buildvfs_kfd fil = kopen4loadfrommod(artGetIndexedFileName(MAXARTFILES_BASE), 0);

    if (fil == buildvfs_kfd_invalid)
    {
        artClearMapArtFilename();
        return;
    }

    kclose(fil);

    // Allocate backup arrays.
    ALLOC_MAPART_ARRAY(tilefilenum, g_bakTileFileNum);
    ALLOC_MAPART_ARRAY(tilefileoffs, g_bakTileFileOffs);
    ALLOC_MAPART_ARRAY(tilesiz, g_bakTileSiz);
    ALLOC_MAPART_ARRAY(picanm, g_bakPicAnm);
    ALLOC_MAPART_ARRAY(faketile, g_bakFakeTile);
    ALLOC_MAPART_ARRAY(faketiledata, g_bakFakeTileData);
    ALLOC_MAPART_ARRAY(rottile, g_bakRottile);

    for (bssize_t i=MAXARTFILES_BASE; i<MAXARTFILES_TOTAL; i++)
    {
        int ret = artReadIndexedFile(i);

        if (ret != 0)
        {
            // NOTE: i == MAXARTFILES_BASE && ret == -1 can only have happened
            // if the file was deleted between the above file existence check
            // and now.  Very cornerly... but I like my code to be prepared to
            // any eventuality.
            if (i == MAXARTFILES_BASE || ret != -1)
                artClearMapArt();
            break;
        }
    }

    artUpdateManifest();
#ifdef USE_OPENGL
    //POGOTODO: review this to ensure we're not invalidating more than we have to
    gltexinvalidatetype(INVALIDATE_ART);
# ifdef POLYMER
    if (videoGetRenderMode() == REND_POLYMER)
        polymer_texinvalidate();
# endif
#endif
}

//
// ART loading
//

void tileSetupDummy(int32_t const tile)
{
    bitmap_set(faketile, tile);
    DO_FREE_AND_NULL(faketiledata[tile]);
}

static void tileSetDataSafe(int32_t const tile, int32_t tsiz, char const * const buffer)
{
    int const compressed_tsiz = LZ4_compressBound(tsiz);
    char * newtile = (char *) Xmalloc(compressed_tsiz);

    if ((tsiz = LZ4_compress_default(buffer, newtile, tsiz, compressed_tsiz)) != -1)
    {
        faketilesize[tile] = tsiz;
        faketiledata[tile] = (char *) Xrealloc(newtile, tsiz);
        bitmap_set(faketile, tile);
        tilefilenum[tile] = MAXARTFILES_TOTAL;
    }
    else
    {
        Xfree(newtile);
    }
}

void tileSetData(int32_t const tile, int32_t tsiz, char const * const buffer)
{
    int const compressed_tsiz = LZ4_compressBound(tsiz);
    faketiledata[tile] = (char *) Xrealloc(faketiledata[tile], compressed_tsiz);

    if ((tsiz = LZ4_compress_default(buffer, faketiledata[tile], tsiz, compressed_tsiz)) != -1)
    {
        faketilesize[tile] = tsiz;
        faketiledata[tile] = (char *) Xrealloc(faketiledata[tile], tsiz);
        bitmap_set(faketile, tile);
        tilefilenum[tile] = MAXARTFILES_TOTAL;
    }
    else
    {
        DO_FREE_AND_NULL(faketiledata[tile]);
        bitmap_clear(faketile, tile);
    }
}

static void tileSoftDelete(int32_t const tile)
{
    tilesiz[tile].x = 0;
    tilesiz[tile].y = 0;
    picsiz[tile] = 0;

    // CACHE1D_FREE
    walock[tile] = CACHE1D_FREE;
    waloff[tile] = 0;

    bitmap_clear(faketile, tile);

    Bmemset(&picanm[tile], 0, sizeof(picanm_t));
}

void tileDelete(int32_t const tile)
{
    tileSoftDelete(tile);

    DO_FREE_AND_NULL(faketiledata[tile]);

    vox_undefine(tile);

#ifdef USE_OPENGL
    for (ssize_t i=MAXPALOOKUPS-1; i>=0; --i)
        hicclearsubst(tile, i);

    md_undefinetile(tile);
#endif
}

void tileUpdatePicSiz(int32_t picnum)
{
    int j = 15;

    while ((j > 1) && (pow2long[j] > tilesiz[picnum].x))
        j--;
    picsiz[picnum] = j;

    j = 15;
    while ((j > 1) && (pow2long[j] > tilesiz[picnum].y))
        j--;
    picsiz[picnum] |= j<<4;
}

void tileSetSize(int32_t picnum, int16_t dasizx, int16_t dasizy)
{
    tilesiz[picnum].x = dasizx;
    tilesiz[picnum].y = dasizy;

    tileUpdatePicSiz(picnum);
}

int32_t artReadHeader(buildvfs_kfd const fil, char const * const fn, artheader_t * const local)
{
    int32_t artversion;
    kread(fil, &artversion, 4); artversion = B_LITTLE32(artversion);

    if (artversion == B_LITTLE32(0x4c495542))
    {
        kread(fil, &artversion, 4); artversion = B_LITTLE32(artversion);
        if (artversion == B_LITTLE32(0x54524144))
        {
            kread(fil, &artversion, 4); artversion = B_LITTLE32(artversion);
        }
        else
        {
            LOG_F(ERROR, "Unable to load %s: bad version.", fn);
            kclose(fil);
            return 1;
        }
    }

    if (artversion != 1)
    {
        LOG_F(ERROR, "Unable to load %s: bad version.", fn);
        kclose(fil);
        return 1;
    }

    int32_t numtiles_dummy;
    kread(fil, &numtiles_dummy, 4);

    kread(fil, &local->tilestart, 4); local->tilestart = B_LITTLE32(local->tilestart);
    kread(fil, &local->tileend, 4);   local->tileend   = B_LITTLE32(local->tileend);

    if (EDUKE32_PREDICT_FALSE((uint32_t) local->tilestart >= MAXUSERTILES || (uint32_t) local->tileend >= MAXUSERTILES || local->tileend < local->tilestart))
    {
        LOG_F(ERROR, "Unable to load %s: tile range is reversed, negative, or exceeds MAXTILES: file likely corrupt.", fn);
        kclose(fil);
        return 1;
    }

    local->numtiles = (local->tileend-local->tilestart+1);

    return 0;
}

int32_t artReadHeaderFromBuffer(uint8_t const * const buf, artheader_t * const local)
{
    int const artversion = B_LITTLE32(B_UNBUF32(&buf[0]));
    if (EDUKE32_PREDICT_FALSE(artversion != 1))
    {
        LOG_F(ERROR, "Unable to load art: bad version.");
        return 1;
    }

    local->tilestart = B_LITTLE32(B_UNBUF32(&buf[8]));
    local->tileend   = B_LITTLE32(B_UNBUF32(&buf[12]));

    if (EDUKE32_PREDICT_FALSE((unsigned) local->tilestart >= MAXUSERTILES || (unsigned) local->tileend >= MAXUSERTILES || local->tileend < local->tilestart))
    {
        LOG_F(ERROR, "Unable to load art: tile range is reversed, negative, or exceeds MAXTILES: data likely corrupt.");
        return 1;
    }

    local->numtiles = (local->tileend-local->tilestart+1);

    return 0;
}

int32_t artCheckUnitFileHeader(uint8_t const * const buf, int32_t length)
{
    if (EDUKE32_PREDICT_FALSE(length <= ARTv1_UNITOFFSET))
        return -1;

    artheader_t local;
    if (EDUKE32_PREDICT_FALSE(artReadHeaderFromBuffer(buf, &local) != 0))
        return -2;

    if (EDUKE32_PREDICT_FALSE(local.numtiles != 1))
        return -3;

    return 0;
}

void tileConvertAnimFormat(int32_t const picnum, uint32_t const picanmdisk)
{
    EDUKE32_STATIC_ASSERT(PICANM_ANIMTYPE_MASK == 192);

    picanm_t * const thispicanm = &picanm[picnum];
    thispicanm->num = picanmdisk&63;
    thispicanm->xofs = (picanmdisk>>8)&255;
    thispicanm->yofs = (picanmdisk>>16)&255;
    thispicanm->sf = ((picanmdisk>>24)&15) | (picanmdisk&192);
    thispicanm->extra = (picanmdisk>>28)&15;
}

void artReadManifest(buildvfs_kfd const fil, artheader_t * const local)
{
    int16_t *tilesizx = (int16_t *) Xmalloc(local->numtiles * sizeof(int16_t));
    int16_t *tilesizy = (int16_t *) Xmalloc(local->numtiles * sizeof(int16_t));
    kread(fil, tilesizx, local->numtiles*sizeof(int16_t));
    kread(fil, tilesizy, local->numtiles*sizeof(int16_t));

    local->tileread = (uint8_t*)Xcalloc(1, (local->numtiles + 7) >> 3);

    for (bssize_t i=local->tilestart; i<=local->tileend; i++)
    {
        int32_t const localIndex = i - local->tilestart;
        int16_t const tilex = B_LITTLE16(tilesizx[localIndex]);
        int16_t const tiley = B_LITTLE16(tilesizy[localIndex]);
        int32_t const dasiz = tilex * tiley;

        uint32_t picanmdisk;
        kread(fil, &picanmdisk, sizeof(uint32_t));

        if (dasiz == 0)
            continue;

        tilesiz[i].x = tilex;
        tilesiz[i].y = tiley;

        bitmap_set(local->tileread, localIndex);

        picanmdisk = B_LITTLE32(picanmdisk);
        tileConvertAnimFormat(i, picanmdisk);
    }

    DO_FREE_AND_NULL(tilesizx);
    DO_FREE_AND_NULL(tilesizy);
}

void artPreloadFile(buildvfs_kfd const fil, artheader_t * const local)
{
    char *buffer = NULL;
    int32_t buffersize = 0;

    for (bssize_t i=local->tilestart; i<=local->tileend; i++)
    {
        int32_t const localIndex = i - local->tilestart;
        if (!bitmap_test(local->tileread, localIndex))
            continue;

        int const dasiz = tilesiz[i].x * tilesiz[i].y;
        if (dasiz == 0)
        {
            tileDelete(i);
            continue;
        }

        maybe_grow_buffer(&buffer, &buffersize, dasiz);
        kread(fil, buffer, dasiz);
        tileSetData(i, dasiz, buffer);
    }

    DO_FREE_AND_NULL(buffer);
    DO_FREE_AND_NULL(local->tileread);
}

static void artPreloadFileSafe(buildvfs_kfd const fil, artheader_t * const local)
{
    char *buffer = NULL;
    int32_t buffersize = 0;

    for (bssize_t i=local->tilestart; i<=local->tileend; i++)
    {
        int32_t const localIndex = i - local->tilestart;
        if (!bitmap_test(local->tileread, localIndex))
            continue;

        int const dasiz = tilesiz[i].x * tilesiz[i].y;
        if (dasiz == 0)
        {
            tileSoftDelete(i);
            continue;
        }

        maybe_grow_buffer(&buffer, &buffersize, dasiz);
        kread(fil, buffer, dasiz);
        tileSetDataSafe(i, dasiz, buffer);
    }

    DO_FREE_AND_NULL(buffer);
    DO_FREE_AND_NULL(local->tileread);
}

static const char *artGetIndexedFileName(int32_t tilefilei)
{
    if (tilefilei >= MAXARTFILES_BASE)
    {
        int32_t o = mapartfnXXofs;
        tilefilei -= MAXARTFILES_BASE;

        mapartfilename[o+1] = '0' + tilefilei%10;
        mapartfilename[o+0] = '0' + (tilefilei/10)%10;

        return mapartfilename;
    }
    else
    {
        Bsnprintf(artfilename, sizeof(artfilename), artfilenameformat, tilefilei);

        return artfilename;
    }
}

// Returns:
//  0: successfully read ART file
// >0: error with the ART file
// -1: ART file does not exist
//<-1: per-map ART issue
static int32_t artReadIndexedFile(int32_t tilefilei)
{
    const char *fn = artGetIndexedFileName(tilefilei);
    const int32_t permap = (tilefilei >= MAXARTFILES_BASE);  // is it a per-map ART file?
    buildvfs_kfd fil;

    if ((fil = kopen4load(fn, 0)) != buildvfs_kfd_invalid)
    {
        artheader_t local;
        int const headerval = artReadHeader(fil, fn, &local);
        if (headerval != 0)
        {
            kclose(fil);
            return headerval;
        }

        if (permap)
        {
            // Check whether we can evict existing tiles to make place for
            // per-map ART ones.
            for (int i=local.tilestart; i<=local.tileend; i++)
            {
                // Tiles having dummytile replacements or those that are
                // cache1d-locked can't be replaced.
                if (bitmap_test(faketile, i) || walock[i] >= CACHE1D_LOCKED)
                {
                    LOG_F(WARNING, "Per-map .art file could not be loaded %s: tile %d is locked by %s.", fn, i, walock[i] >= CACHE1D_LOCKED ? "cache1d" : "dummytile");
                    kclose(fil);
                    return -3;
                }
            }

            // Free existing tiles from the cache1d. CACHE1D_FREE
            Bmemset(&waloff[local.tilestart], 0, local.numtiles*sizeof(intptr_t));
            Bmemset(&walock[local.tilestart], 1, local.numtiles*sizeof(walock[0]));
        }

        artReadManifest(fil, &local);

#ifndef USE_PHYSFS
        if (cache1d_file_fromzip(fil))
#else
        if (1)
#endif
        {
            if (permap)
                artPreloadFileSafe(fil, &local);
            else
                artPreloadFile(fil, &local);
        }
        else
        {
            int offscount = ktell(fil);

            for (bssize_t i=local.tilestart; i<=local.tileend; ++i)
            {
                int32_t const localIndex = i - local.tilestart;
                if (!bitmap_test(local.tileread, localIndex))
                    continue;

                int const dasiz = tilesiz[i].x * tilesiz[i].y;

                tilefilenum[i] = tilefilei;
                tilefileoffs[i] = offscount;

                offscount += dasiz;
                // artsize += ((dasiz+15)&0xfffffff0);
            }

            DO_FREE_AND_NULL(local.tileread);
        }

        if (permap)
            LOG_F(INFO, "Per-map .art file %s loaded", fn);

        kclose(fil);
        return 0;
    }

    return -1;
}

//
// loadpics
//
int32_t artLoadFiles(const char *filename, int32_t askedsize)
{
    Bstrncpyz(artfilenameformat, filename, sizeof(artfilenameformat));

    Bmemset(&tilesiz[0], 0, sizeof(vec2_16_t) * MAXTILES);
    Bmemset(picanm, 0, sizeof(picanm));

    for (auto &rot : rottile)
        rot = { -1, -1 };

    //    artsize = 0;

    for (int tilefilei=0; tilefilei<MAXARTFILES_BASE; tilefilei++)
        artReadIndexedFile(tilefilei);

    Bmemset(gotpic, 0, sizeof(gotpic));
    //cachesize = min((int32_t)((Bgetsysmemsize()/100)*60),max(artsize,askedsize));
    g_vm_size = (Bgetsysmemsize() <= (uint32_t)askedsize) ? (int32_t)((Bgetsysmemsize() / 100) * 60) : askedsize;
    g_vm_data = Xmalloc(g_vm_size);
    g_cache.initBuffer((intptr_t) g_vm_data, g_vm_size);

    artUpdateManifest();

    artfil = buildvfs_kfd_invalid;
    artfilnum = -1;
    artfilplc = 0L;

    return 0;
}

//
// loadtile
//
static void tilePostLoad(int16_t tilenume);

bool (*rt_tileload_callback)(int16_t tileNum) = nullptr;

bool tileLoad(int16_t tileNum)
{
    if ((unsigned) tileNum >= (unsigned) MAXTILES) return 0;
    int const dasiz = tilesiz[tileNum].x*tilesiz[tileNum].y;
    if (dasiz <= 0) return 0;

    // Allocate storage if necessary.
    if (waloff[tileNum] == 0)
    {
        walock[tileNum] = CACHE1D_UNLOCKED;
        g_cache.allocateBlock(&waloff[tileNum], dasiz, &walock[tileNum]);
    }

    if (!duke64 || !rt_tileload_callback || !rt_tileload_callback(tileNum))
        tileLoadData(tileNum, dasiz, (char *) waloff[tileNum]);

#ifdef USE_OPENGL
    if (videoGetRenderMode() >= REND_POLYMOST &&
        in3dmode())
    {
        //POGOTODO: this type stuff won't be necessary down the line -- review this
        int type;
        for (type = 0; type <= 1; ++type)
        {
            gltexinvalidate(tileNum, 0, (type ? DAMETH_CLAMPED : DAMETH_MASK) | DAMETH_INDEXED);
            texcache_fetch(tileNum, 0, 0, (type ? DAMETH_CLAMPED : DAMETH_MASK) | DAMETH_INDEXED);
        }
    }
#endif

    tilePostLoad(tileNum);

    return (waloff[tileNum] != 0 && tilesiz[tileNum].x > 0 && tilesiz[tileNum].y > 0);
}

void tileMaybeRotate(int16_t tilenume)
{
    auto &rot = rottile[tilenume];
    auto &siz = tilesiz[rot.owner];

    auto src = (char *)waloff[rot.owner];
    auto dst = (char *)waloff[tilenume];

    // the engine has a squarerotatetile() we could call, but it mirrors at the same time
    for (int x = 0; x < siz.x; ++x)
    {
        int const xofs = siz.x - x - 1;
        int const yofs = siz.y * x;

        for (int y = 0; y < siz.y; ++y)
            *(dst + y * siz.x + xofs) = *(src + y + yofs);
    }

    tileSetSize(tilenume, siz.y, siz.x);
}

void tileLoadData(int16_t tilenume, int32_t dasiz, char *buffer)
{
    int const owner = rottile[tilenume].owner;

    if (owner != -1)
    {
        if (!waloff[owner])
            tileLoad(owner);

        if (waloff[tilenume])
            tileMaybeRotate(tilenume);

        return;
    }

    int const tfn = tilefilenum[tilenume];

    // dummy tiles for highres replacements and tilefromtexture definitions
    if (bitmap_test(faketile, tilenume))
    {
        if (faketiledata[tilenume] != NULL)
            LZ4_decompress_safe(faketiledata[tilenume], buffer, faketilesize[tilenume], dasiz);

        faketimerhandler();
        return;
    }

    // Potentially switch open ART file.
    if (tfn != artfilnum)
    {
        if (artfil != buildvfs_kfd_invalid)
            kclose(artfil);

        char const *fn = artGetIndexedFileName(tfn);

        artfil = kopen4loadfrommod(fn, 0);

        if (artfil == buildvfs_kfd_invalid)
        {
            LOG_F(ERROR, "Failed opening .art file %s!", fn);
            return;
        }

        artfilnum = tfn;
        artfilplc = 0L;

        faketimerhandler();
    }

    // Seek to the right position.
    if (artfilplc != tilefileoffs[tilenume])
    {
        klseek(artfil, tilefileoffs[tilenume], BSEEK_SET);
        faketimerhandler();
    }

    kread(artfil, buffer, dasiz);
    faketimerhandler();
    artfilplc = tilefileoffs[tilenume]+dasiz;
}

static void tilePostLoad(int16_t tilenume)
{
#if !defined DEBUG_TILESIZY_512 && !defined DEBUG_TILEOFFSETS
    UNREFERENCED_PARAMETER(tilenume);
#endif
#ifdef DEBUG_TILESIZY_512
    if (tilesizy[tilenume] >= 512)
    {
        int32_t i;
        char *p = (char *) waloff[tilenume];
        for (i=0; i<tilesizx[tilenume]*tilesizy[tilenume]; i++)
            p[i] = i;
    }
#endif
#ifdef DEBUG_TILEOFFSETS
    // Add some dark blue marker lines to STEAM and CEILINGSTEAM.
    // See test_tileoffsets.map.
    if (tilenume==1250 || tilenume==1255)
    {
        char *p = (char *) waloff[tilenume];
        p[0] = p[1] = p[2] = p[3] = 254;
    }

    // Add some offset to the cocktail glass neon sign. It's more asymmetric
    // than the steam, and thus more suited to debugging the spatial
    // orientation of drawn sprites.
    if (tilenume==1008)
    {
        picanm[tilenume].xofs = 8;
        picanm[tilenume].yofs = 12;
    }
#endif
}

int32_t tileGetCRC32(int16_t tileNum)
{
    if ((unsigned)tileNum >= (unsigned)MAXTILES)
        return 0;
    int const dasiz = tilesiz[tileNum].x * tilesiz[tileNum].y;
    if (dasiz <= 0)
        return 0;

    auto data = (char *)Xmalloc(dasiz);
    tileLoadData(tileNum, dasiz, data);

    int32_t crc = Bcrc32((unsigned char *)data, (unsigned int)dasiz, 0);

    Xfree(data);

    return crc;
}

vec2_16_t tileGetSize(int16_t tileNum)
{
    if ((unsigned)tileNum >= (unsigned)MAXTILES)
        return vec2_16_t{};

    return tilesiz[tileNum];
}

// Assumes pic has been initialized to zero.
void artConvertRGB(palette_t * const pic, uint8_t const * const buf, int32_t const bufsizx, int32_t const sizx, int32_t const sizy)
{
    for (bssize_t y = 0; y < sizy; ++y)
    {
        palette_t * const picrow = &pic[bufsizx * y];

        for (bssize_t x = 0; x < sizx; ++x)
        {
            uint8_t index = buf[sizy * x + y];

            if (index == 255)
                continue;

            index *= 3;

            // pic is BGRA
            picrow[x].r = palette[index+2];
            picrow[x].g = palette[index+1];
            picrow[x].b = palette[index];
            picrow[x].f = 255;
        }
    }
}

//
// allocatepermanenttile
//
intptr_t tileCreate(int16_t tilenume, int32_t xsiz, int32_t ysiz)
{
    if (xsiz <= 0 || ysiz <= 0 || (unsigned) tilenume >= MAXTILES)
        return 0;

    int const dasiz = xsiz*ysiz;

    walock[tilenume] = CACHE1D_PERMANENT;
    g_cache.allocateBlock(&waloff[tilenume], dasiz, &walock[tilenume]);

    tileSetSize(tilenume, xsiz, ysiz);
    Bmemset(&picanm[tilenume], 0, sizeof(picanm_t));

    return waloff[tilenume];
}

//
// copytilepiece
//
void tileCopySection(int32_t tilenume1, int32_t sx1, int32_t sy1, int32_t xsiz, int32_t ysiz,
    int32_t tilenume2, int32_t sx2, int32_t sy2)
{
    char *ptr1, *ptr2, dat;
    int32_t xsiz1, ysiz1, xsiz2, ysiz2, i, j, x1, y1, x2, y2;

    xsiz1 = tilesiz[tilenume1].x; ysiz1 = tilesiz[tilenume1].y;
    xsiz2 = tilesiz[tilenume2].x; ysiz2 = tilesiz[tilenume2].y;
    if ((xsiz1 > 0) && (ysiz1 > 0) && (xsiz2 > 0) && (ysiz2 > 0))
    {
        if (waloff[tilenume1] == 0) tileLoad(tilenume1);
        if (waloff[tilenume2] == 0) tileLoad(tilenume2);

        x1 = sx1;
        for (i=0; i<xsiz; i++)
        {
            y1 = sy1;
            for (j=0; j<ysiz; j++)
            {
                x2 = sx2+i;
                y2 = sy2+j;
                if ((x2 >= 0) && (y2 >= 0) && (x2 < xsiz2) && (y2 < ysiz2))
                {
                    ptr1 = (char *) (waloff[tilenume1] + x1*ysiz1 + y1);
                    ptr2 = (char *) (waloff[tilenume2] + x2*ysiz2 + y2);
                    dat = *ptr1;
                    if (dat != 255)
                        *ptr2 = *ptr1;
                }

                y1++; if (y1 >= ysiz1) y1 = 0;
            }
            x1++; if (x1 >= xsiz1) x1 = 0;
        }
    }
}

void Buninitart(void)
{
    if (artfil != buildvfs_kfd_invalid)
        kclose(artfil);

    DO_FREE_AND_NULL(g_vm_data);
}


code:
// Build engine timer stuff

#include "timer.h"

#include "build.h"
#include "build_cpuid.h"
#include "compat.h"
#include "enet.h"
#include "renderlayer.h"

#include <time.h>

#ifdef _WIN32
# include "winbits.h"
# include <mmsystem.h>
#endif

#include <atomic>

#if defined RENDERTYPESDL && (SDL_MAJOR_VERSION >= 2)
# define HAVE_TIMER_SDL
#endif

#if !defined _WIN32 && !defined HAVE_TIMER_SDL && !defined HAVE_TIMER_RDTSC
# error No platform timer implementation!
#endif

#define CLOCK_FREQ 1000000ULL

EDUKE32_STATIC_ASSERT(CLOCK_FREQ <= 1000000000ULL && CLOCK_FREQ >= 1000000ULL);

#ifdef CLOCK_MONOTONIC_RAW
# define CLOCK_TYPE CLOCK_MONOTONIC_RAW
#else
# define CLOCK_TYPE CLOCK_MONOTONIC
#endif

static int sys_timer;
uint64_t   clockLastSampleTime;
static int clockTicksPerSecond;

static void(*usertimercallback)(void);

#ifdef HAVE_TIMER_RDTSC
static uint64_t tsc_freq;

static FORCE_INLINE ATTRIBUTE((flatten)) void timerFenceRDTSC(void)
{
#if defined __SSE2__
    // On Intel, LFENCE serializes the instruction stream and MFENCE does not.
    // On AMD, MFENCE is dispatch serializing. LFENCE is not, unless:
    // - The processor is AMD family 0Fh/11h, or
    // - The processor is AMD family 10h/12h/14h or later, and MSR (Model Specific Register) C001_1029[1] is set by the kernel
    // https://hadibrais.wordpress.com/2018/05/14/the-significance-of-the-x86-lfence-instruction/
    // https://stackoverflow.com/a/50332912

#if 0
    // MFENCE before LFENCE is preferable when using both.
    // https://www.felixcloutier.com/x86/rdtsc
    _mm_mfence();
#else
    // The above conditions are reasonable assumptions to make for using only LFENCE.
    // In the future, we may want to detect CPUs requiring MFENCE and switch to that if necessary.
#endif

    _mm_lfence();

#elif defined EDUKE32_CPU_X86
    // 32-bit x86 without SSE2 uses CPUID.
    // Use inline assembly to avoid the compiler optimizing out the intrinsic versions.
# if defined _MSC_VER
    __asm
    {
        xor eax, eax
        cpuid
    }
# else
    asm volatile
    (
        "xor %%eax, %%eax\n"
        "cpuid"
        ::: "eax", "ebx", "ecx", "edx"
    );
# endif
#else
    // Everything else gets this.
# if defined __GNUC__
    __sync_synchronize();
# else
    std::atomic_thread_fence(memory_order_seq_cst);
# endif
#endif
}

static FORCE_INLINE ATTRIBUTE((flatten)) uint64_t timerSampleRDTSC(void)
{
    timerFenceRDTSC();  // We need to serialize the instruction stream before executing RDTSC.
    uint64_t const result = eduke32_rdtsc();
    timerFenceRDTSC();  // Some sources suggest serialization is also necessary or desirable after RDTSC.
    // If this code is ever changed to run by itself in a loop in its own thread, only one fence should be needed.

    return result;
}
#endif

// returns ticks since epoch in the format and frequency specified
template<typename T> T timerGetTicks(T freq)
{
    timespec ts;
    enet_gettime(CLOCK_TYPE, &ts);
    return ts.tv_sec * freq + (T)((uint64_t)ts.tv_nsec * freq / (T)1000000000);
}

int timerGetClockRate(void) { return clockTicksPerSecond; }
uint64_t timerGetNanoTickRate(void) { return CLOCK_FREQ / 100 * clockTicksPerSecond; }
uint64_t timerGetNanoTicks(void) { return timerGetTicks<uint64_t>(timerGetNanoTickRate()); }
uint32_t timer120(void) { return timerGetTicks<uint32_t>(120); }
uint32_t timerGetTicks(void) { return timerGetTicks<uint32_t>(1000); }
double   timerGetFractionalTicks(void) { return timerGetTicks<double>(1000.0); }

ATTRIBUTE((flatten)) void timerUpdateClock(void)
{
    if (!clockTicksPerSecond) return;

    auto time = timerGetNanoTicks();
    auto ld   = lldiv((time - clockLastSampleTime) * clockTicksPerSecond, timerGetNanoTickRate());

    totalclock.setFraction((ld.rem * 65536) / timerGetNanoTickRate());
#if 0
    DLOG_F(DEBUG, "totalclock fraction %d", totalclock.getFraction());
#endif

    if (ld.quot <= 0) return;

    totalclock += ld.quot;
    clockLastSampleTime += ld.quot * tabledivide64_noinline(timerGetNanoTickRate(), clockTicksPerSecond);

    if (usertimercallback)
        for (int cnt = ld.quot; cnt > 0; cnt--)
            usertimercallback();
}

static inline int timerGetCounterType(void)
{
    switch (sys_timer)
    {
#ifdef HAVE_TIMER_SDL
        default:
        case TIMER_AUTO:
        case TIMER_SDL:
            return TIMER_SDL;
#endif // HAVE_TIMER_SDL
#ifdef _WIN32
#if !defined HAVE_TIMER_SDL
        default:
        case TIMER_AUTO:
#endif // !HAVE_TIMER_SDL
        case TIMER_QPC:
            return TIMER_QPC;
#endif // _WIN32
#ifdef HAVE_TIMER_RDTSC
#if !defined _WIN32 && !defined HAVE_TIMER_SDL
        default:
        case TIMER_AUTO:
#endif // !_WIN32 && !HAVE_TIMER_SDL
        case TIMER_RDTSC:
            return TIMER_RDTSC;
#endif
    }
}

uint64_t timerGetPerformanceCounter(void)
{
    switch (timerGetCounterType())
    {
        default:
#ifdef HAVE_TIMER_SDL
        case TIMER_SDL: return SDL_GetPerformanceCounter();
#endif
#ifdef _WIN32
        case TIMER_QPC:
        {
            LARGE_INTEGER li;
            QueryPerformanceCounter(&li);
            return li.QuadPart;
        }
#endif // _WIN32
#ifdef HAVE_TIMER_RDTSC
        case TIMER_RDTSC: return timerSampleRDTSC();
#endif
    }
}

uint64_t timerGetPerformanceFrequency(void)
{
    switch (timerGetCounterType())
    {
        default:
#ifdef HAVE_TIMER_SDL
        case TIMER_SDL: return SDL_GetPerformanceFrequency();
#endif
#ifdef _WIN32
        case TIMER_QPC:
        {
            LARGE_INTEGER li;
            QueryPerformanceFrequency(&li);
            return li.QuadPart;
        }
#endif // _WIN32
#ifdef HAVE_TIMER_RDTSC
        case TIMER_RDTSC: return tsc_freq;
#endif
    }
}

static int osdcmd_sys_timer(osdcmdptr_t parm)
{
    static char constexpr const *s[] = { "auto", "QPC", "SDL", "RDTSC" };
    int const r = osdcmd_cvar_set(parm);

    if (r != OSDCMD_OK)
        goto print_and_return;

#ifndef _WIN32
    if (sys_timer == TIMER_QPC)
        sys_timer = TIMER_AUTO;
#endif
#ifndef HAVE_TIMER_SDL
    if (sys_timer == TIMER_SDL)
        sys_timer = TIMER_AUTO;
#endif
#ifndef HAVE_TIMER_RDTSC
    if (sys_timer == TIMER_RDTSC)
        sys_timer = TIMER_AUTO;
#endif
    if ((unsigned)sys_timer >= NUMTIMERS)
        sys_timer = TIMER_AUTO;

    if (sys_timer != TIMER_AUTO || !OSD_ParsingScript())
print_and_return:
        LOG_F(INFO, "Using '%s' timer with %g MHz frequency", s[sys_timer], timerGetPerformanceFrequency() / 1.0e6);

#if defined EDUKE32_CPU_X86 && defined HAVE_TIMER_RDTSC
    if (sys_timer == TIMER_RDTSC && !cpu.features.invariant_tsc)
        LOG_F(WARNING, "Invariant TSC support not detected! You may experience timing issues.");
#endif

    clockLastSampleTime = timerGetNanoTicks();

    return r;
}

int timerInit(int const tickspersecond)
{
    static int initDone;

    if (initDone == 0)
    {
        static osdcvardata_t sys_timer_cvar = { "sys_timer",
                                                "engine timing backend:\n"
                                                "   0: auto\n"
#ifdef _WIN32
                                                "   1: WinAPI QueryPerformanceCounter\n"
#endif
#ifdef HAVE_TIMER_SDL
                                                "   2: SDL_GetPerformanceCounter\n"
#endif
#ifdef HAVE_TIMER_RDTSC
                                                "   3: CPU RDTSC instruction\n"
#endif
                                                , (void *)&sys_timer, CVAR_INT | CVAR_FUNCPTR, 0, 4 };

        OSD_RegisterCvar(&sys_timer_cvar, osdcmd_sys_timer);

#ifdef HAVE_TIMER_SDL
        SDL_InitSubSystem(SDL_INIT_TIMER);
#endif
#ifdef HAVE_TIMER_RDTSC
        if (tsc_freq == 0)
        {
            auto const calibrationEndTime = timerGetFractionalTicks() + 100.0;
            auto const samplePeriodBegin  = timerSampleRDTSC();
            do { } while (timerGetFractionalTicks() < calibrationEndTime);
            auto const samplePeriodEnd = timerSampleRDTSC();
            auto const timePerSample   = timerSampleRDTSC() - samplePeriodEnd;

            tsc_freq = (samplePeriodEnd - samplePeriodBegin - timePerSample) * 10;
        }
#endif
        initDone = 1;
    }

    clockTicksPerSecond = tickspersecond;
    clockLastSampleTime = timerGetNanoTicks();

    usertimercallback = nullptr;

    return 0;
}

void(*timerSetCallback(void(*callback)(void)))(void)
{
    void(*oldtimercallback)(void);

    oldtimercallback = usertimercallback;
    usertimercallback = callback;

    return oldtimercallback;
}