diff --git a/src/modules/graphics/Texture.cpp b/src/modules/graphics/Texture.cpp index 477eb33cb..245f10220 100644 --- a/src/modules/graphics/Texture.cpp +++ b/src/modules/graphics/Texture.cpp @@ -89,6 +89,23 @@ const Texture::Filter &Texture::getDefaultFilter() return defaultFilter; } +bool Texture::validateFilter(const Filter &f, bool mipmapsAllowed) +{ + if (!mipmapsAllowed && f.mipmap != FILTER_NONE) + return false; + + if (f.mag != FILTER_LINEAR && f.mag != FILTER_NEAREST) + return false; + + if (f.min != FILTER_LINEAR && f.min != FILTER_NEAREST) + return false; + + if (f.mipmap != FILTER_LINEAR && f.mipmap != FILTER_NEAREST && f.mipmap != FILTER_NONE) + return false; + + return true; +} + bool Texture::getConstant(const char *in, FilterMode &out) { return filterModes.find(in, out); @@ -113,6 +130,7 @@ StringMap::Entry Texture::filterM { { "linear", Texture::FILTER_LINEAR }, { "nearest", Texture::FILTER_NEAREST }, + { "none", Texture::FILTER_NONE }, }; StringMap Texture::filterModes(Texture::filterModeEntries, sizeof(Texture::filterModeEntries)); diff --git a/src/modules/graphics/Texture.h b/src/modules/graphics/Texture.h index 7b5a525a4..5a0158926 100644 --- a/src/modules/graphics/Texture.h +++ b/src/modules/graphics/Texture.h @@ -105,6 +105,8 @@ public: static void setDefaultFilter(const Filter &f); static const Filter &getDefaultFilter(); + static bool validateFilter(const Filter &f, bool mipmapsAllowed); + static bool getConstant(const char *in, FilterMode &out); static bool getConstant(FilterMode in, const char *&out); diff --git a/src/modules/graphics/opengl/Canvas.cpp b/src/modules/graphics/opengl/Canvas.cpp index 2f536efaf..955696ed6 100644 --- a/src/modules/graphics/opengl/Canvas.cpp +++ b/src/modules/graphics/opengl/Canvas.cpp @@ -637,6 +637,9 @@ void Canvas::drawq(Quad *quad, float x, float y, float angle, float sx, float sy void Canvas::setFilter(const Texture::Filter &f) { + if (!validateFilter(f, false)) + throw love::Exception("Invalid texture filter."); + filter = f; gl.bindTexture(texture); gl.setTextureFilter(filter); diff --git a/src/modules/graphics/opengl/Font.cpp b/src/modules/graphics/opengl/Font.cpp index 5cb062cb3..df51c302d 100644 --- a/src/modules/graphics/opengl/Font.cpp +++ b/src/modules/graphics/opengl/Font.cpp @@ -528,6 +528,9 @@ float Font::getSpacing() const void Font::setFilter(const Texture::Filter &f) { + if (!Texture::validateFilter(f, false)) + throw love::Exception("Invalid texture filter."); + filter = f; for (auto it = textures.begin(); it != textures.end(); ++it) diff --git a/src/modules/graphics/opengl/Graphics.cpp b/src/modules/graphics/opengl/Graphics.cpp index aee31ce30..d36cae05b 100644 --- a/src/modules/graphics/opengl/Graphics.cpp +++ b/src/modules/graphics/opengl/Graphics.cpp @@ -423,10 +423,10 @@ void Graphics::discardStencil() activeStencil = false; } -Image *Graphics::newImage(love::image::ImageData *data, Image::Format format) +Image *Graphics::newImage(love::image::ImageData *data, const Image::Flags &flags) { // Create the image. - Image *image = new Image(data, format); + Image *image = new Image(data, flags); if (!isCreated()) return image; @@ -444,16 +444,16 @@ Image *Graphics::newImage(love::image::ImageData *data, Image::Format format) if (!success) { image->release(); - return 0; + return nullptr; } return image; } -Image *Graphics::newImage(love::image::CompressedData *cdata, Image::Format format) +Image *Graphics::newImage(love::image::CompressedData *cdata, const Image::Flags &flags) { // Create the image. - Image *image = new Image(cdata, format); + Image *image = new Image(cdata, flags); if (!isCreated()) return image; @@ -471,7 +471,7 @@ Image *Graphics::newImage(love::image::CompressedData *cdata, Image::Format form if (!success) { image->release(); - return 0; + return nullptr; } return image; diff --git a/src/modules/graphics/opengl/Graphics.h b/src/modules/graphics/opengl/Graphics.h index f68fc0c07..52fc6e3bd 100644 --- a/src/modules/graphics/opengl/Graphics.h +++ b/src/modules/graphics/opengl/Graphics.h @@ -192,8 +192,8 @@ public: /** * Creates an Image object with padding and/or optimization. **/ - Image *newImage(love::image::ImageData *data, Image::Format format = Image::FORMAT_NORMAL); - Image *newImage(love::image::CompressedData *cdata, Image::Format format = Image::FORMAT_NORMAL); + Image *newImage(love::image::ImageData *data, const Image::Flags &flags); + Image *newImage(love::image::CompressedData *cdata, const Image::Flags &flags); Quad *newQuad(Quad::Viewport v, float sw, float sh); diff --git a/src/modules/graphics/opengl/Image.cpp b/src/modules/graphics/opengl/Image.cpp index e6cb81ca4..7a391c071 100644 --- a/src/modules/graphics/opengl/Image.cpp +++ b/src/modules/graphics/opengl/Image.cpp @@ -33,17 +33,16 @@ namespace opengl float Image::maxMipmapSharpness = 0.0f; -Texture::FilterMode Image::defaultMipmapFilter = Texture::FILTER_NONE; +Texture::FilterMode Image::defaultMipmapFilter = Texture::FILTER_NEAREST; float Image::defaultMipmapSharpness = 0.0f; -Image::Image(love::image::ImageData *data, Format format) +Image::Image(love::image::ImageData *data, const Flags &flags) : data(data) , cdata(nullptr) , texture(0) , mipmapSharpness(defaultMipmapSharpness) - , mipmapsCreated(false) , compressed(false) - , format(format) + , flags(flags) , usingDefaultTexture(false) { width = data->getWidth(); @@ -53,14 +52,13 @@ Image::Image(love::image::ImageData *data, Format format) preload(); } -Image::Image(love::image::CompressedData *cdata, Format format) +Image::Image(love::image::CompressedData *cdata, const Flags &flags) : data(nullptr) , cdata(cdata) , texture(0) , mipmapSharpness(defaultMipmapSharpness) - , mipmapsCreated(false) , compressed(true) - , format(format) + , flags(flags) , usingDefaultTexture(false) { width = cdata->getWidth(0); @@ -119,99 +117,16 @@ GLuint Image::getGLTexture() const return texture; } -void Image::uploadCompressedMipmaps() -{ - if (!isCompressed() || !cdata || !hasCompressedTextureSupport(cdata->getFormat())) - return; - - bind(); - - int count = cdata->getMipmapCount(); - - // We have to inform OpenGL if the image doesn't have all mipmap levels. - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, count - 1); - - for (int i = 1; i < count; i++) - { - glCompressedTexImage2D(GL_TEXTURE_2D, - i, - getCompressedFormat(cdata->getFormat()), - cdata->getWidth(i), - cdata->getHeight(i), - 0, - GLsizei(cdata->getSize(i)), - cdata->getData(i)); - } -} - -void Image::createMipmaps() -{ - // Only valid for Images created with ImageData. - if (!data || isCompressed()) - return; - - // Some old drivers claim support for NPOT textures, but fail when creating - // mipmaps. We can't detect which systems will do this, so we fail gracefully - // for all NPOT images. - int w = int(width), h = int(height); - if (w != next_p2(w) || h != next_p2(h)) - { - throw love::Exception("Cannot create mipmaps: " - "image does not have power of two dimensions."); - } - - bind(); - - // Prevent other threads from changing the ImageData while we upload it. - love::thread::Lock lock(data->getMutex()); - - if (GLEE_VERSION_3_0 || GLEE_ARB_framebuffer_object) - { - if (gl.getVendor() == OpenGL::VENDOR_ATI_AMD) - { - // AMD/ATI drivers have several bugs when generating mipmaps, - // re-uploading the entire base image seems to be required. - uploadTexture(); - - // More bugs: http://www.opengl.org/wiki/Common_Mistakes#Automatic_mipmap_generation - glEnable(GL_TEXTURE_2D); - } - - glGenerateMipmap(GL_TEXTURE_2D); - } - else - { - glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE); - glTexSubImage2D(GL_TEXTURE_2D, - 0, - 0, - 0, - (GLsizei)width, - (GLsizei)height, - GL_RGBA, - GL_UNSIGNED_BYTE, - data->getData()); - glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_FALSE); - } -} - -void Image::checkMipmapsCreated() -{ - if (mipmapsCreated || filter.mipmap == FILTER_NONE || usingDefaultTexture) - return; - - if (isCompressed() && cdata && hasCompressedTextureSupport(cdata->getFormat())) - uploadCompressedMipmaps(); - else if (data) - createMipmaps(); - else - return; - - mipmapsCreated = true; -} - void Image::setFilter(const Texture::Filter &f) { + if (!validateFilter(f, flags.mipmaps)) + { + if (f.mipmap != FILTER_NONE && !flags.mipmaps) + throw love::Exception("Non-mipmapped image cannot have mipmap filtering."); + else + throw love::Exception("Invalid texture filter."); + } + filter = f; // We don't want filtering or (attempted) mipmaps on the default texture. @@ -223,7 +138,6 @@ void Image::setFilter(const Texture::Filter &f) bind(); gl.setTextureFilter(filter); - checkMipmapsCreated(); } void Image::setWrap(const Texture::Wrap &w) @@ -260,6 +174,7 @@ void Image::bind() const void Image::preload() { + // For colors. memset(vertices, 255, sizeof(Vertex)*4); vertices[0].x = 0; @@ -280,7 +195,8 @@ void Image::preload() vertices[3].s = 1; vertices[3].t = 0; - filter.mipmap = defaultMipmapFilter; + if (flags.mipmaps) + filter.mipmap = defaultMipmapFilter; } bool Image::load() @@ -293,9 +209,85 @@ void Image::unload() return unloadVolatile(); } +void Image::uploadCompressedData() +{ + GLenum format = getCompressedFormat(cdata->getFormat()); + int count = flags.mipmaps ? cdata->getMipmapCount() : 1; + + // We have to inform OpenGL if the image doesn't have all mipmap levels. + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, count - 1); + + for (int i = 0; i < count; i++) + { + glCompressedTexImage2D(GL_TEXTURE_2D, + i, + format, + cdata->getWidth(i), + cdata->getHeight(i), + 0, + GLsizei(cdata->getSize(i)), + cdata->getData(i)); + } +} + +void Image::uploadImageData() +{ + if (flags.mipmaps) + { + // NPOT mipmap generation isn't always supported on old GPUs/drivers... + if (width != next_p2(width) || height != next_p2(height)) + { + throw love::Exception("Cannot create mipmaps: " + "image does not have power-of-two dimensions."); + } + + if (!GLEE_VERSION_3_0 && !GLEE_ARB_framebuffer_object) + glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE); + } + + GLenum iformat = flags.sRGB ? GL_SRGB8_ALPHA8 : GL_RGBA8; + + { + love::thread::Lock lock(data->getMutex()); + glTexImage2D(GL_TEXTURE_2D, + 0, + iformat, + (GLsizei)width, + (GLsizei)height, + 0, + GL_RGBA, + GL_UNSIGNED_BYTE, + data->getData()); + } + + if (flags.mipmaps) + { + if (GLEE_VERSION_3_0 || GLEE_ARB_framebuffer_object) + { + // Driver bug: http://www.opengl.org/wiki/Common_Mistakes#Automatic_mipmap_generation + if (gl.getVendor() == OpenGL::VENDOR_ATI_AMD) + glEnable(GL_TEXTURE_2D); + + glGenerateMipmap(GL_TEXTURE_2D); + } + else + glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_FALSE); + } +} + +void Image::uploadTexture() +{ + bind(); + + if (isCompressed() && cdata) + uploadCompressedData(); + else if (data) + uploadImageData(); +} + bool Image::loadVolatile() { - if (format == FORMAT_SRGB && !hasSRGBSupport()) + if (flags.sRGB && !hasSRGBSupport()) throw love::Exception("sRGB images are not supported on this system."); if (isCompressed() && cdata && !hasCompressedTextureSupport(cdata->getFormat())) @@ -320,6 +312,9 @@ bool Image::loadVolatile() gl.setTextureWrap(wrap); setMipmapSharpness(mipmapSharpness); + if (!flags.mipmaps) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); + // Use a default texture if the size is too big for the system. if (width > gl.getMaxTextureSize() || height > gl.getMaxTextureSize()) { @@ -327,55 +322,27 @@ bool Image::loadVolatile() return true; } - // Mutex lock will potentially cover texture loading and mipmap creation. - love::thread::EmptyLock lock; - if (data) - lock.setLock(data->getMutex()); - while (glGetError() != GL_NO_ERROR); // Clear errors. - uploadTexture(); + try + { + uploadTexture(); - GLenum glerr = glGetError(); - if (glerr != GL_NO_ERROR) - throw love::Exception("Cannot create image (error code 0x%x)", glerr); + GLenum glerr = glGetError(); + if (glerr != GL_NO_ERROR) + throw love::Exception("Cannot create image (error code 0x%x)", glerr); + } + catch (love::Exception &) + { + gl.deleteTexture(texture); + texture = 0; + throw; + } usingDefaultTexture = false; - mipmapsCreated = false; - checkMipmapsCreated(); - return true; } -void Image::uploadTexture() -{ - if (isCompressed() && cdata) - { - GLenum format = getCompressedFormat(cdata->getFormat()); - glCompressedTexImage2D(GL_TEXTURE_2D, - 0, - format, - cdata->getWidth(0), - cdata->getHeight(0), - 0, - GLsizei(cdata->getSize(0)), - cdata->getData(0)); - } - else if (data) - { - GLenum iformat = (format == FORMAT_SRGB) ? GL_SRGB8_ALPHA8 : GL_RGBA8; - glTexImage2D(GL_TEXTURE_2D, - 0, - iformat, - (GLsizei)width, - (GLsizei)height, - 0, - GL_RGBA, - GL_UNSIGNED_BYTE, - data->getData()); - } -} - void Image::unloadVolatile() { // Delete the hardware texture. @@ -398,32 +365,20 @@ bool Image::refresh() return true; } - // We want this lock to potentially cover mipmap creation as well. - love::thread::EmptyLock lock; - - bind(); - - if (data && !isCompressed()) - lock.setLock(data->getMutex()); - while (glGetError() != GL_NO_ERROR); // Clear errors. uploadTexture(); - if (glGetError() != GL_NO_ERROR) - uploadDefaultTexture(); - else - usingDefaultTexture = false; - - mipmapsCreated = false; - checkMipmapsCreated(); + GLenum glerr = glGetError(); + if (glerr != GL_NO_ERROR) + throw love::Exception("Cannot refresh image (error code 0x%x)", glerr); return true; } -Image::Format Image::getFormat() const +const Image::Flags &Image::getFlags() const { - return format; + return flags; } void Image::uploadDefaultTexture() @@ -492,22 +447,20 @@ bool Image::isCompressed() const GLenum Image::getCompressedFormat(image::CompressedData::Format cformat) const { - bool srgb = format == FORMAT_SRGB; - switch (cformat) { case image::CompressedData::FORMAT_DXT1: - if (srgb) + if (flags.sRGB) return GL_COMPRESSED_SRGB_S3TC_DXT1_EXT; else return GL_COMPRESSED_RGB_S3TC_DXT1_EXT; case image::CompressedData::FORMAT_DXT3: - if (srgb) + if (flags.sRGB) return GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT; else return GL_COMPRESSED_RGBA_S3TC_DXT3_EXT; case image::CompressedData::FORMAT_DXT5: - if (srgb) + if (flags.sRGB) return GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT; else return GL_COMPRESSED_RGBA_S3TC_DXT5_EXT; @@ -520,7 +473,7 @@ GLenum Image::getCompressedFormat(image::CompressedData::Format cformat) const case image::CompressedData::FORMAT_BC5s: return GL_COMPRESSED_SIGNED_RG_RGTC2; default: - if (srgb) + if (flags.sRGB) return GL_SRGB8_ALPHA8; else return GL_RGBA8; @@ -557,23 +510,23 @@ bool Image::hasSRGBSupport() return GLEE_VERSION_2_1 || GLEE_EXT_texture_sRGB; } -bool Image::getConstant(const char *in, Format &out) +bool Image::getConstant(const char *in, FlagType &out) { - return formats.find(in, out); + return flagNames.find(in, out); } -bool Image::getConstant(Format in, const char *&out) +bool Image::getConstant(FlagType in, const char *&out) { - return formats.find(in, out); + return flagNames.find(in, out); } -StringMap::Entry Image::formatEntries[] = +StringMap::Entry Image::flagNameEntries[] = { - {"normal", Image::FORMAT_NORMAL}, - {"srgb", Image::FORMAT_SRGB}, + {"mipmaps", Image::FLAG_TYPE_MIPMAPS}, + {"srgb", Image::FLAG_TYPE_SRGB}, }; -StringMap Image::formats(Image::formatEntries, sizeof(Image::formatEntries)); +StringMap Image::flagNames(Image::flagNameEntries, sizeof(Image::flagNameEntries)); } // opengl } // graphics diff --git a/src/modules/graphics/opengl/Image.h b/src/modules/graphics/opengl/Image.h index ab8c62fcd..1c8bfc88e 100644 --- a/src/modules/graphics/opengl/Image.h +++ b/src/modules/graphics/opengl/Image.h @@ -51,11 +51,19 @@ class Image : public Texture { public: - enum Format + enum FlagType { - FORMAT_NORMAL, - FORMAT_SRGB, - FORMAT_MAX_ENUM + FLAG_TYPE_MIPMAPS, + FLAG_TYPE_SRGB, + FLAG_TYPE_MAX_ENUM + }; + + struct Flags + { + bool mipmaps; + bool sRGB; + + Flags() : mipmaps(false), sRGB(false) {} }; /** @@ -64,14 +72,14 @@ public: * * @param data The data from which to load the image. **/ - Image(love::image::ImageData *data, Format format = FORMAT_NORMAL); + Image(love::image::ImageData *data, const Flags &flags); /** * Creates a new Image with compressed image data. * * @param cdata The compressed data from which to load the image. **/ - Image(love::image::CompressedData *cdata, Format format = FORMAT_NORMAL); + Image(love::image::CompressedData *cdata, const Flags &flags); /** * Destructor. Deletes the hardware texture and other resources. @@ -123,12 +131,11 @@ public: /** * Re-uploads the ImageData or CompressedData associated with this Image to - * the GPU, allowing situations where lovers modify an ImageData after image - * creation from the ImageData, and apply the changes with Image:refresh(). + * the GPU. **/ bool refresh(); - Format getFormat() const; + const Flags &getFlags() const; static void setDefaultMipmapSharpness(float sharpness); static float getDefaultMipmapSharpness(); @@ -139,8 +146,8 @@ public: static bool hasCompressedTextureSupport(image::CompressedData::Format format); static bool hasSRGBSupport(); - static bool getConstant(const char *in, Format &out); - static bool getConstant(Format in, const char *&out); + static bool getConstant(const char *in, FlagType &out); + static bool getConstant(FlagType in, const char *&out); private: @@ -162,14 +169,11 @@ private: // Mipmap texture LOD bias (sharpness) value. float mipmapSharpness; - // True if mipmaps have been created for this Image. - bool mipmapsCreated; - // Whether this Image is using a compressed texture. bool compressed; - // The format to interpret the image's data as. - Format format; + // The flags used to initialize this Image. + Flags flags; // True if the image wasn't able to be properly created and it had to fall // back to a default texture. @@ -177,12 +181,10 @@ private: void preload(); + void uploadCompressedData(); + void uploadImageData(); void uploadTexture(); - void uploadCompressedMipmaps(); - void createMipmaps(); - void checkMipmapsCreated(); - static float maxMipmapSharpness; static FilterMode defaultMipmapFilter; @@ -190,8 +192,8 @@ private: GLenum getCompressedFormat(image::CompressedData::Format cformat) const; - static StringMap::Entry formatEntries[]; - static StringMap formats; + static StringMap::Entry flagNameEntries[]; + static StringMap flagNames; }; // Image diff --git a/src/modules/graphics/opengl/wrap_Graphics.cpp b/src/modules/graphics/opengl/wrap_Graphics.cpp index 929edd263..6a04042d0 100644 --- a/src/modules/graphics/opengl/wrap_Graphics.cpp +++ b/src/modules/graphics/opengl/wrap_Graphics.cpp @@ -146,16 +146,24 @@ int w_setInvertedStencil(lua_State *L) return setStencil(L, true); } +static const char *imageFlagName(Image::FlagType flagtype) +{ + const char *name = nullptr; + Image::getConstant(flagtype, name); + return name; +} + int w_newImage(lua_State *L) { love::image::ImageData *data = nullptr; love::image::CompressedData *cdata = nullptr; - Image::Format format = Image::FORMAT_NORMAL; - const char *fstr = lua_isnoneornil(L, 2) ? nullptr : luaL_checkstring(L, 2); - - if (fstr != nullptr && !Image::getConstant(fstr, format)) - return luaL_error(L, "Invalid Image format: %s", fstr); + Image::Flags flags; + if (!lua_isnoneornil(L, 2)) + { + flags.mipmaps = luax_boolflag(L, 2, imageFlagName(Image::FLAG_TYPE_MIPMAPS), flags.mipmaps); + flags.sRGB = luax_boolflag(L, 2, imageFlagName(Image::FLAG_TYPE_SRGB), flags.sRGB); + } bool releasedata = false; @@ -199,9 +207,9 @@ int w_newImage(lua_State *L) luax_catchexcept(L, [&]() { if (cdata) - image = instance->newImage(cdata, format); + image = instance->newImage(cdata, flags); else if (data) - image = instance->newImage(data, format); + image = instance->newImage(data, flags); }, [&]() { if (releasedata && data) diff --git a/src/modules/graphics/opengl/wrap_Image.cpp b/src/modules/graphics/opengl/wrap_Image.cpp index da9107e5a..79102d531 100644 --- a/src/modules/graphics/opengl/wrap_Image.cpp +++ b/src/modules/graphics/opengl/wrap_Image.cpp @@ -48,9 +48,7 @@ int w_Image_setMipmapFilter(lua_State *L) } luax_catchexcept(L, [&](){ t->setFilter(f); }); - - float sharpness = (float) luaL_optnumber(L, 3, 0); - t->setMipmapSharpness(sharpness); + t->setMipmapSharpness((float) luaL_optnumber(L, 3, 0.0)); return 0; }