From 0910b093f895c66d6c5cb2d5fdf39c37aa1f638e Mon Sep 17 00:00:00 2001
From: DBotThePony <dbotthepony@yandex.ru>
Date: Tue, 28 Mar 2023 22:33:54 +0700
Subject: [PATCH] =?UTF-8?q?Caffeine=20=D0=BA=D0=B0=D0=BA=20=D0=B1=D0=B8?=
 =?UTF-8?q?=D0=B1=D0=BB=D0=B8=D0=BE=D1=82=D0=B5=D0=BA=D0=B0=20=D0=B4=D0=BB?=
 =?UTF-8?q?=D1=8F=20=D0=BA=D0=B5=D1=88=D0=B5=D0=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 build.gradle.kts                              |  2 +
 .../ru/dbotthepony/kstarbound/Starbound.kt    | 30 +++++++--
 .../kstarbound/client/gl/GLStateTracker.kt    | 63 +++++++++++++------
 .../kstarbound/client/gl/GLTexture.kt         |  4 ++
 .../ru/dbotthepony/kstarbound/lua/LuaState.kt |  2 +-
 5 files changed, 77 insertions(+), 24 deletions(-)

diff --git a/build.gradle.kts b/build.gradle.kts
index 480c493b..c63a9d1c 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -82,6 +82,8 @@ dependencies {
 
 	implementation("ru.dbotthepony:kbox2d:2.4.1.+")
 	implementation("ru.dbotthepony:kvector:1.3.2")
+
+	implementation("com.github.ben-manes.caffeine:caffeine:3.1.5")
 }
 
 tasks.getByName<Test>("test") {
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt
index b8fe0cab..67660d32 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt
@@ -1,5 +1,7 @@
 package ru.dbotthepony.kstarbound
 
+import com.github.benmanes.caffeine.cache.Cache
+import com.github.benmanes.caffeine.cache.Caffeine
 import com.google.common.cache.CacheBuilder
 import com.google.common.collect.Interner
 import com.google.common.collect.Interners
@@ -70,9 +72,11 @@ import ru.dbotthepony.kstarbound.util.WriteOnce
 import ru.dbotthepony.kstarbound.util.traverseJsonPath
 import ru.dbotthepony.kvector.vector.nint.Vector2i
 import java.io.*
+import java.lang.ref.WeakReference
 import java.text.DateFormat
 import java.time.Duration
 import java.util.*
+import java.util.concurrent.locks.LockSupport
 import java.util.function.BiConsumer
 import java.util.function.BinaryOperator
 import java.util.function.Function
@@ -223,11 +227,29 @@ class Starbound : ISBFileLocator {
 
 	val atlasRegistry = AtlasConfiguration.Registry(this, pathStack, gson)
 
-	private val imageCache = CacheBuilder.newBuilder()
-		.concurrencyLevel(8)
+	private val imageCache: Cache<String, ImageData> = Caffeine.newBuilder()
 		.softValues()
-		.expireAfterAccess(Duration.ofMinutes(10))
-		.build<String, ImageData>()
+		.expireAfterAccess(Duration.ofMinutes(20))
+		.weigher<String, ImageData> { key, value -> value.data.capacity() }
+		.maximumWeight(1_024L * 1_024L * 256L /* 256 МиБ */)
+		.build()
+
+	init {
+		val ref = WeakReference(imageCache)
+
+		val worker = Runnable {
+			while (true) {
+				val get = ref.get() ?: break
+				get.cleanUp()
+				LockSupport.parkNanos(1_000_000_000L)
+			}
+		}
+
+		Thread(worker, "Image Data Cache Cleaner for $this").also {
+			it.isDaemon = true
+			it.start()
+		}
+	}
 
 	fun item(name: String): ItemStack {
 		return ItemStack(items[name] ?: return ItemStack.EMPTY)
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt
index 7845e179..5ef72df3 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt
@@ -1,5 +1,7 @@
 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.*
@@ -23,8 +25,11 @@ import ru.dbotthepony.kvector.util2d.AABB
 import ru.dbotthepony.kvector.vector.Color
 import java.io.File
 import java.lang.ref.Cleaner
+import java.lang.ref.WeakReference
+import java.time.Duration
 import java.util.*
 import java.util.concurrent.ThreadFactory
+import java.util.concurrent.locks.LockSupport
 import kotlin.collections.ArrayList
 import kotlin.collections.HashMap
 import kotlin.math.roundToInt
@@ -127,7 +132,6 @@ class GLStateTracker(val client: StarboundClient) {
 	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
@@ -161,13 +165,11 @@ class GLStateTracker(val client: StarboundClient) {
 	var gcHits = 0L
 		private set
 
-	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
-		}
-	})
+	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) {
@@ -433,25 +435,48 @@ class GLStateTracker(val client: StarboundClient) {
 	fun newVAO() = VertexArrayObject(this)
 	fun newTexture(name: String = "<unknown>") = GLTexture2D(this, name)
 
-	private val named2DTextures = HashMap<String, GLTexture2D>()
+	/**
+	 * Так как текстуры не занимают память в куче JVM, а только видеопамять, то
+	 * такой кеш довольно хрупкий
+	 */
+	private val named2DTextures: Cache<String, GLTexture2D> = Caffeine.newBuilder()
+		.softValues()
+		.expireAfterAccess(Duration.ofMinutes(5))
+		.build()
+
+	init {
+		val ref = WeakReference(named2DTextures)
+
+		val worker = Runnable {
+			while (true) {
+				val get = ref.get() ?: break
+				get.cleanUp()
+				LockSupport.parkNanos(1_000_000_000L)
+			}
+		}
+
+		Thread(worker, "OpenGL Texture Cache Cleaner for $this").also {
+			it.isDaemon = true
+			it.start()
+		}
+	}
+
+	private val missingTexture: GLTexture2D by lazy {
+		newTexture(missingTexturePath).upload(client.starbound.readDirect(missingTexturePath), GL_RGBA, GL_RGBA).generateMips().also {
+			it.textureMinFilter = GL_NEAREST
+			it.textureMagFilter = GL_NEAREST
+		}
+	}
 
-	private var missingTexture: GLTexture2D? = null
 	private val missingTexturePath = "/assetmissing.png"
 
 	fun loadTexture(path: String): GLTexture2D {
 		ensureSameThread()
 
-		if (missingTexture == null) {
-			missingTexture = newTexture(missingTexturePath).upload(client.starbound.readDirect(missingTexturePath), GL_RGBA, GL_RGBA).generateMips().also {
-				it.textureMinFilter = GL_NEAREST
-				it.textureMagFilter = GL_NEAREST
-			}
-		}
-
-		return named2DTextures.computeIfAbsent(path) {
+		return named2DTextures.get(path) {
 			if (!client.starbound.exists(path)) {
 				LOGGER.error("Texture {} is missing! Falling back to {}", path, missingTexturePath)
-				missingTexture!!
+				missingTexture
 			} else {
 				newTexture(path).upload(client.starbound.imageData(path)).generateMips().also {
 					it.textureMinFilter = GL_NEAREST
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLTexture.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLTexture.kt
index ba58fb8b..09086ef7 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLTexture.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLTexture.kt
@@ -30,6 +30,10 @@ private class GLTexturePropertyTracker(private val flag: Int, private var value:
 
 @Suppress("SameParameterValue")
 class GLTexture2D(val state: GLStateTracker, val name: String = "<unknown>") : AutoCloseable {
+	init {
+		state.ensureSameThread()
+	}
+
 	val pointer = glGenTextures()
 
 	init {
diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaState.kt
index 61774069..a7b557f1 100644
--- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaState.kt
+++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaState.kt
@@ -1038,7 +1038,7 @@ class LuaState private constructor(private val pointer: Pointer, val stringInter
 		private val LOGGER = LogManager.getLogger()
 
 		private val CLEANER: Cleaner = Cleaner.create {
-			val thread = Thread(it, "Lua cleaner")
+			val thread = Thread(it, "Lua State Cleaner")
 			thread.priority = 1
 			thread
 		}