KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt
2022-09-11 01:10:40 +07:00

473 lines
13 KiB
Kotlin

package ru.dbotthepony.kstarbound.client.gl
import org.apache.logging.log4j.LogManager
import org.lwjgl.opengl.GL
import org.lwjgl.opengl.GL46.*
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.client.freetype.FreeType
import ru.dbotthepony.kstarbound.client.gl.vertex.StreamVertexBuilder
import ru.dbotthepony.kstarbound.client.gl.vertex.VertexType
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.matrix.nfloat.Matrix4f
import ru.dbotthepony.kvector.util2d.AABB
import ru.dbotthepony.kvector.vector.Color
import java.io.FileNotFoundException
import java.lang.ref.Cleaner
import java.util.concurrent.ThreadFactory
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<T>(private var value: T, private val lambda: (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
lambda.invoke(value)
checkForGLError()
this.value = value
}
}
open class GLTransformableProgram(state: GLStateTracker, vararg shaders: GLShader) : GLShaderProgram(state, *shaders) {
init {
link()
}
val transform = this["_transform"]!!
init {
transform.set(Matrix4f.IDENTITY)
}
}
open class GLTransformableColorableProgram(state: GLStateTracker, vararg shaders: GLShader) : GLTransformableProgram(state, *shaders) {
val color = this["_color"]!!
init {
color.set(Color.WHITE)
}
}
interface GLCleanable : Cleaner.Cleanable {
/**
* Выставляет флаг на то, что объект был удалён вручную и вызывает clean()
*/
fun cleanManual(): Unit
}
interface GLStreamBuilderList {
val small: StreamVertexBuilder
}
class GLStateTracker {
init {
// 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 var 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 {} got leaked.", name, nativeRef)
cleanerHits.add {
fn(nativeRef)
checkForGLError()
}
}
return object : GLCleanable {
override fun cleanManual() {
cleanManual = true
clean()
}
override fun clean() = cleanable.clean()
}
}
fun cleanup() {
val copy = cleanerHits
cleanerHits = ArrayList()
for (lambda in copy) {
lambda.invoke()
}
}
var blend by GLStateSwitchTracker(GL_BLEND)
var depthTest by GLStateSwitchTracker(GL_DEPTH_TEST)
var VBO: GLVertexBufferObject? = null
set(value) {
ensureSameThread()
if (field === value) return
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: GLVertexBufferObject? = null
set(value) {
ensureSameThread()
if (field === value) return
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: GLVertexArrayObject? = null
set(value) {
ensureSameThread()
if (field === value) return
field = value
if (value == null) {
glBindVertexArray(0)
checkForGLError()
return
}
glBindVertexArray(value.pointer)
checkForGLError()
}
var program: GLShaderProgram? = null
private set
var texture2D: GLTexture2D? = null
set(value) {
ensureSameThread()
if (field === value) return
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<IStruct4f>(Color.WHITE) {
val (r, g, b, a) = it
glClearColor(r, g, b, a)
}
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): GLVertexBufferObject {
return GLVertexBufferObject(this, type)
}
fun newEBO() = newVBO(VBOType.ELEMENT_ARRAY)
fun newVAO() = GLVertexArrayObject(this)
fun newTexture(name: String = "<unknown>") = GLTexture2D(this, name)
private val named2DTextures = HashMap<String, GLTexture2D>()
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: GLVertexBufferObject): GLVertexBufferObject {
if (obj.type == VBOType.ARRAY)
VBO = obj
else
EBO = obj
return obj
}
fun unbind(obj: GLVertexBufferObject): GLVertexBufferObject {
if (obj.type == VBOType.ARRAY)
if (obj == VBO)
VBO = null
else
if (obj == EBO)
EBO = null
return obj
}
fun bind(obj: GLVertexArrayObject): GLVertexArrayObject {
VAO = obj
return obj
}
fun unbind(obj: GLVertexArrayObject): GLVertexArrayObject {
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
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 flat2DLines = object : GLStreamBuilderList {
override val small by lazy {
return@lazy StreamVertexBuilder(this@GLStateTracker, GLAttributeList.VEC2F, VertexType.LINES, 1024)
}
}
val flat2DTriangles = object : GLStreamBuilderList {
override val small by lazy {
return@lazy StreamVertexBuilder(this@GLStateTracker, GLAttributeList.VEC2F, VertexType.TRIANGLES, 1024)
}
}
val flat2DQuads = object : GLStreamBuilderList {
override val small by lazy {
return@lazy StreamVertexBuilder(this@GLStateTracker, GLAttributeList.VEC2F, VertexType.QUADS, 1024)
}
}
val flat2DTexturedQuads = object : GLStreamBuilderList {
override val small by lazy {
return@lazy StreamVertexBuilder(this@GLStateTracker, GLAttributeList.VERTEX_TEXTURE, VertexType.QUADS, 1024)
}
}
val flat2DQuadLines = object : GLStreamBuilderList {
override val small by lazy {
return@lazy StreamVertexBuilder(this@GLStateTracker, GLAttributeList.VEC2F, VertexType.QUADS_AS_LINES, 1024)
}
}
val flat2DQuadWireframe = object : GLStreamBuilderList {
override val small by lazy {
return@lazy StreamVertexBuilder(this@GLStateTracker, GLAttributeList.VEC2F, VertexType.QUADS_AS_LINES_WIREFRAME, 1024)
}
}
val matrixStack = Matrix4fStack()
val freeType = FreeType()
val font = Font(this)
inline fun quadWireframe(color: Color = Color.WHITE, lambda: (StreamVertexBuilder) -> Unit) {
val builder = flat2DQuadWireframe.small
builder.begin()
lambda.invoke(builder)
builder.upload()
flatProgram.use()
flatProgram.color.set(color)
flatProgram.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)
}
}