package ru.dbotthepony.kstarbound.client.gl import org.apache.logging.log4j.LogManager import org.lwjgl.opengl.GL import org.lwjgl.opengl.GL14 import org.lwjgl.opengl.GL46.* import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.client.freetype.FreeType import ru.dbotthepony.kstarbound.client.freetype.InvalidArgumentException import ru.dbotthepony.kstarbound.client.gl.program.GLPrograms import ru.dbotthepony.kstarbound.client.gl.shader.GLShader import ru.dbotthepony.kstarbound.client.gl.program.GLShaderProgram import ru.dbotthepony.kstarbound.client.gl.shader.GLTransformableColorableProgram import ru.dbotthepony.kstarbound.client.gl.shader.GLTransformableProgram 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.quad import ru.dbotthepony.kstarbound.client.render.Box2DRenderer import ru.dbotthepony.kstarbound.client.render.Font import ru.dbotthepony.kstarbound.client.render.TileRenderers import ru.dbotthepony.kvector.api.IStruct4f import ru.dbotthepony.kvector.matrix.Matrix4fStack import ru.dbotthepony.kvector.util2d.AABB import ru.dbotthepony.kvector.vector.Color import java.io.FileNotFoundException import java.lang.ref.Cleaner import java.util.* import java.util.concurrent.ThreadFactory import kotlin.collections.ArrayList import kotlin.collections.HashMap 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 GLStateGenericTracker(private var value: T, private val callback: (T) -> Unit) { operator fun getValue(glStateTracker: GLStateTracker, property: KProperty<*>): T { return value } operator fun setValue(glStateTracker: GLStateTracker, property: KProperty<*>, value: T) { glStateTracker.ensureSameThread() if (value == this.value) return callback.invoke(value) checkForGLError() this.value = value } } @Suppress("unused") data class BlendFunc( val sourceColor: Func, val destinationColor: Func, val sourceAlpha: Func, val destinationAlpha: Func ) { constructor() : this( Func.ONE, Func.ZERO, Func.ONE, Func.ZERO ) constructor( source: Func, destination: Func ) : this( source, destination, source, destination ) enum class Func(val enum: Int) { ZERO(GL_ZERO), ONE(GL_ONE), SRC_COLOR(GL_SRC_COLOR), ONE_MINUS_SRC_COLOR(GL_ONE_MINUS_SRC_COLOR), SRC_ALPHA(GL_SRC_ALPHA), ONE_MINUS_SRC_ALPHA(GL_ONE_MINUS_SRC_ALPHA), DST_ALPHA(GL_DST_ALPHA), ONE_MINUS_DST_ALPHA(GL_ONE_MINUS_DST_ALPHA), DST_COLOR(GL_DST_COLOR), ONE_MINUS_DST_COLOR(GL_ONE_MINUS_DST_COLOR), SRC_ALPHA_SATURATE(GL_SRC_ALPHA_SATURATE); } companion object { val DEFAULT = BlendFunc() val ALPHA_TEST = BlendFunc(Func.SRC_ALPHA, Func.ONE_MINUS_SRC_ALPHA) val ONLY_ALPHA = BlendFunc( Func.ZERO, Func.ONE, Func.ONE, Func.ZERO ) val ONLY_COLOR = BlendFunc( Func.ONE, Func.ZERO, Func.ZERO, Func.ONE ) } } interface GLCleanable : Cleaner.Cleanable { /** * Выставляет флаг на то, что объект был удалён вручную и вызывает clean() */ fun cleanManual() } interface GLStreamBuilderList { val small: StreamVertexBuilder } @Suppress("PropertyName", "unused") class GLStateTracker { 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. GL.createCapabilities() } private val cleanerHits = ArrayList<() -> Unit>() private val cleaner = Cleaner.create(object : ThreadFactory { override fun newThread(r: Runnable): Thread { val thread = Thread(r, "OpenGL Object Cleaner@" + System.identityHashCode(this)) thread.priority = 2 return thread } }) fun registerCleanable(ref: Any, fn: (Int) -> Unit, name: String, nativeRef: Int): GLCleanable { var cleanManual = false val cleanable = cleaner.register(ref) { if (!cleanManual) LOGGER.error("{} with ID {} was GC'd by JVM, but it should have been removed manually.", name, nativeRef) synchronized(cleanerHits) { cleanerHits.add { fn(nativeRef) checkForGLError() } } } return object : GLCleanable { override fun cleanManual() { cleanManual = true clean() } override fun clean() = cleanable.clean() } } fun cleanup() { synchronized(cleanerHits) { for (lambda in cleanerHits) { lambda.invoke() } cleanerHits.clear() } } var blend by GLStateSwitchTracker(GL_BLEND) var depthTest by GLStateSwitchTracker(GL_DEPTH_TEST) var VBO: VertexBufferObject? = null set(value) { ensureSameThread() if (field === value) return isMe(value?.state) field = value if (value == null) { glBindBuffer(GL_ARRAY_BUFFER, 0) checkForGLError() return } if (!value.isArray) throw IllegalArgumentException("Provided buffer object is not of Array type") glBindBuffer(GL_ARRAY_BUFFER, value.pointer) checkForGLError() } var EBO: VertexBufferObject? = null set(value) { ensureSameThread() if (field === value) return isMe(value?.state) field = value if (value == null) { glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0) checkForGLError() return } if (!value.isElementArray) throw IllegalArgumentException("Provided buffer object is not of Array type") glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, value.pointer) checkForGLError() } var VAO: VertexArrayObject? = null set(value) { ensureSameThread() if (field === value) return isMe(value?.state) field = value if (value == null) { glBindVertexArray(0) checkForGLError() return } glBindVertexArray(value.pointer) checkForGLError() } 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 private set var texture2D: GLTexture2D? = null set(value) { ensureSameThread() if (field === value) return isMe(value?.state) field = value if (value == null) return glBindTexture(GL_TEXTURE_2D, value.pointer) checkForGLError() } var activeTexture = 0 set(value) { ensureSameThread() if (field == value) return require(value >= 0) { "Invalid texture block $value" } require(value < 80) { "Too big texture block index $value, OpenGL 4.6 guarantee only 80!" } field = value glActiveTexture(GL_TEXTURE0 + value) checkForGLError() } var clearColor by GLStateGenericTracker(Color.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() } val thread: Thread = Thread.currentThread() val tileRenderers = TileRenderers(this) fun ensureSameThread() { if (thread !== Thread.currentThread()) { throw IllegalAccessException("Trying to access $this outside of $thread!") } } fun isSameThread() = thread === Thread.currentThread() fun program(vararg shaders: GLShader): GLShaderProgram { return GLShaderProgram(this, *shaders) } 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) private val named2DTextures = HashMap() fun loadNamedTexture(path: String, memoryFormat: Int, fileFormat: Int): GLTexture2D { return named2DTextures.computeIfAbsent(path) { if (!Starbound.pathExists(path)) { throw FileNotFoundException("Unable to locate $path") } return@computeIfAbsent newTexture(path).upload(Starbound.readDirect(path), memoryFormat, fileFormat).generateMips() } } fun loadNamedTexture(path: String): GLTexture2D { return named2DTextures.computeIfAbsent(path) { if (!Starbound.pathExists(path)) { throw FileNotFoundException("Unable to locate $path") } return@computeIfAbsent newTexture(path).upload(Starbound.readDirect(path)).generateMips() } } private var loadedEmptyTexture = false private val missingTexturePath = "/assetmissing.png" fun loadNamedTextureSafe(path: String, memoryFormat: Int, fileFormat: Int): GLTexture2D { if (!loadedEmptyTexture) { loadedEmptyTexture = true named2DTextures[missingTexturePath] = newTexture(missingTexturePath).upload(Starbound.readDirect(missingTexturePath), memoryFormat, fileFormat).generateMips() } return named2DTextures.computeIfAbsent(path) { if (!Starbound.pathExists(path)) { LOGGER.error("Texture {} is missing! Falling back to {}", path, missingTexturePath) return@computeIfAbsent named2DTextures[missingTexturePath]!! } return@computeIfAbsent newTexture(path).upload(Starbound.readDirect(path), memoryFormat, fileFormat).generateMips() } } fun loadNamedTextureSafe(path: String): GLTexture2D { if (!loadedEmptyTexture) { loadedEmptyTexture = true named2DTextures[missingTexturePath] = newTexture(missingTexturePath).upload(Starbound.readDirect(missingTexturePath), GL_RGBA, GL_RGBA).generateMips() } return named2DTextures.computeIfAbsent(path) { if (!Starbound.pathExists(path)) { LOGGER.error("Texture {} is missing! Falling back to {}", path, missingTexturePath) return@computeIfAbsent named2DTextures[missingTexturePath]!! } return@computeIfAbsent newTexture(path).upload(Starbound.readDirect(path)).generateMips() } } 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 use(obj: GLShaderProgram): GLShaderProgram { ensureSameThread() if (obj == program) { return obj } check(obj.linked) { "Program is not linked!" } program = obj glUseProgram(obj.pointer) checkForGLError() return obj } val shaderVertexTexture: GLTransformableProgram val shaderVertexTextureColor: 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") shaderVertexTexture = GLTransformableProgram(this, textureF, textureV) shaderVertexTextureColor = GLTransformableColorableProgram(this, textureColorF, textureV) textureF.unlink() textureColorF.unlink() textureV.unlink() } val programs = GLPrograms(this) val flat2DLines = object : GLStreamBuilderList { override val small by lazy { return@lazy StreamVertexBuilder(this@GLStateTracker, GLAttributeList.VEC2F, GeometryType.LINES, 1024) } } val flat2DTriangles = object : GLStreamBuilderList { override val small by lazy { return@lazy StreamVertexBuilder(this@GLStateTracker, GLAttributeList.VEC2F, GeometryType.TRIANGLES, 1024) } } val flat2DQuads = object : GLStreamBuilderList { override val small by lazy { return@lazy StreamVertexBuilder(this@GLStateTracker, GLAttributeList.VEC2F, GeometryType.QUADS, 1024) } } val flat2DTexturedQuads = object : GLStreamBuilderList { override val small by lazy { return@lazy StreamVertexBuilder(this@GLStateTracker, GLAttributeList.VERTEX_TEXTURE, GeometryType.QUADS, 1024) } } val flat2DQuadLines = object : GLStreamBuilderList { override val small by lazy { return@lazy StreamVertexBuilder(this@GLStateTracker, GLAttributeList.VEC2F, GeometryType.QUADS_AS_LINES, 1024) } } val flat2DQuadWireframe = object : GLStreamBuilderList { override val small by lazy { return@lazy StreamVertexBuilder(this@GLStateTracker, GLAttributeList.VEC2F, GeometryType.QUADS_AS_LINES_WIREFRAME, 1024) } } val quadWireframe by lazy { StreamVertexBuilder(this, GLAttributeList.VEC2F, GeometryType.QUADS_AS_LINES_WIREFRAME, 16384) } val matrixStack = Matrix4fStack() val freeType = FreeType() val font = Font(this) inline fun quadWireframe(color: Color = Color.WHITE, lambda: (StreamVertexBuilder) -> Unit) { val builder = quadWireframe builder.begin() lambda.invoke(builder) builder.upload() programs.flat.use() programs.flat.color.set(color) programs.flat.transform.set(matrixStack.last) builder.draw(GL_LINES) } inline fun quadWireframe(value: AABB, color: Color = Color.WHITE, chain: (StreamVertexBuilder) -> Unit = {}) { quadWireframe(color) { it.quad(value.mins.x.toFloat(), value.mins.y.toFloat(), value.maxs.x.toFloat(), value.maxs.y.toFloat()) chain(it) } } val box2dRenderer = Box2DRenderer(this) companion object { private val LOGGER = LogManager.getLogger(GLStateTracker::class.java) private val TRACKERS = ThreadLocal() } }