Skip to content

Commit

Permalink
feat: cache + fabric support
Browse files Browse the repository at this point in the history
  • Loading branch information
Zxnii committed Jul 31, 2024
1 parent 8e7f1a4 commit 9e33d7e
Show file tree
Hide file tree
Showing 11 changed files with 355 additions and 84 deletions.
4 changes: 3 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[versions]
kotlin = "1.9.10"
kotlinx-coroutines = "1.8.1"
kotlinx-serialization = "1.6.2"
lwjgl = "3.3.3"
asm = "5.0.3"
Expand All @@ -25,6 +26,7 @@ kotlin-common = { module = "org.jetbrains.kotlin:kotlin-stdlib-common", version.
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" }

kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }

annotations = { module = "org.jetbrains:annotations", version.ref = "annotations" }
Expand All @@ -38,4 +40,4 @@ pgt-main = { id = "org.polyfrost.multi-version", version.ref = "pgt" }
pgt-root = { id = "org.polyfrost.multi-version.root", version.ref = "pgt" }
pgt-defaults-repo = { id = "org.polyfrost.defaults.repo", version.ref = "pgt" }
pgt-defaults-java = { id = "org.polyfrost.defaults.java", version.ref = "pgt" }
pgt-defaults-loom = { id = "org.polyfrost.defaults.loom", version.ref = "pgt" }
pgt-defaults-loom = { id = "org.polyfrost.defaults.loom", version.ref = "pgt" }
2 changes: 2 additions & 0 deletions modules/core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ dependencies {
compileOnly(project(":modules:lwjgl"))

compileOnly(rootProject.libs.asmtree)
compileOnly(rootProject.libs.mixins)
compileOnly(rootProject.libs.bundles.lwjgl)

implementation(rootProject.libs.kotlinx.coroutines)
implementation(rootProject.libs.kotlinx.serialization.json)
}

Expand Down
5 changes: 5 additions & 0 deletions modules/core/src/main/kotlin/org/polyfrost/spice/Constants.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.polyfrost.spice

import kotlin.io.path.Path

val spiceDirectory = Path("spice").toAbsolutePath()
19 changes: 10 additions & 9 deletions modules/core/src/main/kotlin/org/polyfrost/spice/Spice.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.apache.logging.log4j.LogManager
import org.lwjgl.Version
import org.lwjgl.glfw.GLFW.glfwGetVersion
import org.lwjgl.glfw.GLFW.glfwRawMouseMotionSupported
import org.lwjgl.glfw.GLFW.*
import org.lwjgl.openal.AL10.AL_VERSION
import org.lwjgl.openal.AL10.alGetString
import org.lwjgl.system.Configuration.GLFW_CHECK_THREAD0
Expand All @@ -15,7 +14,10 @@ import org.polyfrost.spice.debug.DebugSection
import org.polyfrost.spice.platform.api.Platform
import org.polyfrost.spice.util.isMac
import org.polyfrost.spice.util.isOptifineLoaded
import kotlin.io.path.*
import kotlin.io.path.createDirectories
import kotlin.io.path.exists
import kotlin.io.path.readText
import kotlin.io.path.writeText

object Spice {
@JvmStatic
Expand Down Expand Up @@ -59,8 +61,7 @@ object Spice {
lateinit var openalVersion: String
private set

private val configDirectory = Path("spice").toAbsolutePath()
private val configFile = configDirectory.resolve("config.json")
private val configFile = spiceDirectory.resolve("config.json")
private val json = Json { ignoreUnknownKeys = true }

@JvmStatic
Expand All @@ -74,7 +75,7 @@ object Spice {
})

if (isMac()) GLFW_CHECK_THREAD0.set(false)

if (!glfwInit()) throw RuntimeException("Failed to initialize GLFW")
if (isOptifineLoaded()) logger.warn("OptiFine is enabled! No performance patches will be applied.")

// todo: store in jar and load
Expand All @@ -92,15 +93,15 @@ object Spice {
}

logger.info("Spice Version: $version")
logger.info("Platform: $platform")
logger.info("Platform: ${platform.id}")

initializeDebugSections()
}

@JvmStatic
fun saveOptions() {
if (!configDirectory.exists()) {
configDirectory.createDirectories()
if (!spiceDirectory.exists()) {
spiceDirectory.createDirectories()
}

if (!options.needsSave) return
Expand Down
130 changes: 130 additions & 0 deletions modules/core/src/main/kotlin/org/polyfrost/spice/patcher/Cache.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package org.polyfrost.spice.patcher

import kotlinx.coroutines.coroutineScope
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter.COMPUTE_FRAMES
import org.objectweb.asm.tree.ClassNode
import org.polyfrost.spice.patcher.lwjgl.LibraryTransformer
import org.polyfrost.spice.patcher.lwjgl.LwjglTransformer
import org.polyfrost.spice.spiceDirectory
import org.spongepowered.asm.transformers.MixinClassWriter
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry
import kotlin.io.path.createDirectories
import kotlin.io.path.exists
import kotlin.io.path.outputStream

private val provider by lazy { LwjglTransformer.provider }
private val cacheDirectory = spiceDirectory.resolve(".cache")

fun currentHash(): String = provider.hash
fun isCached(hash: String): Boolean = cachePath(hash).exists()

fun loadCache(hash: String): Map<String, ClassNode> =
loadCacheBuffers(hash).mapValues { (_, buffer) ->
ClassNode()
.also { ClassReader(buffer).accept(it, 0) }
}

fun loadCacheBuffers(hash: String): Map<String, ByteArray> {
val path = cachePath(hash)
val jar = JarFile(path.toFile())

val manifest = Json.decodeFromString<CacheManifest>(
jar
.getInputStream(jar.getEntry("cache-manifest.json"))
.use {
it
.readBytes()
.toString(Charsets.UTF_8)
})

return manifest.transformable
.associateWith { transformable ->
jar
.getInputStream(jar.getEntry("$transformable.class"))
.use { it.readBytes() }
}
}

suspend fun buildCache(hash: String, `in`: List<ClassNode>): Map<String, ClassNode> {
// todo: abuse coroutines.
return coroutineScope {
val transformers = arrayOf(
LwjglTransformer,
LibraryTransformer
)

val transformable = mutableSetOf<String>()
val provider = LwjglTransformer.provider

val transformed = mutableMapOf<String, ClassNode>()
val buffers = mutableMapOf<String, ByteArray>()

`in`.forEach { node ->
transformable.add(node.name)

transformers.forEach transform@{ transformer ->
val targets = transformer.getClassNames()

if (targets != null
&& !targets.contains(node.name.replace("/", "."))
) return@transform

transformer.transform(node)
}

transformed[node.name] = node
buffers["${node.name}.class"] =
MixinClassWriter(COMPUTE_FRAMES)
.also { node.accept(it) }
.toByteArray()
}

provider.allEntries.forEach { entry ->
if (!entry.endsWith(".class") || !entry.startsWith("org/lwjgl/")) return@forEach

if (!buffers.contains(entry)) {
buffers[entry] =
provider.readFile(entry) ?: return@forEach
}
}

JarOutputStream(cachePath(hash).outputStream())
.use { out ->
out.putNextEntry(ZipEntry("cache-manifest.json"))
out.write(
Json.encodeToString<CacheManifest>(
CacheManifest(
transformable.toList()
)
).toByteArray(Charsets.UTF_8)
)
out.closeEntry()

buffers.forEach { (name, buffer) ->
out.putNextEntry(ZipEntry(name))
out.write(buffer)
out.closeEntry()
}

out.finish()
}

transformed
}
}

private fun cachePath(hash: String) =
cacheDirectory
.createDirectories()
.resolve("$hash.jar")

@Serializable
private data class CacheManifest(
val transformable: List<String>
)
Original file line number Diff line number Diff line change
@@ -1,26 +1,37 @@
package org.polyfrost.spice.patcher.lwjgl

import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.objectweb.asm.ClassReader
import org.objectweb.asm.tree.ClassNode
import org.polyfrost.spice.util.UrlByteArrayConnection
import java.io.InputStream
import java.net.URL
import java.net.URLConnection
import java.net.URLStreamHandler
import java.security.MessageDigest
import java.util.jar.JarInputStream

class LwjglProvider {
private val cacheLock = Mutex()
private val fileCache = mutableMapOf<String, ByteArray>()
private val jar by lazy {
JarInputStream(
LwjglProvider::class.java
.classLoader
.getResource("lwjgl.jar")
?.openStream() ?: return@lazy null
)
}

private val jar by lazy { JarInputStream(openStream() ?: return@lazy null) }

private var closed = false

@OptIn(ExperimentalStdlibApi::class)
val hash by lazy {
openStream()?.use {
val digest = MessageDigest.getInstance("SHA-1")

digest
.digest(it.readBytes())
.toHexString()
} ?: "0"
}

val url = URL("spice", "", -1, "/", object : URLStreamHandler() {
override fun openConnection(url: URL): URLConnection? {
return UrlByteArrayConnection(
Expand All @@ -30,46 +41,76 @@ class LwjglProvider {
}
})

val allEntries: Collection<String> by lazy {
if (closed) fileCache.keys
else {
readEntryUntil(null)

fileCache.keys
}
}

fun readFile(path: String): ByteArray? {
if (fileCache.contains(path)) return fileCache[path]!!
if (closed || jar == null) return null

while (true) {
val entry = jar!!.nextEntry ?: run {
jar!!.close()
closed = true
return readEntryUntil(path)
}

return null
}
fun getClassNode(name: String): ClassNode? {
val buffer = readFile("$name.class") ?: return null
val classNode = ClassNode()

ClassReader(buffer).accept(classNode, 0)

if (entry.isDirectory) continue
return classNode
}

val length = entry.size.toInt()
val entryBuffer = ByteArray(length)
private fun openStream(): InputStream? =
LwjglProvider::class.java
.classLoader
.getResource("lwjgl.jar")
?.openStream()

var offset = 0
private fun readEntryUntil(path: String?): ByteArray? {
return runBlocking {
if (closed || jar == null) return@runBlocking null

while (true) {
val read = jar!!.read(entryBuffer, offset, length - offset)
val entry = jar!!.nextEntry ?: run {
jar!!.close()
closed = true

offset += read
return@runBlocking null
}

if (offset == length) break
}
if (entry.isDirectory) continue

jar!!.closeEntry()
fileCache[entry.name] = entryBuffer
val length = entry.size.toInt()
val entryBuffer = ByteArray(length)

if (entry.name == path) return entryBuffer
}
}
var offset = 0

fun getClassNode(name: String): ClassNode? {
val buffer = readFile("$name.class") ?: return null
val classNode = ClassNode()
while (true) {
val read = jar!!.read(entryBuffer, offset, length - offset)

ClassReader(buffer).accept(classNode, 0)
offset += read

return classNode
if (offset == length) break
}

jar!!.closeEntry()

cacheLock.withLock {
fileCache[entry.name] = entryBuffer
}

if (path != null && entry.name == path) return@runBlocking entryBuffer
}

// compiler isn't smart enough
@Suppress("UNREACHABLE_CODE")
null
}
}
}
}
Loading

0 comments on commit 9e33d7e

Please sign in to comment.