diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt
index c4109c07d..11f382b2f 100644
--- a/source/CMakeLists.txt
+++ b/source/CMakeLists.txt
@@ -1061,6 +1061,7 @@ set (PCH_SOURCES
 	core/secrets.cpp
 	core/compositesavegame.cpp
 	core/savegamehelp.cpp
+	core/precache.cpp
 	core/quotes.cpp
 	core/screenshot.cpp
 	core/raze_music.cpp
diff --git a/source/build/include/build.h b/source/build/include/build.h
index 5accd465f..027be46bb 100644
--- a/source/build/include/build.h
+++ b/source/build/include/build.h
@@ -664,7 +664,6 @@ typedef struct s_equation
 void    renderSetRollAngle(float rolla);
 #endif
 
-void PrecacheHardwareTextures(int nTile);
 void Polymost_Startup();
 
 typedef uint16_t polytintflags_t;
@@ -842,9 +841,6 @@ extern int32_t rintersect(int32_t x1, int32_t y1, int32_t z1,
     int32_t x3, int32_t y3, int32_t x4, int32_t y4,
     int32_t *intx, int32_t *inty, int32_t *intz);
 
-void markTileForPrecache(int tilenum, int palnum);
-void precacheMarkedTiles();
-
 extern int32_t(*animateoffs_replace)(int const tilenum, int fakevar);
 extern int32_t(*getpalookup_replace)(int32_t davis, int32_t dashade);
 extern void(*initspritelists_replace)(void);
diff --git a/source/build/src/polymost.cpp b/source/build/src/polymost.cpp
index 496edb870..a4b04a143 100644
--- a/source/build/src/polymost.cpp
+++ b/source/build/src/polymost.cpp
@@ -99,7 +99,6 @@ static int32_t r_parallaxskyclamping = 1;
 #define Bfabsf fabsf
 
 static int32_t drawingskybox = 0;
-static int32_t hicprecaching = 0;
 
 static hitdata_t polymost_hitdata;
 
@@ -3543,45 +3542,6 @@ _drawsprite_return:
 
 static_assert((int)RS_YFLIP == (int)HUDFLAG_FLIPPED);
 
-void polymost_precache(int32_t dapicnum, int32_t dapalnum, int32_t datype)
-{
-    // dapicnum and dapalnum are like you'd expect
-    // datype is 0 for a wall/floor/ceiling and 1 for a sprite
-    //    basically this just means walls are repeating
-    //    while sprites are clamped
-
-   if ((dapalnum < (MAXPALOOKUPS - RESERVEDPALS)) && (!lookups.checkTable(dapalnum))) return;//dapalnum = 0;
-
-    //Printf("precached %d %d type %d\n", dapicnum, dapalnum, datype);
-    hicprecaching = 1;
-    int palid = TRANSLATION(Translation_Remap + curbasepal, dapalnum);
-    auto tex = tileGetTexture(dapicnum);
-    if (tex->isValid())
-        GLInterface.SetTexture(tex, palid, CLAMP_NONE);
-    hicprecaching = 0;
-
-    if (datype == 0 || !hw_models) return;
-
-    int const mid = md_tilehasmodel(dapicnum, dapalnum);
-
-    if (mid < 0 || models[mid]->mdnum < 2) return;
-
-    int const surfaces = (models[mid]->mdnum == 3) ? ((md3model_t *)models[mid])->head.numsurfs : 0;
-
-    for (int i = 0; i <= surfaces; i++)
-	{
-        auto tex = mdloadskin((md2model_t *)models[mid], 0, dapalnum, i, nullptr);
-        int palid = TRANSLATION(Translation_Remap + curbasepal, dapalnum);
-        if (tex) GLInterface.SetTexture(tex, palid, CLAMP_NONE);
-	}
-}
-
-void PrecacheHardwareTextures(int nTile)
-{
-	// PRECACHE
-	// This really *really* needs improvement on the game side - the entire precaching logic has no clue about the different needs of a hardware renderer.
-	polymost_precache(nTile, 0, 1);
-}
 
 extern char* voxfilenames[MAXVOXELS];
 void (*PolymostProcessVoxels_Callback)(void) = NULL;
diff --git a/source/core/precache.cpp b/source/core/precache.cpp
new file mode 100644
index 000000000..3341630e5
--- /dev/null
+++ b/source/core/precache.cpp
@@ -0,0 +1,133 @@
+/*
+** precache.cpp
+**
+** 
+**
+**---------------------------------------------------------------------------
+** Copyright 2019-2021 Christoph Oelckers
+** All rights reserved.
+**
+** Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions
+** are met:
+**
+** 1. Redistributions of source code must retain the above copyright
+**    notice, this list of conditions and the following disclaimer.
+** 2. Redistributions in binary form must reproduce the above copyright
+**    notice, this list of conditions and the following disclaimer in the
+**    documentation and/or other materials provided with the distribution.
+** 3. The name of the author may not be used to endorse or promote products
+**    derived from this software without specific prior written permission.
+**
+** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+**---------------------------------------------------------------------------
+**
+*/
+
+#include "build.h"
+#include "palette.h"
+#include "v_video.h"
+#include "hw_material.h"
+#include "glbackend/gl_models.h"
+
+static void PrecacheTex(FGameTexture* tex, int palid)
+{
+	if (!tex || !tex->isValid()) return;
+	int scaleflags = 0;
+	if (shouldUpscale(tex, UF_Texture)) scaleflags |= CTF_Upscale;
+
+	auto mat = FMaterial::ValidateTexture(tex, scaleflags);
+	screen->PrecacheMaterial(mat, palid);
+}
+
+static void doprecache(int32_t dapicnum, int32_t dapalnum, int32_t datype)
+{
+    // dapicnum and dapalnum are like you'd expect
+    // datype is 0 for a wall/floor/ceiling and 1 for a sprite
+    //    basically this just means walls are repeating
+    //    while sprites are clamped
+
+   if ((dapalnum < (MAXPALOOKUPS - RESERVEDPALS)) && (!lookups.checkTable(dapalnum))) return;//dapalnum = 0;
+
+    //Printf("precached %d %d type %d\n", dapicnum, dapalnum, datype);
+    int palid = TRANSLATION(Translation_Remap + curbasepal, dapalnum);
+    auto tex = tileGetTexture(dapicnum);
+    PrecacheTex(tex, palid);
+
+    if (datype == 0 || !hw_models) return;
+
+    int const mid = md_tilehasmodel(dapicnum, dapalnum);
+
+	if (mid < 0 || models[mid]->mdnum < 2)
+	{
+		int vox = tiletovox[dapicnum];
+		if (vox != -1 && voxmodels[vox] && voxmodels[vox]->model)
+		{
+			FHWModelRenderer mr(*screen->RenderState(), 0);
+			voxmodels[vox]->model->BuildVertexBuffer(&mr);
+		}
+		return;
+	}
+
+    int const surfaces = (models[mid]->mdnum == 3) ? ((md3model_t *)models[mid])->head.numsurfs : 0;
+
+    for (int i = 0; i <= surfaces; i++)
+	{
+        auto tex = mdloadskin((md2model_t *)models[mid], 0, dapalnum, i, nullptr);
+        int palid = TRANSLATION(Translation_Remap + curbasepal, dapalnum);
+        if (tex) PrecacheTex(tex, palid);
+	}
+}
+
+void PrecacheHardwareTextures(int nTile)
+{
+	// PRECACHE
+	// This really *really* needs improvement on the game side - the entire precaching logic has no clue about the different needs of a hardware renderer.
+	doprecache(nTile, 0, 1);
+}
+
+
+TMap<int64_t, bool> cachemap;
+
+void markTileForPrecache(int tilenum, int palnum)
+{
+	int i, j;
+	if ((picanm[tilenum].sf & PICANM_ANIMTYPE_MASK) == PICANM_ANIMTYPE_BACK)
+	{
+		i = tilenum - picanm[tilenum].num;
+		j = tilenum;
+	}
+	else
+	{
+		i = tilenum;
+		j = tilenum + picanm[tilenum].num;
+	}
+
+	for (; i <= j; i++)
+	{
+		int64_t val = i + (int64_t(palnum) << 32);
+		cachemap.Insert(val, true);
+	}
+}
+
+void precacheMarkedTiles()
+{
+	decltype(cachemap)::Iterator it(cachemap);
+	decltype(cachemap)::Pair* pair;
+	while (it.NextPair(pair))
+	{
+		int dapicnum = pair->Key & 0x7fffffff;
+		int dapalnum = pair->Key >> 32;
+		doprecache(dapicnum, dapalnum, 0);
+	}
+}
+
diff --git a/source/core/precache.h b/source/core/precache.h
new file mode 100644
index 000000000..30c6cea27
--- /dev/null
+++ b/source/core/precache.h
@@ -0,0 +1,5 @@
+#pragma once
+
+void PrecacheHardwareTextures(int nTile);
+void markTileForPrecache(int tilenum, int palnum);
+void precacheMarkedTiles();
diff --git a/source/games/blood/src/preload.cpp b/source/games/blood/src/preload.cpp
index 5c998974e..4c0e0661d 100644
--- a/source/games/blood/src/preload.cpp
+++ b/source/games/blood/src/preload.cpp
@@ -26,6 +26,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 #include "blood.h"
 #include "view.h"
 #include "g_input.h"
+#include "precache.h"
 
 BEGIN_BLD_NS
 
diff --git a/source/games/blood/src/view.cpp b/source/games/blood/src/view.cpp
index c85cd6c2f..16e6e8c24 100644
--- a/source/games/blood/src/view.cpp
+++ b/source/games/blood/src/view.cpp
@@ -42,6 +42,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 #include "automap.h"
 #include "gamefuncs.h"
 #include "v_draw.h"
+#include "precache.h"
 
 BEGIN_BLD_NS
 
diff --git a/source/games/duke/src/premap_d.cpp b/source/games/duke/src/premap_d.cpp
index e07263069..a3a67e6ea 100644
--- a/source/games/duke/src/premap_d.cpp
+++ b/source/games/duke/src/premap_d.cpp
@@ -37,6 +37,7 @@ source as it is released.
 #include "build.h"
 #include "names_d.h"
 #include "dukeactor.h"
+#include "precache.h"
 
 BEGIN_DUKE_NS 
 
diff --git a/source/games/duke/src/premap_r.cpp b/source/games/duke/src/premap_r.cpp
index 196d6d052..5ff71c275 100644
--- a/source/games/duke/src/premap_r.cpp
+++ b/source/games/duke/src/premap_r.cpp
@@ -31,6 +31,7 @@ Prepared for public release: 03/21/2003 - Charlie Wiederhold, 3D Realms
 #include "names_r.h"
 #include "mapinfo.h"
 #include "dukeactor.h"
+#include "precache.h"
 
 BEGIN_DUKE_NS 
 
diff --git a/source/games/exhumed/src/enginesubs.cpp b/source/games/exhumed/src/enginesubs.cpp
index e896bd2be..6f64dcefb 100644
--- a/source/games/exhumed/src/enginesubs.cpp
+++ b/source/games/exhumed/src/enginesubs.cpp
@@ -17,6 +17,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 //-------------------------------------------------------------------------
 #include "ns.h"
 #include "engine.h"
+#include "precache.h"
 
 //#include <io.h>
 //#include <fcntl.h>
diff --git a/source/games/sw/src/cache.cpp b/source/games/sw/src/cache.cpp
index 654025157..10cec872d 100644
--- a/source/games/sw/src/cache.cpp
+++ b/source/games/sw/src/cache.cpp
@@ -40,6 +40,7 @@ not load" error messages.
 #include "misc.h"
 #include "sounds.h"
 #include "network.h"
+#include "precache.h"
 
 BEGIN_SW_NS
 
diff --git a/source/glbackend/glbackend.cpp b/source/glbackend/glbackend.cpp
index a8be0093c..1e2ee04ac 100644
--- a/source/glbackend/glbackend.cpp
+++ b/source/glbackend/glbackend.cpp
@@ -439,41 +439,3 @@ void videoShowFrame(int32_t w)
 		twod->Clear();
 	}
 }
-
-TMap<int64_t, bool> cachemap;
-
-void markTileForPrecache(int tilenum, int palnum)
-{
-	int i, j;
-	if ((picanm[tilenum].sf & PICANM_ANIMTYPE_MASK) == PICANM_ANIMTYPE_BACK)
-	{
-		i = tilenum - picanm[tilenum].num;
-		j = tilenum;
-	}
-	else
-	{
-		i = tilenum;
-		j = tilenum + picanm[tilenum].num;
-	}
-
-	for (; i <= j; i++)
-	{
-		int64_t val = i + (int64_t(palnum) << 32);
-		cachemap.Insert(val, true);
-	}
-}
-
-void polymost_precache(int32_t dapicnum, int32_t dapalnum, int32_t datype);
-
-void precacheMarkedTiles()
-{
-	decltype(cachemap)::Iterator it(cachemap);
-	decltype(cachemap)::Pair* pair;
-	while (it.NextPair(pair))
-	{
-		int dapicnum = pair->Key & 0x7fffffff;
-		int dapalnum = pair->Key >> 32;
-		polymost_precache(dapicnum, dapalnum, 0);
-	}
-}
-