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") { 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 = Caffeine.newBuilder() .softValues() - .expireAfterAccess(Duration.ofMinutes(10)) - .build() + .expireAfterAccess(Duration.ofMinutes(20)) + .weigher { 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 = "") = GLTexture2D(this, name) - private val named2DTextures = HashMap() + /** + * Так как текстуры не занимают память в куче JVM, а только видеопамять, то + * такой кеш довольно хрупкий + */ + private val named2DTextures: Cache = 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 = "") : 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 }