Browse Source

Add a new “stencil8” pixel format for Canvases (resolves issue #1003).

Also fixed Canvas MSAA (resolves issue #1271).

stencil-formatted Canvases can’t be drawn, and can only be used as the value for a new ‘depthstencil’ field to the table-argument variant of love.graphics.setCanvas.

--HG--
branch : minor
Alex Szpakowski 8 years ago
parent
commit
018a3831cb

+ 8 - 0
src/common/pixelformat.cpp

@@ -52,6 +52,8 @@ static StringMap<PixelFormat, PIXELFORMAT_MAX_ENUM>::Entry formatEntries[] =
 	{ "rgb565",   PIXELFORMAT_RGB565   },
 	{ "rgb10a2",  PIXELFORMAT_RGB10A2  },
 	{ "rg11b10f", PIXELFORMAT_RG11B10F },
+
+	{ "stencil8", PIXELFORMAT_STENCIL8 },
 	
 	{ "DXT1",      PIXELFORMAT_DXT1       },
 	{ "DXT3",      PIXELFORMAT_DXT3       },
@@ -112,11 +114,17 @@ bool isPixelFormatCompressed(PixelFormat format)
 	return iformat >= (int) PIXELFORMAT_DXT1 && iformat < (int) PIXELFORMAT_MAX_ENUM;
 }
 
+bool isPixelFormatDepthStencil(PixelFormat format)
+{
+	return format == PIXELFORMAT_STENCIL8;
+}
+
 size_t getPixelFormatSize(PixelFormat format)
 {
 	switch (format)
 	{
 	case PIXELFORMAT_R8:
+	case PIXELFORMAT_STENCIL8:
 		return 1;
 	case PIXELFORMAT_RG8:
 	case PIXELFORMAT_R16:

+ 8 - 0
src/common/pixelformat.h

@@ -57,6 +57,9 @@ enum PixelFormat
 	PIXELFORMAT_RGB10A2,
 	PIXELFORMAT_RG11B10F,
 
+	// depth/stencil formats
+	PIXELFORMAT_STENCIL8,
+
 	// compressed formats
 	PIXELFORMAT_DXT1,
 	PIXELFORMAT_DXT3,
@@ -106,6 +109,11 @@ bool getConstant(const char *in, PixelFormat &out);
  **/
 bool isPixelFormatCompressed(PixelFormat format);
 
+/**
+ * Gets whether the specified pixel format is a depth/stencil type.
+ **/
+bool isPixelFormatDepthStencil(PixelFormat format);
+
 /**
  * Gets the size in bytes of the specified pixel format.
  * NOTE: Currently returns 0 for compressed formats.

+ 1 - 1
src/modules/graphics/Canvas.h

@@ -55,7 +55,7 @@ public:
 
 	virtual int getMSAA() const = 0;
 	virtual int getRequestedMSAA() const = 0;
-	virtual ptrdiff_t getMSAAHandle() const = 0;
+	virtual ptrdiff_t getRenderTargetHandle() const = 0;
 
 	void draw(Graphics *gfx, Quad *q, const Matrix4 &t) override;
 	void drawLayer(Graphics *gfx, int layer, Quad *q, const Matrix4 &t) override;

+ 42 - 20
src/modules/graphics/Graphics.cpp

@@ -187,9 +187,9 @@ int Graphics::getPixelHeight() const
 
 double Graphics::getCurrentPixelDensity() const
 {
-	if (states.back().renderTargets.size() > 0)
+	if (states.back().renderTargets.colors.size() > 0)
 	{
-		love::graphics::Canvas *c = states.back().renderTargets[0].canvas;
+		Canvas *c = states.back().renderTargets.colors[0].canvas;
 		return (double) c->getPixelHeight() / (double) c->getHeight();
 	}
 
@@ -291,19 +291,28 @@ void Graphics::restoreStateChecked(const DisplayState &s)
 	setFont(s.font.get());
 	setShader(s.shader.get());
 
-	bool canvaseschanged = s.renderTargets.size() != cur.renderTargets.size();
+	const auto &sRTs = s.renderTargets;
+	const auto &curRTs = cur.renderTargets;
+
+	bool canvaseschanged = sRTs.colors.size() != curRTs.colors.size();
 	if (!canvaseschanged)
 	{
-		for (size_t i = 0; i < s.renderTargets.size() && i < cur.renderTargets.size(); i++)
+		for (size_t i = 0; i < sRTs.colors.size() && i < curRTs.colors.size(); i++)
 		{
-			const auto &rt1 = s.renderTargets[i];
-			const auto &rt2 = cur.renderTargets[i];
+			const auto &rt1 = sRTs.colors[i];
+			const auto &rt2 = curRTs.colors[i];
 			if (rt1.canvas.get() != rt2.canvas.get() || rt1.slice != rt2.slice)
 			{
 				canvaseschanged = true;
 				break;
 			}
 		}
+
+		if (!canvaseschanged && (sRTs.depthStencil.canvas.get() != curRTs.depthStencil.canvas.get()
+			|| sRTs.depthStencil.slice != curRTs.depthStencil.slice))
+		{
+			canvaseschanged = true;
+		}
 	}
 
 	if (canvaseschanged)
@@ -401,45 +410,58 @@ void Graphics::setCanvas(RenderTarget rt)
 	if (rt.canvas == nullptr)
 		return setCanvas();
 
-	std::vector<RenderTarget> rts = {rt};
+	RenderTargets rts;
+	rts.colors.push_back(rt);
+
 	setCanvas(rts);
 }
 
-void Graphics::setCanvas(const std::vector<RenderTargetStrongRef> &rts)
+void Graphics::setCanvas(const RenderTargetsStrongRef &rts)
 {
-	std::vector<RenderTarget> canvaslist;
-	canvaslist.reserve(rts.size());
+	RenderTargets targets;
+	targets.colors.reserve(rts.colors.size());
 
-	for (const auto &rt : rts)
-		canvaslist.emplace_back(rt.canvas.get(), rt.slice);
+	for (const auto &rt : rts.colors)
+		targets.colors.emplace_back(rt.canvas.get(), rt.slice);
 
-	return setCanvas(canvaslist);
+	targets.depthStencil = RenderTarget(rts.depthStencil.canvas, rts.depthStencil.slice);
+
+	return setCanvas(targets);
 }
 
-std::vector<Graphics::RenderTarget> Graphics::getCanvas() const
+Graphics::RenderTargets Graphics::getCanvas() const
 {
-	std::vector<RenderTarget> rts;
-	rts.reserve(states.back().renderTargets.size());
+	const auto &curRTs = states.back().renderTargets;
+
+	RenderTargets rts;
+	rts.colors.reserve(curRTs.colors.size());
+
+	for (const auto &rt : curRTs.colors)
+		rts.colors.emplace_back(rt.canvas.get(), rt.slice);
 
-	for (const auto &rt : states.back().renderTargets)
-		rts.emplace_back(rt.canvas.get(), rt.slice);
+	rts.depthStencil = RenderTarget(curRTs.depthStencil.canvas, curRTs.depthStencil.slice);
 
 	return rts;
 }
 
 bool Graphics::isCanvasActive() const
 {
-	return !states.back().renderTargets.empty();
+	return !states.back().renderTargets.colors.empty();
 }
 
 bool Graphics::isCanvasActive(love::graphics::Canvas *canvas) const
 {
-	for (const auto &rt : states.back().renderTargets)
+	const auto &curRTs = states.back().renderTargets;
+
+	for (const auto &rt : curRTs.colors)
 	{
 		if (rt.canvas.get() == canvas)
 			return true;
 	}
 
+	if (curRTs.depthStencil.canvas.get() == canvas)
+		return true;
+
 	return false;
 }
 

+ 31 - 6
src/modules/graphics/Graphics.h

@@ -315,13 +315,18 @@ public:
 
 	struct RenderTarget
 	{
-		Canvas *canvas = nullptr;
-		int slice = 0;
+		Canvas *canvas;
+		int slice;
 
 		RenderTarget(Canvas *canvas, int slice = 0)
 			: canvas(canvas)
 			, slice(slice)
 		{}
+
+		RenderTarget()
+			: canvas(nullptr)
+			, slice(0)
+		{}
 	};
 
 	struct RenderTargetStrongRef
@@ -335,6 +340,26 @@ public:
 		{}
 	};
 
+	struct RenderTargets
+	{
+		std::vector<RenderTarget> colors;
+		RenderTarget depthStencil;
+
+		RenderTargets()
+			: depthStencil(nullptr)
+		{}
+	};
+
+	struct RenderTargetsStrongRef
+	{
+		std::vector<RenderTargetStrongRef> colors;
+		RenderTargetStrongRef depthStencil;
+
+		RenderTargetsStrongRef()
+			: depthStencil(nullptr)
+		{}
+	};
+
 	Graphics();
 	virtual ~Graphics();
 
@@ -456,11 +481,11 @@ public:
 	Shader *getShader() const;
 
 	void setCanvas(RenderTarget rt);
-	virtual void setCanvas(const std::vector<RenderTarget> &rts) = 0;
-	void setCanvas(const std::vector<RenderTargetStrongRef> &rts);
+	virtual void setCanvas(const RenderTargets &rts) = 0;
+	void setCanvas(const RenderTargetsStrongRef &rts);
 	virtual void setCanvas() = 0;
 
-	std::vector<RenderTarget> getCanvas() const;
+	RenderTargets getCanvas() const;
 	bool isCanvasActive() const;
 	bool isCanvasActive(Canvas *canvas) const;
 
@@ -813,7 +838,7 @@ protected:
 		StrongRef<Font> font;
 		StrongRef<Shader> shader;
 
-		std::vector<RenderTargetStrongRef> renderTargets;
+		RenderTargetsStrongRef renderTargets;
 
 		ColorMask colorMask = ColorMask(true, true, true, true);
 

+ 9 - 1
src/modules/graphics/Shader.cpp

@@ -206,6 +206,14 @@ void Shader::checkMainTextureType(TextureType textype) const
 	}
 }
 
+void Shader::checkMainTexture(Texture *texture) const
+{
+	if (!texture->isReadable())
+		throw love::Exception("Textures with non-readable formats cannot be sampled from in a shader.");
+
+	checkMainTextureType(texture->getTextureType());
+}
+
 bool Shader::validate(Graphics *gfx, bool gles, const ShaderSource &source, bool checkWithDefaults, std::string &err)
 {
 	if (source.vertex.empty() && source.pixel.empty())
@@ -242,7 +250,7 @@ bool Shader::validate(Graphics *gfx, bool gles, const ShaderSource &source, bool
 
 		if (!s.parse(&defaultTBuiltInResource, defaultversion, defaultprofile, forcedefault, forwardcompat, EShMsgSuppressWarnings))
 		{
-			const char *stagename;
+			const char *stagename = "unknown";
 			getConstant(stage, stagename);
 			err = "Error validating " + std::string(stagename) + " shader:\n\n"
 				+ std::string(s.getInfoLog()) + "\n" + std::string(s.getInfoDebugLog());

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

@@ -190,6 +190,7 @@ public:
 
 	TextureType getMainTextureType() const;
 	void checkMainTextureType(TextureType textype) const;
+	void checkMainTexture(Texture *texture) const;
 
 	virtual ptrdiff_t getHandle() const = 0;
 

+ 12 - 0
src/modules/graphics/Texture.cpp

@@ -51,6 +51,7 @@ float Texture::defaultMipmapSharpness = 0.0f;
 Texture::Texture(TextureType texType)
 	: texType(texType)
 	, format(PIXELFORMAT_UNKNOWN)
+	, readable(true)
 	, width(0)
 	, height(0)
 	, depth(1)
@@ -84,6 +85,11 @@ PixelFormat Texture::getPixelFormat() const
 	return format;
 }
 
+bool Texture::isReadable() const
+{
+	return readable;
+}
+
 void Texture::draw(Graphics *gfx, const Matrix4 &m)
 {
 	draw(gfx, quad, m);
@@ -93,6 +99,9 @@ void Texture::draw(Graphics *gfx, Quad *q, const Matrix4 &localTransform)
 {
 	using namespace vertex;
 
+	if (!readable)
+		throw love::Exception("Textures with non-readable formats cannot be drawn.");
+
 	if (texType == TEXTURE_2D_ARRAY)
 	{
 		drawLayer(gfx, q->getLayer(), q, localTransform);
@@ -132,6 +141,9 @@ void Texture::drawLayer(Graphics *gfx, int layer, Quad *q, const Matrix4 &m)
 {
 	using namespace vertex;
 
+	if (!readable)
+		throw love::Exception("Textures with non-readable formats cannot be drawn.");
+
 	if (texType != TEXTURE_2D_ARRAY)
 		throw love::Exception("drawLayer can only be used with Array Textures!");
 

+ 3 - 0
src/modules/graphics/Texture.h

@@ -120,6 +120,8 @@ public:
 	TextureType getTextureType() const;
 	PixelFormat getPixelFormat() const;
 
+	bool isReadable() const;
+
 	int getWidth() const;
 	int getHeight() const;
 	int getDepth() const;
@@ -166,6 +168,7 @@ protected:
 	TextureType texType;
 
 	PixelFormat format;
+	bool readable;
 
 	int width;
 	int height;

+ 112 - 77
src/modules/graphics/opengl/Canvas.cpp

@@ -69,8 +69,9 @@ static GLenum createFBO(GLuint &framebuffer, TextureType texType, GLuint texture
 	return status;
 }
 
-static bool createMSAABuffer(int width, int height, int &samples, PixelFormat pixelformat, GLuint &buffer)
+static bool createRenderbuffer(int width, int height, int &samples, PixelFormat pixelformat, GLuint &buffer)
 {
+	int reqsamples = samples;
 	bool unusedSRGB = false;
 	OpenGL::TextureFormat fmt = OpenGL::convertPixelFormat(pixelformat, true, unusedSRGB);
 
@@ -84,16 +85,23 @@ static bool createMSAABuffer(int width, int height, int &samples, PixelFormat pi
 	glGenRenderbuffers(1, &buffer);
 	glBindRenderbuffer(GL_RENDERBUFFER, buffer);
 
-	glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, fmt.internalformat, width, height);
-	glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, buffer);
+	if (samples > 1)
+		glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, fmt.internalformat, width, height);
+	else
+		glRenderbufferStorage(GL_RENDERBUFFER, fmt.internalformat, width, height);
+
+	glFramebufferRenderbuffer(GL_FRAMEBUFFER, fmt.framebufferAttachments[0], GL_RENDERBUFFER, buffer);
 
-	glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_SAMPLES, &samples);
+	if (samples > 1)
+		glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_SAMPLES, &samples);
+	else
+		samples = 0;
 
 	glBindRenderbuffer(GL_RENDERBUFFER, 0);
 
 	GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
 
-	if (status == GL_FRAMEBUFFER_COMPLETE && samples > 1)
+	if (status == GL_FRAMEBUFFER_COMPLETE && (reqsamples <= 1 || samples > 1))
 	{
 		// Initialize the buffer to transparent black.
 		glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
@@ -109,16 +117,17 @@ static bool createMSAABuffer(int width, int height, int &samples, PixelFormat pi
 	gl.bindFramebuffer(OpenGL::FRAMEBUFFER_ALL, current_fbo);
 	gl.deleteFramebuffer(fbo);
 
-	return status == GL_FRAMEBUFFER_COMPLETE && samples > 1;
+	return status == GL_FRAMEBUFFER_COMPLETE;
 }
 
 Canvas::Canvas(const Settings &settings)
 	: love::graphics::Canvas(settings.type)
 	, fbo(0)
 	, texture(0)
-    , msaa_buffer(0)
-	, actual_samples(0)
-	, texture_memory(0)
+    , renderbuffer(0)
+	, requestedSamples(settings.msaa)
+	, actualSamples(0)
+	, textureMemory(0)
 {
 	this->width = settings.width;
 	this->height = settings.height;
@@ -140,6 +149,8 @@ Canvas::Canvas(const Settings &settings)
 
 	this->format = getSizedFormat(settings.format);
 
+	readable = this->format != PIXELFORMAT_STENCIL8;
+
 	initQuad();
 	loadVolatile();
 
@@ -167,9 +178,12 @@ bool Canvas::loadVolatile()
 		throw love::Exception("The %s canvas format is not supported by your OpenGL drivers.", fstr);
 	}
 
-	if (settings.msaa > 1 && texType != TEXTURE_2D)
+	if (requestedSamples > 1 && texType != TEXTURE_2D)
 		throw love::Exception("MSAA is only supported for 2D texture types.");
 
+	if (!readable && texType != TEXTURE_2D)
+		throw love::Exception("Non-readable pixel formats are only supported for 2D texture types.");
+
 	if (!gl.isTextureTypeSupported(texType))
 	{
 		const char *textypestr = "unknown";
@@ -214,68 +228,74 @@ bool Canvas::loadVolatile()
 	OpenGL::TempDebugGroup debuggroup("Canvas load");
 
 	fbo = texture = 0;
-	msaa_buffer = 0;
+	renderbuffer = 0;
 	status = GL_FRAMEBUFFER_COMPLETE;
 
-	// getMaxRenderbufferSamples will be 0 on systems that don't support
-	// multisampled renderbuffers / don't export FBO multisample extensions.
-	settings.msaa = std::min(settings.msaa, gl.getMaxRenderbufferSamples());
-	settings.msaa = std::max(settings.msaa, 0);
-
-	glGenTextures(1, &texture);
-	gl.bindTextureToUnit(this, 0, false);
+	if (isReadable())
+	{
+		glGenTextures(1, &texture);
+		gl.bindTextureToUnit(this, 0, false);
 
-	GLenum gltype = OpenGL::getGLTextureType(texType);
+		GLenum gltype = OpenGL::getGLTextureType(texType);
 
-	if (GLAD_ANGLE_texture_usage)
-		glTexParameteri(gltype, GL_TEXTURE_USAGE_ANGLE, GL_FRAMEBUFFER_ATTACHMENT_ANGLE);
+		if (GLAD_ANGLE_texture_usage)
+			glTexParameteri(gltype, GL_TEXTURE_USAGE_ANGLE, GL_FRAMEBUFFER_ATTACHMENT_ANGLE);
 
-	setFilter(filter);
-	setWrap(wrap);
+		setFilter(filter);
+		setWrap(wrap);
 
-	while (glGetError() != GL_NO_ERROR)
-		/* Clear the error buffer. */;
+		while (glGetError() != GL_NO_ERROR)
+			/* Clear the error buffer. */;
 
-	bool isSRGB = format == PIXELFORMAT_sRGBA8;
-	if (!gl.rawTexStorage(texType, 1, format, isSRGB, pixelWidth, pixelHeight, texType == TEXTURE_VOLUME ? depth : layers))
-	{
-		status = GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT;
-		return false;
-	}
+		bool isSRGB = format == PIXELFORMAT_sRGBA8;
+		if (!gl.rawTexStorage(texType, 1, format, isSRGB, pixelWidth, pixelHeight, texType == TEXTURE_VOLUME ? depth : layers))
+		{
+			status = GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT;
+			return false;
+		}
 
-	if (glGetError() != GL_NO_ERROR)
-	{
-        gl.deleteTexture(texture);
-        texture = 0;
-        status = GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT;
-		return false;
-	}
+		if (glGetError() != GL_NO_ERROR)
+		{
+			gl.deleteTexture(texture);
+			texture = 0;
+			status = GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT;
+			return false;
+		}
 
-	// Create a canvas-local FBO used for glReadPixels as well as MSAA blitting.
-	status = createFBO(fbo, texType, texture, texType == TEXTURE_VOLUME ? depth : layers, true);
+		// Create a canvas-local FBO used for glReadPixels as well as MSAA blitting.
+		status = createFBO(fbo, texType, texture, texType == TEXTURE_VOLUME ? depth : layers, true);
 
-	if (status != GL_FRAMEBUFFER_COMPLETE)
-	{
-		if (fbo != 0)
+		if (status != GL_FRAMEBUFFER_COMPLETE)
 		{
-			gl.deleteFramebuffer(fbo);
-			fbo = 0;
+			if (fbo != 0)
+			{
+				gl.deleteFramebuffer(fbo);
+				fbo = 0;
+			}
+			return false;
 		}
-		return false;
 	}
 
-	actual_samples = settings.msaa == 1 ? 0 : settings.msaa;
+	// getMaxRenderbufferSamples will be 0 on systems that don't support
+	// multisampled renderbuffers / don't export FBO multisample extensions.
+	actualSamples = requestedSamples;
+	actualSamples = std::min(actualSamples, gl.getMaxRenderbufferSamples());
+	actualSamples = std::max(actualSamples, 0);
+	actualSamples = actualSamples == 1 ? 0 : actualSamples;
+
+	if (!isReadable() || actualSamples > 0)
+		createRenderbuffer(pixelWidth, pixelHeight, actualSamples, format, renderbuffer);
 
-	if (actual_samples > 0 && !createMSAABuffer(pixelWidth, pixelHeight, actual_samples, format, msaa_buffer))
-		actual_samples = 0;
+	size_t prevmemsize = textureMemory;
 
-	size_t prevmemsize = texture_memory;
+	textureMemory = getPixelFormatSize(format) * pixelWidth * pixelHeight;
 
-	texture_memory = getPixelFormatSize(format) * pixelWidth * pixelHeight;
-	if (msaa_buffer != 0)
-		texture_memory += (texture_memory * actual_samples);
+	if (actualSamples > 0 && isReadable())
+		textureMemory += (textureMemory * actualSamples);
+	else if (actualSamples > 0)
+		textureMemory *= actualSamples;
 
-	gl.updateTextureMemorySize(prevmemsize, texture_memory);
+	gl.updateTextureMemorySize(prevmemsize, textureMemory);
 
 	return true;
 }
@@ -285,18 +305,18 @@ void Canvas::unloadVolatile()
 	if (fbo != 0)
 		gl.deleteFramebuffer(fbo);
 
-	if (msaa_buffer != 0)
-		glDeleteRenderbuffers(1, &msaa_buffer);
+	if (renderbuffer != 0)
+		glDeleteRenderbuffers(1, &renderbuffer);
 
 	if (texture != 0)
 		gl.deleteTexture(texture);
 
 	fbo = 0;
-	msaa_buffer = 0;
+	renderbuffer = 0;
 	texture = 0;
 
-	gl.updateTextureMemorySize(texture_memory, 0);
-	texture_memory = 0;
+	gl.updateTextureMemorySize(textureMemory, 0);
+	textureMemory = 0;
 }
 
 void Canvas::setFilter(const Texture::Filter &f)
@@ -355,6 +375,9 @@ ptrdiff_t Canvas::getHandle() const
 
 love::image::ImageData *Canvas::newImageData(love::image::Image *module, int slice, int x, int y, int w, int h)
 {
+	if (!isReadable())
+		throw love::Exception("Canvas:newImageData cannot be called on non-readable Canvases.");
+
 	if (x < 0 || y < 0 || w <= 0 || h <= 0 || (x + w) > getPixelWidth() || (y + h) > getPixelHeight())
 		throw love::Exception("Invalid rectangle dimensions.");
 
@@ -423,7 +446,7 @@ PixelFormat Canvas::getSizedFormat(PixelFormat format)
 	case PIXELFORMAT_NORMAL:
 		if (isGammaCorrect())
 			return PIXELFORMAT_sRGBA8;
-		else if (!OpenGL::isPixelFormatSupported(PIXELFORMAT_RGBA8, true, false))
+		else if (!OpenGL::isPixelFormatSupported(PIXELFORMAT_RGBA8, true, true, false))
 			// 32-bit render targets don't have guaranteed support on GLES2.
 			return PIXELFORMAT_RGBA4;
 		else
@@ -456,7 +479,10 @@ bool Canvas::isFormatSupported(PixelFormat format)
 	bool supported = true;
 	format = getSizedFormat(format);
 
-	if (!OpenGL::isPixelFormatSupported(format, true, false))
+	bool depthstencil = isPixelFormatDepthStencil(format);
+	bool readable = !depthstencil;
+
+	if (!OpenGL::isPixelFormatSupported(format, true, readable, false))
 		return false;
 
 	if (checkedFormats[format])
@@ -466,28 +492,37 @@ bool Canvas::isFormatSupported(PixelFormat format)
 	// drivers are still allowed to throw FRAMEBUFFER_UNSUPPORTED when attaching
 	// a texture to a FBO whose format the driver doesn't like. So we should
 	// test with an actual FBO.
+	if (readable)
+	{
+		GLuint texture = 0;
+		glGenTextures(1, &texture);
+		gl.bindTextureToUnit(TEXTURE_2D, texture, 0, false);
 
-	GLuint texture = 0;
-	glGenTextures(1, &texture);
-	gl.bindTextureToUnit(TEXTURE_2D, texture, 0, false);
-
-	Texture::Filter f;
-	f.min = f.mag = Texture::FILTER_NEAREST;
-	gl.setTextureFilter(TEXTURE_2D, f);
+		Texture::Filter f;
+		f.min = f.mag = Texture::FILTER_NEAREST;
+		gl.setTextureFilter(TEXTURE_2D, f);
 
-	Texture::Wrap w;
-	gl.setTextureWrap(TEXTURE_2D, w);
+		Texture::Wrap w;
+		gl.setTextureWrap(TEXTURE_2D, w);
 
-	bool unusedSRGB = false;
-	OpenGL::TextureFormat fmt = OpenGL::convertPixelFormat(format, false, unusedSRGB);
+		bool unusedSRGB = false;
+		gl.rawTexStorage(TEXTURE_2D, 1, format, unusedSRGB, 2, 2);
 
-	glTexImage2D(GL_TEXTURE_2D, 0, fmt.internalformat, 2, 2, 0, fmt.externalformat, fmt.type, nullptr);
+		GLuint fbo = 0;
+		supported = (createFBO(fbo, TEXTURE_2D, texture, 1, false) == GL_FRAMEBUFFER_COMPLETE);
+		gl.deleteFramebuffer(fbo);
 
-	GLuint fbo = 0;
-	supported = (createFBO(fbo, TEXTURE_2D, texture, 1, false) == GL_FRAMEBUFFER_COMPLETE);
-	gl.deleteFramebuffer(fbo);
+		gl.deleteTexture(texture);
+	}
+	else
+	{
+		int samples = 0;
+		GLuint renderbuffer = 0;
+		supported = createRenderbuffer(2, 2, samples, format, renderbuffer);
 
-	gl.deleteTexture(texture);
+		if (supported)
+			glDeleteRenderbuffers(1, &renderbuffer);
+	}
 
 	// Cache the result so we don't do this for every isFormatSupported call.
 	checkedFormats[format] = true;

+ 8 - 10
src/modules/graphics/opengl/Canvas.h

@@ -56,20 +56,19 @@ public:
 
 	int getMSAA() const override
 	{
-		return actual_samples;
+		return actualSamples;
 	}
 
 	int getRequestedMSAA() const override
 	{
-		return settings.msaa;
+		return requestedSamples;
 	}
 
-	ptrdiff_t getMSAAHandle() const override
+	ptrdiff_t getRenderTargetHandle() const override
 	{
-		return msaa_buffer;
+		return renderbuffer != 0 ? renderbuffer : texture;
 	}
 
-
 	inline GLenum getStatus() const
 	{
 		return status;
@@ -87,18 +86,17 @@ public:
 
 private:
 
-    Settings settings;
-
 	GLuint fbo;
 
 	GLuint texture;
-	GLuint msaa_buffer;
+	GLuint renderbuffer;
 
 	GLenum status;
 
-	int actual_samples;
+	int requestedSamples;
+	int actualSamples;
 
-	size_t texture_memory;
+	size_t textureMemory;
 
 	static bool supportedFormats[PIXELFORMAT_MAX_ENUM];
 	static bool checkedFormats[PIXELFORMAT_MAX_ENUM];

+ 145 - 126
src/modules/graphics/opengl/Graphics.cpp

@@ -169,7 +169,7 @@ void Graphics::setViewportSize(int width, int height, int pixelwidth, int pixelh
 	this->pixelWidth = pixelwidth;
 	this->pixelHeight = pixelheight;
 
-	if (states.back().renderTargets.empty())
+	if (!isCanvasActive())
 	{
 		// Set the viewport to top-left corner.
 		gl.setViewport({0, 0, pixelwidth, pixelheight});
@@ -302,8 +302,8 @@ void Graphics::unSetMode()
 	for (const auto &pair : framebufferObjects)
 		gl.deleteFramebuffer(pair.second);
 
-	for (const CachedRenderbuffer &rb : stencilBuffers)
-		glDeleteRenderbuffers(1, &rb.renderbuffer);
+	for (love::graphics::Canvas *c : stencilBuffers)
+		c->release();
 
 	framebufferObjects.clear();
 	stencilBuffers.clear();
@@ -355,6 +355,9 @@ void Graphics::flushStreamDraws()
 			Shader::standardShaders[Shader::STANDARD_ARRAY]->attach();
 		}
 
+		if (!sbstate.texture->isReadable())
+			throw love::Exception("Textures with non-readable formats cannot be drawn.");
+
 		if (Shader::current)
 			Shader::current->checkMainTextureType(textype);
 	}
@@ -502,28 +505,38 @@ void Graphics::setDebug(bool enable)
 	::printf("OpenGL debug output enabled (LOVE_GRAPHICS_DEBUG=1)\n");
 }
 
-void Graphics::setCanvas(const std::vector<RenderTarget> &rts)
+void Graphics::setCanvas(const RenderTargets &rts)
 {
 	DisplayState &state = states.back();
-	int ncanvases = (int) rts.size();
+	int ncanvases = (int) rts.colors.size();
 
-	if (ncanvases == 0)
+	if (ncanvases == 0 && rts.depthStencil.canvas == nullptr)
 		return setCanvas();
+	else if (ncanvases == 0)
+		throw love::Exception("At least one color render target is required when using a custom depth/stencil buffer.");
+
+	const auto &prevRTs = state.renderTargets;
 
-	if (ncanvases == (int) state.renderTargets.size())
+	if (ncanvases == (int) prevRTs.colors.size())
 	{
 		bool modified = false;
 
 		for (int i = 0; i < ncanvases; i++)
 		{
-			if (rts[i].canvas != state.renderTargets[i].canvas.get()
-				|| rts[i].slice != state.renderTargets[i].slice)
+			if (rts.colors[i].canvas != prevRTs.colors[i].canvas.get()
+				|| rts.colors[i].slice != prevRTs.colors[i].slice)
 			{
 				modified = true;
 				break;
 			}
 		}
 
+		if (!modified && rts.depthStencil.canvas == prevRTs.depthStencil.canvas
+			&& rts.depthStencil.slice == prevRTs.depthStencil.slice)
+		{
+			modified = true;
+		}
+
 		if (!modified)
 			return;
 	}
@@ -531,32 +544,53 @@ void Graphics::setCanvas(const std::vector<RenderTarget> &rts)
 	if (ncanvases > gl.getMaxRenderTargets())
 		throw love::Exception("This system can't simultaneously render to %d canvases.", ncanvases);
 
-	love::graphics::Canvas *firstcanvas = rts[0].canvas;
+	love::graphics::Canvas *firstcanvas = rts.colors[0].canvas;
 
 	bool multiformatsupported = Canvas::isMultiFormatMultiCanvasSupported();
 	PixelFormat firstformat = firstcanvas->getPixelFormat();
 
+	if (isPixelFormatDepthStencil(firstformat))
+		throw love::Exception("Depth/stencil format Canvases must be used with the 'depthstencil' field of the table passed into setCanvas.");
+
 	bool hasSRGBcanvas = firstformat == PIXELFORMAT_sRGBA8;
 	int pixelwidth = firstcanvas->getPixelWidth();
 	int pixelheight = firstcanvas->getPixelHeight();
 
 	for (int i = 1; i < ncanvases; i++)
 	{
-		love::graphics::Canvas *c = rts[i].canvas;
+		love::graphics::Canvas *c = rts.colors[i].canvas;
+		PixelFormat format = c->getPixelFormat();
 
 		if (c->getPixelWidth() != pixelwidth || c->getPixelHeight() != pixelheight)
-			throw love::Exception("All canvases in must have the same pixel dimensions.");
+			throw love::Exception("All canvases must have the same pixel dimensions.");
 
-		if (!multiformatsupported && c->getPixelFormat() != firstformat)
+		if (!multiformatsupported && format != firstformat)
 			throw love::Exception("This system doesn't support multi-canvas rendering with different canvas formats.");
 
 		if (c->getRequestedMSAA() != firstcanvas->getRequestedMSAA())
-			throw love::Exception("All Canvases in must have the same MSAA value.");
+			throw love::Exception("All Canvases must have the same MSAA value.");
+
+		if (isPixelFormatDepthStencil(format))
+			throw love::Exception("Depth/stencil format Canvases must be used with the 'depthstencil' field of the table passed into setCanvas.");
 
-		if (c->getPixelFormat() == PIXELFORMAT_sRGBA8)
+		if (format == PIXELFORMAT_sRGBA8)
 			hasSRGBcanvas = true;
 	}
 
+	if (rts.depthStencil.canvas != nullptr)
+	{
+		love::graphics::Canvas *c = rts.depthStencil.canvas;
+
+		if (!isPixelFormatDepthStencil(c->getPixelFormat()))
+			throw love::Exception("Only depth/stencil format Canvases can be used with the 'depthstencil' field of the table passed into setCanvas.");
+
+		if (c->getPixelWidth() != pixelwidth || c->getPixelHeight() != pixelheight)
+			throw love::Exception("All canvases must have the same pixel dimensions.");
+
+		if (c->getRequestedMSAA() != firstcanvas->getRequestedMSAA())
+			throw love::Exception("All Canvases must have the same MSAA value.");
+	}
+
 	OpenGL::TempDebugGroup debuggroup("setCanvas(...)");
 
 	endPass();
@@ -583,13 +617,15 @@ void Graphics::setCanvas(const std::vector<RenderTarget> &rts)
 			gl.setFramebufferSRGB(false);
 	}
 
-	std::vector<RenderTargetStrongRef> canvasrefs;
-	canvasrefs.reserve(rts.size());
+	RenderTargetsStrongRef refs;
+	refs.colors.reserve(rts.colors.size());
+
+	for (auto c : rts.colors)
+		refs.colors.emplace_back(c.canvas, c.slice);
 
-	for (auto c : rts)
-		canvasrefs.emplace_back(c.canvas, c.slice);
+	refs.depthStencil = RenderTargetStrongRef(rts.depthStencil.canvas, rts.depthStencil.slice);
 
-	std::swap(state.renderTargets, canvasrefs);
+	std::swap(state.renderTargets, refs);
 
 	canvasSwitchCount++;
 }
@@ -598,14 +634,14 @@ void Graphics::setCanvas()
 {
 	DisplayState &state = states.back();
 
-	if (state.renderTargets.empty())
+	if (state.renderTargets.colors.empty() && state.renderTargets.depthStencil.canvas == nullptr)
 		return;
 
 	OpenGL::TempDebugGroup debuggroup("setCanvas()");
 
 	endPass();
 
-	state.renderTargets.clear();
+	state.renderTargets = RenderTargetsStrongRef();
 
 	gl.bindFramebuffer(OpenGL::FRAMEBUFFER_ALL, gl.getDefaultFBO());
 
@@ -635,21 +671,25 @@ void Graphics::endPass()
 {
 	flushStreamDraws();
 
-	// Discard the stencil buffer.
-	discard({}, true);
+	auto &rts = states.back().renderTargets;
 
-	auto &canvases = states.back().renderTargets;
+	// Discard the stencil buffer if we're using an internal cached one.
+	if (rts.depthStencil.canvas.get() == nullptr)
+		discard({}, true);
 
 	// Resolve MSAA buffers. MSAA is only supported for 2D render targets so we
 	// don't have to worry about resolving to slices.
-	if (canvases.size() > 0 && canvases[0].canvas->getMSAA() > 1)
+	if (rts.colors.size() > 0 && rts.colors[0].canvas->getMSAA() > 1)
 	{
-		int w = canvases[0].canvas->getPixelWidth();
-		int h = canvases[0].canvas->getPixelHeight();
+		int w = rts.colors[0].canvas->getPixelWidth();
+		int h = rts.colors[0].canvas->getPixelHeight();
 
-		for (int i = 0; i < (int) canvases.size(); i++)
+		for (int i = 0; i < (int) rts.colors.size(); i++)
 		{
-			Canvas *c = (Canvas *) canvases[i].canvas.get();
+			Canvas *c = (Canvas *) rts.colors[i].canvas.get();
+
+			if (!c->isReadable())
+				continue;
 
 			glReadBuffer(GL_COLOR_ATTACHMENT0 + i);
 
@@ -685,7 +725,7 @@ void Graphics::clear(const std::vector<OptionalColorf> &colors)
 	if (colors.size() == 0)
 		return;
 
-	int ncanvases = (int) states.back().renderTargets.size();
+	int ncanvases = (int) states.back().renderTargets.colors.size();
 	int ncolors = std::min((int) colors.size(), ncanvases);
 
 	if (ncolors <= 1 && ncanvases <= 1)
@@ -767,7 +807,7 @@ void Graphics::discard(OpenGL::FramebufferTarget target, const std::vector<bool>
 	attachments.reserve(colorbuffers.size());
 
 	// glDiscardFramebuffer uses different attachment enums for the default FBO.
-	if (states.back().renderTargets.empty() && gl.getDefaultFBO() == 0)
+	if (!isCanvasActive() && gl.getDefaultFBO() == 0)
 	{
 		if (colorbuffers.size() > 0 && colorbuffers[0])
 			attachments.push_back(GL_COLOR);
@@ -780,7 +820,7 @@ void Graphics::discard(OpenGL::FramebufferTarget target, const std::vector<bool>
 	}
 	else
 	{
-		int rendertargetcount = std::max((int) states.back().renderTargets.size(), 1);
+		int rendertargetcount = std::max((int) states.back().renderTargets.colors.size(), 1);
 
 		for (int i = 0; i < (int) colorbuffers.size(); i++)
 		{
@@ -802,11 +842,18 @@ void Graphics::discard(OpenGL::FramebufferTarget target, const std::vector<bool>
 		glDiscardFramebufferEXT(gltarget, (GLint) attachments.size(), &attachments[0]);
 }
 
-void Graphics::bindCachedFBO(const std::vector<RenderTarget> &targets)
+void Graphics::bindCachedFBO(const RenderTargets &targets)
 {
-	int ntargets = (int) targets.size();
-	uint32 hash = XXH32(&targets[0], sizeof(RenderTarget) * ntargets, 0);
+	RenderTarget hashtargets[MAX_COLOR_RENDER_TARGETS + 1];
+	int hashcount = 0;
+
+	for (int i = 0; i < (int) targets.colors.size(); i++)
+		hashtargets[hashcount++] = targets.colors[i];
 
+	if (targets.depthStencil.canvas != nullptr)
+		hashtargets[hashcount++] = targets.depthStencil;
+
+	uint32 hash = XXH32(hashtargets, sizeof(RenderTarget) * hashcount, 0);
 	GLuint fbo = framebufferObjects[hash];
 
 	if (fbo != 0)
@@ -815,47 +862,66 @@ void Graphics::bindCachedFBO(const std::vector<RenderTarget> &targets)
 	}
 	else
 	{
-		int w = targets[0].canvas->getPixelWidth();
-		int h = targets[0].canvas->getPixelHeight();
-		int msaa = std::max(targets[0].canvas->getMSAA(), 1);
+		int w = targets.colors[0].canvas->getPixelWidth();
+		int h = targets.colors[0].canvas->getPixelHeight();
+		int msaa = targets.colors[0].canvas->getMSAA();
+		int reqmsaa = targets.colors[0].canvas->getRequestedMSAA();
+
+		RenderTarget depthstencil = targets.depthStencil;
+
+		if (depthstencil.canvas == nullptr)
+		{
+			depthstencil.canvas = getCachedStencilBuffer(w, h, reqmsaa);
+			depthstencil.slice = 0;
+		}
 
 		glGenFramebuffers(1, &fbo);
 		gl.bindFramebuffer(OpenGL::FRAMEBUFFER_ALL, fbo);
 
+		int ncolortargets = 0;
 		GLenum drawbuffers[MAX_COLOR_RENDER_TARGETS];
 
-		for (int i = 0; i < ntargets; i++)
+		auto attachCanvas = [&](const RenderTarget &rt)
 		{
-			drawbuffers[i] = GL_COLOR_ATTACHMENT0 + i;
+			bool renderbuffer = msaa > 1 || !rt.canvas->isReadable();
+			bool srgb = false;
+			OpenGL::TextureFormat fmt = OpenGL::convertPixelFormat(rt.canvas->getPixelFormat(), renderbuffer, srgb);
 
-			if (msaa > 1)
+			if (fmt.framebufferAttachments[0] == GL_COLOR_ATTACHMENT0)
 			{
-				GLuint rbo = (GLuint) targets[i].canvas->getMSAAHandle();
-				glFramebufferRenderbuffer(GL_FRAMEBUFFER, drawbuffers[i], GL_RENDERBUFFER, rbo);
+				fmt.framebufferAttachments[0] = GL_COLOR_ATTACHMENT0 + ncolortargets;
+				drawbuffers[ncolortargets] = fmt.framebufferAttachments[0];
+				ncolortargets++;
 			}
-			else
+
+			GLuint handle = (GLuint) rt.canvas->getRenderTargetHandle();
+
+			for (GLenum attachment : fmt.framebufferAttachments)
 			{
-				GLuint tex = (GLuint) targets[i].canvas->getHandle();
-				TextureType textype = targets[i].canvas->getTextureType();
+				if (attachment == GL_NONE)
+					continue;
+				else if (renderbuffer)
+					glFramebufferRenderbuffer(GL_FRAMEBUFFER, attachment, GL_RENDERBUFFER, handle);
+				else
+				{
+					TextureType textype = rt.canvas->getTextureType();
 
-				int layer = textype == TEXTURE_CUBE ? 0 : targets[i].slice;
-				int face = textype == TEXTURE_CUBE ? targets[i].slice : 0;
+					int layer = textype == TEXTURE_CUBE ? 0 : rt.slice;
+					int face = textype == TEXTURE_CUBE ? rt.slice : 0;
 
-				gl.framebufferTexture(drawbuffers[i], textype, tex, 0, layer, face);
+					gl.framebufferTexture(attachment, textype, handle, 0, layer, face);
+				}
 			}
-		}
+		};
 
-		if (ntargets > 1)
-			glDrawBuffers(ntargets, drawbuffers);
+		for (const auto &rt : targets.colors)
+			attachCanvas(rt);
 
-		GLuint stencil = attachCachedStencilBuffer(w, h, targets[0].canvas->getRequestedMSAA());
+		if (depthstencil.canvas != nullptr)
+			attachCanvas(depthstencil);
 
-		if (stencil == 0)
-		{
-			gl.deleteFramebuffer(fbo);
-			gl.bindFramebuffer(OpenGL::FRAMEBUFFER_ALL, gl.getDefaultFBO());
-			throw love::Exception("Could not create stencil buffer!");
-		}
+		if (ncolortargets > 1)
+			glDrawBuffers(ncolortargets, drawbuffers);
 
 		GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
 
@@ -870,80 +936,33 @@ void Graphics::bindCachedFBO(const std::vector<RenderTarget> &targets)
 	}
 }
 
-GLuint Graphics::attachCachedStencilBuffer(int w, int h, int samples)
+love::graphics::Canvas *Graphics::getCachedStencilBuffer(int w, int h, int samples)
 {
-	samples = samples == 1 ? 0 : samples;
+	love::graphics::Canvas *canvas = nullptr;
 
-	for (const CachedRenderbuffer &rb : stencilBuffers)
+	for (love::graphics::Canvas *c : stencilBuffers)
 	{
-		if (rb.w == w && rb.h == h && rb.samples == samples)
+		if (c->getPixelWidth() == w && c->getPixelHeight() == h && c->getRequestedMSAA() == samples)
 		{
-			// Attach the buffer to the framebuffer object.
-			for (GLenum attachment : rb.attachments)
-			{
-				if (attachment != GL_NONE)
-					glFramebufferRenderbuffer(GL_FRAMEBUFFER, attachment, GL_RENDERBUFFER, rb.renderbuffer);
-			}
-
-			return rb.renderbuffer;
+			canvas = c;
+			break;
 		}
 	}
 
-	OpenGL::TempDebugGroup debuggroup("Create cached stencil buffer");
-
-	CachedRenderbuffer rb;
-	rb.w = w;
-	rb.h = h;
-	rb.samples = samples;
-
-	rb.attachments[0] = GL_STENCIL_ATTACHMENT;
-	rb.attachments[1] = GL_NONE;
-
-	GLenum format = GL_STENCIL_INDEX8;
-
-	// Prefer a combined depth/stencil buffer.
-	if (GLAD_ES_VERSION_3_0 || GLAD_VERSION_3_0 || GLAD_ARB_framebuffer_object)
-	{
-		format = GL_DEPTH24_STENCIL8;
-		rb.attachments[0] = GL_DEPTH_STENCIL_ATTACHMENT;
-	}
-	else if (GLAD_EXT_packed_depth_stencil || GLAD_OES_packed_depth_stencil)
-	{
-		format = GL_DEPTH24_STENCIL8;
-		rb.attachments[0] = GL_DEPTH_ATTACHMENT;
-		rb.attachments[1] = GL_STENCIL_ATTACHMENT;
-	}
-
-	glGenRenderbuffers(1, &rb.renderbuffer);
-	glBindRenderbuffer(GL_RENDERBUFFER, rb.renderbuffer);
-
-	if (rb.samples > 1)
-		glRenderbufferStorageMultisample(GL_RENDERBUFFER, rb.samples, format, rb.w, rb.h);
-	else
-		glRenderbufferStorage(GL_RENDERBUFFER, format, rb.w, rb.h);
-
-	// Attach the buffer to the framebuffer object.
-	for (GLenum attachment : rb.attachments)
+	if (canvas == nullptr)
 	{
-		if (attachment != GL_NONE)
-			glFramebufferRenderbuffer(GL_FRAMEBUFFER, attachment, GL_RENDERBUFFER, rb.renderbuffer);
-	}
-
-	glBindRenderbuffer(GL_RENDERBUFFER, 0);
+		Canvas::Settings settings;
+		settings.format = PIXELFORMAT_STENCIL8;
+		settings.width = w;
+		settings.height = h;
+		settings.msaa = samples;
 
-	if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
-	{
-		glDeleteRenderbuffers(1, &rb.renderbuffer);
-		rb.renderbuffer = 0;
-	}
+		canvas = newCanvas(settings);
 
-	if (rb.renderbuffer != 0)
-	{
-		glClear(GL_STENCIL_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
-		stencilBuffers.push_back(rb);
+		stencilBuffers.push_back(canvas);
 	}
 
-	return rb.renderbuffer;
+	return canvas;
 }
 
 void Graphics::present(void *screenshotCallbackData)
@@ -951,7 +970,7 @@ void Graphics::present(void *screenshotCallbackData)
 	if (!isActive())
 		return;
 
-	if (!states.back().renderTargets.empty())
+	if (isCanvasActive())
 		throw love::Exception("present cannot be called while a Canvas is active.");
 
 	endPass();
@@ -1083,7 +1102,7 @@ void Graphics::setScissor(const Rect &rect)
 	glrect.h = (int) (rect.h * density);
 
 	// OpenGL's reversed y-coordinate is compensated for in OpenGL::setScissor.
-	gl.setScissor(glrect, !state.renderTargets.empty());
+	gl.setScissor(glrect, isCanvasActive());
 
 	state.scissor = true;
 	state.scissorRect = rect;
@@ -1100,7 +1119,7 @@ void Graphics::setScissor()
 
 void Graphics::drawToStencilBuffer(StencilAction action, int value)
 {
-	if (states.back().renderTargets.empty() && !windowHasStencil)
+	if (!isCanvasActive() && !windowHasStencil)
 		throw love::Exception("The window must have stenciling enabled to draw to the main screen's stencil buffer.");
 
 	flushStreamDraws();
@@ -1161,7 +1180,7 @@ void Graphics::stopDrawToStencilBuffer()
 
 void Graphics::setStencilTest(CompareMode compare, int value)
 {
-	if (compare != COMPARE_ALWAYS && states.back().renderTargets.empty() && !windowHasStencil)
+	if (compare != COMPARE_ALWAYS && !isCanvasActive() && !windowHasStencil)
 		throw love::Exception("The window must have stenciling enabled to use setStencilTest on the main screen.");
 
 	DisplayState &state = states.back();

+ 4 - 13
src/modules/graphics/opengl/Graphics.h

@@ -97,7 +97,7 @@ public:
 
 	void setColor(Colorf c) override;
 
-	void setCanvas(const std::vector<RenderTarget> &rts) override;
+	void setCanvas(const RenderTargets &rts) override;
 	void setCanvas() override;
 
 	void setScissor(const Rect &rect) override;
@@ -131,26 +131,17 @@ public:
 
 private:
 
-	struct CachedRenderbuffer
-	{
-		int w;
-		int h;
-		int samples;
-		GLenum attachments[2];
-		GLuint renderbuffer;
-	};
-
 	love::graphics::StreamBuffer *newStreamBuffer(BufferType type, size_t size) override;
 
 	void endPass();
-	void bindCachedFBO(const std::vector<RenderTarget> &targets);
+	void bindCachedFBO(const RenderTargets &targets);
 	void discard(OpenGL::FramebufferTarget target, const std::vector<bool> &colorbuffers, bool depthstencil);
-	GLuint attachCachedStencilBuffer(int w, int h, int samples);
+	love::graphics::Canvas *getCachedStencilBuffer(int w, int h, int samples);
 
 	void setDebug(bool enable);
 
 	std::unordered_map<uint32, GLuint> framebufferObjects;
-	std::vector<CachedRenderbuffer> stencilBuffers;
+	std::vector<love::graphics::Canvas *> stencilBuffers;
 
 	QuadIndices *quadIndices;
 

+ 2 - 2
src/modules/graphics/opengl/Image.cpp

@@ -237,7 +237,7 @@ bool Image::loadVolatile()
 
 	OpenGL::TempDebugGroup debuggroup("Image load");
 
-	if (!OpenGL::isPixelFormatSupported(format, false, sRGB))
+	if (!OpenGL::isPixelFormatSupported(format, false, true, sRGB))
 	{
 		const char *str;
 		if (love::getConstant(format, str))
@@ -511,7 +511,7 @@ Image::MipmapsType Image::getMipmapsType() const
 
 bool Image::isFormatSupported(PixelFormat pixelformat)
 {
-	return OpenGL::isPixelFormatSupported(pixelformat, false, false);
+	return OpenGL::isPixelFormatSupported(pixelformat, false, true, false);
 }
 
 bool Image::hasSRGBSupport()

+ 1 - 1
src/modules/graphics/opengl/Mesh.cpp

@@ -100,7 +100,7 @@ void Mesh::drawInstanced(love::graphics::Graphics *gfx, const love::Matrix4 &m,
 		throw love::Exception("Instancing is not supported on this system.");
 
 	if (Shader::current && texture.get())
-		Shader::current->checkMainTextureType(texture->getTextureType());
+		Shader::current->checkMainTexture(texture);
 
 	gfx->flushStreamDraws();
 

+ 33 - 1
src/modules/graphics/opengl/OpenGL.cpp

@@ -1173,6 +1173,9 @@ OpenGL::TextureFormat OpenGL::convertPixelFormat(PixelFormat pixelformat, bool r
 {
 	TextureFormat f;
 
+	f.framebufferAttachments[0] = GL_COLOR_ATTACHMENT0;
+	f.framebufferAttachments[1] = GL_NONE;
+
 	if (pixelformat == PIXELFORMAT_RGBA8 && isSRGB)
 		pixelformat = PIXELFORMAT_sRGBA8;
 	else if (pixelformat == PIXELFORMAT_ETC1)
@@ -1314,6 +1317,32 @@ OpenGL::TextureFormat OpenGL::convertPixelFormat(PixelFormat pixelformat, bool r
 		f.type = GL_UNSIGNED_INT_10F_11F_11F_REV;
 		break;
 
+	case PIXELFORMAT_STENCIL8:
+		// Prefer a combined depth/stencil buffer due to driver issues.
+		if (GLAD_ES_VERSION_3_0 || GLAD_VERSION_3_0 || GLAD_ARB_framebuffer_object)
+		{
+			f.internalformat = GL_DEPTH24_STENCIL8;
+			f.externalformat = GL_DEPTH_STENCIL;
+			f.type = GL_UNSIGNED_INT_24_8;
+			f.framebufferAttachments[0] = GL_DEPTH_STENCIL_ATTACHMENT;
+		}
+		else if (GLAD_EXT_packed_depth_stencil || GLAD_OES_packed_depth_stencil)
+		{
+			f.internalformat = GL_DEPTH24_STENCIL8;
+			f.externalformat = GL_DEPTH_STENCIL;
+			f.type = GL_UNSIGNED_INT_24_8;
+			f.framebufferAttachments[0] = GL_DEPTH_ATTACHMENT;
+			f.framebufferAttachments[1] = GL_STENCIL_ATTACHMENT;
+		}
+		else
+		{
+			f.internalformat = GL_STENCIL_INDEX8;
+			f.externalformat = GL_STENCIL;
+			f.type = GL_UNSIGNED_BYTE;
+			f.framebufferAttachments[0] = GL_STENCIL_ATTACHMENT;
+		}
+		break;
+
 	case PIXELFORMAT_DXT1:
 		f.internalformat = isSRGB ? GL_COMPRESSED_SRGB_S3TC_DXT1_EXT : GL_COMPRESSED_RGB_S3TC_DXT1_EXT;
 		break;
@@ -1454,7 +1483,7 @@ OpenGL::TextureFormat OpenGL::convertPixelFormat(PixelFormat pixelformat, bool r
 	return f;
 }
 
-bool OpenGL::isPixelFormatSupported(PixelFormat pixelformat, bool rendertarget, bool isSRGB)
+bool OpenGL::isPixelFormatSupported(PixelFormat pixelformat, bool rendertarget, bool readable, bool isSRGB)
 {
 	if (rendertarget && isPixelFormatCompressed(pixelformat))
 		return false;
@@ -1548,6 +1577,9 @@ bool OpenGL::isPixelFormatSupported(PixelFormat pixelformat, bool rendertarget,
 		else
 			return GLAD_VERSION_3_0 || GLAD_EXT_packed_float || GLAD_APPLE_texture_packed_float;
 
+	case PIXELFORMAT_STENCIL8:
+		return rendertarget && !readable;
+
 	case PIXELFORMAT_DXT1:
 		return GLAD_EXT_texture_compression_s3tc || GLAD_EXT_texture_compression_dxt1;
 	case PIXELFORMAT_DXT3:

+ 4 - 1
src/modules/graphics/opengl/OpenGL.h

@@ -96,6 +96,9 @@ public:
 		GLenum externalformat = 0;
 		GLenum type = 0;
 
+		// For depth/stencil formats.
+		GLenum framebufferAttachments[2];
+
 		bool swizzled = false;
 		GLint swizzle[4];
 	};
@@ -379,7 +382,7 @@ public:
 
 	static TextureFormat convertPixelFormat(PixelFormat pixelformat, bool renderbuffer, bool &isSRGB);
 	static bool isTexStorageSupported();
-	static bool isPixelFormatSupported(PixelFormat pixelformat, bool rendertarget, bool isSRGB);
+	static bool isPixelFormatSupported(PixelFormat pixelformat, bool rendertarget, bool readable, bool isSRGB);
 	static bool hasTextureFilteringSupport(PixelFormat pixelformat);
 
 	static const char *errorString(GLenum errorcode);

+ 1 - 1
src/modules/graphics/opengl/ParticleSystem.cpp

@@ -58,7 +58,7 @@ void ParticleSystem::draw(Graphics *gfx, const Matrix4 &m)
 		return;
 
 	if (Shader::current && texture.get())
-		Shader::current->checkMainTextureType(texture->getTextureType());
+		Shader::current->checkMainTexture(texture);
 
 	OpenGL::TempDebugGroup debuggroup("ParticleSystem draw");
 

+ 1 - 1
src/modules/graphics/opengl/Shader.cpp

@@ -677,7 +677,7 @@ void Shader::sendTextures(const UniformInfo *info, Texture **textures, int count
 	{
 		if (textures[i] != nullptr)
 		{
-			if (textures[i]->getTextureType() != info->textureType)
+			if (textures[i]->getTextureType() != info->textureType || !textures[i]->isReadable())
 				continue;
 
 			textures[i]->retain();

+ 1 - 1
src/modules/graphics/opengl/SpriteBatch.cpp

@@ -76,7 +76,7 @@ void SpriteBatch::draw(Graphics *gfx, const Matrix4 &m)
 		}
 
 		if (Shader::current)
-			Shader::current->checkMainTextureType(texture->getTextureType());
+			Shader::current->checkMainTexture(texture);
 	}
 
 	OpenGL::TempDebugGroup debuggroup("SpriteBatch draw");

+ 9 - 3
src/modules/graphics/wrap_Canvas.cpp

@@ -57,11 +57,14 @@ int w_Canvas_renderTo(lua_State *L)
 	if (graphics)
 	{
 		// Save the current render targets so we can restore them when we're done.
-		std::vector<Graphics::RenderTarget> oldtargets = graphics->getCanvas();
+		Graphics::RenderTargets oldtargets = graphics->getCanvas();
 
-		for (auto c : oldtargets)
+		for (auto c : oldtargets.colors)
 			c.canvas->retain();
 
+		if (oldtargets.depthStencil.canvas != nullptr)
+			oldtargets.depthStencil.canvas->retain();
+
 		luax_catchexcept(L, [&](){ graphics->setCanvas(rt); });
 
 		lua_settop(L, 2); // make sure the function is on top of the stack
@@ -69,9 +72,12 @@ int w_Canvas_renderTo(lua_State *L)
 
 		graphics->setCanvas(oldtargets);
 
-		for (auto c : oldtargets)
+		for (auto c : oldtargets.colors)
 			c.canvas->release();
 
+		if (oldtargets.depthStencil.canvas != nullptr)
+			oldtargets.depthStencil.canvas->release();
+
 		if (status != 0)
 			return lua_error(L);
 	}

+ 63 - 45
src/modules/graphics/wrap_Graphics.cpp

@@ -124,7 +124,7 @@ int w_discard(lua_State *L)
 	else
 	{
 		bool discardcolor = luax_optboolean(L, 1, true);
-		size_t numbuffers = std::max((size_t) 1, instance()->getCanvas().size());
+		size_t numbuffers = std::max((size_t) 1, instance()->getCanvas().colors.size());
 		colorbuffers = std::vector<bool>(numbuffers, discardcolor);
 	}
 
@@ -201,6 +201,21 @@ int w_getPixelDensity(lua_State *L)
 	return 1;
 }
 
+static Graphics::RenderTarget checkRenderTarget(lua_State *L, int idx)
+{
+	lua_rawgeti(L, idx, 1);
+	Graphics::RenderTarget target(luax_checkcanvas(L, -1), 0);
+	lua_pop(L, 1);
+
+	TextureType type = target.canvas->getTextureType();
+	if (type == TEXTURE_2D_ARRAY || type == TEXTURE_VOLUME)
+		target.slice = luax_checkintflag(L, idx, "layer") - 1;
+	else if (type == TEXTURE_CUBE)
+		target.slice = luax_checkintflag(L, idx, "face") - 1;
+
+	return target;
+}
+
 int w_setCanvas(lua_State *L)
 {
 	// Disable stencil writes.
@@ -214,7 +229,7 @@ int w_setCanvas(lua_State *L)
 	}
 
 	bool is_table = lua_istable(L, 1);
-	std::vector<Graphics::RenderTarget> targets;
+	Graphics::RenderTargets targets;
 
 	if (is_table)
 	{
@@ -227,29 +242,24 @@ int w_setCanvas(lua_State *L)
 			lua_rawgeti(L, 1, i);
 
 			if (table_of_tables)
-			{
-				lua_rawgeti(L, -1, 1);
-				Graphics::RenderTarget target(luax_checkcanvas(L, -1), 0);
-				lua_pop(L, 1);
-
-				TextureType type = target.canvas->getTextureType();
-				if (type == TEXTURE_2D_ARRAY || type == TEXTURE_VOLUME)
-					target.slice = luax_checkintflag(L, -1, "layer") - 1;
-				else if (type == TEXTURE_CUBE)
-					target.slice = luax_checkintflag(L, -1, "face") - 1;
-
-				targets.push_back(target);
-			}
+				targets.colors.push_back(checkRenderTarget(L, -1));
 			else
 			{
-				targets.emplace_back(luax_checkcanvas(L, -1), 0);
+				targets.colors.emplace_back(luax_checkcanvas(L, -1), 0);
 
-				if (targets.back().canvas->getTextureType() != TEXTURE_2D)
+				if (targets.colors.back().canvas->getTextureType() != TEXTURE_2D)
 					return luaL_error(L, "The table-of-tables variant of setCanvas must be used with non-2D Canvases.");
 			}
 
 			lua_pop(L, 1);
 		}
+
+		lua_getfield(L, 1, "depthstencil");
+		if (lua_istable(L, -1))
+			targets.depthStencil = checkRenderTarget(L, -1);
+		else if (!lua_isnoneornil(L, -1))
+			targets.depthStencil.canvas = luax_checkcanvas(L, -1);
+		lua_pop(L, 1);
 	}
 	else
 	{
@@ -261,19 +271,19 @@ int w_setCanvas(lua_State *L)
 			if (i == 1 && type != TEXTURE_2D)
 			{
 				target.slice = (int) luaL_checknumber(L, 2) - 1;
-				targets.push_back(target);
+				targets.colors.push_back(target);
 				break;
 			}
 
 			if (i > 1 && type != TEXTURE_2D)
 				return luaL_error(L, "This variant of setCanvas only supports 2D texture types.");
 
-			targets.push_back(target);
+			targets.colors.push_back(target);
 		}
 	}
 
 	luax_catchexcept(L, [&]() {
-		if (targets.size() > 0)
+		if (targets.colors.size() > 0)
 			instance()->setCanvas(targets);
 		else
 			instance()->setCanvas();
@@ -282,10 +292,31 @@ int w_setCanvas(lua_State *L)
 	return 0;
 }
 
+static void pushRenderTarget(lua_State *L, const Graphics::RenderTarget &rt)
+{
+	lua_createtable(L, 1, 1);
+
+	luax_pushtype(L, rt.canvas);
+	lua_rawseti(L, -2, 1);
+
+	TextureType type = rt.canvas->getTextureType();
+
+	if (type == TEXTURE_2D_ARRAY || type == TEXTURE_VOLUME)
+	{
+		lua_pushnumber(L, rt.slice + 1);
+		lua_setfield(L, -2, "layer");
+	}
+	else if (type == TEXTURE_VOLUME)
+	{
+		lua_pushnumber(L, rt.slice + 1);
+		lua_setfield(L, -2, "face");
+	}
+}
+
 int w_getCanvas(lua_State *L)
 {
-	const std::vector<Graphics::RenderTarget> targets = instance()->getCanvas();
-	int ntargets = (int) targets.size();
+	Graphics::RenderTargets targets = instance()->getCanvas();
+	int ntargets = (int) targets.colors.size();
 
 	if (ntargets == 0)
 	{
@@ -294,7 +325,7 @@ int w_getCanvas(lua_State *L)
 	}
 
 	bool hasNon2DTextureType = false;
-	for (const auto &rt : targets)
+	for (const auto &rt : targets.colors)
 	{
 		if (rt.canvas->getTextureType() != TEXTURE_2D)
 		{
@@ -303,40 +334,27 @@ int w_getCanvas(lua_State *L)
 		}
 	}
 
-	if (hasNon2DTextureType)
+	if (hasNon2DTextureType || targets.depthStencil.canvas != nullptr)
 	{
 		lua_createtable(L, ntargets, 0);
 
 		for (int i = 0; i < ntargets; i++)
 		{
-			const auto &rt = targets[i];
-
-			lua_createtable(L, 1, 1);
-
-			luax_pushtype(L, rt.canvas);
-			lua_rawseti(L, -2, 1);
-
-			TextureType type = rt.canvas->getTextureType();
-
-			if (type == TEXTURE_2D_ARRAY || type == TEXTURE_VOLUME)
-			{
-				lua_pushnumber(L, rt.slice + 1);
-				lua_setfield(L, -2, "layer");
-			}
-			else if (type == TEXTURE_VOLUME)
-			{
-				lua_pushnumber(L, rt.slice + 1);
-				lua_setfield(L, -2, "face");
-			}
-
+			pushRenderTarget(L, targets.colors[i]);
 			lua_rawseti(L, -2, i + 1);
 		}
 
+		if (targets.depthStencil.canvas != nullptr)
+		{
+			pushRenderTarget(L, targets.depthStencil);
+			lua_setfield(L, -2, "depthstencil");
+		}
+
 		return 1;
 	}
 	else
 	{
-		for (const auto &rt : targets)
+		for (const auto &rt : targets.colors)
 			luax_pushtype(L, rt.canvas);
 
 		return ntargets;

+ 8 - 0
src/modules/graphics/wrap_Texture.cpp

@@ -237,6 +237,13 @@ int w_Texture_getFormat(lua_State *L)
 	return 1;
 }
 
+int w_Texture_isReadable(lua_State *L)
+{
+	Texture *t = luax_checktexture(L, 1);
+	luax_pushboolean(L, t->isReadable());
+	return 1;
+}
+
 const luaL_Reg w_Texture_functions[] =
 {
 	{ "getTextureType", w_Texture_getTextureType },
@@ -257,6 +264,7 @@ const luaL_Reg w_Texture_functions[] =
 	{ "setWrap", w_Texture_setWrap },
 	{ "getWrap", w_Texture_getWrap },
 	{ "getFormat", w_Texture_getFormat },
+	{ "isReadable", w_Texture_isReadable },
 	{ 0, 0 }
 };