611 lines
16 KiB
Kotlin
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
|
|
}
|
|
}
|
|
}
|
|
}
|