Browse Source

Revamped and streamlined retina / high-DPI support (resolves issue #1122).

With the highdpi window flag enabled on a retina-capable display and OS, content should now appear to the user at the same size and in the same positions as with the flag disabled.

As a result, mouse and touch coordinates, Texture and graphics dimensions, and the graphics coordinate system now use pixel density-scaled units instead of pixels. Raw pixel units should generally only be used for things such as shader algorithms which execute per-pixel and rely on accurate pixel dimensions. love.window.fromPixels and friends typically don’t need to be used anymore.

Images, Canvases, and Fonts can have an optional explicit ‘pixel density’ set when creating them. This allows for easily loading high pixel density content which displays at the same size as regular or low pixel density content.

API changes:

- Added Texture:getPixelWidth/getPixelHeight/getPixelDimensions and Texture:getPixelDensity. Texture:getWidth/getHeight return the pixel density-scaled width and height (as it will appear on the screen when drawn) rather than the number of pixels on each texture dimension.

- Added love.graphics.getPixelWidth/getPixelHeight/getPixelDimensions.

- Added optional ‘pixeldensity’ field to the settings table parameter of love.graphics.newImage. It defaults to 1, or if the file the Image was loaded from has “@2x”, “@3x”, etc. at the end of its name, it uses that number as the pixel density scale by default.

- love.graphics.newCanvas now takes a table as its third parameter, with fields “format”, “msaa”, and “pixeldensity”. pixeldensity defaults to the main screen’s pixel density. The width and height parameters specify the visual size that the Canvas will be drawn at / can be drawn to (pixel density-scaled units).

- love.graphics.newVideo accepts a table as its second parameter, with optional fields “audio” and “pixeldensity”. pixeldensity defaults to 1.

- love.graphics.newFont variants have an optional pixeldensity parameter at the end of the argument list. For TrueType fonts this defaults to the current pixel density scale of the screen, and for BMFonts and ImageFonts this defaults to 1.

- Added Font:getPixelDensity.

- Renamed love.window.getPixelScale to love.window.getPixelDensity.

--HG--
branch : minor
Alex Szpakowski 8 years ago
parent
commit
d26002f5cc
53 changed files with 871 additions and 532 deletions
  1. 14 0
      src/common/runtime.cpp
  2. 1 0
      src/common/runtime.h
  3. 42 25
      src/modules/event/sdl/Event.cpp
  4. 15 3
      src/modules/filesystem/FileData.cpp
  5. 4 0
      src/modules/filesystem/FileData.h
  6. 3 1
      src/modules/font/BMFontRasterizer.cpp
  7. 1 1
      src/modules/font/BMFontRasterizer.h
  8. 12 6
      src/modules/font/Font.cpp
  9. 5 3
      src/modules/font/Font.h
  10. 3 1
      src/modules/font/ImageRasterizer.cpp
  11. 1 1
      src/modules/font/ImageRasterizer.h
  12. 5 0
      src/modules/font/Rasterizer.cpp
  13. 3 0
      src/modules/font/Rasterizer.h
  14. 15 2
      src/modules/font/freetype/Font.cpp
  15. 4 3
      src/modules/font/freetype/Font.h
  16. 7 2
      src/modules/font/freetype/TrueTypeRasterizer.cpp
  17. 1 1
      src/modules/font/freetype/TrueTypeRasterizer.h
  18. 30 14
      src/modules/font/wrap_Font.cpp
  19. 5 2
      src/modules/graphics/Graphics.h
  20. 13 12
      src/modules/graphics/Quad.cpp
  21. 17 0
      src/modules/graphics/Texture.cpp
  22. 8 0
      src/modules/graphics/Texture.h
  23. 22 25
      src/modules/graphics/opengl/Canvas.cpp
  24. 11 5
      src/modules/graphics/opengl/Canvas.h
  25. 50 38
      src/modules/graphics/opengl/Font.cpp
  26. 6 2
      src/modules/graphics/opengl/Font.h
  27. 0 1
      src/modules/graphics/opengl/GLBuffer.cpp
  28. 89 31
      src/modules/graphics/opengl/Graphics.cpp
  29. 15 14
      src/modules/graphics/opengl/Graphics.h
  30. 53 47
      src/modules/graphics/opengl/Image.cpp
  31. 16 14
      src/modules/graphics/opengl/Image.h
  32. 1 11
      src/modules/graphics/opengl/OpenGL.cpp
  33. 1 6
      src/modules/graphics/opengl/OpenGL.h
  34. 21 10
      src/modules/graphics/opengl/Video.cpp
  35. 7 1
      src/modules/graphics/opengl/Video.h
  36. 2 2
      src/modules/graphics/opengl/wrap_Canvas.cpp
  37. 8 0
      src/modules/graphics/opengl/wrap_Font.cpp
  38. 88 32
      src/modules/graphics/opengl/wrap_Graphics.cpp
  39. 8 4
      src/modules/graphics/opengl/wrap_Graphics.lua
  40. 10 7
      src/modules/graphics/opengl/wrap_Image.cpp
  41. 1 0
      src/modules/graphics/opengl/wrap_Image.h
  42. 25 0
      src/modules/graphics/opengl/wrap_Video.cpp
  43. 33 0
      src/modules/graphics/wrap_Texture.cpp
  44. 2 2
      src/modules/keyboard/sdl/Keyboard.cpp
  45. 8 8
      src/modules/mouse/sdl/Mouse.cpp
  46. 9 2
      src/modules/window/Window.h
  47. 73 11
      src/modules/window/sdl/Window.cpp
  48. 9 2
      src/modules/window/sdl/Window.h
  49. 3 3
      src/modules/window/wrap_Window.cpp
  50. 2 3
      src/scripts/boot.lua
  51. 2 6
      src/scripts/boot.lua.h
  52. 30 55
      src/scripts/nogame.lua
  53. 57 113
      src/scripts/nogame.lua.h

+ 14 - 0
src/common/runtime.cpp

@@ -194,6 +194,20 @@ int luax_intflag(lua_State *L, int table_index, const char *key, int defaultValu
 	return retval;
 }
 
+double luax_numberflag(lua_State *L, int table_index, const char *key, double defaultValue)
+{
+	lua_getfield(L, table_index, key);
+
+	int retval;
+	if (!lua_isnumber(L, -1))
+		retval = defaultValue;
+	else
+		retval = lua_tonumber(L, -1);
+
+	lua_pop(L, 1);
+	return retval;
+}
+
 int luax_assert_argc(lua_State *L, int min)
 {
 	int argc = lua_gettop(L);

+ 1 - 0
src/common/runtime.h

@@ -161,6 +161,7 @@ void luax_pushstring(lua_State *L, const std::string &str);
 
 bool luax_boolflag(lua_State *L, int table_index, const char *key, bool defaultValue);
 int luax_intflag(lua_State *L, int table_index, const char *key, int defaultValue);
+double luax_numberflag(lua_State *L, int table_index, const char *key, double defaultValue);
 
 /**
  * Convert the value at the specified index to an Lua number, and then

+ 42 - 25
src/modules/event/sdl/Event.cpp

@@ -44,26 +44,30 @@ namespace sdl
 
 // SDL reports mouse coordinates in the window coordinate system in OS X, but
 // we want them in pixel coordinates (may be different with high-DPI enabled.)
-static void windowToPixelCoords(double *x, double *y)
+static void windowToDPICoords(double *x, double *y)
 {
 	auto window = Module::getInstance<window::Window>(Module::M_WINDOW);
 	if (window)
-		window->windowToPixelCoords(x, y);
+		window->windowToDPICoords(x, y);
 }
 
 #ifndef LOVE_MACOSX
-static void normalizedToPixelCoords(double *x, double *y)
+static void normalizedToDPICoords(double *x, double *y)
 {
-	auto window = Module::getInstance<window::Window>(Module::M_WINDOW);
-	int w = 1, h = 1;
+	double w = 1.0, h = 1.0;
 
+	auto window = Module::getInstance<window::Window>(Module::M_WINDOW);
 	if (window)
-		window->getPixelDimensions(w, h);
+	{
+		w = window->getWidth();
+		h = window->getHeight();
+		window->windowToDPICoords(&w, &h);
+	}
 
 	if (x)
-		*x = ((*x) * (double) w);
+		*x = ((*x) * w);
 	if (y)
-		*y = ((*y) * (double) h);
+		*y = ((*y) * h);
 }
 #endif
 
@@ -249,8 +253,8 @@ Message *Event::convert(const SDL_Event &e)
 			double y = (double) e.motion.y;
 			double xrel = (double) e.motion.xrel;
 			double yrel = (double) e.motion.yrel;
-			windowToPixelCoords(&x, &y);
-			windowToPixelCoords(&xrel, &yrel);
+			windowToDPICoords(&x, &y);
+			windowToDPICoords(&xrel, &yrel);
 			vargs.emplace_back(x);
 			vargs.emplace_back(y);
 			vargs.emplace_back(xrel);
@@ -276,7 +280,7 @@ Message *Event::convert(const SDL_Event &e)
 
 			double px = (double) e.button.x;
 			double py = (double) e.button.y;
-			windowToPixelCoords(&px, &py);
+			windowToDPICoords(&px, &py);
 			vargs.emplace_back(px);
 			vargs.emplace_back(py);
 			vargs.emplace_back((double) button);
@@ -313,15 +317,15 @@ Message *Event::convert(const SDL_Event &e)
 		if (touchNormalizationBug || fabs(touchinfo.x) >= 1.5 || fabs(touchinfo.y) >= 1.5 || fabs(touchinfo.dx) >= 1.5 || fabs(touchinfo.dy) >= 1.5)
 		{
 			touchNormalizationBug = true;
-			windowToPixelCoords(&touchinfo.x, &touchinfo.y);
-			windowToPixelCoords(&touchinfo.dx, &touchinfo.dy);
+			windowToDPICoords(&touchinfo.x, &touchinfo.y);
+			windowToDPICoords(&touchinfo.dx, &touchinfo.dy);
 		}
 		else
 #endif
 		{
-			// SDL's coords are normalized to [0, 1], but we want them in pixels.
-			normalizedToPixelCoords(&touchinfo.x, &touchinfo.y);
-			normalizedToPixelCoords(&touchinfo.dx, &touchinfo.dy);
+			// SDL's coords are normalized to [0, 1], but we want screen coords.
+			normalizedToDPICoords(&touchinfo.x, &touchinfo.y);
+			normalizedToDPICoords(&touchinfo.dx, &touchinfo.dy);
 		}
 
 		// We need to update the love.touch.sdl internal state from here.
@@ -536,6 +540,7 @@ Message *Event::convertWindowEvent(const SDL_Event &e)
 	vargs.reserve(4);
 
 	window::Window *win = nullptr;
+	graphics::Graphics *gfx = nullptr;
 
 	if (e.type != SDL_WINDOWEVENT)
 		return nullptr;
@@ -559,17 +564,29 @@ Message *Event::convertWindowEvent(const SDL_Event &e)
 		break;
 	case SDL_WINDOWEVENT_RESIZED:
 		{
-			int px_w = e.window.data1;
-			int px_h = e.window.data2;
+			double width  = e.window.data1;
+			double height = e.window.data2;
+
+			gfx = Module::getInstance<graphics::Graphics>(Module::M_GRAPHICS);
+			win = Module::getInstance<window::Window>(Module::M_WINDOW);
 
-			SDL_Window *sdlwin = SDL_GetWindowFromID(e.window.windowID);
-			if (sdlwin)
-				SDL_GL_GetDrawableSize(sdlwin, &px_w, &px_h);
+			// WINDOWEVENT_SIZE_CHANGED will always occur before RESIZED.
+			// The size values in the Window aren't necessarily the same as the
+			// graphics size, which is what we want to output.
+			if (gfx)
+			{
+				width  = gfx->getWidth();
+				height = gfx->getHeight();
+			}
+			else if (win)
+			{
+				width  = win->getWidth();
+				height = win->getHeight();
+				windowToDPICoords(&width, &height);
+			}
 
-			vargs.emplace_back((double) px_w);
-			vargs.emplace_back((double) px_h);
-			vargs.emplace_back((double) e.window.data1);
-			vargs.emplace_back((double) e.window.data2);
+			vargs.emplace_back(width);
+			vargs.emplace_back(height);
 			msg = new Message("resize", vargs);
 		}
 		break;

+ 15 - 3
src/modules/filesystem/FileData.cpp

@@ -45,8 +45,15 @@ FileData::FileData(uint64 size, const std::string &filename)
 		throw love::Exception("Out of memory.");
 	}
 
-	if (filename.rfind('.') != std::string::npos)
-		extension = filename.substr(filename.rfind('.')+1);
+	size_t dotpos = filename.rfind('.');
+
+	if (dotpos != std::string::npos)
+	{
+		extension = filename.substr(dotpos + 1);
+		name = filename.substr(0, dotpos);
+	}
+	else
+		name = filename;
 }
 
 FileData::~FileData()
@@ -56,7 +63,7 @@ FileData::~FileData()
 
 void *FileData::getData() const
 {
-	return (void *)data;
+	return data;
 }
 
 size_t FileData::getSize() const
@@ -75,5 +82,10 @@ const std::string &FileData::getExtension() const
 	return extension;
 }
 
+const std::string &FileData::getName() const
+{
+	return name;
+}
+
 } // filesystem
 } // love

+ 4 - 0
src/modules/filesystem/FileData.h

@@ -49,6 +49,7 @@ public:
 
 	const std::string &getFilename() const;
 	const std::string &getExtension() const;
+	const std::string &getName() const;
 
 private:
 
@@ -64,6 +65,9 @@ private:
 	// The extension (without dot). Used to identify file type.
 	std::string extension;
 
+	// The file name without the extension (and without the dot).
+	std::string name;
+
 }; // FileData
 
 } // filesystem

+ 3 - 1
src/modules/font/BMFontRasterizer.cpp

@@ -131,11 +131,13 @@ std::string BMFontLine::getAttributeString(const char *name) const
 } // anonymous namespace
 
 
-BMFontRasterizer::BMFontRasterizer(love::filesystem::FileData *fontdef, const std::vector<image::ImageData *> &imagelist)
+BMFontRasterizer::BMFontRasterizer(love::filesystem::FileData *fontdef, const std::vector<image::ImageData *> &imagelist, float pixeldensity)
 	: fontSize(0)
 	, unicode(false)
 	, lineHeight(0)
 {
+	this->pixelDensity = pixeldensity;
+
 	const std::string &filename = fontdef->getFilename();
 
 	size_t separatorpos = filename.rfind('/');

+ 1 - 1
src/modules/font/BMFontRasterizer.h

@@ -42,7 +42,7 @@ class BMFontRasterizer : public Rasterizer
 {
 public:
 
-	BMFontRasterizer(love::filesystem::FileData *fontdef, const std::vector<image::ImageData *> &imagelist);
+	BMFontRasterizer(love::filesystem::FileData *fontdef, const std::vector<image::ImageData *> &imagelist, float pixeldensity);
 	virtual ~BMFontRasterizer();
 
 	// Implements Rasterizer.

+ 12 - 6
src/modules/font/Font.cpp

@@ -47,12 +47,18 @@ Rasterizer *Font::newTrueTypeRasterizer(int size, TrueTypeRasterizer::Hinting hi
 	return newTrueTypeRasterizer(data.get(), size, hinting);
 }
 
-Rasterizer *Font::newBMFontRasterizer(love::filesystem::FileData *fontdef, const std::vector<image::ImageData *> &images)
+Rasterizer *Font::newTrueTypeRasterizer(int size, float pixeldensity, TrueTypeRasterizer::Hinting hinting)
 {
-	return new BMFontRasterizer(fontdef, images);
+	StrongRef<DefaultFontData> data(new DefaultFontData, Acquire::NORETAIN);
+	return newTrueTypeRasterizer(data.get(), size, pixeldensity, hinting);
+}
+
+Rasterizer *Font::newBMFontRasterizer(love::filesystem::FileData *fontdef, const std::vector<image::ImageData *> &images, float pixeldensity)
+{
+	return new BMFontRasterizer(fontdef, images, pixeldensity);
 }
 
-Rasterizer *Font::newImageRasterizer(love::image::ImageData *data, const std::string &text, int extraspacing)
+Rasterizer *Font::newImageRasterizer(love::image::ImageData *data, const std::string &text, int extraspacing, float pixeldensity)
 {
 	std::vector<uint32> glyphs;
 	glyphs.reserve(text.size());
@@ -70,12 +76,12 @@ Rasterizer *Font::newImageRasterizer(love::image::ImageData *data, const std::st
 		throw love::Exception("UTF-8 decoding error: %s", e.what());
 	}
 
-	return newImageRasterizer(data, &glyphs[0], (int) glyphs.size(), extraspacing);
+	return newImageRasterizer(data, &glyphs[0], (int) glyphs.size(), extraspacing, pixeldensity);
 }
 
-Rasterizer *Font::newImageRasterizer(love::image::ImageData *data, uint32 *glyphs, int numglyphs, int extraspacing)
+Rasterizer *Font::newImageRasterizer(love::image::ImageData *data, uint32 *glyphs, int numglyphs, int extraspacing, float pixeldensity)
 {
-	return new ImageRasterizer(data, glyphs, numglyphs, extraspacing);
+	return new ImageRasterizer(data, glyphs, numglyphs, extraspacing, pixeldensity);
 }
 
 GlyphData *Font::newGlyphData(Rasterizer *r, const std::string &text)

+ 5 - 3
src/modules/font/Font.h

@@ -48,12 +48,14 @@ public:
 	virtual Rasterizer *newRasterizer(love::filesystem::FileData *data) = 0;
 
 	virtual Rasterizer *newTrueTypeRasterizer(int size, TrueTypeRasterizer::Hinting hinting);
+	virtual Rasterizer *newTrueTypeRasterizer(int size, float pixeldensity, TrueTypeRasterizer::Hinting hinting);
 	virtual Rasterizer *newTrueTypeRasterizer(love::Data *data, int size, TrueTypeRasterizer::Hinting hinting) = 0;
+	virtual Rasterizer *newTrueTypeRasterizer(love::Data *data, int size, float pixeldensity, TrueTypeRasterizer::Hinting hinting) = 0;
 
-	virtual Rasterizer *newBMFontRasterizer(love::filesystem::FileData *fontdef, const std::vector<image::ImageData *> &images);
+	virtual Rasterizer *newBMFontRasterizer(love::filesystem::FileData *fontdef, const std::vector<image::ImageData *> &images, float pixeldensity);
 
-	virtual Rasterizer *newImageRasterizer(love::image::ImageData *data, const std::string &glyphs, int extraspacing);
-	virtual Rasterizer *newImageRasterizer(love::image::ImageData *data, uint32 *glyphs, int length, int extraspacing);
+	virtual Rasterizer *newImageRasterizer(love::image::ImageData *data, const std::string &glyphs, int extraspacing, float pixeldensity);
+	virtual Rasterizer *newImageRasterizer(love::image::ImageData *data, uint32 *glyphs, int length, int extraspacing, float pixeldensity);
 
 	virtual GlyphData *newGlyphData(Rasterizer *r, const std::string &glyph);
 	virtual GlyphData *newGlyphData(Rasterizer *r, uint32 glyph);

+ 3 - 1
src/modules/font/ImageRasterizer.cpp

@@ -34,12 +34,14 @@ inline bool equal(const love::image::pixel &a, const love::image::pixel &b)
 	return (a.r == b.r && a.g == b.g && a.b == b.b && a.a == b.a);
 }
 
-ImageRasterizer::ImageRasterizer(love::image::ImageData *data, uint32 *glyphs, int numglyphs, int extraspacing)
+ImageRasterizer::ImageRasterizer(love::image::ImageData *data, uint32 *glyphs, int numglyphs, int extraspacing, float pixeldensity)
 	: imageData(data)
 	, glyphs(glyphs)
 	, numglyphs(numglyphs)
 	, extraSpacing(extraspacing)
 {
+	this->pixelDensity = pixeldensity;
+
 	if (data->getFormat() != PIXELFORMAT_RGBA8)
 		throw love::Exception("Only 32-bit RGBA images are supported in Image Fonts!");
 

+ 1 - 1
src/modules/font/ImageRasterizer.h

@@ -39,7 +39,7 @@ namespace font
 class ImageRasterizer : public Rasterizer
 {
 public:
-	ImageRasterizer(love::image::ImageData *imageData, uint32 *glyphs, int numglyphs, int extraspacing);
+	ImageRasterizer(love::image::ImageData *imageData, uint32 *glyphs, int numglyphs, int extraspacing, float pixeldensity);
 	virtual ~ImageRasterizer();
 
 	// Implement Rasterizer

+ 5 - 0
src/modules/font/Rasterizer.cpp

@@ -102,5 +102,10 @@ float Rasterizer::getKerning(uint32 /*leftglyph*/, uint32 /*rightglyph*/) const
 	return 0.0f;
 }
 
+float Rasterizer::getPixelDensity() const
+{
+	return pixelDensity;
+}
+
 } // font
 } // love

+ 3 - 0
src/modules/font/Rasterizer.h

@@ -120,9 +120,12 @@ public:
 
 	virtual DataType getDataType() const = 0;
 
+	float getPixelDensity() const;
+
 protected:
 
 	FontMetrics metrics;
+	float pixelDensity;
 
 }; // Rasterizer
 

+ 15 - 2
src/modules/font/freetype/Font.cpp

@@ -20,9 +20,12 @@
 
 #include "Font.h"
 
+// LOVE
 #include "TrueTypeRasterizer.h"
 #include "font/BMFontRasterizer.h"
+#include "window/Window.h"
 
+// C++
 #include <string.h>
 
 namespace love
@@ -48,14 +51,24 @@ Rasterizer *Font::newRasterizer(love::filesystem::FileData *data)
 	if (TrueTypeRasterizer::accepts(library, data))
 		return newTrueTypeRasterizer(data, 12, TrueTypeRasterizer::HINTING_NORMAL);
 	else if (BMFontRasterizer::accepts(data))
-		return newBMFontRasterizer(data, {});
+		return newBMFontRasterizer(data, {}, 1.0f);
 
 	throw love::Exception("Invalid font file: %s", data->getFilename().c_str());
 }
 
 Rasterizer *Font::newTrueTypeRasterizer(love::Data *data, int size, TrueTypeRasterizer::Hinting hinting)
 {
-	return new TrueTypeRasterizer(library, data, size, hinting);
+	float pixeldensity = 1.0f;
+	auto window = Module::getInstance<window::Window>(Module::M_WINDOW);
+	if (window != nullptr)
+		pixeldensity = window->getPixelDensity();
+
+	return newTrueTypeRasterizer(data, size, pixeldensity, hinting);
+}
+
+Rasterizer *Font::newTrueTypeRasterizer(love::Data *data, int size, float pixeldensity, TrueTypeRasterizer::Hinting hinting)
+{
+	return new TrueTypeRasterizer(library, data, size, pixeldensity, hinting);
 }
 
 const char *Font::getName() const

+ 4 - 3
src/modules/font/freetype/Font.h

@@ -44,11 +44,12 @@ public:
 	virtual ~Font();
 
 	// Implements Font
-	Rasterizer *newRasterizer(love::filesystem::FileData *data);
-	Rasterizer *newTrueTypeRasterizer(love::Data *data, int size, TrueTypeRasterizer::Hinting hinting);
+	Rasterizer *newRasterizer(love::filesystem::FileData *data) override;
+	Rasterizer *newTrueTypeRasterizer(love::Data *data, int size, TrueTypeRasterizer::Hinting hinting) override;
+	Rasterizer *newTrueTypeRasterizer(love::Data *data, int size, float pixeldensity, TrueTypeRasterizer::Hinting hinting) override;
 
 	// Implement Module
-	const char *getName() const;
+	const char *getName() const override;
 
 private:
 

+ 7 - 2
src/modules/font/freetype/TrueTypeRasterizer.cpp

@@ -20,9 +20,11 @@
 
 // LOVE
 #include "TrueTypeRasterizer.h"
-
 #include "common/Exception.h"
 
+// C
+#include <math.h>
+
 namespace love
 {
 namespace font
@@ -30,10 +32,13 @@ namespace font
 namespace freetype
 {
 
-TrueTypeRasterizer::TrueTypeRasterizer(FT_Library library, love::Data *data, int size, Hinting hinting)
+TrueTypeRasterizer::TrueTypeRasterizer(FT_Library library, love::Data *data, int size, float pixeldensity, Hinting hinting)
 	: data(data)
 	, hinting(hinting)
 {
+	this->pixelDensity = pixeldensity;
+	size = floorf(size * pixeldensity + 0.5f);
+
 	if (size <= 0)
 		throw love::Exception("Invalid TrueType font size: %d", size);
 

+ 1 - 1
src/modules/font/freetype/TrueTypeRasterizer.h

@@ -44,7 +44,7 @@ class TrueTypeRasterizer : public love::font::TrueTypeRasterizer
 {
 public:
 
-	TrueTypeRasterizer(FT_Library library, love::Data *data, int size, Hinting hinting);
+	TrueTypeRasterizer(FT_Library library, love::Data *data, int size, float pixeldensity, Hinting hinting);
 	virtual ~TrueTypeRasterizer();
 
 	// Implement Rasterizer

+ 30 - 14
src/modules/font/wrap_Font.cpp

@@ -78,7 +78,13 @@ int w_newTrueTypeRasterizer(lua_State *L)
 		if (hintstr && !TrueTypeRasterizer::getConstant(hintstr, hinting))
 			return luaL_error(L, "Invalid TrueType font hinting mode: %s", hintstr);
 
-		luax_catchexcept(L, [&](){ t = instance()->newTrueTypeRasterizer(size, hinting); });
+		if (lua_isnoneornil(L, 3))
+			luax_catchexcept(L, [&](){ t = instance()->newTrueTypeRasterizer(size, hinting); });
+		else
+		{
+			float pixeldensity = (float) luaL_checknumber(L, 3);
+			luax_catchexcept(L, [&](){ t = instance()->newTrueTypeRasterizer(size, pixeldensity, hinting); });
+		}
 	}
 	else
 	{
@@ -98,10 +104,21 @@ int w_newTrueTypeRasterizer(lua_State *L)
 		if (hintstr && !TrueTypeRasterizer::getConstant(hintstr, hinting))
 			return luaL_error(L, "Invalid TrueType font hinting mode: %s", hintstr);
 
-		luax_catchexcept(L,
-			[&]() { t = instance()->newTrueTypeRasterizer(d, size, hinting); },
-			[&](bool) { d->release(); }
-		);
+		if (lua_isnoneornil(L, 4))
+		{
+			luax_catchexcept(L,
+				[&]() { t = instance()->newTrueTypeRasterizer(d, size, hinting); },
+				[&](bool) { d->release(); }
+			);
+		}
+		else
+		{
+			float pixeldensity = (float) luaL_checknumber(L, 4);
+			luax_catchexcept(L,
+				[&]() { t = instance()->newTrueTypeRasterizer(d, size, pixeldensity, hinting); },
+				[&](bool) { d->release(); }
+			);
+		}
 	}
 
 	luax_pushtype(L, t);
@@ -121,6 +138,7 @@ int w_newBMFontRasterizer(lua_State *L)
 
 	filesystem::FileData *d = filesystem::luax_getfiledata(L, 1);
 	std::vector<image::ImageData *> images;
+	float pixeldensity = (float) luaL_optnumber(L, 3, 1.0);
 
 	if (lua_istable(L, 2))
 	{
@@ -138,17 +156,14 @@ int w_newBMFontRasterizer(lua_State *L)
 	}
 	else
 	{
-		for (int i = 2; i <= lua_gettop(L); i++)
-		{
-			convimagedata(L, i);
-			image::ImageData *id = luax_checktype<image::ImageData>(L, i);
-			images.push_back(id);
-			id->retain();
-		}
+		convimagedata(L, 2);
+		image::ImageData *id = luax_checktype<image::ImageData>(L, 2);
+		images.push_back(id);
+		id->retain();
 	}
 
 	luax_catchexcept(L,
-		[&]() { t = instance()->newBMFontRasterizer(d, images); },
+		[&]() { t = instance()->newBMFontRasterizer(d, images, pixeldensity); },
 		[&](bool) { d->release(); for (auto id : images) id->release(); }
 	);
 
@@ -166,8 +181,9 @@ int w_newImageRasterizer(lua_State *L)
 	image::ImageData *d = luax_checktype<image::ImageData>(L, 1);
 	std::string glyphs = luax_checkstring(L, 2);
 	int extraspacing = (int) luaL_optnumber(L, 3, 0);
+	float pixeldensity = (float) luaL_optnumber(L, 4, 1.0);
 
-	luax_catchexcept(L, [&](){ t = instance()->newImageRasterizer(d, glyphs, extraspacing); });
+	luax_catchexcept(L, [&](){ t = instance()->newImageRasterizer(d, glyphs, extraspacing, pixeldensity); });
 
 	luax_pushtype(L, t);
 	t->release();

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

@@ -290,14 +290,14 @@ public:
 	/**
 	 * Sets the current graphics display viewport dimensions.
 	 **/
-	virtual void setViewportSize(int width, int height) = 0;
+	virtual void setViewportSize(int width, int height, int pixelwidth, int pixelheight) = 0;
 
 	/**
 	 * Sets the current graphics display viewport and initializes the renderer.
 	 * @param width The viewport width.
 	 * @param height The viewport height.
 	 **/
-	virtual bool setMode(int width, int height) = 0;
+	virtual bool setMode(int width, int height, int pixelwidth, int pixelheight) = 0;
 
 	/**
 	 * Un-sets the current graphics display mode (uninitializing objects if
@@ -319,6 +319,9 @@ public:
 	 **/
 	virtual bool isActive() const = 0;
 
+	virtual int getWidth() const = 0;
+	virtual int getHeight() const = 0;
+
 	virtual bool isCanvasActive() const = 0;
 
 	virtual void flushStreamDraws() = 0;

+ 13 - 12
src/modules/graphics/Quad.cpp

@@ -46,12 +46,13 @@ Quad::~Quad()
 void Quad::refresh(const Quad::Viewport &v, double sw, double sh)
 {
 	viewport = v;
+	this->sw = sw;
+	this->sh = sh;
 
 	// Vertices are ordered for use with triangle strips:
-	// 0----2
-	// |  / |
-	// | /  |
-	// 1----3
+	// 0---2
+	// | / |
+	// 1---3
 	vertices[0].x = 0.0f;
 	vertices[0].y = 0.0f;
 	vertices[1].x = 0.0f;
@@ -61,14 +62,14 @@ void Quad::refresh(const Quad::Viewport &v, double sw, double sh)
 	vertices[3].x = (float) v.w;
 	vertices[3].y = (float) v.h;
 
-	vertices[0].s = (float) (v.x/sw);
-	vertices[0].t = (float) (v.y/sh);
-	vertices[1].s = (float) (v.x/sw);
-	vertices[1].t = (float) ((v.y+v.h)/sh);
-	vertices[2].s = (float) ((v.x+v.w)/sw);
-	vertices[2].t = (float) (v.y/sh);
-	vertices[3].s = (float) ((v.x+v.w)/sw);
-	vertices[3].t = (float) ((v.y+v.h)/sh);
+	vertices[0].s = (float) (v.x / sw);
+	vertices[0].t = (float) (v.y / sh);
+	vertices[1].s = (float) (v.x / sw);
+	vertices[1].t = (float) ((v.y + v.h) / sh);
+	vertices[2].s = (float) ((v.x + v.w) / sw);
+	vertices[2].t = (float) (v.y / sh);
+	vertices[3].s = (float) ((v.x + v.w) / sw);
+	vertices[3].t = (float) ((v.y + v.h) / sh);
 }
 
 void Quad::setViewport(const Quad::Viewport &v)

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

@@ -33,6 +33,8 @@ Texture::Texture()
 	: format(PIXELFORMAT_UNKNOWN)
 	, width(0)
 	, height(0)
+	, pixelWidth(0)
+	, pixelHeight(0)
 	, filter(getDefaultFilter())
 	, wrap()
 	, vertices()
@@ -90,6 +92,21 @@ int Texture::getHeight() const
 	return height;
 }
 
+int Texture::getPixelWidth() const
+{
+	return pixelWidth;
+}
+
+int Texture::getPixelHeight() const
+{
+	return pixelHeight;
+}
+
+float Texture::getPixelDensity() const
+{
+	return (float) pixelHeight / (float) height;
+}
+
 const Texture::Filter &Texture::getFilter() const
 {
 	return filter;

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

@@ -94,6 +94,11 @@ public:
 	virtual int getWidth() const;
 	virtual int getHeight() const;
 
+	virtual int getPixelWidth() const;
+	virtual int getPixelHeight() const;
+
+	float getPixelDensity() const;
+
 	virtual void setFilter(const Filter &f) = 0;
 	virtual const Filter &getFilter() const;
 
@@ -125,6 +130,9 @@ protected:
 	int width;
 	int height;
 
+	int pixelWidth;
+	int pixelHeight;
+
 	Filter filter;
 	Wrap wrap;
 

+ 22 - 25
src/modules/graphics/opengl/Canvas.cpp

@@ -100,35 +100,32 @@ static bool createMSAABuffer(int width, int height, int &samples, PixelFormat pi
 love::Type Canvas::type("Canvas", &Texture::type);
 int Canvas::canvasCount = 0;
 
-Canvas::Canvas(int width, int height, PixelFormat format, int msaa)
-	: fbo(0)
+Canvas::Canvas(int width, int height, const Settings &settings)
+	: settings(settings)
+	, fbo(0)
 	, texture(0)
     , msaa_buffer(0)
-	, requested_format(format)
-    , requested_samples(msaa)
 	, actual_samples(0)
 	, texture_memory(0)
 {
 	this->width = width;
 	this->height = height;
-
-	float w = static_cast<float>(width);
-	float h = static_cast<float>(height);
+	this->pixelWidth = (int) ((width * settings.pixeldensity) + 0.5);
+	this->pixelHeight = (int) ((height * settings.pixeldensity) + 0.5);
 
 	// Vertices are ordered for use with triangle strips:
-	// 0----2
-	// |  / |
-	// | /  |
-	// 1----3
+	// 0---2
+	// | / |
+	// 1---3
 	// world coordinates
 	vertices[0].x = 0;
 	vertices[0].y = 0;
 	vertices[1].x = 0;
-	vertices[1].y = h;
-	vertices[2].x = w;
+	vertices[1].y = (float) height;
+	vertices[2].x = (float) width;
 	vertices[2].y = 0;
-	vertices[3].x = w;
-	vertices[3].y = h;
+	vertices[3].x = (float) width;
+	vertices[3].y = (float) height;
 
 	// texture coordinates
 	vertices[0].s = 0;
@@ -140,7 +137,7 @@ Canvas::Canvas(int width, int height, PixelFormat format, int msaa)
 	vertices[3].s = 1;
 	vertices[3].t = 1;
 
-	this->format = getSizedFormat(requested_format);
+	this->format = getSizedFormat(settings.format);
 
 	loadVolatile();
 
@@ -165,7 +162,7 @@ bool Canvas::loadVolatile()
 	status = GL_FRAMEBUFFER_COMPLETE;
 
 	// glTexImage2D is guaranteed to error in this case.
-	if (width > gl.getMaxTextureSize() || height > gl.getMaxTextureSize())
+	if (pixelWidth > gl.getMaxTextureSize() || pixelHeight > gl.getMaxTextureSize())
 	{
 		status = GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT;
 		return false;
@@ -173,8 +170,8 @@ bool Canvas::loadVolatile()
 
 	// getMaxRenderbufferSamples will be 0 on systems that don't support
 	// multisampled renderbuffers / don't export FBO multisample extensions.
-	requested_samples = std::min(requested_samples, gl.getMaxRenderbufferSamples());
-	requested_samples = std::max(requested_samples, 0);
+	settings.msaa = std::min(settings.msaa, gl.getMaxRenderbufferSamples());
+	settings.msaa = std::max(settings.msaa, 0);
 
 	glGenTextures(1, &texture);
 	gl.bindTextureToUnit(texture, 0, false);
@@ -191,8 +188,8 @@ bool Canvas::loadVolatile()
 	while (glGetError() != GL_NO_ERROR)
 		/* Clear the error buffer. */;
 
-	glTexImage2D(GL_TEXTURE_2D, 0, fmt.internalformat, width, height, 0,
-	             fmt.externalformat, fmt.type, nullptr);
+	glTexImage2D(GL_TEXTURE_2D, 0, fmt.internalformat, pixelWidth, pixelHeight,
+	             0, fmt.externalformat, fmt.type, nullptr);
 
 	if (glGetError() != GL_NO_ERROR)
 	{
@@ -215,14 +212,14 @@ bool Canvas::loadVolatile()
 		return false;
 	}
 
-	actual_samples = requested_samples == 1 ? 0 : requested_samples;
+	actual_samples = settings.msaa == 1 ? 0 : settings.msaa;
 
 	if (actual_samples > 0 && !createMSAABuffer(width, height, actual_samples, format, msaa_buffer))
 		actual_samples = 0;
 
 	size_t prevmemsize = texture_memory;
 
-	texture_memory = getPixelFormatSize(format) * width * height;
+	texture_memory = getPixelFormatSize(format) * pixelWidth * pixelHeight;
 	if (msaa_buffer != 0)
 		texture_memory += (texture_memory * actual_samples);
 
@@ -276,7 +273,7 @@ bool Canvas::setWrap(const Texture::Wrap &w)
 	wrap = w;
 
 	if ((GLAD_ES_VERSION_2_0 && !(GLAD_ES_VERSION_3_0 || GLAD_OES_texture_npot))
-		&& (width != nextP2(width) || height != nextP2(height)))
+		&& (pixelWidth != nextP2(pixelWidth) || pixelHeight != nextP2(pixelHeight)))
 	{
 		if (wrap.s != WRAP_CLAMP || wrap.t != WRAP_CLAMP)
 			success = false;
@@ -306,7 +303,7 @@ ptrdiff_t Canvas::getHandle() const
 
 love::image::ImageData *Canvas::newImageData(love::image::Image *module, int x, int y, int w, int h)
 {
-	if (x < 0 || y < 0 || w <= 0 || h <= 0 || (x + w) > getWidth() || (y + h) > getHeight())
+	if (x < 0 || y < 0 || w <= 0 || h <= 0 || (x + w) > getPixelWidth() || (y + h) > getPixelHeight())
 		throw love::Exception("Invalid rectangle dimensions.");
 
 	Graphics *gfx = Module::getInstance<Graphics>(Module::M_GRAPHICS);

+ 11 - 5
src/modules/graphics/opengl/Canvas.h

@@ -45,7 +45,14 @@ public:
 
 	static love::Type type;
 
-	Canvas(int width, int height, PixelFormat format = PIXELFORMAT_NORMAL, int msaa = 0);
+    struct Settings
+    {
+        PixelFormat format = PIXELFORMAT_NORMAL;
+        float pixeldensity = 1.0f;
+        int msaa = 0;
+    };
+
+	Canvas(int width, int height, const Settings &settings);
 	virtual ~Canvas();
 
 	// Implements Volatile.
@@ -71,7 +78,7 @@ public:
 
 	inline int getRequestedMSAA() const
 	{
-		return requested_samples;
+		return settings.msaa;
 	}
 
 	inline ptrdiff_t getMSAAHandle() const
@@ -95,16 +102,15 @@ private:
 
 	void drawv(Graphics *gfx, const Matrix4 &t, const Vertex *v) override;
 
+    Settings settings;
+
 	GLuint fbo;
 
 	GLuint texture;
 	GLuint msaa_buffer;
 
-	PixelFormat requested_format;
-
 	GLenum status;
 
-	int requested_samples;
 	int actual_samples;
 
 	size_t texture_memory;

+ 50 - 38
src/modules/graphics/opengl/Font.cpp

@@ -47,19 +47,20 @@ static inline uint16 normToUint16(double n)
 love::Type Font::type("Font", &Object::type);
 int Font::fontCount = 0;
 
-Font::Font(love::font::Rasterizer *r, const Texture::Filter &filter)
+Font::Font(love::font::Rasterizer *r, const Texture::Filter &f)
 	: rasterizers({r})
 	, height(r->getHeight())
 	, lineHeight(1)
 	, textureWidth(128)
 	, textureHeight(128)
-	, filter(filter)
+	, filter(f)
+	, pixelDensity(r->getPixelDensity())
 	, useSpacesAsTab(false)
 	, quadIndices(20) // We make this bigger at draw-time, if needed.
 	, textureCacheID(0)
 	, textureMemorySize(0)
 {
-	this->filter.mipmap = Texture::FILTER_NONE;
+	filter.mipmap = Texture::FILTER_NONE;
 
 	// Try to find the best texture size match for the font size. default to the
 	// largest texture size if no rough match is found.
@@ -100,9 +101,10 @@ Font::TextureSize Font::getNextTextureSize() const
 {
 	TextureSize size = {textureWidth, textureHeight};
 
-	int maxsize = std::min(4096, gl.getMaxTextureSize());
+	int maxwidth  = std::min(8192, gl.getMaxTextureSize());
+	int maxheight = std::min(4096, gl.getMaxTextureSize());
 
-	if (size.width * 2 <= maxsize || size.height * 2 <= maxsize)
+	if (size.width * 2 <= maxwidth || size.height * 2 <= maxheight)
 	{
 		// {128, 128} -> {256, 128} -> {256, 256} -> {512, 256} -> etc.
 		if (size.width == size.height)
@@ -114,7 +116,7 @@ Font::TextureSize Font::getNextTextureSize() const
 	return size;
 }
 
-void Font::createTexture()
+bool Font::createTexture()
 {
 	auto gfx = Module::getInstance<graphics::Graphics>(Module::M_GRAPHICS);
 	gfx->flushStreamDraws();
@@ -200,6 +202,8 @@ void Font::createTexture()
 	}
 	else
 		textures.push_back(t);
+
+	return true;
 }
 
 love::font::GlyphData *Font::getRasterizerGlyphData(uint32 glyph)
@@ -236,32 +240,35 @@ const Font::Glyph &Font::addGlyph(uint32 glyph)
 	int w = gd->getWidth();
 	int h = gd->getHeight();
 
-	if (textureX + w + TEXTURE_PADDING > textureWidth)
+	if (w + TEXTURE_PADDING * 2 < textureWidth && h + TEXTURE_PADDING * 2 < textureHeight)
 	{
-		// out of space - new row!
-		textureX = TEXTURE_PADDING;
-		textureY += rowHeight;
-		rowHeight = TEXTURE_PADDING;
-	}
+		if (textureX + w + TEXTURE_PADDING > textureWidth)
+		{
+			// Out of space - new row!
+			textureX = TEXTURE_PADDING;
+			textureY += rowHeight;
+			rowHeight = TEXTURE_PADDING;
+		}
 
-	if (textureY + h + TEXTURE_PADDING > textureHeight)
-	{
-		// totally out of space - new texture!
-		createTexture();
+		if (textureY + h + TEXTURE_PADDING > textureHeight)
+		{
+			// Totally out of space - new texture!
+			createTexture();
 
-		// Makes sure the above code for checking if the glyph can fit at
-		// the current position in the texture is run again for this glyph.
-		return addGlyph(glyph);
+			// Makes sure the above code for checking if the glyph can fit at
+			// the current position in the texture is run again for this glyph.
+			return addGlyph(glyph);
+		}
 	}
 
 	Glyph g;
 
 	g.texture = 0;
-	g.spacing = gd->getAdvance();
+	g.spacing = floorf(gd->getAdvance() / pixelDensity + 0.5f);
 
 	memset(g.vertices, 0, sizeof(GlyphVertex) * 4);
 
-	// don't waste space for empty glyphs. also fixes a divide by zero bug with ATI drivers
+	// Don't waste space for empty glyphs.
 	if (w > 0 && h > 0)
 	{
 		bool isSRGB = isGammaCorrect();
@@ -278,31 +285,31 @@ const Font::Glyph &Font::addGlyph(uint32 glyph)
 
 		Color c(255, 255, 255, 255);
 
-		// 0----2
-		// |  / |
-		// | /  |
-		// 1----3
-		const GlyphVertex verts[4] = {
-			{float(0), float(0), normToUint16((tX+0)/tWidth), normToUint16((tY+0)/tHeight), c},
-			{float(0), float(h), normToUint16((tX+0)/tWidth), normToUint16((tY+h)/tHeight), c},
-			{float(w), float(0), normToUint16((tX+w)/tWidth), normToUint16((tY+0)/tHeight), c},
-			{float(w), float(h), normToUint16((tX+w)/tWidth), normToUint16((tY+h)/tHeight), c}
+		// 0---2
+		// | / |
+		// 1---3
+		const GlyphVertex verts[4] =
+		{
+			{0.0f,           0.0f,           normToUint16((tX+0)/tWidth), normToUint16((tY+0)/tHeight), c},
+			{0.0f,           h/pixelDensity, normToUint16((tX+0)/tWidth), normToUint16((tY+h)/tHeight), c},
+			{w/pixelDensity, 0.0f,           normToUint16((tX+w)/tWidth), normToUint16((tY+0)/tHeight), c},
+			{w/pixelDensity, h/pixelDensity, normToUint16((tX+w)/tWidth), normToUint16((tY+h)/tHeight), c}
 		};
 
 		// Copy vertex data to the glyph and set proper bearing.
 		for (int i = 0; i < 4; i++)
 		{
 			g.vertices[i] = verts[i];
-			g.vertices[i].x += gd->getBearingX();
-			g.vertices[i].y -= gd->getBearingY();
+			g.vertices[i].x += gd->getBearingX() / pixelDensity;
+			g.vertices[i].y -= gd->getBearingY() / pixelDensity;
 		}
 
 		textureX += w + TEXTURE_PADDING;
 		rowHeight = std::max(rowHeight, h + TEXTURE_PADDING);
 	}
 
-	const auto p = glyphs.insert(std::make_pair(glyph, g));
-	return p.first->second;
+	glyphs[glyph] = g;
+	return glyphs[glyph];
 }
 
 const Font::Glyph &Font::findGlyph(uint32 glyph)
@@ -329,7 +336,7 @@ float Font::getKerning(uint32 leftglyph, uint32 rightglyph)
 	{
 		if (r->hasGlyph(leftglyph) && r->hasGlyph(rightglyph))
 		{
-			k = r->getKerning(leftglyph, rightglyph);
+			k = floorf(r->getKerning(leftglyph, rightglyph) / pixelDensity + 0.5f);
 			break;
 		}
 	}
@@ -390,7 +397,7 @@ void Font::getCodepointsFromString(const std::vector<ColoredString> &strs, Color
 
 float Font::getHeight() const
 {
-	return (float) height;
+	return (float) floorf(height / pixelDensity + 0.5f);
 }
 
 std::vector<Font::DrawCommand> Font::generateVertices(const ColoredCodepoints &codepoints, const Colorf &constantcolor, std::vector<GlyphVertex> &vertices, float extra_spacing, Vector offset, TextInfo *info)
@@ -945,12 +952,12 @@ void Font::unloadVolatile()
 
 int Font::getAscent() const
 {
-	return rasterizers[0]->getAscent();
+	return floorf(rasterizers[0]->getAscent() / pixelDensity + 0.5f);
 }
 
 int Font::getDescent() const
 {
-	return rasterizers[0]->getDescent();
+	return floorf(rasterizers[0]->getDescent() / pixelDensity + 0.5f);
 }
 
 float Font::getBaseline() const
@@ -1014,6 +1021,11 @@ void Font::setFallbacks(const std::vector<Font *> &fallbacks)
 		rasterizers.push_back(f->rasterizers[0]);
 }
 
+float Font::getPixelDensity() const
+{
+	return pixelDensity;
+}
+
 uint32 Font::getTextureCacheID() const
 {
 	return textureCacheID;

+ 6 - 2
src/modules/graphics/opengl/Font.h

@@ -100,7 +100,7 @@ public:
 		int vertexcount;
 	};
 
-	Font(love::font::Rasterizer *r, const Texture::Filter &filter = Texture::getDefaultFilter());
+	Font(love::font::Rasterizer *r, const Texture::Filter &filter);
 
 	virtual ~Font();
 
@@ -179,6 +179,8 @@ public:
 
 	void setFallbacks(const std::vector<Font *> &fallbacks);
 
+	float getPixelDensity() const;
+
 	uint32 getTextureCacheID() const;
 
 	static bool getConstant(const char *in, AlignMode &out);
@@ -202,7 +204,7 @@ private:
 	};
 
 	TextureSize getNextTextureSize() const;
-	void createTexture();
+	bool createTexture();
 	love::font::GlyphData *getRasterizerGlyphData(uint32 glyph);
 	const Glyph &addGlyph(uint32 glyph);
 	const Glyph &findGlyph(uint32 glyph);
@@ -230,6 +232,8 @@ private:
 
 	Texture::Filter filter;
 
+	float pixelDensity;
+
 	int textureX, textureY;
 	int rowHeight;
 

+ 0 - 1
src/modules/graphics/opengl/GLBuffer.cpp

@@ -366,7 +366,6 @@ void QuadIndices::fill()
 	using namespace love::graphics::vertex;
 
 	fillIndices(TriangleIndexMode::QUADS, 0, maxSize * 4, (T *) indices);
-
 	indexBuffer->fill(0, indexBuffer->getSize(), indices);
 }
 

+ 89 - 31
src/modules/graphics/opengl/Graphics.cpp

@@ -56,6 +56,8 @@ Graphics::Graphics()
 	: quadIndices(nullptr)
 	, width(0)
 	, height(0)
+	, pixelWidth(0)
+	, pixelHeight(0)
 	, created(false)
 	, active(true)
 	, writingToStencil(false)
@@ -74,10 +76,11 @@ Graphics::Graphics()
 
 		if (window->isOpen())
 		{
-			int w = 0, h = 0;
-			window->getPixelDimensions(w, h);
+			double w = window->getWidth();
+			double h = window->getHeight();
+			window->windowToDPICoords(&w, &h);
 
-			setMode(w, h);
+			setMode((int) w, (int) h, window->getPixelWidth(), window->getPixelHeight());
 		}
 	}
 }
@@ -221,22 +224,45 @@ void Graphics::checkSetDefaultFont()
 	states.back().font.set(defaultFont.get());
 }
 
-void Graphics::setViewportSize(int width, int height)
+double Graphics::getCurrentPixelDensity() const
+{
+	if (states.back().canvases.size() > 0)
+	{
+		Canvas *c = states.back().canvases[0];
+		return (double) c->getPixelHeight() / (double) c->getHeight();
+	}
+
+	return getScreenPixelDensity();
+}
+
+double Graphics::getScreenPixelDensity() const
+{
+	return (double) getPixelHeight() / (double) getHeight();
+}
+
+void Graphics::setViewportSize(int width, int height, int pixelwidth, int pixelheight)
 {
 	this->width = width;
 	this->height = height;
+	this->pixelWidth = pixelwidth;
+	this->pixelHeight = pixelheight;
 
 	if (states.back().canvases.empty())
 	{
 		// Set the viewport to top-left corner.
-		gl.setViewport({0, 0, width, height}, false);
+		gl.setViewport({0, 0, pixelwidth, pixelheight});
+
+		// Re-apply the scissor if it was active, since the rectangle passed to
+		// glScissor is affected by the viewport dimensions.
+		if (states.back().scissor)
+			setScissor(states.back().scissorRect);
 
 		// Set up the projection matrix
 		projectionMatrix = Matrix4::ortho(0.0, (float) width, (float) height, 0.0);
 	}
 }
 
-bool Graphics::setMode(int width, int height)
+bool Graphics::setMode(int width, int height, int pixelwidth, int pixelheight)
 {
 	this->width = width;
 	this->height = height;
@@ -247,6 +273,8 @@ bool Graphics::setMode(int width, int height)
 
 	created = true;
 
+	setViewportSize(width, height, pixelwidth, pixelheight);
+
 	// Enable blending
 	glEnable(GL_BLEND);
 
@@ -310,8 +338,6 @@ bool Graphics::setMode(int width, int height)
 	if (quadIndices == nullptr)
 		quadIndices = new QuadIndices(20);
 
-	setViewportSize(width, height);
-
 	// Restore the graphics state.
 	restoreState(states.back());
 
@@ -608,13 +634,15 @@ void Graphics::setCanvas(const std::vector<Canvas *> &canvases)
 	PixelFormat firstformat = firstcanvas->getPixelFormat();
 
 	bool hasSRGBcanvas = firstformat == PIXELFORMAT_sRGBA8;
+	int pixelwidth = firstcanvas->getPixelWidth();
+	int pixelheight = firstcanvas->getPixelHeight();
 
 	for (int i = 1; i < ncanvases; i++)
 	{
 		Canvas *c = canvases[i];
 
-		if (c->getWidth() != firstcanvas->getWidth() || c->getHeight() != firstcanvas->getHeight())
-			throw love::Exception("All canvases in must have the same dimensions.");
+		if (c->getPixelWidth() != pixelwidth || c->getPixelHeight() != pixelheight)
+			throw love::Exception("All canvases in must have the same pixel dimensions.");
 
 		if (!multiformatsupported && c->getPixelFormat() != firstformat)
 			throw love::Exception("This system doesn't support multi-canvas rendering with different canvas formats.");
@@ -632,10 +660,15 @@ void Graphics::setCanvas(const std::vector<Canvas *> &canvases)
 
 	bindCachedFBO(canvases);
 
+	gl.setViewport({0, 0, pixelwidth, pixelheight});
+
+	// Re-apply the scissor if it was active, since the rectangle passed to
+	// glScissor is affected by the viewport dimensions.
+	if (state.scissor)
+		setScissor(state.scissorRect);
+
 	int w = firstcanvas->getWidth();
 	int h = firstcanvas->getHeight();
-
-	gl.setViewport({0, 0, w, h}, true);
 	projectionMatrix = Matrix4::ortho(0.0, (float) w, 0.0, (float) h);
 
 	// Make sure the correct sRGB setting is used when drawing to the canvases.
@@ -683,7 +716,13 @@ void Graphics::setCanvas()
 	state.canvases.clear();
 
 	gl.bindFramebuffer(OpenGL::FRAMEBUFFER_ALL, gl.getDefaultFBO());
-	gl.setViewport({0, 0, width, height}, false);
+
+	gl.setViewport({0, 0, pixelWidth, pixelHeight});
+
+	// Re-apply the scissor if it was active, since the rectangle passed to
+	// glScissor is affected by the viewport dimensions.
+	if (state.scissor)
+		setScissor(state.scissorRect);
 
 	// The projection matrix is flipped compared to rendering to a canvas, due
 	// to OpenGL considering (0,0) bottom-left instead of top-left.
@@ -723,8 +762,8 @@ void Graphics::endPass()
 	// Resolve MSAA buffers.
 	if (canvases.size() > 0 && canvases[0]->getMSAA() > 1)
 	{
-		int w = canvases[0]->getWidth();
-		int h = canvases[0]->getHeight();
+		int w = canvases[0]->getPixelWidth();
+		int h = canvases[0]->getPixelHeight();
 
 		for (int i = 0; i < (int) canvases.size(); i++)
 		{
@@ -909,8 +948,8 @@ void Graphics::bindCachedFBO(const std::vector<Canvas *> &canvases)
 	}
 	else
 	{
-		int w = canvases[0]->getWidth();
-		int h = canvases[0]->getHeight();
+		int w = canvases[0]->getPixelWidth();
+		int h = canvases[0]->getPixelHeight();
 		int msaa = std::max(canvases[0]->getMSAA(), 1);
 
 		glGenFramebuffers(1, &fbo);
@@ -1054,8 +1093,8 @@ void Graphics::present(void *screenshotCallbackData)
 
 	if (!pendingScreenshotCallbacks.empty())
 	{
-		int w = getWidth();
-		int h = getHeight();
+		int w = getPixelWidth();
+		int h = getPixelHeight();
 
 		size_t row = 4 * w;
 		size_t size = row * h;
@@ -1170,6 +1209,16 @@ int Graphics::getHeight() const
 	return height;
 }
 
+int Graphics::getPixelWidth() const
+{
+	return pixelWidth;
+}
+
+int Graphics::getPixelHeight() const
+{
+	return pixelHeight;
+}
+
 bool Graphics::isCreated() const
 {
 	return created;
@@ -1182,8 +1231,17 @@ void Graphics::setScissor(const Rect &rect)
 	DisplayState &state = states.back();
 
 	glEnable(GL_SCISSOR_TEST);
+
+	double density = getCurrentPixelDensity();
+
+	Rect glrect;
+	glrect.x = (int) (rect.x * density);
+	glrect.y = (int) (rect.y * density);
+	glrect.w = (int) (rect.w * density);
+	glrect.h = (int) (rect.h * density);
+
 	// OpenGL's reversed y-coordinate is compensated for in OpenGL::setScissor.
-	gl.setScissor(rect, !state.canvases.empty());
+	gl.setScissor(glrect, !state.canvases.empty());
 
 	state.scissor = true;
 	state.scissorRect = rect;
@@ -1358,14 +1416,14 @@ void Graphics::clearStencil()
 	glClear(GL_STENCIL_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 }
 
-Image *Graphics::newImage(const std::vector<love::image::ImageData *> &data, const Image::Flags &flags)
+Image *Graphics::newImage(const std::vector<love::image::ImageData *> &data, const Image::Settings &settings)
 {
-	return new Image(data, flags);
+	return new Image(data, settings);
 }
 
-Image *Graphics::newImage(const std::vector<love::image::CompressedImageData *> &cdata, const Image::Flags &flags)
+Image *Graphics::newImage(const std::vector<love::image::CompressedImageData *> &cdata, const Image::Settings &settings)
 {
-	return new Image(cdata, flags);
+	return new Image(cdata, settings);
 }
 
 Quad *Graphics::newQuad(Quad::Viewport v, double sw, double sh)
@@ -1388,15 +1446,15 @@ ParticleSystem *Graphics::newParticleSystem(Texture *texture, int size)
 	return new ParticleSystem(texture, size);
 }
 
-Canvas *Graphics::newCanvas(int width, int height, PixelFormat format, int msaa)
+Canvas *Graphics::newCanvas(int width, int height, const Canvas::Settings &settings)
 {
 	if (!Canvas::isSupported())
 		throw love::Exception("Canvases are not supported by your OpenGL drivers!");
 
-	if (!Canvas::isFormatSupported(format))
+	if (!Canvas::isFormatSupported(settings.format))
 	{
 		const char *fstr = "rgba8";
-		love::getConstant(Canvas::getSizedFormat(format), fstr);
+		love::getConstant(Canvas::getSizedFormat(settings.format), fstr);
 		throw love::Exception("The %s canvas format is not supported by your OpenGL drivers.", fstr);
 	}
 
@@ -1405,7 +1463,7 @@ Canvas *Graphics::newCanvas(int width, int height, PixelFormat format, int msaa)
 	else if (height > gl.getMaxTextureSize())
 		throw Exception("Cannot create canvas: height of %d pixels is too large for this system.", height);
 
-	Canvas *canvas = new Canvas(width, height, format, msaa);
+	Canvas *canvas = new Canvas(width, height, settings);
 	GLenum err = canvas->getStatus();
 
 	// everything ok, return canvas (early out)
@@ -1447,9 +1505,9 @@ Text *Graphics::newText(Font *font, const std::vector<Font::ColoredString> &text
 	return new Text(font, text);
 }
 
-Video *Graphics::newVideo(love::video::VideoStream *stream)
+Video *Graphics::newVideo(love::video::VideoStream *stream, float pixeldensity)
 {
-	return new Video(stream);
+	return new Video(stream, pixeldensity);
 }
 
 bool Graphics::isGammaCorrect() const
@@ -1690,7 +1748,7 @@ void Graphics::setPointSize(float size)
 	if (streamBufferState.primitiveMode == vertex::PrimitiveMode::POINTS)
 		flushStreamDraws();
 
-	gl.setPointSize(size);
+	gl.setPointSize(size * getCurrentPixelDensity());
 	states.back().pointSize = size;
 }
 

+ 15 - 14
src/modules/graphics/opengl/Graphics.h

@@ -80,8 +80,8 @@ public:
 	// Implements Module.
 	const char *getName() const override;
 
-	void setViewportSize(int width, int height) override;
-	bool setMode(int width, int height) override;
+	void setViewportSize(int width, int height, int pixelwidth, int pixelheight) override;
+	bool setMode(int width, int height, int pixelwidth, int pixelheight) override;
 	void unSetMode() override;
 
 	void setActive(bool active) override;
@@ -109,15 +109,13 @@ public:
 	 **/
 	void present(void *screenshotCallbackData);
 
-	/**
-	 * Gets the width of the current graphics viewport.
-	 **/
-	int getWidth() const;
+	int getWidth() const override;
+	int getHeight() const override;
+	int getPixelWidth() const;
+	int getPixelHeight() const;
 
-	/**
-	 * Gets the height of the current graphics viewport.
-	 **/
-	int getHeight() const;
+	double getCurrentPixelDensity() const;
+	double getScreenPixelDensity() const;
 
 	/**
 	 * True if a graphics viewport is set.
@@ -172,8 +170,8 @@ public:
 	/**
 	 * Creates an Image object with padding and/or optimization.
 	 **/
-	Image *newImage(const std::vector<love::image::ImageData *> &data, const Image::Flags &flags);
-	Image *newImage(const std::vector<love::image::CompressedImageData *> &cdata, const Image::Flags &flags);
+	Image *newImage(const std::vector<love::image::ImageData *> &data, const Image::Settings &settings);
+	Image *newImage(const std::vector<love::image::CompressedImageData *> &cdata, const Image::Settings &settings);
 
 	Quad *newQuad(Quad::Viewport v, double sw, double sh);
 
@@ -186,7 +184,7 @@ public:
 
 	ParticleSystem *newParticleSystem(Texture *texture, int size);
 
-	Canvas *newCanvas(int width, int height, PixelFormat format = PIXELFORMAT_NORMAL, int msaa = 0);
+	Canvas *newCanvas(int width, int height, const Canvas::Settings &settings);
 
 	Shader *newShader(const Shader::ShaderSource &source);
 
@@ -198,7 +196,7 @@ public:
 
 	Text *newText(Font *font, const std::vector<Font::ColoredString> &text = {});
 
-	Video *newVideo(love::video::VideoStream *stream);
+	Video *newVideo(love::video::VideoStream *stream, float pixeldensity);
 
 	bool isGammaCorrect() const;
 
@@ -440,6 +438,9 @@ private:
 
 	int width;
 	int height;
+	int pixelWidth;
+	int pixelHeight;
+
 	bool created;
 	bool active;
 

+ 53 - 47
src/modules/graphics/opengl/Image.cpp

@@ -94,11 +94,11 @@ static bool verifyMipmapLevels(const std::vector<T> &miplevels)
 	return true;
 }
 
-Image::Image(const std::vector<love::image::ImageData *> &imagedata, const Flags &flags)
+Image::Image(const std::vector<love::image::ImageData *> &imagedata, const Settings &settings)
 	: texture(0)
 	, mipmapSharpness(defaultMipmapSharpness)
 	, compressed(false)
-	, flags(flags)
+	, settings(settings)
 	, sRGB(false)
 	, usingDefaultTexture(false)
 	, textureMemorySize(0)
@@ -106,11 +106,14 @@ Image::Image(const std::vector<love::image::ImageData *> &imagedata, const Flags
 	if (imagedata.empty())
 		throw love::Exception("");
 
-	width = imagedata[0]->getWidth();
-	height = imagedata[0]->getHeight();
+	pixelWidth  = imagedata[0]->getWidth();
+	pixelHeight = imagedata[0]->getHeight();
+
+	width  = (int) (pixelWidth / settings.pixeldensity + 0.5);
+	height = (int) (pixelHeight / settings.pixeldensity + 0.5);
 
 	if (verifyMipmapLevels(imagedata))
-		this->flags.mipmaps = true;
+		this->settings.mipmaps = true;
 
 	for (const auto &id : imagedata)
 		data.push_back(id);
@@ -123,24 +126,27 @@ Image::Image(const std::vector<love::image::ImageData *> &imagedata, const Flags
 	++imageCount;
 }
 
-Image::Image(const std::vector<love::image::CompressedImageData *> &compresseddata, const Flags &flags)
+Image::Image(const std::vector<love::image::CompressedImageData *> &compresseddata, const Settings &settings)
 	: texture(0)
 	, mipmapSharpness(defaultMipmapSharpness)
 	, compressed(true)
-	, flags(flags)
+	, settings(settings)
 	, sRGB(false)
 	, usingDefaultTexture(false)
 	, textureMemorySize(0)
 {
-	width = compresseddata[0]->getWidth(0);
-	height = compresseddata[0]->getHeight(0);
+	pixelWidth  = compresseddata[0]->getWidth(0);
+	pixelHeight = compresseddata[0]->getHeight(0);
+
+	width  = (int) (pixelWidth / settings.pixeldensity + 0.5);
+	height = (int) (pixelHeight / settings.pixeldensity + 0.5);
 
 	if (verifyMipmapLevels(compresseddata))
-		this->flags.mipmaps = true;
-	else if (flags.mipmaps && getMipmapCount(width, height) != compresseddata[0]->getMipmapCount())
+		this->settings.mipmaps = true;
+	else if (settings.mipmaps && getMipmapCount(pixelWidth, pixelHeight) != compresseddata[0]->getMipmapCount())
 	{
 		if (compresseddata[0]->getMipmapCount() == 1)
-			this->flags.mipmaps = false;
+			this->settings.mipmaps = false;
 		else
 		{
 			throw love::Exception("Image cannot have mipmaps: compressed image data does not have all required mipmap levels (expected %d, got %d)",
@@ -172,10 +178,9 @@ void Image::preload()
 		vertices[i].color = Color(255, 255, 255, 255);
 
 	// Vertices are ordered for use with triangle strips:
-	// 0----2
-	// |  / |
-	// | /  |
-	// 1----3
+	// 0---2
+	// | / |
+	// 1---3
 	vertices[0].x = 0.0f;
 	vertices[0].y = 0.0f;
 	vertices[1].x = 0.0f;
@@ -194,13 +199,13 @@ void Image::preload()
 	vertices[3].s = 1.0f;
 	vertices[3].t = 1.0f;
 
-	if (flags.mipmaps)
+	if (settings.mipmaps)
 		filter.mipmap = defaultMipmapFilter;
 
 	if (!isGammaCorrect())
-		flags.linear = false;
+		settings.linear = false;
 
-	if (isGammaCorrect() && !flags.linear)
+	if (isGammaCorrect() && !settings.linear)
 		sRGB = true;
 	else
 		sRGB = false;
@@ -210,7 +215,7 @@ void Image::generateMipmaps()
 {
 	// The GL_GENERATE_MIPMAP texparameter is set in loadVolatile if we don't
 	// have support for glGenerateMipmap.
-	if (flags.mipmaps && !isCompressed() &&
+	if (settings.mipmaps && !isCompressed() &&
 		(GLAD_ES_VERSION_2_0 || GLAD_VERSION_3_0 || GLAD_ARB_framebuffer_object))
 	{
 		if (gl.bugs.generateMipmapsRequiresTexture2DEnable)
@@ -239,13 +244,13 @@ void Image::loadFromCompressedData()
 	OpenGL::TextureFormat fmt = OpenGL::convertPixelFormat(format, false, sRGB);
 
 	if (isGammaCorrect() && !sRGB)
-		flags.linear = true;
+		settings.linear = true;
 
 	int count = 1;
 
-	if (flags.mipmaps && cdata.size() > 1)
+	if (settings.mipmaps && cdata.size() > 1)
 		count = (int) cdata.size();
-	else if (flags.mipmaps)
+	else if (settings.mipmaps)
 		count = cdata[0]->getMipmapCount();
 
 	for (int i = 0; i < count; i++)
@@ -266,9 +271,9 @@ void Image::loadFromImageData()
 	OpenGL::TextureFormat fmt = OpenGL::convertPixelFormat(format, false, sRGB);
 
 	if (isGammaCorrect() && !sRGB)
-		flags.linear = true;
+		settings.linear = true;
 
-	int mipcount = flags.mipmaps ? (int) data.size() : 1;
+	int mipcount = settings.mipmaps ? (int) data.size() : 1;
 
 	for (int i = 0; i < mipcount; i++)
 	{
@@ -310,16 +315,16 @@ bool Image::loadVolatile()
 		if (sRGB && (GLAD_ES_VERSION_2_0 && GLAD_EXT_sRGB && !GLAD_ES_VERSION_3_0)
 			&& data.size() <= 1)
 		{
-			flags.mipmaps = false;
+			settings.mipmaps = false;
 			filter.mipmap = FILTER_NONE;
 		}
 	}
 
 	// NPOT textures don't support mipmapping without full NPOT support.
 	if ((GLAD_ES_VERSION_2_0 && !(GLAD_ES_VERSION_3_0 || GLAD_OES_texture_npot))
-		&& (width != nextP2(width) || height != nextP2(height)))
+		&& (pixelWidth != nextP2(pixelWidth) || pixelHeight != nextP2(pixelHeight)))
 	{
-		flags.mipmaps = false;
+		settings.mipmaps = false;
 		filter.mipmap = FILTER_NONE;
 	}
 
@@ -334,16 +339,16 @@ bool Image::loadVolatile()
 	setMipmapSharpness(mipmapSharpness);
 
 	// Use a default texture if the size is too big for the system.
-	if (width > gl.getMaxTextureSize() || height > gl.getMaxTextureSize())
+	if (pixelWidth > gl.getMaxTextureSize() || pixelHeight > gl.getMaxTextureSize())
 	{
 		loadDefaultTexture();
 		return true;
 	}
 
-	if (!flags.mipmaps && (GLAD_ES_VERSION_3_0 || GLAD_VERSION_1_0))
+	if (!settings.mipmaps && (GLAD_ES_VERSION_3_0 || GLAD_VERSION_1_0))
 		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
 
-	if (flags.mipmaps && !isCompressed() && data.size() <= 1 &&
+	if (settings.mipmaps && !isCompressed() && data.size() <= 1 &&
 		!(GLAD_ES_VERSION_2_0 || GLAD_VERSION_3_0 || GLAD_ARB_framebuffer_object))
 	{
 		// Auto-generate mipmaps every time the texture is modified, if
@@ -378,7 +383,7 @@ bool Image::loadVolatile()
 	else
 		textureMemorySize = data[0]->getSize();
 
-	if (flags.mipmaps)
+	if (settings.mipmaps)
 		textureMemorySize *= 1.33334;
 
 	gl.updateTextureMemorySize(prevmemsize, textureMemorySize);
@@ -406,7 +411,7 @@ bool Image::refresh(int xoffset, int yoffset, int w, int h)
 		return false;
 
 	if (xoffset < 0 || yoffset < 0 || w <= 0 || h <= 0 ||
-		(xoffset + w) > width || (yoffset + h) > height)
+		(xoffset + w) > pixelWidth || (yoffset + h) > pixelHeight)
 	{
 		throw love::Exception("Invalid rectangle dimensions.");
 	}
@@ -424,7 +429,7 @@ bool Image::refresh(int xoffset, int yoffset, int w, int h)
 	bool isSRGB = sRGB;
 	OpenGL::TextureFormat fmt = OpenGL::convertPixelFormat(format, false, isSRGB);
 
-	int mipcount = flags.mipmaps ? (int) data.size() : 1;
+	int mipcount = settings.mipmaps ? (int) data.size() : 1;
 
 	// Reupload the sub-rectangle of each mip level (if we have custom mipmaps.)
 	for (int i = 0; i < mipcount; i++)
@@ -465,9 +470,9 @@ const std::vector<StrongRef<love::image::CompressedImageData>> &Image::getCompre
 
 void Image::setFilter(const Texture::Filter &f)
 {
-	if (!validateFilter(f, flags.mipmaps))
+	if (!validateFilter(f, settings.mipmaps))
 	{
-		if (f.mipmap != FILTER_NONE && !flags.mipmaps)
+		if (f.mipmap != FILTER_NONE && !settings.mipmaps)
 			throw love::Exception("Non-mipmapped image cannot have mipmap filtering.");
 		else
 			throw love::Exception("Invalid texture filter.");
@@ -500,7 +505,7 @@ bool Image::setWrap(const Texture::Wrap &w)
 	wrap = w;
 
 	if ((GLAD_ES_VERSION_2_0 && !(GLAD_ES_VERSION_3_0 || GLAD_OES_texture_npot))
-		&& (width != nextP2(width) || height != nextP2(height)))
+		&& (pixelWidth != nextP2(pixelWidth) || pixelHeight != nextP2(pixelHeight)))
 	{
 		if (wrap.s != WRAP_CLAMP || wrap.t != WRAP_CLAMP)
 			success = false;
@@ -543,9 +548,9 @@ float Image::getMipmapSharpness() const
 	return mipmapSharpness;
 }
 
-const Image::Flags &Image::getFlags() const
+const Image::Settings &Image::getFlags() const
 {
-	return flags;
+	return settings;
 }
 
 void Image::setDefaultMipmapSharpness(float sharpness)
@@ -583,23 +588,24 @@ bool Image::hasSRGBSupport()
 	return GLAD_ES_VERSION_3_0 || GLAD_EXT_sRGB || GLAD_VERSION_2_1 || GLAD_EXT_texture_sRGB;
 }
 
-bool Image::getConstant(const char *in, FlagType &out)
+bool Image::getConstant(const char *in, SettingType &out)
 {
-	return flagNames.find(in, out);
+	return settingTypes.find(in, out);
 }
 
-bool Image::getConstant(FlagType in, const char *&out)
+bool Image::getConstant(SettingType in, const char *&out)
 {
-	return flagNames.find(in, out);
+	return settingTypes.find(in, out);
 }
 
-StringMap<Image::FlagType, Image::FLAG_TYPE_MAX_ENUM>::Entry Image::flagNameEntries[] =
+StringMap<Image::SettingType, Image::SETTING_MAX_ENUM>::Entry Image::settingTypeEntries[] =
 {
-	{"mipmaps", FLAG_TYPE_MIPMAPS},
-	{"linear", FLAG_TYPE_LINEAR},
+	{ "mipmaps",      SETTING_MIPMAPS      },
+	{ "linear",       SETTING_LINEAR       },
+	{ "pixeldensity", SETTING_PIXELDENSITY },
 };
 
-StringMap<Image::FlagType, Image::FLAG_TYPE_MAX_ENUM> Image::flagNames(Image::flagNameEntries, sizeof(Image::flagNameEntries));
+StringMap<Image::SettingType, Image::SETTING_MAX_ENUM> Image::settingTypes(Image::settingTypeEntries, sizeof(Image::settingTypeEntries));
 
 } // opengl
 } // graphics

+ 16 - 14
src/modules/graphics/opengl/Image.h

@@ -54,17 +54,19 @@ public:
 
 	static love::Type type;
 
-	enum FlagType
+	enum SettingType
 	{
-		FLAG_TYPE_MIPMAPS,
-		FLAG_TYPE_LINEAR,
-		FLAG_TYPE_MAX_ENUM
+		SETTING_MIPMAPS,
+		SETTING_LINEAR,
+		SETTING_PIXELDENSITY,
+		SETTING_MAX_ENUM
 	};
 
-	struct Flags
+	struct Settings
 	{
 		bool mipmaps = false;
 		bool linear = false;
+		float pixeldensity = 1.0f;
 	};
 
 	/**
@@ -75,14 +77,14 @@ public:
 	 * array is a mipmap level. If more than the base level is present, all
 	 * mip levels must be present.
 	 **/
-	Image(const std::vector<love::image::ImageData *> &data, const Flags &flags);
+	Image(const std::vector<love::image::ImageData *> &data, const Settings &settings);
 
 	/**
 	 * Creates a new Image with compressed image data.
 	 *
 	 * @param cdata The compressed data from which to load the image.
 	 **/
-	Image(const std::vector<love::image::CompressedImageData *> &cdata, const Flags &flags);
+	Image(const std::vector<love::image::CompressedImageData *> &cdata, const Settings &settings);
 
 	virtual ~Image();
 
@@ -112,7 +114,7 @@ public:
 	 **/
 	bool refresh(int xoffset, int yoffset, int w, int h);
 
-	const Flags &getFlags() const;
+	const Settings &getFlags() const;
 
 	static void setDefaultMipmapSharpness(float sharpness);
 	static float getDefaultMipmapSharpness();
@@ -122,8 +124,8 @@ public:
 	static bool isFormatSupported(PixelFormat pixelformat);
 	static bool hasSRGBSupport();
 
-	static bool getConstant(const char *in, FlagType &out);
-	static bool getConstant(FlagType in, const char *&out);
+	static bool getConstant(const char *in, SettingType &out);
+	static bool getConstant(SettingType in, const char *&out);
 
 	static int imageCount;
 
@@ -154,8 +156,8 @@ private:
 	// Whether this Image is using a compressed texture.
 	bool compressed;
 
-	// The flags used to initialize this Image.
-	Flags flags;
+	// The settings used to initialize this Image.
+	Settings settings;
 
 	bool sRGB;
 
@@ -170,8 +172,8 @@ private:
 	static FilterMode defaultMipmapFilter;
 	static float defaultMipmapSharpness;
 
-	static StringMap<FlagType, FLAG_TYPE_MAX_ENUM>::Entry flagNameEntries[];
-	static StringMap<FlagType, FLAG_TYPE_MAX_ENUM> flagNames;
+	static StringMap<SettingType, SETTING_MAX_ENUM>::Entry settingTypeEntries[];
+	static StringMap<SettingType, SETTING_MAX_ENUM> settingTypes;
 
 }; // Image
 

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

@@ -416,15 +416,10 @@ void OpenGL::useVertexAttribArrays(uint32 arraybits)
 		glVertexAttrib4f(ATTRIB_COLOR, 1.0f, 1.0f, 1.0f, 1.0f);
 }
 
-void OpenGL::setViewport(const Rect &v, bool canvasActive)
+void OpenGL::setViewport(const Rect &v)
 {
 	glViewport(v.x, v.y, v.w, v.h);
 	state.viewport = v;
-
-	// glScissor starts from the lower left, so we compensate when setting the
-	// scissor. When the viewport is changed, we need to manually update the
-	// scissor again.
-	setScissor(state.scissor, canvasActive);
 }
 
 Rect OpenGL::getViewport() const
@@ -446,11 +441,6 @@ void OpenGL::setScissor(const Rect &v, bool canvasActive)
 	state.scissor = v;
 }
 
-Rect OpenGL::getScissor() const
-{
-	return state.scissor;
-}
-
 void OpenGL::setPointSize(float size)
 {
 	if (GLAD_VERSION_1_0)

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

@@ -244,7 +244,7 @@ public:
 	 * Sets the OpenGL rendering viewport to the specified rectangle.
 	 * The y-coordinate starts at the top.
 	 **/
-	void setViewport(const Rect &v, bool canvasActive);
+	void setViewport(const Rect &v);
 
 	/**
 	 * Gets the current OpenGL rendering viewport rectangle.
@@ -257,11 +257,6 @@ public:
 	 **/
 	void setScissor(const Rect &v, bool canvasActive);
 
-	/**
-	 * Gets the current scissor box (regardless of whether scissoring is enabled.)
-	 **/
-	Rect getScissor() const;
-
 	/**
 	 * Sets the global point size.
 	 **/

+ 21 - 10
src/modules/graphics/opengl/Video.cpp

@@ -33,8 +33,10 @@ namespace opengl
 
 love::Type Video::type("Video", &Drawable::type);
 
-Video::Video(love::video::VideoStream *stream)
+Video::Video(love::video::VideoStream *stream, float pixeldensity)
 	: stream(stream)
+	, width(stream->getWidth() / pixeldensity)
+	, height(stream->getHeight() / pixeldensity)
 	, filter(Texture::getDefaultFilter())
 {
 	filter.mipmap = Texture::FILTER_NONE;
@@ -45,18 +47,17 @@ Video::Video(love::video::VideoStream *stream)
 		vertices[i].color = Color(255, 255, 255, 255);
 
 	// Vertices are ordered for use with triangle strips:
-	// 0----2
-	// |  / |
-	// | /  |
-	// 1----3
+	// 0---2
+	// | / |
+	// 1---3
 	vertices[0].x = 0.0f;
 	vertices[0].y = 0.0f;
 	vertices[1].x = 0.0f;
-	vertices[1].y = (float) stream->getHeight();
-	vertices[2].x = (float) stream->getWidth();
+	vertices[1].y = (float) height;
+	vertices[2].x = (float) width;
 	vertices[2].y = 0.0f;
-	vertices[3].x = (float) stream->getWidth();
-	vertices[3].y = (float) stream->getHeight();
+	vertices[3].x = (float) width;
+	vertices[3].y = (float) height;
 
 	vertices[0].s = 0.0f;
 	vertices[0].t = 0.0f;
@@ -185,10 +186,20 @@ void Video::setSource(love::audio::Source *source)
 
 int Video::getWidth() const
 {
-	return stream->getWidth();
+	return width;
 }
 
 int Video::getHeight() const
+{
+	return height;
+}
+
+int Video::getPixelWidth() const
+{
+	return stream->getWidth();
+}
+
+int Video::getPixelHeight() const
 {
 	return stream->getHeight();
 }

+ 7 - 1
src/modules/graphics/opengl/Video.h

@@ -42,7 +42,7 @@ public:
 
 	static love::Type type;
 
-	Video(love::video::VideoStream *stream);
+	Video(love::video::VideoStream *stream, float pixeldensity = 1.0f);
 	~Video();
 
 	// Volatile
@@ -60,6 +60,9 @@ public:
 	int getWidth() const;
 	int getHeight() const;
 
+	int getPixelWidth() const;
+	int getPixelHeight() const;
+
 	void setFilter(const Texture::Filter &f);
 	const Texture::Filter &getFilter() const;
 
@@ -70,6 +73,9 @@ private:
 	StrongRef<love::video::VideoStream> stream;
 	StrongRef<love::audio::Source> source;
 
+	int width;
+	int height;
+
 	GLuint textures[3];
 
 	Vertex vertices[4];

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

@@ -90,8 +90,8 @@ int w_Canvas_newImageData(lua_State *L)
 	love::image::Image *image = luax_getmodule<love::image::Image>(L, love::image::Image::type);
 	int x = (int) luaL_optnumber(L, 2, 0);
 	int y = (int) luaL_optnumber(L, 3, 0);
-	int w = (int) luaL_optnumber(L, 4, canvas->getWidth());
-	int h = (int) luaL_optnumber(L, 5, canvas->getHeight());
+	int w = (int) luaL_optnumber(L, 4, canvas->getPixelWidth());
+	int h = (int) luaL_optnumber(L, 5, canvas->getPixelHeight());
 
 	love::image::ImageData *img = nullptr;
 	luax_catchexcept(L, [&](){ img = canvas->newImageData(image, x, y, w, h); });

+ 8 - 0
src/modules/graphics/opengl/wrap_Font.cpp

@@ -188,6 +188,13 @@ int w_Font_setFallbacks(lua_State *L)
 	return 0;
 }
 
+int w_Font_getPixelDensity(lua_State *L)
+{
+	Font *t = luax_checkfont(L, 1);
+	lua_pushnumber(L, t->getPixelDensity());
+	return 1;
+}
+
 static const luaL_Reg w_Font_functions[] =
 {
 	{ "getHeight", w_Font_getHeight },
@@ -202,6 +209,7 @@ static const luaL_Reg w_Font_functions[] =
 	{ "getBaseline", w_Font_getBaseline },
 	{ "hasGlyphs", w_Font_hasGlyphs },
 	{ "setFallbacks", w_Font_setFallbacks },
+	{ "getPixelDensity", w_Font_getPixelDensity },
 	{ 0, 0 }
 };
 

+ 88 - 32
src/modules/graphics/opengl/wrap_Graphics.cpp

@@ -32,6 +32,7 @@
 
 #include <cassert>
 #include <cstring>
+#include <cstdlib>
 
 #include <algorithm>
 
@@ -175,6 +176,25 @@ int w_getDimensions(lua_State *L)
 	return 2;
 }
 
+int w_getPixelWidth(lua_State *L)
+{
+	lua_pushinteger(L, instance()->getPixelWidth());
+	return 1;
+}
+
+int w_getPixelHeight(lua_State *L)
+{
+	lua_pushinteger(L, instance()->getPixelHeight());
+	return 1;
+}
+
+int w_getPixelDimensions(lua_State *L)
+{
+	lua_pushinteger(L, instance()->getPixelWidth());
+	lua_pushinteger(L, instance()->getPixelHeight());
+	return 2;
+}
+
 int w_setCanvas(lua_State *L)
 {
 	// Disable stencil writes.
@@ -385,13 +405,6 @@ int w_getStencilTest(lua_State *L)
 	return 2;
 }
 
-static const char *imageFlagName(Image::FlagType flagtype)
-{
-	const char *name = nullptr;
-	Image::getConstant(flagtype, name);
-	return name;
-}
-
 int w_newImage(lua_State *L)
 {
 	luax_checkgraphicscreated(L);
@@ -399,13 +412,7 @@ int w_newImage(lua_State *L)
 	std::vector<love::image::ImageData *> data;
 	std::vector<love::image::CompressedImageData *> cdata;
 
-	Image::Flags flags;
-	if (!lua_isnoneornil(L, 2))
-	{
-		luaL_checktype(L, 2, LUA_TTABLE);
-		flags.mipmaps = luax_boolflag(L, 2, imageFlagName(Image::FLAG_TYPE_MIPMAPS), flags.mipmaps);
-		flags.linear = luax_boolflag(L, 2, imageFlagName(Image::FLAG_TYPE_LINEAR), flags.linear);
-	}
+	Image::Settings settings;
 
 	bool releasedata = false;
 
@@ -418,6 +425,20 @@ int w_newImage(lua_State *L)
 
 		love::filesystem::FileData *fdata = love::filesystem::luax_getfiledata(L, 1);
 
+		// Parse a density scale of 2.0 from "[email protected]".
+		const std::string &fname = fdata->getName();
+		size_t namelen = fname.length();
+		size_t atpos = fname.rfind('@');
+
+		if (atpos != std::string::npos && atpos + 2 < namelen
+			&& (fname[namelen - 1] == 'x' || fname[namelen - 1] == 'X'))
+		{
+			char *end = nullptr;
+			float density = std::strtof(fname.c_str() + atpos + 1, &end);
+			if (end != nullptr && density > 0.0f)
+				settings.pixeldensity = density;
+		}
+
 		if (imagemodule->isCompressed(fdata))
 		{
 			luax_catchexcept(L,
@@ -441,12 +462,18 @@ int w_newImage(lua_State *L)
 	else
 		data.push_back(love::image::luax_checkimagedata(L, 1));
 
-	if (lua_istable(L, 2))
+	if (!lua_isnoneornil(L, 2))
 	{
-		lua_getfield(L, 2, imageFlagName(Image::FLAG_TYPE_MIPMAPS));
+		luaL_checktype(L, 2, LUA_TTABLE);
+
+		settings.mipmaps = luax_boolflag(L, 2, luax_imageSettingName(Image::SETTING_MIPMAPS), settings.mipmaps);
+		settings.linear = luax_boolflag(L, 2, luax_imageSettingName(Image::SETTING_LINEAR), settings.linear);
+		settings.pixeldensity = (float) luax_numberflag(L, 2, luax_imageSettingName(Image::SETTING_PIXELDENSITY), settings.pixeldensity);
+
+		lua_getfield(L, 2, luax_imageSettingName(Image::SETTING_MIPMAPS));
 
 		// Add all manually specified mipmap images to the array of imagedata.
-		// i.e. flags = {mipmaps = {mip1, mip2, ...}}.
+		// i.e. settings = {mipmaps = {mip1, mip2, ...}}.
 		if (lua_istable(L, -1))
 		{
 			for (size_t i = 1; i <= luax_objlen(L, -1); i++)
@@ -480,9 +507,9 @@ int w_newImage(lua_State *L)
 	luax_catchexcept(L,
 		[&]() {
 			if (!cdata.empty())
-				image = instance()->newImage(cdata, flags);
+				image = instance()->newImage(cdata, settings);
 			else if (!data.empty())
-				image = instance()->newImage(data, flags);
+				image = instance()->newImage(data, settings);
 		},
 		[&](bool) {
 			if (releasedata)
@@ -514,8 +541,20 @@ int w_newQuad(lua_State *L)
 	v.w = luaL_checknumber(L, 3);
 	v.h = luaL_checknumber(L, 4);
 
-	double sw = luaL_checknumber(L, 5);
-	double sh = luaL_checknumber(L, 6);
+	double sw = 0.0f;
+	double sh = 0.0f;
+
+	if (luax_istype(L, 5, Texture::type))
+	{
+		Texture *texture = luax_checktexture(L, 5);
+		sw = texture->getWidth();
+		sh = texture->getHeight();
+	}
+	else
+	{
+		sw = luaL_checknumber(L, 5);
+		sh = luaL_checknumber(L, 6);
+	}
 
 	Quad *quad = instance()->newQuad(v, sw, sh);
 	luax_pushtype(L, quad);
@@ -641,19 +680,31 @@ int w_newCanvas(lua_State *L)
 	luax_checkgraphicscreated(L);
 
 	// check if width and height are given. else default to screen dimensions.
-	int width       = (int) luaL_optnumber(L, 1, instance()->getWidth());
-	int height      = (int) luaL_optnumber(L, 2, instance()->getHeight());
-	const char *str = luaL_optstring(L, 3, "normal");
-	int msaa        = (int) luaL_optnumber(L, 4, 0);
+	int width  = (int) luaL_optnumber(L, 1, instance()->getWidth());
+	int height = (int) luaL_optnumber(L, 2, instance()->getHeight());
 
-	PixelFormat format;
-	if (!getConstant(str, format))
-		return luaL_error(L, "Invalid pixel format: %s", str);
+	Canvas::Settings settings;
+
+	// Default to the screen's current pixel density scale.
+	settings.pixeldensity = instance()->getScreenPixelDensity();
+
+	if (!lua_isnoneornil(L, 3))
+	{
+		lua_getfield(L, 3, "format");
+		if (!lua_isnoneornil(L, -1))
+		{
+			const char *str = luaL_checkstring(L, -1);
+			if (!getConstant(str, settings.format))
+				return luaL_error(L, "Invalid Canvas format: %s", str);
+		}
+		lua_pop(L, 1);
+
+		settings.pixeldensity = (float) luax_numberflag(L, 3, "pixeldensity", settings.pixeldensity);
+		settings.msaa = luax_intflag(L, 3, "msaa", settings.msaa);
+	}
 
 	Canvas *canvas = nullptr;
-	luax_catchexcept(L,
-		[&](){ canvas = instance()->newCanvas(width, height, format, msaa); }
-	);
+	luax_catchexcept(L, [&](){ canvas = instance()->newCanvas(width, height, settings); });
 
 	if (canvas == nullptr)
 		return luaL_error(L, "Canvas not created, but no error thrown. I don't even...");
@@ -1018,9 +1069,11 @@ int w_newVideo(lua_State *L)
 		luax_convobj(L, 1, "video", "newVideoStream");
 
 	auto stream = luax_checktype<love::video::VideoStream>(L, 1);
+	float pixeldensity = (float) luaL_optnumber(L, 2, 1.0);
 	Video *video = nullptr;
 
-	luax_catchexcept(L, [&]() { video = instance()->newVideo(stream); });
+	luax_catchexcept(L, [&]() { video = instance()->newVideo(stream, pixeldensity); });
+
 	luax_pushtype(L, video);
 	video->release();
 	return 1;
@@ -2107,6 +2160,9 @@ static const luaL_Reg functions[] =
 	{ "getWidth", w_getWidth },
 	{ "getHeight", w_getHeight },
 	{ "getDimensions", w_getDimensions },
+	{ "getPixelWidth", w_getPixelWidth },
+	{ "getPixelHeight", w_getPixelHeight },
+	{ "getPixelDimensions", w_getPixelDimensions },
 
 	{ "setScissor", w_setScissor },
 	{ "intersectScissor", w_intersectScissor },

+ 8 - 4
src/modules/graphics/opengl/wrap_Graphics.lua

@@ -341,16 +341,20 @@ end
 
 love.graphics._setDefaultShaderCode(defaults, defaults_gammacorrect)
 
-function love.graphics.newVideo(file, loadaudio)
-	local video = love.graphics._newVideo(file)
+
+function love.graphics.newVideo(file, settings)
+	settings = settings == nil and {} or settings
+	if type(settings) ~= "table" then error("bad argument #2 to newVideo (expected table)", 2) end
+
+	local video = love.graphics._newVideo(file, settings.pixeldensity)
 	local source, success
 
-	if loadaudio ~= false and love.audio then
+	if settings.audio ~= false and love.audio then
 		success, source = pcall(love.audio.newSource, video:getStream():getFilename(), "stream")
 	end
 	if success then
 		video:setSource(source)
-	elseif loadaudio == true then
+	elseif settings.audio == true then
 		if love.audio then
 			error("Video had no audio track", 2)
 		else

+ 10 - 7
src/modules/graphics/opengl/wrap_Image.cpp

@@ -114,25 +114,28 @@ int w_Image_getData(lua_State *L)
 	return n;
 }
 
-static const char *imageFlagName(Image::FlagType flagtype)
+const char *luax_imageSettingName(Image::SettingType settingtype)
 {
 	const char *name = nullptr;
-	Image::getConstant(flagtype, name);
+	Image::getConstant(settingtype, name);
 	return name;
 }
 
 int w_Image_getFlags(lua_State *L)
 {
 	Image *i = luax_checkimage(L, 1);
-	Image::Flags flags = i->getFlags();
+	Image::Settings settings = i->getFlags();
 
 	lua_createtable(L, 0, 2);
 
-	lua_pushboolean(L, flags.mipmaps);
-	lua_setfield(L, -2, imageFlagName(Image::FLAG_TYPE_MIPMAPS));
+	lua_pushboolean(L, settings.mipmaps);
+	lua_setfield(L, -2, luax_imageSettingName(Image::SETTING_MIPMAPS));
 
-	lua_pushboolean(L, flags.linear);
-	lua_setfield(L, -2, imageFlagName(Image::FLAG_TYPE_LINEAR));
+	lua_pushboolean(L, settings.linear);
+	lua_setfield(L, -2, luax_imageSettingName(Image::SETTING_LINEAR));
+
+	lua_pushnumber(L, settings.pixeldensity);
+	lua_setfield(L, -2, luax_imageSettingName(Image::SETTING_PIXELDENSITY));
 
 	return 1;
 }

+ 1 - 0
src/modules/graphics/opengl/wrap_Image.h

@@ -33,6 +33,7 @@ namespace graphics
 namespace opengl
 {
 
+const char *luax_imageSettingName(Image::SettingType settingtype);
 Image *luax_checkimage(lua_State *L, int idx);
 extern "C" int luaopen_image(lua_State *L);
 

+ 25 - 0
src/modules/graphics/opengl/wrap_Video.cpp

@@ -90,6 +90,28 @@ int w_Video_getDimensions(lua_State *L)
 	return 2;
 }
 
+int w_Video_getPixelWidth(lua_State *L)
+{
+	Video *video = luax_checkvideo(L, 1);
+	lua_pushnumber(L, video->getPixelWidth());
+	return 1;
+}
+
+int w_Video_getPixelHeight(lua_State *L)
+{
+	Video *video = luax_checkvideo(L, 1);
+	lua_pushnumber(L, video->getPixelHeight());
+	return 1;
+}
+
+int w_Video_getPixelDimensions(lua_State *L)
+{
+	Video *video = luax_checkvideo(L, 1);
+	lua_pushnumber(L, video->getPixelWidth());
+	lua_pushnumber(L, video->getPixelHeight());
+	return 2;
+}
+
 int w_Video_setFilter(lua_State *L)
 {
 	Video *video = luax_checkvideo(L, 1);
@@ -136,6 +158,9 @@ static const luaL_Reg functions[] =
 	{ "getWidth", w_Video_getWidth },
 	{ "getHeight", w_Video_getHeight },
 	{ "getDimensions", w_Video_getDimensions },
+	{ "getPixelWidth", w_Video_getPixelWidth },
+	{ "getPixelHeight", w_Video_getPixelHeight },
+	{ "getPixelDimensions", w_Video_getPixelDimensions },
 	{ "setFilter", w_Video_setFilter },
 	{ "getFilter", w_Video_getFilter },
 	{ 0, 0 }

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

@@ -52,6 +52,35 @@ int w_Texture_getDimensions(lua_State *L)
 	return 2;
 }
 
+int w_Texture_getPixelWidth(lua_State *L)
+{
+	Texture *t = luax_checktexture(L, 1);
+	lua_pushnumber(L, t->getPixelWidth());
+	return 1;
+}
+
+int w_Texture_getPixelHeight(lua_State *L)
+{
+	Texture *t = luax_checktexture(L, 1);
+	lua_pushnumber(L, t->getPixelHeight());
+	return 1;
+}
+
+int w_Texture_getPixelDimensions(lua_State *L)
+{
+	Texture *t = luax_checktexture(L, 1);
+	lua_pushnumber(L, t->getPixelWidth());
+	lua_pushnumber(L, t->getPixelHeight());
+	return 2;
+}
+
+int w_Texture_getPixelDensity(lua_State *L)
+{
+	Texture *t = luax_checktexture(L, 1);
+	lua_pushnumber(L, t->getPixelDensity());
+	return 1;
+}
+
 int w_Texture_setFilter(lua_State *L)
 {
 	Texture *t = luax_checktexture(L, 1);
@@ -130,6 +159,10 @@ const luaL_Reg w_Texture_functions[] =
 	{ "getWidth", w_Texture_getWidth },
 	{ "getHeight", w_Texture_getHeight },
 	{ "getDimensions", w_Texture_getDimensions },
+	{ "getPixelWidth", w_Texture_getPixelWidth },
+	{ "getPixelHeight", w_Texture_getPixelHeight },
+	{ "getPixelDimensions", w_Texture_getPixelDimensions },
+	{ "getPixelDensity", w_Texture_getPixelDensity },
 	{ "setFilter", w_Texture_setFilter },
 	{ "getFilter", w_Texture_getFilter },
 	{ "setWrap", w_Texture_setWrap },

+ 2 - 2
src/modules/keyboard/sdl/Keyboard.cpp

@@ -124,8 +124,8 @@ void Keyboard::setTextInput(bool enable, double x, double y, double w, double h)
 	auto window = Module::getInstance<window::Window>(M_WINDOW);
 	if (window)
 	{
-		window->pixelToWindowCoords(&x, &y);
-		window->pixelToWindowCoords(&w, &h);
+		window->DPIToWindowCoords(&x, &y);
+		window->DPIToWindowCoords(&w, &h);
 	}
 
 	SDL_Rect rect = {(int) x, (int) y, (int) w, (int) h};

+ 8 - 8
src/modules/mouse/sdl/Mouse.cpp

@@ -34,19 +34,19 @@ namespace sdl
 
 // SDL reports mouse coordinates in the window coordinate system in OS X, but
 // we want them in pixel coordinates (may be different with high-DPI enabled.)
-static void windowToPixelCoords(double *x, double *y)
+static void windowToDPICoords(double *x, double *y)
 {
 	auto window = Module::getInstance<window::Window>(Module::M_WINDOW);
 	if (window)
-		window->windowToPixelCoords(x, y);
+		window->windowToDPICoords(x, y);
 }
 
 // And vice versa for setting mouse coordinates.
-static void pixelToWindowCoords(double *x, double *y)
+static void DPIToWindowCoords(double *x, double *y)
 {
 	auto window = Module::getInstance<window::Window>(Module::M_WINDOW);
 	if (window)
-		window->pixelToWindowCoords(x, y);
+		window->DPIToWindowCoords(x, y);
 }
 
 const char *Mouse::getName() const
@@ -118,7 +118,7 @@ double Mouse::getX() const
 	SDL_GetMouseState(&x, nullptr);
 
 	double dx = (double) x;
-	windowToPixelCoords(&dx, nullptr);
+	windowToDPICoords(&dx, nullptr);
 
 	return dx;
 }
@@ -129,7 +129,7 @@ double Mouse::getY() const
 	SDL_GetMouseState(nullptr, &y);
 
 	double dy = (double) y;
-	windowToPixelCoords(nullptr, &dy);
+	windowToDPICoords(nullptr, &dy);
 
 	return dy;
 }
@@ -141,7 +141,7 @@ void Mouse::getPosition(double &x, double &y) const
 
 	x = (double) mx;
 	y = (double) my;
-	windowToPixelCoords(&x, &y);
+	windowToDPICoords(&x, &y);
 }
 
 void Mouse::setPosition(double x, double y)
@@ -152,7 +152,7 @@ void Mouse::setPosition(double x, double y)
 	if (window)
 		handle = (SDL_Window *) window->getHandle();
 
-	pixelToWindowCoords(&x, &y);
+	DPIToWindowCoords(&x, &y);
 	SDL_WarpMouseInWindow(handle, (int) x, (int) y);
 
 	// SDL_WarpMouse doesn't directly update SDL's internal mouse state in Linux

+ 9 - 2
src/modules/window/Window.h

@@ -164,13 +164,20 @@ public:
 	virtual void setMouseGrab(bool grab) = 0;
 	virtual bool isMouseGrabbed() const = 0;
 
-	virtual void getPixelDimensions(int &w, int &h) const = 0;
+	virtual int getWidth() const = 0;
+	virtual int getHeight() const = 0;
+	virtual int getPixelWidth() const = 0;
+	virtual int getPixelHeight() const = 0;
+
 	// Note: window-space coordinates are not necessarily the same as
 	// density-independent units (which toPixels and fromPixels use.)
 	virtual void windowToPixelCoords(double *x, double *y) const = 0;
 	virtual void pixelToWindowCoords(double *x, double *y) const = 0;
 
-	virtual double getPixelScale() const = 0;
+	virtual void windowToDPICoords(double *x, double *y) const = 0;
+	virtual void DPIToWindowCoords(double *x, double *y) const = 0;
+
+	virtual double getPixelDensity() const = 0;
 
 	virtual double toPixels(double x) const = 0;
 	virtual void toPixels(double wx, double wy, double &px, double &py) const = 0;

+ 73 - 11
src/modules/window/sdl/Window.cpp

@@ -521,7 +521,11 @@ bool Window::setWindow(int width, int height, WindowSettings *settings)
 	updateSettings(f, false);
 
 	if (graphics.get())
-		graphics->setMode(pixelWidth, pixelHeight);
+	{
+		double scaledw, scaledh;
+		fromPixels((double) pixelWidth, (double) pixelHeight, scaledw, scaledh);
+		graphics->setMode((int) scaledw, (int) scaledh, pixelWidth, pixelHeight);
+	}
 
 #ifdef LOVE_ANDROID
 	love::android::setImmersive(f.fullscreen);
@@ -541,7 +545,11 @@ bool Window::onSizeChanged(int width, int height)
 	SDL_GL_GetDrawableSize(window, &pixelWidth, &pixelHeight);
 
 	if (graphics.get())
-		graphics->setViewportSize(pixelWidth, pixelHeight);
+	{
+		double scaledw, scaledh;
+		fromPixels((double) pixelWidth, (double) pixelHeight, scaledw, scaledh);
+		graphics->setViewportSize((int) scaledw, (int) scaledh, pixelWidth, pixelHeight);
+	}
 
 	return true;
 }
@@ -609,7 +617,11 @@ void Window::updateSettings(const WindowSettings &newsettings, bool updateGraphi
 
 	// Update the viewport size now instead of waiting for event polling.
 	if (updateGraphicsViewport && graphics.get())
-		graphics->setViewportSize(pixelWidth, pixelHeight);
+	{
+		double scaledw, scaledh;
+		fromPixels((double) pixelWidth, (double) pixelHeight, scaledw, scaledh);
+		graphics->setViewportSize((int) scaledw, (int) scaledh, pixelWidth, pixelHeight);
+	}
 }
 
 void Window::getWindow(int &width, int &height, WindowSettings &newsettings)
@@ -940,12 +952,27 @@ bool Window::isMouseGrabbed() const
 		return mouseGrabbed;
 }
 
-void Window::getPixelDimensions(int &w, int &h) const
+int Window::getWidth() const
+{
+	return windowWidth;
+}
+
+int Window::getHeight() const
 {
-	w = pixelWidth;
-	h = pixelHeight;
+	return windowHeight;
 }
 
+int Window::getPixelWidth() const
+{
+	return pixelWidth;
+}
+
+int Window::getPixelHeight() const
+{
+	return pixelHeight;
+}
+
+
 void Window::windowToPixelCoords(double *x, double *y) const
 {
 	if (x != nullptr)
@@ -962,7 +989,42 @@ void Window::pixelToWindowCoords(double *x, double *y) const
 		*y = (*y) * ((double) windowHeight / (double) pixelHeight);
 }
 
-double Window::getPixelScale() const
+void Window::windowToDPICoords(double *x, double *y) const
+{
+	double px = x != nullptr ? *x : 0.0;
+	double py = y != nullptr ? *y : 0.0;
+
+	windowToPixelCoords(&px, &py);
+
+	double dpix = 0.0;
+	double dpiy = 0.0;
+
+	fromPixels(px, py, dpix, dpiy);
+
+	if (x != nullptr)
+		*x = dpix;
+	if (y != nullptr)
+		*y = dpiy;
+}
+
+void Window::DPIToWindowCoords(double *x, double *y) const
+{
+	double dpix = x != nullptr ? *x : 0.0;
+	double dpiy = y != nullptr ? *y : 0.0;
+
+	double px = 0.0;
+	double py = 0.0;
+
+	toPixels(dpix, dpiy, px, py);
+	pixelToWindowCoords(&px, &py);
+
+	if (x != nullptr)
+		*x = px;
+	if (y != nullptr)
+		*y = py;
+}
+
+double Window::getPixelDensity() const
 {
 #ifdef LOVE_ANDROID
 	return love::android::getScreenScale();
@@ -973,24 +1035,24 @@ double Window::getPixelScale() const
 
 double Window::toPixels(double x) const
 {
-	return x * getPixelScale();
+	return x * getPixelDensity();
 }
 
 void Window::toPixels(double wx, double wy, double &px, double &py) const
 {
-	double scale = getPixelScale();
+	double scale = getPixelDensity();
 	px = wx * scale;
 	py = wy * scale;
 }
 
 double Window::fromPixels(double x) const
 {
-	return x / getPixelScale();
+	return x / getPixelDensity();
 }
 
 void Window::fromPixels(double px, double py, double &wx, double &wy) const
 {
-	double scale = getPixelScale();
+	double scale = getPixelDensity();
 	wx = px / scale;
 	wy = py / scale;
 }

+ 9 - 2
src/modules/window/sdl/Window.h

@@ -90,11 +90,18 @@ public:
 	void setMouseGrab(bool grab);
 	bool isMouseGrabbed() const;
 
-	void getPixelDimensions(int &w, int &h) const;
+	int getWidth() const;
+	int getHeight() const;
+	int getPixelWidth() const;
+	int getPixelHeight() const;
+
 	void windowToPixelCoords(double *x, double *y) const;
 	void pixelToWindowCoords(double *x, double *y) const;
 
-	double getPixelScale() const;
+	void windowToDPICoords(double *x, double *y) const;
+	void DPIToWindowCoords(double *x, double *y) const;
+
+	double getPixelDensity() const;
 
 	double toPixels(double x) const;
 	void toPixels(double wx, double wy, double &px, double &py) const;

+ 3 - 3
src/modules/window/wrap_Window.cpp

@@ -408,9 +408,9 @@ int w_isVisible(lua_State *L)
 	return 1;
 }
 
-int w_getPixelScale(lua_State *L)
+int w_getPixelDensity(lua_State *L)
 {
-	lua_pushnumber(L, instance()->getPixelScale());
+	lua_pushnumber(L, instance()->getPixelDensity());
 	return 1;
 }
 
@@ -570,7 +570,7 @@ static const luaL_Reg functions[] =
 	{ "hasFocus", w_hasFocus },
 	{ "hasMouseFocus", w_hasMouseFocus },
 	{ "isVisible", w_isVisible },
-	{ "getPixelScale", w_getPixelScale },
+	{ "getPixelDensity", w_getPixelDensity },
 	{ "toPixels", w_toPixels },
 	{ "fromPixels", w_fromPixels },
 	{ "minimize", w_minimize },

+ 2 - 3
src/scripts/boot.lua

@@ -603,8 +603,7 @@ function love.errhand(msg)
 	if love.audio then love.audio.stop() end
 
 	love.graphics.reset()
-
-	local font = love.graphics.setNewFont(math.floor(love.window.toPixels(14)))
+	local font = love.graphics.setNewFont(14)
 
 	love.graphics.setColor(1, 1, 1, 1)
 
@@ -630,8 +629,8 @@ function love.errhand(msg)
 	p = string.gsub(p, "%[string \"(.-)\"%]", "%1")
 
 	local function draw()
+		local pos = 70
 		love.graphics.clear(89/255, 157/255, 220/255)
-		local pos = love.window.toPixels(70)
 		love.graphics.printf(p, pos, pos, love.graphics.getWidth() - pos)
 		love.graphics.present()
 	end

+ 2 - 6
src/scripts/boot.lua.h

@@ -1103,9 +1103,7 @@ const unsigned char boot_lua[] =
 	0x65, 0x74, 0x28, 0x29, 0x0a,
 	0x09, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x20, 0x66, 0x6f, 0x6e, 0x74, 0x20, 0x3d, 0x20, 0x6c, 0x6f, 0x76, 0x65, 
 	0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x69, 0x63, 0x73, 0x2e, 0x73, 0x65, 0x74, 0x4e, 0x65, 0x77, 0x46, 0x6f, 
-	0x6e, 0x74, 0x28, 0x6d, 0x61, 0x74, 0x68, 0x2e, 0x66, 0x6c, 0x6f, 0x6f, 0x72, 0x28, 0x6c, 0x6f, 0x76, 0x65, 
-	0x2e, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x2e, 0x74, 0x6f, 0x50, 0x69, 0x78, 0x65, 0x6c, 0x73, 0x28, 0x31, 
-	0x34, 0x29, 0x29, 0x29, 0x0a,
+	0x6e, 0x74, 0x28, 0x31, 0x34, 0x29, 0x0a,
 	0x09, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x69, 0x63, 0x73, 0x2e, 0x73, 0x65, 0x74, 
 	0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x28, 0x31, 0x2c, 0x20, 0x31, 0x2c, 0x20, 0x31, 0x2c, 0x20, 0x31, 0x29, 0x0a,
 	0x09, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x20, 0x74, 0x72, 0x61, 0x63, 0x65, 0x20, 0x3d, 0x20, 0x64, 0x65, 0x62, 
@@ -1140,12 +1138,10 @@ const unsigned char boot_lua[] =
 	0x5c, 0x22, 0x25, 0x5d, 0x22, 0x2c, 0x20, 0x22, 0x25, 0x31, 0x22, 0x29, 0x0a,
 	0x09, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x20, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x64, 0x72, 
 	0x61, 0x77, 0x28, 0x29, 0x0a,
+	0x09, 0x09, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x20, 0x70, 0x6f, 0x73, 0x20, 0x3d, 0x20, 0x37, 0x30, 0x0a,
 	0x09, 0x09, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x69, 0x63, 0x73, 0x2e, 0x63, 0x6c, 
 	0x65, 0x61, 0x72, 0x28, 0x38, 0x39, 0x2f, 0x32, 0x35, 0x35, 0x2c, 0x20, 0x31, 0x35, 0x37, 0x2f, 0x32, 0x35, 
 	0x35, 0x2c, 0x20, 0x32, 0x32, 0x30, 0x2f, 0x32, 0x35, 0x35, 0x29, 0x0a,
-	0x09, 0x09, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x20, 0x70, 0x6f, 0x73, 0x20, 0x3d, 0x20, 0x6c, 0x6f, 0x76, 0x65, 
-	0x2e, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x2e, 0x74, 0x6f, 0x50, 0x69, 0x78, 0x65, 0x6c, 0x73, 0x28, 0x37, 
-	0x30, 0x29, 0x0a,
 	0x09, 0x09, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x69, 0x63, 0x73, 0x2e, 0x70, 0x72, 
 	0x69, 0x6e, 0x74, 0x66, 0x28, 0x70, 0x2c, 0x20, 0x70, 0x6f, 0x73, 0x2c, 0x20, 0x70, 0x6f, 0x73, 0x2c, 0x20, 
 	0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x69, 0x63, 0x73, 0x2e, 0x67, 0x65, 0x74, 0x57, 

+ 30 - 55
src/scripts/nogame.lua

@@ -633,7 +633,7 @@ function love.nogame()
 	end
 
 	function Toast:center()
-		local ww, wh = love.window.fromPixels(love.graphics.getDimensions())
+		local ww, wh = love.graphics.getDimensions()
 		self.x = math.floor(ww / 2 / 32) * 32 + 16
 		self.y = math.floor(wh / 2 / 32) * 32 + 16
 	end
@@ -727,10 +727,9 @@ function love.nogame()
 	function Mosaic:init()
 		local mosaic_image = g_images.mosaic[1]
 
-		local sw, sh = mosaic_image:getDimensions()
-		local ww, wh = love.window.fromPixels(love.graphics.getDimensions())
+		local ww, wh = love.graphics.getDimensions()
 
-		if love.window.getPixelScale() > 1 then
+		if love.window.getPixelDensity() > 1 then
 			mosaic_image = g_images.mosaic[2]
 		end
 
@@ -758,20 +757,11 @@ function love.nogame()
 		-- Insert only once. This way it appears half as often.
 		table.insert(COLORS, { 220/255, 239/255, 113/255 }) -- LIME
 
-		-- When using the higher-res mosaic sprite sheet, we want to draw its
-		-- sprites at the same scale as the regular-resolution one, because
-		-- we'll globally love.graphics.scale *everything* by the screen's
-		-- pixel density ratio.
-		-- We can avoid a lot of Quad scaling by taking advantage of the fact
-		-- that Quads use normalized texture coordinates internally - if we use 
-		-- the 'source image size' and quad size of the @1x image for the Quads
-		-- even when rendering them using the @2x image, it will automatically
-		-- scale as expected.
 		local QUADS = {
-			love.graphics.newQuad(0,  0,  32, 32, sw, sh),
-			love.graphics.newQuad(0,  32, 32, 32, sw, sh),
-			love.graphics.newQuad(32, 32, 32, 32, sw, sh),
-			love.graphics.newQuad(32, 0,  32, 32, sw, sh),
+			love.graphics.newQuad(0,  0,  32, 32, mosaic_image),
+			love.graphics.newQuad(0,  32, 32, 32, mosaic_image),
+			love.graphics.newQuad(32, 32, 32, 32, mosaic_image),
+			love.graphics.newQuad(32, 0,  32, 32, mosaic_image),
 		}
 
 		local exclude_left = math.floor(ww / 2 / 32)
@@ -844,19 +834,19 @@ function love.nogame()
 		end
 
 		local GLYPHS = {
-			N = love.graphics.newQuad(0,  64, 32, 32, sw, sh),
-			O = love.graphics.newQuad(32, 64, 32, 32, sw, sh),
-			G = love.graphics.newQuad(0,  96, 32, 32, sw, sh),
-			A = love.graphics.newQuad(32, 96, 32, 32, sw, sh),
-			M = love.graphics.newQuad(64, 96, 32, 32, sw, sh),
-			E = love.graphics.newQuad(96, 96, 32, 32, sw, sh),
-
-			U = love.graphics.newQuad(64, 0,  32, 32, sw, sh),
-			P = love.graphics.newQuad(96, 0,  32, 32, sw, sh),
-			o = love.graphics.newQuad(64, 32, 32, 32, sw, sh),
-			S = love.graphics.newQuad(96, 32, 32, 32, sw, sh),
-			R = love.graphics.newQuad(64, 64, 32, 32, sw, sh),
-			T = love.graphics.newQuad(96, 64, 32, 32, sw, sh),
+			N = love.graphics.newQuad(0,  64, 32, 32, mosaic_image),
+			O = love.graphics.newQuad(32, 64, 32, 32, mosaic_image),
+			G = love.graphics.newQuad(0,  96, 32, 32, mosaic_image),
+			A = love.graphics.newQuad(32, 96, 32, 32, mosaic_image),
+			M = love.graphics.newQuad(64, 96, 32, 32, mosaic_image),
+			E = love.graphics.newQuad(96, 96, 32, 32, mosaic_image),
+
+			U = love.graphics.newQuad(64, 0,  32, 32, mosaic_image),
+			P = love.graphics.newQuad(96, 0,  32, 32, mosaic_image),
+			o = love.graphics.newQuad(64, 32, 32, 32, mosaic_image),
+			S = love.graphics.newQuad(96, 32, 32, 32, mosaic_image),
+			R = love.graphics.newQuad(64, 64, 32, 32, mosaic_image),
+			T = love.graphics.newQuad(96, 64, 32, 32, mosaic_image),
 		}
 
 		local INITIAL_TEXT_COLOR = { 15/16, 15/16, 15/16 }
@@ -932,9 +922,9 @@ function love.nogame()
 	function love.load()
 		love.graphics.setBackgroundColor(136/255, 193/255, 206/255)
 
-		local function load_image(file, name)
+		local function load_image(file, name, flags)
 			local decoded = love.math.decode("base64", file)
-			return love.graphics.newImage(love.filesystem.newFileData(decoded, name:gsub("_", ".")))
+			return love.graphics.newImage(love.filesystem.newFileData(decoded, name:gsub("_", ".")), flags)
 		end
 
 		g_images = {}
@@ -963,11 +953,8 @@ function love.nogame()
 
 	function love.draw()
 		love.graphics.setColor(1, 1, 1)
-		love.graphics.push()
-		love.graphics.scale(love.window.getPixelScale())
 		g_entities.mosaic:draw()
 		g_entities.toast:draw()
-		love.graphics.pop()
 	end
 
 	function love.resize(w, h)
@@ -988,10 +975,17 @@ function love.nogame()
 		end
 	end
 
-	function love.mousepressed(x, y, b)
+	function love.mousepressed(x, y, b, istouch, clicks)
 		local tx = x / love.graphics.getWidth()
 		local ty = y / love.graphics.getHeight()
 		g_entities.toast:look_at(tx, ty)
+
+		-- Double-tap the screen (when using a touch screen) to exit.
+		if istouch and clicks == 2 then
+			if love.window.showMessageBox("Exit No-Game Screen", "", {"OK", "Cancel"}) == 1 then
+				love.event.quit()
+			end
+		end
 	end
 
 	function love.mousemoved(x, y)
@@ -1002,25 +996,6 @@ function love.nogame()
 		end
 	end
 
-	local last_touch = {time=0, x=0, y=0}
-
-	function love.touchpressed(id, x, y, pressure)
-		-- Double-tap the screen (when using a touch screen) to exit.
-		if #love.touch.getTouches() == 1 then
-			local dist = math.sqrt((x-last_touch.x)^2 + (y-last_touch.y)^2)
-			local difftime = love.timer.getTime() - last_touch.time
-			if difftime < 0.3 and dist < 50 then
-				if love.window.showMessageBox("Exit No-Game Screen", "", {"OK", "Cancel"}) == 1 then
-					love.event.quit()
-				end
-			end
-
-			last_touch.time = love.timer.getTime()
-			last_touch.x = x
-			last_touch.y = y
-		end
-	end
-
 	function love.conf(t)
 		t.title = "L\195\150VE " .. love._version .. " (" .. love._version_codename .. ")"
 		t.gammacorrect = true

+ 57 - 113
src/scripts/nogame.lua.h

@@ -2961,9 +2961,8 @@ const unsigned char nogame_lua[] =
 	0x09, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x54, 0x6f, 0x61, 0x73, 0x74, 0x3a, 0x63, 0x65, 
 	0x6e, 0x74, 0x65, 0x72, 0x28, 0x29, 0x0a,
 	0x09, 0x09, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x20, 0x77, 0x77, 0x2c, 0x20, 0x77, 0x68, 0x20, 0x3d, 0x20, 0x6c, 
-	0x6f, 0x76, 0x65, 0x2e, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x2e, 0x66, 0x72, 0x6f, 0x6d, 0x50, 0x69, 0x78, 
-	0x65, 0x6c, 0x73, 0x28, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x69, 0x63, 0x73, 0x2e, 
-	0x67, 0x65, 0x74, 0x44, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x28, 0x29, 0x29, 0x0a,
+	0x6f, 0x76, 0x65, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x69, 0x63, 0x73, 0x2e, 0x67, 0x65, 0x74, 0x44, 0x69, 
+	0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x28, 0x29, 0x0a,
 	0x09, 0x09, 0x73, 0x65, 0x6c, 0x66, 0x2e, 0x78, 0x20, 0x3d, 0x20, 0x6d, 0x61, 0x74, 0x68, 0x2e, 0x66, 0x6c, 
 	0x6f, 0x6f, 0x72, 0x28, 0x77, 0x77, 0x20, 0x2f, 0x20, 0x32, 0x20, 0x2f, 0x20, 0x33, 0x32, 0x29, 0x20, 0x2a, 
 	0x20, 0x33, 0x32, 0x20, 0x2b, 0x20, 0x31, 0x36, 0x0a,
@@ -3127,16 +3126,12 @@ const unsigned char nogame_lua[] =
 	0x09, 0x09, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x20, 0x6d, 0x6f, 0x73, 0x61, 0x69, 0x63, 0x5f, 0x69, 0x6d, 0x61, 
 	0x67, 0x65, 0x20, 0x3d, 0x20, 0x67, 0x5f, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x6d, 0x6f, 0x73, 0x61, 
 	0x69, 0x63, 0x5b, 0x31, 0x5d, 0x0a,
-	0x09, 0x09, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x20, 0x73, 0x77, 0x2c, 0x20, 0x73, 0x68, 0x20, 0x3d, 0x20, 0x6d, 
-	0x6f, 0x73, 0x61, 0x69, 0x63, 0x5f, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x3a, 0x67, 0x65, 0x74, 0x44, 0x69, 0x6d, 
-	0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x28, 0x29, 0x0a,
 	0x09, 0x09, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x20, 0x77, 0x77, 0x2c, 0x20, 0x77, 0x68, 0x20, 0x3d, 0x20, 0x6c, 
-	0x6f, 0x76, 0x65, 0x2e, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x2e, 0x66, 0x72, 0x6f, 0x6d, 0x50, 0x69, 0x78, 
-	0x65, 0x6c, 0x73, 0x28, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x69, 0x63, 0x73, 0x2e, 
-	0x67, 0x65, 0x74, 0x44, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x28, 0x29, 0x29, 0x0a,
+	0x6f, 0x76, 0x65, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x69, 0x63, 0x73, 0x2e, 0x67, 0x65, 0x74, 0x44, 0x69, 
+	0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x28, 0x29, 0x0a,
 	0x09, 0x09, 0x69, 0x66, 0x20, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x2e, 0x67, 
-	0x65, 0x74, 0x50, 0x69, 0x78, 0x65, 0x6c, 0x53, 0x63, 0x61, 0x6c, 0x65, 0x28, 0x29, 0x20, 0x3e, 0x20, 0x31, 
-	0x20, 0x74, 0x68, 0x65, 0x6e, 0x0a,
+	0x65, 0x74, 0x50, 0x69, 0x78, 0x65, 0x6c, 0x44, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x79, 0x28, 0x29, 0x20, 0x3e, 
+	0x20, 0x31, 0x20, 0x74, 0x68, 0x65, 0x6e, 0x0a,
 	0x09, 0x09, 0x09, 0x6d, 0x6f, 0x73, 0x61, 0x69, 0x63, 0x5f, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x20, 0x3d, 0x20, 
 	0x67, 0x5f, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x6d, 0x6f, 0x73, 0x61, 0x69, 0x63, 0x5b, 0x32, 0x5d, 0x0a,
 	0x09, 0x09, 0x65, 0x6e, 0x64, 0x0a,
@@ -3184,53 +3179,23 @@ const unsigned char nogame_lua[] =
 	0x4f, 0x52, 0x53, 0x2c, 0x20, 0x7b, 0x20, 0x32, 0x32, 0x30, 0x2f, 0x32, 0x35, 0x35, 0x2c, 0x20, 0x32, 0x33, 
 	0x39, 0x2f, 0x32, 0x35, 0x35, 0x2c, 0x20, 0x31, 0x31, 0x33, 0x2f, 0x32, 0x35, 0x35, 0x20, 0x7d, 0x29, 0x20, 
 	0x2d, 0x2d, 0x20, 0x4c, 0x49, 0x4d, 0x45, 0x0a,
-	0x09, 0x09, 0x2d, 0x2d, 0x20, 0x57, 0x68, 0x65, 0x6e, 0x20, 0x75, 0x73, 0x69, 0x6e, 0x67, 0x20, 0x74, 0x68, 
-	0x65, 0x20, 0x68, 0x69, 0x67, 0x68, 0x65, 0x72, 0x2d, 0x72, 0x65, 0x73, 0x20, 0x6d, 0x6f, 0x73, 0x61, 0x69, 
-	0x63, 0x20, 0x73, 0x70, 0x72, 0x69, 0x74, 0x65, 0x20, 0x73, 0x68, 0x65, 0x65, 0x74, 0x2c, 0x20, 0x77, 0x65, 
-	0x20, 0x77, 0x61, 0x6e, 0x74, 0x20, 0x74, 0x6f, 0x20, 0x64, 0x72, 0x61, 0x77, 0x20, 0x69, 0x74, 0x73, 0x0a,
-	0x09, 0x09, 0x2d, 0x2d, 0x20, 0x73, 0x70, 0x72, 0x69, 0x74, 0x65, 0x73, 0x20, 0x61, 0x74, 0x20, 0x74, 0x68, 
-	0x65, 0x20, 0x73, 0x61, 0x6d, 0x65, 0x20, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x20, 0x61, 0x73, 0x20, 0x74, 0x68, 
-	0x65, 0x20, 0x72, 0x65, 0x67, 0x75, 0x6c, 0x61, 0x72, 0x2d, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 
-	0x6f, 0x6e, 0x20, 0x6f, 0x6e, 0x65, 0x2c, 0x20, 0x62, 0x65, 0x63, 0x61, 0x75, 0x73, 0x65, 0x0a,
-	0x09, 0x09, 0x2d, 0x2d, 0x20, 0x77, 0x65, 0x27, 0x6c, 0x6c, 0x20, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x6c, 
-	0x79, 0x20, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x69, 0x63, 0x73, 0x2e, 0x73, 0x63, 
-	0x61, 0x6c, 0x65, 0x20, 0x2a, 0x65, 0x76, 0x65, 0x72, 0x79, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x2a, 0x20, 0x62, 
-	0x79, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x63, 0x72, 0x65, 0x65, 0x6e, 0x27, 0x73, 0x0a,
-	0x09, 0x09, 0x2d, 0x2d, 0x20, 0x70, 0x69, 0x78, 0x65, 0x6c, 0x20, 0x64, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x79, 
-	0x20, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x2e, 0x0a,
-	0x09, 0x09, 0x2d, 0x2d, 0x20, 0x57, 0x65, 0x20, 0x63, 0x61, 0x6e, 0x20, 0x61, 0x76, 0x6f, 0x69, 0x64, 0x20, 
-	0x61, 0x20, 0x6c, 0x6f, 0x74, 0x20, 0x6f, 0x66, 0x20, 0x51, 0x75, 0x61, 0x64, 0x20, 0x73, 0x63, 0x61, 0x6c, 
-	0x69, 0x6e, 0x67, 0x20, 0x62, 0x79, 0x20, 0x74, 0x61, 0x6b, 0x69, 0x6e, 0x67, 0x20, 0x61, 0x64, 0x76, 0x61, 
-	0x6e, 0x74, 0x61, 0x67, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x66, 0x61, 0x63, 0x74, 0x0a,
-	0x09, 0x09, 0x2d, 0x2d, 0x20, 0x74, 0x68, 0x61, 0x74, 0x20, 0x51, 0x75, 0x61, 0x64, 0x73, 0x20, 0x75, 0x73, 
-	0x65, 0x20, 0x6e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x20, 0x74, 0x65, 0x78, 0x74, 0x75, 
-	0x72, 0x65, 0x20, 0x63, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x73, 0x20, 0x69, 0x6e, 0x74, 
-	0x65, 0x72, 0x6e, 0x61, 0x6c, 0x6c, 0x79, 0x20, 0x2d, 0x20, 0x69, 0x66, 0x20, 0x77, 0x65, 0x20, 0x75, 0x73, 
-	0x65, 0x20, 0x0a,
-	0x09, 0x09, 0x2d, 0x2d, 0x20, 0x74, 0x68, 0x65, 0x20, 0x27, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x20, 0x69, 
-	0x6d, 0x61, 0x67, 0x65, 0x20, 0x73, 0x69, 0x7a, 0x65, 0x27, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x71, 0x75, 0x61, 
-	0x64, 0x20, 0x73, 0x69, 0x7a, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x40, 0x31, 0x78, 0x20, 
-	0x69, 0x6d, 0x61, 0x67, 0x65, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x74, 0x68, 0x65, 0x20, 0x51, 0x75, 0x61, 0x64, 
-	0x73, 0x0a,
-	0x09, 0x09, 0x2d, 0x2d, 0x20, 0x65, 0x76, 0x65, 0x6e, 0x20, 0x77, 0x68, 0x65, 0x6e, 0x20, 0x72, 0x65, 0x6e, 
-	0x64, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x20, 0x74, 0x68, 0x65, 0x6d, 0x20, 0x75, 0x73, 0x69, 0x6e, 0x67, 0x20, 
-	0x74, 0x68, 0x65, 0x20, 0x40, 0x32, 0x78, 0x20, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x2c, 0x20, 0x69, 0x74, 0x20, 
-	0x77, 0x69, 0x6c, 0x6c, 0x20, 0x61, 0x75, 0x74, 0x6f, 0x6d, 0x61, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x6c, 0x79, 0x0a,
-	0x09, 0x09, 0x2d, 0x2d, 0x20, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x20, 0x61, 0x73, 0x20, 0x65, 0x78, 0x70, 0x65, 
-	0x63, 0x74, 0x65, 0x64, 0x2e, 0x0a,
 	0x09, 0x09, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x20, 0x51, 0x55, 0x41, 0x44, 0x53, 0x20, 0x3d, 0x20, 0x7b, 0x0a,
 	0x09, 0x09, 0x09, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x69, 0x63, 0x73, 0x2e, 0x6e, 
 	0x65, 0x77, 0x51, 0x75, 0x61, 0x64, 0x28, 0x30, 0x2c, 0x20, 0x20, 0x30, 0x2c, 0x20, 0x20, 0x33, 0x32, 0x2c, 
-	0x20, 0x33, 0x32, 0x2c, 0x20, 0x73, 0x77, 0x2c, 0x20, 0x73, 0x68, 0x29, 0x2c, 0x0a,
+	0x20, 0x33, 0x32, 0x2c, 0x20, 0x6d, 0x6f, 0x73, 0x61, 0x69, 0x63, 0x5f, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x29, 
+	0x2c, 0x0a,
 	0x09, 0x09, 0x09, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x69, 0x63, 0x73, 0x2e, 0x6e, 
 	0x65, 0x77, 0x51, 0x75, 0x61, 0x64, 0x28, 0x30, 0x2c, 0x20, 0x20, 0x33, 0x32, 0x2c, 0x20, 0x33, 0x32, 0x2c, 
-	0x20, 0x33, 0x32, 0x2c, 0x20, 0x73, 0x77, 0x2c, 0x20, 0x73, 0x68, 0x29, 0x2c, 0x0a,
+	0x20, 0x33, 0x32, 0x2c, 0x20, 0x6d, 0x6f, 0x73, 0x61, 0x69, 0x63, 0x5f, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x29, 
+	0x2c, 0x0a,
 	0x09, 0x09, 0x09, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x69, 0x63, 0x73, 0x2e, 0x6e, 
 	0x65, 0x77, 0x51, 0x75, 0x61, 0x64, 0x28, 0x33, 0x32, 0x2c, 0x20, 0x33, 0x32, 0x2c, 0x20, 0x33, 0x32, 0x2c, 
-	0x20, 0x33, 0x32, 0x2c, 0x20, 0x73, 0x77, 0x2c, 0x20, 0x73, 0x68, 0x29, 0x2c, 0x0a,
+	0x20, 0x33, 0x32, 0x2c, 0x20, 0x6d, 0x6f, 0x73, 0x61, 0x69, 0x63, 0x5f, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x29, 
+	0x2c, 0x0a,
 	0x09, 0x09, 0x09, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x69, 0x63, 0x73, 0x2e, 0x6e, 
 	0x65, 0x77, 0x51, 0x75, 0x61, 0x64, 0x28, 0x33, 0x32, 0x2c, 0x20, 0x30, 0x2c, 0x20, 0x20, 0x33, 0x32, 0x2c, 
-	0x20, 0x33, 0x32, 0x2c, 0x20, 0x73, 0x77, 0x2c, 0x20, 0x73, 0x68, 0x29, 0x2c, 0x0a,
+	0x20, 0x33, 0x32, 0x2c, 0x20, 0x6d, 0x6f, 0x73, 0x61, 0x69, 0x63, 0x5f, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x29, 
+	0x2c, 0x0a,
 	0x09, 0x09, 0x7d, 0x0a,
 	0x09, 0x09, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x20, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x6c, 0x65, 
 	0x66, 0x74, 0x20, 0x3d, 0x20, 0x6d, 0x61, 0x74, 0x68, 0x2e, 0x66, 0x6c, 0x6f, 0x6f, 0x72, 0x28, 0x77, 0x77, 
@@ -3383,40 +3348,52 @@ const unsigned char nogame_lua[] =
 	0x09, 0x09, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x20, 0x47, 0x4c, 0x59, 0x50, 0x48, 0x53, 0x20, 0x3d, 0x20, 0x7b, 0x0a,
 	0x09, 0x09, 0x09, 0x4e, 0x20, 0x3d, 0x20, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x69, 
 	0x63, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x51, 0x75, 0x61, 0x64, 0x28, 0x30, 0x2c, 0x20, 0x20, 0x36, 0x34, 0x2c, 
-	0x20, 0x33, 0x32, 0x2c, 0x20, 0x33, 0x32, 0x2c, 0x20, 0x73, 0x77, 0x2c, 0x20, 0x73, 0x68, 0x29, 0x2c, 0x0a,
+	0x20, 0x33, 0x32, 0x2c, 0x20, 0x33, 0x32, 0x2c, 0x20, 0x6d, 0x6f, 0x73, 0x61, 0x69, 0x63, 0x5f, 0x69, 0x6d, 
+	0x61, 0x67, 0x65, 0x29, 0x2c, 0x0a,
 	0x09, 0x09, 0x09, 0x4f, 0x20, 0x3d, 0x20, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x69, 
 	0x63, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x51, 0x75, 0x61, 0x64, 0x28, 0x33, 0x32, 0x2c, 0x20, 0x36, 0x34, 0x2c, 
-	0x20, 0x33, 0x32, 0x2c, 0x20, 0x33, 0x32, 0x2c, 0x20, 0x73, 0x77, 0x2c, 0x20, 0x73, 0x68, 0x29, 0x2c, 0x0a,
+	0x20, 0x33, 0x32, 0x2c, 0x20, 0x33, 0x32, 0x2c, 0x20, 0x6d, 0x6f, 0x73, 0x61, 0x69, 0x63, 0x5f, 0x69, 0x6d, 
+	0x61, 0x67, 0x65, 0x29, 0x2c, 0x0a,
 	0x09, 0x09, 0x09, 0x47, 0x20, 0x3d, 0x20, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x69, 
 	0x63, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x51, 0x75, 0x61, 0x64, 0x28, 0x30, 0x2c, 0x20, 0x20, 0x39, 0x36, 0x2c, 
-	0x20, 0x33, 0x32, 0x2c, 0x20, 0x33, 0x32, 0x2c, 0x20, 0x73, 0x77, 0x2c, 0x20, 0x73, 0x68, 0x29, 0x2c, 0x0a,
+	0x20, 0x33, 0x32, 0x2c, 0x20, 0x33, 0x32, 0x2c, 0x20, 0x6d, 0x6f, 0x73, 0x61, 0x69, 0x63, 0x5f, 0x69, 0x6d, 
+	0x61, 0x67, 0x65, 0x29, 0x2c, 0x0a,
 	0x09, 0x09, 0x09, 0x41, 0x20, 0x3d, 0x20, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x69, 
 	0x63, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x51, 0x75, 0x61, 0x64, 0x28, 0x33, 0x32, 0x2c, 0x20, 0x39, 0x36, 0x2c, 
-	0x20, 0x33, 0x32, 0x2c, 0x20, 0x33, 0x32, 0x2c, 0x20, 0x73, 0x77, 0x2c, 0x20, 0x73, 0x68, 0x29, 0x2c, 0x0a,
+	0x20, 0x33, 0x32, 0x2c, 0x20, 0x33, 0x32, 0x2c, 0x20, 0x6d, 0x6f, 0x73, 0x61, 0x69, 0x63, 0x5f, 0x69, 0x6d, 
+	0x61, 0x67, 0x65, 0x29, 0x2c, 0x0a,
 	0x09, 0x09, 0x09, 0x4d, 0x20, 0x3d, 0x20, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x69, 
 	0x63, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x51, 0x75, 0x61, 0x64, 0x28, 0x36, 0x34, 0x2c, 0x20, 0x39, 0x36, 0x2c, 
-	0x20, 0x33, 0x32, 0x2c, 0x20, 0x33, 0x32, 0x2c, 0x20, 0x73, 0x77, 0x2c, 0x20, 0x73, 0x68, 0x29, 0x2c, 0x0a,
+	0x20, 0x33, 0x32, 0x2c, 0x20, 0x33, 0x32, 0x2c, 0x20, 0x6d, 0x6f, 0x73, 0x61, 0x69, 0x63, 0x5f, 0x69, 0x6d, 
+	0x61, 0x67, 0x65, 0x29, 0x2c, 0x0a,
 	0x09, 0x09, 0x09, 0x45, 0x20, 0x3d, 0x20, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x69, 
 	0x63, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x51, 0x75, 0x61, 0x64, 0x28, 0x39, 0x36, 0x2c, 0x20, 0x39, 0x36, 0x2c, 
-	0x20, 0x33, 0x32, 0x2c, 0x20, 0x33, 0x32, 0x2c, 0x20, 0x73, 0x77, 0x2c, 0x20, 0x73, 0x68, 0x29, 0x2c, 0x0a,
+	0x20, 0x33, 0x32, 0x2c, 0x20, 0x33, 0x32, 0x2c, 0x20, 0x6d, 0x6f, 0x73, 0x61, 0x69, 0x63, 0x5f, 0x69, 0x6d, 
+	0x61, 0x67, 0x65, 0x29, 0x2c, 0x0a,
 	0x09, 0x09, 0x09, 0x55, 0x20, 0x3d, 0x20, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x69, 
 	0x63, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x51, 0x75, 0x61, 0x64, 0x28, 0x36, 0x34, 0x2c, 0x20, 0x30, 0x2c, 0x20, 
-	0x20, 0x33, 0x32, 0x2c, 0x20, 0x33, 0x32, 0x2c, 0x20, 0x73, 0x77, 0x2c, 0x20, 0x73, 0x68, 0x29, 0x2c, 0x0a,
+	0x20, 0x33, 0x32, 0x2c, 0x20, 0x33, 0x32, 0x2c, 0x20, 0x6d, 0x6f, 0x73, 0x61, 0x69, 0x63, 0x5f, 0x69, 0x6d, 
+	0x61, 0x67, 0x65, 0x29, 0x2c, 0x0a,
 	0x09, 0x09, 0x09, 0x50, 0x20, 0x3d, 0x20, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x69, 
 	0x63, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x51, 0x75, 0x61, 0x64, 0x28, 0x39, 0x36, 0x2c, 0x20, 0x30, 0x2c, 0x20, 
-	0x20, 0x33, 0x32, 0x2c, 0x20, 0x33, 0x32, 0x2c, 0x20, 0x73, 0x77, 0x2c, 0x20, 0x73, 0x68, 0x29, 0x2c, 0x0a,
+	0x20, 0x33, 0x32, 0x2c, 0x20, 0x33, 0x32, 0x2c, 0x20, 0x6d, 0x6f, 0x73, 0x61, 0x69, 0x63, 0x5f, 0x69, 0x6d, 
+	0x61, 0x67, 0x65, 0x29, 0x2c, 0x0a,
 	0x09, 0x09, 0x09, 0x6f, 0x20, 0x3d, 0x20, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x69, 
 	0x63, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x51, 0x75, 0x61, 0x64, 0x28, 0x36, 0x34, 0x2c, 0x20, 0x33, 0x32, 0x2c, 
-	0x20, 0x33, 0x32, 0x2c, 0x20, 0x33, 0x32, 0x2c, 0x20, 0x73, 0x77, 0x2c, 0x20, 0x73, 0x68, 0x29, 0x2c, 0x0a,
+	0x20, 0x33, 0x32, 0x2c, 0x20, 0x33, 0x32, 0x2c, 0x20, 0x6d, 0x6f, 0x73, 0x61, 0x69, 0x63, 0x5f, 0x69, 0x6d, 
+	0x61, 0x67, 0x65, 0x29, 0x2c, 0x0a,
 	0x09, 0x09, 0x09, 0x53, 0x20, 0x3d, 0x20, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x69, 
 	0x63, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x51, 0x75, 0x61, 0x64, 0x28, 0x39, 0x36, 0x2c, 0x20, 0x33, 0x32, 0x2c, 
-	0x20, 0x33, 0x32, 0x2c, 0x20, 0x33, 0x32, 0x2c, 0x20, 0x73, 0x77, 0x2c, 0x20, 0x73, 0x68, 0x29, 0x2c, 0x0a,
+	0x20, 0x33, 0x32, 0x2c, 0x20, 0x33, 0x32, 0x2c, 0x20, 0x6d, 0x6f, 0x73, 0x61, 0x69, 0x63, 0x5f, 0x69, 0x6d, 
+	0x61, 0x67, 0x65, 0x29, 0x2c, 0x0a,
 	0x09, 0x09, 0x09, 0x52, 0x20, 0x3d, 0x20, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x69, 
 	0x63, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x51, 0x75, 0x61, 0x64, 0x28, 0x36, 0x34, 0x2c, 0x20, 0x36, 0x34, 0x2c, 
-	0x20, 0x33, 0x32, 0x2c, 0x20, 0x33, 0x32, 0x2c, 0x20, 0x73, 0x77, 0x2c, 0x20, 0x73, 0x68, 0x29, 0x2c, 0x0a,
+	0x20, 0x33, 0x32, 0x2c, 0x20, 0x33, 0x32, 0x2c, 0x20, 0x6d, 0x6f, 0x73, 0x61, 0x69, 0x63, 0x5f, 0x69, 0x6d, 
+	0x61, 0x67, 0x65, 0x29, 0x2c, 0x0a,
 	0x09, 0x09, 0x09, 0x54, 0x20, 0x3d, 0x20, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x69, 
 	0x63, 0x73, 0x2e, 0x6e, 0x65, 0x77, 0x51, 0x75, 0x61, 0x64, 0x28, 0x39, 0x36, 0x2c, 0x20, 0x36, 0x34, 0x2c, 
-	0x20, 0x33, 0x32, 0x2c, 0x20, 0x33, 0x32, 0x2c, 0x20, 0x73, 0x77, 0x2c, 0x20, 0x73, 0x68, 0x29, 0x2c, 0x0a,
+	0x20, 0x33, 0x32, 0x2c, 0x20, 0x33, 0x32, 0x2c, 0x20, 0x6d, 0x6f, 0x73, 0x61, 0x69, 0x63, 0x5f, 0x69, 0x6d, 
+	0x61, 0x67, 0x65, 0x29, 0x2c, 0x0a,
 	0x09, 0x09, 0x7d, 0x0a,
 	0x09, 0x09, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x20, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, 0x4c, 0x5f, 0x54, 0x45, 
 	0x58, 0x54, 0x5f, 0x43, 0x4f, 0x4c, 0x4f, 0x52, 0x20, 0x3d, 0x20, 0x7b, 0x20, 0x31, 0x35, 0x2f, 0x31, 0x36, 
@@ -3561,7 +3538,7 @@ const unsigned char nogame_lua[] =
 	0x30, 0x36, 0x2f, 0x32, 0x35, 0x35, 0x29, 0x0a,
 	0x09, 0x09, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x20, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x6c, 
 	0x6f, 0x61, 0x64, 0x5f, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x28, 0x66, 0x69, 0x6c, 0x65, 0x2c, 0x20, 0x6e, 0x61, 
-	0x6d, 0x65, 0x29, 0x0a,
+	0x6d, 0x65, 0x2c, 0x20, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x29, 0x0a,
 	0x09, 0x09, 0x09, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x20, 0x64, 0x65, 0x63, 0x6f, 0x64, 0x65, 0x64, 0x20, 0x3d, 
 	0x20, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x6d, 0x61, 0x74, 0x68, 0x2e, 0x64, 0x65, 0x63, 0x6f, 0x64, 0x65, 0x28, 
 	0x22, 0x62, 0x61, 0x73, 0x65, 0x36, 0x34, 0x22, 0x2c, 0x20, 0x66, 0x69, 0x6c, 0x65, 0x29, 0x0a,
@@ -3570,7 +3547,7 @@ const unsigned char nogame_lua[] =
 	0x65, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x2e, 0x6e, 0x65, 0x77, 0x46, 0x69, 
 	0x6c, 0x65, 0x44, 0x61, 0x74, 0x61, 0x28, 0x64, 0x65, 0x63, 0x6f, 0x64, 0x65, 0x64, 0x2c, 0x20, 0x6e, 0x61, 
 	0x6d, 0x65, 0x3a, 0x67, 0x73, 0x75, 0x62, 0x28, 0x22, 0x5f, 0x22, 0x2c, 0x20, 0x22, 0x2e, 0x22, 0x29, 0x29, 
-	0x29, 0x0a,
+	0x2c, 0x20, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x29, 0x0a,
 	0x09, 0x09, 0x65, 0x6e, 0x64, 0x0a,
 	0x09, 0x09, 0x67, 0x5f, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x73, 0x20, 0x3d, 0x20, 0x7b, 0x7d, 0x0a,
 	0x09, 0x09, 0x67, 0x5f, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x74, 0x6f, 0x61, 0x73, 0x74, 0x20, 0x3d, 
@@ -3628,17 +3605,10 @@ const unsigned char nogame_lua[] =
 	0x77, 0x28, 0x29, 0x0a,
 	0x09, 0x09, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x69, 0x63, 0x73, 0x2e, 0x73, 0x65, 
 	0x74, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x28, 0x31, 0x2c, 0x20, 0x31, 0x2c, 0x20, 0x31, 0x29, 0x0a,
-	0x09, 0x09, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x69, 0x63, 0x73, 0x2e, 0x70, 0x75, 
-	0x73, 0x68, 0x28, 0x29, 0x0a,
-	0x09, 0x09, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x69, 0x63, 0x73, 0x2e, 0x73, 0x63, 
-	0x61, 0x6c, 0x65, 0x28, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x2e, 0x67, 0x65, 
-	0x74, 0x50, 0x69, 0x78, 0x65, 0x6c, 0x53, 0x63, 0x61, 0x6c, 0x65, 0x28, 0x29, 0x29, 0x0a,
 	0x09, 0x09, 0x67, 0x5f, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x2e, 0x6d, 0x6f, 0x73, 0x61, 0x69, 
 	0x63, 0x3a, 0x64, 0x72, 0x61, 0x77, 0x28, 0x29, 0x0a,
 	0x09, 0x09, 0x67, 0x5f, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x2e, 0x74, 0x6f, 0x61, 0x73, 0x74, 
 	0x3a, 0x64, 0x72, 0x61, 0x77, 0x28, 0x29, 0x0a,
-	0x09, 0x09, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x69, 0x63, 0x73, 0x2e, 0x70, 0x6f, 
-	0x70, 0x28, 0x29, 0x0a,
 	0x09, 0x65, 0x6e, 0x64, 0x0a,
 	0x09, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x72, 0x65, 0x73, 
 	0x69, 0x7a, 0x65, 0x28, 0x77, 0x2c, 0x20, 0x68, 0x29, 0x0a,
@@ -3668,7 +3638,8 @@ const unsigned char nogame_lua[] =
 	0x09, 0x09, 0x65, 0x6e, 0x64, 0x0a,
 	0x09, 0x65, 0x6e, 0x64, 0x0a,
 	0x09, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x6d, 0x6f, 0x75, 
-	0x73, 0x65, 0x70, 0x72, 0x65, 0x73, 0x73, 0x65, 0x64, 0x28, 0x78, 0x2c, 0x20, 0x79, 0x2c, 0x20, 0x62, 0x29, 0x0a,
+	0x73, 0x65, 0x70, 0x72, 0x65, 0x73, 0x73, 0x65, 0x64, 0x28, 0x78, 0x2c, 0x20, 0x79, 0x2c, 0x20, 0x62, 0x2c, 
+	0x20, 0x69, 0x73, 0x74, 0x6f, 0x75, 0x63, 0x68, 0x2c, 0x20, 0x63, 0x6c, 0x69, 0x63, 0x6b, 0x73, 0x29, 0x0a,
 	0x09, 0x09, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x20, 0x74, 0x78, 0x20, 0x3d, 0x20, 0x78, 0x20, 0x2f, 0x20, 0x6c, 
 	0x6f, 0x76, 0x65, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x69, 0x63, 0x73, 0x2e, 0x67, 0x65, 0x74, 0x57, 0x69, 
 	0x64, 0x74, 0x68, 0x28, 0x29, 0x0a,
@@ -3677,6 +3648,21 @@ const unsigned char nogame_lua[] =
 	0x69, 0x67, 0x68, 0x74, 0x28, 0x29, 0x0a,
 	0x09, 0x09, 0x67, 0x5f, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x2e, 0x74, 0x6f, 0x61, 0x73, 0x74, 
 	0x3a, 0x6c, 0x6f, 0x6f, 0x6b, 0x5f, 0x61, 0x74, 0x28, 0x74, 0x78, 0x2c, 0x20, 0x74, 0x79, 0x29, 0x0a,
+	0x09, 0x09, 0x2d, 0x2d, 0x20, 0x44, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x2d, 0x74, 0x61, 0x70, 0x20, 0x74, 0x68, 
+	0x65, 0x20, 0x73, 0x63, 0x72, 0x65, 0x65, 0x6e, 0x20, 0x28, 0x77, 0x68, 0x65, 0x6e, 0x20, 0x75, 0x73, 0x69, 
+	0x6e, 0x67, 0x20, 0x61, 0x20, 0x74, 0x6f, 0x75, 0x63, 0x68, 0x20, 0x73, 0x63, 0x72, 0x65, 0x65, 0x6e, 0x29, 
+	0x20, 0x74, 0x6f, 0x20, 0x65, 0x78, 0x69, 0x74, 0x2e, 0x0a,
+	0x09, 0x09, 0x69, 0x66, 0x20, 0x69, 0x73, 0x74, 0x6f, 0x75, 0x63, 0x68, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x63, 
+	0x6c, 0x69, 0x63, 0x6b, 0x73, 0x20, 0x3d, 0x3d, 0x20, 0x32, 0x20, 0x74, 0x68, 0x65, 0x6e, 0x0a,
+	0x09, 0x09, 0x09, 0x69, 0x66, 0x20, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x2e, 
+	0x73, 0x68, 0x6f, 0x77, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x42, 0x6f, 0x78, 0x28, 0x22, 0x45, 0x78, 
+	0x69, 0x74, 0x20, 0x4e, 0x6f, 0x2d, 0x47, 0x61, 0x6d, 0x65, 0x20, 0x53, 0x63, 0x72, 0x65, 0x65, 0x6e, 0x22, 
+	0x2c, 0x20, 0x22, 0x22, 0x2c, 0x20, 0x7b, 0x22, 0x4f, 0x4b, 0x22, 0x2c, 0x20, 0x22, 0x43, 0x61, 0x6e, 0x63, 
+	0x65, 0x6c, 0x22, 0x7d, 0x29, 0x20, 0x3d, 0x3d, 0x20, 0x31, 0x20, 0x74, 0x68, 0x65, 0x6e, 0x0a,
+	0x09, 0x09, 0x09, 0x09, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x71, 0x75, 0x69, 
+	0x74, 0x28, 0x29, 0x0a,
+	0x09, 0x09, 0x09, 0x65, 0x6e, 0x64, 0x0a,
+	0x09, 0x09, 0x65, 0x6e, 0x64, 0x0a,
 	0x09, 0x65, 0x6e, 0x64, 0x0a,
 	0x09, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x6d, 0x6f, 0x75, 
 	0x73, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x64, 0x28, 0x78, 0x2c, 0x20, 0x79, 0x29, 0x0a,
@@ -3692,48 +3678,6 @@ const unsigned char nogame_lua[] =
 	0x74, 0x3a, 0x6c, 0x6f, 0x6f, 0x6b, 0x5f, 0x61, 0x74, 0x28, 0x74, 0x78, 0x2c, 0x20, 0x74, 0x79, 0x29, 0x0a,
 	0x09, 0x09, 0x65, 0x6e, 0x64, 0x0a,
 	0x09, 0x65, 0x6e, 0x64, 0x0a,
-	0x09, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x20, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x74, 0x6f, 0x75, 0x63, 0x68, 0x20, 
-	0x3d, 0x20, 0x7b, 0x74, 0x69, 0x6d, 0x65, 0x3d, 0x30, 0x2c, 0x20, 0x78, 0x3d, 0x30, 0x2c, 0x20, 0x79, 0x3d, 
-	0x30, 0x7d, 0x0a,
-	0x09, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x74, 0x6f, 0x75, 
-	0x63, 0x68, 0x70, 0x72, 0x65, 0x73, 0x73, 0x65, 0x64, 0x28, 0x69, 0x64, 0x2c, 0x20, 0x78, 0x2c, 0x20, 0x79, 
-	0x2c, 0x20, 0x70, 0x72, 0x65, 0x73, 0x73, 0x75, 0x72, 0x65, 0x29, 0x0a,
-	0x09, 0x09, 0x2d, 0x2d, 0x20, 0x44, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x2d, 0x74, 0x61, 0x70, 0x20, 0x74, 0x68, 
-	0x65, 0x20, 0x73, 0x63, 0x72, 0x65, 0x65, 0x6e, 0x20, 0x28, 0x77, 0x68, 0x65, 0x6e, 0x20, 0x75, 0x73, 0x69, 
-	0x6e, 0x67, 0x20, 0x61, 0x20, 0x74, 0x6f, 0x75, 0x63, 0x68, 0x20, 0x73, 0x63, 0x72, 0x65, 0x65, 0x6e, 0x29, 
-	0x20, 0x74, 0x6f, 0x20, 0x65, 0x78, 0x69, 0x74, 0x2e, 0x0a,
-	0x09, 0x09, 0x69, 0x66, 0x20, 0x23, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x74, 0x6f, 0x75, 0x63, 0x68, 0x2e, 0x67, 
-	0x65, 0x74, 0x54, 0x6f, 0x75, 0x63, 0x68, 0x65, 0x73, 0x28, 0x29, 0x20, 0x3d, 0x3d, 0x20, 0x31, 0x20, 0x74, 
-	0x68, 0x65, 0x6e, 0x0a,
-	0x09, 0x09, 0x09, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x20, 0x64, 0x69, 0x73, 0x74, 0x20, 0x3d, 0x20, 0x6d, 0x61, 
-	0x74, 0x68, 0x2e, 0x73, 0x71, 0x72, 0x74, 0x28, 0x28, 0x78, 0x2d, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x74, 0x6f, 
-	0x75, 0x63, 0x68, 0x2e, 0x78, 0x29, 0x5e, 0x32, 0x20, 0x2b, 0x20, 0x28, 0x79, 0x2d, 0x6c, 0x61, 0x73, 0x74, 
-	0x5f, 0x74, 0x6f, 0x75, 0x63, 0x68, 0x2e, 0x79, 0x29, 0x5e, 0x32, 0x29, 0x0a,
-	0x09, 0x09, 0x09, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x20, 0x64, 0x69, 0x66, 0x66, 0x74, 0x69, 0x6d, 0x65, 0x20, 
-	0x3d, 0x20, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x72, 0x2e, 0x67, 0x65, 0x74, 0x54, 0x69, 
-	0x6d, 0x65, 0x28, 0x29, 0x20, 0x2d, 0x20, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x74, 0x6f, 0x75, 0x63, 0x68, 0x2e, 
-	0x74, 0x69, 0x6d, 0x65, 0x0a,
-	0x09, 0x09, 0x09, 0x69, 0x66, 0x20, 0x64, 0x69, 0x66, 0x66, 0x74, 0x69, 0x6d, 0x65, 0x20, 0x3c, 0x20, 0x30, 
-	0x2e, 0x33, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x64, 0x69, 0x73, 0x74, 0x20, 0x3c, 0x20, 0x35, 0x30, 0x20, 0x74, 
-	0x68, 0x65, 0x6e, 0x0a,
-	0x09, 0x09, 0x09, 0x09, 0x69, 0x66, 0x20, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, 
-	0x2e, 0x73, 0x68, 0x6f, 0x77, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x42, 0x6f, 0x78, 0x28, 0x22, 0x45, 
-	0x78, 0x69, 0x74, 0x20, 0x4e, 0x6f, 0x2d, 0x47, 0x61, 0x6d, 0x65, 0x20, 0x53, 0x63, 0x72, 0x65, 0x65, 0x6e, 
-	0x22, 0x2c, 0x20, 0x22, 0x22, 0x2c, 0x20, 0x7b, 0x22, 0x4f, 0x4b, 0x22, 0x2c, 0x20, 0x22, 0x43, 0x61, 0x6e, 
-	0x63, 0x65, 0x6c, 0x22, 0x7d, 0x29, 0x20, 0x3d, 0x3d, 0x20, 0x31, 0x20, 0x74, 0x68, 0x65, 0x6e, 0x0a,
-	0x09, 0x09, 0x09, 0x09, 0x09, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x71, 0x75, 
-	0x69, 0x74, 0x28, 0x29, 0x0a,
-	0x09, 0x09, 0x09, 0x09, 0x65, 0x6e, 0x64, 0x0a,
-	0x09, 0x09, 0x09, 0x65, 0x6e, 0x64, 0x0a,
-	0x09, 0x09, 0x09, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x74, 0x6f, 0x75, 0x63, 0x68, 0x2e, 0x74, 0x69, 0x6d, 0x65, 
-	0x20, 0x3d, 0x20, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x72, 0x2e, 0x67, 0x65, 0x74, 0x54, 
-	0x69, 0x6d, 0x65, 0x28, 0x29, 0x0a,
-	0x09, 0x09, 0x09, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x74, 0x6f, 0x75, 0x63, 0x68, 0x2e, 0x78, 0x20, 0x3d, 0x20, 
-	0x78, 0x0a,
-	0x09, 0x09, 0x09, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x74, 0x6f, 0x75, 0x63, 0x68, 0x2e, 0x79, 0x20, 0x3d, 0x20, 
-	0x79, 0x0a,
-	0x09, 0x09, 0x65, 0x6e, 0x64, 0x0a,
-	0x09, 0x65, 0x6e, 0x64, 0x0a,
 	0x09, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x63, 0x6f, 0x6e, 
 	0x66, 0x28, 0x74, 0x29, 0x0a,
 	0x09, 0x09, 0x74, 0x2e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0x22, 0x4c, 0x5c, 0x31, 0x39, 0x35,