KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt
DBotThePony e8eed40a73
object oriented file system skeleton
so there at least some preparation for java nio filesystem
2022-11-24 15:30:47 +07:00

634 lines
16 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.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.concurrent.ThreadFactory
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
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("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 MULTIPLY_WITH_ALPHA = BlendFunc(Func.SRC_ALPHA, Func.ONE_MINUS_SRC_ALPHA)
val ADDITIVE = BlendFunc(Func.ONE, Func.ONE)
val ONLY_ALPHA = BlendFunc(
Func.ZERO,
Func.ONE,
Func.ONE,
Func.ZERO
)
val ONLY_BLEND_ALPHA = BlendFunc(
Func.ZERO,
Func.ONE,
Func.ONE,
Func.ONE
)
val ONLY_COLOR = BlendFunc(
Func.ONE,
Func.ZERO,
Func.ZERO,
Func.ONE
)
val MULTIPLY_BY_SRC = BlendFunc(
Func.ZERO,
Func.SRC_COLOR,
Func.ONE,
Func.ZERO
)
}
}
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.isTraceEnabled)
LOGGER.trace("{} with ID {} was GC'd by JVM, without manually calling close()", name, nativeRef)
if (isSameThread()) {
fn(nativeRef)
checkForGLError()
} else {
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 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) 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 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 texture2D: GLTexture2D? by TexturesTracker(80)
var clearColor by GLStateGenericTracker<IStruct4f>(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 = "<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.exists(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.exists(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.exists(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.exists(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 flat2DTexturedQuads = object : GLStreamBuilderList {
override val small by lazy {
return@lazy StreamVertexBuilder(this@GLStateTracker, GLAttributeList.VERTEX_TEXTURE, GeometryType.QUADS, 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 quadColor(lambda: (StreamVertexBuilder) -> Unit) {
val builder = programs.flatColor.builder
builder.begin()
lambda.invoke(builder)
builder.upload()
programs.flatColor.use()
programs.flatColor.transform.set(matrixStack.last)
builder.draw(GL_TRIANGLES)
}
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<GLStateTracker>()
}
}