瀏覽代碼

using latest love-android @ changeset 8659be0e75a3

fysx 11 年之前
父節點
當前提交
49265f0596
共有 58 個文件被更改,包括 2005 次插入363 次删除
  1. 30 0
      jni/love/src/common/int.h
  2. 1 0
      jni/love/src/modules/audio/openal/Audio.cpp
  3. 4 19
      jni/love/src/modules/audio/openal/Source.cpp
  4. 32 0
      jni/love/src/modules/event/sdl/Event.cpp
  5. 37 12
      jni/love/src/modules/filesystem/physfs/Filesystem.cpp
  6. 2 2
      jni/love/src/modules/filesystem/physfs/Filesystem.h
  7. 2 2
      jni/love/src/modules/filesystem/physfs/wrap_Filesystem.cpp
  8. 2 0
      jni/love/src/modules/graphics/Graphics.cpp
  9. 16 0
      jni/love/src/modules/graphics/Graphics.h
  10. 0 20
      jni/love/src/modules/graphics/Texture.cpp
  11. 0 14
      jni/love/src/modules/graphics/Texture.h
  12. 237 43
      jni/love/src/modules/graphics/opengl/Canvas.cpp
  13. 33 4
      jni/love/src/modules/graphics/opengl/Canvas.h
  14. 35 22
      jni/love/src/modules/graphics/opengl/Font.cpp
  15. 3 0
      jni/love/src/modules/graphics/opengl/Font.h
  16. 67 18
      jni/love/src/modules/graphics/opengl/Graphics.cpp
  17. 7 3
      jni/love/src/modules/graphics/opengl/Graphics.h
  18. 46 4
      jni/love/src/modules/graphics/opengl/Image.cpp
  19. 19 5
      jni/love/src/modules/graphics/opengl/Image.h
  20. 11 0
      jni/love/src/modules/graphics/opengl/OpenGL.cpp
  21. 6 1
      jni/love/src/modules/graphics/opengl/OpenGL.h
  22. 2 3
      jni/love/src/modules/graphics/opengl/Shader.cpp
  23. 12 4
      jni/love/src/modules/graphics/opengl/VertexBuffer.cpp
  24. 3 3
      jni/love/src/modules/graphics/opengl/wrap_Canvas.cpp
  25. 36 11
      jni/love/src/modules/graphics/opengl/wrap_Graphics.cpp
  26. 2 0
      jni/love/src/modules/graphics/opengl/wrap_Graphics.h
  27. 5 0
      jni/love/src/modules/image/CompressedData.cpp
  28. 5 0
      jni/love/src/modules/image/CompressedData.h
  29. 24 14
      jni/love/src/modules/image/magpie/CompressedData.cpp
  30. 8 3
      jni/love/src/modules/image/magpie/CompressedData.h
  31. 74 0
      jni/love/src/modules/image/magpie/CompressedFormatHandler.h
  32. 23 4
      jni/love/src/modules/image/magpie/Image.cpp
  33. 4 0
      jni/love/src/modules/image/magpie/Image.h
  34. 241 0
      jni/love/src/modules/image/magpie/KTXHandler.cpp
  35. 53 0
      jni/love/src/modules/image/magpie/KTXHandler.h
  36. 156 0
      jni/love/src/modules/image/magpie/PKMHandler.cpp
  37. 53 0
      jni/love/src/modules/image/magpie/PKMHandler.h
  38. 385 0
      jni/love/src/modules/image/magpie/PVRHandler.cpp
  39. 51 0
      jni/love/src/modules/image/magpie/PVRHandler.h
  40. 6 6
      jni/love/src/modules/image/magpie/ddsHandler.cpp
  41. 7 24
      jni/love/src/modules/image/magpie/ddsHandler.h
  42. 2 2
      jni/love/src/modules/joystick/Joystick.h
  43. 78 42
      jni/love/src/modules/joystick/sdl/Joystick.cpp
  44. 7 3
      jni/love/src/modules/joystick/sdl/Joystick.h
  45. 7 7
      jni/love/src/modules/joystick/sdl/JoystickModule.cpp
  46. 2 1
      jni/love/src/modules/joystick/sdl/wrap_Joystick.cpp
  47. 2 2
      jni/love/src/modules/joystick/sdl/wrap_JoystickModule.cpp
  48. 2 2
      jni/love/src/modules/sound/lullaby/Mpg123Decoder.cpp
  49. 25 31
      jni/love/src/modules/system/System.cpp
  50. 1 0
      jni/love/src/modules/thread/LuaThread.cpp
  51. 1 1
      jni/love/src/modules/thread/sdl/Thread.cpp
  52. 5 0
      jni/love/src/modules/thread/threads.cpp
  53. 8 0
      jni/love/src/modules/thread/threads.h
  54. 18 7
      jni/love/src/modules/window/sdl/Window.cpp
  55. 1 1
      jni/love/src/scripts/boot.lua
  56. 2 2
      jni/love/src/scripts/boot.lua.h
  57. 28 7
      jni/love/src/scripts/graphics.lua
  58. 76 14
      jni/love/src/scripts/graphics.lua.h

+ 30 - 0
jni/love/src/common/int.h

@@ -21,6 +21,15 @@
 #ifndef LOVE_INT_H
 #define LOVE_INT_H
 
+// fixes compile errors of the form:
+// "error: expected unqualified-id before '__extension__'"
+#ifdef LOVE_ANDROID
+#include <sys/endian.h>
+#undef swap16
+#undef swap32
+#undef swap64
+#endif
+
 // C standard sized integer types.
 // This header was added to Visual studio in VS 2012, which is LOVE's current
 // minimum supported VS version (as of this comment's commit date.)
@@ -47,6 +56,27 @@ typedef uint32_t uint32;
 typedef int64_t int64;
 typedef uint64_t uint64;
 
+static inline uint16 swap16(uint16 x)
+{
+	return (x >> 8) | (x << 8);
+}
+
+static inline uint32 swap32(uint32 x)
+{
+	return ((x & 0x000000FF) << 24) |
+	       ((x & 0x0000FF00) <<  8) |
+	       ((x & 0x00FF0000) >>  8) |
+	       ((x & 0xFF000000) >> 24);
+}
+
+static inline uint64 swap64(uint64 x)
+{
+	return ((x << 56) & 0xFF00000000000000ULL) | ((x << 40) & 0x00FF000000000000ULL) |
+	       ((x << 24) & 0x0000FF0000000000ULL) | ((x <<  8) & 0x000000FF00000000ULL) |
+	       ((x >>  8) & 0x00000000FF000000ULL) | ((x >> 24) & 0x0000000000FF0000ULL) |
+	       ((x >> 40) & 0x000000000000FF00ULL) | ((x >> 56) & 0x00000000000000FFULL);
+}
+
 } // love
 
 #endif // LOVE_INT_H

+ 1 - 0
jni/love/src/modules/audio/openal/Audio.cpp

@@ -37,6 +37,7 @@ Audio::PoolThread::PoolThread(Pool *pool)
 	, finish(false)
 {
 	mutex = thread::newMutex();
+	threadName = "AudioPool";
 }
 
 Audio::PoolThread::~PoolThread()

+ 4 - 19
jni/love/src/modules/audio/openal/Source.cpp

@@ -255,10 +255,7 @@ bool Source::update()
 
 			alGetSourcef(source, AL_SAMPLE_OFFSET, &curOffsetSamples);
 
-			ALint b;
-			alGetSourcei(source, AL_BUFFER, &b);
-			int freq;
-			alGetBufferi(b, AL_FREQUENCY, &freq);
+			int freq = decoder->getSampleRate();
 			curOffsetSecs = curOffsetSamples / freq;
 
 			// Get a free buffer.
@@ -334,11 +331,7 @@ void Source::seekAtomic(float offset, void *unit)
 			if (type == TYPE_STREAM)
 			{
 				offsetSamples = offset;
-				ALint buffer;
-				alGetSourcei(source, AL_BUFFER, &buffer);
-				int freq;
-				alGetBufferi(buffer, AL_FREQUENCY, &freq);
-				offset /= freq;
+				offset /= decoder->getSampleRate();
 				offsetSeconds = offset;
 				decoder->seek(offset);
 			}
@@ -353,11 +346,7 @@ void Source::seekAtomic(float offset, void *unit)
 			{
 				offsetSeconds = offset;
 				decoder->seek(offset);
-				ALint buffer;
-				alGetSourcei(source, AL_BUFFER, &buffer);
-				int freq;
-				alGetBufferi(buffer, AL_FREQUENCY, &freq);
-				offsetSamples = offset*freq;
+				offsetSamples = offset * decoder->getSampleRate();
 			}
 			else
 			{
@@ -399,11 +388,7 @@ float Source::tellAtomic(void *unit) const
 		default:
 			{
 				alGetSourcef(source, AL_SAMPLE_OFFSET, &offset);
-				ALint buffer;
-				alGetSourcei(source, AL_BUFFER, &buffer);
-				int freq;
-				alGetBufferi(buffer, AL_FREQUENCY, &freq);
-				offset /= freq;
+				offset /= decoder->getSampleRate();
 				if (type == TYPE_STREAM) offset += offsetSeconds;
 			}
 			break;

+ 32 - 0
jni/love/src/modules/event/sdl/Event.cpp

@@ -59,6 +59,34 @@ static void windowToPixelCoords(int *x, int *y)
 #endif
 }
 
+// SDL's event watch callbacks trigger when the event is actually posted inside
+// SDL, unlike with SDL_PollEvents. This is useful for some events which require
+// handling inside the function which triggered them on some backends.
+static int SDLCALL watchAppEvents(void * /*udata*/, SDL_Event *event)
+{
+	graphics::Graphics *gfx = (graphics::Graphics *) Module::findInstance("love.graphics.");
+
+	switch (event->type)
+	{
+	// On iOS, calling any OpenGL ES function after the function which triggers
+	// SDL_APP_DIDENTERBACKGROUND is called will kill the app, so we handle it
+	// with an event watch callback, which will be called inside that function.
+	case SDL_APP_DIDENTERBACKGROUND:
+	case SDL_APP_WILLENTERFOREGROUND:
+		// On Android, before calling setActive we should probably also call a
+		// window function to un-set the graphics mode and delete the context,
+		// after DIDENTERBACKGROUND (and the reverse after WILLENTERFOREGROUND.)
+		if (gfx)
+			gfx->setActive(event->type == SDL_APP_WILLENTERFOREGROUND);
+		break;
+	default:
+		break;
+	}
+
+	// Don't prevent the event from being processed further.
+	return 1;
+}
+
 
 const char *Event::getName() const
 {
@@ -69,10 +97,13 @@ Event::Event()
 {
 	if (SDL_InitSubSystem(SDL_INIT_EVENTS) < 0)
 		throw love::Exception("%s", SDL_GetError());
+
+	SDL_AddEventWatch(watchAppEvents, this);
 }
 
 Event::~Event()
 {
+	SDL_DelEventWatch(watchAppEvents, this);
 	SDL_QuitSubSystem(SDL_INIT_EVENTS);
 }
 
@@ -260,6 +291,7 @@ Message *Event::convert(const SDL_Event &e) const
 		SDL_free(e.drop.file);
 		break;
 	case SDL_QUIT:
+	case SDL_APP_TERMINATING:
 		msg = new Message("quit");
 		break;
 	case SDL_APP_LOWMEMORY:

+ 37 - 12
jni/love/src/modules/filesystem/physfs/Filesystem.cpp

@@ -279,6 +279,11 @@ bool Filesystem::setIdentity(const char *ident, bool appendToPath)
 	// (No error on fail, it means that the path doesn't exist).
 	PHYSFS_addToSearchPath(save_path_full.c_str(), appendToPath);
 
+	// HACK: This forces setupWriteDirectory to be called the next time a file
+	// is opened for writing - otherwise it won't be called at all if it was
+	// already called at least once before.
+	PHYSFS_setWriteDir(nullptr);
+
 	return true;
 }
 
@@ -349,16 +354,36 @@ bool Filesystem::setupWriteDirectory()
 	if (save_identity.empty() || save_path_full.empty() || save_path_relative.empty())
 		return false;
 
-	// Set the appdata folder as writable directory.
+	// We need to make sure the write directory is created. To do that, we also
+	// need to make sure all its parent directories are also created.
+	std::string temp_writedir = getDriveRoot(save_path_full);
+	std::string temp_createdir = skipDriveRoot(save_path_full);
+
+	// On some sandboxed platforms, physfs will break when its write directory
+	// is the root of the drive and it tries to create a folder (even if the
+	// folder's path is in a writable location.) If the user's home folder is
+	// in the save path, we'll try starting from there instead.
+	if (save_path_full.find(getUserDirectory()) == 0)
+	{
+		temp_writedir = getUserDirectory();
+		temp_createdir = save_path_full.substr(getUserDirectory().length());
+
+		// Strip leading '/' characters from the path we want to create.
+		size_t startpos = temp_createdir.find_first_not_of('/');
+		if (startpos != std::string::npos)
+			temp_createdir = temp_createdir.substr(startpos);
+	}
+
+	// Set either '/' or the user's home as a writable directory.
 	// (We must create the save folder before mounting it).
-	if (!PHYSFS_setWriteDir(getDriveRoot(save_path_full).c_str()))
+	if (!PHYSFS_setWriteDir(temp_writedir.c_str()))
 		return false;
 
-	// Create the save folder. (We're now "at" %APPDATA%).
-	if (!createDirectory(skipDriveRoot(save_path_full).c_str()))
+	// Create the save folder. (We're now "at" either '/' or the user's home).
+	if (!createDirectory(temp_createdir.c_str()))
 	{
 		// Clear the write directory in case of error.
-		PHYSFS_setWriteDir(0);
+		PHYSFS_setWriteDir(nullptr);
 		return false;
 	}
 
@@ -369,7 +394,7 @@ bool Filesystem::setupWriteDirectory()
 	// Add the directory. (Will not be readded if already present).
 	if (!PHYSFS_addToSearchPath(save_path_full.c_str(), 0))
 	{
-		PHYSFS_setWriteDir(0); // Clear the write directory in case of error.
+		PHYSFS_setWriteDir(nullptr); // Clear the write directory in case of error.
 		return false;
 	}
 
@@ -505,12 +530,12 @@ const char *Filesystem::getWorkingDirectory()
 	return cwd.c_str();
 }
 
-const char *Filesystem::getUserDirectory()
+std::string Filesystem::getUserDirectory()
 {
-	return PHYSFS_getUserDir();
+	return std::string(PHYSFS_getUserDir());
 }
 
-const char *Filesystem::getAppdataDirectory()
+std::string Filesystem::getAppdataDirectory()
 {
 #ifdef LOVE_WINDOWS
 	if (appdata.empty())
@@ -519,7 +544,7 @@ const char *Filesystem::getAppdataDirectory()
 		appdata = to_utf8(w_appdata);
 		replace_char(appdata, '\\', '/');
 	}
-	return appdata.c_str();
+	return appdata;
 #elif defined(LOVE_MACOSX)
 	if (appdata.empty())
 	{
@@ -527,7 +552,7 @@ const char *Filesystem::getAppdataDirectory()
 		udir.append("/Library/Application Support");
 		appdata = udir;
 	}
-	return appdata.c_str();
+	return appdata;
 #elif defined(LOVE_LINUX)
 	if (appdata.empty())
 	{
@@ -537,7 +562,7 @@ const char *Filesystem::getAppdataDirectory()
 		else
 			appdata = xdgdatahome;
 	}
-	return appdata.c_str();
+	return appdata;
 #else
 	return getUserDirectory();
 #endif

+ 2 - 2
jni/love/src/modules/filesystem/physfs/Filesystem.h

@@ -145,14 +145,14 @@ public:
 	/**
 	 * Gets the user home directory.
 	 **/
-	const char *getUserDirectory();
+	std::string getUserDirectory();
 
 	/**
 	 * Gets the APPDATA directory. On Windows, this is the folder
 	 * in the %APPDATA% enviroment variable. On Linux, this is the
 	 * user home folder.
 	 **/
-	const char *getAppdataDirectory();
+	std::string getAppdataDirectory();
 
 	/**
 	 * Gets the full path of the save folder.

+ 2 - 2
jni/love/src/modules/filesystem/physfs/wrap_Filesystem.cpp

@@ -211,13 +211,13 @@ int w_getWorkingDirectory(lua_State *L)
 
 int w_getUserDirectory(lua_State *L)
 {
-	lua_pushstring(L, instance->getUserDirectory());
+	luax_pushstring(L, instance->getUserDirectory());
 	return 1;
 }
 
 int w_getAppdataDirectory(lua_State *L)
 {
-	lua_pushstring(L, instance->getAppdataDirectory());
+	luax_pushstring(L, instance->getAppdataDirectory());
 	return 1;
 }
 

+ 2 - 0
jni/love/src/modules/graphics/Graphics.cpp

@@ -176,6 +176,8 @@ StringMap<Graphics::Support, Graphics::SUPPORT_MAX_ENUM>::Entry Graphics::suppor
 	{ "mipmap", Graphics::SUPPORT_MIPMAP },
 	{ "dxt", Graphics::SUPPORT_DXT },
 	{ "bc5", Graphics::SUPPORT_BC5 },
+	{ "etc1", Graphics::SUPPORT_ETC1 },
+	{ "pvrtc1", Graphics::SUPPORT_PVRTC1 },
 	{ "instancing", Graphics::SUPPORT_INSTANCING },
 	{ "srgb", Graphics::SUPPORT_SRGB },
 };

+ 16 - 0
jni/love/src/modules/graphics/Graphics.h

@@ -95,6 +95,8 @@ public:
 		SUPPORT_MIPMAP,
 		SUPPORT_DXT,
 		SUPPORT_BC5,
+		SUPPORT_ETC1,
+		SUPPORT_PVRTC1,
 		SUPPORT_INSTANCING,
 		SUPPORT_SRGB,
 		SUPPORT_MAX_ENUM
@@ -145,6 +147,20 @@ public:
 	 **/
 	virtual void unSetMode() = 0;
 
+	/**
+	 * Sets whether the module is active (internal use only.)
+	 **/
+	virtual void setActive(bool active) = 0;
+
+	/**
+	 * Gets whether the module is active. Graphics module methods are only
+	 * guaranteed to work when it is active. Calling them otherwise may cause
+	 * the program to crash (or worse.)
+	 * Normally the module will always be active as long as a window exists, it
+	 * may be different on some platforms (especially mobile ones.)
+	 **/
+	virtual bool isActive() const = 0;
+
 	static bool getConstant(const char *in, DrawMode &out);
 	static bool getConstant(DrawMode in, const char  *&out);
 

+ 0 - 20
jni/love/src/modules/graphics/Texture.cpp

@@ -109,16 +109,6 @@ bool Texture::getConstant(WrapMode in, const char  *&out)
 	return wrapModes.find(in, out);
 }
 
-bool Texture::getConstant(const char *in, Format &out)
-{
-	return formats.find(in, out);
-}
-
-bool Texture::getConstant(Format in, const char *&out)
-{
-	return formats.find(in, out);
-}
-
 StringMap<Texture::FilterMode, Texture::FILTER_MAX_ENUM>::Entry Texture::filterModeEntries[] =
 {
 	{ "linear", Texture::FILTER_LINEAR },
@@ -135,15 +125,5 @@ StringMap<Texture::WrapMode, Texture::WRAP_MAX_ENUM>::Entry Texture::wrapModeEnt
 
 StringMap<Texture::WrapMode, Texture::WRAP_MAX_ENUM> Texture::wrapModes(Texture::wrapModeEntries, sizeof(Texture::wrapModeEntries));
 
-StringMap<Texture::Format, Texture::FORMAT_MAX_ENUM>::Entry Texture::formatEntries[] =
-{
-	{"normal", Texture::FORMAT_NORMAL},
-	{"hdr", Texture::FORMAT_HDR},
-	{"srgb", Texture::FORMAT_SRGB},
-};
-
-StringMap<Texture::Format, Texture::FORMAT_MAX_ENUM> Texture::formats(Texture::formatEntries, sizeof(Texture::formatEntries));
-
-
 } // graphics
 } // love

+ 0 - 14
jni/love/src/modules/graphics/Texture.h

@@ -55,14 +55,6 @@ public:
 		FILTER_MAX_ENUM
 	};
 
-	enum Format
-	{
-		FORMAT_NORMAL,
-		FORMAT_HDR,
-		FORMAT_SRGB,
-		FORMAT_MAX_ENUM
-	};
-
 	struct Filter
 	{
 		Filter();
@@ -119,9 +111,6 @@ public:
 	static bool getConstant(const char *in, WrapMode &out);
 	static bool getConstant(WrapMode in, const char  *&out);
 
-	static bool getConstant(const char *in, Format &out);
-	static bool getConstant(Format in, const char *&out);
-
 protected:
 
 	int width;
@@ -143,9 +132,6 @@ private:
 	static StringMap<WrapMode, WRAP_MAX_ENUM>::Entry wrapModeEntries[];
 	static StringMap<WrapMode, WRAP_MAX_ENUM> wrapModes;
 
-	static StringMap<Format, FORMAT_MAX_ENUM>::Entry formatEntries[];
-	static StringMap<Format, FORMAT_MAX_ENUM> formats;
-
 }; // Texture
 
 } // graphics

+ 237 - 43
jni/love/src/modules/graphics/opengl/Canvas.cpp

@@ -451,7 +451,7 @@ static void getStrategy()
 	}
 }
 
-Canvas::Canvas(int width, int height, Texture::Format format, int fsaa)
+Canvas::Canvas(int width, int height, Format format, int fsaa)
 	: fbo(0)
     , resolve_fbo(0)
 	, texture(0)
@@ -556,36 +556,31 @@ bool Canvas::loadVolatile()
 	glGenTextures(1, &texture);
 	gl.bindTexture(texture);
 
+	if (GLAD_ANGLE_texture_usage)
+		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_USAGE_ANGLE, GL_FRAMEBUFFER_ATTACHMENT_ANGLE);
+
 	setFilter(filter);
 	setWrap(wrap);
 
-	GLint internalformat;
-	GLenum textype;
-	switch (format)
-	{
-	case Texture::FORMAT_HDR:
-		internalformat = GL_RGBA16F;
-		textype = GL_FLOAT;
-		break;
-	case Texture::FORMAT_SRGB:
-		internalformat = GL_SRGB_ALPHA;
-		textype = GL_UNSIGNED_BYTE;
-		break;
-	case Texture::FORMAT_NORMAL:
-	default:
-		internalformat = GL_RGBA;
-		textype = GL_UNSIGNED_BYTE;
-	}
+	GLenum internalformat = GL_RGBA;
+	GLenum externalformat = GL_RGBA;
+	GLenum textype = GL_UNSIGNED_BYTE;
+
+	convertFormat(format, internalformat, externalformat, textype);
+
+	// in ES2, the internalformat and format params of TexImage have to match.
+	if (GLAD_ES_VERSION_2_0 && !GLAD_ES_VERSION_3_0)
+		internalformat = externalformat;
 
 	while (glGetError() != GL_NO_ERROR)
 		/* Clear the error buffer. */;
 
 	glTexImage2D(GL_TEXTURE_2D,
 	             0,
-	             internalformat,
+	             (GLint) internalformat,
 	             width, height,
 	             0,
-	             GL_RGBA,
+	             externalformat,
 	             textype,
 	             nullptr);
 
@@ -736,10 +731,13 @@ void Canvas::setupGrab()
 	gl.matrices.projection.push(Matrix::ortho(0.0, width, 0.0, height));
 
 	// Make sure the correct sRGB setting is used when drawing to the canvas.
-	if (format == FORMAT_SRGB)
-		glEnable(GL_FRAMEBUFFER_SRGB);
-	else if (screenHasSRGB)
-		glDisable(GL_FRAMEBUFFER_SRGB);
+	if (GLAD_VERSION_1_1 || GLAD_EXT_sRGB_write_control)
+	{
+		if (format == FORMAT_SRGB)
+			glEnable(GL_FRAMEBUFFER_SRGB);
+		else if (screenHasSRGB)
+			glDisable(GL_FRAMEBUFFER_SRGB);
+	}
 
 	if (fsaa_buffer != 0)
 		fsaa_dirty = true;
@@ -828,8 +826,11 @@ void Canvas::stopGrab(bool switchingToOtherCanvas)
 
 	if (switchingToOtherCanvas)
 	{
-		if (format == FORMAT_SRGB)
-			glDisable(GL_FRAMEBUFFER_SRGB);
+		if (GLAD_VERSION_1_1 || GLAD_EXT_sRGB_write_control)
+		{
+			if (format == FORMAT_SRGB)
+				glDisable(GL_FRAMEBUFFER_SRGB);
+		}
 	}
 	else
 	{
@@ -838,10 +839,13 @@ void Canvas::stopGrab(bool switchingToOtherCanvas)
 		current = nullptr;
 		gl.setViewport(systemViewport);
 
-		if (format == FORMAT_SRGB && !screenHasSRGB)
-			glDisable(GL_FRAMEBUFFER_SRGB);
-		else if (format != FORMAT_SRGB && screenHasSRGB)
-			glEnable(GL_FRAMEBUFFER_SRGB);
+		if (GLAD_VERSION_1_1 || GLAD_EXT_sRGB_write_control)
+		{
+			if (format == FORMAT_SRGB && !screenHasSRGB)
+				glDisable(GL_FRAMEBUFFER_SRGB);
+			else if (format != FORMAT_SRGB && screenHasSRGB)
+				glEnable(GL_FRAMEBUFFER_SRGB);
+		}
 	}
 
 	gl.matrices.projection.pop();
@@ -1007,33 +1011,196 @@ bool Canvas::resolveMSAA()
 	return true;
 }
 
+Canvas::Format Canvas::getSizedFormat(Canvas::Format format)
+{
+	switch (format)
+	{
+	case FORMAT_NORMAL:
+		// 32-bit render targets don't have guaranteed support on OpenGL ES 2.
+		if (GLAD_ES_VERSION_2_0 && !(GLAD_ES_VERSION_3_0 || GLAD_OES_rgb8_rgba8 || GLAD_ARM_rgba8))
+			return FORMAT_RGBA4;
+		else
+			return FORMAT_RGBA8;
+	case FORMAT_HDR:
+		return FORMAT_RGBA16F;
+	default:
+		return format;
+	}
+}
+
+void Canvas::convertFormat(Canvas::Format format, GLenum &internalformat, GLenum &externalformat, GLenum &type)
+{
+	format = getSizedFormat(format);
+	externalformat = GL_RGBA;
+
+	switch (format)
+	{
+	case FORMAT_RGBA8:
+	default:
+		internalformat = GL_RGBA8;
+		type = GL_UNSIGNED_BYTE;
+		break;
+	case FORMAT_RGBA4:
+		internalformat = GL_RGBA4;
+		type = GL_UNSIGNED_SHORT_4_4_4_4;
+		break;
+	case FORMAT_RGB5A1:
+		internalformat = GL_RGB5_A1;
+		type = GL_UNSIGNED_SHORT_5_5_5_1;
+		break;
+	case FORMAT_RGB565:
+		internalformat = GL_RGB565;
+		externalformat = GL_RGB;
+		type = GL_UNSIGNED_SHORT_5_6_5;
+		break;
+	case FORMAT_RGB10A2:
+		internalformat = GL_RGB10_A2;
+		type = GL_UNSIGNED_INT_2_10_10_10_REV;
+		break;
+	case FORMAT_RG11B10F:
+		internalformat = GL_R11F_G11F_B10F;
+		externalformat = GL_RGB;
+		type = GL_UNSIGNED_INT_10F_11F_11F_REV;
+		break;
+	case FORMAT_RGBA16F:
+		internalformat = GL_RGBA16F;
+		if (GLAD_OES_texture_float)
+			type = GL_HALF_FLOAT_OES;
+		else if (GLAD_VERSION_1_1)
+			type = GL_FLOAT;
+		else
+			type = GL_HALF_FLOAT;
+		break;
+	case FORMAT_RGBA32F:
+		internalformat = GL_RGBA32F;
+		type = GL_FLOAT;
+		break;
+	case FORMAT_SRGB:
+		internalformat = GL_SRGB8_ALPHA8;
+		type = GL_UNSIGNED_BYTE;
+		if (GLAD_ES_VERSION_2_0)
+			externalformat = GL_SRGB_ALPHA;
+		break;
+	}
+}
+
 bool Canvas::isSupported()
 {
 	getStrategy();
 	return (strategy != &strategyNone);
 }
 
-bool Canvas::isHDRSupported()
+bool Canvas::isMultiCanvasSupported()
 {
-	return GLAD_VERSION_3_0 || (isSupported() && GLAD_ARB_texture_float);
+	// system must support at least 4 simultanious active canvases.
+	return gl.getMaxRenderTargets() >= 4;
 }
 
-bool Canvas::isSRGBSupported()
-{
-	if (GLAD_VERSION_3_0)
-		return true;
+bool Canvas::supportedFormats[] = {false};
+bool Canvas::checkedFormats[] = {false};
 
+bool Canvas::isFormatSupported(Canvas::Format format)
+{
 	if (!isSupported())
 		return false;
 
-	return (GLAD_ARB_framebuffer_sRGB || GLAD_EXT_framebuffer_sRGB)
-		&& GLAD_EXT_texture_sRGB;
-}
+	bool supported = true;
+	format = getSizedFormat(format);
 
-bool Canvas::isMultiCanvasSupported()
-{
-	// system must support at least 4 simultanious active canvases.
-	return gl.getMaxRenderTargets() >= 4;
+	switch (format)
+	{
+	case FORMAT_RGBA8:
+		if (GLAD_VERSION_1_1)
+			supported = true;
+		else
+			supported = GLAD_ES_VERSION_3_0 || GLAD_OES_rgb8_rgba8 || GLAD_ARM_rgba8;
+		break;
+	case FORMAT_RGBA4:
+	case FORMAT_RGB5A1:
+		supported = true;
+		break;
+	case FORMAT_RGB565:
+		supported = GLAD_ES_VERSION_2_0 || GLAD_VERSION_4_2 || GLAD_ARB_ES2_compatibility;
+		break;
+	case FORMAT_RGB10A2:
+		if (GLAD_VERSION_1_1)
+			supported = true;
+		else
+			supported = GLAD_ES_VERSION_3_0;
+		break;
+	case FORMAT_RG11B10F:
+		if (GLAD_VERSION_1_1)
+			supported = GLAD_VERSION_3_0 || GLAD_EXT_packed_float;
+		else
+			supported = GLAD_ES_VERSION_3_0 /*|| GLAD_NV_packed_float*/;
+		break;
+	case FORMAT_RGBA16F:
+		if (GLAD_VERSION_1_1)
+			supported = GLAD_VERSION_3_0 || GLAD_ARB_texture_float;
+		else
+			supported = GLAD_EXT_color_buffer_half_float && (GLAD_ES_VERSION_3_0 || GLAD_OES_texture_half_float);
+		break;
+	case FORMAT_RGBA32F:
+		if (GLAD_VERSION_1_1)
+			supported = GLAD_VERSION_3_0 || GLAD_ARB_texture_float;
+		else
+			supported = false; // GLAD_EXT_color_buffer_float && GLAD_ES_VERSION_3_0;
+		break;
+	case FORMAT_SRGB:
+		if (GLAD_VERSION_1_1)
+			supported = GLAD_VERSION_3_0 || ((GLAD_ARB_framebuffer_sRGB || GLAD_EXT_framebuffer_sRGB)
+				&& (GLAD_VERSION_2_1 || GLAD_EXT_texture_sRGB));
+		else
+			supported = GLAD_ES_VERSION_3_0 || GLAD_EXT_sRGB;
+		break;
+	default:
+		supported = false;
+		break;
+	}
+
+	if (!supported)
+		return false;
+
+	if (checkedFormats[format])
+		return supportedFormats[format];
+
+	// Even though we might have the necessary OpenGL version or extension,
+	// 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.
+
+	GLenum internalformat = GL_RGBA;
+	GLenum externalformat = GL_RGBA;
+	GLenum textype = GL_UNSIGNED_BYTE;
+	convertFormat(format, internalformat, externalformat, textype);
+
+	if (GLAD_ES_VERSION_2_0 && !GLAD_ES_VERSION_3_0)
+		internalformat = externalformat;
+
+	GLuint texture = 0;
+	glGenTextures(1, &texture);
+	gl.bindTexture(texture);
+
+	Texture::Filter f;
+	f.min = f.mag = Texture::FILTER_NEAREST;
+	gl.setTextureFilter(f);
+
+	Texture::Wrap w;
+	gl.setTextureWrap(w);
+
+	glTexImage2D(GL_TEXTURE_2D, 0, internalformat, 2, 2, 0, externalformat, textype, nullptr);
+
+	GLuint fbo = 0;
+	supported = (strategy->createFBO(fbo, texture) == GL_FRAMEBUFFER_COMPLETE);
+	strategy->deleteFBO(fbo, 0, 0);
+
+	gl.deleteTexture(texture);
+
+	// Cache the result so we don't do this for every isFormatSupported call.
+	checkedFormats[format] = true;
+	supportedFormats[format] = supported;
+
+	return supported;
 }
 
 void Canvas::bindDefaultCanvas()
@@ -1042,6 +1209,33 @@ void Canvas::bindDefaultCanvas()
 		current->stopGrab();
 }
 
+bool Canvas::getConstant(const char *in, Format &out)
+{
+	return formats.find(in, out);
+}
+
+bool Canvas::getConstant(Format in, const char *&out)
+{
+	return formats.find(in, out);
+}
+
+StringMap<Canvas::Format, Canvas::FORMAT_MAX_ENUM>::Entry Canvas::formatEntries[] =
+{
+	{"normal", Canvas::FORMAT_NORMAL},
+	{"hdr", Canvas::FORMAT_HDR},
+	{"rgba8", Canvas::FORMAT_RGBA8},
+	{"rgba4", Canvas::FORMAT_RGBA4},
+	{"rgb5a1", Canvas::FORMAT_RGB5A1},
+	{"rgb565", Canvas::FORMAT_RGB565},
+	{"rgb10a2", Canvas::FORMAT_RGB10A2},
+	{"rg11b10f", Canvas::FORMAT_RG11B10F},
+	{"rgba16f", Canvas::FORMAT_RGBA16F},
+	{"rgba32f", Canvas::FORMAT_RGBA32F},
+	{"srgb", Canvas::FORMAT_SRGB},
+};
+
+StringMap<Canvas::Format, Canvas::FORMAT_MAX_ENUM> Canvas::formats(Canvas::formatEntries, sizeof(Canvas::formatEntries));
+
 } // opengl
 } // graphics
 } // love

+ 33 - 4
jni/love/src/modules/graphics/opengl/Canvas.h

@@ -25,6 +25,7 @@
 #include "image/Image.h"
 #include "image/ImageData.h"
 #include "common/Matrix.h"
+#include "common/StringMap.h"
 #include "Texture.h"
 #include "OpenGL.h"
 
@@ -39,7 +40,24 @@ class Canvas : public Texture
 {
 public:
 
-	Canvas(int width, int height, Texture::Format format = Texture::FORMAT_NORMAL, int fsaa = 0);
+	// Different Canvas render target formats.
+	enum Format
+	{
+		FORMAT_NORMAL,   // Usually RGBA8 or a similar fallback. Always supported.
+		FORMAT_HDR,      // Usually RGBA16F. Not always supported.
+		FORMAT_RGBA8,    // RGBA with 8 bits per component.
+		FORMAT_RGBA4,    // RGBA with 4 bits per component.
+		FORMAT_RGB5A1,   // RGB with 5 bits per component, and A with 1 bit.
+		FORMAT_RGB565,   // RGB with 5, 6, and 5 bits each, respectively.
+		FORMAT_RGB10A2,  // RGB with 10 bits each, and A with 2 bits.
+		FORMAT_RG11B10F, // Floating point [0, +inf]. RG with 11 FP bits each, and B with 10 FP bits.
+		FORMAT_RGBA16F,  // Floating point [-inf, +inf]. RGBA with 16 FP bits per component.
+		FORMAT_RGBA32F,  // Floating point [-inf, +inf]. RGBA with 32 FP bits per component.
+		FORMAT_SRGB,     // sRGB with 8 bits per component, plus 8 bit linear A.
+		FORMAT_MAX_ENUM
+	};
+
+	Canvas(int width, int height, Format format = FORMAT_NORMAL, int fsaa = 0);
 	virtual ~Canvas();
 
 	// Implements Volatile.
@@ -85,7 +103,7 @@ public:
 		return status;
 	}
 
-	inline Texture::Format getTextureFormat() const
+	inline Format getTextureFormat() const
 	{
 		return format;
 	}
@@ -98,9 +116,8 @@ public:
 	bool resolveMSAA();
 
 	static bool isSupported();
-	static bool isHDRSupported();
-	static bool isSRGBSupported();
 	static bool isMultiCanvasSupported();
+	static bool isFormatSupported(Format format);
 
 	static Canvas *current;
 	static void bindDefaultCanvas();
@@ -111,10 +128,16 @@ public:
 	// Whether the main screen should have linear -> sRGB conversions enabled.
 	static bool screenHasSRGB;
 
+	static bool getConstant(const char *in, Format &out);
+	static bool getConstant(Format in, const char *&out);
+
 private:
 
 	bool createFSAAFBO(GLenum internalformat);
 
+	static Format getSizedFormat(Format format);
+	static void convertFormat(Format format, GLenum &internalformat, GLenum &externalformat, GLenum &type);
+
 	GLuint fbo;
 	GLuint resolve_fbo;
 
@@ -134,6 +157,12 @@ private:
 	void setupGrab();
 	void drawv(const Matrix &t, const Vertex *v);
 
+	static bool supportedFormats[FORMAT_MAX_ENUM];
+	static bool checkedFormats[FORMAT_MAX_ENUM];
+
+	static StringMap<Format, FORMAT_MAX_ENUM>::Entry formatEntries[];
+	static StringMap<Format, FORMAT_MAX_ENUM> formats;
+
 }; // Canvas
 
 } // opengl

+ 35 - 22
jni/love/src/modules/graphics/opengl/Font.cpp

@@ -42,6 +42,7 @@ const int Font::TEXTURE_HEIGHTS[] = {128, 128, 256, 256, 512, 512,  1024};
 
 Font::Font(love::font::Rasterizer *r, const Texture::Filter &filter)
 	: rasterizer(r)
+	, indexBuffer(nullptr)
 	, height(r->getHeight())
 	, lineHeight(1)
 	, mSpacing(1)
@@ -68,6 +69,8 @@ Font::Font(love::font::Rasterizer *r, const Texture::Filter &filter)
 	textureWidth = TEXTURE_WIDTHS[textureSizeIndex];
 	textureHeight = TEXTURE_HEIGHTS[textureSizeIndex];
 
+	indexBuffer = new VertexIndex(20);
+
 	love::font::GlyphData *gd = nullptr;
 
 	try
@@ -93,6 +96,7 @@ Font::Font(love::font::Rasterizer *r, const Texture::Filter &filter)
 
 Font::~Font()
 {
+	delete indexBuffer;
 	rasterizer->release();
 	unloadVolatile();
 }
@@ -368,28 +372,17 @@ void Font::print(const std::string &text, float x, float y, float extra_spacing,
 	// second (using the struct's < operator).
 	std::sort(glyphinfolist.begin(), glyphinfolist.end());
 
-	std::vector<uint16> indices;
-
-	int indicescount = 0;
+	int maxvertices = 0;
 	for (auto it = glyphinfolist.begin(); it != glyphinfolist.end(); ++it)
-	{
-		if ((it->vertexcount / 4) * 6 > indicescount)
-			indicescount = (it->vertexcount / 4) * 6;
-	}
+		maxvertices = std::max(maxvertices, it->vertexcount);
 
-	indices.reserve(indicescount);
-
-	for (int i = 0; i < indicescount / 6; i++)
+	// If the index buffer is too small for the number of glyphs we're going to
+	// draw, we need to make a new one that has the correct size.
+	if ((size_t) maxvertices / 4 > indexBuffer->getSize())
 	{
-		// First triangle.
-		indices.push_back(i * 4 + 0);
-		indices.push_back(i * 4 + 1);
-		indices.push_back(i * 4 + 2);
-
-		// Second triangle.
-		indices.push_back(i * 4 + 0);
-		indices.push_back(i * 4 + 2);
-		indices.push_back(i * 4 + 3);
+		VertexIndex *newIndexBuffer = new VertexIndex(maxvertices / 4);
+		delete indexBuffer;
+		indexBuffer = newIndexBuffer;
 	}
 
 	gl.matrices.transform.push(gl.matrices.transform.top());
@@ -403,16 +396,36 @@ void Font::print(const std::string &text, float x, float y, float extra_spacing,
 
 	gl.prepareDraw();
 
+	VertexBuffer::Bind index_bind(*indexBuffer->getVertexBuffer());
+	GLenum elemtype = indexBuffer->getType();
+	const GLvoid *elemoffset = indexBuffer->getPointer(0);
+
+	// Optimization: we can take these calls out of the loop if we have support.
+	if (GLAD_VERSION_3_2 || GLAD_ARB_draw_elements_base_vertex)
+	{
+		gl.setVertexAttribArray(OpenGL::ATTRIB_POS, 2, GL_FLOAT, sizeof(GlyphVertex), &glyphverts[0].x);
+		gl.setVertexAttribArray(OpenGL::ATTRIB_TEXCOORD, 2, GL_FLOAT, sizeof(GlyphVertex), &glyphverts[0].s);
+	}
+
 	// We need to draw a new vertex array for every section of the string which
 	// uses a different texture than the previous section.
 	for (auto it = glyphinfolist.begin(); it != glyphinfolist.end(); ++it)
 	{
 		gl.bindTexture(it->texture);
 
-		gl.setVertexAttribArray(OpenGL::ATTRIB_POS, 2, GL_FLOAT, sizeof(GlyphVertex), (GLvoid *)&glyphverts[it->startvertex].x);
-		gl.setVertexAttribArray(OpenGL::ATTRIB_TEXCOORD, 2, GL_FLOAT, sizeof(GlyphVertex), (GLvoid *)&glyphverts[it->startvertex].s);
+		if (GLAD_VERSION_3_2 || GLAD_ARB_draw_elements_base_vertex)
+		{
+			// Optimization: setting a base vertex is faster than redoing the
+			// setVertexAttribArray calls before each draw.
+			glDrawElementsBaseVertex(GL_TRIANGLES, (it->vertexcount / 4) * 6, elemtype, elemoffset, it->startvertex);
+		}
+		else
+		{
+			gl.setVertexAttribArray(OpenGL::ATTRIB_POS, 2, GL_FLOAT, sizeof(GlyphVertex), &glyphverts[it->startvertex].x);
+			gl.setVertexAttribArray(OpenGL::ATTRIB_TEXCOORD, 2, GL_FLOAT, sizeof(GlyphVertex), &glyphverts[it->startvertex].s);
 
-		glDrawElements(GL_TRIANGLES, (it->vertexcount / 4) * 6, GL_UNSIGNED_SHORT, &indices[0]);
+			glDrawElements(GL_TRIANGLES, (it->vertexcount / 4) * 6, elemtype, elemoffset);
+		}
 	}
 
 	gl.disableVertexAttribArray(OpenGL::ATTRIB_POS);

+ 3 - 0
jni/love/src/modules/graphics/opengl/Font.h

@@ -31,6 +31,7 @@
 #include "font/Rasterizer.h"
 #include "graphics/Texture.h"
 #include "graphics/Volatile.h"
+#include "VertexBuffer.h"
 
 #include "OpenGL.h"
 
@@ -185,6 +186,8 @@ private:
 
 	love::font::Rasterizer *rasterizer;
 
+	VertexIndex *indexBuffer;
+
 	int height;
 	float lineHeight;
 	float mSpacing; // modifies the spacing by multiplying it with this value

+ 67 - 18
jni/love/src/modules/graphics/opengl/Graphics.cpp

@@ -52,6 +52,7 @@ Graphics::Graphics()
 	, width(0)
 	, height(0)
 	, created(false)
+	, active(true)
 	, activeStencil(false)
 	, savedState()
 {
@@ -72,7 +73,10 @@ Graphics::~Graphics()
 		currentFont->release();
 
 	if (Shader::defaultShader)
+	{
 		Shader::defaultShader->release();
+		Shader::defaultShader = nullptr;
+	}
 
 	currentWindow->release();
 }
@@ -235,12 +239,16 @@ bool Graphics::setMode(int width, int height, bool &sRGB)
 	}
 
 	// Set whether drawing converts input from linear -> sRGB colorspace.
-	if (GLAD_VERSION_3_0 || GLAD_ARB_framebuffer_sRGB || GLAD_EXT_framebuffer_sRGB)
+	if (GLAD_VERSION_3_0 || GLAD_ARB_framebuffer_sRGB || GLAD_EXT_framebuffer_sRGB
+		|| (GLAD_ES_VERSION_3_0 || GLAD_EXT_sRGB))
 	{
-		if (sRGB)
-			glEnable(GL_FRAMEBUFFER_SRGB);
-		else
-			glDisable(GL_FRAMEBUFFER_SRGB);
+		if (GLAD_VERSION_1_1 || GLAD_EXT_sRGB_write_control)
+		{
+			if (sRGB)
+				glEnable(GL_FRAMEBUFFER_SRGB);
+			else
+				glDisable(GL_FRAMEBUFFER_SRGB);
+		}
 	}
 	else
 		sRGB = false;
@@ -282,6 +290,23 @@ void Graphics::unSetMode()
 	created = false;
 }
 
+void Graphics::setActive(bool active)
+{
+	// Make sure all pending OpenGL commands have fully executed before
+	// returning, if we're going from active to inactive.
+	if (isCreated() && this->active && !active)
+		glFinish();
+
+	this->active = active;
+}
+
+bool Graphics::isActive() const
+{
+	// The graphics module is only completely 'active' if there's a window, a
+	// context, and the active variable is set.
+	return active && isCreated() && currentWindow && currentWindow->isCreated();
+}
+
 static void APIENTRY debugCB(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei /*len*/, const GLchar *msg, const GLvoid* /*usr*/)
 {
 	// Human-readable strings for the debug info.
@@ -364,6 +389,9 @@ void Graphics::clear()
 
 void Graphics::present()
 {
+	if (!isActive())
+		return;
+
 	// Make sure we don't have a canvas active.
 	Canvas *c = Canvas::current;
 	Canvas::bindDefaultCanvas();
@@ -463,7 +491,7 @@ void Graphics::discardStencil()
 	activeStencil = false;
 }
 
-Image *Graphics::newImage(love::image::ImageData *data, Texture::Format format)
+Image *Graphics::newImage(love::image::ImageData *data, Image::Format format)
 {
 	// Create the image.
 	Image *image = new Image(data, format);
@@ -490,7 +518,7 @@ Image *Graphics::newImage(love::image::ImageData *data, Texture::Format format)
 	return image;
 }
 
-Image *Graphics::newImage(love::image::CompressedData *cdata, Texture::Format format)
+Image *Graphics::newImage(love::image::CompressedData *cdata, Image::Format format)
 {
 	// Create the image.
 	Image *image = new Image(cdata, format);
@@ -537,13 +565,14 @@ ParticleSystem *Graphics::newParticleSystem(Texture *texture, int size)
 	return new ParticleSystem(texture, size);
 }
 
-Canvas *Graphics::newCanvas(int width, int height, Texture::Format format, int fsaa)
+Canvas *Graphics::newCanvas(int width, int height, Canvas::Format format, int fsaa)
 {
-	if (format == Texture::FORMAT_HDR && !Canvas::isHDRSupported())
-		throw Exception("HDR Canvases are not supported by your OpenGL implementation");
-
-	if (format == Texture::FORMAT_SRGB && !Canvas::isSRGBSupported())
-		throw Exception("sRGB Canvases are not supported by your OpenGL implementation");
+	if (!Canvas::isFormatSupported(format))
+	{
+		const char *fstr = "rgba8";
+		Canvas::getConstant(format, fstr);
+		throw love::Exception("The %s canvas format is not supported by your OpenGL implementation.", fstr);
+	}
 
 	if (width > gl.getMaxTextureSize())
 		throw Exception("Cannot create canvas: width of %d pixels is too large for this system.", width);
@@ -594,8 +623,8 @@ Canvas *Graphics::newCanvas(int width, int height, Texture::Format format, int f
 	}
 
 	canvas->release();
-	throw Exception(error_string.str().c_str());
-	return NULL; // never reached
+	throw Exception("%s", error_string.str().c_str());
+	return nullptr; // never reached
 }
 
 Shader *Graphics::newShader(const Shader::ShaderSources &sources)
@@ -972,7 +1001,24 @@ void Graphics::circle(DrawMode mode, float x, float y, float radius, int points)
 	float angle_shift = (two_pi / points);
 	float phi = .0f;
 
-	float *coords = new float[2 * (points + 1)];
+	float *coords = nullptr;
+	int extrapoints = 1;
+
+	// Fill mode will use a triangle fan internally, so we want the "hub" of the
+	// fan (its first vertex) to be the midpoint of the circle.
+	if (mode == DRAW_FILL)
+	{
+		extrapoints = 2;
+		coords = new float[2 * (points + extrapoints)];
+
+		coords[0] = x;
+		coords[1] = y;
+
+		coords += 2;
+	}
+	else
+		coords = new float[2 * (points + extrapoints)];
+
 	for (int i = 0; i < points; ++i, phi += angle_shift)
 	{
 		coords[2*i]   = x + radius * cosf(phi);
@@ -982,7 +1028,10 @@ void Graphics::circle(DrawMode mode, float x, float y, float radius, int points)
 	coords[2*points]   = coords[0];
 	coords[2*points+1] = coords[1];
 
-	polygon(mode, coords, (points + 1) * 2);
+	if (mode == DRAW_FILL)
+		coords -= 2;
+
+	polygon(mode, coords, 2 * (points + extrapoints));
 
 	delete[] coords;
 }
@@ -1059,7 +1108,7 @@ void Graphics::polygon(DrawMode mode, const float *coords, size_t count)
 		gl.enableVertexAttribArray(OpenGL::ATTRIB_POS);
 		gl.setVertexAttribArray(OpenGL::ATTRIB_POS, 2, GL_FLOAT, 0, (GLvoid *) coords);
 
-		glDrawArrays(GL_TRIANGLE_FAN, 0, count/2-1); // opengl will close the polygon for us
+		glDrawArrays(GL_TRIANGLE_FAN, 0, count / 2);
 
 		gl.disableVertexAttribArray(OpenGL::ATTRIB_POS);
 	}

+ 7 - 3
jni/love/src/modules/graphics/opengl/Graphics.h

@@ -118,6 +118,9 @@ public:
 	virtual bool setMode(int width, int height, bool &sRGB);
 	virtual void unSetMode();
 
+	virtual void setActive(bool active);
+	virtual bool isActive() const;
+
 	void setDebug(bool enable);
 
 	/**
@@ -194,8 +197,8 @@ public:
 	/**
 	 * Creates an Image object with padding and/or optimization.
 	 **/
-	Image *newImage(love::image::ImageData *data, Texture::Format format = Texture::FORMAT_NORMAL);
-	Image *newImage(love::image::CompressedData *cdata, Texture::Format format = Texture::FORMAT_NORMAL);
+	Image *newImage(love::image::ImageData *data, Image::Format format = Image::FORMAT_NORMAL);
+	Image *newImage(love::image::CompressedData *cdata, Image::Format format = Image::FORMAT_NORMAL);
 
 	Quad *newQuad(Quad::Viewport v, float sw, float sh);
 
@@ -208,7 +211,7 @@ public:
 
 	ParticleSystem *newParticleSystem(Texture *texture, int size);
 
-	Canvas *newCanvas(int width, int height, Texture::Format format = Texture::FORMAT_NORMAL, int fsaa = 0);
+	Canvas *newCanvas(int width, int height, Canvas::Format format = Canvas::FORMAT_NORMAL, int fsaa = 0);
 
 	Shader *newShader(const Shader::ShaderSources &sources);
 
@@ -480,6 +483,7 @@ private:
 	int width;
 	int height;
 	bool created;
+	bool active;
 
 	bool activeStencil;
 

+ 46 - 4
jni/love/src/modules/graphics/opengl/Image.cpp

@@ -36,7 +36,7 @@ float Image::maxMipmapSharpness = 0.0f;
 Texture::FilterMode Image::defaultMipmapFilter = Texture::FILTER_NONE;
 float Image::defaultMipmapSharpness = 0.0f;
 
-Image::Image(love::image::ImageData *data, Texture::Format format)
+Image::Image(love::image::ImageData *data, Format format)
 	: data(data)
 	, cdata(nullptr)
 	, paddedWidth(width)
@@ -55,7 +55,7 @@ Image::Image(love::image::ImageData *data, Texture::Format format)
 	preload();
 }
 
-Image::Image(love::image::CompressedData *cdata, Texture::Format format)
+Image::Image(love::image::CompressedData *cdata, Format format)
 	: data(nullptr)
 	, cdata(cdata)
 	, paddedWidth(width)
@@ -504,7 +504,7 @@ bool Image::refresh()
 	return true;
 }
 
-Texture::Format Image::getFormat() const
+Image::Format Image::getFormat() const
 {
 	return format;
 }
@@ -602,6 +602,20 @@ GLenum Image::getCompressedFormat(image::CompressedData::Format cformat) const
 		return GL_COMPRESSED_RG_RGTC2;
 	case image::CompressedData::FORMAT_BC5s:
 		return GL_COMPRESSED_SIGNED_RG_RGTC2;
+	case image::CompressedData::FORMAT_ETC1:
+		// The ETC2 format can load ETC1 textures.
+		if (GLAD_VERSION_4_3 || GLAD_ES_VERSION_3_0 || GLAD_ARB_ES3_compatibility)
+			return GL_COMPRESSED_RGB8_ETC2;
+		else
+			return GL_ETC1_RGB8_OES;
+	case image::CompressedData::FORMAT_PVR1_RGB2:
+		return GL_COMPRESSED_RGB_PVRTC_2BPPV1_IMG;
+	case image::CompressedData::FORMAT_PVR1_RGB4:
+		return GL_COMPRESSED_RGB_PVRTC_4BPPV1_IMG;
+	case image::CompressedData::FORMAT_PVR1_RGBA2:
+		return GL_COMPRESSED_RGBA_PVRTC_2BPPV1_IMG;
+	case image::CompressedData::FORMAT_PVR1_RGBA4:
+		return GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG;
 	default:
 		if (srgb)
 			return GL_SRGB_ALPHA;
@@ -635,14 +649,24 @@ bool Image::hasCompressedTextureSupport(image::CompressedData::Format format)
 	switch (format)
 	{
 	case image::CompressedData::FORMAT_DXT1:
+		return GLAD_EXT_texture_compression_s3tc || GLAD_EXT_texture_compression_dxt1;
 	case image::CompressedData::FORMAT_DXT3:
+		return GLAD_EXT_texture_compression_s3tc || GLAD_ANGLE_texture_compression_dxt3;
 	case image::CompressedData::FORMAT_DXT5:
-		return GLAD_EXT_texture_compression_s3tc;
+		return GLAD_EXT_texture_compression_s3tc || GLAD_ANGLE_texture_compression_dxt5;
 	case image::CompressedData::FORMAT_BC4:
 	case image::CompressedData::FORMAT_BC4s:
 	case image::CompressedData::FORMAT_BC5:
 	case image::CompressedData::FORMAT_BC5s:
 		return (GLAD_VERSION_3_0 || GLAD_ARB_texture_compression_rgtc || GLAD_EXT_texture_compression_rgtc);
+	case image::CompressedData::FORMAT_ETC1:
+		// ETC2 support guarantees ETC1 support as well.
+		return GLAD_VERSION_4_3 || GLAD_ES_VERSION_3_0 || GLAD_ARB_ES3_compatibility || GLAD_OES_compressed_ETC1_RGB8_texture;
+	case image::CompressedData::FORMAT_PVR1_RGB2:
+	case image::CompressedData::FORMAT_PVR1_RGB4:
+	case image::CompressedData::FORMAT_PVR1_RGBA2:
+	case image::CompressedData::FORMAT_PVR1_RGBA4:
+		return GLAD_IMG_texture_compression_pvrtc;
 	default:
 		break;
 	}
@@ -655,6 +679,24 @@ bool Image::hasSRGBSupport()
 	return GLAD_VERSION_2_1 || GLAD_EXT_texture_sRGB || GLAD_EXT_sRGB;
 }
 
+bool Image::getConstant(const char *in, Format &out)
+{
+	return formats.find(in, out);
+}
+
+bool Image::getConstant(Format in, const char *&out)
+{
+	return formats.find(in, out);
+}
+
+StringMap<Image::Format, Image::FORMAT_MAX_ENUM>::Entry Image::formatEntries[] =
+{
+	{"normal", Image::FORMAT_NORMAL},
+	{"srgb", Image::FORMAT_SRGB},
+};
+
+StringMap<Image::Format, Image::FORMAT_MAX_ENUM> Image::formats(Image::formatEntries, sizeof(Image::formatEntries));
+
 } // opengl
 } // graphics
 } // love

+ 19 - 5
jni/love/src/modules/graphics/opengl/Image.h

@@ -25,6 +25,7 @@
 #include "common/config.h"
 #include "common/Matrix.h"
 #include "common/Vector.h"
+#include "common/StringMap.h"
 #include "common/math.h"
 #include "image/ImageData.h"
 #include "image/CompressedData.h"
@@ -50,20 +51,27 @@ class Image : public Texture
 {
 public:
 
+	enum Format
+	{
+		FORMAT_NORMAL,
+		FORMAT_SRGB,
+		FORMAT_MAX_ENUM
+	};
+
 	/**
 	 * Creates a new Image. Not that anything is ready to use
 	 * before load is called.
 	 *
 	 * @param data The data from which to load the image.
 	 **/
-	Image(love::image::ImageData *data, Texture::Format format = Texture::FORMAT_NORMAL);
+	Image(love::image::ImageData *data, Format format = FORMAT_NORMAL);
 
 	/**
 	 * Creates a new Image with compressed image data.
 	 *
 	 * @param cdata The compressed data from which to load the image.
 	 **/
-	Image(love::image::CompressedData *cdata, Texture::Format format = Texture::FORMAT_NORMAL);
+	Image(love::image::CompressedData *cdata, Format format = FORMAT_NORMAL);
 
 	/**
 	 * Destructor. Deletes the hardware texture and other resources.
@@ -120,7 +128,7 @@ public:
 	 **/
 	bool refresh();
 
-	Texture::Format getFormat() const;
+	Format getFormat() const;
 
 	static void setDefaultMipmapSharpness(float sharpness);
 	static float getDefaultMipmapSharpness();
@@ -136,6 +144,9 @@ public:
 
 	static bool hasSRGBSupport();
 
+	static bool getConstant(const char *in, Format &out);
+	static bool getConstant(Format in, const char *&out);
+
 private:
 
 	void uploadDefaultTexture();
@@ -165,8 +176,8 @@ private:
 	// Whether this Image is using a compressed texture.
 	bool compressed;
 
-	// The format to interpret the texture's data as.
-	Texture::Format format;
+	// The format to interpret the image's data as.
+	Format format;
 
 	// True if the image wasn't able to be properly created and it had to fall
 	// back to a default texture.
@@ -188,6 +199,9 @@ private:
 
 	GLenum getCompressedFormat(image::CompressedData::Format cformat) const;
 
+	static StringMap<Format, FORMAT_MAX_ENUM>::Entry formatEntries[];
+	static StringMap<Format, FORMAT_MAX_ENUM> formats;
+
 }; // Image
 
 } // opengl

+ 11 - 0
jni/love/src/modules/graphics/opengl/OpenGL.cpp

@@ -160,6 +160,7 @@ void OpenGL::initVendor()
 	}
 
 	// http://feedback.wildfiregames.com/report/opengl/feature/GL_VENDOR
+	// http://stackoverflow.com/questions/2093594/opengl-extensions-available-on-different-android-devices
 	if (strstr(vstr, "ATI Technologies"))
 		vendor = VENDOR_ATI_AMD;
 	else if (strstr(vstr, "NVIDIA"))
@@ -172,6 +173,16 @@ void OpenGL::initVendor()
 		vendor = VENDOR_APPLE;
 	else if (strstr(vstr, "Microsoft"))
 		vendor = VENDOR_MICROSOFT;
+	else if (strstr(vstr, "Imagination"))
+		vendor = VENDOR_IMGTEC;
+	else if (strstr(vstr, "ARM"))
+		vendor = VENDOR_ARM;
+	else if (strstr(vstr, "Qualcomm"))
+		vendor = VENDOR_QUALCOMM;
+	else if (strstr(vstr, "Broadcom"))
+		vendor = VENDOR_BROADCOM;
+	else if (strstr(vstr, "Vivante"))
+		vendor = VENDOR_VIVANTE;
 	else
 		vendor = VENDOR_UNKNOWN;
 }

+ 6 - 1
jni/love/src/modules/graphics/opengl/OpenGL.h

@@ -66,8 +66,13 @@ public:
 		VENDOR_NVIDIA,
 		VENDOR_INTEL,
 		VENDOR_MESA_SOFT, // Software renderer.
-		VENDOR_APPLE,     // Software renderer.
+		VENDOR_APPLE,     // Software renderer (or Apple A7 chips and newer.)
 		VENDOR_MICROSOFT, // Software renderer.
+		VENDOR_IMGTEC,
+		VENDOR_ARM,
+		VENDOR_QUALCOMM,
+		VENDOR_BROADCOM,
+		VENDOR_VIVANTE,
 		VENDOR_UNKNOWN
 	};
 

+ 2 - 3
jni/love/src/modules/graphics/opengl/Shader.cpp

@@ -101,10 +101,9 @@ Shader::~Shader()
 		detach();
 
 	for (auto it = boundRetainables.begin(); it != boundRetainables.end(); ++it)
-	{
 		it->second->release();
-		boundRetainables.erase(it);
-	}
+
+	boundRetainables.clear();
 
 	unloadVolatile();
 }

+ 12 - 4
jni/love/src/modules/graphics/opengl/VertexBuffer.cpp

@@ -189,10 +189,18 @@ void VBO::unmap()
 		is_bound = true;
 	}
 
-	// "orphan" current buffer to avoid implicit synchronisation on the GPU:
-	// http://www.seas.upenn.edu/~pcozzi/OpenGLInsights/OpenGLInsights-AsynchronousBufferTransfers.pdf
-	glBufferData(getTarget(), (GLsizeiptr) getSize(), NULL,       getUsage());
-	glBufferData(getTarget(), (GLsizeiptr) getSize(), memory_map, getUsage());
+	if (getUsage() == GL_STATIC_DRAW)
+	{
+		// Upload the mapped data to the buffer.
+		glBufferSubData(getTarget(), 0, (GLsizeiptr) getSize(), memory_map);
+	}
+	else
+	{
+		// "orphan" current buffer to avoid implicit synchronisation on the GPU:
+		// http://www.seas.upenn.edu/~pcozzi/OpenGLInsights/OpenGLInsights-AsynchronousBufferTransfers.pdf
+		glBufferData(getTarget(), (GLsizeiptr) getSize(), NULL,       getUsage());
+		glBufferData(getTarget(), (GLsizeiptr) getSize(), memory_map, getUsage());
+	}
 
 	is_mapped = false;
 }

+ 3 - 3
jni/love/src/modules/graphics/opengl/wrap_Canvas.cpp

@@ -113,10 +113,10 @@ int w_Canvas_clear(lua_State *L)
 int w_Canvas_getFormat(lua_State *L)
 {
 	Canvas *canvas = luax_checkcanvas(L, 1);
-	Texture::Format format = canvas->getTextureFormat();
+	Canvas::Format format = canvas->getTextureFormat();
 	const char *str;
-	if (!Texture::getConstant(format, str))
-		return luaL_error(L, "Unknown texture format.");
+	if (!Canvas::getConstant(format, str))
+		return luaL_error(L, "Unknown Canvas format.");
 
 	lua_pushstring(L, str);
 	return 1;

+ 36 - 11
jni/love/src/modules/graphics/opengl/wrap_Graphics.cpp

@@ -60,6 +60,12 @@ int w_isCreated(lua_State *L)
 	return 1;
 }
 
+int w_isActive(lua_State *L)
+{
+	luax_pushboolean(L, instance->isActive());
+	return 1;
+}
+
 int w_getWidth(lua_State *L)
 {
 	lua_pushinteger(L, instance->getWidth());
@@ -155,14 +161,11 @@ int w_newImage(lua_State *L)
 	love::image::ImageData *data = nullptr;
 	love::image::CompressedData *cdata = nullptr;
 
-	Texture::Format format = Texture::FORMAT_NORMAL;
+	Image::Format format = Image::FORMAT_NORMAL;
 	const char *fstr = lua_isnoneornil(L, 2) ? nullptr : luaL_checkstring(L, 2);
 
-	if (fstr != nullptr && !Texture::getConstant(fstr, format))
-		return luaL_error(L, "Invalid texture format: %s", fstr);
-
-	if (format == Texture::FORMAT_HDR) // For now...
-		return luaL_error(L, "HDR images are not supported.");
+	if (fstr != nullptr && !Image::getConstant(fstr, format))
+		return luaL_error(L, "Invalid Image format: %s", fstr);
 
 	// Convert to FileData, if necessary.
 	if (lua_isstring(L, 1) || luax_istype(L, 1, FILESYSTEM_FILE_T))
@@ -334,9 +337,9 @@ int w_newCanvas(lua_State *L)
 	const char *str = luaL_optstring(L, 3, "normal");
 	int fsaa        = luaL_optint(L, 4, 0);
 
-	Texture::Format format;
-	if (!Texture::getConstant(str, format))
-		return luaL_error(L, "Invalid texture format: %s", str);
+	Canvas::Format format;
+	if (!Canvas::getConstant(str, format))
+		return luaL_error(L, "Invalid Canvas format: %s", str);
 
 	Canvas *canvas = nullptr;
 	EXCEPT_GUARD(canvas = instance->newCanvas(width, height, format, fsaa);)
@@ -1015,7 +1018,7 @@ int w_isSupported(lua_State *L)
 				supported = false;
 			break;
 		case Graphics::SUPPORT_HDR_CANVAS:
-			if (!Canvas::isHDRSupported())
+			if (!Canvas::isFormatSupported(Canvas::FORMAT_HDR))
 				supported = false;
 			break;
 		case Graphics::SUPPORT_MULTI_CANVAS:
@@ -1046,12 +1049,20 @@ int w_isSupported(lua_State *L)
 			if (!Image::hasCompressedTextureSupport(image::CompressedData::FORMAT_BC5))
 				supported = false;
 			break;
+		case Graphics::SUPPORT_ETC1:
+			if (!Image::hasCompressedTextureSupport(image::CompressedData::FORMAT_ETC1))
+				supported = false;
+			break;
+		case Graphics::SUPPORT_PVRTC1:
+			if (!Image::hasCompressedTextureSupport(image::CompressedData::FORMAT_PVR1_RGBA4))
+				supported = false;
+			break;
 		case Graphics::SUPPORT_INSTANCING:
 			if (!(GLAD_ARB_draw_instanced || (GLAD_ES_VERSION_2_0 && GLAD_EXT_draw_instanced)))
 				supported = false;
 			break;
 		case Graphics::SUPPORT_SRGB:
-			if (!Canvas::isSRGBSupported())
+			if (!Canvas::isFormatSupported(Canvas::FORMAT_SRGB))
 				supported = false;
 			break;
 		default:
@@ -1064,6 +1075,18 @@ int w_isSupported(lua_State *L)
 	return 1;
 }
 
+int w_hasCanvasFormat(lua_State *L)
+{
+	const char *str = luaL_checkstring(L, 1);
+	Canvas::Format format;
+
+	if (!Canvas::getConstant(str, format))
+		return luaL_error(L, "Invalid canvas format: %s", str);
+
+	luax_pushboolean(L, Canvas::isFormatSupported(format));
+	return 1;
+}
+
 int w_getRendererInfo(lua_State *L)
 {
 	std::string name, version, vendor, device;
@@ -1441,6 +1464,7 @@ static const luaL_Reg functions[] =
 	{ "_setDefaultShaderCode", w_setDefaultShaderCode },
 
 	{ "isSupported", w_isSupported },
+	{ "hasCanvasFormat", w_hasCanvasFormat },
 	{ "getRendererInfo", w_getRendererInfo },
 	{ "getSystemLimit", w_getSystemLimit },
 
@@ -1450,6 +1474,7 @@ static const luaL_Reg functions[] =
 	{ "printf", w_printf },
 
 	{ "isCreated", w_isCreated },
+	{ "isActive", w_isActive },
 	{ "getWidth", w_getWidth },
 	{ "getHeight", w_getHeight },
 	{ "getDimensions", w_getDimensions },

+ 2 - 0
jni/love/src/modules/graphics/opengl/wrap_Graphics.h

@@ -43,6 +43,7 @@ int w_reset(lua_State *L);
 int w_clear(lua_State *L);
 int w_present(lua_State *L);
 int w_isCreated(lua_State *L);
+int w_isActive(lua_State *L);
 int w_getWidth(lua_State *L);
 int w_getHeight(lua_State *L);
 int w_getDimensions(lua_State *L);
@@ -94,6 +95,7 @@ int w_setShader(lua_State *L);
 int w_getShader(lua_State *L);
 int w_setDefaultShaderCode(lua_State *L);
 int w_isSupported(lua_State *L);
+int w_hasCanvasFormat(lua_State *L);
 int w_getRendererInfo(lua_State *L);
 int w_getSystemLimit(lua_State *L);
 int w_draw(lua_State *L);

+ 5 - 0
jni/love/src/modules/image/CompressedData.cpp

@@ -110,6 +110,11 @@ StringMap<CompressedData::Format, CompressedData::FORMAT_MAX_ENUM>::Entry Compre
 	{"bc4s", CompressedData::FORMAT_BC4s},
 	{"bc5", CompressedData::FORMAT_BC5},
 	{"bc5s", CompressedData::FORMAT_BC5s},
+	{"etc1", CompressedData::FORMAT_ETC1},
+	{"pvr1rgb2", CompressedData::FORMAT_PVR1_RGB2},
+	{"pvr1rgb4", CompressedData::FORMAT_PVR1_RGB4},
+	{"pvr1rgba2", CompressedData::FORMAT_PVR1_RGBA2},
+	{"pvr1rgba4", CompressedData::FORMAT_PVR1_RGBA4},
 };
 
 StringMap<CompressedData::Format, CompressedData::FORMAT_MAX_ENUM> CompressedData::formats(CompressedData::formatEntries, sizeof(CompressedData::formatEntries));

+ 5 - 0
jni/love/src/modules/image/CompressedData.h

@@ -54,6 +54,11 @@ public:
 		FORMAT_BC4s,
 		FORMAT_BC5,
 		FORMAT_BC5s,
+		FORMAT_ETC1,
+		FORMAT_PVR1_RGB2,
+		FORMAT_PVR1_RGB4,
+		FORMAT_PVR1_RGBA2,
+		FORMAT_PVR1_RGBA4,
 		FORMAT_MAX_ENUM
 	};
 

+ 24 - 14
jni/love/src/modules/image/magpie/CompressedData.cpp

@@ -29,29 +29,47 @@ namespace image
 namespace magpie
 {
 
-CompressedData::CompressedData(love::filesystem::FileData *filedata)
+CompressedData::CompressedData(std::list<CompressedFormatHandler *> formats, love::filesystem::FileData *filedata)
+	: formatHandlers(formats)
 {
+	for (CompressedFormatHandler *handler : formatHandlers)
+		handler->retain();
+
 	load(filedata);
 }
 
 CompressedData::~CompressedData()
 {
 	delete[] data;
+
+	for (CompressedFormatHandler *handler : formatHandlers)
+		handler->release();
 }
 
 void CompressedData::load(love::filesystem::FileData *filedata)
 {
+	CompressedFormatHandler *parser = nullptr;
+
+	for (CompressedFormatHandler *handler : formatHandlers)
+	{
+		if (handler->canParse(filedata))
+		{
+			parser = handler;
+			break;
+		}
+	}
+
+	if (parser == nullptr)
+		throw love::Exception("Could not parse compressed data: Unknown format.");
+
 	// SubImage vector will be populated by a parser.
 	std::vector<SubImage> parsedimages;
 	Format texformat = FORMAT_UNKNOWN;
 
-	uint8 *newdata = 0;
 	size_t newdata_size = 0;
+	uint8 *newdata = parser->parse(filedata, parsedimages, newdata_size, texformat);
 
-	if (ddsHandler::canParse(filedata))
-		newdata = ddsHandler::parse(filedata, parsedimages, newdata_size, texformat);
-
-	if (newdata == 0)
+	if (newdata == nullptr)
 		throw love::Exception("Could not parse compressed data.");
 
 	if (texformat == FORMAT_UNKNOWN)
@@ -76,14 +94,6 @@ void CompressedData::load(love::filesystem::FileData *filedata)
 	format = texformat;
 }
 
-bool CompressedData::isCompressed(love::filesystem::FileData *filedata)
-{
-	if (ddsHandler::canParse(filedata))
-		return true;
-
-	return false;
-}
-
 } // magpie
 } // image
 } // love

+ 8 - 3
jni/love/src/modules/image/magpie/CompressedData.h

@@ -22,9 +22,13 @@
 #define LOVE_IMAGE_MAGPIE_COMPRESSED_DATA_H
 
 // LOVE
+#include "CompressedFormatHandler.h"
 #include "filesystem/FileData.h"
 #include "image/CompressedData.h"
 
+// C++
+#include <list>
+
 namespace love
 {
 namespace image
@@ -36,15 +40,16 @@ class CompressedData : public love::image::CompressedData
 {
 public:
 
-	CompressedData(love::filesystem::FileData *filedata);
+	CompressedData(std::list<CompressedFormatHandler *> formats, love::filesystem::FileData *filedata);
 	virtual ~CompressedData();
 
-	static bool isCompressed(love::filesystem::FileData *filedata);
-
 private:
 
 	void load(love::filesystem::FileData *filedata);
 
+	// Compressed image format handlers we use for parsing.
+	std::list<CompressedFormatHandler *> formatHandlers;
+
 }; // CompressedData
 
 } // magpie

+ 74 - 0
jni/love/src/modules/image/magpie/CompressedFormatHandler.h

@@ -0,0 +1,74 @@
+/**
+ * Copyright (c) 2006-2014 LOVE Development Team
+ *
+ * This software is provided 'as-is', without any express or implied
+ * warranty.  In no event will the authors be held liable for any damages
+ * arising from the use of this software.
+ *
+ * Permission is granted to anyone to use this software for any purpose,
+ * including commercial applications, and to alter it and redistribute it
+ * freely, subject to the following restrictions:
+ *
+ * 1. The origin of this software must not be misrepresented; you must not
+ *    claim that you wrote the original software. If you use this software
+ *    in a product, an acknowledgment in the product documentation would be
+ *    appreciated but is not required.
+ * 2. Altered source versions must be plainly marked as such, and must not be
+ *    misrepresented as being the original software.
+ * 3. This notice may not be removed or altered from any source distribution.
+ **/
+
+#ifndef LOVE_IMAGE_MAGPIE_COMPRESSED_HANDLER_H
+#define LOVE_IMAGE_MAGPIE_COMPRESSED_HANDLER_H
+
+// LOVE
+#include "filesystem/FileData.h"
+#include "image/CompressedData.h"
+#include "common/Object.h"
+
+namespace love
+{
+namespace image
+{
+namespace magpie
+{
+
+/**
+ * Base class for all CompressedData parser library interfaces.
+ * We inherit from love::Object to take advantage of reference counting...
+ **/
+class CompressedFormatHandler : public love::Object
+{
+public:
+
+	CompressedFormatHandler() {}
+	virtual ~CompressedFormatHandler() {}
+
+	/**
+	 * Determines whether a particular FileData can be parsed as CompressedData
+	 * by this handler.
+	 * @param data The data to parse.
+	 **/
+	virtual bool canParse(const filesystem::FileData *data) = 0;
+
+	/**
+	 * Parses compressed image filedata into a list of sub-images and returns
+	 * a single block of memory containing all the images.
+	 *
+	 * @param[in] filedata The data to parse.
+	 * @param[out] image The list of sub-images generated. Byte data is a pointer
+	 *             to the returned data.
+	 * @param[out] dataSize The total size in bytes of the returned data.
+	 * @param[out] format The format of the Compressed Data.
+	 *
+	 * @return The single block of memory containing the parsed images.
+	 **/
+	virtual uint8 *parse(filesystem::FileData *filedata, std::vector<CompressedData::SubImage> &images, size_t &dataSize, CompressedData::Format &format) = 0;
+
+}; // CompressedFormatHandler
+
+} // magpie
+} // image
+} // love
+
+#endif // LOVE_IMAGE_MAGPIE_COMPRESSED_HANDLER_H

+ 23 - 4
jni/love/src/modules/image/magpie/Image.cpp

@@ -25,6 +25,11 @@
 
 #include "DevilHandler.h"
 
+#include "ddsHandler.h"
+#include "PVRHandler.h"
+#include "KTXHandler.h"
+#include "PKMHandler.h"
+
 namespace love
 {
 namespace image
@@ -35,14 +40,22 @@ namespace magpie
 Image::Image()
 {
 	formatHandlers.push_back(new DevilHandler);
+
+	compressedFormatHandlers.push_back(new DDSHandler);
+	compressedFormatHandlers.push_back(new PVRHandler);
+	compressedFormatHandlers.push_back(new KTXHandler);
+	compressedFormatHandlers.push_back(new PKMHandler);
 }
 
 Image::~Image()
 {
 	// ImageData objects reference the FormatHandlers in our list, so we should
 	// release them instead of deleting them completely here.
-	for (auto it = formatHandlers.begin(); it != formatHandlers.end(); ++it)
-		(*it)->release();
+	for (FormatHandler *handler : formatHandlers)
+		handler->release();
+
+	for (CompressedFormatHandler *handler : compressedFormatHandlers)
+		handler->release();
 }
 
 const char *Image::getName() const
@@ -67,12 +80,18 @@ love::image::ImageData *Image::newImageData(int width, int height, void *data, b
 
 love::image::CompressedData *Image::newCompressedData(love::filesystem::FileData *data)
 {
-	return new CompressedData(data);
+	return new CompressedData(compressedFormatHandlers, data);
 }
 
 bool Image::isCompressed(love::filesystem::FileData *data)
 {
-	return CompressedData::isCompressed(data);
+	for (CompressedFormatHandler *handler : compressedFormatHandlers)
+	{
+		if (handler->canParse(data))
+			return true;
+	}
+
+	return false;
 }
 
 } // magpie

+ 4 - 0
jni/love/src/modules/image/magpie/Image.h

@@ -24,6 +24,7 @@
 // LOVE
 #include "image/Image.h"
 #include "FormatHandler.h"
+#include "CompressedFormatHandler.h"
 
 // C++
 #include <list>
@@ -63,6 +64,9 @@ private:
 	// Image format handlers we can use for decoding and encoding ImageData.
 	std::list<FormatHandler *> formatHandlers;
 
+	// Compressed image format handers we can use for parsing CompressedData.
+	std::list<CompressedFormatHandler *> compressedFormatHandlers;
+
 }; // Image
 
 } // magpie

+ 241 - 0
jni/love/src/modules/image/magpie/KTXHandler.cpp

@@ -0,0 +1,241 @@
+/**
+ * Copyright (c) 2006-2014 LOVE Development Team
+ *
+ * This software is provided 'as-is', without any express or implied
+ * warranty.  In no event will the authors be held liable for any damages
+ * arising from the use of this software.
+ *
+ * Permission is granted to anyone to use this software for any purpose,
+ * including commercial applications, and to alter it and redistribute it
+ * freely, subject to the following restrictions:
+ *
+ * 1. The origin of this software must not be misrepresented; you must not
+ *    claim that you wrote the original software. If you use this software
+ *    in a product, an acknowledgment in the product documentation would be
+ *    appreciated but is not required.
+ * 2. Altered source versions must be plainly marked as such, and must not be
+ *    misrepresented as being the original software.
+ * 3. This notice may not be removed or altered from any source distribution.
+ **/
+
+// LOVE
+#include "KTXHandler.h"
+#include "common/int.h"
+
+// C
+#include <string.h>
+
+// C++
+#include <algorithm>
+
+namespace love
+{
+namespace image
+{
+namespace magpie
+{
+
+namespace
+{
+
+#define KTX_IDENTIFIER_REF {0xAB, 0x4B, 0x54, 0x58, 0x20, 0x31, 0x31, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A}
+#define KTX_ENDIAN_REF     (0x04030201)
+#define KTX_ENDIAN_REF_REV (0x01020304)
+#define KTX_HEADER_SIZE    (64)
+
+struct KTXHeader
+{
+	uint8  identifier[12];
+	uint32 endianness;
+	uint32 glType;
+	uint32 glTypeSize;
+	uint32 glFormat;
+	uint32 glInternalFormat;
+	uint32 glBaseInternalFormat;
+	uint32 pixelWidth;
+	uint32 pixelHeight;
+	uint32 pixelDepth;
+	uint32 numberOfArrayElements;
+	uint32 numberOfFaces;
+	uint32 numberOfMipmapLevels;
+	uint32 bytesOfKeyValueData;
+};
+
+static_assert(sizeof(KTXHeader) == KTX_HEADER_SIZE, "Real size of KTX header doesn't match struct size!");
+
+enum KTXGLInternalFormat
+{
+	KTX_GL_ETC1_RGB8_OES = 0x8D64,
+
+	// LOVE doesn't support EAC or ETC2 yet, but it won't be hard to add.
+	KTX_GL_COMPRESSED_R11_EAC                        = 0x9270,
+	KTX_GL_COMPRESSED_SIGNED_R11_EAC                 = 0x9271,
+	KTX_GL_COMPRESSED_RG11_EAC                       = 0x9272,
+	KTX_GL_COMPRESSED_SIGNED_RG11_EAC                = 0x9273,
+	KTX_GL_COMPRESSED_RGB8_ETC2                      = 0x9274,
+	KTX_GL_COMPRESSED_SRGB8_ETC2                     = 0x9275,
+	KTX_GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2  = 0x9276,
+	KTX_GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2 = 0x9277,
+	KTX_GL_COMPRESSED_RGBA8_ETC2_EAC                 = 0x9278,
+	KTX_GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC          = 0x9279,
+
+	// I don't know if any KTX file contains PVR data, but why not support it.
+	KTX_GL_COMPRESSED_RGB_PVRTC_4BPPV1_IMG  = 0x8C00,
+	KTX_GL_COMPRESSED_RGB_PVRTC_2BPPV1_IMG  = 0x8C01,
+	KTX_GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG = 0x8C02,
+	KTX_GL_COMPRESSED_RGBA_PVRTC_2BPPV1_IMG = 0x8C03,
+
+	// Same with DXT1/3/5.
+	KTX_GL_COMPRESSED_RGB_S3TC_DXT1_EXT  = 0x83F0,
+	KTX_GL_COMPRESSED_RGBA_S3TC_DXT3_EXT = 0x83F2,
+	KTX_GL_COMPRESSED_RGBA_S3TC_DXT5_EXT = 0x83F3
+};
+
+CompressedData::Format convertFormat(uint32 glformat)
+{
+	switch (glformat)
+	{
+	case KTX_GL_ETC1_RGB8_OES:
+		return CompressedData::FORMAT_ETC1;
+	case KTX_GL_COMPRESSED_RGB_PVRTC_4BPPV1_IMG:
+		return CompressedData::FORMAT_PVR1_RGB4;
+	case KTX_GL_COMPRESSED_RGB_PVRTC_2BPPV1_IMG:
+		return CompressedData::FORMAT_PVR1_RGB2;
+	case KTX_GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG:
+		return CompressedData::FORMAT_PVR1_RGBA4;
+	case KTX_GL_COMPRESSED_RGBA_PVRTC_2BPPV1_IMG:
+		return CompressedData::FORMAT_PVR1_RGBA2;
+	case KTX_GL_COMPRESSED_RGB_S3TC_DXT1_EXT:
+		return CompressedData::FORMAT_DXT1;
+	case KTX_GL_COMPRESSED_RGBA_S3TC_DXT3_EXT:
+		return CompressedData::FORMAT_DXT3;
+	case KTX_GL_COMPRESSED_RGBA_S3TC_DXT5_EXT:
+		return CompressedData::FORMAT_DXT5;
+	default:
+		return CompressedData::FORMAT_UNKNOWN;
+	}
+}
+
+} // Anonymous namespace.
+
+bool KTXHandler::canParse(const filesystem::FileData *data)
+{
+	if (data->getSize() < sizeof(KTXHeader))
+		return false;
+
+	KTXHeader *header = (KTXHeader *) data->getData();
+	uint8 ktxidentifier[12] = KTX_IDENTIFIER_REF;
+
+	if (memcmp(header->identifier, ktxidentifier, 12) != 0)
+		return false;
+
+	if (header->endianness != KTX_ENDIAN_REF && header->endianness != KTX_ENDIAN_REF_REV)
+		return false;
+
+	return true;
+}
+
+uint8 *KTXHandler::parse(filesystem::FileData *filedata, std::vector<CompressedData::SubImage> &images, size_t &dataSize, CompressedData::Format &format)
+{
+	if (!canParse(filedata))
+		throw love::Exception("Could not decode compressed data (not a KTX file?)");
+
+	KTXHeader header = *(KTXHeader *) filedata->getData();
+
+	if (header.endianness == KTX_ENDIAN_REF_REV)
+	{
+		uint32 *headerArray = (uint32 *) &header.glType;
+		for (int i = 0; i < 12; i++)
+			headerArray[i] = swap32(headerArray[i]);
+	}
+
+	header.numberOfMipmapLevels = std::max(header.numberOfMipmapLevels, 1u);
+
+	CompressedData::Format cformat = convertFormat(header.glInternalFormat);
+
+	if (cformat == CompressedData::FORMAT_UNKNOWN)
+		throw love::Exception("Unsupported image format in KTX file.");
+
+	if (header.numberOfArrayElements > 0)
+		throw love::Exception("Texture arrays in KTX files are not supported.");
+
+	if (header.pixelDepth > 1)
+		throw love::Exception("3D textures in KTX files are not supported.");
+
+	if (header.numberOfFaces > 1)
+		throw love::Exception("Cubemap textures in KTX files are not supported.");
+
+	size_t fileoffset = sizeof(KTXHeader) + header.bytesOfKeyValueData;
+	const uint8 *filebytes = (uint8 *) filedata->getData();
+	size_t totalsize = 0;
+
+	// Calculate the total size needed to hold the data in memory.
+	for (int i = 0; i < (int) header.numberOfMipmapLevels; i++)
+	{
+		if (fileoffset + sizeof(uint32) > filedata->getSize())
+			throw love::Exception("Could not parse KTX file: unexpected EOF.");
+
+		uint32 mipsize = *(uint32 *) (filebytes + fileoffset);
+
+		if (header.endianness == KTX_ENDIAN_REF_REV)
+			mipsize = swap32(mipsize);
+
+		fileoffset += sizeof(uint32);
+
+		// All mipsize fields are at a file offset that's a multiple of 4, so
+		// there might be some padding after the actual data in this mip level.
+		uint32 mipsizepadded = (mipsize + 3) & ~uint32(3);
+
+		totalsize += mipsizepadded;
+		fileoffset += mipsizepadded;
+	}
+
+	uint8 *data = nullptr;
+	try
+	{
+		data = new uint8[totalsize];
+	}
+	catch (std::bad_alloc &)
+	{
+		throw love::Exception("Out of memory.");
+	}
+
+	// Reset the file offset to the start of the file's image data.
+	fileoffset = sizeof(KTXHeader) + header.bytesOfKeyValueData;
+	size_t dataoffset = 0;
+
+	// Copy each mipmap level of the image from the file to our block of memory.
+	for (int i = 0; i < (int) header.numberOfMipmapLevels; i++)
+	{
+		uint32 mipsize = *(uint32 *) (filebytes + fileoffset);
+
+		if (header.endianness == KTX_ENDIAN_REF_REV)
+			mipsize = swap32(mipsize);
+
+		fileoffset += sizeof(uint32);
+
+		uint32 mipsizepadded = (mipsize + 3) & ~uint32(3);
+
+		CompressedData::SubImage mip;
+		mip.width = (int) std::max(header.pixelWidth >> i, 1u);
+		mip.height = (int) std::max(header.pixelHeight >> i, 1u);
+		mip.size = mipsize;
+
+		memcpy(data + dataoffset, filebytes + fileoffset, mipsize);
+		mip.data = data + dataoffset;
+
+		fileoffset += mipsizepadded;
+		dataoffset += mipsizepadded;
+
+		images.push_back(mip);
+	}
+
+	dataSize = totalsize;
+	format = cformat;
+
+	return data;
+}
+
+} // magpie
+} // image
+} // love

+ 53 - 0
jni/love/src/modules/image/magpie/KTXHandler.h

@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2006-2014 LOVE Development Team
+ *
+ * This software is provided 'as-is', without any express or implied
+ * warranty.  In no event will the authors be held liable for any damages
+ * arising from the use of this software.
+ *
+ * Permission is granted to anyone to use this software for any purpose,
+ * including commercial applications, and to alter it and redistribute it
+ * freely, subject to the following restrictions:
+ *
+ * 1. The origin of this software must not be misrepresented; you must not
+ *    claim that you wrote the original software. If you use this software
+ *    in a product, an acknowledgment in the product documentation would be
+ *    appreciated but is not required.
+ * 2. Altered source versions must be plainly marked as such, and must not be
+ *    misrepresented as being the original software.
+ * 3. This notice may not be removed or altered from any source distribution.
+ **/
+
+#ifndef LOVE_IMAGE_MAGPIE_KTX_HANDLER_H
+#define LOVE_IMAGE_MAGPIE_KTX_HANDLER_H
+
+#include "common/config.h"
+#include "CompressedFormatHandler.h"
+
+namespace love
+{
+namespace image
+{
+namespace magpie
+{
+
+/**
+ * Handles KTX files with compressed image data inside.
+ **/
+class KTXHandler : public CompressedFormatHandler
+{
+public:
+
+	virtual ~KTXHandler() {}
+
+	// Implements CompressedFormatHandler.
+	virtual bool canParse(const filesystem::FileData *data);
+	virtual uint8 *parse(filesystem::FileData *filedata, std::vector<CompressedData::SubImage> &images, size_t &dataSize, CompressedData::Format &format);
+
+}; // KTXHandler
+
+} // magpie
+} // image
+} // love
+
+#endif // LOVE_IMAGE_MAGPIE_KTX_HANDLER_H

+ 156 - 0
jni/love/src/modules/image/magpie/PKMHandler.cpp

@@ -0,0 +1,156 @@
+/**
+ * Copyright (c) 2006-2014 LOVE Development Team
+ *
+ * This software is provided 'as-is', without any express or implied
+ * warranty.  In no event will the authors be held liable for any damages
+ * arising from the use of this software.
+ *
+ * Permission is granted to anyone to use this software for any purpose,
+ * including commercial applications, and to alter it and redistribute it
+ * freely, subject to the following restrictions:
+ *
+ * 1. The origin of this software must not be misrepresented; you must not
+ *    claim that you wrote the original software. If you use this software
+ *    in a product, an acknowledgment in the product documentation would be
+ *    appreciated but is not required.
+ * 2. Altered source versions must be plainly marked as such, and must not be
+ *    misrepresented as being the original software.
+ * 3. This notice may not be removed or altered from any source distribution.
+ **/
+
+// LOVE
+#include "PKMHandler.h"
+#include "common/int.h"
+
+namespace love
+{
+namespace image
+{
+namespace magpie
+{
+
+namespace
+{
+
+// Big endian to host (and vice versa.)
+inline uint16 swap16big(uint16 x)
+{
+#ifdef LOVE_BIG_ENDIAN
+	return x;
+#else
+	return swap16(x);
+#endif
+}
+
+static const uint8 pkmIdentifier[] = {'P','K','M',' '};
+
+struct PKMHeader
+{
+	uint8 identifier[4];
+	uint8 version[2];
+	uint16 textureFormatBig;
+	uint16 extendedWidthBig;
+	uint16 extendedHeightBig;
+	uint16 widthBig;
+	uint16 heightBig;
+};
+
+enum PKMTextureFormat
+{
+	ETC1_RGB_NO_MIPMAPS = 0,
+	ETC2PACKAGE_RGB_NO_MIPMAPS,
+	ETC2PACKAGE_RGBA_NO_MIPMAPS_OLD,
+	ETC2PACKAGE_RGBA_NO_MIPMAPS,
+	ETC2PACKAGE_RGBA1_NO_MIPMAPS,
+	ETC2PACKAGE_R_NO_MIPMAPS,
+	ETC2PACKAGE_RG_NO_MIPMAPS,
+	ETC2PACKAGE_R_SIGNED_NO_MIPMAPS,
+	ETC2PACKAGE_RG_SIGNED_NO_MIPMAPS
+};
+
+CompressedData::Format convertFormat(uint16 texformat)
+{
+	switch (texformat)
+	{
+	case ETC1_RGB_NO_MIPMAPS:
+		return CompressedData::FORMAT_ETC1;
+	default:
+		return CompressedData::FORMAT_UNKNOWN;
+	}
+}
+
+} // Anonymous namespace.
+
+bool PKMHandler::canParse(const filesystem::FileData *data)
+{
+	if (data->getSize() <= sizeof(PKMHeader))
+		return false;
+
+	const PKMHeader *header = (const PKMHeader *) data->getData();
+
+	if (memcmp(header->identifier, pkmIdentifier, 4) != 0)
+		return false;
+
+	// At the time of this writing, only v1.0 and v2.0 exist.
+	if ((header->version[0] != '2' && header->version[0] != '1') || header->version[1] != '0')
+		return false;
+
+	return true;
+}
+
+uint8 *PKMHandler::parse(filesystem::FileData *filedata, std::vector<CompressedData::SubImage> &images, size_t &dataSize, CompressedData::Format &format)
+{
+	if (!canParse(filedata))
+		throw love::Exception("Could not decode compressed data (not a PKM file?)");
+
+	PKMHeader header = *(const PKMHeader *) filedata->getData();
+
+	header.textureFormatBig = swap16big(header.textureFormatBig);
+	header.extendedWidthBig = swap16big(header.extendedWidthBig);
+	header.extendedHeightBig = swap16big(header.extendedHeightBig);
+	header.widthBig = swap16big(header.widthBig);
+	header.heightBig = swap16big(header.heightBig);
+
+	CompressedData::Format cformat = convertFormat(header.textureFormatBig);
+
+	if (cformat == CompressedData::FORMAT_UNKNOWN)
+		throw love::Exception("Could not parse PKM file: unsupported texture format.");
+
+	// The rest of the file after the header is all texture data.
+	size_t totalsize = filedata->getSize() - sizeof(PKMHeader);
+	uint8 *data = nullptr;
+
+	try
+	{
+		data = new uint8[totalsize];
+	}
+	catch (std::bad_alloc &)
+	{
+		throw love::Exception("Out of memory.");
+	}
+
+	// PKM files only store a single mipmap level.
+	memcpy(data, (uint8 *) filedata->getData() + sizeof(PKMHeader), totalsize);
+
+	CompressedData::SubImage mip;
+
+	// TODO: verify whether glCompressedTexImage works properly with the unpadded
+	// width and height values (extended == padded.)
+	mip.width = header.widthBig;
+	mip.height = header.heightBig;
+
+	mip.size = totalsize;
+	mip.data = data;
+
+	images.push_back(mip);
+
+	dataSize = totalsize;
+	format = cformat;
+
+	return data;
+}
+
+} // magpie
+} // image
+} // love
+

+ 53 - 0
jni/love/src/modules/image/magpie/PKMHandler.h

@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2006-2014 LOVE Development Team
+ *
+ * This software is provided 'as-is', without any express or implied
+ * warranty.  In no event will the authors be held liable for any damages
+ * arising from the use of this software.
+ *
+ * Permission is granted to anyone to use this software for any purpose,
+ * including commercial applications, and to alter it and redistribute it
+ * freely, subject to the following restrictions:
+ *
+ * 1. The origin of this software must not be misrepresented; you must not
+ *    claim that you wrote the original software. If you use this software
+ *    in a product, an acknowledgment in the product documentation would be
+ *    appreciated but is not required.
+ * 2. Altered source versions must be plainly marked as such, and must not be
+ *    misrepresented as being the original software.
+ * 3. This notice may not be removed or altered from any source distribution.
+ **/
+
+#ifndef LOVE_IMAGE_MAGPIE_PKM_HANDLER_H
+#define LOVE_IMAGE_MAGPIE_PKM_HANDLER_H
+
+#include "common/config.h"
+#include "CompressedFormatHandler.h"
+
+namespace love
+{
+namespace image
+{
+namespace magpie
+{
+
+/**
+ * Handles PKM files with compressed ETC data inside.
+ **/
+class PKMHandler : public CompressedFormatHandler
+{
+public:
+
+	virtual ~PKMHandler() {}
+
+	// Implements CompressedFormatHandler.
+	virtual bool canParse(const filesystem::FileData *data);
+	virtual uint8 *parse(filesystem::FileData *filedata, std::vector<CompressedData::SubImage> &images, size_t &dataSize, CompressedData::Format &format);
+
+}; // PKMHandler
+
+} // magpie
+} // image
+} // love
+
+#endif // LOVE_IMAGE_MAGPIE_PKM_HANDLER_H

+ 385 - 0
jni/love/src/modules/image/magpie/PVRHandler.cpp

@@ -0,0 +1,385 @@
+/**
+ * Copyright (c) 2006-2014 LOVE Development Team
+ *
+ * This software is provided 'as-is', without any express or implied
+ * warranty.  In no event will the authors be held liable for any damages
+ * arising from the use of this software.
+ *
+ * Permission is granted to anyone to use this software for any purpose,
+ * including commercial applications, and to alter it and redistribute it
+ * freely, subject to the following restrictions:
+ *
+ * 1. The origin of this software must not be misrepresented; you must not
+ *    claim that you wrote the original software. If you use this software
+ *    in a product, an acknowledgment in the product documentation would be
+ *    appreciated but is not required.
+ * 2. Altered source versions must be plainly marked as such, and must not be
+ *    misrepresented as being the original software.
+ * 3. This notice may not be removed or altered from any source distribution.
+ **/
+
+// LOVE
+#include "PVRHandler.h"
+#include "common/int.h"
+
+// C++
+#include <algorithm>
+
+namespace love
+{
+namespace image
+{
+namespace magpie
+{
+
+namespace
+{
+
+// 'P' 'V' 'R' 3
+static const uint32 PVRTEX3_IDENT = 0x03525650;
+static const uint32 PVRTEX3_IDENT_REV = 0x50565203;
+
+#pragma pack(push, 4)
+struct PVRTexHeaderV3
+{
+	uint32 version;      /// Version of the file header, used to identify it.
+	uint32 flags;        /// Various format flags.
+	uint64 pixelFormat;  /// The pixel format, 8cc value storing the 4 channel identifiers and their respective sizes.
+	uint32 colorSpace;   /// The Color Space of the texture, currently either linear RGB or sRGB.
+	uint32 channelType;  /// Variable type that the channel is stored in. Supports signed/unsigned int/short/byte or float for now.
+	uint32 height;       /// Height of the texture.
+	uint32 width;        /// Width of the texture.
+	uint32 depth;        /// Depth of the texture. (Z-slices)
+	uint32 numSurfaces;  /// Number of members in a Texture Array.
+	uint32 numFaces;     /// Number of faces in a Cube Map. Maybe be a value other than 6.
+	uint32 numMipmaps;   /// Number of MIP Maps in the texture - NB: Includes top level.
+	uint32 metaDataSize; /// Size of the accompanying meta data.
+};
+#pragma pack(pop)
+
+enum PVRV3PixelFormat
+{
+	ePVRTPF_PVRTCI_2bpp_RGB = 0x00,
+	ePVRTPF_PVRTCI_2bpp_RGBA,
+	ePVRTPF_PVRTCI_4bpp_RGB,
+	ePVRTPF_PVRTCI_4bpp_RGBA,
+	ePVRTPF_PVRTCII_2bpp,
+	ePVRTPF_PVRTCII_4bpp,
+	ePVRTPF_ETC1 = 0x06,
+	ePVRTPF_DXT1,
+	ePVRTPF_DXT2,
+	ePVRTPF_DXT3,
+	ePVRTPF_DXT4,
+	ePVRTPF_DXT5,
+	ePVRTF_UNKNOWN_FORMAT = 0x7F
+};
+
+// 'P' 'V' 'R' '!'
+static const uint32 PVRTEX2_IDENT = 0x21525650;
+static const uint32 PVRTEX2_IDENT_REV = 0x50565221;
+
+struct PVRTexHeaderV2
+{
+	uint32 headerSize;
+	uint32 height;
+	uint32 width;
+	uint32 numMipmaps;
+	uint32 flags;
+	uint32 dataSize;
+	uint32 bpp;
+	uint32 bitmaskRed;
+	uint32 bitmaskGreen;
+	uint32 bitmaskBlue;
+	uint32 bitmaskAlpha;
+	uint32 pvrTag;
+	uint32 numSurfaces;
+};
+
+// The legacy V2 pixel types we support.
+enum PVRPixelTypeV2
+{
+	PixelTypePVRTC2 = 0x18,
+	PixelTypePVRTC4,
+	PixelTypePVRTCII2 = 0x1C,
+	PixelTypePVRTCII4,
+	PixelTypeDXT1 = 0x20,
+	PixelTypeDXT3 = 0x22,
+	PixelTypeDXT5 = 0x24,
+	PixelTypeETC1 = 0x36
+};
+
+// Convert a V2 header to V3.
+void ConvertPVRHeader(PVRTexHeaderV2 header2, PVRTexHeaderV3 *header3)
+{
+	// If the header's endianness doesn't match our own, we swap everything.
+	if (header2.pvrTag == PVRTEX2_IDENT_REV)
+	{
+		// All of the struct's members are uint32 values, so we can do this.
+		uint32 *headerArray = (uint32 *) &header2;
+		for (size_t i = 0; i < sizeof(PVRTexHeaderV2) / sizeof(uint32); i++)
+			headerArray[i] = swap32(headerArray[i]);
+	}
+
+	memset(header3, 0, sizeof(PVRTexHeaderV3));
+
+	header3->version = PVRTEX3_IDENT;
+	header3->height = header2.height;
+	header3->width = header2.width;
+	header3->depth = 1;
+	header3->numSurfaces = header2.numSurfaces;
+	header3->numFaces = 1;
+	header3->numMipmaps = header2.numMipmaps;
+	header3->metaDataSize = 0;
+
+	switch ((PVRPixelTypeV2) (header2.flags & 0xFF))
+	{
+	case PixelTypePVRTC2:
+		header3->pixelFormat = ePVRTPF_PVRTCI_2bpp_RGBA;
+		break;
+	case PixelTypePVRTC4:
+		header3->pixelFormat = ePVRTPF_PVRTCI_4bpp_RGBA;
+		break;
+	case PixelTypePVRTCII2:
+		header3->pixelFormat = ePVRTPF_PVRTCII_2bpp;
+		break;
+	case PixelTypePVRTCII4:
+		header3->pixelFormat = ePVRTPF_PVRTCII_4bpp;
+		break;
+	case PixelTypeDXT1:
+		header3->pixelFormat = ePVRTPF_DXT1;
+		break;
+	case PixelTypeDXT3:
+		header3->pixelFormat = ePVRTPF_DXT3;
+		break;
+	case PixelTypeDXT5:
+		header3->pixelFormat = ePVRTPF_DXT5;
+		break;
+	case PixelTypeETC1:
+		header3->pixelFormat = ePVRTPF_ETC1;
+		break;
+	default:
+		header3->pixelFormat = ePVRTF_UNKNOWN_FORMAT;
+		break;
+	}
+}
+
+CompressedData::Format convertFormat(PVRV3PixelFormat format)
+{
+	switch (format)
+	{
+	case ePVRTPF_PVRTCI_2bpp_RGB:
+		return CompressedData::FORMAT_PVR1_RGB2;
+	case ePVRTPF_PVRTCI_2bpp_RGBA:
+		return CompressedData::FORMAT_PVR1_RGBA2;
+	case ePVRTPF_PVRTCI_4bpp_RGB:
+		return CompressedData::FORMAT_PVR1_RGB4;
+	case ePVRTPF_PVRTCI_4bpp_RGBA:
+		return CompressedData::FORMAT_PVR1_RGBA4;
+	case ePVRTPF_ETC1:
+		return CompressedData::FORMAT_ETC1;
+	case ePVRTPF_DXT1:
+		return CompressedData::FORMAT_DXT1;
+	case ePVRTPF_DXT3:
+		return CompressedData::FORMAT_DXT3;
+	case ePVRTPF_DXT5:
+		return CompressedData::FORMAT_DXT5;
+	default:
+		return CompressedData::FORMAT_UNKNOWN;
+	}
+}
+
+int getBitsPerPixel(uint64 pixelformat)
+{
+	// Uncompressed formats have their bits per pixel stored in the high bits.
+	if ((pixelformat & 0xFFFFFFFF) != pixelformat)
+	{
+		const uint8 *charformat = (const uint8 *) &pixelformat;
+		return charformat[4] + charformat[5] + charformat[6] + charformat[7];
+	}
+
+	switch (pixelformat)
+	{
+	case ePVRTPF_PVRTCI_2bpp_RGB:
+	case ePVRTPF_PVRTCI_2bpp_RGBA:
+	case ePVRTPF_PVRTCII_2bpp:
+		return 2;
+	case ePVRTPF_PVRTCI_4bpp_RGB:
+	case ePVRTPF_PVRTCI_4bpp_RGBA:
+	case ePVRTPF_PVRTCII_4bpp:
+	case ePVRTPF_ETC1:
+	case ePVRTPF_DXT1:
+		return 4;
+	case ePVRTPF_DXT2:
+	case ePVRTPF_DXT3:
+	case ePVRTPF_DXT4:
+	case ePVRTPF_DXT5:
+		return 8;
+	default:
+		return 0;
+	}
+}
+
+void getFormatMinDimensions(uint64 pixelformat, int &minX, int &minY)
+{
+	switch (pixelformat)
+	{
+	case ePVRTPF_PVRTCI_2bpp_RGB:
+	case ePVRTPF_PVRTCI_2bpp_RGBA:
+		minX = 16;
+		minY = 8;
+		break;
+	case ePVRTPF_PVRTCI_4bpp_RGB:
+	case ePVRTPF_PVRTCI_4bpp_RGBA:
+		minX = minY = 8;
+		break;
+	case ePVRTPF_PVRTCII_2bpp:
+		minX = 8;
+		minY = 4;
+		break;
+	case ePVRTPF_PVRTCII_4bpp:
+		minX = minY = 4;
+		break;
+	case ePVRTPF_DXT1:
+	case ePVRTPF_DXT2:
+	case ePVRTPF_DXT3:
+	case ePVRTPF_DXT4:
+	case ePVRTPF_DXT5:
+	case ePVRTPF_ETC1:
+		minX = minY = 4;
+		break;
+	default: // We don't handle all possible formats, but that's fine.
+		minX = minY = 1;
+		break;
+	}
+}
+
+size_t getMipLevelSize(const PVRTexHeaderV3 &header, int miplevel)
+{
+	int smallestwidth = 1;
+	int smallestheight = 1;
+	getFormatMinDimensions(header.pixelFormat, smallestwidth, smallestheight);
+
+	int width = std::max((int) header.width >> miplevel, 1);
+	int height = std::max((int) header.height >> miplevel, 1);
+	int depth = std::max((int) header.depth >> miplevel, 1);
+
+	// Pad the dimensions.
+	width += (-width) % smallestwidth;
+	height += (-height) % smallestheight;
+
+	return getBitsPerPixel(header.pixelFormat) * width * height * depth / 8;
+}
+
+} // Anonymous namespace.
+
+
+bool PVRHandler::canParse(const filesystem::FileData *data)
+{
+	if (data->getSize() < sizeof(PVRTexHeaderV2) || data->getSize() < sizeof(PVRTexHeaderV3))
+		return false;
+
+	PVRTexHeaderV3 *header3 = (PVRTexHeaderV3 *) data->getData();
+
+	// Magic number (FourCC identifier.)
+	if (header3->version == PVRTEX3_IDENT || header3->version == PVRTEX3_IDENT_REV)
+		return true;
+
+	// Maybe it has a V2 header.
+	PVRTexHeaderV2 *header2 = (PVRTexHeaderV2 *) data->getData();
+
+	// FourCC identifier.
+	if (header2->pvrTag == PVRTEX2_IDENT || header2->pvrTag == PVRTEX2_IDENT_REV)
+		return true;
+
+	return false;
+}
+
+uint8 *PVRHandler::parse(filesystem::FileData *filedata, std::vector<CompressedData::SubImage> &images, size_t &dataSize, CompressedData::Format &format)
+{
+	if (!canParse(filedata))
+		throw love::Exception("Could not decode compressed data (not a PVR file?)");
+
+	PVRTexHeaderV3 header3 = *(PVRTexHeaderV3 *) filedata->getData();
+
+	// If the header isn't the V3 format, assume it's V2 and convert.
+	if (header3.version != PVRTEX3_IDENT && header3.version != PVRTEX3_IDENT_REV)
+		ConvertPVRHeader(*(PVRTexHeaderV2 *) filedata->getData(), &header3);
+
+	// If the header's endianness doesn't match our own, then we swap everything.
+	if (header3.version == PVRTEX3_IDENT_REV)
+	{
+		header3.version = PVRTEX3_IDENT;
+		header3.flags = swap32(header3.flags);
+		header3.pixelFormat = swap64(header3.pixelFormat);
+		header3.colorSpace = swap32(header3.colorSpace);
+		header3.channelType = swap32(header3.channelType);
+		header3.height = swap32(header3.height);
+		header3.width = swap32(header3.width);
+		header3.depth = swap32(header3.depth);
+		header3.numFaces = swap32(header3.numFaces);
+		header3.numMipmaps = swap32(header3.numMipmaps);
+		header3.metaDataSize = swap32(header3.metaDataSize);
+	}
+
+	if (header3.depth > 1)
+		throw love::Exception("Image depths greater than 1 in PVR files are unsupported.");
+
+	CompressedData::Format cformat = convertFormat((PVRV3PixelFormat) header3.pixelFormat);
+
+	if (cformat == CompressedData::FORMAT_UNKNOWN)
+		throw love::Exception("Could not parse PVR file: unsupported image format.");
+
+	size_t totalsize = 0;
+	uint8 *data = nullptr;
+
+	// Ignore faces and surfaces except the first ones (for now.)
+	for (int i = 0; i < (int) header3.numMipmaps; i++)
+		totalsize += getMipLevelSize(header3, i);
+
+	size_t fileoffset = sizeof(PVRTexHeaderV3) + header3.metaDataSize;
+
+	// Make sure the file actually holds this much data...
+	if (filedata->getSize() < fileoffset + totalsize)
+		throw love::Exception("Could not parse PVR file: invalid size calculation.");
+
+	try
+	{
+		data = new uint8[totalsize];
+	}
+	catch (std::bad_alloc &)
+	{
+		throw love::Exception("Out of memory.");
+	}
+
+	size_t curoffset = 0;
+	const uint8 *filebytes = (uint8 *) filedata->getData() + fileoffset;
+
+	for (int i = 0; i < (int) header3.numMipmaps; i++)
+	{
+		size_t mipsize = getMipLevelSize(header3, i);
+
+		if (curoffset + mipsize > totalsize)
+			break; // Just in case.
+
+		CompressedData::SubImage mip;
+		mip.width = std::max((int) header3.width >> i, 1);
+		mip.height = std::max((int) header3.height >> i, 1);
+		mip.size = mipsize;
+
+		memcpy(data + curoffset, filebytes + curoffset, mipsize);
+		mip.data = data + curoffset;
+
+		curoffset += mipsize;
+
+		images.push_back(mip);
+	}
+
+	dataSize = totalsize;
+	format = cformat;
+
+	return data;
+}
+
+} // magpie
+} // image
+} // love

+ 51 - 0
jni/love/src/modules/image/magpie/PVRHandler.h

@@ -0,0 +1,51 @@
+/**
+ * Copyright (c) 2006-2014 LOVE Development Team
+ *
+ * This software is provided 'as-is', without any express or implied
+ * warranty.  In no event will the authors be held liable for any damages
+ * arising from the use of this software.
+ *
+ * Permission is granted to anyone to use this software for any purpose,
+ * including commercial applications, and to alter it and redistribute it
+ * freely, subject to the following restrictions:
+ *
+ * 1. The origin of this software must not be misrepresented; you must not
+ *    claim that you wrote the original software. If you use this software
+ *    in a product, an acknowledgment in the product documentation would be
+ *    appreciated but is not required.
+ * 2. Altered source versions must be plainly marked as such, and must not be
+ *    misrepresented as being the original software.
+ * 3. This notice may not be removed or altered from any source distribution.
+ **/
+
+#ifndef LOVE_IMAGE_MAGPIE_PVR_HANDLER_H
+#define LOVE_IMAGE_MAGPIE_PVR_HANDLER_H
+
+// LOVE
+#include "common/config.h"
+#include "CompressedFormatHandler.h"
+
+namespace love
+{
+namespace image
+{
+namespace magpie
+{
+
+class PVRHandler : public CompressedFormatHandler
+{
+public:
+
+	virtual ~PVRHandler() {}
+
+	// Implements CompressedFormatHandler.
+	virtual bool canParse(const filesystem::FileData *data);
+	virtual uint8 *parse(filesystem::FileData *filedata, std::vector<CompressedData::SubImage> &images, size_t &dataSize, CompressedData::Format &format);
+
+}; // PVRHandler
+
+} // magpie
+} // image
+} // love
+
+#endif // LOVE_IMAGE_MAGPIE_PVR_HANDLER_H

+ 6 - 6
jni/love/src/modules/image/magpie/ddsHandler.cpp

@@ -18,8 +18,10 @@
  * 3. This notice may not be removed or altered from any source distribution.
  **/
 
+// LOVE
 #include "ddsHandler.h"
 
+// C++
 #include <algorithm>
 
 namespace love
@@ -29,7 +31,7 @@ namespace image
 namespace magpie
 {
 
-bool ddsHandler::canParse(const filesystem::FileData *data)
+bool DDSHandler::canParse(const filesystem::FileData *data)
 {
 	std::string ext = data->getExtension();
 	std::transform(ext.begin(), ext.end(), ext.begin(), tolower);
@@ -40,14 +42,14 @@ bool ddsHandler::canParse(const filesystem::FileData *data)
 	return dds::isCompressedDDS(data->getData(), data->getSize());
 }
 
-uint8 *ddsHandler::parse(filesystem::FileData *filedata, std::vector<CompressedData::SubImage> &images, size_t &dataSize, CompressedData::Format &format)
+uint8 *DDSHandler::parse(filesystem::FileData *filedata, std::vector<CompressedData::SubImage> &images, size_t &dataSize, CompressedData::Format &format)
 {
 	if (!dds::isDDS(filedata->getData(), filedata->getSize()))
 		throw love::Exception("Could not decode compressed data (not a DDS file?)");
 
 	CompressedData::Format texformat = CompressedData::FORMAT_UNKNOWN;
 
-	uint8 *data = 0;
+	uint8 *data = nullptr;
 	dataSize = 0;
 	images.clear();
 
@@ -107,7 +109,7 @@ uint8 *ddsHandler::parse(filesystem::FileData *filedata, std::vector<CompressedD
 	return data;
 }
 
-CompressedData::Format ddsHandler::convertFormat(dds::Format ddsformat)
+CompressedData::Format DDSHandler::convertFormat(dds::Format ddsformat)
 {
 	switch (ddsformat)
 	{
@@ -128,8 +130,6 @@ CompressedData::Format ddsHandler::convertFormat(dds::Format ddsformat)
 	default:
 		return CompressedData::FORMAT_UNKNOWN;
 	}
-
-	return CompressedData::FORMAT_UNKNOWN;
 }
 
 } // magpie

+ 7 - 24
jni/love/src/modules/image/magpie/ddsHandler.h

@@ -22,9 +22,7 @@
 #define LOVE_IMAGE_MAGPIE_DDS_HANDLER_H
 
 // LOVE
-#include "common/EnumMap.h"
-#include "filesystem/FileData.h"
-#include "image/CompressedData.h"
+#include "CompressedFormatHandler.h"
 
 // dds parser
 #include "ddsparse/ddsparse.h"
@@ -42,36 +40,21 @@ namespace magpie
 /**
  * Interface between CompressedData and the ddsparse library.
  **/
-class ddsHandler
+class DDSHandler : public CompressedFormatHandler
 {
 public:
 
-	/**
-	 * Determines whether a particular FileData can be parsed as CompressedData
-	 * by this handler.
-	 * @param data The data to parse.
-	 **/
-	static bool canParse(const filesystem::FileData *data);
+	virtual ~DDSHandler() {}
 
-	/**
-	 * Parses compressed image filedata into a list of sub-images and returns
-	 * a single block of memory containing all the images.
-	 *
-	 * @param[in] filedata The data to parse.
-	 * @param[out] image The list of sub-images generated. Byte data is a pointer
-	 *             to the returned data.
-	 * @param[out] dataSize The total size in bytes of the returned data.
-	 * @param[out] format The format of the Compressed Data.
-	 *
-	 * @return The single block of memory containing the parsed images.
-	 **/
-	static uint8 *parse(filesystem::FileData *filedata, std::vector<CompressedData::SubImage> &images, size_t &dataSize, CompressedData::Format &format);
+	// Implements CompressedFormatHandler.
+	virtual bool canParse(const filesystem::FileData *data);
+	virtual uint8 *parse(filesystem::FileData *filedata, std::vector<CompressedData::SubImage> &images, size_t &dataSize, CompressedData::Format &format);
 
 private:
 
 	static CompressedData::Format convertFormat(dds::Format ddsformat);
 
-}; // ddsHandler
+}; // DDSHandler
 
 } // magpie
 } // image

+ 2 - 2
jni/love/src/modules/joystick/Joystick.h

@@ -157,9 +157,9 @@ public:
 	virtual int getID() const = 0;
 
 	virtual bool isVibrationSupported() = 0;
-	virtual bool setVibration(float left, float right) = 0;
+	virtual bool setVibration(float left, float right, float duration = -1.0f) = 0;
 	virtual bool setVibration() = 0;
-	virtual void getVibration(float &left, float &right) const = 0;
+	virtual void getVibration(float &left, float &right) = 0;
 
 	static bool getConstant(const char *in, Hat &out);
 	static bool getConstant(Hat in, const char *&out);

+ 78 - 42
jni/love/src/modules/joystick/sdl/Joystick.cpp

@@ -25,6 +25,11 @@
 
 // C++
 #include <algorithm>
+#include <limits>
+
+#ifndef SDL_TICKS_PASSED
+#define SDL_TICKS_PASSED(A, B)  ((Sint32)((B) - (A)) <= 0)
+#endif
 
 namespace love
 {
@@ -34,9 +39,9 @@ namespace sdl
 {
 
 Joystick::Joystick(int id)
-	: joyhandle(0)
-	, controller(0)
-	, haptic(0)
+	: joyhandle(nullptr)
+	, controller(nullptr)
+	, haptic(nullptr)
 	, instanceid(-1)
 	, id(id)
 	, vibration()
@@ -44,9 +49,9 @@ Joystick::Joystick(int id)
 }
 
 Joystick::Joystick(int id, int joyindex)
-	: joyhandle(0)
-	, controller(0)
-	, haptic(0)
+	: joyhandle(nullptr)
+	, controller(nullptr)
+	, haptic(nullptr)
 	, instanceid(-1)
 	, id(id)
 	, vibration()
@@ -95,10 +100,7 @@ bool Joystick::open(int deviceindex)
 void Joystick::close()
 {
 	if (haptic)
-	{
-		SDL_HapticRumbleStop(haptic);
 		SDL_HapticClose(haptic);
-	}
 
 	if (controller)
 		SDL_GameControllerClose(controller);
@@ -106,16 +108,16 @@ void Joystick::close()
 	if (joyhandle)
 		SDL_JoystickClose(joyhandle);
 
-	joyhandle = 0;
-	controller = 0;
-	haptic = 0;
+	joyhandle = nullptr;
+	controller = nullptr;
+	haptic = nullptr;
 	instanceid = -1;
 	vibration = Vibration();
 }
 
 bool Joystick::isConnected() const
 {
-	return joyhandle != 0 && SDL_JoystickGetAttached(joyhandle);
+	return joyhandle != nullptr && SDL_JoystickGetAttached(joyhandle);
 }
 
 const char *Joystick::getName() const
@@ -209,7 +211,7 @@ bool Joystick::openGamepad(int deviceindex)
 	if (isGamepad())
 	{
 		SDL_GameControllerClose(controller);
-		controller = 0;
+		controller = nullptr;
 	}
 
 	controller = SDL_GameControllerOpen(deviceindex);
@@ -218,7 +220,7 @@ bool Joystick::openGamepad(int deviceindex)
 
 bool Joystick::isGamepad() const
 {
-	return controller != 0;
+	return controller != nullptr;
 }
 
 float Joystick::getGamepadAxis(love::joystick::Joystick::GamepadAxis axis) const
@@ -289,13 +291,13 @@ bool Joystick::checkCreateHaptic()
 	if (haptic)
 	{
 		SDL_HapticClose(haptic);
-		haptic = 0;
+		haptic = nullptr;
 	}
 
 	haptic = SDL_HapticOpenFromJoystick(joyhandle);
 	vibration = Vibration();
 
-	return haptic != 0;
+	return haptic != nullptr;
 }
 
 bool Joystick::isVibrationSupported()
@@ -312,8 +314,8 @@ bool Joystick::isVibrationSupported()
 	if (isGamepad() && (features & SDL_HAPTIC_CUSTOM) != 0)
 		return true;
 
-	// Check SDL's simple rumble as a last resort.
-	if (SDL_HapticRumbleSupported(haptic) == 1)
+	// Test for simple sine wave support as a last resort.
+	if ((features & SDL_HAPTIC_SINE) != 0)
 		return true;
 
 	return false;
@@ -325,7 +327,7 @@ bool Joystick::runVibrationEffect()
 	{
 		if (SDL_HapticUpdateEffect(haptic, vibration.id, &vibration.effect) == 0)
 		{
-			if (SDL_HapticRunEffect(haptic, vibration.id, 1) != -1)
+			if (SDL_HapticRunEffect(haptic, vibration.id, 1) == 0)
 				return true;
 		}
 
@@ -336,17 +338,14 @@ bool Joystick::runVibrationEffect()
 
 	vibration.id = SDL_HapticNewEffect(haptic, &vibration.effect);
 
-	if (vibration.id != -1 && SDL_HapticRunEffect(haptic, vibration.id, 1) != -1)
+	if (vibration.id != -1 && SDL_HapticRunEffect(haptic, vibration.id, 1) == 0)
 		return true;
 	
 	return false;
 }
 
-bool Joystick::setVibration(float left, float right)
+bool Joystick::setVibration(float left, float right, float duration)
 {
-	// TODO: support non-infinite durations? The working Tattiebogle Xbox
-	// controller driver in OS X seems to ignore durations under 1 second.
-
 	left = std::min(std::max(left, 0.0f), 1.0f);
 	right = std::min(std::max(right, 0.0f), 1.0f);
 
@@ -356,24 +355,32 @@ bool Joystick::setVibration(float left, float right)
 	if (!checkCreateHaptic())
 		return false;
 
+	Uint32 length = SDL_HAPTIC_INFINITY;
+	if (duration >= 0.0f)
+	{
+		float maxduration = std::numeric_limits<Uint32>::max() / 1000.0f;
+		length = Uint32(std::min(duration, maxduration) * 1000);
+	}
+
 	bool success = false;
 	unsigned int features = SDL_HapticQuery(haptic);
+	int axes = SDL_HapticNumAxes(haptic);
 
 	if ((features & SDL_HAPTIC_LEFTRIGHT) != 0)
 	{
 		memset(&vibration.effect, 0, sizeof(SDL_HapticEffect));
 		vibration.effect.type = SDL_HAPTIC_LEFTRIGHT;
 
-		vibration.effect.leftright.length = SDL_HAPTIC_INFINITY;
+		vibration.effect.leftright.length = length;
 		vibration.effect.leftright.large_magnitude = Uint16(left * LOVE_UINT16_MAX);
 		vibration.effect.leftright.small_magnitude = Uint16(right * LOVE_UINT16_MAX);
 
 		success = runVibrationEffect();
 	}
 
-	// Some gamepad drivers only give support for controlling the motors via
-	// a custom FF effect.
-	if (!success && isGamepad() && (features & SDL_HAPTIC_CUSTOM) != 0)
+	// Some gamepad drivers only give support for controlling individual motors
+	// through a custom FF effect.
+	if (!success && isGamepad() && (features & SDL_HAPTIC_CUSTOM) && axes == 2)
 	{
 		// NOTE: this may cause issues with drivers which support custom effects
 		// but aren't similar to https://github.com/d235j/360Controller .
@@ -385,7 +392,7 @@ bool Joystick::setVibration(float left, float right)
 		memset(&vibration.effect, 0, sizeof(SDL_HapticEffect));
 		vibration.effect.type = SDL_HAPTIC_CUSTOM;
 
-		vibration.effect.custom.length = SDL_HAPTIC_INFINITY;
+		vibration.effect.custom.length = length;
 		vibration.effect.custom.channels = 2;
 		vibration.effect.custom.period = 10;
 		vibration.effect.custom.samples = 2;
@@ -394,19 +401,36 @@ bool Joystick::setVibration(float left, float right)
 		success = runVibrationEffect();
 	}
 
-	// Fall back to a simple rumble if all else fails. SDL's simple rumble API
-	// only supports a single strength value.
-	if (!success && SDL_HapticRumbleInit(haptic) == 0)
+	// Fall back to a simple sine wave if all else fails. This only supports a
+	// single strength value.
+	if (!success && (features & SDL_HAPTIC_SINE) != 0)
 	{
+		memset(&vibration.effect, 0, sizeof(SDL_HapticEffect));
+		vibration.effect.type = SDL_HAPTIC_SINE;
+
+		vibration.effect.periodic.length = length;
+		vibration.effect.periodic.period = 10;
+
 		float strength = std::max(left, right);
-		int played = SDL_HapticRumblePlay(haptic, strength, SDL_HAPTIC_INFINITY);
-		success = (played == 0);
+		vibration.effect.periodic.magnitude = Sint16(strength * 0x7FFF);
+
+		success = runVibrationEffect();
 	}
 
 	if (success)
 	{
 		vibration.left = left;
 		vibration.right = right;
+
+		if (length == SDL_HAPTIC_INFINITY)
+			vibration.endtime = SDL_HAPTIC_INFINITY;
+		else
+			vibration.endtime = SDL_GetTicks() + length;
+	}
+	else
+	{
+		vibration.left = vibration.right = 0.0f;
+		vibration.endtime = SDL_HAPTIC_INFINITY;
 	}
 
 	return success;
@@ -417,12 +441,7 @@ bool Joystick::setVibration()
 	bool success = true;
 
 	if (SDL_WasInit(SDL_INIT_HAPTIC) && haptic && SDL_HapticIndex(haptic) != -1)
-	{
-		// Stop all playing effects on the haptic device.
-		// FIXME: We should only stop the vibration effect, in case we use the
-		// Haptic API for other things in the future.
-		success = (SDL_HapticStopAll(haptic) == 0);
-	}
+		success = (SDL_HapticStopEffect(haptic, vibration.id) == 0);
 
 	if (success)
 		vibration.left = vibration.right = 0.0f;
@@ -430,8 +449,25 @@ bool Joystick::setVibration()
 	return success;
 }
 
-void Joystick::getVibration(float &left, float &right) const
+void Joystick::getVibration(float &left, float &right)
 {
+	if (vibration.endtime != SDL_HAPTIC_INFINITY)
+	{
+		// With some drivers, the effect physically stops at the right time, but
+		// SDL_HapticGetEffectStatus still thinks it's playing. So we explicitly
+		// stop it once it's done, just to be sure.
+		if (SDL_TICKS_PASSED(SDL_GetTicks(), vibration.endtime))
+		{
+			setVibration();
+			vibration.endtime = SDL_HAPTIC_INFINITY;
+		}
+	}
+
+	// Check if the haptic effect has stopped playing.
+	int id = vibration.id;
+	if (!haptic || id == -1 || SDL_HapticGetEffectStatus(haptic, id) != 1)
+		vibration.left = vibration.right = 0.0f;
+
 	left = vibration.left;
 	right = vibration.right;
 }

+ 7 - 3
jni/love/src/modules/joystick/sdl/Joystick.h

@@ -74,9 +74,9 @@ public:
 	int getID() const;
 
 	bool isVibrationSupported();
-	bool setVibration(float left, float right);
+	bool setVibration(float left, float right, float duration = -1.0f);
 	bool setVibration();
-	void getVibration(float &left, float &right) const;
+	void getVibration(float &left, float &right);
 
 	static bool getConstant(Hat in, Uint8 &out);
 	static bool getConstant(Uint8 in, Hat &out);
@@ -111,8 +111,12 @@ private:
 		Uint16 data[4];
 		int id;
 
+		Uint32 endtime;
+
 		Vibration()
-			: left(0.0f), right(0.0f), effect(), data(), id(-1)
+			: left(0.0f), right(0.0f)
+			, effect(), data(), id(-1)
+			, endtime(SDL_HAPTIC_INFINITY)
 		{}
 
 	} vibration;

+ 7 - 7
jni/love/src/modules/joystick/sdl/JoystickModule.cpp

@@ -77,7 +77,7 @@ const char *JoystickModule::getName() const
 love::joystick::Joystick *JoystickModule::getJoystick(int joyindex)
 {
 	if (joyindex < 0 || (size_t) joyindex >= activeSticks.size())
-		return 0;
+		return nullptr;
 
 	return activeSticks[joyindex];
 }
@@ -107,13 +107,13 @@ love::joystick::Joystick *JoystickModule::getJoystickFromID(int instanceid)
 			return activeSticks[i];
 	}
 
-	return 0;
+	return nullptr;
 }
 
 love::joystick::Joystick *JoystickModule::addJoystick(int deviceindex)
 {
 	if (deviceindex < 0 || deviceindex >= SDL_NumJoysticks())
-		return 0;
+		return nullptr;
 
 	std::string guidstr = getDeviceGUID(deviceindex);
 	joystick::Joystick *joystick = 0;
@@ -140,7 +140,7 @@ love::joystick::Joystick *JoystickModule::addJoystick(int deviceindex)
 	removeJoystick(joystick);
 
 	if (!joystick->open(deviceindex))
-		return 0;
+		return nullptr;
 
 	// Make sure multiple instances of the same physical joystick aren't added
 	// to the active list.
@@ -262,7 +262,7 @@ bool JoystickModule::setGamepadMapping(const std::string &guid, Joystick::Gamepa
 	if (status == 1)
 		checkGamepads(guid);
 
-	return  status >= 0;
+	return status >= 0;
 }
 
 Joystick::JoystickInput JoystickModule::getGamepadMapping(const std::string &guid, Joystick::GamepadInput gpinput)
@@ -318,7 +318,7 @@ std::string JoystickModule::stringFromGamepadInput(Joystick::GamepadInput gpinpu
 	SDL_GameControllerAxis sdlaxis;
 	SDL_GameControllerButton sdlbutton;
 
-	const char *gpinputname = 0;
+	const char *gpinputname = nullptr;
 
 	switch (gpinput.type)
 	{
@@ -435,7 +435,7 @@ void JoystickModule::checkGamepads(const std::string &guid) const
 			// Big hack time: open the index as a game controller and compare
 			// the underlying joystick handle to the active stick's.
 			SDL_GameController *ctrl = SDL_GameControllerOpen(d_index);
-			if (ctrl == NULL)
+			if (ctrl == nullptr)
 				continue;
 
 			SDL_Joystick *stick = SDL_GameControllerGetJoystick(ctrl);

+ 2 - 1
jni/love/src/modules/joystick/sdl/wrap_Joystick.cpp

@@ -207,7 +207,8 @@ int w_Joystick_setVibration(lua_State *L)
 	{
 		float left = (float) luaL_checknumber(L, 2);
 		float right = (float) luaL_optnumber(L, 3, left);
-		success = j->setVibration(left, right);
+		float duration = (float) luaL_optnumber(L, 4, -1.0); // -1 is infinite.
+		success = j->setVibration(left, right, duration);
 	}
 
 	luax_pushboolean(L, success);

+ 2 - 2
jni/love/src/modules/joystick/sdl/wrap_JoystickModule.cpp

@@ -28,7 +28,7 @@ namespace joystick
 namespace sdl
 {
 
-static JoystickModule *instance = 0;
+static JoystickModule *instance = nullptr;
 
 int w_getJoysticks(lua_State *L)
 {
@@ -194,7 +194,7 @@ static const lua_CFunction types[] =
 
 extern "C" int luaopen_love_joystick(lua_State *L)
 {
-	if (instance == 0)
+	if (instance == nullptr)
 	{
 		EXCEPT_GUARD(instance = new JoystickModule();)
 	}

+ 2 - 2
jni/love/src/modules/sound/lullaby/Mpg123Decoder.cpp

@@ -117,7 +117,7 @@ int Mpg123Decoder::decode()
 			size += numbytes;
 			long rate = 0;
 			int encoding = 0;
-			int ret = mpg123_getformat(handle, &rate, &channels, &encoding);
+			mpg123_getformat(handle, &rate, &channels, &encoding);
 			if (rate == 0)
 				rate = sampleRate;
 			else
@@ -126,7 +126,7 @@ int Mpg123Decoder::decode()
 				channels = MPG123_STEREO;
 			if (encoding == 0)
 				encoding = MPG123_ENC_SIGNED_16;
-			ret = mpg123_format(handle, rate, channels, encoding);
+			int ret = mpg123_format(handle, rate, channels, encoding);
 			if (ret != MPG123_OK)
 				throw love::Exception("Could not set output format.");
 		}

+ 25 - 31
jni/love/src/modules/system/System.cpp

@@ -27,8 +27,9 @@
 #elif defined(LOVE_ANDROID)
 #include "common/android.h"
 #elif defined(LOVE_LINUX)
-#include <stdlib.h>
-#include <unistd.h>
+#include <spawn.h>
+//#include <stdlib.h>
+//#include <unistd.h>
 #include <sys/wait.h>
 #elif defined(LOVE_WINDOWS)
 #include "common/utf8.h"
@@ -57,12 +58,17 @@ std::string System::getOS() const
 #endif
 }
 
+extern "C"
+{
+	extern char **environ; // The environment, always available
+}
+
 bool System::openURL(const std::string &url) const
 {
-	bool success = false;
 
 #if defined(LOVE_MACOSX)
 
+	bool success = false;
 	// We could be lazy and use system("open " + url), but this is safer.
 	CFURLRef cfurl = CFURLCreateWithBytes(nullptr,
 	                                      (const UInt8 *) url.c_str(),
@@ -72,38 +78,28 @@ bool System::openURL(const std::string &url) const
 
 	success = LSOpenCFURLRef(cfurl, nullptr) == noErr;
 	CFRelease(cfurl);
+	return success;
 
 #elif defined(LOVE_ANDROID)
 
-	success = love::android::openURL (url);	
+	return love::android::openURL (url);	
 
 #elif defined(LOVE_LINUX)
 
-	// Spawn a child process, which we'll replace with xdg-open.
-	pid_t pid = vfork();
-
-	if (pid == 0) // Child process.
-	{
-		// Replace the child process with xdg-open and pass in the URL.
-		execlp("xdg-open", "xdg-open", url.c_str(), nullptr);
-
-		// exec will only return if it errored, so we should exit with non-zero.
-		_exit(1);
-	}
-	else if (pid > 0) // Parent process.
-	{
-		// Wait for xdg-open to complete (or fail.)
-		int status = 0;
-		if (waitpid(pid, &status, 0) == pid)
-			success = (status == 0);
-		else
-			success = false;
-	}
+	pid_t pid;
+	const char *argv[] = {"xdg-open", url.c_str(), nullptr};
+
+	// Note: at the moment this process inherits our file descriptors.
+	// Note: the below const_cast is really ugly as well.
+	if (posix_spawnp(&pid, "xdg-open", nullptr, nullptr, const_cast<char **>(argv), environ) != 0)
+		return false;
+
+	// Wait for xdg-open to complete (or fail.)
+	int status = 0;
+	if (waitpid(pid, &status, 0) == pid)
+		return (status == 0);
 	else
-	{
-		// vfork() failed.
-		success = false;
-	}
+		return false;
 
 #elif defined(LOVE_WINDOWS)
 
@@ -117,11 +113,9 @@ bool System::openURL(const std::string &url) const
 	                                 nullptr,
 	                                 SW_SHOW);
 
-	success = (int) result > 32;
+	return (int) result > 32;
 
 #endif
-
-	return success;
 }
 
 bool System::getConstant(const char *in, System::PowerState &out)

+ 1 - 0
jni/love/src/modules/thread/LuaThread.cpp

@@ -38,6 +38,7 @@ LuaThread::LuaThread(const std::string &name, love::Data *code)
 	, nargs(0)
 {
 	code->retain();
+	threadName = name;
 }
 
 LuaThread::~LuaThread()

+ 1 - 1
jni/love/src/modules/thread/sdl/Thread.cpp

@@ -51,7 +51,7 @@ bool Thread::start()
 		return false;
 	if (thread) // Clean old handle up
 		SDL_WaitThread(thread, 0);
-	thread = SDL_CreateThread(thread_runner, NULL, this);
+	thread = SDL_CreateThread(thread_runner, t->getThreadName(), this);
 	running = (thread != 0);
 	return running;
 }

+ 5 - 0
jni/love/src/modules/thread/threads.cpp

@@ -99,5 +99,10 @@ bool Threadable::isRunning() const
 	return owner->isRunning();
 }
 
+const char *Threadable::getThreadName() const
+{
+	return threadName.empty() ? nullptr : threadName.c_str();
+}
+
 } // thread
 } // love

+ 8 - 0
jni/love/src/modules/thread/threads.h

@@ -21,9 +21,13 @@
 #ifndef LOVE_THREAD_THREADS_H
 #define LOVE_THREAD_THREADS_H
 
+// LOVE
 #include "common/config.h"
 #include "Thread.h"
 
+// C++
+#include <string>
+
 namespace love
 {
 namespace thread
@@ -83,9 +87,13 @@ public:
 	bool start();
 	void wait();
 	bool isRunning() const;
+	const char *getThreadName() const;
 
 protected:
+
 	Thread *owner;
+	std::string threadName;
+
 };
 
 Mutex *newMutex();

+ 18 - 7
jni/love/src/modules/window/sdl/Window.cpp

@@ -269,11 +269,14 @@ bool Window::setContext(int fsaa, bool vsync, bool sRGB)
 	if (!context)
 	{
 		int flags = 0;
+		int profilemask = 0;
 		SDL_GL_GetAttribute(SDL_GL_CONTEXT_FLAGS, &flags);
+		SDL_GL_GetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, &profilemask);
 		if (flags & SDL_GL_CONTEXT_DEBUG_FLAG)
 		{
+			profilemask &= ~SDL_GL_CONTEXT_PROFILE_COMPATIBILITY;
 			SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0);
-			SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, 0);
+			SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, profilemask);
 			context = SDL_GL_CreateContext(window);
 		}
 	}
@@ -315,6 +318,7 @@ void Window::setWindowGLAttributes(int fsaa, bool /* sRGB */) const
 	SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8);
 	SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
 	SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 1);
+	SDL_GL_SetAttribute(SDL_GL_RETAINED_BACKING, 0);
 
 	// FSAA.
 	SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, (fsaa > 0) ? 1 : 0);
@@ -331,19 +335,26 @@ void Window::setWindowGLAttributes(int fsaa, bool /* sRGB */) const
 
 	int contextprofile = 0;
 
-	bool tryES2 = false;
+	bool tryGLES = false;
 
 	const char *driver = SDL_GetCurrentVideoDriver();
+	const char *glesdrivers[] = {"RPI", "Android", "uikit", "winrt"};
 
 	// We always want to use OpenGL ES on certain video backends.
-	if (driver && (strstr(driver, "RPI") || strstr(driver, "Android") || strstr(driver, "uikit")))
-		tryES2 = true;
+	for (size_t i = 0; i < sizeof(glesdrivers) / sizeof(glesdrivers[0]); i++)
+	{
+		if (driver && strstr(driver, glesdrivers[i]))
+		{
+			tryGLES = true;
+			break;
+		}
+	}
 
 	const char *hint = SDL_GetHint("LOVE_GRAPHICS_USE_OPENGLES");
 	if (hint)
-		tryES2 = (*hint) != '0';
+		tryGLES = (*hint) != '0';
 
-	if (tryES2)
+	if (tryGLES)
 	{
 		// Use OpenGL ES 2+.
 		contextprofile = SDL_GL_CONTEXT_PROFILE_ES;
@@ -363,7 +374,7 @@ void Window::setWindowGLAttributes(int fsaa, bool /* sRGB */) const
 	if (debugenv && *debugenv == '1')
 	{
 		SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_DEBUG_FLAG);
-		if (!tryES2)
+		if (!tryGLES)
 			contextprofile |= SDL_GL_CONTEXT_PROFILE_COMPATIBILITY;
 	}
 	else

+ 1 - 1
jni/love/src/scripts/boot.lua

@@ -526,7 +526,7 @@ function love.run()
 		-- Call update and draw
 		if love.update then love.update(dt) end -- will pass 0 if love.timer is disabled
 
-		if love.window and love.graphics and love.window.isCreated() then
+		if love.window and love.graphics and love.graphics.isActive() then
 			love.graphics.clear()
 			love.graphics.origin()
 			if love.draw then love.draw() end

+ 2 - 2
jni/love/src/scripts/boot.lua.h

@@ -943,8 +943,8 @@ const unsigned char boot_lua[] =
 	0x73, 0x20, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x0a,
 	0x09, 0x09, 0x69, 0x66, 0x20, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x20, 0x61, 
 	0x6e, 0x64, 0x20, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x69, 0x63, 0x73, 0x20, 0x61, 
-	0x6e, 0x64, 0x20, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x2e, 0x69, 0x73, 0x43, 
-	0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x28, 0x29, 0x20, 0x74, 0x68, 0x65, 0x6e, 0x0a,
+	0x6e, 0x64, 0x20, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x69, 0x63, 0x73, 0x2e, 0x69, 
+	0x73, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x28, 0x29, 0x20, 0x74, 0x68, 0x65, 0x6e, 0x0a,
 	0x09, 0x09, 0x09, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x69, 0x63, 0x73, 0x2e, 0x63, 
 	0x6c, 0x65, 0x61, 0x72, 0x28, 0x29, 0x0a,
 	0x09, 0x09, 0x09, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x69, 0x63, 0x73, 0x2e, 0x6f, 

+ 28 - 7
jni/love/src/scripts/graphics.lua

@@ -1317,9 +1317,9 @@ uniform mat4 TransformMatrix;
 uniform mat4 ProjectionMatrix;
 uniform mat4 TransformProjectionMatrix;
 // uniform mat4 NormalMatrix;
-uniform float love_PointSize;
 uniform sampler2D _tex0_;
-uniform vec4 love_ScreenSize;]]
+uniform mediump vec4 love_ScreenSize;
+uniform mediump float love_PointSize;]]
 
 	GLSL.VERTEX = {
 		HEADER = [[
@@ -1357,7 +1357,7 @@ attribute vec4 VertexPosition;
 attribute vec4 VertexTexCoord;
 attribute vec4 VertexColor;
 
-varying vec4 VaryingTexCoord;
+varying mediump vec4 VaryingTexCoord;
 varying lowp vec4 VaryingColor;
 
 //#if defined(GL_EXT_draw_instanced)
@@ -1414,18 +1414,36 @@ void main() {
 
 precision mediump float;
 
-varying vec4 VaryingTexCoord;
+varying mediump vec4 VaryingTexCoord;
 varying lowp vec4 VaryingColor;]],
 
 		FOOTER = GLSL.PIXEL.FOOTER,
 		FOOTER_MULTI_CANVAS = GLSL.PIXEL.FOOTER_MULTI_CANVAS,
 	}
 
+	-- Unfortunately, GLSL 1.20 can't deal with precision qualifier statements.
+	local function stripPrecision(code)
+		-- Remove "precision mediump float;", etc.
+		code = code:gsub("precision(%s+)lowp(%s+)(%w+)(%s*);", "")
+		code = code:gsub("precision(%s+)mediump(%s+)(%w+)(%s*);", "")
+		code = code:gsub("precision(%s+)highp(%s+)(%w+)(%s*);", "")
+
+		-- Remove all remaining "lowp", "mediump", "highp".
+		code = code:gsub("(%W+)lowp(%s+)", "%1"):gsub("^lowp(%s+)", "")
+		code = code:gsub("(%W+)mediump(%s+)", "%1"):gsub("^mediump(%s+)", "")
+		code = code:gsub("(%W+)highp(%s+)", "%1"):gsub("^highp(%s+)", "")
+
+		return code
+	end
+
 	local function createVertexCode(vertexcode, lang)
+		if lang == GLSL then
+			vertexcode = stripPrecision(vertexcode)
+		end
 		local vertexcodes = {
 			lang.VERSION,
 			lang.SYNTAX, lang.VERTEX.HEADER, lang.UNIFORMS,
-			"#line 1",
+			lang == GLSLES and "#line 1" or "#line 0", -- specification differences for #line...
 			vertexcode,
 			lang.VERTEX.FOOTER,
 		}
@@ -1433,10 +1451,13 @@ varying lowp vec4 VaryingColor;]],
 	end
 
 	local function createPixelCode(pixelcode, lang, is_multicanvas)
+		if lang == GLSL then
+			pixelcode = stripPrecision(pixelcode)
+		end
 		local pixelcodes = {
 			lang.VERSION,
 			lang.SYNTAX, lang.PIXEL.HEADER, lang.UNIFORMS,
-			"#line 1",
+			lang == GLSLES and "#line 1" or "#line 0",
 			pixelcode,
 			is_multicanvas and lang.PIXEL.FOOTER_MULTI_CANVAS or lang.PIXEL.FOOTER,
 		}
@@ -1539,7 +1560,7 @@ vec4 position(mat4 transform_proj, vec4 vertpos) {
 	return transform_proj * vertpos;	
 }]],
 		pixel = [[
-vec4 effect(vec4 vcolor, Image texture, vec2 texcoord, vec2 pixcoord) {
+lowp vec4 effect(lowp vec4 vcolor, Image texture, vec2 texcoord, vec2 pixcoord) {
 	return Texel(texture, texcoord) * vcolor;	
 }]]
 	}

+ 76 - 14
jni/love/src/scripts/graphics.lua.h

@@ -6312,12 +6312,14 @@ const unsigned char graphics_lua[] =
 	0x69, 0x78, 0x3b, 0x0a,
 	0x2f, 0x2f, 0x20, 0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x20, 0x6d, 0x61, 0x74, 0x34, 0x20, 0x4e, 0x6f, 
 	0x72, 0x6d, 0x61, 0x6c, 0x4d, 0x61, 0x74, 0x72, 0x69, 0x78, 0x3b, 0x0a,
-	0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x20, 0x6c, 0x6f, 0x76, 0x65, 
-	0x5f, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x53, 0x69, 0x7a, 0x65, 0x3b, 0x0a,
 	0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x20, 0x73, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x72, 0x32, 0x44, 0x20, 
 	0x5f, 0x74, 0x65, 0x78, 0x30, 0x5f, 0x3b, 0x0a,
-	0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x20, 0x76, 0x65, 0x63, 0x34, 0x20, 0x6c, 0x6f, 0x76, 0x65, 0x5f, 
-	0x53, 0x63, 0x72, 0x65, 0x65, 0x6e, 0x53, 0x69, 0x7a, 0x65, 0x3b, 0x5d, 0x5d, 0x0a,
+	0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x20, 0x6d, 0x65, 0x64, 0x69, 0x75, 0x6d, 0x70, 0x20, 0x76, 0x65, 
+	0x63, 0x34, 0x20, 0x6c, 0x6f, 0x76, 0x65, 0x5f, 0x53, 0x63, 0x72, 0x65, 0x65, 0x6e, 0x53, 0x69, 0x7a, 0x65, 
+	0x3b, 0x0a,
+	0x75, 0x6e, 0x69, 0x66, 0x6f, 0x72, 0x6d, 0x20, 0x6d, 0x65, 0x64, 0x69, 0x75, 0x6d, 0x70, 0x20, 0x66, 0x6c, 
+	0x6f, 0x61, 0x74, 0x20, 0x6c, 0x6f, 0x76, 0x65, 0x5f, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x53, 0x69, 0x7a, 0x65, 
+	0x3b, 0x5d, 0x5d, 0x0a,
 	0x09, 0x47, 0x4c, 0x53, 0x4c, 0x2e, 0x56, 0x45, 0x52, 0x54, 0x45, 0x58, 0x20, 0x3d, 0x20, 0x7b, 0x0a,
 	0x09, 0x09, 0x48, 0x45, 0x41, 0x44, 0x45, 0x52, 0x20, 0x3d, 0x20, 0x5b, 0x5b, 0x0a,
 	0x23, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x20, 0x56, 0x45, 0x52, 0x54, 0x45, 0x58, 0x0a,
@@ -6373,8 +6375,9 @@ const unsigned char graphics_lua[] =
 	0x74, 0x65, 0x78, 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x3b, 0x0a,
 	0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x20, 0x76, 0x65, 0x63, 0x34, 0x20, 0x56, 0x65, 0x72, 
 	0x74, 0x65, 0x78, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x3b, 0x0a,
-	0x76, 0x61, 0x72, 0x79, 0x69, 0x6e, 0x67, 0x20, 0x76, 0x65, 0x63, 0x34, 0x20, 0x56, 0x61, 0x72, 0x79, 0x69, 
-	0x6e, 0x67, 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x3b, 0x0a,
+	0x76, 0x61, 0x72, 0x79, 0x69, 0x6e, 0x67, 0x20, 0x6d, 0x65, 0x64, 0x69, 0x75, 0x6d, 0x70, 0x20, 0x76, 0x65, 
+	0x63, 0x34, 0x20, 0x56, 0x61, 0x72, 0x79, 0x69, 0x6e, 0x67, 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, 
+	0x3b, 0x0a,
 	0x76, 0x61, 0x72, 0x79, 0x69, 0x6e, 0x67, 0x20, 0x6c, 0x6f, 0x77, 0x70, 0x20, 0x76, 0x65, 0x63, 0x34, 0x20, 
 	0x56, 0x61, 0x72, 0x79, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x3b, 0x0a,
 	0x2f, 0x2f, 0x23, 0x69, 0x66, 0x20, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64, 0x28, 0x47, 0x4c, 0x5f, 0x45, 
@@ -6470,8 +6473,9 @@ const unsigned char graphics_lua[] =
 	0x23, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x20, 0x50, 0x49, 0x58, 0x45, 0x4c, 0x0a,
 	0x70, 0x72, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x20, 0x6d, 0x65, 0x64, 0x69, 0x75, 0x6d, 0x70, 0x20, 
 	0x66, 0x6c, 0x6f, 0x61, 0x74, 0x3b, 0x0a,
-	0x76, 0x61, 0x72, 0x79, 0x69, 0x6e, 0x67, 0x20, 0x76, 0x65, 0x63, 0x34, 0x20, 0x56, 0x61, 0x72, 0x79, 0x69, 
-	0x6e, 0x67, 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x3b, 0x0a,
+	0x76, 0x61, 0x72, 0x79, 0x69, 0x6e, 0x67, 0x20, 0x6d, 0x65, 0x64, 0x69, 0x75, 0x6d, 0x70, 0x20, 0x76, 0x65, 
+	0x63, 0x34, 0x20, 0x56, 0x61, 0x72, 0x79, 0x69, 0x6e, 0x67, 0x54, 0x65, 0x78, 0x43, 0x6f, 0x6f, 0x72, 0x64, 
+	0x3b, 0x0a,
 	0x76, 0x61, 0x72, 0x79, 0x69, 0x6e, 0x67, 0x20, 0x6c, 0x6f, 0x77, 0x70, 0x20, 0x76, 0x65, 0x63, 0x34, 0x20, 
 	0x56, 0x61, 0x72, 0x79, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x3b, 0x5d, 0x5d, 0x2c, 0x0a,
 	0x09, 0x09, 0x46, 0x4f, 0x4f, 0x54, 0x45, 0x52, 0x20, 0x3d, 0x20, 0x47, 0x4c, 0x53, 0x4c, 0x2e, 0x50, 0x49, 
@@ -6481,16 +6485,65 @@ const unsigned char graphics_lua[] =
 	0x4f, 0x4f, 0x54, 0x45, 0x52, 0x5f, 0x4d, 0x55, 0x4c, 0x54, 0x49, 0x5f, 0x43, 0x41, 0x4e, 0x56, 0x41, 0x53, 
 	0x2c, 0x0a,
 	0x09, 0x7d, 0x0a,
+	0x09, 0x2d, 0x2d, 0x20, 0x55, 0x6e, 0x66, 0x6f, 0x72, 0x74, 0x75, 0x6e, 0x61, 0x74, 0x65, 0x6c, 0x79, 0x2c, 
+	0x20, 0x47, 0x4c, 0x53, 0x4c, 0x20, 0x31, 0x2e, 0x32, 0x30, 0x20, 0x63, 0x61, 0x6e, 0x27, 0x74, 0x20, 0x64, 
+	0x65, 0x61, 0x6c, 0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x70, 0x72, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 
+	0x20, 0x71, 0x75, 0x61, 0x6c, 0x69, 0x66, 0x69, 0x65, 0x72, 0x20, 0x73, 0x74, 0x61, 0x74, 0x65, 0x6d, 0x65, 
+	0x6e, 0x74, 0x73, 0x2e, 0x0a,
+	0x09, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x20, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x73, 0x74, 
+	0x72, 0x69, 0x70, 0x50, 0x72, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x28, 0x63, 0x6f, 0x64, 0x65, 0x29, 0x0a,
+	0x09, 0x09, 0x2d, 0x2d, 0x20, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x20, 0x22, 0x70, 0x72, 0x65, 0x63, 0x69, 
+	0x73, 0x69, 0x6f, 0x6e, 0x20, 0x6d, 0x65, 0x64, 0x69, 0x75, 0x6d, 0x70, 0x20, 0x66, 0x6c, 0x6f, 0x61, 0x74, 
+	0x3b, 0x22, 0x2c, 0x20, 0x65, 0x74, 0x63, 0x2e, 0x0a,
+	0x09, 0x09, 0x63, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x20, 0x63, 0x6f, 0x64, 0x65, 0x3a, 0x67, 0x73, 0x75, 0x62, 
+	0x28, 0x22, 0x70, 0x72, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x28, 0x25, 0x73, 0x2b, 0x29, 0x6c, 0x6f, 
+	0x77, 0x70, 0x28, 0x25, 0x73, 0x2b, 0x29, 0x28, 0x25, 0x77, 0x2b, 0x29, 0x28, 0x25, 0x73, 0x2a, 0x29, 0x3b, 
+	0x22, 0x2c, 0x20, 0x22, 0x22, 0x29, 0x0a,
+	0x09, 0x09, 0x63, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x20, 0x63, 0x6f, 0x64, 0x65, 0x3a, 0x67, 0x73, 0x75, 0x62, 
+	0x28, 0x22, 0x70, 0x72, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x28, 0x25, 0x73, 0x2b, 0x29, 0x6d, 0x65, 
+	0x64, 0x69, 0x75, 0x6d, 0x70, 0x28, 0x25, 0x73, 0x2b, 0x29, 0x28, 0x25, 0x77, 0x2b, 0x29, 0x28, 0x25, 0x73, 
+	0x2a, 0x29, 0x3b, 0x22, 0x2c, 0x20, 0x22, 0x22, 0x29, 0x0a,
+	0x09, 0x09, 0x63, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x20, 0x63, 0x6f, 0x64, 0x65, 0x3a, 0x67, 0x73, 0x75, 0x62, 
+	0x28, 0x22, 0x70, 0x72, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x28, 0x25, 0x73, 0x2b, 0x29, 0x68, 0x69, 
+	0x67, 0x68, 0x70, 0x28, 0x25, 0x73, 0x2b, 0x29, 0x28, 0x25, 0x77, 0x2b, 0x29, 0x28, 0x25, 0x73, 0x2a, 0x29, 
+	0x3b, 0x22, 0x2c, 0x20, 0x22, 0x22, 0x29, 0x0a,
+	0x09, 0x09, 0x2d, 0x2d, 0x20, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x20, 0x61, 0x6c, 0x6c, 0x20, 0x72, 0x65, 
+	0x6d, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x20, 0x22, 0x6c, 0x6f, 0x77, 0x70, 0x22, 0x2c, 0x20, 0x22, 0x6d, 
+	0x65, 0x64, 0x69, 0x75, 0x6d, 0x70, 0x22, 0x2c, 0x20, 0x22, 0x68, 0x69, 0x67, 0x68, 0x70, 0x22, 0x2e, 0x0a,
+	0x09, 0x09, 0x63, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x20, 0x63, 0x6f, 0x64, 0x65, 0x3a, 0x67, 0x73, 0x75, 0x62, 
+	0x28, 0x22, 0x28, 0x25, 0x57, 0x2b, 0x29, 0x6c, 0x6f, 0x77, 0x70, 0x28, 0x25, 0x73, 0x2b, 0x29, 0x22, 0x2c, 
+	0x20, 0x22, 0x25, 0x31, 0x22, 0x29, 0x3a, 0x67, 0x73, 0x75, 0x62, 0x28, 0x22, 0x5e, 0x6c, 0x6f, 0x77, 0x70, 
+	0x28, 0x25, 0x73, 0x2b, 0x29, 0x22, 0x2c, 0x20, 0x22, 0x22, 0x29, 0x0a,
+	0x09, 0x09, 0x63, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x20, 0x63, 0x6f, 0x64, 0x65, 0x3a, 0x67, 0x73, 0x75, 0x62, 
+	0x28, 0x22, 0x28, 0x25, 0x57, 0x2b, 0x29, 0x6d, 0x65, 0x64, 0x69, 0x75, 0x6d, 0x70, 0x28, 0x25, 0x73, 0x2b, 
+	0x29, 0x22, 0x2c, 0x20, 0x22, 0x25, 0x31, 0x22, 0x29, 0x3a, 0x67, 0x73, 0x75, 0x62, 0x28, 0x22, 0x5e, 0x6d, 
+	0x65, 0x64, 0x69, 0x75, 0x6d, 0x70, 0x28, 0x25, 0x73, 0x2b, 0x29, 0x22, 0x2c, 0x20, 0x22, 0x22, 0x29, 0x0a,
+	0x09, 0x09, 0x63, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x20, 0x63, 0x6f, 0x64, 0x65, 0x3a, 0x67, 0x73, 0x75, 0x62, 
+	0x28, 0x22, 0x28, 0x25, 0x57, 0x2b, 0x29, 0x68, 0x69, 0x67, 0x68, 0x70, 0x28, 0x25, 0x73, 0x2b, 0x29, 0x22, 
+	0x2c, 0x20, 0x22, 0x25, 0x31, 0x22, 0x29, 0x3a, 0x67, 0x73, 0x75, 0x62, 0x28, 0x22, 0x5e, 0x68, 0x69, 0x67, 
+	0x68, 0x70, 0x28, 0x25, 0x73, 0x2b, 0x29, 0x22, 0x2c, 0x20, 0x22, 0x22, 0x29, 0x0a,
+	0x09, 0x09, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x20, 0x63, 0x6f, 0x64, 0x65, 0x0a,
+	0x09, 0x65, 0x6e, 0x64, 0x0a,
 	0x09, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x20, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x63, 0x72, 
 	0x65, 0x61, 0x74, 0x65, 0x56, 0x65, 0x72, 0x74, 0x65, 0x78, 0x43, 0x6f, 0x64, 0x65, 0x28, 0x76, 0x65, 0x72, 
 	0x74, 0x65, 0x78, 0x63, 0x6f, 0x64, 0x65, 0x2c, 0x20, 0x6c, 0x61, 0x6e, 0x67, 0x29, 0x0a,
+	0x09, 0x09, 0x69, 0x66, 0x20, 0x6c, 0x61, 0x6e, 0x67, 0x20, 0x3d, 0x3d, 0x20, 0x47, 0x4c, 0x53, 0x4c, 0x20, 
+	0x74, 0x68, 0x65, 0x6e, 0x0a,
+	0x09, 0x09, 0x09, 0x76, 0x65, 0x72, 0x74, 0x65, 0x78, 0x63, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x20, 0x73, 0x74, 
+	0x72, 0x69, 0x70, 0x50, 0x72, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x28, 0x76, 0x65, 0x72, 0x74, 0x65, 
+	0x78, 0x63, 0x6f, 0x64, 0x65, 0x29, 0x0a,
+	0x09, 0x09, 0x65, 0x6e, 0x64, 0x0a,
 	0x09, 0x09, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x20, 0x76, 0x65, 0x72, 0x74, 0x65, 0x78, 0x63, 0x6f, 0x64, 0x65, 
 	0x73, 0x20, 0x3d, 0x20, 0x7b, 0x0a,
 	0x09, 0x09, 0x09, 0x6c, 0x61, 0x6e, 0x67, 0x2e, 0x56, 0x45, 0x52, 0x53, 0x49, 0x4f, 0x4e, 0x2c, 0x0a,
 	0x09, 0x09, 0x09, 0x6c, 0x61, 0x6e, 0x67, 0x2e, 0x53, 0x59, 0x4e, 0x54, 0x41, 0x58, 0x2c, 0x20, 0x6c, 0x61, 
 	0x6e, 0x67, 0x2e, 0x56, 0x45, 0x52, 0x54, 0x45, 0x58, 0x2e, 0x48, 0x45, 0x41, 0x44, 0x45, 0x52, 0x2c, 0x20, 
 	0x6c, 0x61, 0x6e, 0x67, 0x2e, 0x55, 0x4e, 0x49, 0x46, 0x4f, 0x52, 0x4d, 0x53, 0x2c, 0x0a,
-	0x09, 0x09, 0x09, 0x22, 0x23, 0x6c, 0x69, 0x6e, 0x65, 0x20, 0x31, 0x22, 0x2c, 0x0a,
+	0x09, 0x09, 0x09, 0x6c, 0x61, 0x6e, 0x67, 0x20, 0x3d, 0x3d, 0x20, 0x47, 0x4c, 0x53, 0x4c, 0x45, 0x53, 0x20, 
+	0x61, 0x6e, 0x64, 0x20, 0x22, 0x23, 0x6c, 0x69, 0x6e, 0x65, 0x20, 0x31, 0x22, 0x20, 0x6f, 0x72, 0x20, 0x22, 
+	0x23, 0x6c, 0x69, 0x6e, 0x65, 0x20, 0x30, 0x22, 0x2c, 0x20, 0x2d, 0x2d, 0x20, 0x73, 0x70, 0x65, 0x63, 0x69, 
+	0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x64, 0x69, 0x66, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 
+	0x65, 0x73, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x23, 0x6c, 0x69, 0x6e, 0x65, 0x2e, 0x2e, 0x2e, 0x0a,
 	0x09, 0x09, 0x09, 0x76, 0x65, 0x72, 0x74, 0x65, 0x78, 0x63, 0x6f, 0x64, 0x65, 0x2c, 0x0a,
 	0x09, 0x09, 0x09, 0x6c, 0x61, 0x6e, 0x67, 0x2e, 0x56, 0x45, 0x52, 0x54, 0x45, 0x58, 0x2e, 0x46, 0x4f, 0x4f, 
 	0x54, 0x45, 0x52, 0x2c, 0x0a,
@@ -6503,13 +6556,21 @@ const unsigned char graphics_lua[] =
 	0x65, 0x61, 0x74, 0x65, 0x50, 0x69, 0x78, 0x65, 0x6c, 0x43, 0x6f, 0x64, 0x65, 0x28, 0x70, 0x69, 0x78, 0x65, 
 	0x6c, 0x63, 0x6f, 0x64, 0x65, 0x2c, 0x20, 0x6c, 0x61, 0x6e, 0x67, 0x2c, 0x20, 0x69, 0x73, 0x5f, 0x6d, 0x75, 
 	0x6c, 0x74, 0x69, 0x63, 0x61, 0x6e, 0x76, 0x61, 0x73, 0x29, 0x0a,
+	0x09, 0x09, 0x69, 0x66, 0x20, 0x6c, 0x61, 0x6e, 0x67, 0x20, 0x3d, 0x3d, 0x20, 0x47, 0x4c, 0x53, 0x4c, 0x20, 
+	0x74, 0x68, 0x65, 0x6e, 0x0a,
+	0x09, 0x09, 0x09, 0x70, 0x69, 0x78, 0x65, 0x6c, 0x63, 0x6f, 0x64, 0x65, 0x20, 0x3d, 0x20, 0x73, 0x74, 0x72, 
+	0x69, 0x70, 0x50, 0x72, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x28, 0x70, 0x69, 0x78, 0x65, 0x6c, 0x63, 
+	0x6f, 0x64, 0x65, 0x29, 0x0a,
+	0x09, 0x09, 0x65, 0x6e, 0x64, 0x0a,
 	0x09, 0x09, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x20, 0x70, 0x69, 0x78, 0x65, 0x6c, 0x63, 0x6f, 0x64, 0x65, 0x73, 
 	0x20, 0x3d, 0x20, 0x7b, 0x0a,
 	0x09, 0x09, 0x09, 0x6c, 0x61, 0x6e, 0x67, 0x2e, 0x56, 0x45, 0x52, 0x53, 0x49, 0x4f, 0x4e, 0x2c, 0x0a,
 	0x09, 0x09, 0x09, 0x6c, 0x61, 0x6e, 0x67, 0x2e, 0x53, 0x59, 0x4e, 0x54, 0x41, 0x58, 0x2c, 0x20, 0x6c, 0x61, 
 	0x6e, 0x67, 0x2e, 0x50, 0x49, 0x58, 0x45, 0x4c, 0x2e, 0x48, 0x45, 0x41, 0x44, 0x45, 0x52, 0x2c, 0x20, 0x6c, 
 	0x61, 0x6e, 0x67, 0x2e, 0x55, 0x4e, 0x49, 0x46, 0x4f, 0x52, 0x4d, 0x53, 0x2c, 0x0a,
-	0x09, 0x09, 0x09, 0x22, 0x23, 0x6c, 0x69, 0x6e, 0x65, 0x20, 0x31, 0x22, 0x2c, 0x0a,
+	0x09, 0x09, 0x09, 0x6c, 0x61, 0x6e, 0x67, 0x20, 0x3d, 0x3d, 0x20, 0x47, 0x4c, 0x53, 0x4c, 0x45, 0x53, 0x20, 
+	0x61, 0x6e, 0x64, 0x20, 0x22, 0x23, 0x6c, 0x69, 0x6e, 0x65, 0x20, 0x31, 0x22, 0x20, 0x6f, 0x72, 0x20, 0x22, 
+	0x23, 0x6c, 0x69, 0x6e, 0x65, 0x20, 0x30, 0x22, 0x2c, 0x0a,
 	0x09, 0x09, 0x09, 0x70, 0x69, 0x78, 0x65, 0x6c, 0x63, 0x6f, 0x64, 0x65, 0x2c, 0x0a,
 	0x09, 0x09, 0x09, 0x69, 0x73, 0x5f, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x63, 0x61, 0x6e, 0x76, 0x61, 0x73, 0x20, 
 	0x61, 0x6e, 0x64, 0x20, 0x6c, 0x61, 0x6e, 0x67, 0x2e, 0x50, 0x49, 0x58, 0x45, 0x4c, 0x2e, 0x46, 0x4f, 0x4f, 
@@ -6725,10 +6786,11 @@ const unsigned char graphics_lua[] =
 	0x70, 0x72, 0x6f, 0x6a, 0x20, 0x2a, 0x20, 0x76, 0x65, 0x72, 0x74, 0x70, 0x6f, 0x73, 0x3b, 0x09, 0x0a,
 	0x7d, 0x5d, 0x5d, 0x2c, 0x0a,
 	0x09, 0x09, 0x70, 0x69, 0x78, 0x65, 0x6c, 0x20, 0x3d, 0x20, 0x5b, 0x5b, 0x0a,
-	0x76, 0x65, 0x63, 0x34, 0x20, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x28, 0x76, 0x65, 0x63, 0x34, 0x20, 0x76, 
-	0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x2c, 0x20, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x20, 0x74, 0x65, 0x78, 0x74, 0x75, 
-	0x72, 0x65, 0x2c, 0x20, 0x76, 0x65, 0x63, 0x32, 0x20, 0x74, 0x65, 0x78, 0x63, 0x6f, 0x6f, 0x72, 0x64, 0x2c, 
-	0x20, 0x76, 0x65, 0x63, 0x32, 0x20, 0x70, 0x69, 0x78, 0x63, 0x6f, 0x6f, 0x72, 0x64, 0x29, 0x20, 0x7b, 0x0a,
+	0x6c, 0x6f, 0x77, 0x70, 0x20, 0x76, 0x65, 0x63, 0x34, 0x20, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x28, 0x6c, 
+	0x6f, 0x77, 0x70, 0x20, 0x76, 0x65, 0x63, 0x34, 0x20, 0x76, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x2c, 0x20, 0x49, 
+	0x6d, 0x61, 0x67, 0x65, 0x20, 0x74, 0x65, 0x78, 0x74, 0x75, 0x72, 0x65, 0x2c, 0x20, 0x76, 0x65, 0x63, 0x32, 
+	0x20, 0x74, 0x65, 0x78, 0x63, 0x6f, 0x6f, 0x72, 0x64, 0x2c, 0x20, 0x76, 0x65, 0x63, 0x32, 0x20, 0x70, 0x69, 
+	0x78, 0x63, 0x6f, 0x6f, 0x72, 0x64, 0x29, 0x20, 0x7b, 0x0a,
 	0x09, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x20, 0x54, 0x65, 0x78, 0x65, 0x6c, 0x28, 0x74, 0x65, 0x78, 0x74, 
 	0x75, 0x72, 0x65, 0x2c, 0x20, 0x74, 0x65, 0x78, 0x63, 0x6f, 0x6f, 0x72, 0x64, 0x29, 0x20, 0x2a, 0x20, 0x76, 
 	0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3b, 0x09, 0x0a,