From fdaf6c3611566a9e818baed2357e02bf93e1a0e8 Mon Sep 17 00:00:00 2001
From: Jonas Kulla <Nyocurio@gmail.com>
Date: Wed, 27 Jul 2016 11:56:43 +0200
Subject: [PATCH 1/6] Bitmap: Split surface pixel address calculation into
 helper

---
 src/bitmap.cpp | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

diff --git a/src/bitmap.cpp b/src/bitmap.cpp
index 24fb70f..b2d1f3d 100644
--- a/src/bitmap.cpp
+++ b/src/bitmap.cpp
@@ -768,6 +768,14 @@ void Bitmap::clear()
 	p->onModified();
 }
 
+static uint32_t &getPixelAt(SDL_Surface *surf, SDL_PixelFormat *form, int x, int y)
+{
+	size_t offset = x*form->BytesPerPixel + y*surf->pitch;
+	uint8_t *bytes = (uint8_t*) surf->pixels + offset;
+
+	return *((uint32_t*) bytes);
+}
+
 Color Bitmap::getPixel(int x, int y) const
 {
 	guardDisposed();
@@ -790,9 +798,7 @@ Color Bitmap::getPixel(int x, int y) const
 		glState.viewport.pop();
 	}
 
-	size_t offset = x*p->format->BytesPerPixel + y*p->surface->pitch;
-	uint8_t *bytes = (uint8_t*) p->surface->pixels + offset;
-	uint32_t pixel = *((uint32_t*) bytes);
+	uint32_t pixel = getPixelAt(p->surface, p->format, x, y);
 
 	return Color((pixel >> p->format->Rshift) & 0xFF,
 	             (pixel >> p->format->Gshift) & 0xFF,

From e98c2e0535b6baf1691cff53996ecfd02121899f Mon Sep 17 00:00:00 2001
From: Jonas Kulla <Nyocurio@gmail.com>
Date: Wed, 27 Jul 2016 11:59:08 +0200
Subject: [PATCH 2/6] Bitmap: Don't throw away cached surface in setPixel()

Instead, update the surface with the same change. For many
consecutive getPixel() -> setPixel() calls on the same bitmap,
this avoids calling glReadPixels at every iteration.
---
 src/bitmap.cpp | 15 ++++++++++++---
 1 file changed, 12 insertions(+), 3 deletions(-)

diff --git a/src/bitmap.cpp b/src/bitmap.cpp
index b2d1f3d..a677207 100644
--- a/src/bitmap.cpp
+++ b/src/bitmap.cpp
@@ -221,9 +221,9 @@ struct BitmapPrivate
 		surf = surfConv;
 	}
 
-	void onModified()
+	void onModified(bool freeSurface = true)
 	{
-		if (surface)
+		if (surface && freeSurface)
 		{
 			SDL_FreeSurface(surface);
 			surface = 0;
@@ -825,7 +825,16 @@ void Bitmap::setPixel(int x, int y, const Color &color)
 
 	p->addTaintedArea(IntRect(x, y, 1, 1));
 
-	p->onModified();
+	/* Setting just a single pixel is no reason to throw away the
+	 * whole cached surface; we can just apply the same change */
+
+	if (p->surface)
+	{
+		uint32_t &surfPixel = getPixelAt(p->surface, p->format, x, y);
+		surfPixel = SDL_MapRGBA(p->format, pixel[0], pixel[1], pixel[2], pixel[3]);
+	}
+
+	p->onModified(false);
 }
 
 void Bitmap::hueChange(int hue)

From d4e09f55bd67a3165a9c1a8fd49d7c8a1e71cf1f Mon Sep 17 00:00:00 2001
From: Jonas Kulla <Nyocurio@gmail.com>
Date: Wed, 27 Jul 2016 12:03:45 +0200
Subject: [PATCH 3/6] WindowVX: Fix move() not setting the correct dirty flags

---
 src/windowvx.cpp | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/src/windowvx.cpp b/src/windowvx.cpp
index 02a70d9..fe2aaef 100644
--- a/src/windowvx.cpp
+++ b/src/windowvx.cpp
@@ -852,10 +852,15 @@ void WindowVX::move(int x, int y, int width, int height)
 
 	const Vec2i size(std::max(0, width), std::max(0, height));
 
-	if (p->geo.w != size.x || p->geo.h != size.y)
+	if (p->geo.size() != size)
+	{
+		p->base.vertDirty = true;
 		p->base.texSizeDirty = true;
+		p->clipRectDirty = true;
+		p->ctrlVertDirty = true;
+	}
 
-	p->geo = IntRect(x, y, size.x, size.y);
+	p->geo = IntRect(Vec2i(x, y), size);
 	p->updateBaseQuad();
 }
 

From 0ec1fce4acc0e5cb942ae4fad7d730ee23b30488 Mon Sep 17 00:00:00 2001
From: Jonas Kulla <Nyocurio@gmail.com>
Date: Mon, 12 Sep 2016 20:16:39 +0200
Subject: [PATCH 4/6] MRI: Bind Audio.setup_midi

---
 binding-mri/audio-binding.cpp | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/binding-mri/audio-binding.cpp b/binding-mri/audio-binding.cpp
index 262f5c7..59919ad 100644
--- a/binding-mri/audio-binding.cpp
+++ b/binding-mri/audio-binding.cpp
@@ -97,6 +97,15 @@ DEF_FADE( me )
 
 DEF_PLAY_STOP( se )
 
+RB_METHOD(audioSetupMidi)
+{
+	RB_UNUSED_PARAM;
+
+	shState->audio().setupMidi();
+
+	return Qnil;
+}
+
 RB_METHOD(audioReset)
 {
 	RB_UNUSED_PARAM;
@@ -135,6 +144,8 @@ audioBindingInit()
 	{
 	BIND_POS( bgm );
 	BIND_POS( bgs );
+
+	_rb_define_module_function(module, "setup_midi", audioSetupMidi);
 	}
 
 	BIND_PLAY_STOP( se )

From 541e24f67822c20f54b3ab36c1d3a7e6fa0caa76 Mon Sep 17 00:00:00 2001
From: Jonas Kulla <Nyocurio@gmail.com>
Date: Tue, 4 Oct 2016 15:16:57 +0200
Subject: [PATCH 5/6] Bitmap: Use more accurate HSV-based hue shift algorithm

The previously YIQ-based algorithm turned out to be both slow,
and horribly inaccurate.

Another algorithm based on rotating the color value in the
RGB cube along the diagonal axis was also considered, which was
acceptable in terms of accuracy, and very fast.

In the end, I decided on a HSV-based one, because it is by far
the most accurate one, while still being a tad faster than the
YIQ solution.
Algorithm source: gamedev.stackexchange.com/a/59808/24839

A very simple GPU time benchmark when shifting a 2048^2 bitmap:

         YIQ rot   RGB rot   HSV shift
radeon   13.4 ms   2.8 ms    11.4 ms
intel    13.0 ms   6.0 ms    10.5 ms

radeon: HD 3650 mobility
intel:  N3540 integrated (Baytrail)

However hue shifting has never shown up as a bottleneck before,
so these are more academic.
---
 shader/hue.frag | 70 ++++++++++++++++++++++---------------------------
 src/bitmap.cpp  |  7 ++---
 src/shader.cpp  |  6 -----
 src/shader.h    |  3 +--
 4 files changed, 34 insertions(+), 52 deletions(-)

diff --git a/shader/hue.frag b/shader/hue.frag
index 405c91b..61143ac 100644
--- a/shader/hue.frag
+++ b/shader/hue.frag
@@ -1,48 +1,40 @@
 
-uniform sampler2D inputTexture;
-uniform float hueAdjust;
+uniform sampler2D texture;
+uniform mediump float hueAdjust;
 
 varying vec2 v_texCoord;
 
+/* Source: gamedev.stackexchange.com/a/59808/24839 */
+vec3 rgb2hsv(vec3 c)
+{
+	const vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
+	vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
+	vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
+
+	float d = q.x - min(q.w, q.y);
+
+	/* Avoid divide-by-zero situations by adding a very tiny delta.
+	 * Since we always deal with underlying 8-Bit color values, this
+	 * should never mask a real value */
+	const float eps = 1.0e-10;
+
+	return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + eps)), d / (q.x + eps), q.x);
+}
+
+vec3 hsv2rgb(vec3 c)
+{
+	const vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
+	vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
+	return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
+}
+
 void main ()
 {
-	const vec4  kRGBToYPrime = vec4 (0.299, 0.587, 0.114, 0.0);
-	const vec4  kRGBToI      = vec4 (0.596, -0.275, -0.321, 0.0);
-	const vec4  kRGBToQ      = vec4 (0.212, -0.523, 0.311, 0.0);
+	vec4 color = texture2D (texture, v_texCoord.xy);
+	vec3 hsv = rgb2hsv(color.rgb);
 
-	const vec4  kYIQToR      = vec4 (1.0, 0.956, 0.621, 0.0);
-	const vec4  kYIQToG      = vec4 (1.0, -0.272, -0.647, 0.0);
-	const vec4  kYIQToB      = vec4 (1.0, -1.107, 1.704, 0.0);
+	hsv.x += hueAdjust;
+	color.rgb = hsv2rgb(hsv);
 
-	/* Sample the input pixel */
-	vec4    color   = texture2D (inputTexture, v_texCoord.xy);
-
-	/* Convert to YIQ */
-	float   YPrime  = dot (color, kRGBToYPrime);
-	float   I       = dot (color, kRGBToI);
-	float   Q       = dot (color, kRGBToQ);
-
-	/* Calculate the hue and chroma */
-	float   hue     = atan (Q, I);
-	float   chroma  = sqrt (I * I + Q * Q);
-
-	/* Make the user's adjustments */
-	hue += hueAdjust;
-
-	/* Remember old I and color */
-	float IOriginal = I;
-	vec4 coOriginal = color;
-
-	/* Convert back to YIQ */
-	Q = chroma * sin (hue);
-	I = chroma * cos (hue);
-
-	/* Convert back to RGB */
-	vec4 yIQ = vec4 (YPrime, I, Q, 0.0);
-	color.r  = dot  (yIQ, kYIQToR);
-	color.g  = dot  (yIQ, kYIQToG);
-	color.b  = dot  (yIQ, kYIQToB);
-
-	/* Save the result */
-	gl_FragColor = (IOriginal == 0.0) ? coOriginal : color;
+	gl_FragColor = color;
 }
diff --git a/src/bitmap.cpp b/src/bitmap.cpp
index a677207..96cd968 100644
--- a/src/bitmap.cpp
+++ b/src/bitmap.cpp
@@ -854,13 +854,10 @@ void Bitmap::hueChange(int hue)
 	quad.setTexPosRect(texRect, texRect);
 	quad.setColor(Vec4(1, 1, 1, 1));
 
-	/* Calculate hue parameter */
-	hue = wrapRange(hue, 0, 359);
-	float hueAdj = -((M_PI * 2) / 360) * hue;
-
 	HueShader &shader = shState->shaders().hue;
 	shader.bind();
-	shader.setHueAdjust(hueAdj);
+	/* Shader expects normalized value */
+	shader.setHueAdjust(wrapRange(hue, 0, 359) / 360.0f);
 
 	FBO::bind(newTex.fbo);
 	p->pushSetViewport(shader);
diff --git a/src/shader.cpp b/src/shader.cpp
index 3b22483..824454d 100644
--- a/src/shader.cpp
+++ b/src/shader.cpp
@@ -551,7 +551,6 @@ HueShader::HueShader()
 	ShaderBase::init();
 
 	GET_U(hueAdjust);
-	GET_U(inputTexture);
 }
 
 void HueShader::setHueAdjust(float value)
@@ -559,11 +558,6 @@ void HueShader::setHueAdjust(float value)
 	gl.Uniform1f(u_hueAdjust, value);
 }
 
-void HueShader::setInputTexture(TEX::ID tex)
-{
-	setTexUniform(u_inputTexture, 0, tex);
-}
-
 
 SimpleMatrixShader::SimpleMatrixShader()
 {
diff --git a/src/shader.h b/src/shader.h
index 323b10a..7b4eb02 100644
--- a/src/shader.h
+++ b/src/shader.h
@@ -241,10 +241,9 @@ public:
 	HueShader();
 
 	void setHueAdjust(float value);
-	void setInputTexture(TEX::ID tex);
 
 private:
-	GLint u_hueAdjust, u_inputTexture;
+	GLint u_hueAdjust;
 };
 
 class SimpleMatrixShader : public ShaderBase

From 55cec53911f6706f6ad7ae58095644235e2259ba Mon Sep 17 00:00:00 2001
From: Jonas Kulla <Nyocurio@gmail.com>
Date: Fri, 17 Feb 2017 19:29:38 +0100
Subject: [PATCH 6/6] Sprite: Clamp src_rect to bitmap bounds

---
 src/sprite.cpp | 21 ++++++++++++++++-----
 1 file changed, 16 insertions(+), 5 deletions(-)

diff --git a/src/sprite.cpp b/src/sprite.cpp
index 760bb0b..7999546 100644
--- a/src/sprite.cpp
+++ b/src/sprite.cpp
@@ -134,12 +134,23 @@ struct SpritePrivate
 
 	void onSrcRectChange()
 	{
-		if (mirrored)
-			quad.setTexRect(srcRect->toFloatRect().hFlipped());
-		else
-			quad.setTexRect(srcRect->toFloatRect());
+		FloatRect rect = srcRect->toFloatRect();
+		Vec2i bmSize;
 
-		quad.setPosRect(IntRect(0, 0, srcRect->width, srcRect->height));
+		if (bitmap)
+			bmSize = Vec2i(bitmap->width(), bitmap->height());
+
+		if (mirrored)
+			rect = rect.hFlipped();
+
+		/* Clamp the rectangle so it doesn't reach outside
+		 * the bitmap bounds */
+		rect.w = clamp<int>(rect.w, 0, bmSize.x-rect.x);
+		rect.h = clamp<int>(rect.h, 0, bmSize.y-rect.y);
+
+		quad.setTexRect(rect);
+
+		quad.setPosRect(FloatRect(0, 0, rect.w, rect.h));
 		recomputeBushDepth();
 
 		wave.dirty = true;