diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt
index 20afb172..cdfd20f7 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt
@@ -341,50 +341,21 @@ class GLStateTracker {
 
 	val shaderVertexTexture: GLTransformableProgram
 	val shaderVertexTextureColor: GLTransformableColorableProgram
-	val shaderVertexTextureHSVColor: GLTransformableColorableProgram
 
 	init {
 		val textureF = GLShader.internalFragment("shaders/fragment/texture.glsl")
 		val textureColorF = GLShader.internalFragment("shaders/fragment/texture_color.glsl")
 		val textureV = GLShader.internalVertex("shaders/vertex/texture.glsl")
 
-		val textureFragmentHSV = GLShader.internalFragment("shaders/fragment/texture_color_per_vertex.glsl")
-		val textureVertexHSV = GLShader.internalVertex("shaders/vertex/texture_hsv.glsl")
-
 		shaderVertexTexture = GLTransformableProgram(this, textureF, textureV)
 		shaderVertexTextureColor = GLTransformableColorableProgram(this, textureColorF, textureV)
-		shaderVertexTextureHSVColor = GLTransformableColorableProgram(this, textureFragmentHSV, textureVertexHSV)
 
 		textureF.unlink()
 		textureColorF.unlink()
 		textureV.unlink()
-		textureFragmentHSV.unlink()
-		textureVertexHSV.unlink()
 	}
 
-	val fontProgram: GLTransformableColorableProgram
-
-	init {
-		val vertex = GLShader.internalVertex("shaders/vertex/font.glsl")
-		val fragment = GLShader.internalFragment("shaders/fragment/font.glsl")
-
-		fontProgram = GLTransformableColorableProgram(this, vertex, fragment)
-
-		vertex.unlink()
-		fragment.unlink()
-	}
-
-	val flatProgram: GLTransformableColorableProgram
-
-	init {
-		val vertex = GLShader.internalVertex("shaders/vertex/flat_vertex_2d.glsl")
-		val fragment = GLShader.internalFragment("shaders/fragment/flat_color.glsl")
-
-		flatProgram = GLTransformableColorableProgram(this, vertex, fragment)
-
-		vertex.unlink()
-		fragment.unlink()
-	}
+	val programs = GLPrograms(this)
 
 	val flat2DLines = object : GLStreamBuilderList {
 		override val small by lazy {
@@ -434,9 +405,9 @@ class GLStateTracker {
 		lambda.invoke(builder)
 		builder.upload()
 
-		flatProgram.use()
-		flatProgram.color.set(color)
-		flatProgram.transform.set(matrixStack.last)
+		programs.flat.use()
+		programs.flat.color.set(color)
+		programs.flat.transform.set(matrixStack.last)
 
 		builder.draw(GL_LINES)
 	}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/Programs.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/Programs.kt
new file mode 100644
index 00000000..a9f4de23
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/Programs.kt
@@ -0,0 +1,41 @@
+package ru.dbotthepony.kstarbound.client.gl
+
+import ru.dbotthepony.kstarbound.client.gl.shader.GLShader
+import ru.dbotthepony.kstarbound.client.gl.shader.GLShaderProgram
+import ru.dbotthepony.kstarbound.client.gl.shader.GLTransformableColorableProgram
+import kotlin.properties.ReadOnlyProperty
+import kotlin.reflect.KProperty
+
+private class SimpleProgram<T : GLShaderProgram>(private val name: String, private val factory: (GLStateTracker, Array<out GLShader>) -> T) : ReadOnlyProperty<GLPrograms, T> {
+	private var value: T? = null
+
+	override fun getValue(thisRef: GLPrograms, property: KProperty<*>): T {
+		val value = value
+
+		if (value != null) {
+			return value
+		}
+
+		val vertex = GLShader.internalVertex("shaders/$name.vsh")
+		val fragment = GLShader.internalFragment("shaders/$name.fsh")
+
+		val newValue = factory.invoke(thisRef.state, arrayOf(vertex, fragment))
+
+		this.value = newValue
+
+		vertex.unlink()
+		fragment.unlink()
+
+		if (!newValue.linked) {
+			newValue.link()
+		}
+
+		return newValue
+	}
+}
+
+class GLPrograms(val state: GLStateTracker) {
+	val tile by SimpleProgram("tile", ::GLTransformableColorableProgram)
+	val font by SimpleProgram("font", ::GLTransformableColorableProgram)
+	val flat by SimpleProgram("flat", ::GLTransformableColorableProgram)
+}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/shader/GLShaderProgram.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/shader/GLShaderProgram.kt
index ec8a4e8d..bf915490 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/shader/GLShaderProgram.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/shader/GLShaderProgram.kt
@@ -61,6 +61,7 @@ open class GLShaderProgram(val state: GLStateTracker, vararg shaders: GLShader)
 
 	fun link() {
 		check(!linked) { "Program is already linked!" }
+		state.ensureSameThread()
 		glLinkProgram(pointer)
 
 		val success = intArrayOf(0)
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/shader/GLTransformableColorableProgram.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/shader/GLTransformableColorableProgram.kt
index e0765a43..16725abe 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/shader/GLTransformableColorableProgram.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/shader/GLTransformableColorableProgram.kt
@@ -9,4 +9,4 @@ open class GLTransformableColorableProgram(state: GLStateTracker, vararg shaders
 	init {
 		color.set(Color.WHITE)
 	}
-}
\ No newline at end of file
+}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Box2DRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Box2DRenderer.kt
index 8d17286b..64b355e1 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Box2DRenderer.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Box2DRenderer.kt
@@ -33,9 +33,9 @@ class Box2DRenderer(val state: GLStateTracker) : IDebugDraw {
 
 		builder.upload()
 
-		state.flatProgram.use()
-		state.flatProgram.color.set(color)
-		state.flatProgram.transform.set(state.matrixStack.last)
+		state.programs.flat.use()
+		state.programs.flat.color.set(color)
+		state.programs.flat.transform.set(state.matrixStack.last)
 
 		builder.draw(GL_LINES)
 	}
@@ -59,9 +59,9 @@ class Box2DRenderer(val state: GLStateTracker) : IDebugDraw {
 
 		builder.upload()
 
-		state.flatProgram.use()
-		state.flatProgram.color.set(color)
-		state.flatProgram.transform.set(state.matrixStack.last)
+		state.programs.flat.use()
+		state.programs.flat.color.set(color)
+		state.programs.flat.transform.set(state.matrixStack.last)
 
 		builder.draw(GL_TRIANGLES)
 	}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Font.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Font.kt
index 89cef7ee..fe51b6c2 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Font.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Font.kt
@@ -112,8 +112,8 @@ class Font(
 		if (scale != 1f)
 			stack.scale(x = scale, y = scale)
 
-		state.fontProgram.use()
-		state.fontProgram.color.set(color)
+		state.programs.font.use()
+		state.programs.font.color.set(color)
 		state.activeTexture = 0
 
 		val space = getGlyph(' ')
@@ -343,7 +343,7 @@ class Font(
 			stack.translateWithMultiplication(bearingX, -bearingY)
 
 			texture!!.bind()
-			state.fontProgram.transform.set(stack.last)
+			state.programs.font.transform.set(stack.last)
 			glDrawElements(GL_TRIANGLES, indexCount, elementIndexType, 0L)
 			checkForGLError()
 
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt
index 4a9dd224..28e1af4c 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt
@@ -82,7 +82,7 @@ class TileRenderers(val state: GLStateTracker) {
 		}
 	}
 
-	private inner class ForegroundTileProgram(private val texture: GLTexture2D) : ConfiguredShaderProgram(state.shaderVertexTextureHSVColor) {
+	private inner class ForegroundTileProgram(private val texture: GLTexture2D) : ConfiguredShaderProgram(state.programs.tile) {
 		override fun setup() {
 			super.setup()
 			state.activeTexture = 0
@@ -112,7 +112,7 @@ class TileRenderers(val state: GLStateTracker) {
 		}
 	}
 
-	private inner class BackgroundTileProgram(private val texture: GLTexture2D) : ConfiguredShaderProgram(state.shaderVertexTextureHSVColor) {
+	private inner class BackgroundTileProgram(private val texture: GLTexture2D) : ConfiguredShaderProgram(state.programs.tile) {
 		override fun setup() {
 			super.setup()
 			state.activeTexture = 0
diff --git a/src/main/resources/shaders/fragment/flat_color.glsl b/src/main/resources/shaders/flat.fsh
similarity index 100%
rename from src/main/resources/shaders/fragment/flat_color.glsl
rename to src/main/resources/shaders/flat.fsh
diff --git a/src/main/resources/shaders/vertex/flat_vertex_2d.glsl b/src/main/resources/shaders/flat.vsh
similarity index 100%
rename from src/main/resources/shaders/vertex/flat_vertex_2d.glsl
rename to src/main/resources/shaders/flat.vsh
diff --git a/src/main/resources/shaders/fragment/font.glsl b/src/main/resources/shaders/font.fsh
similarity index 100%
rename from src/main/resources/shaders/fragment/font.glsl
rename to src/main/resources/shaders/font.fsh
diff --git a/src/main/resources/shaders/vertex/font.glsl b/src/main/resources/shaders/font.vsh
similarity index 100%
rename from src/main/resources/shaders/vertex/font.glsl
rename to src/main/resources/shaders/font.vsh
diff --git a/src/main/resources/shaders/fragment/texture_color_per_vertex.glsl b/src/main/resources/shaders/tile.fsh
similarity index 100%
rename from src/main/resources/shaders/fragment/texture_color_per_vertex.glsl
rename to src/main/resources/shaders/tile.fsh
diff --git a/src/main/resources/shaders/vertex/texture_hsv.glsl b/src/main/resources/shaders/tile.vsh
similarity index 100%
rename from src/main/resources/shaders/vertex/texture_hsv.glsl
rename to src/main/resources/shaders/tile.vsh