From edb11dc2230f1fdfcb571eccb19be0732f13585d Mon Sep 17 00:00:00 2001
From: Ian Curtis <i.curtis@gmail.com>
Date: Thu, 13 Jun 2024 13:36:30 +0100
Subject: [PATCH] Performance improvements The old texture code was being
 bottle necked by the texture reads. We mirrored the real3d texture memory
 directly, including the mipmaps in a single large texture. I *think* most h/w
 has some sort of texture cache for a 2x2 or 4x4 block of pixels for a
 texture. What we were doing was reading the base texture, then reading the
 mipmap data from a totally separate part of the same texture which I can only
 assume flushed this cache. What I did was to create mipmap chains for the
 texture sheet, then copy the mipmap data there. Doing this basically doubles
 performance.

---
 Makefiles/Rules.inc                     |  1 +
 Src/Graphics/New3D/Model.h              | 10 +++-
 Src/Graphics/New3D/New3D.cpp            | 57 ++++++++++++--------
 Src/Graphics/New3D/New3D.h              |  4 +-
 Src/Graphics/New3D/PolyHeader.cpp       | 14 ++---
 Src/Graphics/New3D/R3DShader.cpp        | 35 +++++-------
 Src/Graphics/New3D/R3DShader.h          | 10 ++--
 Src/Graphics/New3D/R3DShaderCommon.h    | 62 +++++----------------
 Src/Graphics/New3D/R3DShaderQuads.h     |  3 +-
 Src/Graphics/New3D/R3DShaderTriangles.h |  3 +-
 Src/Graphics/New3D/TextureBank.cpp      | 71 +++++++++++++++++++++++++
 Src/Graphics/New3D/TextureBank.h        | 33 ++++++++++++
 VS2008/Supermodel.vcxproj               |  2 +
 VS2008/Supermodel.vcxproj.filters       |  6 +++
 14 files changed, 198 insertions(+), 113 deletions(-)
 create mode 100644 Src/Graphics/New3D/TextureBank.cpp
 create mode 100644 Src/Graphics/New3D/TextureBank.h

diff --git a/Makefiles/Rules.inc b/Makefiles/Rules.inc
index 0a815c1..377d814 100644
--- a/Makefiles/Rules.inc
+++ b/Makefiles/Rules.inc
@@ -114,6 +114,7 @@ SRC_FILES = \
 	Src/Graphics/New3D/R3DShader.cpp \
 	Src/Graphics/New3D/R3DFloat.cpp \
 	Src/Graphics/New3D/R3DScrollFog.cpp \
+	Src/Graphics/New3D/TextureBank.cpp \
 	Src/Graphics/FBO.cpp \
 	Src/Graphics/Render2D.cpp \
 	Src/Graphics/SuperAA.cpp \
diff --git a/Src/Graphics/New3D/Model.h b/Src/Graphics/New3D/Model.h
index 12ee33b..6707733 100644
--- a/Src/Graphics/New3D/Model.h
+++ b/Src/Graphics/New3D/Model.h
@@ -128,10 +128,16 @@ struct Mesh
 	enum TexWrapMode : int { repeat = 0, repeatClamp, mirror, mirrorClamp };
 
 	// texture
-	int format, x, y, width, height = 0;
+	int		format		= 0;
+	int		x			= 0;
+	int		y			= 0;
+	int		width		= 0;
+	int		height		= 0;
+	int		page		= 0;
+	bool	inverted	= false;
+
 	TexWrapMode wrapModeU;
 	TexWrapMode wrapModeV;
-	bool inverted = false;
 
 	// microtexture
 	bool	microTexture		= false;
diff --git a/Src/Graphics/New3D/New3D.cpp b/Src/Graphics/New3D/New3D.cpp
index 7319525..6abe615 100644
--- a/Src/Graphics/New3D/New3D.cpp
+++ b/Src/Graphics/New3D/New3D.cpp
@@ -21,7 +21,6 @@ CNew3D::CNew3D(const Util::Config::Node &config, const std::string& gameName) :
 	m_r3dShader(config),
 	m_r3dScrollFog(config),
 	m_gameName(gameName),
-	m_textureBuffer(0),
 	m_vao(0),
 	m_aaTarget(0)
 {
@@ -43,16 +42,6 @@ CNew3D::CNew3D(const Util::Config::Node &config, const std::string& gameName) :
 	m_r3dShader.LoadShader();
 	glUseProgram(0);
 
-	// setup our texture memory
-
-	glGenTextures(1, &m_textureBuffer);
-	glBindTexture(GL_TEXTURE_2D, m_textureBuffer);
-	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
-	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
-	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
-	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
-	glTexImage2D(GL_TEXTURE_2D, 0, GL_R16UI, 2048, 2048, 0, GL_RED_INTEGER, GL_UNSIGNED_SHORT, nullptr);	// allocate storage
-
 	// setup up our vertex buffer memory
 
 	glGenVertexArrays(1, &m_vao);
@@ -87,11 +76,6 @@ CNew3D::~CNew3D()
 		m_vao = 0;
 	}
 
-	if (m_textureBuffer) {
-		glDeleteTextures(1, &m_textureBuffer);
-		m_textureBuffer = 0;
-	}
-
 	m_r3dShader.UnloadShader();
 }
 
@@ -102,6 +86,9 @@ void CNew3D::AttachMemory(const UINT32 *cullingRAMLoPtr, const UINT32 *cullingRA
 	m_polyRAM		= polyRAMPtr;
 	m_vrom			= vromPtr;
 	m_textureRAM	= textureRAMPtr;
+
+	m_textureBank[0].AttachMemory(textureRAMPtr);
+	m_textureBank[1].AttachMemory(textureRAMPtr + (2048*1024));
 }
 
 void CNew3D::SetStepping(int stepping)
@@ -143,12 +130,28 @@ bool CNew3D::Init(unsigned xOffset, unsigned yOffset, unsigned xRes, unsigned yR
 
 void CNew3D::UploadTextures(unsigned level, unsigned x, unsigned y, unsigned width, unsigned height)
 {
-	glBindTexture(GL_TEXTURE_2D, m_textureBuffer);
-	glPixelStorei(GL_UNPACK_ALIGNMENT, 2);
+	// handle case of entire sheet invalidation
+	if (width == 2048 && height == 2048) {
 
-	for (unsigned i = 0; i < height; i++) {
-		glTexSubImage2D(GL_TEXTURE_2D, 0, x, y + i, width, 1, GL_RED_INTEGER, GL_UNSIGNED_SHORT, m_textureRAM + ((y + i) * 2048) + x);
+		height = 1024;
+
+		const int mipXBase[] = { 0, 1024, 1536, 1792, 1920, 1984, 2016, 2032, 2040, 2044, 2046, 2047 };
+		const int mipYBase[] = { 0, 512, 768, 896, 960, 992, 1008, 1016, 1020, 1022, 1023 };
+
+		for (int i = 0; i < m_textureBank[0].GetNumberOfLevels(); i++) {
+			m_textureBank[0].UploadTextures(i, mipXBase[i], mipYBase[i], width, height);
+			m_textureBank[1].UploadTextures(i, mipXBase[i], mipYBase[i], width, height);
+			width = (width > 1) ? width / 2 : 1;
+			height = (height > 1) ? height / 2 : 1;
+		}
+
+		return;
 	}
+
+	int page;
+	TranslateTexture(x, y, width, height, page);
+
+	m_textureBank[page].UploadTextures(level, x, y, width, height);
 }
 
 void CNew3D::DrawScrollFog()
@@ -280,7 +283,10 @@ void CNew3D::DrawAmbientFog()
 bool CNew3D::RenderScene(int priority, bool renderOverlay, Layer layer)
 {
 	glActiveTexture(GL_TEXTURE0);
-	glBindTexture(GL_TEXTURE_2D, m_textureBuffer);
+	m_textureBank[0].Bind();
+	glActiveTexture(GL_TEXTURE1);
+	m_textureBank[1].Bind();
+	glActiveTexture(GL_TEXTURE0);
 
 	bool hasOverlay = false;		// (high priority polys)
 
@@ -1249,6 +1255,7 @@ void CNew3D::SetMeshValues(SortingMesh *currentMesh, PolyHeader &ph)
 		currentMesh->height			= ph.TexHeight();
 		currentMesh->microTexture	= ph.MicroTexture();
 		currentMesh->inverted		= ph.TranslatorMapOffset() == 2;
+		currentMesh->page			= ph.Page();
 
 		{
 			bool smoothU = ph.TexSmoothU();
@@ -1592,6 +1599,14 @@ void CNew3D::CalcViewport(Viewport* vp)
 	}
 }
 
+void CNew3D::TranslateTexture(unsigned& x, unsigned& y, int width, int height, int& page)
+{
+	page = y / 1024;
+
+	// remove page from y coordinate
+	y -= (page * 1024);
+}
+
 void CNew3D::SetSunClamp(bool enable)
 {
 	m_sunClamp = enable;
diff --git a/Src/Graphics/New3D/New3D.h b/Src/Graphics/New3D/New3D.h
index 1b63273..90f4065 100644
--- a/Src/Graphics/New3D/New3D.h
+++ b/Src/Graphics/New3D/New3D.h
@@ -43,6 +43,7 @@
 #include "PolyHeader.h"
 #include "R3DFrameBuffers.h"
 #include <mutex>
+#include "TextureBank.h"
 
 namespace New3D {
 
@@ -224,6 +225,7 @@ private:
 	void TranslateLosPosition(int inX, int inY, int& outX, int& outY);
 	bool ProcessLos(int priority);
 	void CalcViewport(Viewport* vp);
+	void TranslateTexture(unsigned& x, unsigned& y, int width, int height, int& page);
 
 	/*
 	* Data
@@ -261,7 +263,6 @@ private:
 	UINT32 m_colorTableAddr = 0x400;		// address of color table in polygon RAM
 	LODBlendTable* m_LODBlendTable;
 
-	GLuint			m_textureBuffer;
 	NodeAttributes	m_nodeAttribs;
 	Mat4			m_modelMat;				// current modelview matrix
 
@@ -281,6 +282,7 @@ private:
 	std::vector<FVertex> m_polyBufferRam;		// dynamic polys
 	std::vector<FVertex> m_polyBufferRom;		// rom polys
 	std::unordered_map<UINT32, std::shared_ptr<std::vector<Mesh>>> m_romMap;	// a hash table for all the ROM models. The meshes don't have model matrices or tex offsets yet
+	TextureBank			m_textureBank[2];
 
 	GLuint m_vao;
 	VBO m_vbo;								// large VBO to hold our poly data, start of VBO is ROM data, ram polys follow
diff --git a/Src/Graphics/New3D/PolyHeader.cpp b/Src/Graphics/New3D/PolyHeader.cpp
index 2287932..543e9c1 100644
--- a/Src/Graphics/New3D/PolyHeader.cpp
+++ b/Src/Graphics/New3D/PolyHeader.cpp
@@ -296,19 +296,11 @@ int PolyHeader::X()
 
 int PolyHeader::Y()
 {
-	//=======
+	//====
 	int y;
-	int page;
-	//=======
+	//====
 
-	if (Page()) {
-		page = 1024;
-	}
-	else {
-		page = 0;
-	}
-
-	y = (32 * (header[5] & 0x1F) + page);	// if we hit 2nd page add 1024 to y coordinate
+	y = 32 * (header[5] & 0x1F);	// if we hit 2nd page add 1024 to y coordinate
 	y &= 2047;
 
 	return y;
diff --git a/Src/Graphics/New3D/R3DShader.cpp b/Src/Graphics/New3D/R3DShader.cpp
index 2c4c9e0..fe7c302 100644
--- a/Src/Graphics/New3D/R3DShader.cpp
+++ b/Src/Graphics/New3D/R3DShader.cpp
@@ -38,6 +38,7 @@ void R3DShader::Start()
 	m_specularValue		= 0;
 	m_microTexScale		= 0;
 	m_microTexID		= -1;
+	m_texturePage		= -1;
 
 	m_baseTexInfo[0]	= -1;
 	m_baseTexInfo[1]	= -1;
@@ -100,7 +101,9 @@ bool R3DShader::LoadShader(const char* vertexShader, const char* fragmentShader)
 
 	PrintProgramResult(m_shaderProgram);
 
-	m_locTexture1			= glGetUniformLocation(m_shaderProgram, "tex1");
+	m_locTextureBank[0]		= glGetUniformLocation(m_shaderProgram, "textureBank[0]");
+	m_locTextureBank[1]		= glGetUniformLocation(m_shaderProgram, "textureBank[1]");
+	m_locTexturePage		= glGetUniformLocation(m_shaderProgram, "texturePage");
 	m_locTexture1Enabled	= glGetUniformLocation(m_shaderProgram, "textureEnabled");
 	m_locTexture2Enabled	= glGetUniformLocation(m_shaderProgram, "microTexture");
 	m_locTextureAlpha		= glGetUniformLocation(m_shaderProgram, "textureAlpha");
@@ -202,7 +205,8 @@ void R3DShader::SetMeshUniforms(const Mesh* m)
 	}
 
 	if (m_dirtyMesh) {
-		glUniform1i(m_locTexture1, 0);
+		glUniform1i(m_locTextureBank[0], 0);
+		glUniform1i(m_locTextureBank[1], 1);
 	}
 
 	if (m_dirtyMesh || m->textured != m_textured1) {
@@ -215,6 +219,11 @@ void R3DShader::SetMeshUniforms(const Mesh* m)
 		m_textured2 = m->microTexture;
 	}
 
+	if (m_dirtyMesh || (m->page ^ m_transPage) != m_texturePage) {
+		glUniform1i(m_locTexturePage, m->page ^ m_transPage);
+		m_texturePage = (m->page ^ m_transPage);
+	}
+
 	if (m_dirtyMesh || m->microTextureScale != m_microTexScale) {
 		glUniform1f(m_locMicroTexScale, m->microTextureScale);
 		m_microTexScale = m->microTextureScale;
@@ -232,10 +241,7 @@ void R3DShader::SetMeshUniforms(const Mesh* m)
 		m_baseTexInfo[2] = m->width;
 		m_baseTexInfo[3] = m->height;
 
-		int translatedX, translatedY;
-		CalcTexOffset(m_transX, m_transY, m_transPage, m->x, m->y, translatedX, translatedY);	// need to apply model translation
-
-		glUniform4i(m_locBaseTexInfo, translatedX, translatedY, m->width, m->height);
+		glUniform4i(m_locBaseTexInfo, (m->x + m_transX), (m->y + m_transY), m->width, m->height);
 	}
 
 	if (m_dirtyMesh || m_baseTexType != m->format) {
@@ -426,21 +432,4 @@ void R3DShader::PrintProgramResult(GLuint program)
 	}
 }
 
-void R3DShader::CalcTexOffset(int offX, int offY, int page, int x, int y, int& newX, int& newY)
-{
-	newX = (x + offX) & 2047;	// wrap around 2048, shouldn't be required
-
-	int oldPage = y / 1024;
-
-	y -= (oldPage * 1024);	// remove page from tex y
-
-	// calc newY with wrap around, wraps around in the same sheet, not into another memory sheet
-
-	newY = (y + offY) & 1023;
-
-	// add page to Y
-
-	newY += ((oldPage + page) & 1) * 1024;		// max page 0-1
-}
-
 } // New3D
diff --git a/Src/Graphics/New3D/R3DShader.h b/Src/Graphics/New3D/R3DShader.h
index 84ca182..ac849fb 100644
--- a/Src/Graphics/New3D/R3DShader.h
+++ b/Src/Graphics/New3D/R3DShader.h
@@ -30,8 +30,6 @@ private:
 	void PrintShaderResult(GLuint shader);
 	void PrintProgramResult(GLuint program);
 
-	void CalcTexOffset(int offX, int offY, int page, int x, int y, int& newX, int& newY);
-
 	// run-time config
 	const Util::Config::Node &m_config;
 
@@ -42,9 +40,10 @@ private:
 	GLuint m_fragmentShader;
 
 	// mesh uniform locations
-	GLint m_locTexture1;
-	GLint m_locTexture1Enabled;
-	GLint m_locTexture2Enabled;
+	GLint m_locTextureBank[2];		// 2 banks
+	GLint m_locTexture1Enabled;		// base texture
+	GLint m_locTexture2Enabled;		// micro texture
+	GLint m_locTexturePage;
 	GLint m_locTextureAlpha;
 	GLint m_locAlphaTest;
 	GLint m_locMicroTexScale;
@@ -70,6 +69,7 @@ private:
 	bool	m_fixedShading;
 	bool	m_translatorMap;
 	bool	m_polyAlpha;
+	int		m_texturePage;
 
 	bool	m_layered;
 	bool	m_noLosReturn;
diff --git a/Src/Graphics/New3D/R3DShaderCommon.h b/Src/Graphics/New3D/R3DShaderCommon.h
index df12c1b..929994e 100644
--- a/Src/Graphics/New3D/R3DShaderCommon.h
+++ b/Src/Graphics/New3D/R3DShaderCommon.h
@@ -82,36 +82,13 @@ vec4 ExtractColour(int type, uint value)
 	return c;
 }
 
-int GetPage(int yCoord)
-{
-	return yCoord / 1024;
-}
-
-int GetNextPage(int yCoord)
-{
-	return (GetPage(yCoord) + 1) & 1;
-}
-
-int GetNextPageOffset(int yCoord)
-{
-	return GetNextPage(yCoord) * 1024;
-}
-
-// wrapping tex coords would be super easy but we combined tex sheets so have to handle wrap around between sheets
 // hardware testing would be useful because i don't know exactly what happens if you try to read outside the texture sheet
 // wrap around is a good guess
-ivec2 WrapTexCoords(ivec2 pos, ivec2 coordinate)
+ivec2 WrapTexCoords(ivec2 pos, ivec2 coordinate, int level)
 {
 	ivec2 newCoord;
-
-	newCoord.x = coordinate.x & 2047;
-	newCoord.y = coordinate.y;
-
-	int page = GetPage(pos.y);
-
-	newCoord.y -= (page * 1024);	// remove page
-	newCoord.y &= 1023;				// wrap around in the same sheet
-	newCoord.y += (page * 1024);	// add page back
+	newCoord.x = coordinate.x & (2047 >> level);
+	newCoord.y = coordinate.y & (1023 >> level);
 
 	return newCoord;
 }
@@ -125,19 +102,11 @@ ivec2 GetTextureSize(int level, ivec2 size)
 
 ivec2 GetTexturePosition(int level, ivec2 pos)
 {
-	const int mipXBase[] = int[](0, 1024, 1536, 1792, 1920, 1984, 2016, 2032, 2040, 2044, 2046, 2047);
-	const int mipYBase[] = int[](0, 512, 768, 896, 960, 992, 1008, 1016, 1020, 1022, 1023);
-
 	int mipDivisor = 1 << level;
 
-	int page = pos.y / 1024;
-	pos.y -= (page * 1024);		// remove page from tex y
-
 	ivec2 retPos;
-	retPos.x = mipXBase[level] + (pos.x / mipDivisor);
-	retPos.y = mipYBase[level] + (pos.y / mipDivisor);
-
-	retPos.y += (page * 1024);	// add page back to tex y
+	retPos.x = pos.x / mipDivisor;
+	retPos.y = pos.y / mipDivisor;
 
 	return retPos;
 }
@@ -206,16 +175,16 @@ float LinearTexLocations(int wrapMode, float size, float u, out float u0, out fl
 	}
 }
 
-vec4 texBiLinear(usampler2D texSampler, ivec2 wrapMode, vec2 texSize, ivec2 texPos, vec2 texCoord)
+vec4 texBiLinear(usampler2D texSampler, ivec2 wrapMode, vec2 texSize, ivec2 texPos, vec2 texCoord, int level)
 {
 	float tx[2], ty[2];
 	float a = LinearTexLocations(wrapMode.s, texSize.x, texCoord.x, tx[0], tx[1]);
 	float b = LinearTexLocations(wrapMode.t, texSize.y, texCoord.y, ty[0], ty[1]);
 
-	vec4 p0q0 = ExtractColour(baseTexType,texelFetch(texSampler, WrapTexCoords(texPos,ivec2(vec2(tx[0],ty[0]) * texSize + texPos)), 0).r);
-    vec4 p1q0 = ExtractColour(baseTexType,texelFetch(texSampler, WrapTexCoords(texPos,ivec2(vec2(tx[1],ty[0]) * texSize + texPos)), 0).r);
-    vec4 p0q1 = ExtractColour(baseTexType,texelFetch(texSampler, WrapTexCoords(texPos,ivec2(vec2(tx[0],ty[1]) * texSize + texPos)), 0).r);
-    vec4 p1q1 = ExtractColour(baseTexType,texelFetch(texSampler, WrapTexCoords(texPos,ivec2(vec2(tx[1],ty[1]) * texSize + texPos)), 0).r);
+	vec4 p0q0 = ExtractColour(baseTexType,texelFetch(texSampler, WrapTexCoords(texPos,ivec2(vec2(tx[0],ty[0]) * texSize + texPos),level), level).r);
+    vec4 p1q0 = ExtractColour(baseTexType,texelFetch(texSampler, WrapTexCoords(texPos,ivec2(vec2(tx[1],ty[0]) * texSize + texPos),level), level).r);
+    vec4 p0q1 = ExtractColour(baseTexType,texelFetch(texSampler, WrapTexCoords(texPos,ivec2(vec2(tx[0],ty[1]) * texSize + texPos),level), level).r);
+    vec4 p1q1 = ExtractColour(baseTexType,texelFetch(texSampler, WrapTexCoords(texPos,ivec2(vec2(tx[1],ty[1]) * texSize + texPos),level), level).r);
 
 	if(alphaTest) {
 		if(p0q0.a > p1q0.a)		{ p1q0.rgb = p0q0.rgb; }
@@ -254,15 +223,15 @@ vec4 textureR3D(usampler2D texSampler, ivec2 wrapMode, ivec2 texSize, ivec2 texP
 	ivec2 texSize0 = GetTextureSize(iLevel, texSize);
 	ivec2 texSize1 = GetTextureSize(iLevel+1, texSize); 
 
-	vec4 texLevel0 = texBiLinear(texSampler, wrapMode, vec2(texSize0), texPos0, texCoord);
-	vec4 texLevel1 = texBiLinear(texSampler, wrapMode, vec2(texSize1), texPos1, texCoord);
+	vec4 texLevel0 = texBiLinear(texSampler, wrapMode, vec2(texSize0), texPos0, texCoord, iLevel);
+	vec4 texLevel1 = texBiLinear(texSampler, wrapMode, vec2(texSize1), texPos1, texCoord, iLevel+1);
 
 	return mix(texLevel0, texLevel1, fract(fLevel));	// linear blend between our mipmap levels
 }
 
 vec4 GetTextureValue()
 {
-	vec4 tex1Data = textureR3D(tex1, textureWrapMode, ivec2(baseTexInfo.zw), ivec2(baseTexInfo.xy), fsTexCoord);
+	vec4 tex1Data = textureR3D(textureBank[texturePage], textureWrapMode, ivec2(baseTexInfo.zw), ivec2(baseTexInfo.xy), fsTexCoord);
 
 	if(textureInverted) {
 		tex1Data.rgb = vec3(1.0) - vec3(tex1Data.rgb);
@@ -271,11 +240,8 @@ vec4 GetTextureValue()
 	if (microTexture) {
 		vec2 scale			= (vec2(baseTexInfo.zw) / 128.0) * microTextureScale;
 		ivec2 pos			= GetMicroTexturePos(microTextureID);
-
-		// add page offset to microtexture position
-		pos.y				+= GetNextPageOffset(baseTexInfo.y);
 	
-		vec4 tex2Data		= textureR3D(tex1, ivec2(0), ivec2(128), pos, fsTexCoord * scale);
+		vec4 tex2Data		= textureR3D(textureBank[(texturePage+1)&1], ivec2(0), ivec2(128), pos, fsTexCoord * scale);
 
 		float lod			= mip_map_level(fsTexCoord * scale * vec2(128.0));
 
diff --git a/Src/Graphics/New3D/R3DShaderQuads.h b/Src/Graphics/New3D/R3DShaderQuads.h
index 5a81d49..5ae1dee 100644
--- a/Src/Graphics/New3D/R3DShaderQuads.h
+++ b/Src/Graphics/New3D/R3DShaderQuads.h
@@ -175,7 +175,7 @@ static const char *fragmentShaderR3DQuads = R"glsl(
 
 #version 450 core
 
-uniform usampler2D tex1;			// entire texture sheet
+uniform usampler2D textureBank[2];			// entire texture sheet
 
 // texturing
 uniform bool	textureEnabled;
@@ -189,6 +189,7 @@ uniform bool	textureAlpha;
 uniform bool	alphaTest;
 uniform bool	discardAlpha;
 uniform ivec2	textureWrapMode;
+uniform int		texturePage;
 
 // general
 uniform vec3	fogColour;
diff --git a/Src/Graphics/New3D/R3DShaderTriangles.h b/Src/Graphics/New3D/R3DShaderTriangles.h
index b28ac24..f8971bd 100644
--- a/Src/Graphics/New3D/R3DShaderTriangles.h
+++ b/Src/Graphics/New3D/R3DShaderTriangles.h
@@ -66,7 +66,7 @@ static const char *fragmentShaderR3D = R"glsl(
 
 #version 410 core
 
-uniform usampler2D tex1;			// entire texture sheet
+uniform usampler2D textureBank[2];			// entire texture sheet
 
 // texturing
 uniform bool	textureEnabled;
@@ -80,6 +80,7 @@ uniform bool	textureAlpha;
 uniform bool	alphaTest;
 uniform bool	discardAlpha;
 uniform ivec2	textureWrapMode;
+uniform int		texturePage;
 
 // general
 uniform vec3	fogColour;
diff --git a/Src/Graphics/New3D/TextureBank.cpp b/Src/Graphics/New3D/TextureBank.cpp
new file mode 100644
index 0000000..a4f3717
--- /dev/null
+++ b/Src/Graphics/New3D/TextureBank.cpp
@@ -0,0 +1,71 @@
+#include "TextureBank.h"
+
+New3D::TextureBank::TextureBank()
+{
+	glGenTextures(1, &m_texID);
+	glBindTexture(GL_TEXTURE_2D, m_texID);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
+
+	int width = 2048;
+	int height = 1024;
+	int level = 0;
+
+	while (width>=1 && height>=1) {
+
+		glTexImage2D(GL_TEXTURE_2D, level, GL_R16UI, width, height, 0, GL_RED_INTEGER, GL_UNSIGNED_SHORT, nullptr);	// allocate storage
+
+		width	= (width > 1) ? width / 2 : 1;
+		height	= (height > 1) ? height / 2 : 1;
+
+		level++;
+
+		if (width == 1 && height == 1) {
+			glTexImage2D(GL_TEXTURE_2D, level, GL_R16UI, width, height, 0, GL_RED_INTEGER, GL_UNSIGNED_SHORT, nullptr);	// allocate storage
+			break;
+		}
+	}
+
+	m_numLevels = level;
+}
+
+New3D::TextureBank::~TextureBank()
+{
+	if (m_texID) {
+		glDeleteTextures(1, &m_texID);
+		m_texID = 0;
+	}
+}
+
+void New3D::TextureBank::AttachMemory(const UINT16* textureRam)
+{
+	m_textureRam = textureRam;
+}
+
+void New3D::TextureBank::Bind()
+{
+	glBindTexture(GL_TEXTURE_2D, m_texID);
+}
+
+void New3D::TextureBank::UploadTextures(int level, int x, int y, int width, int height)
+{
+	glBindTexture(GL_TEXTURE_2D, m_texID);
+	glPixelStorei(GL_UNPACK_ALIGNMENT, 2);
+
+	const int mipXBase[] = { 0, 1024, 1536, 1792, 1920, 1984, 2016, 2032, 2040, 2044, 2046, 2047 };
+	const int mipYBase[] = { 0, 512, 768, 896, 960, 992, 1008, 1016, 1020, 1022, 1023 };
+
+	int subX = x - mipXBase[level];
+	int subY = y - mipYBase[level];
+
+	for (unsigned i = 0; i < height; i++) {
+		glTexSubImage2D(GL_TEXTURE_2D, level, subX, subY + i, width, 1, GL_RED_INTEGER, GL_UNSIGNED_SHORT, m_textureRam + ((y + i) * 2048) + x);
+	}
+}
+
+int New3D::TextureBank::GetNumberOfLevels()
+{
+	return m_numLevels;
+}
diff --git a/Src/Graphics/New3D/TextureBank.h b/Src/Graphics/New3D/TextureBank.h
new file mode 100644
index 0000000..905d575
--- /dev/null
+++ b/Src/Graphics/New3D/TextureBank.h
@@ -0,0 +1,33 @@
+#pragma once
+
+#ifndef _TEXTUREBANK_H_
+#define _TEXTUREBANK_H_
+
+#include "Types.h"
+#include <GL/glew.h>
+
+// texture banks are a fixed size
+// 2048x1024 pixels, each pixel is 16bits in size
+
+namespace New3D {
+
+	class TextureBank
+	{
+	public:
+		TextureBank();
+		~TextureBank();
+
+		void AttachMemory(const UINT16* textureRam);
+		void Bind();
+		void UploadTextures(int level, int x, int y, int width, int height);
+		int GetNumberOfLevels();
+
+	private:
+		GLuint m_texID = 0;
+		const UINT16* m_textureRam = nullptr;
+		int m_numLevels = 0;
+	};
+
+}
+
+#endif
diff --git a/VS2008/Supermodel.vcxproj b/VS2008/Supermodel.vcxproj
index e4abddc..cfff9af 100644
--- a/VS2008/Supermodel.vcxproj
+++ b/VS2008/Supermodel.vcxproj
@@ -324,6 +324,7 @@ xcopy /D /Y "$(ProjectDir)..\Assets\*" "$(TargetDir)Assets"</Command>
     <ClCompile Include="..\Src\Graphics\New3D\R3DFrameBuffers.cpp" />
     <ClCompile Include="..\Src\Graphics\New3D\R3DScrollFog.cpp" />
     <ClCompile Include="..\Src\Graphics\New3D\R3DShader.cpp" />
+    <ClCompile Include="..\Src\Graphics\New3D\TextureBank.cpp" />
     <ClCompile Include="..\Src\Graphics\New3D\VBO.cpp" />
     <ClCompile Include="..\Src\Graphics\New3D\Vec.cpp" />
     <ClCompile Include="..\Src\Graphics\Render2D.cpp" />
@@ -503,6 +504,7 @@ xcopy /D /Y "$(ProjectDir)..\Assets\*" "$(TargetDir)Assets"</Command>
     <ClInclude Include="..\Src\Graphics\New3D\R3DShaderCommon.h" />
     <ClInclude Include="..\Src\Graphics\New3D\R3DShaderQuads.h" />
     <ClInclude Include="..\Src\Graphics\New3D\R3DShaderTriangles.h" />
+    <ClInclude Include="..\Src\Graphics\New3D\TextureBank.h" />
     <ClInclude Include="..\Src\Graphics\New3D\VBO.h" />
     <ClInclude Include="..\Src\Graphics\New3D\Vec.h" />
     <ClInclude Include="..\Src\Graphics\Render2D.h" />
diff --git a/VS2008/Supermodel.vcxproj.filters b/VS2008/Supermodel.vcxproj.filters
index acebf72..7562eb3 100644
--- a/VS2008/Supermodel.vcxproj.filters
+++ b/VS2008/Supermodel.vcxproj.filters
@@ -473,6 +473,9 @@
     <ClCompile Include="..\Src\Graphics\SuperAA.cpp">
       <Filter>Source Files\Graphics</Filter>
     </ClCompile>
+    <ClCompile Include="..\Src\Graphics\New3D\TextureBank.cpp">
+      <Filter>Source Files\Graphics\New</Filter>
+    </ClCompile>
   </ItemGroup>
   <ItemGroup>
     <MASM Include="..\Src\CPU\68K\Turbo68K\Turbo68K.asm">
@@ -862,6 +865,9 @@
     <ClInclude Include="..\Src\Graphics\SuperAA.h">
       <Filter>Header Files\Graphics</Filter>
     </ClInclude>
+    <ClInclude Include="..\Src\Graphics\New3D\TextureBank.h">
+      <Filter>Header Files\Graphics\New</Filter>
+    </ClInclude>
   </ItemGroup>
   <ItemGroup>
     <CustomBuild Include="..\Src\Debugger\ReadMe.txt">