KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt

611 lines
16 KiB
Kotlin

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<T>(private var value: T, private val callback: (T) -> Unit) : ReadWriteProperty<GLStateTracker, T> {
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<GLStateTracker, GLTexture2D?> {
private val values = arrayOfNulls<GLTexture2D>(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<ScissorRect>()
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<IStruct4f>(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 = "<unknown>") = GLTexture2D(this, name)
// минимальное время хранения 5 минут и...
private val named2DTextures0: Cache<String, GLTexture2D> = Caffeine.newBuilder()
.expireAfterAccess(Duration.ofMinutes(5))
.build()
// ...бесконечное хранение пока кто-то все ещё использует текстуру
private val named2DTextures1: Cache<String, GLTexture2D> = 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<GLStateTracker>()
private fun readInternal(file: String): String {
return ClassLoader.getSystemClassLoader().getResourceAsStream(file)!!.bufferedReader()
.let {
val read = it.readText()
it.close()
read
}
}
}
}