Browse Source

Add (low level) functionality to allow rendering 3D Meshes.

- Add love.graphics.setDepthMode(testcomparison, enablewrites).
- Add love.graphics.setMeshCullMode.
- Add love.graphics.setFrontFaceWinding.

Depth testing and depth writes require a depth buffer to work. The mesh cull mode controls whether front/back faces of a mesh are culled. The front face winding determines whether a clockwise or counterclockwise-oriented face in a mesh is considered front facing.

--HG--
branch : minor
Alex Szpakowski 7 years ago
parent
commit
7354de2059

+ 1 - 1
platform/xcode/liblove.xcodeproj/project.pbxproj

@@ -4012,7 +4012,7 @@
 		08FB7793FE84155DC02AAC07 /* Project object */ = {
 			isa = PBXProject;
 			attributes = {
-				LastUpgradeCheck = 0900;
+				LastUpgradeCheck = 0920;
 				TargetAttributes = {
 					FA0B78DC1A958B90000E1D17 = {
 						CreatedOnToolsVersion = 6.1.1;

+ 1 - 1
platform/xcode/love.xcodeproj/project.pbxproj

@@ -326,7 +326,7 @@
 		29B97313FDCFA39411CA2CEA /* Project object */ = {
 			isa = PBXProject;
 			attributes = {
-				LastUpgradeCheck = 0900;
+				LastUpgradeCheck = 0920;
 				TargetAttributes = {
 					FA0B7F051A95AAF3000E1D17 = {
 						CreatedOnToolsVersion = 6.1.1;

+ 3 - 2
src/common/Matrix.cpp

@@ -382,16 +382,17 @@ Matrix4 Matrix4::inverse() const
 	return inv;
 }
 
-Matrix4 Matrix4::ortho(float left, float right, float bottom, float top)
+Matrix4 Matrix4::ortho(float left, float right, float bottom, float top, float near, float far)
 {
 	Matrix4 m;
 
 	m.e[0] = 2.0f / (right - left);
 	m.e[5] = 2.0f / (top - bottom);
-	m.e[10] = -1.0;
+	m.e[10] = -2.0f / (far - near);
 
 	m.e[12] = -(right + left) / (right - left);
 	m.e[13] = -(top + bottom) / (top - bottom);
+	m.e[14] = -(far + near) / (far - near);
 
 	return m;
 }

+ 2 - 3
src/common/Matrix.h

@@ -218,10 +218,9 @@ public:
 	Matrix4 inverse() const;
 
 	/**
-	 * Creates a new orthographic projection matrix with depth in the range of
-	 * [-1, 1].
+	 * Creates a new orthographic projection matrix.
 	 **/
-	static Matrix4 ortho(float left, float right, float bottom, float top);
+	static Matrix4 ortho(float left, float right, float bottom, float top, float near, float far);
 
 private:
 

+ 1 - 1
src/modules/graphics/Font.cpp

@@ -471,7 +471,7 @@ std::vector<Font::DrawCommand> Font::generateVertices(const ColoredCodepoints &c
 		// Add kerning to the current horizontal offset.
 		dx += getKerning(prevglyph, g);
 
-		if (glyph.texture != 0)
+		if (glyph.texture != nullptr)
 		{
 			// Copy the vertices and set their colors and relative positions.
 			for (int j = 0; j < 4; j++)

+ 62 - 5
src/modules/graphics/Graphics.cpp

@@ -374,6 +374,10 @@ void Graphics::restoreState(const DisplayState &s)
 		setScissor();
 
 	setStencilTest(s.stencilCompare, s.stencilTestValue);
+	setDepthMode(s.depthTest, s.depthWrite);
+
+	setMeshCullMode(s.meshCullMode);
+	setFrontFaceWinding(s.winding);
 
 	setFont(s.font.get());
 	setShader(s.shader.get());
@@ -417,6 +421,14 @@ void Graphics::restoreStateChecked(const DisplayState &s)
 	if (s.stencilCompare != cur.stencilCompare || s.stencilTestValue != cur.stencilTestValue)
 		setStencilTest(s.stencilCompare, s.stencilTestValue);
 
+	if (s.depthTest != cur.depthTest || s.depthWrite != cur.depthWrite)
+		setDepthMode(s.depthTest, s.depthWrite);
+
+	setMeshCullMode(s.meshCullMode);
+
+	if (s.winding != cur.winding)
+		setFrontFaceWinding(s.winding);
+
 	setFont(s.font.get());
 	setShader(s.shader.get());
 
@@ -788,13 +800,46 @@ bool Graphics::getScissor(Rect &rect) const
 	return state.scissor;
 }
 
-void Graphics::getStencilTest(CompareMode &compare, int &value)
+void Graphics::setStencilTest()
+{
+	setStencilTest(COMPARE_ALWAYS, 0);
+}
+
+void Graphics::getStencilTest(CompareMode &compare, int &value) const
 {
 	const DisplayState &state = states.back();
 	compare = state.stencilCompare;
 	value = state.stencilTestValue;
 }
 
+void Graphics::setDepthMode()
+{
+	setDepthMode(COMPARE_ALWAYS, false);
+}
+
+void Graphics::getDepthMode(CompareMode &compare, bool &write) const
+{
+	const DisplayState &state = states.back();
+	compare = state.depthTest;
+	write = state.depthWrite;
+}
+
+void Graphics::setMeshCullMode(CullMode cull)
+{
+	// Handled inside the draw() graphics API implementations.
+	states.back().meshCullMode = cull;
+}
+
+CullMode Graphics::getMeshCullMode() const
+{
+	return states.back().meshCullMode;
+}
+
+vertex::Winding Graphics::getFrontFaceWinding() const
+{
+	return states.back().winding;
+}
+
 Graphics::ColorMask Graphics::getColorMask() const
 {
 	return states.back().colorMask;
@@ -1051,14 +1096,26 @@ void Graphics::flushStreamDraws()
 	if (sbstate.indexCount > 0)
 	{
 		usedsizes[2] = sizeof(uint16) * sbstate.indexCount;
-		size_t offset = sbstate.indexBuffer->unmap(usedsizes[2]);
 
-		sbstate.indexBufferMap = StreamBuffer::MapInfo();
+		DrawIndexedCommand cmd(&attributes, &buffers, sbstate.indexBuffer);
+		cmd.primitiveType = sbstate.primitiveMode;
+		cmd.indexCount = sbstate.indexCount;
+		cmd.indexType = INDEX_UINT16;
+		cmd.indexBufferOffset = sbstate.indexBuffer->unmap(usedsizes[2]);
+		cmd.texture = sbstate.texture;
+		draw(cmd);
 
-		drawIndexed(sbstate.primitiveMode, sbstate.indexCount, 1, INDEX_UINT16, sbstate.indexBuffer, offset, attributes, buffers, sbstate.texture);
+		sbstate.indexBufferMap = StreamBuffer::MapInfo();
 	}
 	else
-		draw(sbstate.primitiveMode, 0, sbstate.vertexCount, 1, attributes, buffers, sbstate.texture);
+	{
+		DrawCommand cmd(&attributes, &buffers);
+		cmd.primitiveType = sbstate.primitiveMode;
+		cmd.vertexStart = 0;
+		cmd.vertexCount = sbstate.vertexCount;
+		cmd.texture = sbstate.texture;
+		draw(cmd);
+	}
 
 	for (int i = 0; i < 2; i++)
 	{

+ 68 - 4
src/modules/graphics/Graphics.h

@@ -250,6 +250,54 @@ public:
 		}
 	};
 
+	struct DrawCommand
+	{
+		PrimitiveType primitiveType = PRIMITIVE_TRIANGLES;
+
+		const vertex::Attributes *attributes;
+		const vertex::Buffers *buffers;
+
+		int vertexStart = 0;
+		int vertexCount = 0;
+		int instanceCount = 1;
+
+		Texture *texture = nullptr;
+
+		// TODO: This should be moved out to a state transition API?
+		CullMode cullMode = CULL_NONE;
+
+		DrawCommand(const vertex::Attributes *attribs, const vertex::Buffers *buffers)
+			: attributes(attribs)
+			, buffers(buffers)
+		{}
+	};
+
+	struct DrawIndexedCommand
+	{
+		PrimitiveType primitiveType = PRIMITIVE_TRIANGLES;
+
+		const vertex::Attributes *attributes;
+		const vertex::Buffers *buffers;
+
+		int indexCount = 0;
+		int instanceCount = 1;
+
+		IndexDataType indexType = INDEX_UINT16;
+		Resource *indexBuffer;
+		size_t indexBufferOffset = 0;
+
+		Texture *texture = nullptr;
+
+		// TODO: This should be moved out to a state transition API?
+		CullMode cullMode = CULL_NONE;
+
+		DrawIndexedCommand(const vertex::Attributes *attribs, const vertex::Buffers *buffers, Resource *indexbuffer)
+			: attributes(attribs)
+			, buffers(buffers)
+			, indexBuffer(indexbuffer)
+		{}
+	};
+
 	struct StreamDrawCommand
 	{
 		PrimitiveType primitiveMode = PRIMITIVE_TRIANGLES;
@@ -537,8 +585,18 @@ public:
 	 * Sets whether stencil testing is enabled.
 	 **/
 	virtual void setStencilTest(CompareMode compare, int value) = 0;
-	virtual void setStencilTest() = 0;
-	void getStencilTest(CompareMode &compare, int &value);
+	void setStencilTest();
+	void getStencilTest(CompareMode &compare, int &value) const;
+
+	virtual void setDepthMode(CompareMode compare, bool write) = 0;
+	void setDepthMode();
+	void getDepthMode(CompareMode &compare, bool &write) const;
+
+	void setMeshCullMode(CullMode cull);
+	CullMode getMeshCullMode() const;
+
+	virtual void setFrontFaceWinding(vertex::Winding winding) = 0;
+	vertex::Winding getFrontFaceWinding() const;
 
 	/**
 	 * Sets the enabled color components when rendering.
@@ -769,8 +827,8 @@ public:
 	Vector2 transformPoint(Vector2 point);
 	Vector2 inverseTransformPoint(Vector2 point);
 
-	virtual void draw(PrimitiveType primtype, int vertexstart, int vertexcount, int instancecount, const vertex::Attributes &attribs, const vertex::Buffers &buffers, Texture *texture) = 0;
-	virtual void drawIndexed(PrimitiveType primtype, int indexcount, int instancecount, IndexDataType datatype, Resource *indexbuffer, size_t indexoffset, const vertex::Attributes &attribs, const vertex::Buffers &buffers, Texture *texture) = 0;
+	virtual void draw(const DrawCommand &cmd) = 0;
+	virtual void draw(const DrawIndexedCommand &cmd) = 0;
 
 	void flushStreamDraws();
 	StreamVertexData requestStreamDraw(const StreamDrawCommand &command);
@@ -852,6 +910,12 @@ protected:
 		CompareMode stencilCompare = COMPARE_ALWAYS;
 		int stencilTestValue = 0;
 
+		CompareMode depthTest = COMPARE_ALWAYS;
+		bool depthWrite = false;
+
+		CullMode meshCullMode = CULL_NONE;
+		vertex::Winding winding = vertex::WINDING_CCW;
+
 		StrongRef<Font> font;
 		StrongRef<Shader> shader;
 

+ 28 - 16
src/modules/graphics/Mesh.cpp

@@ -633,37 +633,49 @@ void Mesh::drawInstanced(Graphics *gfx, const Matrix4 &m, int instancecount)
 
 	Graphics::TempTransform transform(gfx, m);
 
-	int start = 0;
-	int count = 0;
-
 	if (useIndexBuffer && ibo != nullptr && indexCount > 0)
 	{
 		// Make sure the index buffer isn't mapped (sends data to GPU if needed.)
 		ibo->unmap();
 
-		start = std::min(std::max(0, rangeStart), (int) indexCount - 1);
+		Graphics::DrawIndexedCommand cmd(&attributes, &buffers, ibo);
+
+		cmd.primitiveType = primitiveType;
+		cmd.indexType = indexDataType;
+		cmd.instanceCount = instancecount;
+		cmd.texture = texture;
+		cmd.cullMode = gfx->getMeshCullMode();
 
-		count = (int) indexCount;
+		int start = std::min(std::max(0, rangeStart), (int) indexCount - 1);
+		cmd.indexBufferOffset = start * vertex::getIndexDataSize(indexDataType);
+
+		cmd.indexCount = (int) indexCount;
 		if (rangeCount > 0)
-			count = std::min(count, rangeCount);
+			cmd.indexCount = std::min(cmd.indexCount, rangeCount);
 
-		count = std::min(count, (int) indexCount - start);
+		cmd.indexCount = std::min(cmd.indexCount, (int) indexCount - start);
 
-		size_t offset = start * vertex::getIndexDataSize(indexDataType);
-		if (count > 0)
-			gfx->drawIndexed(primitiveType, count, instancecount, indexDataType, ibo, offset, attributes, buffers, texture);
+		if (cmd.indexCount > 0)
+			gfx->draw(cmd);
 	}
-	else
+	else if (vertexCount > 0)
 	{
-		start = std::min(std::max(0, rangeStart), (int) vertexCount - 1);
+		Graphics::DrawCommand cmd(&attributes, &buffers);
+
+		cmd.primitiveType = primitiveType;
+		cmd.vertexStart = std::min(std::max(0, rangeStart), (int) vertexCount - 1);
 
-		count = (int) vertexCount;
+		cmd.vertexCount = (int) vertexCount;
 		if (rangeCount > 0)
-			count = std::min(count, rangeCount);
+			cmd.vertexCount = std::min(cmd.vertexCount, rangeCount);
 
-		count = std::min(count, (int) vertexCount - start);
+		cmd.vertexCount = std::min(cmd.vertexCount, (int) vertexCount - cmd.vertexStart);
+		cmd.instanceCount = instancecount;
+		cmd.texture = texture;
+		cmd.cullMode = gfx->getMeshCullMode();
 
-		gfx->draw(primitiveType, start, count, instancecount, attributes, buffers, texture);
+		if (cmd.vertexCount > 0)
+			gfx->draw(cmd);
 	}
 }
 

+ 7 - 2
src/modules/graphics/ParticleSystem.cpp

@@ -1101,8 +1101,13 @@ void ParticleSystem::draw(Graphics *gfx, const Matrix4 &m)
 
 	Graphics::TempTransform transform(gfx, m);
 
-	int count = quadIndices.getIndexCount(getCount());
-	gfx->drawIndexed(PRIMITIVE_TRIANGLES, count, 1, quadIndices.getType(), quadIndices.getBuffer(), 0, vertexAttributes, vertexbuffers, texture);
+	Graphics::DrawIndexedCommand cmd(&vertexAttributes, &vertexbuffers, quadIndices.getBuffer());
+	cmd.primitiveType = PRIMITIVE_TRIANGLES;
+	cmd.indexBufferOffset = 0;
+	cmd.indexCount = (int) quadIndices.getIndexCount(pCount);
+	cmd.indexType = quadIndices.getType();
+	cmd.texture = texture;
+	gfx->draw(cmd);
 }
 
 bool ParticleSystem::getConstant(const char *in, AreaSpreadDistribution &out)

+ 7 - 3
src/modules/graphics/SpriteBatch.cpp

@@ -385,6 +385,8 @@ void SpriteBatch::draw(Graphics *gfx, const Matrix4 &m)
 		}
 	}
 
+	Graphics::DrawIndexedCommand cmd(&attributes, &buffers, quad_indices.getBuffer());
+
 	int start = std::min(std::max(0, range_start), next - 1);
 
 	int count = next;
@@ -393,13 +395,15 @@ void SpriteBatch::draw(Graphics *gfx, const Matrix4 &m)
 
 	count = std::min(count, next - start);
 
-	size_t indexbytestart = quad_indices.getIndexCount(start) * quad_indices.getElementSize();
-	size_t indexcount = quad_indices.getIndexCount(count);
+	cmd.indexBufferOffset = quad_indices.getIndexCount(start) * quad_indices.getElementSize();
+	cmd.indexCount = (int) quad_indices.getIndexCount(count);
+	cmd.indexType = quad_indices.getType();
+	cmd.texture = texture;
 
 	Graphics::TempTransform transform(gfx, m);
 
 	if (count > 0)
-		gfx->drawIndexed(PRIMITIVE_TRIANGLES, indexcount, 1, quad_indices.getType(), quad_indices.getBuffer(), indexbytestart, attributes, buffers, texture);
+		gfx->draw(cmd);
 }
 
 } // graphics

+ 6 - 5
src/modules/graphics/Text.cpp

@@ -266,15 +266,16 @@ void Text::draw(Graphics *gfx, const Matrix4 &m)
 
 	Graphics::TempTransform transform(gfx, m);
 
+	Graphics::DrawIndexedCommand drawcmd(&vertexAttributes, &vertexBuffers, quadIndices.getBuffer());
+	drawcmd.indexType = quadIndices.getType();
 	size_t elemsize = quadIndices.getElementSize();
-	IndexDataType datatype = quadIndices.getType();
-	Buffer *indexbuffer = quadIndices.getBuffer();
 
 	for (const Font::DrawCommand &cmd : draw_commands)
 	{
-		int count = (cmd.vertexcount / 4) * 6;
-		size_t offset = (cmd.startvertex / 4) * 6 * elemsize;
-		gfx->drawIndexed(PRIMITIVE_TRIANGLES, count, 1, datatype, indexbuffer, offset, vertexAttributes, vertexBuffers, cmd.texture);
+		drawcmd.indexCount = (cmd.vertexcount / 4) * 6;
+		drawcmd.indexBufferOffset = (cmd.startvertex / 4) * 6 * elemsize;
+		drawcmd.texture = cmd.texture;
+		gfx->draw(drawcmd);
 	}
 }
 

+ 2 - 8
src/modules/graphics/depthstencil.cpp

@@ -34,19 +34,12 @@ CompareMode getReversedCompareMode(CompareMode mode)
 		return COMPARE_GREATER;
 	case COMPARE_LEQUAL:
 		return COMPARE_GEQUAL;
-	case COMPARE_EQUAL:
-		return COMPARE_EQUAL;
 	case COMPARE_GEQUAL:
 		return COMPARE_LEQUAL;
 	case COMPARE_GREATER:
 		return COMPARE_LESS;
-	case COMPARE_NOTEQUAL:
-		return COMPARE_NOTEQUAL;
-	case COMPARE_ALWAYS:
-		return COMPARE_ALWAYS;
-	case COMPARE_MAX_ENUM:
 	default:
-		return COMPARE_MAX_ENUM;
+		return mode;
 	}
 }
 
@@ -71,6 +64,7 @@ static StringMap<CompareMode, COMPARE_MAX_ENUM>::Entry compareModeEntries[] =
 	{ "greater",  COMPARE_GREATER  },
 	{ "notequal", COMPARE_NOTEQUAL },
 	{ "always",   COMPARE_ALWAYS   },
+	{ "never",    COMPARE_NEVER    },
 };
 
 static StringMap<CompareMode, COMPARE_MAX_ENUM> compareModes(compareModeEntries, sizeof(compareModeEntries));

+ 1 - 0
src/modules/graphics/depthstencil.h

@@ -48,6 +48,7 @@ enum CompareMode
 	COMPARE_GREATER,
 	COMPARE_NOTEQUAL,
 	COMPARE_ALWAYS,
+	COMPARE_NEVER,
 	COMPARE_MAX_ENUM
 };
 

+ 14 - 0
src/modules/graphics/opengl/Canvas.cpp

@@ -68,9 +68,16 @@ static GLenum createFBO(GLuint &framebuffer, TextureType texType, PixelFormat fo
 
 					if (isPixelFormatDepthStencil(format))
 					{
+						bool hadDepthWrites = gl.hasDepthWrites();
+						if (!hadDepthWrites) // glDepthMask also affects glClear.
+							gl.setDepthWrites(true);
+
 						gl.clearDepth(1.0);
 						glClearStencil(0);
 						glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
+
+						if (!hadDepthWrites)
+							gl.setDepthWrites(hadDepthWrites);
 					}
 					else
 					{
@@ -128,9 +135,16 @@ static bool createRenderbuffer(int width, int height, int &samples, PixelFormat
 	{
 		if (isPixelFormatDepthStencil(pixelformat))
 		{
+			bool hadDepthWrites = gl.hasDepthWrites();
+			if (!hadDepthWrites) // glDepthMask also affects glClear.
+				gl.setDepthWrites(true);
+
 			gl.clearDepth(1.0);
 			glClearStencil(0);
 			glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
+
+			if (!hadDepthWrites)
+				gl.setDepthWrites(hadDepthWrites);
 		}
 		else
 		{

+ 104 - 42
src/modules/graphics/opengl/Graphics.cpp

@@ -149,7 +149,7 @@ void Graphics::setViewportSize(int width, int height, int pixelwidth, int pixelh
 			setScissor(states.back().scissorRect);
 
 		// Set up the projection matrix
-		projectionMatrix = Matrix4::ortho(0.0, (float) width, (float) height, 0.0);
+		projectionMatrix = Matrix4::ortho(0.0, (float) width, (float) height, 0.0, -10.0f, 10.0f);
 	}
 }
 
@@ -206,7 +206,7 @@ bool Graphics::setMode(int width, int height, int pixelwidth, int pixelheight, b
 		|| GLAD_ES_VERSION_3_0 || GLAD_EXT_sRGB)
 	{
 		if (GLAD_VERSION_1_0 || GLAD_EXT_sRGB_write_control)
-			gl.setFramebufferSRGB(isGammaCorrect());
+			gl.setEnableState(OpenGL::ENABLE_FRAMEBUFFER_SRGB, isGammaCorrect());
 	}
 	else
 		setGammaCorrect(false);
@@ -316,38 +316,40 @@ void Graphics::setActive(bool enable)
 	active = enable;
 }
 
-void Graphics::draw(PrimitiveType primtype, int vertexstart, int vertexcount, int instancecount, const vertex::Attributes &attribs, const vertex::Buffers &buffers, love::graphics::Texture *texture)
+void Graphics::draw(const DrawCommand &cmd)
 {
 	gl.prepareDraw();
-	gl.setVertexAttributes(attribs, buffers);
-	gl.bindTextureToUnit(texture, 0, false);
+	gl.setVertexAttributes(*cmd.attributes, *cmd.buffers);
+	gl.bindTextureToUnit(cmd.texture, 0, false);
+	gl.setCullMode(cmd.cullMode);
 
-	GLenum glprimitivetype = OpenGL::getGLPrimitiveType(primtype);
+	GLenum glprimitivetype = OpenGL::getGLPrimitiveType(cmd.primitiveType);
 
-	if (instancecount > 1)
-		glDrawArraysInstanced(glprimitivetype, vertexstart, vertexcount, instancecount);
+	if (cmd.instanceCount > 1)
+		glDrawArraysInstanced(glprimitivetype, cmd.vertexStart, cmd.vertexCount, cmd.instanceCount);
 	else
-		glDrawArrays(glprimitivetype, vertexstart, vertexcount);
+		glDrawArrays(glprimitivetype, cmd.vertexStart, cmd.vertexCount);
 
 	++drawCalls;
 }
 
-void Graphics::drawIndexed(PrimitiveType primtype, int indexcount, int instancecount, IndexDataType datatype, Resource *indexbuffer, size_t indexoffset, const vertex::Attributes &attribs, const vertex::Buffers &buffers, love::graphics::Texture *texture)
+void Graphics::draw(const DrawIndexedCommand &cmd)
 {
 	gl.prepareDraw();
-	gl.setVertexAttributes(attribs, buffers);
-	gl.bindTextureToUnit(texture, 0, false);
+	gl.setVertexAttributes(*cmd.attributes, *cmd.buffers);
+	gl.bindTextureToUnit(cmd.texture, 0, false);
+	gl.setCullMode(cmd.cullMode);
 
-	const void *gloffset = BUFFER_OFFSET(indexoffset);
-	GLenum glprimitivetype = OpenGL::getGLPrimitiveType(primtype);
-	GLenum gldatatype = OpenGL::getGLIndexDataType(datatype);
+	const void *gloffset = BUFFER_OFFSET(cmd.indexBufferOffset);
+	GLenum glprimitivetype = OpenGL::getGLPrimitiveType(cmd.primitiveType);
+	GLenum gldatatype = OpenGL::getGLIndexDataType(cmd.indexType);
 
-	gl.bindBuffer(BUFFER_INDEX, indexbuffer->getHandle());
+	gl.bindBuffer(BUFFER_INDEX, cmd.indexBuffer->getHandle());
 
-	if (instancecount > 1)
-		glDrawElementsInstanced(glprimitivetype, indexcount, gldatatype, gloffset, instancecount);
+	if (cmd.instanceCount > 1)
+		glDrawElementsInstanced(glprimitivetype, cmd.indexCount, gldatatype, gloffset, cmd.instanceCount);
 	else
-		glDrawElements(glprimitivetype, indexcount, gldatatype, gloffset);
+		glDrawElements(glprimitivetype, cmd.indexCount, gldatatype, gloffset);
 
 	++drawCalls;
 }
@@ -424,20 +426,22 @@ void Graphics::setCanvasInternal(const RenderTargets &rts, int w, int h, int pix
 
 	gl.setViewport({0, 0, pixelw, pixelh});
 
+	// Flip front face winding when rendering to a canvas, since our projection
+	// matrix is flipped.
+	glFrontFace(state.winding == vertex::WINDING_CW ? GL_CCW : GL_CW);
+
 	// Re-apply the scissor if it was active, since the rectangle passed to
 	// glScissor is affected by the viewport dimensions.
 	if (state.scissor)
 		setScissor(state.scissorRect);
 
-	projectionMatrix = Matrix4::ortho(0.0, (float) w, 0.0, (float) h);
+	projectionMatrix = Matrix4::ortho(0.0, (float) w, 0.0, (float) h, -10.0f, 10.0f);
 
 	// Make sure the correct sRGB setting is used when drawing to the canvases.
 	if (GLAD_VERSION_1_0 || GLAD_EXT_sRGB_write_control)
 	{
-		if (hasSRGBcanvas && !gl.hasFramebufferSRGB())
-			gl.setFramebufferSRGB(true);
-		else if (!hasSRGBcanvas && gl.hasFramebufferSRGB())
-			gl.setFramebufferSRGB(false);
+		if (hasSRGBcanvas != gl.isStateEnabled(OpenGL::ENABLE_FRAMEBUFFER_SRGB))
+			gl.setEnableState(OpenGL::ENABLE_FRAMEBUFFER_SRGB, hasSRGBcanvas);
 	}
 }
 
@@ -453,6 +457,10 @@ void Graphics::setCanvas()
 	flushStreamDraws();
 	endPass();
 
+	// Re-apply the correct front face winding, since it may have been flipped
+	// if we were previously rendering to a canvas.
+	glFrontFace(state.winding == vertex::WINDING_CW ? GL_CW : GL_CCW);
+
 	state.renderTargets = RenderTargetsStrongRef();
 
 	gl.bindFramebuffer(OpenGL::FRAMEBUFFER_ALL, gl.getDefaultFBO());
@@ -466,14 +474,12 @@ void Graphics::setCanvas()
 
 	// The projection matrix is flipped compared to rendering to a canvas, due
 	// to OpenGL considering (0,0) bottom-left instead of top-left.
-	projectionMatrix = Matrix4::ortho(0.0, (float) width, (float) height, 0.0);
+	projectionMatrix = Matrix4::ortho(0.0, (float) width, (float) height, 0.0, -10.0f, 10.0f);
 
 	if (GLAD_VERSION_1_0 || GLAD_EXT_sRGB_write_control)
 	{
-		if (isGammaCorrect() && !gl.hasFramebufferSRGB())
-			gl.setFramebufferSRGB(true);
-		else if (!isGammaCorrect() && gl.hasFramebufferSRGB())
-			gl.setFramebufferSRGB(false);
+		if (isGammaCorrect() != gl.isStateEnabled(OpenGL::ENABLE_FRAMEBUFFER_SRGB))
+			gl.setEnableState(OpenGL::ENABLE_FRAMEBUFFER_SRGB, isGammaCorrect());
 	}
 
 	canvasSwitchCount++;
@@ -571,8 +577,13 @@ void Graphics::clear(OptionalColorf c, OptionalInt stencil, OptionalDouble depth
 		flags |= GL_STENCIL_BUFFER_BIT;
 	}
 
+	bool hadDepthWrites = gl.hasDepthWrites();
+
 	if (depth.hasValue)
 	{
+		if (!hadDepthWrites) // glDepthMask also affects glClear.
+			gl.setDepthWrites(true);
+
 		gl.clearDepth(depth.value);
 		flags |= GL_DEPTH_BUFFER_BIT;
 	}
@@ -580,6 +591,9 @@ void Graphics::clear(OptionalColorf c, OptionalInt stencil, OptionalDouble depth
 	if (flags != 0)
 		glClear(flags);
 
+	if (depth.hasValue && !hadDepthWrites)
+		gl.setDepthWrites(hadDepthWrites);
+
 	if (c.hasValue && gl.bugs.clearRequiresDriverTextureStateUpdate && Shader::current)
 	{
 		// This seems to be enough to fix the bug for me. Other methods I've
@@ -652,8 +666,13 @@ void Graphics::clear(const std::vector<OptionalColorf> &colors, OptionalInt sten
 		flags |= GL_STENCIL_BUFFER_BIT;
 	}
 
+	bool hadDepthWrites = gl.hasDepthWrites();
+
 	if (depth.hasValue)
 	{
+		if (!hadDepthWrites) // glDepthMask also affects glClear.
+			gl.setDepthWrites(true);
+
 		gl.clearDepth(depth.value);
 		flags |= GL_DEPTH_BUFFER_BIT;
 	}
@@ -661,6 +680,9 @@ void Graphics::clear(const std::vector<OptionalColorf> &colors, OptionalInt sten
 	if (flags != 0)
 		glClear(flags);
 
+	if (depth.hasValue && !hadDepthWrites)
+		gl.setDepthWrites(hadDepthWrites);
+
 	if (gl.bugs.clearRequiresDriverTextureStateUpdate && Shader::current)
 	{
 		// This seems to be enough to fix the bug for me. Other methods I've
@@ -986,7 +1008,8 @@ void Graphics::setScissor(const Rect &rect)
 
 	DisplayState &state = states.back();
 
-	glEnable(GL_SCISSOR_TEST);
+	if (!gl.isStateEnabled(OpenGL::ENABLE_SCISSOR_TEST))
+		gl.setEnableState(OpenGL::ENABLE_SCISSOR_TEST, true);
 
 	double dpiscale = getCurrentDPIScale();
 
@@ -1009,7 +1032,9 @@ void Graphics::setScissor()
 		flushStreamDraws();
 
 	states.back().scissor = false;
-	glDisable(GL_SCISSOR_TEST);
+
+	if (gl.isStateEnabled(OpenGL::ENABLE_SCISSOR_TEST))
+		gl.setEnableState(OpenGL::ENABLE_SCISSOR_TEST, false);
 }
 
 void Graphics::drawToStencilBuffer(StencilAction action, int value)
@@ -1055,7 +1080,9 @@ void Graphics::drawToStencilBuffer(StencilAction action, int value)
 	}
 
 	// The stencil test must be enabled in order to write to the stencil buffer.
-	glEnable(GL_STENCIL_TEST);
+	if (!gl.isStateEnabled(OpenGL::ENABLE_STENCIL_TEST))
+		gl.setEnableState(OpenGL::ENABLE_STENCIL_TEST, true);
+
 	glStencilFunc(GL_ALWAYS, value, 0xFFFFFFFF);
 	glStencilOp(GL_KEEP, GL_KEEP, glaction);
 }
@@ -1093,7 +1120,8 @@ void Graphics::setStencilTest(CompareMode compare, int value)
 
 	if (compare == COMPARE_ALWAYS)
 	{
-		glDisable(GL_STENCIL_TEST);
+		if (gl.isStateEnabled(OpenGL::ENABLE_STENCIL_TEST))
+			gl.setEnableState(OpenGL::ENABLE_STENCIL_TEST, false);
 		return;
 	}
 
@@ -1107,14 +1135,48 @@ void Graphics::setStencilTest(CompareMode compare, int value)
 	 **/
 	GLenum glcompare = OpenGL::getGLCompareMode(getReversedCompareMode(compare));
 
-	glEnable(GL_STENCIL_TEST);
+	if (!gl.isStateEnabled(OpenGL::ENABLE_STENCIL_TEST))
+		gl.setEnableState(OpenGL::ENABLE_STENCIL_TEST, true);
+
 	glStencilFunc(glcompare, value, 0xFFFFFFFF);
 	glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
 }
 
-void Graphics::setStencilTest()
+void Graphics::setDepthMode(CompareMode compare, bool write)
+{
+	DisplayState &state = states.back();
+
+	if (state.depthTest != compare || state.depthWrite != write)
+		flushStreamDraws();
+
+	state.depthTest = compare;
+	state.depthWrite = write;
+
+	bool depthenable = compare != COMPARE_ALWAYS || write;
+
+	if (depthenable != gl.isStateEnabled(OpenGL::ENABLE_DEPTH_TEST))
+		gl.setEnableState(OpenGL::ENABLE_DEPTH_TEST, depthenable);
+
+	if (depthenable)
+	{
+		glDepthFunc(OpenGL::getGLCompareMode(compare));
+		glDepthMask(write ? GL_TRUE : GL_FALSE);
+	}
+}
+
+void Graphics::setFrontFaceWinding(vertex::Winding winding)
 {
-	setStencilTest(COMPARE_ALWAYS, 0);
+	DisplayState &state = states.back();
+
+	if (state.winding != winding)
+		flushStreamDraws();
+
+	state.winding = winding;
+
+	if (isCanvasActive())
+		winding = winding == vertex::WINDING_CW ? vertex::WINDING_CCW : vertex::WINDING_CW;
+
+	glFrontFace(winding == vertex::WINDING_CW ? GL_CW : GL_CCW);
 }
 
 void Graphics::setColor(Colorf c)
@@ -1142,12 +1204,6 @@ void Graphics::setBlendMode(BlendMode mode, BlendAlpha alphamode)
 	if (mode != states.back().blendMode || alphamode != states.back().blendAlphaMode)
 		flushStreamDraws();
 
-	GLenum func   = GL_FUNC_ADD;
-	GLenum srcRGB = GL_ONE;
-	GLenum srcA   = GL_ONE;
-	GLenum dstRGB = GL_ZERO;
-	GLenum dstA   = GL_ZERO;
-
 	if (mode == BLEND_LIGHTEN || mode == BLEND_DARKEN)
 	{
 		if (!capabilities.features[FEATURE_LIGHTEN])
@@ -1170,6 +1226,12 @@ void Graphics::setBlendMode(BlendMode mode, BlendAlpha alphamode)
 		}
 	}
 
+	GLenum func   = GL_FUNC_ADD;
+	GLenum srcRGB = GL_ONE;
+	GLenum srcA   = GL_ONE;
+	GLenum dstRGB = GL_ZERO;
+	GLenum dstA   = GL_ZERO;
+
 	switch (mode)
 	{
 	case BLEND_ALPHA:

+ 6 - 3
src/modules/graphics/opengl/Graphics.h

@@ -69,8 +69,8 @@ public:
 
 	void setActive(bool active) override;
 
-	void draw(PrimitiveType primtype, int vertexstart, int vertexcount, int instancecount, const vertex::Attributes &attribs, const vertex::Buffers &buffers, Texture *texture) override;
-	void drawIndexed(PrimitiveType primtype, int indexcount, int instancecount, IndexDataType datatype, Resource *indexbuffer, size_t indexoffset, const vertex::Attributes &attribs, const vertex::Buffers &buffers, Texture *texture) override;
+	void draw(const DrawCommand &cmd) override;
+	void draw(const DrawIndexedCommand &cmd) override;
 
 	void clear(OptionalColorf color, OptionalInt stencil, OptionalDouble depth) override;
 	void clear(const std::vector<OptionalColorf> &colors, OptionalInt stencil, OptionalDouble depth) override;
@@ -90,7 +90,10 @@ public:
 	void stopDrawToStencilBuffer() override;
 
 	void setStencilTest(CompareMode compare, int value) override;
-	void setStencilTest() override;
+
+	void setDepthMode(CompareMode compare, bool write) override;
+
+	void setFrontFaceWinding(vertex::Winding winding) override;
 
 	void setColorMask(ColorMask mask) override;
 

+ 73 - 9
src/modules/graphics/opengl/OpenGL.cpp

@@ -191,13 +191,22 @@ void OpenGL::setupContext()
 		state.boundFramebuffers[i] = std::numeric_limits<GLuint>::max();
 	bindFramebuffer(FRAMEBUFFER_ALL, getDefaultFBO());
 
+	setEnableState(ENABLE_DEPTH_TEST, state.enableState[ENABLE_DEPTH_TEST]);
+	setEnableState(ENABLE_STENCIL_TEST, state.enableState[ENABLE_STENCIL_TEST]);
+	setEnableState(ENABLE_SCISSOR_TEST, state.enableState[ENABLE_SCISSOR_TEST]);
+	setEnableState(ENABLE_FACE_CULL, state.enableState[ENABLE_FACE_CULL]);
+
 	if (GLAD_VERSION_3_0 || GLAD_ARB_framebuffer_sRGB || GLAD_EXT_framebuffer_sRGB
 		|| GLAD_EXT_sRGB_write_control)
 	{
-		state.framebufferSRGBEnabled = (glIsEnabled(GL_FRAMEBUFFER_SRGB) == GL_TRUE);
+		setEnableState(ENABLE_FRAMEBUFFER_SRGB, state.enableState[ENABLE_FRAMEBUFFER_SRGB]);
 	}
 	else
-		state.framebufferSRGBEnabled = false;
+		state.enableState[ENABLE_FRAMEBUFFER_SRGB] = false;
+
+	GLint faceCull = GL_BACK;
+	glGetIntegerv(GL_CULL_FACE_MODE, &faceCull);
+	state.faceCullMode = faceCull;
 
 	for (int i = 0; i < (int) BUFFER_MAX_ENUM; i++)
 	{
@@ -228,6 +237,8 @@ void OpenGL::setupContext()
 	glActiveTexture(GL_TEXTURE0);
 	state.curTextureUnit = 0;
 
+	setDepthWrites(state.depthWritesEnabled);
+
 	createDefaultTexture();
 
 	contextInitialized = true;
@@ -673,6 +684,24 @@ void OpenGL::setVertexAttributes(const vertex::Attributes &attributes, const ver
 		glVertexAttrib4f(ATTRIB_COLOR, 1.0f, 1.0f, 1.0f, 1.0f);
 }
 
+void OpenGL::setCullMode(CullMode mode)
+{
+	bool enabled = mode != CULL_NONE;
+
+	if (enabled != isStateEnabled(ENABLE_FACE_CULL))
+		setEnableState(ENABLE_FACE_CULL, enabled);
+
+	if (enabled)
+	{
+		GLenum glmode = mode == CULL_BACK ? GL_BACK : GL_FRONT;
+		if (glmode != state.faceCullMode)
+		{
+			glCullFace(glmode);
+			state.faceCullMode = glmode;
+		}
+	}
+}
+
 void OpenGL::clearDepth(double value)
 {
 	if (GLAD_ES_VERSION_2_0)
@@ -729,19 +758,42 @@ float OpenGL::getPointSize() const
 	return state.pointSize;
 }
 
-void OpenGL::setFramebufferSRGB(bool enable)
+void OpenGL::setEnableState(EnableState enablestate, bool enable)
 {
+	GLenum glstate = GL_NONE;
+
+	switch (enablestate)
+	{
+	case ENABLE_DEPTH_TEST:
+		glstate = GL_DEPTH_TEST;
+		break;
+	case ENABLE_STENCIL_TEST:
+		glstate = GL_STENCIL_TEST;
+		break;
+	case ENABLE_SCISSOR_TEST:
+		glstate = GL_SCISSOR_TEST;
+		break;
+	case ENABLE_FACE_CULL:
+		glstate = GL_CULL_FACE;
+		break;
+	case ENABLE_FRAMEBUFFER_SRGB:
+		glstate = GL_FRAMEBUFFER_SRGB;
+		break;
+	case ENABLE_MAX_ENUM:
+		break;
+	}
+
 	if (enable)
-		glEnable(GL_FRAMEBUFFER_SRGB);
+		glEnable(glstate);
 	else
-		glDisable(GL_FRAMEBUFFER_SRGB);
+		glDisable(glstate);
 
-	state.framebufferSRGBEnabled = enable;
+	state.enableState[enablestate] = enable;
 }
 
-bool OpenGL::hasFramebufferSRGB() const
+bool OpenGL::isStateEnabled(EnableState enablestate) const
 {
-	return state.framebufferSRGBEnabled;
+	return state.enableState[enablestate];
 }
 
 void OpenGL::bindFramebuffer(FramebufferTarget target, GLuint framebuffer)
@@ -816,6 +868,17 @@ void OpenGL::framebufferTexture(GLenum attachment, TextureType texType, GLuint t
 	}
 }
 
+void OpenGL::setDepthWrites(bool enable)
+{
+	glDepthMask(enable ? GL_TRUE : GL_FALSE);
+	state.depthWritesEnabled = enable;
+}
+
+bool OpenGL::hasDepthWrites() const
+{
+	return state.depthWritesEnabled;
+}
+
 void OpenGL::useProgram(GLuint program)
 {
 	glUseProgram(program);
@@ -954,7 +1017,6 @@ GLint OpenGL::getGLWrapMode(Texture::WrapMode wmode)
 	case Texture::WRAP_MIRRORED_REPEAT:
 		return GL_MIRRORED_REPEAT;
 	}
-
 }
 
 GLint OpenGL::getGLCompareMode(CompareMode mode)
@@ -975,6 +1037,8 @@ GLint OpenGL::getGLCompareMode(CompareMode mode)
 		return GL_NOTEQUAL;
 	case COMPARE_ALWAYS:
 		return GL_ALWAYS;
+	case COMPARE_NEVER:
+		return GL_NEVER;
 	default:
 		return GL_NEVER;
 	}

+ 30 - 6
src/modules/graphics/opengl/OpenGL.h

@@ -92,6 +92,16 @@ public:
 		FRAMEBUFFER_ALL  = (FRAMEBUFFER_READ | FRAMEBUFFER_DRAW),
 	};
 
+	enum EnableState
+	{
+		ENABLE_DEPTH_TEST,
+		ENABLE_STENCIL_TEST,
+		ENABLE_SCISSOR_TEST,
+		ENABLE_FACE_CULL,
+		ENABLE_FRAMEBUFFER_SRGB,
+		ENABLE_MAX_ENUM
+	};
+
 	struct TextureFormat
 	{
 		GLenum internalformat = 0;
@@ -204,6 +214,11 @@ public:
 	 **/
 	void setVertexAttributes(const vertex::Attributes &attributes, const vertex::Buffers &buffers);
 
+	/**
+	 * Wrapper for glCullFace which eliminates redundant state setting.
+	 **/
+	void setCullMode(CullMode mode);
+
 	/**
 	 * Wrapper for glClearDepth and glClearDepthf.
 	 **/
@@ -236,10 +251,10 @@ public:
 	float getPointSize() const;
 
 	/**
-	 * Calls glEnable/glDisable(GL_FRAMEBUFFER_SRGB).
+	 * State-tracked version of glEnable.
 	 **/
-	void setFramebufferSRGB(bool enable);
-	bool hasFramebufferSRGB() const;
+	void setEnableState(EnableState state, bool enable);
+	bool isStateEnabled(EnableState state) const;
 
 	/**
 	 * Binds a Framebuffer Object to the specified target.
@@ -250,6 +265,12 @@ public:
 
 	void framebufferTexture(GLenum attachment, TextureType texType, GLuint texture, int level, int layer = 0, int face = 0);
 
+	/**
+	 * Calls glDepthMask.
+	 **/
+	void setDepthWrites(bool enable);
+	bool hasDepthWrites() const;
+
 	/**
 	 * Calls glUseProgram.
 	 **/
@@ -415,7 +436,10 @@ private:
 		// Texture unit state (currently bound texture for each texture unit.)
 		std::vector<GLuint> boundTextures[TEXTURE_MAX_ENUM];
 
-		// Currently active texture unit.
+		bool enableState[ENABLE_MAX_ENUM];
+
+		GLenum faceCullMode;
+
 		int curTextureUnit;
 
 		uint32 enabledAttribArrays;
@@ -429,9 +453,9 @@ private:
 
 		float pointSize;
 
-		GLuint boundFramebuffers[2];
+		bool depthWritesEnabled;
 
-		bool framebufferSRGBEnabled;
+		GLuint boundFramebuffers[2];
 
 		GLuint defaultTexture[TEXTURE_MAX_ENUM];
 

+ 47 - 0
src/modules/graphics/vertex.cpp

@@ -327,6 +327,23 @@ static StringMap<DataType, DATA_MAX_ENUM>::Entry dataTypeEntries[] =
 
 static StringMap<DataType, DATA_MAX_ENUM> dataTypes(dataTypeEntries, sizeof(dataTypeEntries));
 
+static StringMap<CullMode, CULL_MAX_ENUM>::Entry cullModeEntries[] =
+{
+	{ "none",  CULL_NONE  },
+	{ "back",  CULL_BACK  },
+	{ "front", CULL_FRONT },
+};
+
+static StringMap<CullMode, CULL_MAX_ENUM> cullModes(cullModeEntries, sizeof(cullModeEntries));
+
+static StringMap<Winding, WINDING_MAX_ENUM>::Entry windingEntries[] =
+{
+	{ "cw",  WINDING_CW  },
+	{ "ccw", WINDING_CCW },
+};
+
+static StringMap<Winding, WINDING_MAX_ENUM> windings(windingEntries, sizeof(windingEntries));
+
 bool getConstant(const char *in, VertexAttribID &out)
 {
 	return attribNames.find(in, out);
@@ -412,6 +429,36 @@ std::vector<std::string> getConstants(DataType)
 	return dataTypes.getNames();
 }
 
+bool getConstant(const char *in, CullMode &out)
+{
+	return cullModes.find(in, out);
+}
+
+bool getConstant(CullMode in, const char *&out)
+{
+	return cullModes.find(in, out);
+}
+
+std::vector<std::string> getConstants(CullMode)
+{
+	return cullModes.getNames();
+}
+
+bool getConstant(const char *in, Winding &out)
+{
+	return windings.find(in, out);
+}
+
+bool getConstant(Winding in, const char *&out)
+{
+	return windings.find(in, out);
+}
+
+std::vector<std::string> getConstants(Winding)
+{
+	return windings.getNames();
+}
+
 } // vertex
 } // graphics
 } // love

+ 23 - 0
src/modules/graphics/vertex.h

@@ -86,6 +86,14 @@ enum AttributeStep
 	STEP_MAX_ENUM
 };
 
+enum CullMode
+{
+	CULL_NONE,
+	CULL_BACK,
+	CULL_FRONT,
+	CULL_MAX_ENUM
+};
+
 namespace vertex
 {
 
@@ -106,6 +114,13 @@ enum DataType
 	DATA_MAX_ENUM
 };
 
+enum Winding
+{
+	WINDING_CW,
+	WINDING_CCW,
+	WINDING_MAX_ENUM
+};
+
 enum class TriangleIndexMode
 {
 	NONE,
@@ -305,6 +320,14 @@ bool getConstant(const char *in, DataType &out);
 bool getConstant(DataType in, const char *&out);
 std::vector<std::string> getConstants(DataType);
 
+bool getConstant(const char *in, CullMode &out);
+bool getConstant(CullMode in, const char *&out);
+std::vector<std::string> getConstants(CullMode);
+
+bool getConstant(const char *in, Winding &out);
+bool getConstant(Winding in, const char *&out);
+std::vector<std::string> getConstants(Winding);
+
 } // vertex
 
 typedef vertex::XYf_STf_RGBAub Vertex;

+ 84 - 0
src/modules/graphics/wrap_Graphics.cpp

@@ -1962,6 +1962,84 @@ int w_getPointSize(lua_State *L)
 	return 1;
 }
 
+int w_setDepthMode(lua_State *L)
+{
+	if (lua_isnoneornil(L, 1) && lua_isnoneornil(L, 2))
+		luax_catchexcept(L, [&]() { instance()->setDepthMode(); });
+	else
+	{
+		CompareMode compare = COMPARE_ALWAYS;
+		const char *str = luaL_checkstring(L, 1);
+		bool write = luax_checkboolean(L, 2);
+
+		if (!getConstant(str, compare))
+			return luax_enumerror(L, "compare mode", getConstants(compare), str);
+
+		luax_catchexcept(L, [&]() { instance()->setDepthMode(compare, write); });
+	}
+
+	return 0;
+}
+
+int w_getDepthMode(lua_State *L)
+{
+	CompareMode compare = COMPARE_ALWAYS;
+	bool write = false;
+	instance()->getDepthMode(compare, write);
+
+	const char *str;
+	if (!getConstant(compare, str))
+		return luaL_error(L, "Unknown compare mode");
+
+	lua_pushstring(L, str);
+	luax_pushboolean(L, write);
+	return 2;
+}
+
+int w_setMeshCullMode(lua_State *L)
+{
+	const char *str = luaL_checkstring(L, 1);
+	CullMode mode;
+
+	if (!vertex::getConstant(str, mode))
+		return luax_enumerror(L, "cull mode", vertex::getConstants(mode), str);
+
+	luax_catchexcept(L, [&]() { instance()->setMeshCullMode(mode); });
+	return 0;
+}
+
+int w_getMeshCullMode(lua_State *L)
+{
+	CullMode mode = instance()->getMeshCullMode();
+	const char *str;
+	if (!vertex::getConstant(mode, str))
+		return luaL_error(L, "Unknown cull mode");
+	lua_pushstring(L, str);
+	return 1;
+}
+
+int w_setFrontFaceWinding(lua_State *L)
+{
+	const char *str = luaL_checkstring(L, 1);
+	vertex::Winding winding;
+
+	if (!vertex::getConstant(str, winding))
+		return luax_enumerror(L, "vertex winding", vertex::getConstants(winding), str);
+
+	luax_catchexcept(L, [&]() { instance()->setFrontFaceWinding(winding); });
+	return 0;
+}
+
+int w_getFrontFaceWinding(lua_State *L)
+{
+	vertex::Winding winding = instance()->getFrontFaceWinding();
+	const char *str;
+	if (!vertex::getConstant(winding, str))
+		return luaL_error(L, "Unknown vertex winding");
+	lua_pushstring(L, str);
+	return 1;
+}
+
 int w_setWireframe(lua_State *L)
 {
 	instance()->setWireframe(luax_checkboolean(L, 1));
@@ -2858,6 +2936,12 @@ static const luaL_Reg functions[] =
 	{ "getLineJoin", w_getLineJoin },
 	{ "setPointSize", w_setPointSize },
 	{ "getPointSize", w_getPointSize },
+	{ "setDepthMode", w_setDepthMode },
+	{ "getDepthMode", w_getDepthMode },
+	{ "setMeshCullMode", w_setMeshCullMode },
+	{ "getMeshCullMode", w_getMeshCullMode },
+	{ "setFrontFaceWinding", w_setFrontFaceWinding },
+	{ "getFrontFaceWinding", w_getFrontFaceWinding },
 	{ "setWireframe", w_setWireframe },
 	{ "isWireframe", w_isWireframe },