package ru.dbotthepony.kstarbound.client.gl import com.github.benmanes.caffeine.cache.Cache import com.github.benmanes.caffeine.cache.Caffeine import org.apache.logging.log4j.LogManager import org.lwjgl.opengl.GL import org.lwjgl.opengl.GL46.* import org.lwjgl.opengl.GLCapabilities import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.client.freetype.FreeType import ru.dbotthepony.kstarbound.client.freetype.InvalidArgumentException import ru.dbotthepony.kstarbound.client.gl.shader.GLPrograms import ru.dbotthepony.kstarbound.client.gl.shader.GLShaderProgram import ru.dbotthepony.kstarbound.client.gl.shader.ShaderCompilationException import ru.dbotthepony.kstarbound.client.gl.vertex.GLAttributeList import ru.dbotthepony.kstarbound.client.gl.vertex.StreamVertexBuilder import ru.dbotthepony.kstarbound.client.gl.vertex.GeometryType import ru.dbotthepony.kstarbound.client.gl.vertex.VertexBuilder import ru.dbotthepony.kstarbound.client.render.Box2DRenderer import ru.dbotthepony.kstarbound.client.render.Font import ru.dbotthepony.kvector.api.IStruct4f import ru.dbotthepony.kvector.arrays.Matrix4fStack import ru.dbotthepony.kvector.util2d.AABB import ru.dbotthepony.kvector.vector.RGBAColor import java.io.File import java.lang.ref.Cleaner import java.time.Duration import java.util.* import kotlin.collections.ArrayList import kotlin.math.roundToInt import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty private class GLStateSwitchTracker(private val enum: Int, private var value: Boolean = false) { operator fun getValue(glStateTracker: GLStateTracker, property: KProperty<*>): Boolean { return value } operator fun setValue(glStateTracker: GLStateTracker, property: KProperty<*>, value: Boolean) { glStateTracker.ensureSameThread() if (value == this.value) return if (value) { glEnable(enum) } else { glDisable(enum) } checkForGLError() this.value = value } } private class GLStateFuncTracker(private val glFunc: (Int) -> Unit, private var value: Int) { operator fun getValue(glStateTracker: GLStateTracker, property: KProperty<*>): Int { return value } operator fun setValue(glStateTracker: GLStateTracker, property: KProperty<*>, value: Int) { glStateTracker.ensureSameThread() if (value == this.value) return glFunc.invoke(value) checkForGLError() this.value = value } } private class GLStateGenericTracker(private var value: T, private val callback: (T) -> Unit) : ReadWriteProperty { override fun getValue(thisRef: GLStateTracker, property: KProperty<*>): T { return value } override fun setValue(thisRef: GLStateTracker, property: KProperty<*>, value: T) { thisRef.ensureSameThread() if (value == this.value) return callback.invoke(value) checkForGLError() this.value = value } } private class TexturesTracker(maxValue: Int) : ReadWriteProperty { private val values = arrayOfNulls(maxValue) override fun getValue(thisRef: GLStateTracker, property: KProperty<*>): GLTexture2D? { return values[thisRef.activeTexture] } override fun setValue(thisRef: GLStateTracker, property: KProperty<*>, value: GLTexture2D?) { thisRef.ensureSameThread() require(value == null || thisRef === value.state) { "$value does not belong to $thisRef" } if (values[thisRef.activeTexture] === value) { return } values[thisRef.activeTexture] = value if (value == null) { glBindTexture(GL_TEXTURE_2D, 0) checkForGLError() return } glBindTexture(GL_TEXTURE_2D, value.pointer) checkForGLError() } } @Suppress("PropertyName", "unused") class GLStateTracker(val client: StarboundClient) { private fun isMe(state: GLStateTracker?) { if (state != null && state != this) { throw InvalidArgumentException("Provided object does not belong to $this state tracker (belongs to $state)") } } init { check(TRACKERS.get() == null) { "Already has state tracker existing at this thread!" } TRACKERS.set(this) } // This line is critical for LWJGL's interoperation with GLFW's // OpenGL context, or any context that is managed externally. // LWJGL detects the context that is current in the current thread, // creates the GLCapabilities instance and makes the OpenGL // bindings available for use. val capabilities: GLCapabilities = GL.createCapabilities() val programs = GLPrograms(this) val flat2DLines by lazy { StreamVertexBuilder(this, GLAttributeList.VEC2F, GeometryType.LINES) } val flat2DTriangles by lazy { StreamVertexBuilder(this, GLAttributeList.VEC2F, GeometryType.TRIANGLES) } val flat2DTexturedQuads by lazy { StreamVertexBuilder(this, GLAttributeList.VERTEX_TEXTURE, GeometryType.QUADS) } val quadWireframe by lazy { StreamVertexBuilder(this, GLAttributeList.VEC2F, GeometryType.QUADS_AS_LINES_WIREFRAME) } val matrixStack = Matrix4fStack() val freeType = FreeType() val font = Font(this) val thread: Thread = Thread.currentThread() val box2dRenderer = Box2DRenderer(this) private val scissorStack = LinkedList() private val cleanerBacklog = ArrayList<() -> Unit>() @Volatile var objectsCleaned = 0L private set @Volatile var gcHits = 0L private set private val cleaner = Cleaner.create { r -> val thread = Thread(r, "OpenGL Object Cleaner for ${this@GLStateTracker}") thread.priority = 2 thread } fun registerCleanable(ref: Any, fn: (Int) -> Unit, nativeRef: Int): Cleaner.Cleanable { val cleanable = cleaner.register(ref) { objectsCleaned++ if (isSameThread()) { fn(nativeRef) checkForGLError() } else { gcHits++ synchronized(cleanerBacklog) { cleanerBacklog.add { fn(nativeRef) checkForGLError() } } } } return cleanable } fun cleanup() { synchronized(cleanerBacklog) { for (lambda in cleanerBacklog) { lambda.invoke() } cleanerBacklog.clear() } } var blend by GLStateSwitchTracker(GL_BLEND) var scissor by GLStateSwitchTracker(GL_SCISSOR_TEST) var cull by GLStateSwitchTracker(GL_CULL_FACE) var cullMode by GLStateFuncTracker(::glCullFace, GL_BACK) var scissorRect by GLStateGenericTracker(ScissorRect(0, 0, 0, 0)) { // require(it.x >= 0) { "Invalid X ${it.x}"} // require(it.y >= 0) { "Invalid Y ${it.y}"} require(it.width >= 0) { "Invalid width ${it.width}"} require(it.height >= 0) { "Invalid height ${it.height}"} glScissor(it.x, it.y, it.width, it.height) } var depthTest by GLStateSwitchTracker(GL_DEPTH_TEST) var VBO: VertexBufferObject? = null set(value) { ensureSameThread() if (field !== value) { isMe(value?.state) 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?.state) 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?.state) glBindVertexArray(value?.pointer ?: 0) checkForGLError("Setting Vertex Array Object") field = value } } var readFramebuffer: GLFrameBuffer? = null set(value) { ensureSameThread() if (field === value) return isMe(value?.state) 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?.state) 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?.state) glUseProgram(value?.pointer ?: 0) checkForGLError("Setting shader program") field = value } } 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!" } glActiveTexture(GL_TEXTURE0 + value) checkForGLError() field = value } } var texture2D: GLTexture2D? by TexturesTracker(80) var clearColor by GLStateGenericTracker(RGBAColor.WHITE) { val (r, g, b, a) = it glClearColor(r, g, b, a) } var blendFunc by GLStateGenericTracker(BlendFunc()) { glBlendFuncSeparate(it.sourceColor.enum, it.destinationColor.enum, it.sourceAlpha.enum, it.destinationAlpha.enum) } init { glActiveTexture(GL_TEXTURE0) checkForGLError() } var viewportX: Int = 0 private set var viewportY: Int = 0 private set var viewportWidth: Int = 0 private set var viewportHeight: Int = 0 private set fun setViewport(x: Int, y: Int, width: Int, height: Int) { ensureSameThread() if (viewportX != x || viewportY != y || viewportWidth != width || viewportHeight != height) { glViewport(x, y, width, height) checkForGLError("Setting viewport") viewportX = x viewportY = y viewportWidth = width viewportHeight = height } } fun pushScissorRect(x: Float, y: Float, width: Float, height: Float) { return pushScissorRect(x.roundToInt(), y.roundToInt(), width.roundToInt(), height.roundToInt()) } @Suppress("NAME_SHADOWING") fun pushScissorRect(x: Int, y: Int, width: Int, height: Int) { var x = x var y = y var width = width var height = height val peek = scissorStack.lastOrNull() if (peek != null) { x = x.coerceAtLeast(peek.x) y = y.coerceAtLeast(peek.y) width = width.coerceAtMost(peek.width) height = height.coerceAtMost(peek.height) if (peek.x == x && peek.y == y && peek.width == width && peek.height == height) { scissorStack.add(peek) return } } val rect = ScissorRect(x, y, width, height) scissorStack.add(rect) scissorRect = rect scissor = true } fun popScissorRect() { scissorStack.removeLast() val peek = scissorStack.lastOrNull() if (peek == null) { scissor = false return } val y = viewportHeight - peek.y - peek.height scissorRect = ScissorRect(peek.x, y, peek.width, peek.height) } val currentScissorRect get() = scissorStack.lastOrNull() fun ensureSameThread() { if (thread !== Thread.currentThread()) { throw IllegalAccessException("Trying to access $this outside of $thread!") } } fun isSameThread() = thread === Thread.currentThread() fun newVBO(type: VBOType = VBOType.ARRAY): VertexBufferObject { return VertexBufferObject(this, type) } fun newEBO() = newVBO(VBOType.ELEMENT_ARRAY) fun newVAO() = VertexArrayObject(this) fun newTexture(name: String = "") = GLTexture2D(this, name) // минимальное время хранения 5 минут и... private val named2DTextures0: Cache = Caffeine.newBuilder() .expireAfterAccess(Duration.ofMinutes(5)) .build() // ...бесконечное хранение пока кто-то все ещё использует текстуру private val named2DTextures1: Cache = Caffeine.newBuilder() .weakValues() .build() private val missingTexture: GLTexture2D by lazy { newTexture(missingTexturePath).upload(Starbound.readDirect(missingTexturePath), GL_RGBA, GL_RGBA).generateMips().also { it.textureMinFilter = GL_NEAREST it.textureMagFilter = GL_NEAREST } } private val missingTexturePath = "/assetmissing.png" fun loadTexture(path: String): GLTexture2D { ensureSameThread() return named2DTextures0.get(path) { named2DTextures1.get(it) { if (!Starbound.exists(it)) { LOGGER.error("Texture {} is missing! Falling back to {}", it, missingTexturePath) missingTexture } else { newTexture(it).upload(Starbound.imageData(it)).generateMips().also { it.textureMinFilter = GL_NEAREST it.textureMagFilter = GL_NEAREST } } } } } 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 } inline fun quadWireframe(color: RGBAColor = RGBAColor.WHITE, lambda: (VertexBuilder) -> Unit) { val builder = quadWireframe builder.builder.begin() lambda.invoke(builder.builder) builder.upload() programs.flat.use() programs.flat.color = color programs.flat.transform = matrixStack.last() builder.draw(GL_LINES) } inline fun quadColor(lambda: (VertexBuilder) -> Unit) { val builder = programs.flatColor.builder builder.builder.begin() lambda.invoke(builder.builder) builder.upload() programs.flatColor.use() programs.flatColor.transform = matrixStack.last() builder.draw(GL_TRIANGLES) } inline fun quadTexture(texture: GLTexture2D, lambda: (VertexBuilder) -> Unit) { val builder = programs.textured2d.builder builder.builder.begin() lambda.invoke(builder.builder) builder.upload() activeTexture = 0 texture.bind() programs.textured2d.use() programs.textured2d.transform = matrixStack.last() programs.textured2d.texture = 0 builder.draw(GL_TRIANGLES) } inline fun quadWireframe(value: AABB, color: RGBAColor = RGBAColor.WHITE, chain: (VertexBuilder) -> Unit = {}) { quadWireframe(color) { it.quad(value.mins.x.toFloat(), value.mins.y.toFloat(), value.maxs.x.toFloat(), value.maxs.y.toFloat()) chain(it) } } inner class Shader(body: String, type: Int) { constructor(body: File, type: Int) : this(body.also { require(it.exists()) { "Shader file does not exists: $body" } }.readText(), type) init { ensureSameThread() } val pointer = glCreateShader(type) init { checkForGLError() registerCleanable(this, ::glDeleteShader, pointer) } init { if (body == "") { throw IllegalArgumentException("Shader source is empty") } glShaderSource(pointer, body) glCompileShader(pointer) val result = intArrayOf(0) glGetShaderiv(pointer, GL_COMPILE_STATUS, result) if (result[0] == 0) { throw ShaderCompilationException(glGetShaderInfoLog(pointer)) } checkForGLError() } } fun vertex(file: File) = Shader(file, GL_VERTEX_SHADER) fun fragment(file: File) = Shader(file, GL_FRAGMENT_SHADER) fun vertex(contents: String) = Shader(contents, GL_VERTEX_SHADER) fun fragment(contents: String) = Shader(contents, GL_FRAGMENT_SHADER) fun internalVertex(file: String) = Shader(readInternal(file), GL_VERTEX_SHADER) fun internalFragment(file: String) = Shader(readInternal(file), GL_FRAGMENT_SHADER) fun internalGeometry(file: String) = Shader(readInternal(file), GL_GEOMETRY_SHADER) companion object { private val LOGGER = LogManager.getLogger(GLStateTracker::class.java) private val TRACKERS = ThreadLocal() private fun readInternal(file: String): String { return ClassLoader.getSystemClassLoader().getResourceAsStream(file)!!.bufferedReader() .let { val read = it.readText() it.close() read } } } }