diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index b0a593bb16d760950c71623f49e7349abd1df8ad..7a3f0564d3dc269239dcccae7e0e1af7200ae9e7 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -418,6 +418,7 @@ endif()
 if(${SRB2_CONFIG_HWRENDER})
 	add_definitions(-DHWRENDER)
 	set(SRB2_HWRENDER_SOURCES
+		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_batching.c
 		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_bsp.c
 		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_cache.c
 		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_clip.c
@@ -433,6 +434,7 @@ if(${SRB2_CONFIG_HWRENDER})
 	)
 
 	set (SRB2_HWRENDER_HEADERS
+		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_batching.h
 		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_clip.h
 		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_data.h
 		${CMAKE_CURRENT_SOURCE_DIR}/hardware/hw_defs.h
diff --git a/src/Makefile b/src/Makefile
index d2dd4757a4ff6114c2324675bf8884e5f4026a7e..e00c84bc9ff63277e33a23b6fbab392c6c94a384 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -225,7 +225,7 @@ else
 	OPTS+=-DHWRENDER
 	OBJS+=$(OBJDIR)/hw_bsp.o $(OBJDIR)/hw_draw.o $(OBJDIR)/hw_light.o \
 		 $(OBJDIR)/hw_main.o $(OBJDIR)/hw_clip.o $(OBJDIR)/hw_md2.o $(OBJDIR)/hw_cache.o $(OBJDIR)/hw_trick.o \
-		 $(OBJDIR)/hw_md2load.o $(OBJDIR)/hw_md3load.o $(OBJDIR)/hw_model.o $(OBJDIR)/u_list.o
+		 $(OBJDIR)/hw_md2load.o $(OBJDIR)/hw_md3load.o $(OBJDIR)/hw_model.o $(OBJDIR)/u_list.o $(OBJDIR)/hw_batching.o
 endif
 
 ifdef NOHS
diff --git a/src/d_main.c b/src/d_main.c
index fe06be2a4304609ee109f9298943336a0f3093b1..e4978205cc1b38028014dd7fc927cf638322713c 100644
--- a/src/d_main.c
+++ b/src/d_main.c
@@ -634,6 +634,28 @@ static void D_Display(void)
 				V_DrawThinString(30, 70, V_MONOSPACE | V_YELLOWMAP, s);
 				snprintf(s, sizeof s - 1, "fin  %d", rs_swaptime / divisor);
 				V_DrawThinString(30, 80, V_MONOSPACE | V_YELLOWMAP, s);
+				if (cv_grbatching.value)
+				{
+					snprintf(s, sizeof s - 1, "bsrt %d", rs_hw_batchsorttime / divisor);
+					V_DrawThinString(80, 55, V_MONOSPACE | V_REDMAP, s);
+					snprintf(s, sizeof s - 1, "bdrw %d", rs_hw_batchdrawtime / divisor);
+					V_DrawThinString(80, 65, V_MONOSPACE | V_REDMAP, s);
+
+					snprintf(s, sizeof s - 1, "npol %d", rs_hw_numpolys);
+					V_DrawThinString(130, 10, V_MONOSPACE | V_PURPLEMAP, s);
+					snprintf(s, sizeof s - 1, "ndc  %d", rs_hw_numcalls);
+					V_DrawThinString(130, 20, V_MONOSPACE | V_PURPLEMAP, s);
+					snprintf(s, sizeof s - 1, "nshd %d", rs_hw_numshaders);
+					V_DrawThinString(130, 30, V_MONOSPACE | V_PURPLEMAP, s);
+					snprintf(s, sizeof s - 1, "nvrt %d", rs_hw_numverts);
+					V_DrawThinString(130, 40, V_MONOSPACE | V_PURPLEMAP, s);
+					snprintf(s, sizeof s - 1, "ntex %d", rs_hw_numtextures);
+					V_DrawThinString(185, 10, V_MONOSPACE | V_PURPLEMAP, s);
+					snprintf(s, sizeof s - 1, "npf  %d", rs_hw_numpolyflags);
+					V_DrawThinString(185, 20, V_MONOSPACE | V_PURPLEMAP, s);
+					snprintf(s, sizeof s - 1, "ncol %d", rs_hw_numcolors);
+					V_DrawThinString(185, 30, V_MONOSPACE | V_PURPLEMAP, s);
+				}
 			}
 			else // software specific stats
 			{
diff --git a/src/hardware/hw_batching.c b/src/hardware/hw_batching.c
new file mode 100644
index 0000000000000000000000000000000000000000..4e11b0ab7fda45e6f78297882f5487405ad41f50
--- /dev/null
+++ b/src/hardware/hw_batching.c
@@ -0,0 +1,450 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C) 1999-2020 by Sonic Team Junior.
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file hw_batching.c
+/// \brief Draw call batching and related things.
+
+#ifdef HWRENDER
+#include "hw_glob.h"
+#include "hw_batching.h"
+#include "../i_system.h"
+
+// The texture for the next polygon given to HWR_ProcessPolygon.
+// Set with HWR_SetCurrentTexture.
+GLMipmap_t *current_texture = NULL;
+
+boolean currently_batching = false;
+
+FOutVector* finalVertexArray = NULL;// contains subset of sorted vertices and texture coordinates to be sent to gpu
+UINT32* finalVertexIndexArray = NULL;// contains indexes for glDrawElements, taking into account fan->triangles conversion
+//     NOTE have this alloced as 3x finalVertexArray size
+int finalVertexArrayAllocSize = 65536;
+//GLubyte* colorArray = NULL;// contains color data to be sent to gpu, if needed
+//int colorArrayAllocSize = 65536;
+// not gonna use this for now, just sort by color and change state when it changes
+// later maybe when using vertex attributes if it's needed
+
+PolygonArrayEntry* polygonArray = NULL;// contains the polygon data from DrawPolygon, waiting to be processed
+int polygonArraySize = 0;
+unsigned int* polygonIndexArray = NULL;// contains sorting pointers for polygonArray
+int polygonArrayAllocSize = 65536;
+
+FOutVector* unsortedVertexArray = NULL;// contains unsorted vertices and texture coordinates from DrawPolygon
+int unsortedVertexArraySize = 0;
+int unsortedVertexArrayAllocSize = 65536;
+
+// Enables batching mode. HWR_ProcessPolygon will collect polygons instead of passing them directly to the rendering backend.
+// Call HWR_RenderBatches to render all the collected geometry.
+void HWR_StartBatching(void)
+{
+    if (currently_batching)
+        I_Error("Repeat call to HWR_StartBatching without HWR_RenderBatches");
+
+    // init arrays if that has not been done yet
+	if (!finalVertexArray)
+	{
+		finalVertexArray = malloc(finalVertexArrayAllocSize * sizeof(FOutVector));
+		finalVertexIndexArray = malloc(finalVertexArrayAllocSize * 3 * sizeof(UINT32));
+		polygonArray = malloc(polygonArrayAllocSize * sizeof(PolygonArrayEntry));
+		polygonIndexArray = malloc(polygonArrayAllocSize * sizeof(unsigned int));
+		unsortedVertexArray = malloc(unsortedVertexArrayAllocSize * sizeof(FOutVector));
+	}
+
+    currently_batching = true;
+}
+
+// This replaces the direct calls to pfnSetTexture in cases where batching is available.
+// The texture selection is saved for the next HWR_ProcessPolygon call.
+// Doing this was easier than getting a texture pointer to HWR_ProcessPolygon.
+void HWR_SetCurrentTexture(GLMipmap_t *texture)
+{
+    if (currently_batching)
+    {
+        current_texture = texture;
+    }
+    else
+    {
+        HWD.pfnSetTexture(texture);
+    }
+}
+
+// If batching is enabled, this function collects the polygon data and the chosen texture
+// for later use in HWR_RenderBatches. Otherwise the rendering backend is used to
+// render the polygon immediately.
+void HWR_ProcessPolygon(FSurfaceInfo *pSurf, FOutVector *pOutVerts, FUINT iNumPts, FBITFIELD PolyFlags, int shader, boolean horizonSpecial)
+{
+    if (currently_batching)
+	{
+		if (!pSurf)
+			I_Error("Got a null FSurfaceInfo in batching");// nulls should not come in the stuff that batching currently applies to
+		if (polygonArraySize == polygonArrayAllocSize)
+		{
+			PolygonArrayEntry* new_array;
+			// ran out of space, make new array double the size
+			polygonArrayAllocSize *= 2;
+			new_array = malloc(polygonArrayAllocSize * sizeof(PolygonArrayEntry));
+			memcpy(new_array, polygonArray, polygonArraySize * sizeof(PolygonArrayEntry));
+			free(polygonArray);
+			polygonArray = new_array;
+			// also need to redo the index array, dont need to copy it though
+			free(polygonIndexArray);
+			polygonIndexArray = malloc(polygonArrayAllocSize * sizeof(unsigned int));
+		}
+
+		while (unsortedVertexArraySize + (int)iNumPts > unsortedVertexArrayAllocSize)
+		{
+			FOutVector* new_array;
+			// need more space for vertices in unsortedVertexArray
+			unsortedVertexArrayAllocSize *= 2;
+			new_array = malloc(unsortedVertexArrayAllocSize * sizeof(FOutVector));
+			memcpy(new_array, unsortedVertexArray, unsortedVertexArraySize * sizeof(FOutVector));
+			free(unsortedVertexArray);
+			unsortedVertexArray = new_array;
+		}
+
+		// add the polygon data to the arrays
+
+		polygonArray[polygonArraySize].surf = *pSurf;
+		polygonArray[polygonArraySize].vertsIndex = unsortedVertexArraySize;
+		polygonArray[polygonArraySize].numVerts = iNumPts;
+		polygonArray[polygonArraySize].polyFlags = PolyFlags;
+		polygonArray[polygonArraySize].texture = current_texture;
+		polygonArray[polygonArraySize].shader = shader;
+		polygonArray[polygonArraySize].horizonSpecial = horizonSpecial;
+		polygonArraySize++;
+
+		memcpy(&unsortedVertexArray[unsortedVertexArraySize], pOutVerts, iNumPts * sizeof(FOutVector));
+		unsortedVertexArraySize += iNumPts;
+	}
+	else
+	{
+        if (shader)
+            HWD.pfnSetShader(shader);
+        HWD.pfnDrawPolygon(pSurf, pOutVerts, iNumPts, PolyFlags);
+    }
+}
+
+static int comparePolygons(const void *p1, const void *p2)
+{
+	unsigned int index1 = *(const unsigned int*)p1;
+	unsigned int index2 = *(const unsigned int*)p2;
+	PolygonArrayEntry* poly1 = &polygonArray[index1];
+	PolygonArrayEntry* poly2 = &polygonArray[index2];
+	int diff;
+	INT64 diff64;
+	
+	int shader1 = poly1->shader;
+	int shader2 = poly2->shader;
+	// make skywalls and horizon lines first in order
+	if (poly1->polyFlags & PF_NoTexture || poly1->horizonSpecial)
+		shader1 = -1;
+	if (poly2->polyFlags & PF_NoTexture || poly2->horizonSpecial)
+		shader2 = -1;
+	diff = shader1 - shader2;
+	if (diff != 0) return diff;
+
+	// skywalls and horizon lines must retain their order for horizon lines to work
+	if (shader1 == -1 && shader2 == -1)
+		return index1 - index2;
+	
+	diff64 = poly1->texture - poly2->texture;
+	if (diff64 != 0) return diff64;
+	
+	diff = poly1->polyFlags - poly2->polyFlags;
+	if (diff != 0) return diff;
+	
+	diff64 = poly1->surf.PolyColor.rgba - poly2->surf.PolyColor.rgba;
+	if (diff64 < 0) return -1; else if (diff64 > 0) return 1;
+	diff64 = poly1->surf.TintColor.rgba - poly2->surf.TintColor.rgba;
+	if (diff64 < 0) return -1; else if (diff64 > 0) return 1;
+	diff64 = poly1->surf.FadeColor.rgba - poly2->surf.FadeColor.rgba;
+	if (diff64 < 0) return -1; else if (diff64 > 0) return 1;
+	
+	diff = poly1->surf.LightInfo.light_level - poly2->surf.LightInfo.light_level;
+	if (diff != 0) return diff;
+	diff = poly1->surf.LightInfo.fade_start - poly2->surf.LightInfo.fade_start;
+	if (diff != 0) return diff;
+	diff = poly1->surf.LightInfo.fade_end - poly2->surf.LightInfo.fade_end;
+	return diff;
+}
+
+static int comparePolygonsNoShaders(const void *p1, const void *p2)
+{
+	unsigned int index1 = *(const unsigned int*)p1;
+	unsigned int index2 = *(const unsigned int*)p2;
+	PolygonArrayEntry* poly1 = &polygonArray[index1];
+	PolygonArrayEntry* poly2 = &polygonArray[index2];
+	int diff;
+	INT64 diff64;
+	
+	GLMipmap_t *texture1 = poly1->texture;
+	GLMipmap_t *texture2 = poly2->texture;
+	if (poly1->polyFlags & PF_NoTexture || poly1->horizonSpecial)
+		texture1 = NULL;
+	if (poly2->polyFlags & PF_NoTexture || poly2->horizonSpecial)
+		texture2 = NULL;
+	diff64 = texture1 - texture2;
+	if (diff64 != 0) return diff64;
+
+	// skywalls and horizon lines must retain their order for horizon lines to work
+	if (texture1 == NULL && texture2 == NULL)
+		return index1 - index2;
+	
+	diff = poly1->polyFlags - poly2->polyFlags;
+	if (diff != 0) return diff;
+	
+	diff64 = poly1->surf.PolyColor.rgba - poly2->surf.PolyColor.rgba;
+	if (diff64 < 0) return -1; else if (diff64 > 0) return 1;
+
+	return 0;
+}
+
+// This function organizes the geometry collected by HWR_ProcessPolygon calls into batches and uses
+// the rendering backend to draw them.
+void HWR_RenderBatches(void)
+{
+    int finalVertexWritePos = 0;// position in finalVertexArray
+	int finalIndexWritePos = 0;// position in finalVertexIndexArray
+
+	int polygonReadPos = 0;// position in polygonIndexArray
+
+	int currentShader;
+	GLMipmap_t *currentTexture;
+	FBITFIELD currentPolyFlags;
+	FSurfaceInfo currentSurfaceInfo;
+
+	int i;
+
+    if (!currently_batching)
+		I_Error("HWR_RenderBatches called without starting batching");
+
+	currently_batching = false;// no longer collecting batches
+	if (!polygonArraySize)
+	{
+		rs_hw_numpolys = rs_hw_numcalls = rs_hw_numshaders = rs_hw_numtextures = rs_hw_numpolyflags = rs_hw_numcolors = 0;
+		return;// nothing to draw
+	}
+	// init stats vars
+	rs_hw_numpolys = polygonArraySize;
+	rs_hw_numcalls = rs_hw_numverts = 0;
+	rs_hw_numshaders = rs_hw_numtextures = rs_hw_numpolyflags = rs_hw_numcolors = 1;
+	// init polygonIndexArray
+	for (i = 0; i < polygonArraySize; i++)
+	{
+		polygonIndexArray[i] = i;
+	}
+
+	// sort polygons
+	rs_hw_batchsorttime = I_GetTimeMicros();
+	if (cv_grshaders.value) // TODO also have the shader availability check here when its done
+		qsort(polygonIndexArray, polygonArraySize, sizeof(unsigned int), comparePolygons);
+	else
+		qsort(polygonIndexArray, polygonArraySize, sizeof(unsigned int), comparePolygonsNoShaders);
+	rs_hw_batchsorttime = I_GetTimeMicros() - rs_hw_batchsorttime;
+	// sort order
+	// 1. shader
+	// 2. texture
+	// 3. polyflags
+	// 4. colors + light level
+	// not sure about what order of the last 2 should be, or if it even matters
+
+	rs_hw_batchdrawtime = I_GetTimeMicros();
+
+	currentShader = polygonArray[polygonIndexArray[0]].shader;
+	currentTexture = polygonArray[polygonIndexArray[0]].texture;
+	currentPolyFlags = polygonArray[polygonIndexArray[0]].polyFlags;
+	currentSurfaceInfo = polygonArray[polygonIndexArray[0]].surf;
+	// For now, will sort and track the colors. Vertex attributes could be used instead of uniforms
+	// and a color array could replace the color calls.
+	
+	// set state for first batch
+	
+	if (cv_grshaders.value) // TODO also have the shader availability check here when its done
+	{
+		HWD.pfnSetShader(currentShader);
+	}
+	
+	if (currentPolyFlags & PF_NoTexture)
+		currentTexture = NULL;
+    else
+	    HWD.pfnSetTexture(currentTexture);
+	
+	while (1)// note: remember handling notexture polyflag as having texture number 0 (also in comparePolygons)
+	{
+		int firstIndex;
+		int lastIndex;
+
+		boolean stopFlag = false;
+		boolean changeState = false;
+		boolean changeShader = false;
+		int nextShader;
+		boolean changeTexture = false;
+		GLMipmap_t *nextTexture;
+		boolean changePolyFlags = false;
+		FBITFIELD nextPolyFlags;
+		boolean changeSurfaceInfo = false;
+		FSurfaceInfo nextSurfaceInfo;
+
+		// steps:
+		// write vertices
+		// check for changes or end, otherwise go back to writing
+			// changes will affect the next vars and the change bools
+			// end could set flag for stopping
+		// execute draw call
+		// could check ending flag here
+		// change states according to next vars and change bools, updating the current vars and reseting the bools
+		// reset write pos
+		// repeat loop
+		
+		int index = polygonIndexArray[polygonReadPos++];
+		int numVerts = polygonArray[index].numVerts;
+		// before writing, check if there is enough room
+		// using 'while' instead of 'if' here makes sure that there will *always* be enough room.
+		// probably never will this loop run more than once though
+		while (finalVertexWritePos + numVerts > finalVertexArrayAllocSize)
+		{
+			FOutVector* new_array;
+			unsigned int* new_index_array;
+			finalVertexArrayAllocSize *= 2;
+			new_array = malloc(finalVertexArrayAllocSize * sizeof(FOutVector));
+			memcpy(new_array, finalVertexArray, finalVertexWritePos * sizeof(FOutVector));
+			free(finalVertexArray);
+			finalVertexArray = new_array;
+			// also increase size of index array, 3x of vertex array since
+			// going from fans to triangles increases vertex count to 3x
+			new_index_array = malloc(finalVertexArrayAllocSize * 3 * sizeof(UINT32));
+			memcpy(new_index_array, finalVertexIndexArray, finalIndexWritePos * sizeof(UINT32));
+			free(finalVertexIndexArray);
+			finalVertexIndexArray = new_index_array;
+		}
+		// write the vertices of the polygon
+		memcpy(&finalVertexArray[finalVertexWritePos], &unsortedVertexArray[polygonArray[index].vertsIndex],
+			numVerts * sizeof(FOutVector));
+		// write the indexes, pointing to the fan vertexes but in triangles format
+		firstIndex = finalVertexWritePos;
+		lastIndex = finalVertexWritePos + numVerts;
+		finalVertexWritePos += 2;
+		while (finalVertexWritePos < lastIndex)
+		{
+			finalVertexIndexArray[finalIndexWritePos++] = firstIndex;
+			finalVertexIndexArray[finalIndexWritePos++] = finalVertexWritePos - 1;
+			finalVertexIndexArray[finalIndexWritePos++] = finalVertexWritePos++;
+		}
+		
+		if (polygonReadPos >= polygonArraySize)
+		{
+			stopFlag = true;
+		}
+		else
+		{
+			// check if a state change is required, set the change bools and next vars
+			int nextIndex = polygonIndexArray[polygonReadPos];
+			nextShader = polygonArray[nextIndex].shader;
+			nextTexture = polygonArray[nextIndex].texture;
+			nextPolyFlags = polygonArray[nextIndex].polyFlags;
+			nextSurfaceInfo = polygonArray[nextIndex].surf;
+			if (nextPolyFlags & PF_NoTexture)
+				nextTexture = 0;
+			if (currentShader != nextShader)
+			{
+				changeState = true;
+				changeShader = true;
+			}
+			if (currentTexture != nextTexture)
+			{
+				changeState = true;
+				changeTexture = true;
+			}
+			if (currentPolyFlags != nextPolyFlags)
+			{
+				changeState = true;
+				changePolyFlags = true;
+			}
+			if (cv_grshaders.value) // TODO also have the shader availability check here when its done
+			{
+				if (currentSurfaceInfo.PolyColor.rgba != nextSurfaceInfo.PolyColor.rgba ||
+					currentSurfaceInfo.TintColor.rgba != nextSurfaceInfo.TintColor.rgba ||
+					currentSurfaceInfo.FadeColor.rgba != nextSurfaceInfo.FadeColor.rgba ||
+					currentSurfaceInfo.LightInfo.light_level != nextSurfaceInfo.LightInfo.light_level ||
+					currentSurfaceInfo.LightInfo.fade_start != nextSurfaceInfo.LightInfo.fade_start ||
+					currentSurfaceInfo.LightInfo.fade_end != nextSurfaceInfo.LightInfo.fade_end)
+				{
+					changeState = true;
+					changeSurfaceInfo = true;
+				}
+			}
+			else
+			{
+				if (currentSurfaceInfo.PolyColor.rgba != nextSurfaceInfo.PolyColor.rgba)
+				{
+					changeState = true;
+					changeSurfaceInfo = true;
+				}
+			}
+		}
+		
+		if (changeState || stopFlag)
+		{
+			// execute draw call
+            HWD.pfnDrawIndexedTriangles(&currentSurfaceInfo, finalVertexArray, finalIndexWritePos, currentPolyFlags, finalVertexIndexArray);
+			// update stats
+			rs_hw_numcalls++;
+			rs_hw_numverts += finalIndexWritePos;
+			// reset write positions
+			finalVertexWritePos = 0;
+			finalIndexWritePos = 0;
+		}
+		else continue;
+		
+		// if we're here then either its time to stop or time to change state
+		if (stopFlag) break;
+		
+		// change state according to change bools and next vars, update current vars and reset bools
+		if (changeShader)
+		{
+			HWD.pfnSetShader(nextShader);
+			currentShader = nextShader;
+			changeShader = false;
+
+			rs_hw_numshaders++;
+		}
+		if (changeTexture)
+		{
+			// texture should be already ready for use from calls to SetTexture during batch collection
+		    HWD.pfnSetTexture(nextTexture);
+			currentTexture = nextTexture;
+			changeTexture = false;
+
+			rs_hw_numtextures++;
+		}
+		if (changePolyFlags)
+		{
+			currentPolyFlags = nextPolyFlags;
+			changePolyFlags = false;
+
+			rs_hw_numpolyflags++;
+		}
+		if (changeSurfaceInfo)
+		{
+			currentSurfaceInfo = nextSurfaceInfo;
+			changeSurfaceInfo = false;
+
+			rs_hw_numcolors++;
+		}
+		// and that should be it?
+	}
+	// reset the arrays (set sizes to 0)
+	polygonArraySize = 0;
+	unsortedVertexArraySize = 0;
+	
+	rs_hw_batchdrawtime = I_GetTimeMicros() - rs_hw_batchdrawtime;
+}
+
+
+#endif // HWRENDER
diff --git a/src/hardware/hw_batching.h b/src/hardware/hw_batching.h
new file mode 100644
index 0000000000000000000000000000000000000000..7c108a4bd2a18e2cee24487afe8c79b4b856c28b
--- /dev/null
+++ b/src/hardware/hw_batching.h
@@ -0,0 +1,37 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C) 1999-2020 by Sonic Team Junior.
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file hw_batching.h
+/// \brief Draw call batching and related things.
+
+#ifndef __HWR_BATCHING_H__
+#define __HWR_BATCHING_H__
+
+#include "hw_defs.h"
+#include "hw_data.h"
+#include "hw_drv.h"
+
+typedef struct 
+{
+	FSurfaceInfo surf;// surf also has its own polyflags for some reason, but it seems unused
+	unsigned int vertsIndex;// location of verts in unsortedVertexArray
+	FUINT numVerts;
+	FBITFIELD polyFlags;
+	GLMipmap_t *texture;
+	int shader;
+	// this tells batching that the plane belongs to a horizon line and must be drawn in correct order with the skywalls
+	boolean horizonSpecial;
+} PolygonArrayEntry;
+
+void HWR_StartBatching(void);
+void HWR_SetCurrentTexture(GLMipmap_t *texture);
+void HWR_ProcessPolygon(FSurfaceInfo *pSurf, FOutVector *pOutVerts, FUINT iNumPts, FBITFIELD PolyFlags, int shader, boolean horizonSpecial);
+void HWR_RenderBatches(void);
+
+#endif
diff --git a/src/hardware/hw_cache.c b/src/hardware/hw_cache.c
index a64feab087517d82de638d773b5e0e36750fc11f..ab9a50dd5d884ba39ee9acf7c5f850d9589d1f67 100644
--- a/src/hardware/hw_cache.c
+++ b/src/hardware/hw_cache.c
@@ -15,6 +15,7 @@
 #ifdef HWRENDER
 #include "hw_glob.h"
 #include "hw_drv.h"
+#include "hw_batching.h"
 
 #include "../doomstat.h"    //gamemode
 #include "../i_video.h"     //rendermode
@@ -738,8 +739,11 @@ GLTexture_t *HWR_GetTexture(INT32 tex)
 	if (!grtex->mipmap.grInfo.data && !grtex->mipmap.downloaded)
 		HWR_GenerateTexture(tex, grtex);
 
-	// Tell the hardware driver to bind the current texture to the flat's mipmap
-	HWD.pfnSetTexture(&grtex->mipmap);
+	// If hardware does not have the texture, then call pfnSetTexture to upload it
+	if (!grtex->mipmap.downloaded)
+		HWD.pfnSetTexture(&grtex->mipmap);
+	
+	HWR_SetCurrentTexture(&grtex->mipmap);
 
 	// The system-memory data can be purged now.
 	Z_ChangeTag(grtex->mipmap.grInfo.data, PU_HWRCACHE_UNLOCKED);
@@ -818,7 +822,11 @@ void HWR_LiterallyGetFlat(lumpnum_t flatlumpnum)
 	if (!grmip->downloaded && !grmip->grInfo.data)
 		HWR_CacheFlat(grmip, flatlumpnum);
 
-	HWD.pfnSetTexture(grmip);
+	// If hardware does not have the texture, then call pfnSetTexture to upload it
+	if (!grmip->downloaded)
+		HWD.pfnSetTexture(grmip);
+	
+	HWR_SetCurrentTexture(grmip);
 
 	// The system-memory data can be purged now.
 	Z_ChangeTag(grmip->grInfo.data, PU_HWRCACHE_UNLOCKED);
@@ -852,14 +860,17 @@ void HWR_GetLevelFlat(levelflat_t *levelflat)
 		if (!grtex->mipmap.grInfo.data && !grtex->mipmap.downloaded)
 			HWR_CacheTextureAsFlat(&grtex->mipmap, texturenum);
 
-		// Tell the hardware driver to bind the current texture to the flat's mipmap
-		HWD.pfnSetTexture(&grtex->mipmap);
+		// If hardware does not have the texture, then call pfnSetTexture to upload it
+		if (!grtex->mipmap.downloaded)
+			HWD.pfnSetTexture(&grtex->mipmap);
+		
+		HWR_SetCurrentTexture(&grtex->mipmap);
 
 		// The system-memory data can be purged now.
 		Z_ChangeTag(grtex->mipmap.grInfo.data, PU_HWRCACHE_UNLOCKED);
 	}
 	else // set no texture
-		HWD.pfnSetTexture(NULL);
+		HWR_SetCurrentTexture(NULL);
 }
 
 //
@@ -881,7 +892,11 @@ static void HWR_LoadMappedPatch(GLMipmap_t *grmip, GLPatch_t *gpatch)
 			Z_Free(patch);
 	}
 
-	HWD.pfnSetTexture(grmip);
+	// If hardware does not have the texture, then call pfnSetTexture to upload it
+	if (!grmip->downloaded)
+		HWD.pfnSetTexture(grmip);
+	
+	HWR_SetCurrentTexture(grmip);
 
 	// The system-memory data can be purged now.
 	Z_ChangeTag(grmip->grInfo.data, PU_HWRCACHE_UNLOCKED);
@@ -908,7 +923,11 @@ void HWR_GetPatch(GLPatch_t *gpatch)
 			Z_Free(ptr);
 	}
 
-	HWD.pfnSetTexture(gpatch->mipmap);
+	// If hardware does not have the texture, then call pfnSetTexture to upload it
+	if (!gpatch->mipmap->downloaded)
+		HWD.pfnSetTexture(gpatch->mipmap);
+
+	HWR_SetCurrentTexture(gpatch->mipmap);
 
 	// The system-memory patch data can be purged now.
 	Z_ChangeTag(gpatch->mipmap->grInfo.data, PU_HWRCACHE_UNLOCKED);
diff --git a/src/hardware/hw_drv.h b/src/hardware/hw_drv.h
index a09f3f224d8839bfcc372fa036eb6dcfc2b4f531..6cc45a3639c33475d4f631c71d2f39a55aa9b8e7 100644
--- a/src/hardware/hw_drv.h
+++ b/src/hardware/hw_drv.h
@@ -36,6 +36,7 @@ EXPORT void HWRAPI(SetPalette) (RGBA_t *ppal);
 EXPORT void HWRAPI(FinishUpdate) (INT32 waitvbl);
 EXPORT void HWRAPI(Draw2DLine) (F2DCoord *v1, F2DCoord *v2, RGBA_t Color);
 EXPORT void HWRAPI(DrawPolygon) (FSurfaceInfo *pSurf, FOutVector *pOutVerts, FUINT iNumPts, FBITFIELD PolyFlags);
+EXPORT void HWRAPI(DrawIndexedTriangles) (FSurfaceInfo *pSurf, FOutVector *pOutVerts, FUINT iNumPts, FBITFIELD PolyFlags, unsigned int *IndexArray);
 EXPORT void HWRAPI(RenderSkyDome) (INT32 tex, INT32 texture_width, INT32 texture_height, FTransform transform);
 EXPORT void HWRAPI(SetBlend) (FBITFIELD PolyFlags);
 EXPORT void HWRAPI(ClearBuffer) (FBOOLEAN ColorMask, FBOOLEAN DepthMask, FRGBAFloat *ClearColor);
@@ -89,6 +90,7 @@ struct hwdriver_s
 	FinishUpdate        pfnFinishUpdate;
 	Draw2DLine          pfnDraw2DLine;
 	DrawPolygon         pfnDrawPolygon;
+	DrawIndexedTriangles    pfnDrawIndexedTriangles;
 	RenderSkyDome       pfnRenderSkyDome;
 	SetBlend            pfnSetBlend;
 	ClearBuffer         pfnClearBuffer;
diff --git a/src/hardware/hw_main.c b/src/hardware/hw_main.c
index 7186f732feb378f4cfc06311a2d2e6df27948673..3163b701aff4cb49b361a6e3e71216b59e72111a 100644
--- a/src/hardware/hw_main.c
+++ b/src/hardware/hw_main.c
@@ -18,6 +18,7 @@
 #include "hw_glob.h"
 #include "hw_light.h"
 #include "hw_drv.h"
+#include "hw_batching.h"
 
 #include "../i_video.h" // for rendermode == render_glide
 #include "../v_video.h"
@@ -150,6 +151,17 @@ int rs_hw_nodedrawtime = 0;
 int rs_hw_spritesorttime = 0;
 int rs_hw_spritedrawtime = 0;
 
+// Render stats for batching
+int rs_hw_numpolys = 0;
+int rs_hw_numverts = 0;
+int rs_hw_numcalls = 0;
+int rs_hw_numshaders = 0;
+int rs_hw_numtextures = 0;
+int rs_hw_numpolyflags = 0;
+int rs_hw_numcolors = 0;
+int rs_hw_batchsorttime = 0;
+int rs_hw_batchdrawtime = 0;
+
 
 // ==========================================================================
 // Lighting
@@ -351,6 +363,8 @@ static void HWR_RenderPlane(subsector_t *subsector, extrasubsector_t *xsub, bool
 	static FOutVector *planeVerts = NULL;
 	static UINT16 numAllocedPlaneVerts = 0;
 
+	int shader;
+
 	// no convex poly were generated for this subsector
 	if (!xsub->planepoly)
 		return;
@@ -433,7 +447,7 @@ static void HWR_RenderPlane(subsector_t *subsector, extrasubsector_t *xsub, bool
 		}
 	}
 	else // set no texture
-		HWD.pfnSetTexture(NULL);
+		HWR_SetCurrentTexture(NULL);
 
 	// reference point for flat texture coord for each vertex around the polygon
 	flatxref = (float)(((fixed_t)pv->x & (~flatflag)) / fflatwidth);
@@ -543,13 +557,13 @@ static void HWR_RenderPlane(subsector_t *subsector, extrasubsector_t *xsub, bool
 		PolyFlags |= PF_Masked|PF_Modulated;
 
 	if (PolyFlags & PF_Fog)
-		HWD.pfnSetShader(6);	// fog shader
+		shader = 6;	// fog shader
 	else if (PolyFlags & PF_Ripple)
-		HWD.pfnSetShader(5);	// water shader
+		shader = 5;	// water shader
 	else
-		HWD.pfnSetShader(1);	// floor shader
+		shader = 1;	// floor shader
 
-	HWD.pfnDrawPolygon(&Surf, planeVerts, nrPlaneVerts, PolyFlags);
+	HWR_ProcessPolygon(&Surf, planeVerts, nrPlaneVerts, PolyFlags, shader, false);
 
 	if (subsector)
 	{
@@ -618,7 +632,7 @@ static void HWR_RenderPlane(subsector_t *subsector, extrasubsector_t *xsub, bool
 					horizonpts[4].y = gr_viewz;
 
 					// Draw
-					HWD.pfnDrawPolygon(&Surf, horizonpts, 6, PolyFlags);
+					HWR_ProcessPolygon(&Surf, horizonpts, 6, PolyFlags, shader, true);
 				}
 			}
 		}
@@ -780,8 +794,7 @@ static void HWR_ProjectWall(FOutVector *wallVerts, FSurfaceInfo *pSurf, FBITFIEL
 {
 	HWR_Lighting(pSurf, lightlevel, wallcolormap);
 
-	HWD.pfnSetShader(2);	// wall shader
-	HWD.pfnDrawPolygon(pSurf, wallVerts, 4, blendmode|PF_Modulated|PF_Occlude);
+	HWR_ProcessPolygon(pSurf, wallVerts, 4, blendmode|PF_Modulated|PF_Occlude, 2, false); // wall shader
 
 #ifdef WALLSPLATS
 	if (gr_curline->linedef->splats && cv_splats.value)
@@ -1009,7 +1022,7 @@ static void HWR_SplitWall(sector_t *sector, FOutVector *wallVerts, INT32 texnum,
 // Draw walls into the depth buffer so that anything behind is culled properly
 static void HWR_DrawSkyWall(FOutVector *wallVerts, FSurfaceInfo *Surf)
 {
-	HWD.pfnSetTexture(NULL);
+	HWR_SetCurrentTexture(NULL);
 	// no texture
 	wallVerts[3].t = wallVerts[2].t = 0;
 	wallVerts[0].t = wallVerts[1].t = 0;
@@ -2739,7 +2752,7 @@ static void HWR_RenderPolyObjectPlane(polyobj_t *polysector, boolean isceiling,
 		}
 	}
 	else // set no texture
-		HWD.pfnSetTexture(NULL);
+		HWR_SetCurrentTexture(NULL);
 
 	// reference point for flat texture coord for each vertex around the polygon
 	flatxref = (float)((polysector->origVerts[0].x & (~flatflag)) / fflatwidth);
@@ -2837,8 +2850,7 @@ static void HWR_RenderPolyObjectPlane(polyobj_t *polysector, boolean isceiling,
 	else
 		blendmode |= PF_Masked|PF_Modulated|PF_Clip;
 
-	HWD.pfnSetShader(1);	// floor shader
-	HWD.pfnDrawPolygon(&Surf, planeVerts, nrPlaneVerts, blendmode);
+	HWR_ProcessPolygon(&Surf, planeVerts, nrPlaneVerts, blendmode, 1, false); // floor shader
 }
 
 static void HWR_AddPolyObjectPlanes(void)
@@ -3625,8 +3637,7 @@ static void HWR_DrawDropShadow(mobj_t *thing, fixed_t scale)
 	HWR_Lighting(&sSurf, 0, colormap);
 	sSurf.PolyColor.s.alpha = alpha;
 
-	HWD.pfnSetShader(3); // sprite shader
-	HWD.pfnDrawPolygon(&sSurf, shadowVerts, 4, PF_Translucent|PF_Modulated|PF_Clip);
+	HWR_ProcessPolygon(&sSurf, shadowVerts, 4, PF_Translucent|PF_Modulated|PF_Clip, 3, false); // sprite shader
 }
 
 // This is expecting a pointer to an array containing 4 wallVerts for a sprite
@@ -3889,8 +3900,7 @@ static void HWR_SplitSprite(gr_vissprite_t *spr)
 
 		Surf.PolyColor.s.alpha = alpha;
 
-		HWD.pfnSetShader(3);	// sprite shader
-		HWD.pfnDrawPolygon(&Surf, wallVerts, 4, blend|PF_Modulated|PF_Clip);
+		HWR_ProcessPolygon(&Surf, wallVerts, 4, blend|PF_Modulated|PF_Clip, 3, false); // sprite shader
 
 		top = bot;
 		endtop = endbot;
@@ -3916,8 +3926,7 @@ static void HWR_SplitSprite(gr_vissprite_t *spr)
 
 	Surf.PolyColor.s.alpha = alpha;
 
-	HWD.pfnSetShader(3);	// sprite shader
-	HWD.pfnDrawPolygon(&Surf, wallVerts, 4, blend|PF_Modulated|PF_Clip);
+	HWR_ProcessPolygon(&Surf, wallVerts, 4, blend|PF_Modulated|PF_Clip, 3, false); // sprite shader
 }
 
 // -----------------+
@@ -4062,8 +4071,7 @@ static void HWR_DrawSprite(gr_vissprite_t *spr)
 			blend = PF_Translucent|PF_Occlude;
 		}
 
-		HWD.pfnSetShader(3);	// sprite shader
-		HWD.pfnDrawPolygon(&Surf, wallVerts, 4, blend|PF_Modulated|PF_Clip);
+		HWR_ProcessPolygon(&Surf, wallVerts, 4, blend|PF_Modulated|PF_Clip, 3, false); // sprite shader
 	}
 }
 
@@ -4162,8 +4170,7 @@ static inline void HWR_DrawPrecipitationSprite(gr_vissprite_t *spr)
 		blend = PF_Translucent|PF_Occlude;
 	}
 
-	HWD.pfnSetShader(3);	// sprite shader
-	HWD.pfnDrawPolygon(&Surf, wallVerts, 4, blend|PF_Modulated|PF_Clip);
+	HWR_ProcessPolygon(&Surf, wallVerts, 4, blend|PF_Modulated|PF_Clip, 3, false); // sprite shader
 }
 #endif
 
@@ -5450,6 +5457,9 @@ void HWR_RenderSkyboxView(INT32 viewnumber, player_t *player)
 
 	validcount++;
 
+	if (cv_grbatching.value)
+		HWR_StartBatching();
+
 	HWR_RenderBSPNode((INT32)numnodes-1);
 
 #ifndef NEWCLIP
@@ -5481,6 +5491,9 @@ void HWR_RenderSkyboxView(INT32 viewnumber, player_t *player)
 	}
 #endif
 
+	if (cv_grbatching.value)
+		HWR_RenderBatches();
+
 	// Check for new console commands.
 	NetUpdate();
 
@@ -5661,6 +5674,9 @@ void HWR_RenderPlayerView(INT32 viewnumber, player_t *player)
 
 	validcount++;
 
+	if (cv_grbatching.value)
+		HWR_StartBatching();
+
 	HWR_RenderBSPNode((INT32)numnodes-1);
 
 #ifndef NEWCLIP
@@ -5694,6 +5710,9 @@ void HWR_RenderPlayerView(INT32 viewnumber, player_t *player)
 
 	rs_bsptime = I_GetTimeMicros() - rs_bsptime;
 
+	if (cv_grbatching.value)
+		HWR_RenderBatches();
+
 	// Check for new console commands.
 	NetUpdate();
 
@@ -5785,6 +5804,8 @@ consvar_t cv_granisotropicmode = {"gr_anisotropicmode", "1", CV_CALL, granisotro
 consvar_t cv_grcorrecttricks = {"gr_correcttricks", "Off", 0, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
 consvar_t cv_grsolvetjoin = {"gr_solvetjoin", "On", 0, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
 
+consvar_t cv_grbatching = {"gr_batching", "On", 0, CV_OnOff, NULL, 0, NULL, NULL, 0, 0, NULL};
+
 static void CV_grfiltermode_OnChange(void)
 {
 	if (rendermode == render_opengl)
@@ -5824,6 +5845,7 @@ void HWR_AddCommands(void)
 	CV_RegisterVar(&cv_grsolvetjoin);
 	
 	CV_RegisterVar(&cv_renderstats);
+	CV_RegisterVar(&cv_grbatching);
 
 #ifndef NEWCLIP
 	CV_RegisterVar(&cv_grclipwalls);
@@ -5949,12 +5971,14 @@ void HWR_RenderWall(FOutVector *wallVerts, FSurfaceInfo *pSurf, FBITFIELD blend,
 	FBITFIELD blendmode = blend;
 	UINT8 alpha = pSurf->PolyColor.s.alpha; // retain the alpha
 
+	int shader;
+
 	// Lighting is done here instead so that fog isn't drawn incorrectly on transparent walls after sorting
 	HWR_Lighting(pSurf, lightlevel, wallcolormap);
 
 	pSurf->PolyColor.s.alpha = alpha; // put the alpha back after lighting
 
-	HWD.pfnSetShader(2);	// wall shader
+	shader = 2;	// wall shader
 
 	if (blend & PF_Environment)
 		blendmode |= PF_Occlude;	// PF_Occlude must be used for solid objects
@@ -5962,12 +5986,12 @@ void HWR_RenderWall(FOutVector *wallVerts, FSurfaceInfo *pSurf, FBITFIELD blend,
 	if (fogwall)
 	{
 		blendmode |= PF_Fog;
-		HWD.pfnSetShader(6);	// fog shader
+		shader = 6;	// fog shader
 	}
 
 	blendmode |= PF_Modulated;	// No PF_Occlude means overlapping (incorrect) transparency
 
-	HWD.pfnDrawPolygon(pSurf, wallVerts, 4, blendmode);
+	HWR_ProcessPolygon(pSurf, wallVerts, 4, blendmode, shader, false);
 
 #ifdef WALLSPLATS
 	if (gr_curline->linedef->splats && cv_splats.value)
diff --git a/src/hardware/hw_main.h b/src/hardware/hw_main.h
index fc49364da17190064c0d265296fda3eca4264494..4aadc2c3977ca46c4b0434088b3327701192eab1 100644
--- a/src/hardware/hw_main.h
+++ b/src/hardware/hw_main.h
@@ -96,6 +96,8 @@ extern consvar_t cv_grskydome;
 extern consvar_t cv_grfakecontrast;
 extern consvar_t cv_grslopecontrast;
 
+extern consvar_t cv_grbatching;
+
 extern float gr_viewwidth, gr_viewheight, gr_baseviewwindowy;
 
 extern float gr_viewwindowx, gr_basewindowcentery;
@@ -111,4 +113,15 @@ extern int rs_hw_nodedrawtime;
 extern int rs_hw_spritesorttime;
 extern int rs_hw_spritedrawtime;
 
+// Render stats for batching
+extern int rs_hw_numpolys;
+extern int rs_hw_numverts;
+extern int rs_hw_numcalls;
+extern int rs_hw_numshaders;
+extern int rs_hw_numtextures;
+extern int rs_hw_numpolyflags;
+extern int rs_hw_numcolors;
+extern int rs_hw_batchsorttime;
+extern int rs_hw_batchdrawtime;
+
 #endif
diff --git a/src/hardware/r_opengl/r_opengl.c b/src/hardware/r_opengl/r_opengl.c
index d80de8aadc349b05baff668e4a019b487cbb221b..bbeec6e0b8e221db8cecdd4ed217e6b9472a57ef 100644
--- a/src/hardware/r_opengl/r_opengl.c
+++ b/src/hardware/r_opengl/r_opengl.c
@@ -1945,10 +1945,9 @@ static void Shader_SetUniforms(FSurfaceInfo *Surface, GLRGBAFloat *poly, GLRGBAF
 #endif
 }
 
-// -----------------+
-// DrawPolygon      : Render a polygon, set the texture, set render mode
-// -----------------+
-EXPORT void HWRAPI(DrawPolygon) (FSurfaceInfo *pSurf, FOutVector *pOutVerts, FUINT iNumPts, FBITFIELD PolyFlags)
+// code that is common between DrawPolygon and DrawIndexedTriangles
+// the corona thing is there too, i have no idea if that stuff works with DrawIndexedTriangles and batching
+static void PreparePolygon(FSurfaceInfo *pSurf, FOutVector *pOutVerts, FBITFIELD PolyFlags)
 {
 	static GLRGBAFloat poly = {0,0,0,0};
 	static GLRGBAFloat tint = {0,0,0,0};
@@ -2013,10 +2012,10 @@ EXPORT void HWRAPI(DrawPolygon) (FSurfaceInfo *pSurf, FOutVector *pOutVerts, FUI
 		//GL_DBG_Printf("Projection: (%f, %f, %f)\n", px, py, pz);
 
 		if ((pz <  0.0l) ||
-		    (px < -8.0l) ||
-		    (py < viewport[1]-8.0l) ||
-		    (px > viewport[2]+8.0l) ||
-		    (py > viewport[1]+viewport[3]+8.0l))
+			(px < -8.0l) ||
+			(py < viewport[1]-8.0l) ||
+			(px > viewport[2]+8.0l) ||
+			(py > viewport[1]+viewport[3]+8.0l))
 			return;
 
 		// the damned slow glReadPixels functions :(
@@ -2051,6 +2050,14 @@ EXPORT void HWRAPI(DrawPolygon) (FSurfaceInfo *pSurf, FOutVector *pOutVerts, FUI
 	}
 
 	Shader_Load(pSurf, &poly, &tint, &fade);
+}
+
+// -----------------+
+// DrawPolygon      : Render a polygon, set the texture, set render mode
+// -----------------+
+EXPORT void HWRAPI(DrawPolygon) (FSurfaceInfo *pSurf, FOutVector *pOutVerts, FUINT iNumPts, FBITFIELD PolyFlags)
+{
+	PreparePolygon(pSurf, pOutVerts, PolyFlags);
 
 	pglVertexPointer(3, GL_FLOAT, sizeof(FOutVector), &pOutVerts[0].x);
 	pglTexCoordPointer(2, GL_FLOAT, sizeof(FOutVector), &pOutVerts[0].s);
@@ -2066,6 +2073,17 @@ EXPORT void HWRAPI(DrawPolygon) (FSurfaceInfo *pSurf, FOutVector *pOutVerts, FUI
 		Clamp2D(GL_TEXTURE_WRAP_T);
 }
 
+EXPORT void HWRAPI(DrawIndexedTriangles) (FSurfaceInfo *pSurf, FOutVector *pOutVerts, FUINT iNumPts, FBITFIELD PolyFlags, unsigned int *IndexArray)
+{
+	PreparePolygon(pSurf, pOutVerts, PolyFlags);
+
+	pglVertexPointer(3, GL_FLOAT, sizeof(FOutVector), &pOutVerts[0].x);
+	pglTexCoordPointer(2, GL_FLOAT, sizeof(FOutVector), &pOutVerts[0].s);
+	pglDrawElements(GL_TRIANGLES, iNumPts, GL_UNSIGNED_INT, IndexArray);
+
+	// the DrawPolygon variant of this has some code about polyflags and wrapping here but havent noticed any problems from omitting it?
+}
+
 typedef struct vbo_vertex_s
 {
 	float x, y, z;
diff --git a/src/sdl/Srb2SDL-vc10.vcxproj b/src/sdl/Srb2SDL-vc10.vcxproj
index 6a55ac2d6837bd3e76fe5db3165732077143e8a5..c24104b925afab8e35558285b9f6a88dfd95e36e 100644
--- a/src/sdl/Srb2SDL-vc10.vcxproj
+++ b/src/sdl/Srb2SDL-vc10.vcxproj
@@ -220,6 +220,7 @@
     <ClInclude Include="..\hardware\hw3dsdrv.h" />
     <ClInclude Include="..\hardware\hw3sound.h" />
     <ClInclude Include="..\hardware\hws_data.h" />
+    <ClInclude Include="..\hardware\hw_batching.h" />
     <ClInclude Include="..\hardware\hw_clip.h" />
     <ClInclude Include="..\hardware\hw_data.h" />
     <ClInclude Include="..\hardware\hw_defs.h" />
@@ -370,6 +371,7 @@
     <ClCompile Include="..\g_game.c" />
     <ClCompile Include="..\g_input.c" />
     <ClCompile Include="..\hardware\hw3sound.c" />
+    <ClCompile Include="..\hardware\hw_batching.c" />
     <ClCompile Include="..\hardware\hw_bsp.c" />
     <ClCompile Include="..\hardware\hw_cache.c" />
     <ClCompile Include="..\hardware\hw_clip.c" />
diff --git a/src/sdl/Srb2SDL-vc10.vcxproj.filters b/src/sdl/Srb2SDL-vc10.vcxproj.filters
index 89ba1b588ab5b149394327b6b5077853772ac5cf..2f0aec7ca3851b6bf1f1760feafec1d50a9d506d 100644
--- a/src/sdl/Srb2SDL-vc10.vcxproj.filters
+++ b/src/sdl/Srb2SDL-vc10.vcxproj.filters
@@ -219,6 +219,9 @@
     <ClInclude Include="..\hardware\hws_data.h">
       <Filter>Hw_Hardware</Filter>
     </ClInclude>
+    <ClInclude Include="..\hardware\hw_batching.h">
+      <Filter>Hw_Hardware</Filter>
+    </ClInclude>
     <ClInclude Include="..\hardware\hw_clip.h">
       <Filter>Hw_Hardware</Filter>
     </ClInclude>
@@ -636,6 +639,9 @@
     <ClCompile Include="..\hardware\hw3sound.c">
       <Filter>Hw_Hardware</Filter>
     </ClCompile>
+    <ClCompile Include="..\hardware\hw_batching.c">
+      <Filter>Hw_Hardware</Filter>
+    </ClCompile>
     <ClCompile Include="..\hardware\hw_bsp.c">
       <Filter>Hw_Hardware</Filter>
     </ClCompile>
diff --git a/src/sdl/hwsym_sdl.c b/src/sdl/hwsym_sdl.c
index 974ac5eddb55a67b2c87bf4698038b352b9584de..416c8d2f5531f1ec09d15770a711fa756118c129 100644
--- a/src/sdl/hwsym_sdl.c
+++ b/src/sdl/hwsym_sdl.c
@@ -80,6 +80,7 @@ void *hwSym(const char *funcName,void *handle)
 	GETFUNC(Init);
 	GETFUNC(Draw2DLine);
 	GETFUNC(DrawPolygon);
+	GETFUNC(DrawIndexedTriangles);
 	GETFUNC(RenderSkyDome);
 	GETFUNC(SetBlend);
 	GETFUNC(ClearBuffer);
diff --git a/src/sdl/i_video.c b/src/sdl/i_video.c
index f06e101241b2599d846b36cce2a73668200588a1..8bff5690f3fe17ae0d938b154587d19afa2d9f26 100644
--- a/src/sdl/i_video.c
+++ b/src/sdl/i_video.c
@@ -1824,6 +1824,7 @@ void VID_StartupOpenGL(void)
 		HWD.pfnFinishUpdate     = NULL;
 		HWD.pfnDraw2DLine       = hwSym("Draw2DLine",NULL);
 		HWD.pfnDrawPolygon      = hwSym("DrawPolygon",NULL);
+		HWD.pfnDrawIndexedTriangles = hwSym("DrawIndexedTriangles",NULL);
 		HWD.pfnRenderSkyDome    = hwSym("RenderSkyDome",NULL);
 		HWD.pfnSetBlend         = hwSym("SetBlend",NULL);
 		HWD.pfnClearBuffer      = hwSym("ClearBuffer",NULL);