Skip to content

Commit 81a0c63

Browse files
committed
various performance tweaks (3x faster load time)
1 parent a35ffa0 commit 81a0c63

File tree

12 files changed

+170
-85
lines changed

12 files changed

+170
-85
lines changed

common/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ dependencies {
2323
// Add dependencies on the required Kotlin modules.
2424
implementation("org.reflections:reflections:0.10.2")
2525
implementation("com.github.Edouard127:KDiscordIPC:$discordIPCVersion")
26+
implementation("com.pngencoder:pngencoder:0.15.0")
2627

2728
// Add Kotlin
2829
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion")

common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaEmoji.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ enum class LambdaEmoji(private val zipUrl: String) {
1717
object Loader : Loadable {
1818
override fun load(): String {
1919
entries.forEach(LambdaEmoji::loadGlyphs)
20-
return "Loaded ${entries.size} emoji pools"
20+
return "Loaded ${entries.size} emoji sets"
2121
}
2222
}
2323
}

common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/EmojiGlyphs.kt

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,22 @@ package com.lambda.graphics.renderer.gui.font.glyph
33
import com.google.common.math.IntMath.pow
44
import com.lambda.Lambda.LOG
55
import com.lambda.graphics.texture.MipmapTexture
6+
import com.lambda.http.Method
7+
import com.lambda.http.request
68
import com.lambda.module.modules.client.RenderSettings
7-
import com.lambda.threading.runGameScheduled
8-
import com.lambda.threading.runIO
99
import com.lambda.util.math.Vec2d
1010
import java.awt.Color
1111
import java.awt.Graphics2D
1212
import java.awt.image.BufferedImage
1313
import java.io.File
14-
import java.net.URL
1514
import java.util.zip.ZipFile
1615
import javax.imageio.ImageIO
1716
import kotlin.math.ceil
1817
import kotlin.math.log2
1918
import kotlin.math.sqrt
2019
import kotlin.system.measureTimeMillis
20+
import kotlin.time.Duration.Companion.days
2121

22-
// TODO: AbstractGlyphs to use for both Font & Emoji glyphs?
2322
class EmojiGlyphs(zipUrl: String) {
2423
private val emojiMap = mutableMapOf<String, GlyphInfo>()
2524
private lateinit var fontTexture: MipmapTexture
@@ -29,9 +28,7 @@ class EmojiGlyphs(zipUrl: String) {
2928

3029
init {
3130
runCatching {
32-
val time = measureTimeMillis {
33-
downloadAndProcessZip(zipUrl)
34-
}
31+
val time = measureTimeMillis { downloadAndProcessZip(zipUrl) }
3532
LOG.info("Loaded ${emojiMap.size} emojis in $time ms")
3633
}.onFailure {
3734
LOG.error("Failed to load emojis: ${it.message}", it)
@@ -40,14 +37,20 @@ class EmojiGlyphs(zipUrl: String) {
4037
}
4138

4239
private fun downloadAndProcessZip(zipUrl: String) {
43-
val file = File.createTempFile("emoji", ".zip").apply { deleteOnExit() }
40+
val file = request(zipUrl) {
41+
method(Method.GET)
42+
}.maybeDownload("emojis.zip", maxAge = 30.days)
4443

45-
URL(zipUrl).openStream().use { input ->
46-
file.outputStream().use { output ->
47-
input.copyTo(output)
48-
}
49-
}
44+
fontTexture = MipmapTexture(processZip(file))
45+
}
5046

47+
/**
48+
* Processes the given zip file and loads the emojis into the texture.
49+
*
50+
* @param file The zip file containing the emojis.
51+
* @return The texture containing the emojis.
52+
*/
53+
private fun processZip(file: File): BufferedImage {
5154
ZipFile(file).use { zip ->
5255
val firstImage = ImageIO.read(zip.getInputStream(zip.entries().nextElement()))
5356
val length = zip.size().toDouble()
@@ -90,7 +93,7 @@ class EmojiGlyphs(zipUrl: String) {
9093
}
9194
}
9295

93-
fontTexture = MipmapTexture(image)
96+
return image
9497
}
9598

9699
fun bind() {

common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/FontGlyphs.kt

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.lambda.graphics.renderer.gui.font.glyph
22

3-
import com.lambda.Lambda
3+
import com.lambda.Lambda.LOG
44
import com.lambda.graphics.texture.MipmapTexture
55
import com.lambda.graphics.texture.TextureUtils.getCharImage
66
import com.lambda.module.modules.client.RenderSettings
@@ -13,52 +13,60 @@ import java.awt.image.BufferedImage
1313
import kotlin.math.max
1414
import kotlin.system.measureTimeMillis
1515

16-
class FontGlyphs(font: Font) {
16+
class FontGlyphs(
17+
private val font: Font
18+
) {
1719
private val charMap = Int2ObjectOpenHashMap<GlyphInfo>()
18-
private val fontTexture: MipmapTexture
20+
private lateinit var fontTexture: MipmapTexture
1921

2022
var fontHeight = 0.0; private set
2123

2224
init {
23-
val time = measureTimeMillis {
24-
val image = BufferedImage(TEXTURE_SIZE, TEXTURE_SIZE, BufferedImage.TYPE_INT_ARGB)
25+
runCatching {
26+
val time = measureTimeMillis { processGlyphs() }
27+
LOG.info("Font ${font.fontName} loaded with ${charMap.size} characters in $time ms")
28+
}.onFailure {
29+
LOG.error("Failed to load font glyphs: ${it.message}", it)
30+
fontTexture = MipmapTexture(BufferedImage(1024, 1024, BufferedImage.TYPE_INT_ARGB))
31+
}
32+
}
2533

26-
val graphics = image.graphics as Graphics2D
27-
graphics.background = Color(0, 0, 0, 0)
34+
private fun processGlyphs() {
35+
val image = BufferedImage(TEXTURE_SIZE, TEXTURE_SIZE, BufferedImage.TYPE_INT_ARGB)
2836

29-
var x = 0
30-
var y = 0
31-
var rowHeight = 0
37+
val graphics = image.graphics as Graphics2D
38+
graphics.background = Color(0, 0, 0, 0)
3239

33-
(Char.MIN_VALUE..<CHAR_AMOUNT.toChar()).forEach { char ->
34-
val charImage = getCharImage(font, char) ?: return@forEach
40+
var x = 0
41+
var y = 0
42+
var rowHeight = 0
3543

36-
rowHeight = max(rowHeight, charImage.height + STEP)
44+
(Char.MIN_VALUE..<CHAR_AMOUNT.toChar()).forEach { char ->
45+
val charImage = getCharImage(font, char) ?: return@forEach
3746

38-
if (x + charImage.width >= TEXTURE_SIZE) {
39-
y += rowHeight
40-
x = 0
41-
rowHeight = 0
42-
}
47+
rowHeight = max(rowHeight, charImage.height + STEP)
4348

44-
check(y + charImage.height <= TEXTURE_SIZE) { "Can't load font glyphs. Texture size is too small" }
49+
if (x + charImage.width >= TEXTURE_SIZE) {
50+
y += rowHeight
51+
x = 0
52+
rowHeight = 0
53+
}
4554

46-
graphics.drawImage(charImage, x, y, null)
55+
check(y + charImage.height <= TEXTURE_SIZE) { "Can't load font glyphs. Texture size is too small" }
4756

48-
val size = Vec2d(charImage.width, charImage.height)
49-
val uv1 = Vec2d(x, y) * ONE_TEXEL_SIZE
50-
val uv2 = Vec2d(x, y).plus(size) * ONE_TEXEL_SIZE
57+
graphics.drawImage(charImage, x, y, null)
5158

52-
charMap[char.code] = GlyphInfo(size, uv1, uv2)
53-
fontHeight = max(fontHeight, size.y)
59+
val size = Vec2d(charImage.width, charImage.height)
60+
val uv1 = Vec2d(x, y) * ONE_TEXEL_SIZE
61+
val uv2 = Vec2d(x, y).plus(size) * ONE_TEXEL_SIZE
5462

55-
x += charImage.width + STEP
56-
}
63+
charMap[char.code] = GlyphInfo(size, uv1, uv2)
64+
fontHeight = max(fontHeight, size.y)
5765

58-
fontTexture = MipmapTexture(image)
66+
x += charImage.width + STEP
5967
}
6068

61-
Lambda.LOG.info("Font ${font.fontName} loaded with ${charMap.size} characters in $time ms")
69+
fontTexture = MipmapTexture(image)
6270
}
6371

6472
fun bind() {

common/src/main/kotlin/com/lambda/graphics/texture/MipmapTexture.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,13 @@ class MipmapTexture(image: BufferedImage, levels: Int = 4) : Texture() {
3636
}
3737

3838
companion object {
39+
/**
40+
* Retrieves an image from the resources folder and generates a mipmap texture.
41+
*
42+
* @param path The path to the image.
43+
* @param levels The number of mipmap levels.
44+
*/
3945
fun fromResource(path: String, levels: Int = 4): MipmapTexture =
4046
MipmapTexture(ImageIO.read(LambdaResource(path).stream), levels)
4147
}
42-
}
48+
}

common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
package com.lambda.graphics.texture
22

3+
import com.lambda.module.modules.client.RenderSettings
34
import com.mojang.blaze3d.systems.RenderSystem
5+
import com.pngencoder.PngEncoder
46
import net.minecraft.client.texture.NativeImage
57
import org.lwjgl.BufferUtils
6-
import org.lwjgl.opengl.GL13C.*
8+
import org.lwjgl.opengl.GL45C.*
79
import java.awt.*
810
import java.awt.image.BufferedImage
911
import java.io.ByteArrayOutputStream
10-
import javax.imageio.ImageIO
1112
import kotlin.math.roundToInt
1213
import kotlin.math.sqrt
1314

1415
object TextureUtils {
1516
private val metricCache = mutableMapOf<Font, FontMetrics>()
16-
17+
private val encoderPreset = PngEncoder()
18+
.withCompressionLevel(RenderSettings.textureCompression)
19+
.withMultiThreadedCompressionEnabled(RenderSettings.threadedCompression)
1720

1821
fun bindTexture(id: Int, slot: Int = 0) {
1922
RenderSystem.activeTexture(GL_TEXTURE0 + slot)
@@ -35,21 +38,6 @@ object TextureUtils {
3538
// Array of floats normalized to [0.0, 1.0] -> [R, G, B, A]
3639
glTexImage2D(GL_TEXTURE_2D, lod, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, readImage(bufferedImage))
3740

38-
// I'd also like to use glTexSubImage2D, but we have an issue where the function
39-
// would return an error about an invalid texture format.
40-
//
41-
// It would allow us to upload texture data asynchronously and is more efficient
42-
// from testing we gain approximately 20% runtime performance.
43-
// If someone with advanced OpenGL knowledge could help us out, that would be great.
44-
// (Very unlikely to happen, but I can hope)
45-
//
46-
// I've also read online that glTexStorage2D can be used for the same purpose as
47-
// glTexImage2D with NULL data.
48-
// However, some users may have ancient hardware that does not support this function.
49-
// as it was implemented in OpenGL 4.2 and ES 3.0.
50-
//
51-
// glTexSubImage2D(GL_TEXTURE_2D, lod, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, readImage(bufferedImage))
52-
5341
setupTexture(GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR)
5442
}
5543

@@ -73,10 +61,10 @@ object TextureUtils {
7361
}
7462

7563
private fun readImage(bufferedImage: BufferedImage): Long {
76-
val stream = ByteArrayOutputStream()
77-
ImageIO.write(bufferedImage, "png", stream)
64+
val bytes = encoderPreset
65+
.withBufferedImage(bufferedImage)
66+
.toBytes()
7867

79-
val bytes = stream.toByteArray()
8068
val buffer = BufferUtils
8169
.createByteBuffer(bytes.size)
8270
.put(bytes)

common/src/main/kotlin/com/lambda/http/Request.kt

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ package com.lambda.http
22

33
import com.lambda.Lambda
44
import com.lambda.util.FolderRegister.cache
5+
import com.lambda.util.FolderRegister.createFileIfNotExists
6+
import com.lambda.util.FolderRegister.createIfNotExists
57
import java.io.File
8+
import java.io.OutputStream
69
import java.net.HttpURLConnection
710
import java.net.URL
8-
import java.time.Instant
911
import kotlin.time.Duration
1012
import kotlin.time.Duration.Companion.days
1113

@@ -33,14 +35,21 @@ data class Request(
3335
/**
3436
* Downloads the resource at the specified path and caches it for future use.
3537
*
36-
* @param path The path to the resource.
38+
* @param name The full name of the file to be cached.
3739
* @param maxAge The maximum age of the cached resource. Default is 4 days.
40+
*
41+
* @return A pair containing the cached file and a boolean indicating whether the file was downloaded.
3842
*/
39-
fun maybeDownload(path: String, maxAge: Duration = 4.days): ByteArray {
40-
val file = File("${cache}/${path.substringAfterLast("/").hashCode()}")
43+
fun maybeDownload(
44+
name: String,
45+
maxAge: Duration = 7.days,
46+
): File {
47+
val (file, wasCreated) = createFileIfNotExists(name, cache, true)
4148

42-
if (file.exists() && Instant.now().toEpochMilli() - file.lastModified() < maxAge.inWholeMilliseconds)
43-
return file.readBytes()
49+
if (System.currentTimeMillis() - file.lastModified() < maxAge.inWholeMilliseconds
50+
&& file.length() > 0
51+
&& !wasCreated)
52+
return file
4453

4554
file.writeText("") // Clear the file before writing to it.
4655

@@ -64,7 +73,7 @@ data class Request(
6473
}
6574
}
6675

67-
return file.readBytes()
76+
return file
6877
}
6978

7079
/**

common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ object RenderSettings : Module(
1818
val baselineOffset by setting("Vertical Offset", 0.0, -10.0..10.0, 0.5) { page == Page.Font }
1919
private val lodBiasSetting by setting("Smoothing", 0.0, -10.0..10.0, 0.5) { page == Page.Font }
2020

21+
// Texture
22+
val textureCompression by setting("Compression", 1, 1..9, 1, description = "Texture compression level, higher is slower") { page == Page.TEXTURE }
23+
val threadedCompression by setting("Threaded Compression", false, description = "Use multiple threads for texture compression") { page == Page.TEXTURE }
24+
2125
// ESP
2226
val uploadsPerTick by setting("Uploads", 16, 1..256, 1, unit = " chunk/tick") { page == Page.ESP }
2327
val rebuildsPerTick by setting("Rebuilds", 64, 1..256, 1, unit = " chunk/tick") { page == Page.ESP }
@@ -28,6 +32,7 @@ object RenderSettings : Module(
2832

2933
private enum class Page {
3034
Font,
31-
ESP
35+
TEXTURE,
36+
ESP,
3237
}
3338
}

0 commit comments

Comments
 (0)