From 85e81a0b0b451d155b84a337dcdf7c9fddd59b81 Mon Sep 17 00:00:00 2001
From: DBotThePony <dbotthepony@yandex.ru>
Date: Wed, 20 Sep 2023 14:51:33 +0700
Subject: [PATCH] OpenGL object tracking cleanup

---
 .../kstarbound/client/StarboundClient.kt      | 166 +++---------------
 ...{VertexBufferObject.kt => BufferObject.kt} |  57 +++---
 .../kstarbound/client/gl/GLFrameBuffer.kt     |  27 +--
 .../kstarbound/client/gl/GLObject.kt          |  11 ++
 .../kstarbound/client/gl/GLTexture.kt         |  14 +-
 .../kstarbound/client/gl/VertexArrayObject.kt |  15 +-
 ...eGenericTracker.kt => GLGenericTracker.kt} |   2 +-
 .../client/gl/properties/GLObjectTracker.kt   |  58 ++++++
 .../client/gl/shader/GLShaderProgram.kt       |  17 +-
 .../client/gl/vertex/StreamVertexBuilder.kt   |   6 +-
 .../client/gl/vertex/VertexBuilder.kt         |   7 +-
 .../kstarbound/client/render/Font.kt          |   6 +-
 12 files changed, 164 insertions(+), 222 deletions(-)
 rename src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/{VertexBufferObject.kt => BufferObject.kt} (55%)
 create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLObject.kt
 rename src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/properties/{GLStateGenericTracker.kt => GLGenericTracker.kt} (81%)
 create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/properties/GLObjectTracker.kt

diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt
index bb51be6e..a925853e 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt
@@ -8,6 +8,7 @@ import org.lwjgl.glfw.Callbacks
 import org.lwjgl.glfw.GLFW
 import org.lwjgl.glfw.GLFWErrorCallback
 import org.lwjgl.opengl.GL
+import org.lwjgl.opengl.GL11
 import org.lwjgl.opengl.GL46.*
 import org.lwjgl.opengl.GLCapabilities
 import org.lwjgl.system.MemoryStack
@@ -21,15 +22,14 @@ import ru.dbotthepony.kstarbound.client.gl.BlendFunc
 import ru.dbotthepony.kstarbound.client.gl.GLFrameBuffer
 import ru.dbotthepony.kstarbound.client.gl.GLTexture2D
 import ru.dbotthepony.kstarbound.client.gl.ScissorRect
-import ru.dbotthepony.kstarbound.client.gl.VBOType
 import ru.dbotthepony.kstarbound.client.gl.VertexArrayObject
-import ru.dbotthepony.kstarbound.client.gl.VertexBufferObject
+import ru.dbotthepony.kstarbound.client.gl.BufferObject
 import ru.dbotthepony.kstarbound.client.gl.checkForGLError
 import ru.dbotthepony.kstarbound.client.gl.properties.GLStateFuncTracker
-import ru.dbotthepony.kstarbound.client.gl.properties.GLStateGenericTracker
+import ru.dbotthepony.kstarbound.client.gl.properties.GLGenericTracker
+import ru.dbotthepony.kstarbound.client.gl.properties.GLObjectTracker
 import ru.dbotthepony.kstarbound.client.gl.properties.GLStateIntTracker
 import ru.dbotthepony.kstarbound.client.gl.properties.GLStateSwitchTracker
-import ru.dbotthepony.kstarbound.client.gl.properties.TexturesTracker
 import ru.dbotthepony.kstarbound.client.gl.shader.GLPrograms
 import ru.dbotthepony.kstarbound.client.gl.shader.GLShader
 import ru.dbotthepony.kstarbound.client.gl.shader.GLShaderProgram
@@ -237,8 +237,6 @@ class StarboundClient : Closeable {
 		putDebugLog("Initialized GLFW window")
 	}
 
-	val programs = GLPrograms()
-
 	val flat2DLines by lazy { StreamVertexBuilder(GLAttributeList.VEC2F, GeometryType.LINES) }
 	val flat2DTriangles by lazy { StreamVertexBuilder(GLAttributeList.VEC2F, GeometryType.TRIANGLES) }
 	val flat2DTexturedQuads by lazy { StreamVertexBuilder(GLAttributeList.VERTEX_TEXTURE, GeometryType.QUADS) }
@@ -308,7 +306,7 @@ class StarboundClient : Closeable {
 
 	var textureUnpackAlignment by GLStateIntTracker(::glPixelStorei, GL_UNPACK_ALIGNMENT, 4)
 
-	var scissorRect by GLStateGenericTracker(ScissorRect(0, 0, 0, 0)) {
+	var scissorRect by GLGenericTracker(ScissorRect(0, 0, 0, 0)) {
 		// require(it.x >= 0) { "Invalid X ${it.x}"}
 		// require(it.y >= 0) { "Invalid Y ${it.y}"}
 
@@ -320,130 +318,42 @@ class StarboundClient : Closeable {
 
 	var depthTest by GLStateSwitchTracker(GL_DEPTH_TEST)
 
-	var VBO: VertexBufferObject? = null
-		set(value) {
-			ensureSameThread()
+	var vbo by GLObjectTracker<BufferObject.VBO>(::glBindBuffer, GL_ARRAY_BUFFER)
+	var ebo by GLObjectTracker<BufferObject.EBO>(::glBindBuffer, GL_ELEMENT_ARRAY_BUFFER)
+	var vao by GLObjectTracker<VertexArrayObject>(::glBindVertexArray)
+	var framebuffer by GLObjectTracker<GLFrameBuffer>(::glBindFramebuffer, GL_FRAMEBUFFER)
+	var program by GLObjectTracker<GLShaderProgram>(::glUseProgram)
 
-			if (field !== value) {
-				isMe(value?.client)
-				require(value?.isArray != false) { "Provided buffer object is not of Array type" }
-				glBindBuffer(GL_ARRAY_BUFFER, value?.pointer ?: 0)
-				checkForGLError("Setting Vertex Buffer Object")
-				field = value
-			}
-		}
-
-	var EBO: VertexBufferObject? = null
-		set(value) {
-			ensureSameThread()
-
-			if (field !== value) {
-				isMe(value?.client)
-				require(value?.isElementArray != false) { "Provided buffer object is not of Array type" }
-				glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, value?.pointer ?: 0)
-				checkForGLError("Setting Element Buffer Object")
-				field = value
-			}
-		}
-
-	var VAO: VertexArrayObject? = null
-		set(value) {
-			ensureSameThread()
-
-			if (field !== value) {
-				isMe(value?.client)
-				glBindVertexArray(value?.pointer ?: 0)
-				checkForGLError("Setting Vertex Array Object")
-				field = value
-			}
-		}
-
-	var readFramebuffer: GLFrameBuffer? = null
-		set(value) {
-			ensureSameThread()
-			if (field === value) return
-			isMe(value?.client)
-			field = value
-
-			if (value == null) {
-				glBindFramebuffer(GL_READ_FRAMEBUFFER, 0)
-				checkForGLError()
-				return
-			}
-
-			glBindFramebuffer(GL_READ_FRAMEBUFFER, value.pointer)
-			checkForGLError()
-		}
-
-	var writeFramebuffer: GLFrameBuffer? = null
-		set(value) {
-			ensureSameThread()
-			if (field === value) return
-			isMe(value?.client)
-			field = value
-
-			if (value == null) {
-				glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0)
-				checkForGLError()
-				return
-			}
-
-			glBindFramebuffer(GL_DRAW_FRAMEBUFFER, value.pointer)
-			checkForGLError()
-		}
-
-	var framebuffer: GLFrameBuffer?
-		get() {
-			val readFramebuffer = readFramebuffer
-			val writeFramebuffer = writeFramebuffer
-
-			if (readFramebuffer == writeFramebuffer) {
-				return writeFramebuffer
-			}
-
-			return null
-		}
-		set(value) {
-			readFramebuffer = value
-			writeFramebuffer = value
-		}
-
-	var program: GLShaderProgram? = null
-		set(value) {
-			ensureSameThread()
-
-			if (value !== field) {
-				isMe(value?.client)
-				glUseProgram(value?.pointer ?: 0)
-				checkForGLError("Setting shader program")
-				field = value
-			}
-		}
+	val maxTextureBlocks = glGetInteger(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS)
+	private val textures = Array(maxTextureBlocks) { GLObjectTracker<GLTexture2D>(GL11::glBindTexture, GL_TEXTURE_2D) }
 
 	var activeTexture = 0
 		set(value) {
 			ensureSameThread()
 
 			if (field != value) {
-				require(value >= 0) { "Invalid texture block $value" }
-				require(value < 80) { "Too big texture block index $value, OpenGL 4.6 guarantee only 80!" }
+				require(value in 0 until maxTextureBlocks) { "Texture block out of bounds: $value (max texture blocks is $maxTextureBlocks)" }
 				glActiveTexture(GL_TEXTURE0 + value)
 				checkForGLError()
 				field = value
 			}
 		}
 
-	var texture2D: GLTexture2D? by TexturesTracker(80)
+	var texture2D: GLTexture2D?
+		get() = textures[activeTexture].get()
+		set(value) { textures[activeTexture].accept(value) }
 
-	var clearColor by GLStateGenericTracker<IStruct4f>(RGBAColor.WHITE) {
+	var clearColor by GLGenericTracker<IStruct4f>(RGBAColor.WHITE) {
 		val (r, g, b, a) = it
 		glClearColor(r, g, b, a)
 	}
 
-	var blendFunc by GLStateGenericTracker(BlendFunc()) {
+	var blendFunc by GLGenericTracker(BlendFunc()) {
 		glBlendFuncSeparate(it.sourceColor.enum, it.destinationColor.enum, it.sourceAlpha.enum, it.destinationAlpha.enum)
 	}
 
+	val programs = GLPrograms()
+
 	init {
 		glActiveTexture(GL_TEXTURE0)
 		checkForGLError()
@@ -538,40 +448,8 @@ class StarboundClient : Closeable {
 		}
 	}
 
-	fun bind(obj: VertexBufferObject): VertexBufferObject {
-		if (obj.type == VBOType.ARRAY)
-			VBO = obj
-		else
-			EBO = obj
-
-		return obj
-	}
-
-	fun unbind(obj: VertexBufferObject): VertexBufferObject {
-		if (obj.type == VBOType.ARRAY)
-			if (obj == VBO)
-				VBO = null
-			else
-				if (obj == EBO)
-					EBO = null
-
-		return obj
-	}
-
-	fun bind(obj: VertexArrayObject): VertexArrayObject {
-		VAO = obj
-		return obj
-	}
-
-	fun unbind(obj: VertexArrayObject): VertexArrayObject {
-		if (obj == VAO)
-			VAO = null
-
-		return obj
-	}
-
-	fun newVBO() = VertexBufferObject.vbo()
-	fun newEBO() = VertexBufferObject.ebo()
+	fun newVBO() = BufferObject.VBO()
+	fun newEBO() = BufferObject.EBO()
 	fun newVAO() = VertexArrayObject()
 
 	inline fun quadWireframe(color: RGBAColor = RGBAColor.WHITE, lambda: (VertexBuilder) -> Unit) {
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/VertexBufferObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/BufferObject.kt
similarity index 55%
rename from src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/VertexBufferObject.kt
rename to src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/BufferObject.kt
index d3c6d699..66090c6d 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/VertexBufferObject.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/BufferObject.kt
@@ -5,41 +5,23 @@ import org.lwjgl.system.MemoryUtil
 import ru.dbotthepony.kstarbound.client.StarboundClient
 import java.nio.ByteBuffer
 
-enum class VBOType(val glType: Int) {
-	ARRAY(GL_ARRAY_BUFFER),
-	ELEMENT_ARRAY(GL_ELEMENT_ARRAY_BUFFER),
-}
-
-class VertexBufferObject private constructor(val type: VBOType) {
-	val client = StarboundClient.current()
-	val pointer = glGenBuffers()
+sealed class BufferObject(val glType: Int) : GLObject() {
+	final override val client = StarboundClient.current()
+	final override val pointer = glGenBuffers()
 
 	init {
 		checkForGLError("Creating Vertex Buffer Object")
 		client.registerCleanable(this, ::glDeleteBuffers, pointer)
 	}
 
-	val isArray get() = type == VBOType.ARRAY
-	val isElementArray get() = type == VBOType.ELEMENT_ARRAY
-
-	fun bind(): VertexBufferObject {
-		client.bind(this)
-		return this
-	}
-
-	fun unbind(): VertexBufferObject {
-		client.unbind(this)
-		return this
-	}
-
-	fun bufferData(data: ByteBuffer, usage: Int): VertexBufferObject {
+	fun bufferData(data: ByteBuffer, usage: Int): BufferObject {
 		client.ensureSameThread()
 		glNamedBufferData(pointer, data, usage)
 		checkForGLError()
 		return this
 	}
 
-	fun bufferData(data: ByteBuffer, usage: Int, length: Long): VertexBufferObject {
+	fun bufferData(data: ByteBuffer, usage: Int, length: Long): BufferObject {
 		client.ensureSameThread()
 
 		if (length > data.remaining().toLong()) {
@@ -52,36 +34,51 @@ class VertexBufferObject private constructor(val type: VBOType) {
 		return this
 	}
 
-	fun bufferData(data: IntArray, usage: Int): VertexBufferObject {
+	fun bufferData(data: IntArray, usage: Int): BufferObject {
 		client.ensureSameThread()
 		glNamedBufferData(pointer, data, usage)
 		checkForGLError()
 		return this
 	}
 
-	fun bufferData(data: FloatArray, usage: Int): VertexBufferObject {
+	fun bufferData(data: FloatArray, usage: Int): BufferObject {
 		client.ensureSameThread()
 		glNamedBufferData(pointer, data, usage)
 		checkForGLError()
 		return this
 	}
 
-	fun bufferData(data: DoubleArray, usage: Int): VertexBufferObject {
+	fun bufferData(data: DoubleArray, usage: Int): BufferObject {
 		client.ensureSameThread()
 		glNamedBufferData(pointer, data, usage)
 		checkForGLError()
 		return this
 	}
 
-	fun bufferData(data: LongArray, usage: Int): VertexBufferObject {
+	fun bufferData(data: LongArray, usage: Int): BufferObject {
 		client.ensureSameThread()
 		glNamedBufferData(pointer, data, usage)
 		checkForGLError()
 		return this
 	}
 
-	companion object {
-		fun vbo() = VertexBufferObject(VBOType.ARRAY)
-		fun ebo() = VertexBufferObject(VBOType.ELEMENT_ARRAY)
+	class VBO : BufferObject(GL_ARRAY_BUFFER) {
+		override fun bind() {
+			client.vbo = this
+		}
+
+		override fun unbind() {
+			if (client.vbo == this) client.vbo = null
+		}
+	}
+
+	class EBO : BufferObject(GL_ELEMENT_ARRAY_BUFFER) {
+		override fun bind() {
+			client.ebo = this
+		}
+
+		override fun unbind() {
+			if (client.ebo == this) client.ebo = null
+		}
 	}
 }
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLFrameBuffer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLFrameBuffer.kt
index cbc4c599..f9e9921d 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLFrameBuffer.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLFrameBuffer.kt
@@ -11,9 +11,9 @@ import org.lwjgl.opengl.GL45.glCheckNamedFramebufferStatus
 import org.lwjgl.opengl.GL46
 import ru.dbotthepony.kstarbound.client.StarboundClient
 
-class GLFrameBuffer {
-	val client = StarboundClient.current()
-	val pointer = GL46.glGenFramebuffers()
+class GLFrameBuffer : GLObject() {
+	override val client = StarboundClient.current()
+	override val pointer = GL46.glGenFramebuffers()
 
 	init {
 		checkForGLError("Creating framebuffer")
@@ -50,25 +50,12 @@ class GLFrameBuffer {
 		attachTexture(width, height, format)
 	}
 
-	fun bind() {
+	override fun bind() {
 		client.framebuffer = this
 	}
 
-	fun bindWrite() {
-		client.writeFramebuffer = this
-	}
-
-	fun bindRead() {
-		client.readFramebuffer = this
-	}
-
-	fun unbind() {
-		if (client.writeFramebuffer == this) {
-			client.writeFramebuffer = null
-		}
-
-		if (client.readFramebuffer == this) {
-			client.readFramebuffer = null
-		}
+	override fun unbind() {
+		if (client.framebuffer == this)
+			client.framebuffer = null
 	}
 }
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLObject.kt
new file mode 100644
index 00000000..095db1ba
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLObject.kt
@@ -0,0 +1,11 @@
+package ru.dbotthepony.kstarbound.client.gl
+
+import ru.dbotthepony.kstarbound.client.StarboundClient
+
+abstract class GLObject {
+	abstract val pointer: Int
+	abstract val client: StarboundClient
+
+	abstract fun bind()
+	abstract fun unbind()
+}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLTexture.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLTexture.kt
index ca91cafa..7714dcd8 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLTexture.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLTexture.kt
@@ -30,9 +30,9 @@ private class GLTexturePropertyTracker(private val flag: Int, private var value:
 }
 
 @Suppress("SameParameterValue")
-class GLTexture2D(val name: String = "<unknown>") {
-	val client = StarboundClient.current()
-	val pointer = glGenTextures()
+class GLTexture2D(val name: String = "<unknown>") : GLObject() {
+	override val client = StarboundClient.current()
+	override val pointer = glGenTextures()
 
 	init {
 		checkForGLError()
@@ -71,9 +71,13 @@ class GLTexture2D(val name: String = "<unknown>") {
 	var textureWrapS by GLTexturePropertyTracker(GL_TEXTURE_WRAP_S, GL_REPEAT)
 	var textureWrapT by GLTexturePropertyTracker(GL_TEXTURE_WRAP_T, GL_REPEAT)
 
-	fun bind(): GLTexture2D {
+	override fun bind() {
 		client.texture2D = this
-		return this
+	}
+
+	override fun unbind() {
+		if (client.texture2D === this)
+			client.texture2D = null
 	}
 
 	fun generateMips(): GLTexture2D {
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/VertexArrayObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/VertexArrayObject.kt
index 503491e8..65861536 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/VertexArrayObject.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/VertexArrayObject.kt
@@ -3,21 +3,22 @@ package ru.dbotthepony.kstarbound.client.gl
 import org.lwjgl.opengl.GL46.*
 import ru.dbotthepony.kstarbound.client.StarboundClient
 
-class VertexArrayObject {
-	val client = StarboundClient.current()
-	val pointer = glGenVertexArrays()
+class VertexArrayObject : GLObject() {
+	override val client = StarboundClient.current()
+	override val pointer = glGenVertexArrays()
 
 	init {
 		checkForGLError()
 		client.registerCleanable(this, ::glDeleteVertexArrays, pointer)
 	}
 
-	fun bind(): VertexArrayObject {
-		return client.bind(this)
+	override fun bind() {
+		client.vao = this
 	}
 
-	fun unbind(): VertexArrayObject {
-		return client.unbind(this)
+	override fun unbind() {
+		if (client.vao == this)
+			client.vao = null
 	}
 
 	fun attribute(position: Int, size: Int, type: Int, normalize: Boolean, stride: Int, offset: Long = 0L): VertexArrayObject {
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/properties/GLStateGenericTracker.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/properties/GLGenericTracker.kt
similarity index 81%
rename from src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/properties/GLStateGenericTracker.kt
rename to src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/properties/GLGenericTracker.kt
index a4af4efa..5db522a5 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/properties/GLStateGenericTracker.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/properties/GLGenericTracker.kt
@@ -5,7 +5,7 @@ import ru.dbotthepony.kstarbound.client.gl.checkForGLError
 import kotlin.properties.ReadWriteProperty
 import kotlin.reflect.KProperty
 
-class GLStateGenericTracker<T>(private var value: T, private val callback: (T) -> Unit) : ReadWriteProperty<StarboundClient, T> {
+class GLGenericTracker<T>(private var value: T, private val callback: (T) -> Unit) : ReadWriteProperty<StarboundClient, T> {
 	override fun getValue(thisRef: StarboundClient, property: KProperty<*>): T {
 		return value
 	}
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/properties/GLObjectTracker.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/properties/GLObjectTracker.kt
new file mode 100644
index 00000000..6379451b
--- /dev/null
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/properties/GLObjectTracker.kt
@@ -0,0 +1,58 @@
+package ru.dbotthepony.kstarbound.client.gl.properties
+
+import ru.dbotthepony.kstarbound.client.StarboundClient
+import ru.dbotthepony.kstarbound.client.gl.GLObject
+import ru.dbotthepony.kstarbound.client.gl.checkForGLError
+import java.util.function.Consumer
+import java.util.function.IntConsumer
+import java.util.function.Supplier
+import kotlin.reflect.KProperty
+
+class GLObjectTracker<T : GLObject>(
+	private val glFunc: IntConsumer,
+	private val nullValue: Int = 0,
+	private val message: String? = null
+) : Supplier<T?>, Consumer<T?> {
+	constructor(glFunc: BiConsumer, type: Int, nullValue: Int = 0, message: String? = null) : this(IntConsumer { glFunc.invoke(type, it) }, nullValue)
+	constructor(glFunc: BiConsumer, type: Int, message: String? = null) : this(IntConsumer { glFunc.invoke(type, it) }, 0, message)
+	constructor(glFunc: IntConsumer, message: String?) : this(glFunc, 0, message)
+
+	fun interface BiConsumer {
+		fun invoke(a: Int, b: Int)
+	}
+
+	private val client = StarboundClient.current()
+	private var value: T? = null
+	private var actualPointer = nullValue
+		set(value) {
+			if (value != field) {
+				glFunc.accept(value)
+				checkForGLError()
+				field = value
+			}
+		}
+
+	operator fun getValue(thisRef: Any?, property: KProperty<*>): T? {
+		return value
+	}
+
+	operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) {
+		accept(value)
+	}
+
+	override fun get(): T? {
+		return value
+	}
+
+	override fun accept(value: T?) {
+		client.ensureSameThread()
+
+		if (this.value !== value) {
+			if (value != null && value.client != client)
+				throw IllegalArgumentException("$value does not belong to $client (it belongs to ${value.client})")
+
+			actualPointer = value?.pointer ?: nullValue
+			this.value = value
+		}
+	}
+}
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 f78e288a..e3baf94b 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
@@ -6,6 +6,7 @@ import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap
 import it.unimi.dsi.fastutil.objects.Object2ObjectFunction
 import org.lwjgl.opengl.GL46.*
 import ru.dbotthepony.kstarbound.client.StarboundClient
+import ru.dbotthepony.kstarbound.client.gl.GLObject
 import ru.dbotthepony.kstarbound.client.gl.checkForGLError
 import ru.dbotthepony.kstarbound.client.gl.vertex.GLAttributeList
 import ru.dbotthepony.kvector.api.IStruct2f
@@ -26,10 +27,9 @@ import kotlin.reflect.KProperty
 open class GLShaderProgram(
 	shaders: Iterable<GLShader>,
 	val attributes: GLAttributeList
-) {
-	val client = StarboundClient.current()
-
-	val pointer = glCreateProgram()
+) : GLObject() {
+	final override val client = StarboundClient.current()
+	final override val pointer = glCreateProgram()
 
 	init {
 		checkForGLError("Creating shader program")
@@ -57,6 +57,15 @@ open class GLShaderProgram(
 		return this
 	}
 
+	final override fun bind() {
+		client.program = this
+	}
+
+	final override fun unbind() {
+		if (client.program === this)
+			client.program = null
+	}
+
 	private val locations = Object2ObjectArrayMap<String, Uniform<*>>()
 	private val uniformsExist = Object2BooleanArrayMap<String>()
 
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/StreamVertexBuilder.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/StreamVertexBuilder.kt
index 7c3d5180..f4eedf99 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/StreamVertexBuilder.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/StreamVertexBuilder.kt
@@ -3,7 +3,7 @@ package ru.dbotthepony.kstarbound.client.gl.vertex
 import org.lwjgl.opengl.GL46
 import ru.dbotthepony.kstarbound.client.StarboundClient
 import ru.dbotthepony.kstarbound.client.gl.VertexArrayObject
-import ru.dbotthepony.kstarbound.client.gl.VertexBufferObject
+import ru.dbotthepony.kstarbound.client.gl.BufferObject
 import ru.dbotthepony.kstarbound.client.gl.checkForGLError
 
 /**
@@ -17,8 +17,8 @@ class StreamVertexBuilder(
 	val state = StarboundClient.current()
 	val builder = VertexBuilder(attributes, type, initialCapacity)
 	private val vao = VertexArrayObject()
-	private val vbo = VertexBufferObject.vbo()
-	private val ebo = VertexBufferObject.ebo()
+	private val vbo = BufferObject.VBO()
+	private val ebo = BufferObject.EBO()
 
 	init {
 		vao.bind()
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/VertexBuilder.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/VertexBuilder.kt
index 54085cbb..72c188fb 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/VertexBuilder.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/vertex/VertexBuilder.kt
@@ -5,7 +5,7 @@ import org.lwjgl.opengl.GL46.GL_UNSIGNED_INT
 import org.lwjgl.opengl.GL46.GL_UNSIGNED_SHORT
 import org.lwjgl.opengl.GL46.GL_UNSIGNED_BYTE
 import ru.dbotthepony.kstarbound.client.gl.GLType
-import ru.dbotthepony.kstarbound.client.gl.VertexBufferObject
+import ru.dbotthepony.kstarbound.client.gl.BufferObject
 import ru.dbotthepony.kvector.util2d.AABB
 import java.nio.ByteBuffer
 import java.nio.ByteOrder
@@ -220,10 +220,7 @@ class VertexBuilder(
 		return this
 	}
 
-	fun upload(vbo: VertexBufferObject, ebo: VertexBufferObject, drawType: Int = GL46.GL_DYNAMIC_DRAW) {
-		require(vbo.isArray) { "$vbo is not an array" }
-		require(ebo.isElementArray) { "$ebo is not an element array" }
-
+	fun upload(vbo: BufferObject.VBO, ebo: BufferObject.EBO, drawType: Int = GL46.GL_DYNAMIC_DRAW) {
 		end()
 
 		check(elementVertices == 0) { "Not fully built vertex element ($mode requires ${mode?.elements} vertex points to be present, yet last strip has only $elementVertices elements)" }
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 23b2c716..bef841a0 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Font.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/Font.kt
@@ -170,7 +170,7 @@ class Font(
 			stack.last().translateWithMultiplication(x = -lineWidth - movedX, y = lineHeight)
 		}
 
-		state.VAO = null
+		state.vao = null
 		stack.pop()
 
 		return TextSize(totalX * scale, totalY * scale)
@@ -258,8 +258,8 @@ class Font(
 		val advanceX: Float
 		val advanceY: Float
 
-		private val vbo: VertexBufferObject? // все три указателя должны хранится во избежание утечки
-		private val ebo: VertexBufferObject? // все три указателя должны хранится во избежание утечки
+		private val vbo: BufferObject.VBO? // все три указателя должны хранится во избежание утечки
+		private val ebo: BufferObject.EBO? // все три указателя должны хранится во избежание утечки
 		private val vao: VertexArrayObject?  // все три указателя должны хранится во избежание утечки
 
 		private val indexCount: Int