Browse Source

Add mipmapping support to Canvases.

- Added an optional mipmap index argument to the non-table variant of love.graphics.setCanvas, and an optional ‘mipmap’ field to the table variant.
- Canvas:setMipmapFilter now works.
- Added Canvas:generateMipmaps.
- Added Canvas:getMipmapMode.
- Added a new ‘mipmaps’ enum field to the table passed into love.graphics.newCanvas. Accepted values are “none” (default), “manual”, and “auto”.

If a Canvas has the manual mipmap mode, you will either need to render to a mipmap level or call Canvas:generateMipmaps. If it has the auto mode, mipmaps will be automatically generated after rendering to that Canvas. Generating mipmaps is not free.

--HG--
branch : minor
Alex Szpakowski 8 years ago
parent
commit
7161d600b7

+ 66 - 2
src/modules/graphics/Canvas.cpp

@@ -29,9 +29,44 @@ namespace graphics
 love::Type Canvas::type("Canvas", &Texture::type);
 int Canvas::canvasCount = 0;
 
-Canvas::Canvas(TextureType textype)
-	: Texture(textype)
+Canvas::Canvas(const Settings &settings)
+	: Texture(settings.type)
 {
+	this->settings = settings;
+
+	width = settings.width;
+	height = settings.height;
+	pixelWidth = (int) ((width * settings.pixeldensity) + 0.5);
+	pixelHeight = (int) ((height * settings.pixeldensity) + 0.5);
+
+	format = settings.format;
+
+	if (texType == TEXTURE_VOLUME)
+		depth = settings.layers;
+	else if (texType == TEXTURE_2D_ARRAY)
+		layers = settings.layers;
+	else
+		layers = 1;
+
+	if (width <= 0 || height <= 0 || layers <= 0)
+		throw love::Exception("Canvas dimensions must be greater than 0.");
+
+	if (texType != TEXTURE_2D && settings.msaa > 1)
+		throw love::Exception("MSAA is only supported for Canvases with the 2D texture type.");
+
+	if (settings.readable.hasValue)
+		readable = settings.readable.value;
+	else
+		readable = !isPixelFormatDepthStencil(format);
+
+	if (readable && isPixelFormatDepthStencil(format) && settings.msaa > 1)
+		throw love::Exception("Readable depth/stencil Canvases with MSAA are not currently supported.");
+
+	if ((!readable || settings.msaa > 1) && settings.mipmaps != MIPMAP_NONE)
+		throw love::Exception("Non-readable and MSAA textures cannot have mipmaps.");
+
+	mipmapCount = settings.mipmaps == MIPMAP_NONE ? 1 : getMipmapCount(pixelWidth, pixelHeight);
+
 	canvasCount++;
 }
 
@@ -40,6 +75,16 @@ Canvas::~Canvas()
 	canvasCount--;
 }
 
+Canvas::MipmapMode Canvas::getMipmapMode() const
+{
+	return settings.mipmaps;
+}
+
+int Canvas::getRequestedMSAA() const
+{
+	return settings.msaa;
+}
+
 void Canvas::draw(Graphics *gfx, Quad *q, const Matrix4 &t)
 {
 	if (gfx->isCanvasActive(this))
@@ -56,6 +101,25 @@ void Canvas::drawLayer(Graphics *gfx, int layer, Quad *quad, const Matrix4 &m)
 	Texture::drawLayer(gfx, layer, quad, m);
 }
 
+bool Canvas::getConstant(const char *in, MipmapMode &out)
+{
+	return mipmapModes.find(in, out);
+}
+
+bool Canvas::getConstant(MipmapMode in, const char *&out)
+{
+	return mipmapModes.find(in, out);
+}
+
+StringMap<Canvas::MipmapMode, Canvas::MIPMAP_MAX_ENUM>::Entry Canvas::mipmapEntries[] =
+{
+	{ "none",   MIPMAP_NONE   },
+	{ "manual", MIPMAP_MANUAL },
+	{ "auto",   MIPMAP_AUTO   },
+};
+
+StringMap<Canvas::MipmapMode, Canvas::MIPMAP_MAX_ENUM> Canvas::mipmapModes(Canvas::mipmapEntries, sizeof(Canvas::mipmapEntries));
+
 } // graphics
 } // love
 

+ 30 - 11
src/modules/graphics/Canvas.h

@@ -23,6 +23,8 @@
 #include "image/Image.h"
 #include "image/ImageData.h"
 #include "Texture.h"
+#include "common/Optional.h"
+#include "common/StringMap.h"
 
 namespace love
 {
@@ -37,38 +39,55 @@ public:
 
 	static love::Type type;
 
-	struct Settings
+	enum MipmapMode
 	{
-		// Defaults to true for color pixel formats, and false for depth/stencil.
-		struct Readable
-		{
-			bool set = false;
-			bool value = false;
-		};
+		MIPMAP_NONE,
+		MIPMAP_MANUAL,
+		MIPMAP_AUTO,
+		MIPMAP_MAX_ENUM
+	};
 
+	struct Settings
+	{
 		int width  = 1;
 		int height = 1;
 		int layers = 1; // depth for 3D textures
+		MipmapMode mipmaps = MIPMAP_NONE;
 		PixelFormat format = PIXELFORMAT_NORMAL;
 		TextureType type = TEXTURE_2D;
 		float pixeldensity = 1.0f;
 		int msaa = 0;
-		Readable readable;
+		OptionalBool readable;
 	};
 
-	Canvas(TextureType textype);
+	Canvas(const Settings &settings);
 	virtual ~Canvas();
 
-	virtual love::image::ImageData *newImageData(love::image::Image *module, int slice, int x, int y, int w, int h) = 0;
+	MipmapMode getMipmapMode() const;
+	int getRequestedMSAA() const;
+
+	virtual love::image::ImageData *newImageData(love::image::Image *module, int slice, int mipmap, const Rect &rect) = 0;
+	virtual void generateMipmaps() = 0;
 
 	virtual int getMSAA() const = 0;
-	virtual int getRequestedMSAA() 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;
 
 	static int canvasCount;
+
+	static bool getConstant(const char *in, MipmapMode &out);
+	static bool getConstant(MipmapMode in, const char *&out);
+
+protected:
+
+	Settings settings;
+
+private:
+
+	static StringMap<MipmapMode, MIPMAP_MAX_ENUM>::Entry mipmapEntries[];
+	static StringMap<MipmapMode, MIPMAP_MAX_ENUM> mipmapModes;
 	
 }; // Canvas
 

+ 7 - 6
src/modules/graphics/Graphics.cpp

@@ -301,7 +301,7 @@ void Graphics::restoreStateChecked(const DisplayState &s)
 		{
 			const auto &rt1 = sRTs.colors[i];
 			const auto &rt2 = curRTs.colors[i];
-			if (rt1.canvas.get() != rt2.canvas.get() || rt1.slice != rt2.slice)
+			if (rt1.canvas.get() != rt2.canvas.get() || rt1.slice != rt2.slice || rt1.mipmap != rt2.mipmap)
 			{
 				canvaseschanged = true;
 				break;
@@ -309,7 +309,8 @@ void Graphics::restoreStateChecked(const DisplayState &s)
 		}
 
 		if (!canvaseschanged && (sRTs.depthStencil.canvas.get() != curRTs.depthStencil.canvas.get()
-			|| sRTs.depthStencil.slice != curRTs.depthStencil.slice))
+			|| sRTs.depthStencil.slice != curRTs.depthStencil.slice
+			|| sRTs.depthStencil.mipmap != curRTs.depthStencil.mipmap))
 		{
 			canvaseschanged = true;
 		}
@@ -422,9 +423,9 @@ void Graphics::setCanvas(const RenderTargetsStrongRef &rts)
 	targets.colors.reserve(rts.colors.size());
 
 	for (const auto &rt : rts.colors)
-		targets.colors.emplace_back(rt.canvas.get(), rt.slice);
+		targets.colors.emplace_back(rt.canvas.get(), rt.slice, rt.mipmap);
 
-	targets.depthStencil = RenderTarget(rts.depthStencil.canvas, rts.depthStencil.slice);
+	targets.depthStencil = RenderTarget(rts.depthStencil.canvas, rts.depthStencil.slice, rts.depthStencil.mipmap);
 
 	return setCanvas(targets);
 }
@@ -437,9 +438,9 @@ Graphics::RenderTargets Graphics::getCanvas() const
 	rts.colors.reserve(curRTs.colors.size());
 
 	for (const auto &rt : curRTs.colors)
-		rts.colors.emplace_back(rt.canvas.get(), rt.slice);
+		rts.colors.emplace_back(rt.canvas.get(), rt.slice, rt.mipmap);
 
-	rts.depthStencil = RenderTarget(curRTs.depthStencil.canvas, curRTs.depthStencil.slice);
+	rts.depthStencil = RenderTarget(curRTs.depthStencil.canvas, curRTs.depthStencil.slice, curRTs.depthStencil.mipmap);
 
 	return rts;
 }

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

@@ -292,10 +292,12 @@ public:
 	{
 		Canvas *canvas;
 		int slice;
+		int mipmap;
 
-		RenderTarget(Canvas *canvas, int slice = 0)
+		RenderTarget(Canvas *canvas, int slice = 0, int mipmap = 0)
 			: canvas(canvas)
 			, slice(slice)
+			, mipmap(mipmap)
 		{}
 
 		RenderTarget()
@@ -308,10 +310,12 @@ public:
 	{
 		StrongRef<Canvas> canvas;
 		int slice = 0;
+		int mipmap = 0;
 
-		RenderTargetStrongRef(Canvas *canvas, int slice = 0)
+		RenderTargetStrongRef(Canvas *canvas, int slice = 0, int mipmap = 0)
 			: canvas(canvas)
 			, slice(slice)
+			, mipmap(mipmap)
 		{}
 	};
 

+ 25 - 10
src/modules/graphics/Texture.cpp

@@ -175,19 +175,19 @@ void Texture::drawLayer(Graphics *gfx, int layer, Quad *q, const Matrix4 &m)
 	}
 }
 
-int Texture::getWidth() const
+int Texture::getWidth(int mip) const
 {
-	return width;
+	return std::max(width >> mip, 1);
 }
 
-int Texture::getHeight() const
+int Texture::getHeight(int mip) const
 {
-	return height;
+	return std::max(height >> mip, 1);
 }
 
-int Texture::getDepth() const
+int Texture::getDepth(int mip) const
 {
-	return depth;
+	return std::max(depth >> mip, 1);
 }
 
 int Texture::getLayerCount() const
@@ -200,14 +200,14 @@ int Texture::getMipmapCount() const
 	return mipmapCount;
 }
 
-int Texture::getPixelWidth() const
+int Texture::getPixelWidth(int mip) const
 {
-	return pixelWidth;
+	return std::max(pixelWidth >> mip, 1);
 }
 
-int Texture::getPixelHeight() const
+int Texture::getPixelHeight(int mip) const
 {
-	return pixelHeight;
+	return std::max(pixelHeight >> mip, 1);
 }
 
 float Texture::getPixelDensity() const
@@ -215,6 +215,21 @@ float Texture::getPixelDensity() const
 	return (float) pixelHeight / (float) height;
 }
 
+void Texture::setFilter(const Filter &f)
+{
+	if (!validateFilter(f, getMipmapCount() > 1))
+	{
+		if (f.mipmap != FILTER_NONE && getMipmapCount() == 1)
+			throw love::Exception("Non-mipmapped texture cannot have mipmap filtering.");
+		else
+			throw love::Exception("Invalid texture filter.");
+	}
+
+	Graphics::flushStreamDrawsGlobal();
+
+	filter = f;
+}
+
 const Texture::Filter &Texture::getFilter() const
 {
 	return filter;

+ 6 - 6
src/modules/graphics/Texture.h

@@ -124,18 +124,18 @@ public:
 
 	bool isReadable() const;
 
-	int getWidth() const;
-	int getHeight() const;
-	int getDepth() const;
+	int getWidth(int mip = 0) const;
+	int getHeight(int mip = 0) const;
+	int getDepth(int mip = 0) const;
 	int getLayerCount() const;
 	int getMipmapCount() const;
 
-	virtual int getPixelWidth() const;
-	virtual int getPixelHeight() const;
+	int getPixelWidth(int mip = 0) const;
+	int getPixelHeight(int mip = 0) const;
 
 	float getPixelDensity() const;
 
-	virtual void setFilter(const Filter &f) = 0;
+	virtual void setFilter(const Filter &f);
 	virtual const Filter &getFilter() const;
 
 	virtual bool setWrap(const Wrap &w) = 0;

+ 80 - 58
src/modules/graphics/opengl/Canvas.cpp

@@ -30,7 +30,7 @@ namespace graphics
 namespace opengl
 {
 
-static GLenum createFBO(GLuint &framebuffer, TextureType texType, PixelFormat format, GLuint texture, int layers)
+static GLenum createFBO(GLuint &framebuffer, TextureType texType, PixelFormat format, GLuint texture, int layers, int mips)
 {
 	// get currently bound fbo to reset to it later
 	GLuint current_fbo = gl.getFramebuffer(OpenGL::FRAMEBUFFER_ALL);
@@ -48,16 +48,23 @@ static GLenum createFBO(GLuint &framebuffer, TextureType texType, PixelFormat fo
 		// Make sure all faces and layers of the texture are initialized to
 		// transparent black. This is unfortunately probably pretty slow for
 		// 2D-array and 3D textures with a lot of layers...
-		for (int layer = layers - 1; layer >= 0; layer--)
+		for (int mip = mips - 1; mip >= 0; mip--)
 		{
-			for (int face = faces - 1; face >= 0; face--)
+			int nlayers = layers;
+			if (texType == TEXTURE_VOLUME)
+				nlayers = std::max(layers >> mip, 1);
+
+			for (int layer = nlayers - 1; layer >= 0; layer--)
 			{
-				for (GLenum attachment : fmt.framebufferAttachments)
+				for (int face = faces - 1; face >= 0; face--)
 				{
-					if (attachment == GL_NONE)
-						continue;
+					for (GLenum attachment : fmt.framebufferAttachments)
+					{
+						if (attachment == GL_NONE)
+							continue;
 
-					gl.framebufferTexture(attachment, texType, texture, 0, layer, face);
+						gl.framebufferTexture(attachment, texType, texture, mip, layer, face);
+					}
 
 					if (isPixelFormatDepthStencil(format))
 					{
@@ -146,41 +153,14 @@ static bool createRenderbuffer(int width, int height, int &samples, PixelFormat
 }
 
 Canvas::Canvas(const Settings &settings)
-	: love::graphics::Canvas(settings.type)
+	: love::graphics::Canvas(settings)
 	, fbo(0)
 	, texture(0)
     , renderbuffer(0)
-	, requestedSamples(settings.msaa)
 	, actualSamples(0)
 	, textureMemory(0)
 {
-	width = settings.width;
-	height = settings.height;
-	pixelWidth = (int) ((width * settings.pixeldensity) + 0.5);
-	pixelHeight = (int) ((height * settings.pixeldensity) + 0.5);
-
-	if (texType == TEXTURE_VOLUME)
-		depth = settings.layers;
-	else if (texType == TEXTURE_2D_ARRAY)
-		layers = settings.layers;
-	else
-		layers = 1;
-
-	if (width <= 0 || height <= 0 || layers <= 0)
-		throw love::Exception("Canvas dimensions must be greater than 0.");
-
-	if (texType != TEXTURE_2D && settings.msaa > 1)
-		throw love::Exception("MSAA is only supported for Canvases with the 2D texture type.");
-
-	format = getSizedFormat(settings.format);
-
-	if (settings.readable.set)
-		readable = settings.readable.value;
-	else
-		readable = !isPixelFormatDepthStencil(format);
-
-	if (readable && isPixelFormatDepthStencil(format) && settings.msaa > 1)
-		throw love::Exception("Readable depth/stencil Canvases with MSAA are not currently supported.");
+	format = getSizedFormat(format);
 
 	initQuad();
 	loadVolatile();
@@ -212,7 +192,7 @@ bool Canvas::loadVolatile()
 		throw love::Exception("The %s%s canvas format is not supported by your OpenGL drivers.", fstr, readablestr);
 	}
 
-	if (requestedSamples > 1 && texType != TEXTURE_2D)
+	if (getRequestedMSAA() > 1 && texType != TEXTURE_2D)
 		throw love::Exception("MSAA is only supported for 2D texture types.");
 
 	if (!readable && texType != TEXTURE_2D)
@@ -221,7 +201,7 @@ bool Canvas::loadVolatile()
 	if (!gl.isTextureTypeSupported(texType))
 	{
 		const char *textypestr = "unknown";
-		getConstant(texType, textypestr);
+		Texture::getConstant(texType, textypestr);
 		throw love::Exception("%s textures are not supported on this system!", textypestr);
 	}
 
@@ -277,13 +257,14 @@ bool Canvas::loadVolatile()
 
 		setFilter(filter);
 		setWrap(wrap);
+		setMipmapSharpness(mipmapSharpness);
 		setDepthSampleMode(depthCompareMode);
 
 		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))
+		if (!gl.rawTexStorage(texType, mipmapCount, format, isSRGB, pixelWidth, pixelHeight, texType == TEXTURE_VOLUME ? depth : layers))
 		{
 			status = GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT;
 			return false;
@@ -298,7 +279,7 @@ bool Canvas::loadVolatile()
 		}
 
 		// Create a canvas-local FBO used for glReadPixels as well as MSAA blitting.
-		status = createFBO(fbo, texType, format, texture, texType == TEXTURE_VOLUME ? depth : layers);
+		status = createFBO(fbo, texType, format, texture, texType == TEXTURE_VOLUME ? depth : layers, mipmapCount);
 
 		if (status != GL_FRAMEBUFFER_COMPLETE)
 		{
@@ -313,7 +294,7 @@ bool Canvas::loadVolatile()
 
 	// getMaxRenderbufferSamples will be 0 on systems that don't support
 	// multisampled renderbuffers / don't export FBO multisample extensions.
-	actualSamples = requestedSamples;
+	actualSamples = getRequestedMSAA();
 	actualSamples = std::min(actualSamples, gl.getMaxRenderbufferSamples());
 	actualSamples = std::max(actualSamples, 0);
 	actualSamples = actualSamples == 1 ? 0 : actualSamples;
@@ -330,6 +311,9 @@ bool Canvas::loadVolatile()
 	else if (actualSamples > 0)
 		textureMemory *= actualSamples;
 
+	if (getMipmapCount() > 1)
+		textureMemory *= 1.33334;
+
 	gl.updateTextureMemorySize(prevmemsize, textureMemory);
 
 	return true;
@@ -356,12 +340,16 @@ void Canvas::unloadVolatile()
 
 void Canvas::setFilter(const Texture::Filter &f)
 {
-	if (!validateFilter(f, false))
-		throw love::Exception("Invalid texture filter.");
+	Texture::setFilter(f);
 
-	Graphics::flushStreamDrawsGlobal();
+	if (!OpenGL::hasTextureFilteringSupport(getPixelFormat()))
+	{
+		filter.mag = filter.min = FILTER_NEAREST;
+
+		if (filter.mipmap == FILTER_LINEAR)
+			filter.mipmap = FILTER_NEAREST;
+	}
 
-	filter = f;
 	gl.bindTextureToUnit(this, 0, false);
 	gl.setTextureFilter(texType, filter);
 }
@@ -402,9 +390,25 @@ bool Canvas::setWrap(const Texture::Wrap &w)
 	return success;
 }
 
-bool Canvas::setMipmapSharpness(float /*sharpness*/)
+bool Canvas::setMipmapSharpness(float sharpness)
 {
-	return false;
+	if (!gl.isSamplerLODBiasSupported())
+		return false;
+
+	Graphics::flushStreamDrawsGlobal();
+
+	float maxbias = gl.getMaxLODBias();
+	if (maxbias > 0.01f)
+		maxbias -= 0.0f;
+
+	mipmapSharpness = std::min(std::max(sharpness, -maxbias), maxbias);
+
+	gl.bindTextureToUnit(this, 0, false);
+
+	// negative bias is sharper
+	glTexParameterf(gl.getGLTextureType(texType), GL_TEXTURE_LOD_BIAS, -mipmapSharpness);
+
+	return true;
 }
 
 void Canvas::setDepthSampleMode(Optional<CompareMode> mode)
@@ -413,11 +417,11 @@ void Canvas::setDepthSampleMode(Optional<CompareMode> mode)
 
 	bool supported = gl.isDepthCompareSampleSupported();
 
-	if (mode.hasValue && !supported)
-		throw love::Exception("Depth comparison sampling in shaders is not supported on this system.");
-
 	if (mode.hasValue)
 	{
+		if (!supported)
+			throw love::Exception("Depth comparison sampling in shaders is not supported on this system.");
+
 		Graphics::flushStreamDrawsGlobal();
 
 		gl.bindTextureToUnit(texType, texture, 0, false);
@@ -448,15 +452,18 @@ ptrdiff_t Canvas::getHandle() const
 	return texture;
 }
 
-love::image::ImageData *Canvas::newImageData(love::image::Image *module, int slice, int x, int y, int w, int h)
+love::image::ImageData *Canvas::newImageData(love::image::Image *module, int slice, int mipmap, const Rect &r)
 {
 	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())
+	if (isPixelFormatDepthStencil(getPixelFormat()))
+		throw love::Exception("Canvas:newImageData cannot be called on Canvases with depth/stencil pixel formats.");
+
+	if (r.x < 0 || r.y < 0 || r.w <= 0 || r.h <= 0 || (r.x + r.w) > getPixelWidth(mipmap) || (r.y + r.h) > getPixelHeight(mipmap))
 		throw love::Exception("Invalid rectangle dimensions.");
 
-	if (slice < 0 || (texType == TEXTURE_VOLUME && slice >= depth)
+	if (slice < 0 || (texType == TEXTURE_VOLUME && slice >= getDepth(mipmap))
 		|| (texType == TEXTURE_2D_ARRAY && slice >= layers)
 		|| (texType == TEXTURE_CUBE && slice >= 6))
 	{
@@ -489,7 +496,7 @@ love::image::ImageData *Canvas::newImageData(love::image::Image *module, int sli
 		break;
 	}
 
-	love::image::ImageData *imagedata = module->newImageData(w, h, dataformat);
+	love::image::ImageData *imagedata = module->newImageData(r.w, r.h, dataformat);
 
 	bool isSRGB = false;
 	OpenGL::TextureFormat fmt = gl.convertPixelFormat(dataformat, false, isSRGB);
@@ -497,16 +504,16 @@ love::image::ImageData *Canvas::newImageData(love::image::Image *module, int sli
 	GLuint current_fbo = gl.getFramebuffer(OpenGL::FRAMEBUFFER_ALL);
 	gl.bindFramebuffer(OpenGL::FRAMEBUFFER_ALL, getFBO());
 
-	if (slice > 0)
+	if (slice > 0 || mipmap > 0)
 	{
 		int layer = texType == TEXTURE_CUBE ? 0 : slice;
 		int face = texType == TEXTURE_CUBE ? slice : 0;
-		gl.framebufferTexture(GL_COLOR_ATTACHMENT0, texType, texture, 0, layer, face);
+		gl.framebufferTexture(GL_COLOR_ATTACHMENT0, texType, texture, mipmap, layer, face);
 	}
 
-	glReadPixels(x, y, w, h, fmt.externalformat, fmt.type, imagedata->getData());
+	glReadPixels(r.x, r.y, r.w, r.h, fmt.externalformat, fmt.type, imagedata->getData());
 
-	if (slice > 0)
+	if (slice > 0 || mipmap > 0)
 		gl.framebufferTexture(GL_COLOR_ATTACHMENT0, texType, texture, 0, 0, 0);
 
 	gl.bindFramebuffer(OpenGL::FRAMEBUFFER_ALL, current_fbo);
@@ -514,6 +521,21 @@ love::image::ImageData *Canvas::newImageData(love::image::Image *module, int sli
 	return imagedata;
 }
 
+void Canvas::generateMipmaps()
+{
+	if (getMipmapCount() == 1 || getMipmapMode() == MIPMAP_NONE)
+		throw love::Exception("generateMipmaps can only be called on a Canvas which was created with mipmaps enabled.");
+
+	gl.bindTextureToUnit(this, 0, false);
+
+	GLenum gltextype = OpenGL::getGLTextureType(texType);
+
+	if (gl.bugs.generateMipmapsRequiresTexture2DEnable)
+		glEnable(gltextype);
+
+	glGenerateMipmap(gltextype);
+}
+
 PixelFormat Canvas::getSizedFormat(PixelFormat format)
 {
 	switch (format)

+ 2 - 7
src/modules/graphics/opengl/Canvas.h

@@ -53,18 +53,14 @@ public:
 	void setDepthSampleMode(Optional<CompareMode> mode) override;
 	ptrdiff_t getHandle() const override;
 
-	love::image::ImageData *newImageData(love::image::Image *module, int slice, int x, int y, int w, int h) override;
+	love::image::ImageData *newImageData(love::image::Image *module, int slice, int mipmap, const Rect &rect) override;
+	void generateMipmaps() override;
 
 	int getMSAA() const override
 	{
 		return actualSamples;
 	}
 
-	int getRequestedMSAA() const override
-	{
-		return requestedSamples;
-	}
-
 	ptrdiff_t getRenderTargetHandle() const override
 	{
 		return renderbuffer != 0 ? renderbuffer : texture;
@@ -114,7 +110,6 @@ private:
 
 	GLenum status;
 
-	int requestedSamples;
 	int actualSamples;
 
 	size_t textureMemory;

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

@@ -518,15 +518,17 @@ void Graphics::setCanvas(const RenderTargets &rts)
 		for (int i = 0; i < ncanvases; i++)
 		{
 			if (rts.colors[i].canvas != prevRTs.colors[i].canvas.get()
-				|| rts.colors[i].slice != prevRTs.colors[i].slice)
+				|| rts.colors[i].slice != prevRTs.colors[i].slice
+				|| rts.colors[i].mipmap != prevRTs.colors[i].mipmap)
 			{
 				modified = true;
 				break;
 			}
 		}
 
-		if (!modified && rts.depthStencil.canvas == prevRTs.depthStencil.canvas
-			&& rts.depthStencil.slice == prevRTs.depthStencil.slice)
+		if (!modified && (rts.depthStencil.canvas != prevRTs.depthStencil.canvas
+			|| rts.depthStencil.slice != prevRTs.depthStencil.slice
+			|| rts.depthStencil.mipmap != prevRTs.depthStencil.mipmap))
 		{
 			modified = true;
 		}
@@ -546,16 +548,23 @@ void Graphics::setCanvas(const RenderTargets &rts)
 	if (isPixelFormatDepthStencil(firstformat))
 		throw love::Exception("Depth/stencil format Canvases must be used with the 'depthstencil' field of the table passed into setCanvas.");
 
+	if (rts.colors[0].mipmap < 0 || rts.colors[0].mipmap >= firstcanvas->getMipmapCount())
+		throw love::Exception("Invalid mipmap level %d.", rts.colors[0].mipmap + 1);
+
 	bool hasSRGBcanvas = firstformat == PIXELFORMAT_sRGBA8;
-	int pixelwidth = firstcanvas->getPixelWidth();
-	int pixelheight = firstcanvas->getPixelHeight();
+	int pixelwidth = firstcanvas->getPixelWidth(rts.colors[0].mipmap);
+	int pixelheight = firstcanvas->getPixelHeight(rts.colors[0].mipmap);
 
 	for (int i = 1; i < ncanvases; i++)
 	{
 		love::graphics::Canvas *c = rts.colors[i].canvas;
 		PixelFormat format = c->getPixelFormat();
+		int mip = rts.colors[i].mipmap;
+
+		if (mip < 0 || mip >= c->getMipmapCount())
+			throw love::Exception("Invalid mipmap level %d.", mip + 1);
 
-		if (c->getPixelWidth() != pixelwidth || c->getPixelHeight() != pixelheight)
+		if (c->getPixelWidth(mip) != pixelwidth || c->getPixelHeight(mip) != pixelheight)
 			throw love::Exception("All canvases must have the same pixel dimensions.");
 
 		if (!multiformatsupported && format != firstformat)
@@ -574,15 +583,19 @@ void Graphics::setCanvas(const RenderTargets &rts)
 	if (rts.depthStencil.canvas != nullptr)
 	{
 		love::graphics::Canvas *c = rts.depthStencil.canvas;
+		int mip = rts.depthStencil.mipmap;
 
 		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)
+		if (c->getPixelWidth(mip) != pixelwidth || c->getPixelHeight(mip) != 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.");
+
+		if (mip < 0 || mip >= c->getMipmapCount())
+			throw love::Exception("Invalid mipmap level %d.", mip + 1);
 	}
 
 	OpenGL::TempDebugGroup debuggroup("setCanvas(...)");
@@ -598,8 +611,8 @@ void Graphics::setCanvas(const RenderTargets &rts)
 	if (state.scissor)
 		setScissor(state.scissorRect);
 
-	int w = firstcanvas->getWidth();
-	int h = firstcanvas->getHeight();
+	int w = firstcanvas->getWidth(rts.colors[0].mipmap);
+	int h = firstcanvas->getHeight(rts.colors[0].mipmap);
 	projectionMatrix = Matrix4::ortho(0.0, (float) w, 0.0, (float) h);
 
 	// Make sure the correct sRGB setting is used when drawing to the canvases.
@@ -666,17 +679,19 @@ void Graphics::endPass()
 	flushStreamDraws();
 
 	auto &rts = states.back().renderTargets;
+	love::graphics::Canvas *depthstencil = rts.depthStencil.canvas.get();
 
 	// Discard the stencil buffer if we're using an internal cached one.
-	if (rts.depthStencil.canvas.get() == nullptr)
+	if (depthstencil == 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 (rts.colors.size() > 0 && rts.colors[0].canvas->getMSAA() > 1)
 	{
-		int w = rts.colors[0].canvas->getPixelWidth();
-		int h = rts.colors[0].canvas->getPixelHeight();
+		int mip = rts.colors[0].mipmap;
+		int w = rts.colors[0].canvas->getPixelWidth(mip);
+		int h = rts.colors[0].canvas->getPixelHeight(mip);
 
 		for (int i = 0; i < (int) rts.colors.size(); i++)
 		{
@@ -695,6 +710,16 @@ void Graphics::endPass()
 				glBlitFramebuffer(0, 0, w, h, 0, 0, w, h, GL_COLOR_BUFFER_BIT, GL_NEAREST);
 		}
 	}
+
+	for (const auto &rt : rts.colors)
+	{
+		if (rt.canvas->getMipmapMode() == Canvas::MIPMAP_AUTO && rt.mipmap == 0)
+			rt.canvas->generateMipmaps();
+	}
+
+	int dsmipmap = rts.depthStencil.mipmap;
+	if (depthstencil != nullptr && depthstencil->getMipmapMode() == Canvas::MIPMAP_AUTO && dsmipmap == 0)
+		depthstencil->generateMipmaps();
 }
 
 void Graphics::clear(OptionalColorf c, OptionalInt stencil, OptionalDouble depth)
@@ -892,8 +917,9 @@ void Graphics::bindCachedFBO(const RenderTargets &targets)
 	}
 	else
 	{
-		int w = targets.colors[0].canvas->getPixelWidth();
-		int h = targets.colors[0].canvas->getPixelHeight();
+		int mip = targets.colors[0].mipmap;
+		int w = targets.colors[0].canvas->getPixelWidth(mip);
+		int h = targets.colors[0].canvas->getPixelHeight(mip);
 		int msaa = targets.colors[0].canvas->getMSAA();
 		int reqmsaa = targets.colors[0].canvas->getRequestedMSAA();
 
@@ -938,8 +964,9 @@ void Graphics::bindCachedFBO(const RenderTargets &targets)
 
 					int layer = textype == TEXTURE_CUBE ? 0 : rt.slice;
 					int face = textype == TEXTURE_CUBE ? rt.slice : 0;
+					int level = rt.mipmap;
 
-					gl.framebufferTexture(attachment, textype, handle, 0, layer, face);
+					gl.framebufferTexture(attachment, textype, handle, level, layer, face);
 				}
 			}
 		};

+ 9 - 24
src/modules/graphics/opengl/Image.cpp

@@ -33,8 +33,6 @@ namespace graphics
 namespace opengl
 {
 
-float Image::maxMipmapSharpness = 0.0f;
-
 Image::Image(TextureType textype, PixelFormat format, int width, int height, int slices, const Settings &settings)
 	: love::graphics::Image(Slices(textype), settings, false)
 	, texture(0)
@@ -98,8 +96,6 @@ void Image::init(PixelFormat fmt, int w, int h, const Settings &settings)
 
 void Image::generateMipmaps()
 {
-	// The GL_GENERATE_MIPMAP texparameter is set in loadVolatile if we don't
-	// have support for glGenerateMipmap.
 	if (getMipmapCount() > 1 && !isCompressed() &&
 		(GLAD_ES_VERSION_2_0 || GLAD_VERSION_3_0 || GLAD_ARB_framebuffer_object || GLAD_EXT_framebuffer_object))
 	{
@@ -270,9 +266,6 @@ bool Image::loadVolatile()
 		filter.mipmap = FILTER_NONE;
 	}
 
-	if (maxMipmapSharpness == 0.0f && GLAD_VERSION_1_4)
-		glGetFloatv(GL_MAX_TEXTURE_LOD_BIAS, &maxMipmapSharpness);
-
 	glGenTextures(1, &texture);
 	gl.bindTextureToUnit(this, 0, false);
 
@@ -418,17 +411,7 @@ ptrdiff_t Image::getHandle() const
 
 void Image::setFilter(const Texture::Filter &f)
 {
-	if (!validateFilter(f, getMipmapCount() > 1))
-	{
-		if (f.mipmap != FILTER_NONE && getMipmapCount() == 1)
-			throw love::Exception("Non-mipmapped image cannot have mipmap filtering.");
-		else
-			throw love::Exception("Invalid texture filter.");
-	}
-
-	Graphics::flushStreamDrawsGlobal();
-
-	filter = f;
+	Texture::setFilter(f);
 
 	if (!OpenGL::hasTextureFilteringSupport(getPixelFormat()))
 	{
@@ -487,20 +470,22 @@ bool Image::setWrap(const Texture::Wrap &w)
 
 bool Image::setMipmapSharpness(float sharpness)
 {
-	// OpenGL ES doesn't support LOD bias via glTexParameter.
-	if (!GLAD_VERSION_1_4)
+	if (!gl.isSamplerLODBiasSupported())
 		return false;
 
 	Graphics::flushStreamDrawsGlobal();
 
-	// LOD bias has the range (-maxbias, maxbias)
-	mipmapSharpness = std::min(std::max(sharpness, -maxMipmapSharpness + 0.01f), maxMipmapSharpness - 0.01f);
+	float maxbias = gl.getMaxLODBias();
+	if (maxbias > 0.01f)
+		maxbias -= 0.0f;
+
+	mipmapSharpness = std::min(std::max(sharpness, -maxbias), maxbias);
 
 	gl.bindTextureToUnit(this, 0, false);
 
 	// negative bias is sharper
-	GLenum gltextype = OpenGL::getGLTextureType(texType);
-	glTexParameterf(gltextype, GL_TEXTURE_LOD_BIAS, -mipmapSharpness);
+	glTexParameterf(gl.getGLTextureType(texType), GL_TEXTURE_LOD_BIAS, -mipmapSharpness);
+
 	return true;
 }
 

+ 0 - 2
src/modules/graphics/opengl/Image.h

@@ -86,8 +86,6 @@ private:
 
 	size_t textureMemorySize;
 
-	static float maxMipmapSharpness;
-
 }; // Image
 
 } // opengl

+ 23 - 21
src/modules/graphics/opengl/OpenGL.cpp

@@ -424,6 +424,11 @@ void OpenGL::initMaxValues()
 	else
 		glGetFloatv(GL_ALIASED_POINT_SIZE_RANGE, limits);
 	maxPointSize = limits[1];
+
+	if (isSamplerLODBiasSupported())
+		glGetFloatv(GL_MAX_TEXTURE_LOD_BIAS, &maxLODBias);
+	else
+		maxLODBias = 0.0f;
 }
 
 void OpenGL::createDefaultTexture()
@@ -922,16 +927,10 @@ void OpenGL::deleteTexture(GLuint texture)
 
 void OpenGL::setTextureFilter(TextureType target, graphics::Texture::Filter &f)
 {
-	GLint gmin, gmag;
+	GLint gmin = f.min == Texture::FILTER_NEAREST ? GL_NEAREST : GL_LINEAR;
+	GLint gmag = f.mag == Texture::FILTER_NEAREST ? GL_NEAREST : GL_LINEAR;
 
-	if (f.mipmap == Texture::FILTER_NONE)
-	{
-		if (f.min == Texture::FILTER_NEAREST)
-			gmin = GL_NEAREST;
-		else // f.min == Texture::FILTER_LINEAR
-			gmin = GL_LINEAR;
-	}
-	else
+	if (f.mipmap != Texture::FILTER_NONE)
 	{
 		if (f.min == Texture::FILTER_NEAREST && f.mipmap == Texture::FILTER_NEAREST)
 			gmin = GL_NEAREST_MIPMAP_NEAREST;
@@ -945,17 +944,6 @@ void OpenGL::setTextureFilter(TextureType target, graphics::Texture::Filter &f)
 			gmin = GL_LINEAR;
 	}
 
-	switch (f.mag)
-	{
-	case Texture::FILTER_NEAREST:
-		gmag = GL_NEAREST;
-		break;
-	case Texture::FILTER_LINEAR:
-	default:
-		gmag = GL_LINEAR;
-		break;
-	}
-
 	GLenum gltarget = getGLTextureType(target);
 
 	glTexParameteri(gltarget, GL_TEXTURE_MIN_FILTER, gmin);
@@ -1138,6 +1126,11 @@ bool OpenGL::isDepthCompareSampleSupported() const
 	return GLAD_VERSION_2_0 || GLAD_ES_VERSION_3_0 || GLAD_EXT_shadow_samplers;
 }
 
+bool OpenGL::isSamplerLODBiasSupported() const
+{
+	return GLAD_VERSION_1_4;
+}
+
 int OpenGL::getMax2DTextureSize() const
 {
 	return std::max(max2DTextureSize, 1);
@@ -1183,6 +1176,11 @@ float OpenGL::getMaxAnisotropy() const
 	return maxAnisotropy;
 }
 
+float OpenGL::getMaxLODBias() const
+{
+	return maxLODBias;
+}
+
 void OpenGL::updateTextureMemorySize(size_t oldsize, size_t newsize)
 {
 	int64 memsize = (int64) stats.textureMemory + ((int64) newsize - (int64) oldsize);
@@ -1748,10 +1746,14 @@ bool OpenGL::hasTextureFilteringSupport(PixelFormat pixelformat)
 {
 	switch (pixelformat)
 	{
+	case PIXELFORMAT_R16F:
+	case PIXELFORMAT_RG16F:
 	case PIXELFORMAT_RGBA16F:
 		return GLAD_VERSION_1_1 || GLAD_ES_VERSION_3_0 || GLAD_OES_texture_half_float_linear;
+	case PIXELFORMAT_R32F:
+	case PIXELFORMAT_RG32F:
 	case PIXELFORMAT_RGBA32F:
-		return GLAD_VERSION_1_1;
+		return GLAD_VERSION_1_1 || GLAD_OES_texture_float_linear;
 	default:
 		return true;
 	}

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

@@ -328,6 +328,7 @@ public:
 	bool isPixelShaderHighpSupported() const;
 	bool isInstancingSupported() const;
 	bool isDepthCompareSampleSupported() const;
+	bool isSamplerLODBiasSupported() const;
 
 	/**
 	 * Returns the maximum supported width or height of a texture.
@@ -363,6 +364,8 @@ public:
 	 **/
 	float getMaxAnisotropy() const;
 
+	float getMaxLODBias() const;
+
 
 	void updateTextureMemorySize(size_t oldsize, size_t newsize);
 
@@ -407,6 +410,7 @@ private:
 
 	bool pixelShaderHighpSupported;
 	float maxAnisotropy;
+	float maxLODBias;
 	int max2DTextureSize;
 	int max3DTextureSize;
 	int maxCubeTextureSize;

+ 34 - 16
src/modules/graphics/wrap_Canvas.cpp

@@ -91,40 +91,58 @@ int w_Canvas_newImageData(lua_State *L)
 	love::image::Image *image = luax_getmodule<love::image::Image>(L, love::image::Image::type);
 
 	int slice = 0;
-	int x = 0;
-	int y = 0;
-	int w = canvas->getPixelWidth();
-	int h = canvas->getPixelHeight();
+	int mipmap = 0;
 
-	int startidx = 2;
+	Rect rect;
+	rect.x = 0;
+	rect.y = 0;
+	rect.w = canvas->getPixelWidth();
+	rect.h = canvas->getPixelHeight();
 
-	if (canvas->getTextureType() != TEXTURE_2D)
+	if (!lua_isnoneornil(L, 2))
 	{
-		slice = (int) luaL_checknumber(L, startidx);
-		startidx++;
-	}
+		rect.x = (int) luaL_checkinteger(L, 2);
+		rect.y = (int) luaL_checkinteger(L, 3);
+		rect.w = (int) luaL_checkinteger(L, 4);
+		rect.h = (int) luaL_checkinteger(L, 5);
 
-	if (!lua_isnoneornil(L, startidx))
-	{
-		x = (int) luaL_checknumber(L, startidx + 0);
-		y = (int) luaL_checknumber(L, startidx + 1);
-		w = (int) luaL_checknumber(L, startidx + 2);
-		h = (int) luaL_checknumber(L, startidx + 3);
+		slice = (int) luaL_optinteger(L, 6, 1) - 1;
+		mipmap = (int) luaL_optinteger(L, 7, 1) - 1;
 	}
 
 	love::image::ImageData *img = nullptr;
-	luax_catchexcept(L, [&](){ img = canvas->newImageData(image, slice, x, y, w, h); });
+	luax_catchexcept(L, [&](){ img = canvas->newImageData(image, slice, mipmap, rect); });
 
 	luax_pushtype(L, img);
 	img->release();
 	return 1;
 }
 
+int w_Canvas_generateMipmaps(lua_State *L)
+{
+	Canvas *c = luax_checkcanvas(L, 1);
+	luax_catchexcept(L, [&]() { c->generateMipmaps(); });
+	return 0;
+}
+
+int w_Canvas_getMipmapMode(lua_State *L)
+{
+	Canvas *c = luax_checkcanvas(L, 1);
+	const char *str;
+	if (!Canvas::getConstant(c->getMipmapMode(), str))
+		return luaL_error(L, "Unknown mipmap mode.");
+
+	lua_pushstring(L, str);
+	return 1;
+}
+
 static const luaL_Reg w_Canvas_functions[] =
 {
 	{ "getMSAA", w_Canvas_getMSAA },
 	{ "renderTo", w_Canvas_renderTo },
 	{ "newImageData", w_Canvas_newImageData },
+	{ "generateMipmaps", w_Canvas_generateMipmaps },
+	{ "getMipmapMode", w_Canvas_getMipmapMode },
 	{ 0, 0 }
 };
 

+ 34 - 10
src/modules/graphics/wrap_Graphics.cpp

@@ -251,6 +251,8 @@ static Graphics::RenderTarget checkRenderTarget(lua_State *L, int idx)
 	else if (type == TEXTURE_CUBE)
 		target.slice = luax_checkintflag(L, idx, "face") - 1;
 
+	target.mipmap = luax_intflag(L, idx, "mipmap", 1) - 1;
+
 	return target;
 }
 
@@ -308,10 +310,16 @@ int w_setCanvas(lua_State *L)
 
 			if (i == 1 && type != TEXTURE_2D)
 			{
-				target.slice = (int) luaL_checknumber(L, 2) - 1;
+				target.slice = (int) luaL_checknumber(L, i + 1) - 1;
+				target.mipmap = (int) luaL_optinteger(L, i + 2, 1) - 1;
 				targets.colors.push_back(target);
 				break;
 			}
+			else if (type == TEXTURE_2D && lua_isnumber(L, i + 1))
+			{
+				target.mipmap = (int) luaL_optinteger(L, i + 1, 1) - 1;
+				i++;
+			}
 
 			if (i > 1 && type != TEXTURE_2D)
 				return luaL_error(L, "This variant of setCanvas only supports 2D texture types.");
@@ -332,7 +340,7 @@ int w_setCanvas(lua_State *L)
 
 static void pushRenderTarget(lua_State *L, const Graphics::RenderTarget &rt)
 {
-	lua_createtable(L, 1, 1);
+	lua_createtable(L, 1, 2);
 
 	luax_pushtype(L, rt.canvas);
 	lua_rawseti(L, -2, 1);
@@ -349,6 +357,9 @@ static void pushRenderTarget(lua_State *L, const Graphics::RenderTarget &rt)
 		lua_pushnumber(L, rt.slice + 1);
 		lua_setfield(L, -2, "face");
 	}
+
+	lua_pushnumber(L, rt.mipmap + 1);
+	lua_setfield(L, -2, "mipmap");
 }
 
 int w_getCanvas(lua_State *L)
@@ -362,17 +373,21 @@ int w_getCanvas(lua_State *L)
 		return 1;
 	}
 
-	bool hasNon2DTextureType = false;
-	for (const auto &rt : targets.colors)
+	bool shouldUseTablesVariant = targets.depthStencil.canvas != nullptr;
+
+	if (!shouldUseTablesVariant)
 	{
-		if (rt.canvas->getTextureType() != TEXTURE_2D)
+		for (const auto &rt : targets.colors)
 		{
-			hasNon2DTextureType = true;
-			break;
+			if (rt.mipmap != 0 || rt.canvas->getTextureType() != TEXTURE_2D)
+			{
+				shouldUseTablesVariant = true;
+				break;
+			}
 		}
 	}
 
-	if (hasNon2DTextureType || targets.depthStencil.canvas != nullptr)
+	if (shouldUseTablesVariant)
 	{
 		lua_createtable(L, ntargets, 0);
 
@@ -1063,7 +1078,7 @@ int w_newCanvas(lua_State *L)
 	if (!lua_isnoneornil(L, startidx))
 	{
 		settings.pixeldensity = (float) luax_numberflag(L, startidx, "pixeldensity", settings.pixeldensity);
-		settings.msaa = luax_intflag(L, startidx, "msaa", 0);
+		settings.msaa = luax_intflag(L, startidx, "msaa", settings.msaa);
 
 		lua_getfield(L, startidx, "format");
 		if (!lua_isnoneornil(L, -1))
@@ -1087,10 +1102,19 @@ int w_newCanvas(lua_State *L)
 		if (!lua_isnoneornil(L, -1))
 		{
 			luaL_checktype(L, -1, LUA_TBOOLEAN);
-			settings.readable.set = true;
+			settings.readable.hasValue = true;
 			settings.readable.value = luax_toboolean(L, -1);
 		}
 		lua_pop(L, 1);
+
+		lua_getfield(L, startidx, "mipmaps");
+		if (!lua_isnoneornil(L, -1))
+		{
+			const char *str = luaL_checkstring(L, -1);
+			if (!Canvas::getConstant(str, settings.mipmaps))
+				return luaL_error(L, "Invalid Canvas mipmap mode: %s", str);
+		}
+		lua_pop(L, 1);
 	}
 
 	Canvas *canvas = nullptr;

+ 26 - 9
src/modules/graphics/wrap_Texture.cpp

@@ -40,32 +40,48 @@ int w_Texture_getTextureType(lua_State *L)
 	return 1;
 }
 
+static int w__optMipmap(lua_State *L, Texture *t, int idx)
+{
+	int mipmap = 0;
+
+	if (!lua_isnoneornil(L, idx))
+	{
+		mipmap = (int) luaL_checkinteger(L, idx) - 1;
+
+		if (mipmap < 0 || mipmap >= t->getMipmapCount())
+			luaL_error(L, "Invalid mipmap index: %d", mipmap + 1);
+	}
+
+	return mipmap;
+}
+
 int w_Texture_getWidth(lua_State *L)
 {
 	Texture *t = luax_checktexture(L, 1);
-	lua_pushnumber(L, t->getWidth());
+	lua_pushnumber(L, t->getWidth(w__optMipmap(L, t, 2)));
 	return 1;
 }
 
 int w_Texture_getHeight(lua_State *L)
 {
 	Texture *t = luax_checktexture(L, 1);
-	lua_pushnumber(L, t->getHeight());
+	lua_pushnumber(L, t->getHeight(w__optMipmap(L, t, 2)));
 	return 1;
 }
 
 int w_Texture_getDimensions(lua_State *L)
 {
 	Texture *t = luax_checktexture(L, 1);
-	lua_pushnumber(L, t->getWidth());
-	lua_pushnumber(L, t->getHeight());
+	int mipmap = w__optMipmap(L, t, 2);
+	lua_pushnumber(L, t->getWidth(mipmap));
+	lua_pushnumber(L, t->getHeight(mipmap));
 	return 2;
 }
 
 int w_Texture_getDepth(lua_State *L)
 {
 	Texture *t = luax_checktexture(L, 1);
-	lua_pushnumber(L, t->getDepth());
+	lua_pushnumber(L, t->getDepth(w__optMipmap(L, t, 2)));
 	return 1;
 }
 
@@ -86,22 +102,23 @@ int w_Texture_getMipmapCount(lua_State *L)
 int w_Texture_getPixelWidth(lua_State *L)
 {
 	Texture *t = luax_checktexture(L, 1);
-	lua_pushnumber(L, t->getPixelWidth());
+	lua_pushnumber(L, t->getPixelWidth(w__optMipmap(L, t, 2)));
 	return 1;
 }
 
 int w_Texture_getPixelHeight(lua_State *L)
 {
 	Texture *t = luax_checktexture(L, 1);
-	lua_pushnumber(L, t->getPixelHeight());
+	lua_pushnumber(L, t->getPixelHeight(w__optMipmap(L, t, 2)));
 	return 1;
 }
 
 int w_Texture_getPixelDimensions(lua_State *L)
 {
 	Texture *t = luax_checktexture(L, 1);
-	lua_pushnumber(L, t->getPixelWidth());
-	lua_pushnumber(L, t->getPixelHeight());
+	int mipmap = w__optMipmap(L, t, 2);
+	lua_pushnumber(L, t->getPixelWidth(mipmap));
+	lua_pushnumber(L, t->getPixelHeight(mipmap));
 	return 2;
 }