From fb867f718afb00589a28494ad8dbbdeeaec9ec13 Mon Sep 17 00:00:00 2001 From: Crosby <32882447+crosby-moe@users.noreply.github.com> Date: Mon, 12 Jan 2026 23:18:21 -0500 Subject: [PATCH 1/6] support the entire utf 16 plane in font rendering --- .../renderer/text/CustomTextRenderer.java | 9 +- .../meteorclient/renderer/text/Font.java | 203 ++++++++++++------ .../meteorclient/systems/hud/HudRenderer.java | 13 +- 3 files changed, 143 insertions(+), 82 deletions(-) diff --git a/src/main/java/meteordevelopment/meteorclient/renderer/text/CustomTextRenderer.java b/src/main/java/meteordevelopment/meteorclient/renderer/text/CustomTextRenderer.java index c31faabc6e..f1242672a1 100644 --- a/src/main/java/meteordevelopment/meteorclient/renderer/text/CustomTextRenderer.java +++ b/src/main/java/meteordevelopment/meteorclient/renderer/text/CustomTextRenderer.java @@ -8,6 +8,7 @@ import meteordevelopment.meteorclient.renderer.MeshBuilder; import meteordevelopment.meteorclient.renderer.MeshRenderer; import meteordevelopment.meteorclient.renderer.MeteorRenderPipelines; +import meteordevelopment.meteorclient.renderer.Texture; import meteordevelopment.meteorclient.utils.Utils; import meteordevelopment.meteorclient.utils.render.color.Color; import net.minecraft.client.MinecraftClient; @@ -38,7 +39,7 @@ public CustomTextRenderer(FontFace fontFace) { fonts = new Font[5]; for (int i = 0; i < fonts.length; i++) { - fonts[i] = new Font(buffer, (int) Math.round(27 * ((i * 0.5) + 1))); + fonts[i] = new Font(fontFace, buffer, (int) Math.round(27 * ((i * 0.5) + 1))); } } @@ -125,11 +126,13 @@ public void end() { if (!scaleOnly) { mesh.end(); + Texture fontAtlas = font.getTexture(); + MeshRenderer.begin() .attachments(MinecraftClient.getInstance().getFramebuffer()) .pipeline(MeteorRenderPipelines.UI_TEXT) .mesh(mesh) - .sampler("u_Texture", font.texture.getGlTextureView(), font.texture.getSampler()) + .sampler("u_Texture", fontAtlas.getGlTextureView(), fontAtlas.getSampler()) .end(); } @@ -139,7 +142,7 @@ public void end() { public void destroy() { for (Font font : this.fonts) { - font.texture.close(); + font.close(); } } } diff --git a/src/main/java/meteordevelopment/meteorclient/renderer/text/Font.java b/src/main/java/meteordevelopment/meteorclient/renderer/text/Font.java index 7886610014..ad95a12927 100644 --- a/src/main/java/meteordevelopment/meteorclient/renderer/text/Font.java +++ b/src/main/java/meteordevelopment/meteorclient/renderer/text/Font.java @@ -7,10 +7,12 @@ import com.mojang.blaze3d.textures.FilterMode; import com.mojang.blaze3d.textures.TextureFormat; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.ints.*; import meteordevelopment.meteorclient.renderer.MeshBuilder; import meteordevelopment.meteorclient.renderer.Texture; +import meteordevelopment.meteorclient.utils.Utils; import meteordevelopment.meteorclient.utils.render.color.Color; +import org.jetbrains.annotations.Nullable; import org.lwjgl.BufferUtils; import org.lwjgl.stb.*; import org.lwjgl.system.MemoryStack; @@ -19,84 +21,125 @@ import java.nio.IntBuffer; public class Font { - public final Texture texture; + private static final int size = 2048; + private final Texture texture; + private final FontFace fontFace; private final int height; private final float scale; private final float ascent; private final Int2ObjectOpenHashMap charMap = new Int2ObjectOpenHashMap<>(); - private static final int size = 2048; + private final Int2IntMap pending = new Int2IntOpenHashMap(); + private @Nullable STBTTFontinfo fontInfo; + private @Nullable ByteBuffer fontBuffer; - public Font(ByteBuffer buffer, int height) { + public Font(FontFace fontFace, ByteBuffer buffer, int height) { + this.fontFace = fontFace; + this.fontBuffer = buffer; this.height = height; // Initialize font - STBTTFontinfo fontInfo = STBTTFontinfo.create(); - STBTruetype.stbtt_InitFont(fontInfo, buffer); + STBTTFontinfo fontInfo = this.getOrCreateFontInfo(); + texture = new Texture(size, size, TextureFormat.RED8, FilterMode.LINEAR, FilterMode.LINEAR); + scale = STBTruetype.stbtt_ScaleForPixelHeight(fontInfo, height); + + // Get font vertical ascent + try (MemoryStack stack = MemoryStack.stackPush()) { + IntBuffer ascent = stack.mallocInt(1); + STBTruetype.stbtt_GetFontVMetrics(fontInfo, ascent, null, null); + this.ascent = ascent.get(0); + } + } + + public Font(FontFace fontFace, int height) { + this(fontFace, readFont(fontFace), height); + } + private void renderAndUploadAtlas() { // Allocate buffers - ByteBuffer bitmap = BufferUtils.createByteBuffer(size * size); - STBTTPackedchar.Buffer[] cdata = { - STBTTPackedchar.create(95), // Basic Latin - STBTTPackedchar.create(96), // Latin 1 Supplement - STBTTPackedchar.create(128), // Latin Extended-A - STBTTPackedchar.create(144), // Greek and Coptic - STBTTPackedchar.create(256), // Cyrillic - STBTTPackedchar.create(1) // infinity symbol - }; + int chars = charMap.size() + pending.size(); - // create and initialise packing context - STBTTPackContext packContext = STBTTPackContext.create(); - STBTruetype.stbtt_PackBegin(packContext, bitmap, size, size, 0 ,1); + STBTTPackRange.Buffer packRange = STBTTPackRange.create(1); + STBTTPackedchar.Buffer packedCharBuffer = STBTTPackedchar.create(chars); // create the pack range, populate with the specific packing ranges - STBTTPackRange.Buffer packRange = STBTTPackRange.create(cdata.length); - packRange.put(STBTTPackRange.create().set(height, 32, null, 95, cdata[0], (byte) 2, (byte) 2)); - packRange.put(STBTTPackRange.create().set(height, 160, null, 96, cdata[1], (byte) 2, (byte) 2)); - packRange.put(STBTTPackRange.create().set(height, 256, null, 128, cdata[2], (byte) 2, (byte) 2)); - packRange.put(STBTTPackRange.create().set(height, 880, null, 144, cdata[3], (byte) 2, (byte) 2)); - packRange.put(STBTTPackRange.create().set(height, 1024, null, 256, cdata[4], (byte) 2, (byte) 2)); - packRange.put(STBTTPackRange.create().set(height, 8734, null, 1, cdata[5], (byte) 2, (byte) 2)); // lol + IntBuffer charBuffer = BufferUtils.createIntBuffer(chars); + for (int c : charMap.keySet()) charBuffer.put(c); + charMap.clear(); + for (int c : pending.keySet()) charBuffer.put(c); + pending.clear(); + charBuffer.flip(); + + packRange.put(STBTTPackRange.create().set(height, 0, charBuffer, chars, packedCharBuffer, (byte) 0, (byte) 0)); packRange.flip(); - // write and finish - STBTruetype.stbtt_PackFontRanges(packContext, buffer, 0, packRange); + // create and initialise packing context + ByteBuffer bitmap = BufferUtils.createByteBuffer(size * size); + + STBTTPackContext packContext = STBTTPackContext.create(); + STBTruetype.stbtt_PackBegin(packContext, bitmap, size, size, 0 ,1); + + // pack and upload + STBTruetype.stbtt_PackFontRanges(packContext, this.fontBuffer, 0, packRange); STBTruetype.stbtt_PackEnd(packContext); - // Create texture object and get font scale - texture = new Texture(size, size, TextureFormat.RED8, FilterMode.LINEAR, FilterMode.LINEAR); texture.upload(bitmap); - scale = STBTruetype.stbtt_ScaleForPixelHeight(fontInfo, height); - // Get font vertical ascent - try (MemoryStack stack = MemoryStack.stackPush()) { - IntBuffer ascent = stack.mallocInt(1); - STBTruetype.stbtt_GetFontVMetrics(fontInfo, ascent, null, null); - this.ascent = ascent.get(0); + // update char data + for (int i = 0; i < chars; i++) { + int codepoint = charBuffer.get(i); + STBTTPackedchar packedChar = packedCharBuffer.get(i); + + float ipw = 1f / size; // pixel width and height + float iph = 1f / size; + + charMap.put(codepoint, new CharData( + packedChar.xoff(), + packedChar.yoff(), + packedChar.xoff2(), + packedChar.yoff2(), + packedChar.x0() * ipw, + packedChar.y0() * iph, + packedChar.x1() * ipw, + packedChar.y1() * iph, + packedChar.xadvance() + )); } - for (int i = 0; i < cdata.length; i++) { - STBTTPackedchar.Buffer cbuf = cdata[i]; - int offset = packRange.get(i).first_unicode_codepoint_in_range(); - - for (int j = 0; j < cbuf.capacity(); j++) { - STBTTPackedchar packedChar = cbuf.get(j); - - float ipw = 1f / size; // pixel width and height - float iph = 1f / size; - - charMap.put(j + offset, new CharData( - packedChar.xoff(), - packedChar.yoff(), - packedChar.xoff2(), - packedChar.yoff2(), - packedChar.x0() * ipw, - packedChar.y0() * iph, - packedChar.x1() * ipw, - packedChar.y1() * iph, - packedChar.xadvance() - )); - } + // clear + this.fontInfo = null; + this.fontBuffer = null; + } + + private STBTTFontinfo getOrCreateFontInfo() { + if (this.fontInfo != null) { + return this.fontInfo; } + + if (this.fontBuffer == null) { + this.fontBuffer = readFont(this.fontFace); + } + + STBTTFontinfo fontInfo = STBTTFontinfo.create(); + STBTruetype.stbtt_InitFont(fontInfo, fontBuffer); + + return this.fontInfo = fontInfo; + } + + private static ByteBuffer readFont(FontFace fontFace) { + byte[] data = Utils.readBytes(fontFace.toStream()); + return BufferUtils.createByteBuffer(data.length).put(data).flip(); + } + + private int addCharacter(int codepoint) { + return pending.computeIfAbsent(codepoint, c -> { + STBTTFontinfo fontInfo = this.getOrCreateFontInfo(); + + try (MemoryStack stack = MemoryStack.stackPush()) { + IntBuffer advance = stack.mallocInt(1); + STBTruetype.stbtt_GetCodepointHMetrics(fontInfo, c, advance, null); + return advance.get(); + } + }); } public double getWidth(String string, int length) { @@ -104,10 +147,13 @@ public double getWidth(String string, int length) { for (int i = 0; i < length; i++) { int cp = string.charAt(i); - CharData c = charMap.get(cp); - if (c == null) c = charMap.get(32); - - width += c.xAdvance; + @Nullable CharData c = charMap.get(cp); + if (c != null) { + width += c.xAdvance; + } else { + int advance = addCharacter(cp); + width += advance * this.scale; + } } return width; @@ -125,20 +171,35 @@ public double render(MeshBuilder mesh, String string, double x, double y, Color for (int i = 0; i < length; i++) { int cp = string.charAt(i); - CharData c = charMap.get(cp); - if (c == null) c = charMap.get(32); + @Nullable CharData c = charMap.get(cp); + if (c != null) { + mesh.quad( + mesh.vec2(x + c.x0 * scale, y + c.y0 * scale).vec2(c.u0, c.v0).color(color).next(), + mesh.vec2(x + c.x0 * scale, y + c.y1 * scale).vec2(c.u0, c.v1).color(color).next(), + mesh.vec2(x + c.x1 * scale, y + c.y1 * scale).vec2(c.u1, c.v1).color(color).next(), + mesh.vec2(x + c.x1 * scale, y + c.y0 * scale).vec2(c.u1, c.v0).color(color).next() + ); + + x += c.xAdvance * scale; + } else { + int advance = addCharacter(cp); + x += advance * this.scale * scale; + } + } - mesh.quad( - mesh.vec2(x + c.x0 * scale, y + c.y0 * scale).vec2(c.u0, c.v0).color(color).next(), - mesh.vec2(x + c.x0 * scale, y + c.y1 * scale).vec2(c.u0, c.v1).color(color).next(), - mesh.vec2(x + c.x1 * scale, y + c.y1 * scale).vec2(c.u1, c.v1).color(color).next(), - mesh.vec2(x + c.x1 * scale, y + c.y0 * scale).vec2(c.u1, c.v0).color(color).next() - ); + return x; + } - x += c.xAdvance * scale; + public Texture getTexture() { + if (fontInfo != null) { + renderAndUploadAtlas(); } - return x; + return this.texture; + } + + public void close() { + this.texture.close(); } private record CharData(float x0, float y0, float x1, float y1, float u0, float v0, float u1, float v1, float xAdvance) {} diff --git a/src/main/java/meteordevelopment/meteorclient/systems/hud/HudRenderer.java b/src/main/java/meteordevelopment/meteorclient/systems/hud/HudRenderer.java index 3316565d67..5ee5cb0a77 100644 --- a/src/main/java/meteordevelopment/meteorclient/systems/hud/HudRenderer.java +++ b/src/main/java/meteordevelopment/meteorclient/systems/hud/HudRenderer.java @@ -27,9 +27,7 @@ import net.minecraft.util.Identifier; import org.joml.Quaternionf; import org.joml.Vector3f; -import org.lwjgl.BufferUtils; -import java.nio.ByteBuffer; import java.time.Duration; import java.util.ArrayList; import java.util.Iterator; @@ -85,11 +83,13 @@ public void end() { FontHolder fontHolder = it.next(); if (fontHolder.visited) { + Texture fontAtlas = fontHolder.font.getTexture(); + MeshRenderer.begin() .attachments(mc.getFramebuffer()) .pipeline(MeteorRenderPipelines.UI_TEXT) .mesh(fontHolder.getMesh()) - .sampler("u_Texture", fontHolder.font.texture.getGlTextureView(), fontHolder.font.texture.getSampler()) + .sampler("u_Texture", fontAtlas.getGlTextureView(), fontAtlas.getSampler()) .end(); } else { @@ -298,10 +298,7 @@ private void onCustomFontChanged(CustomFontChangedEvent event) { } private static FontHolder loadFont(int height) { - byte[] data = Utils.readBytes(Fonts.RENDERER.fontFace.toStream()); - ByteBuffer buffer = BufferUtils.createByteBuffer(data.length).put(data).flip(); - - return new FontHolder(new Font(buffer, height)); + return new FontHolder(new Font(Fonts.RENDERER.fontFace, height)); } private static class FontHolder { @@ -321,7 +318,7 @@ public MeshBuilder getMesh() { } public void destroy() { - font.texture.close(); + font.close(); } } } From 31103b44c56e6994a284a45a46e878a6b6839296 Mon Sep 17 00:00:00 2001 From: Crosby <32882447+crosby-moe@users.noreply.github.com> Date: Tue, 13 Jan 2026 17:46:23 -0500 Subject: [PATCH 2/6] dont invalidate existing char data when updating glyph atlas --- .../meteorclient/renderer/text/Font.java | 157 ++++++++++-------- 1 file changed, 91 insertions(+), 66 deletions(-) diff --git a/src/main/java/meteordevelopment/meteorclient/renderer/text/Font.java b/src/main/java/meteordevelopment/meteorclient/renderer/text/Font.java index ad95a12927..565002b165 100644 --- a/src/main/java/meteordevelopment/meteorclient/renderer/text/Font.java +++ b/src/main/java/meteordevelopment/meteorclient/renderer/text/Font.java @@ -5,6 +5,8 @@ package meteordevelopment.meteorclient.renderer.text; +import com.mojang.blaze3d.opengl.GlConst; +import com.mojang.blaze3d.opengl.GlStateManager; import com.mojang.blaze3d.textures.FilterMode; import com.mojang.blaze3d.textures.TextureFormat; import it.unimi.dsi.fastutil.ints.*; @@ -12,10 +14,13 @@ import meteordevelopment.meteorclient.renderer.Texture; import meteordevelopment.meteorclient.utils.Utils; import meteordevelopment.meteorclient.utils.render.color.Color; +import net.minecraft.client.texture.GlTexture; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.lwjgl.BufferUtils; import org.lwjgl.stb.*; import org.lwjgl.system.MemoryStack; +import org.lwjgl.system.MemoryUtil; import java.nio.ByteBuffer; import java.nio.IntBuffer; @@ -28,20 +33,28 @@ public class Font { private final float scale; private final float ascent; private final Int2ObjectOpenHashMap charMap = new Int2ObjectOpenHashMap<>(); - private final Int2IntMap pending = new Int2IntOpenHashMap(); - private @Nullable STBTTFontinfo fontInfo; - private @Nullable ByteBuffer fontBuffer; + private final STBTTPackContext packContext; + private final ByteBuffer fontAtlasBuffer; + private @Nullable ByteBuffer fileBuffer; - public Font(FontFace fontFace, ByteBuffer buffer, int height) { + public Font(FontFace fontFace, @NotNull ByteBuffer buffer, int height) { this.fontFace = fontFace; - this.fontBuffer = buffer; + this.fileBuffer = buffer; this.height = height; - // Initialize font - STBTTFontinfo fontInfo = this.getOrCreateFontInfo(); + // allocate data + STBTTFontinfo fontInfo = STBTTFontinfo.create(); + STBTruetype.stbtt_InitFont(fontInfo, buffer); + texture = new Texture(size, size, TextureFormat.RED8, FilterMode.LINEAR, FilterMode.LINEAR); scale = STBTruetype.stbtt_ScaleForPixelHeight(fontInfo, height); + // initialize font info & zero out texture + fontAtlasBuffer = BufferUtils.createByteBuffer(size * size); + + packContext = STBTTPackContext.create(); + STBTruetype.stbtt_PackBegin(packContext, fontAtlasBuffer, size, size, 0 ,1); + // Get font vertical ascent try (MemoryStack stack = MemoryStack.stackPush()) { IntBuffer ascent = stack.mallocInt(1); @@ -54,9 +67,23 @@ public Font(FontFace fontFace, int height) { this(fontFace, readFont(fontFace), height); } - private void renderAndUploadAtlas() { + private void upload() { + GlStateManager._bindTexture(((GlTexture) this.texture.getGlTexture()).getGlId()); + GlStateManager._pixelStore(GlConst.GL_UNPACK_ROW_LENGTH, size); + GlStateManager._pixelStore(GlConst.GL_UNPACK_SKIP_PIXELS, 0); + GlStateManager._pixelStore(GlConst.GL_UNPACK_SKIP_ROWS, 0); + GlStateManager._pixelStore(GlConst.GL_UNPACK_ALIGNMENT, 1); + GlStateManager._texSubImage2D(GlConst.GL_TEXTURE_2D, 0, 0, 0, size, size, GlConst.GL_RED, GlConst.GL_UNSIGNED_BYTE, MemoryUtil.memAddress0(this.fontAtlasBuffer)); + } + + // currently unused, but useful in the future hopefully + private void regenerateAtlas() { + if (this.fileBuffer == null) { + this.fileBuffer = readFont(this.fontFace); + } + // Allocate buffers - int chars = charMap.size() + pending.size(); + int chars = charMap.size(); STBTTPackRange.Buffer packRange = STBTTPackRange.create(1); STBTTPackedchar.Buffer packedCharBuffer = STBTTPackedchar.create(chars); @@ -65,24 +92,17 @@ private void renderAndUploadAtlas() { IntBuffer charBuffer = BufferUtils.createIntBuffer(chars); for (int c : charMap.keySet()) charBuffer.put(c); charMap.clear(); - for (int c : pending.keySet()) charBuffer.put(c); - pending.clear(); charBuffer.flip(); packRange.put(STBTTPackRange.create().set(height, 0, charBuffer, chars, packedCharBuffer, (byte) 0, (byte) 0)); packRange.flip(); // create and initialise packing context - ByteBuffer bitmap = BufferUtils.createByteBuffer(size * size); - - STBTTPackContext packContext = STBTTPackContext.create(); - STBTruetype.stbtt_PackBegin(packContext, bitmap, size, size, 0 ,1); // pack and upload - STBTruetype.stbtt_PackFontRanges(packContext, this.fontBuffer, 0, packRange); - STBTruetype.stbtt_PackEnd(packContext); + STBTruetype.stbtt_PackFontRanges(packContext, this.fileBuffer, 0, packRange); - texture.upload(bitmap); + this.upload(); // update char data for (int i = 0; i < chars; i++) { @@ -106,23 +126,7 @@ private void renderAndUploadAtlas() { } // clear - this.fontInfo = null; - this.fontBuffer = null; - } - - private STBTTFontinfo getOrCreateFontInfo() { - if (this.fontInfo != null) { - return this.fontInfo; - } - - if (this.fontBuffer == null) { - this.fontBuffer = readFont(this.fontFace); - } - - STBTTFontinfo fontInfo = STBTTFontinfo.create(); - STBTruetype.stbtt_InitFont(fontInfo, fontBuffer); - - return this.fontInfo = fontInfo; + this.fileBuffer = null; } private static ByteBuffer readFont(FontFace fontFace) { @@ -130,15 +134,43 @@ private static ByteBuffer readFont(FontFace fontFace) { return BufferUtils.createByteBuffer(data.length).put(data).flip(); } - private int addCharacter(int codepoint) { - return pending.computeIfAbsent(codepoint, c -> { - STBTTFontinfo fontInfo = this.getOrCreateFontInfo(); - - try (MemoryStack stack = MemoryStack.stackPush()) { - IntBuffer advance = stack.mallocInt(1); - STBTruetype.stbtt_GetCodepointHMetrics(fontInfo, c, advance, null); - return advance.get(); + private CharData getCharData(int codepoint) { + return charMap.computeIfAbsent(codepoint, c -> { + if (this.fileBuffer == null) { + this.fileBuffer = readFont(this.fontFace); } + + // allocate buffers + STBTTPackRange.Buffer packRange = STBTTPackRange.create(1); + STBTTPackedchar.Buffer packedCharBuffer = STBTTPackedchar.create(1); + + IntBuffer charBuffer = BufferUtils.createIntBuffer(1); + charBuffer.put(codepoint); + charBuffer.flip(); + + packRange.put(STBTTPackRange.create().set(height, 0, charBuffer, 1, packedCharBuffer, (byte) 0, (byte) 0)); + packRange.flip(); + + // pack and upload + STBTruetype.stbtt_PackFontRanges(packContext, this.fileBuffer, 0, packRange); + + // update char data + STBTTPackedchar packedChar = packedCharBuffer.get(0); + + float ipw = 1f / size; // pixel width and height + float iph = 1f / size; + + return new CharData( + packedChar.xoff(), + packedChar.yoff(), + packedChar.xoff2(), + packedChar.yoff2(), + packedChar.x0() * ipw, + packedChar.y0() * iph, + packedChar.x1() * ipw, + packedChar.y1() * iph, + packedChar.xadvance() + ); }); } @@ -147,13 +179,8 @@ public double getWidth(String string, int length) { for (int i = 0; i < length; i++) { int cp = string.charAt(i); - @Nullable CharData c = charMap.get(cp); - if (c != null) { - width += c.xAdvance; - } else { - int advance = addCharacter(cp); - width += advance * this.scale; - } + CharData c = this.getCharData(cp); + width += c.xAdvance(); } return width; @@ -171,28 +198,25 @@ public double render(MeshBuilder mesh, String string, double x, double y, Color for (int i = 0; i < length; i++) { int cp = string.charAt(i); - @Nullable CharData c = charMap.get(cp); - if (c != null) { - mesh.quad( - mesh.vec2(x + c.x0 * scale, y + c.y0 * scale).vec2(c.u0, c.v0).color(color).next(), - mesh.vec2(x + c.x0 * scale, y + c.y1 * scale).vec2(c.u0, c.v1).color(color).next(), - mesh.vec2(x + c.x1 * scale, y + c.y1 * scale).vec2(c.u1, c.v1).color(color).next(), - mesh.vec2(x + c.x1 * scale, y + c.y0 * scale).vec2(c.u1, c.v0).color(color).next() - ); - - x += c.xAdvance * scale; - } else { - int advance = addCharacter(cp); - x += advance * this.scale * scale; - } + CharData c = this.getCharData(cp); + mesh.quad( + mesh.vec2(x + c.x0() * scale, y + c.y0() * scale).vec2(c.u0(), c.v0()).color(color).next(), + mesh.vec2(x + c.x0() * scale, y + c.y1() * scale).vec2(c.u0(), c.v1()).color(color).next(), + mesh.vec2(x + c.x1() * scale, y + c.y1() * scale).vec2(c.u1(), c.v1()).color(color).next(), + mesh.vec2(x + c.x1() * scale, y + c.y0() * scale).vec2(c.u1(), c.v0()).color(color).next() + ); + + x += c.xAdvance() * scale; } return x; } public Texture getTexture() { - if (fontInfo != null) { - renderAndUploadAtlas(); + // flush updates + if (this.fileBuffer != null) { + this.upload(); + this.fileBuffer = null; } return this.texture; @@ -200,6 +224,7 @@ public Texture getTexture() { public void close() { this.texture.close(); + STBTruetype.stbtt_PackEnd(this.packContext); } private record CharData(float x0, float y0, float x1, float y1, float u0, float v0, float u1, float v1, float xAdvance) {} From 1547014ade6abf5c01fa4838db9981bfa37d5400 Mon Sep 17 00:00:00 2001 From: Crosby <32882447+crosby-moe@users.noreply.github.com> Date: Tue, 13 Jan 2026 18:45:27 -0500 Subject: [PATCH 3/6] weakref the file buffer --- .../meteorclient/renderer/text/Font.java | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/main/java/meteordevelopment/meteorclient/renderer/text/Font.java b/src/main/java/meteordevelopment/meteorclient/renderer/text/Font.java index 565002b165..16de5d125b 100644 --- a/src/main/java/meteordevelopment/meteorclient/renderer/text/Font.java +++ b/src/main/java/meteordevelopment/meteorclient/renderer/text/Font.java @@ -22,6 +22,7 @@ import org.lwjgl.system.MemoryStack; import org.lwjgl.system.MemoryUtil; +import java.lang.ref.WeakReference; import java.nio.ByteBuffer; import java.nio.IntBuffer; @@ -36,10 +37,12 @@ public class Font { private final STBTTPackContext packContext; private final ByteBuffer fontAtlasBuffer; private @Nullable ByteBuffer fileBuffer; + private WeakReference fileBufferRef; public Font(FontFace fontFace, @NotNull ByteBuffer buffer, int height) { this.fontFace = fontFace; this.fileBuffer = buffer; + this.fileBufferRef = new WeakReference<>(buffer); this.height = height; // allocate data @@ -76,11 +79,23 @@ private void upload() { GlStateManager._texSubImage2D(GlConst.GL_TEXTURE_2D, 0, 0, 0, size, size, GlConst.GL_RED, GlConst.GL_UNSIGNED_BYTE, MemoryUtil.memAddress0(this.fontAtlasBuffer)); } - // currently unused, but useful in the future hopefully - private void regenerateAtlas() { + private ByteBuffer getFileBuffer() { if (this.fileBuffer == null) { - this.fileBuffer = readFont(this.fontFace); + if (this.fileBufferRef.get() == null) { + ByteBuffer buffer = readFont(this.fontFace); + this.fileBufferRef = new WeakReference<>(buffer); + return this.fileBuffer = buffer; + } else { + return this.fileBuffer = this.fileBufferRef.get(); + } + } else { + return this.fileBuffer; } + } + + // currently unused, but useful in the future hopefully + private void regenerateAtlas() { + ByteBuffer fileBuffer = this.getFileBuffer(); // Allocate buffers int chars = charMap.size(); @@ -100,7 +115,7 @@ private void regenerateAtlas() { // create and initialise packing context // pack and upload - STBTruetype.stbtt_PackFontRanges(packContext, this.fileBuffer, 0, packRange); + STBTruetype.stbtt_PackFontRanges(packContext, fileBuffer, 0, packRange); this.upload(); @@ -136,9 +151,7 @@ private static ByteBuffer readFont(FontFace fontFace) { private CharData getCharData(int codepoint) { return charMap.computeIfAbsent(codepoint, c -> { - if (this.fileBuffer == null) { - this.fileBuffer = readFont(this.fontFace); - } + ByteBuffer fileBuffer = this.getFileBuffer(); // allocate buffers STBTTPackRange.Buffer packRange = STBTTPackRange.create(1); @@ -152,7 +165,7 @@ private CharData getCharData(int codepoint) { packRange.flip(); // pack and upload - STBTruetype.stbtt_PackFontRanges(packContext, this.fileBuffer, 0, packRange); + STBTruetype.stbtt_PackFontRanges(packContext, fileBuffer, 0, packRange); // update char data STBTTPackedchar packedChar = packedCharBuffer.get(0); From 6001c31f6ac2c6a102942b5c4893ac61951599ce Mon Sep 17 00:00:00 2001 From: Crosby <32882447+crosby-moe@users.noreply.github.com> Date: Wed, 14 Jan 2026 14:55:15 -0500 Subject: [PATCH 4/6] slight optimization --- .../meteorclient/renderer/text/Font.java | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/main/java/meteordevelopment/meteorclient/renderer/text/Font.java b/src/main/java/meteordevelopment/meteorclient/renderer/text/Font.java index 16de5d125b..30196b4f43 100644 --- a/src/main/java/meteordevelopment/meteorclient/renderer/text/Font.java +++ b/src/main/java/meteordevelopment/meteorclient/renderer/text/Font.java @@ -93,6 +93,11 @@ private ByteBuffer getFileBuffer() { } } + private static ByteBuffer readFont(FontFace fontFace) { + byte[] data = Utils.readBytes(fontFace.toStream()); + return BufferUtils.createByteBuffer(data.length).put(data).flip(); + } + // currently unused, but useful in the future hopefully private void regenerateAtlas() { ByteBuffer fileBuffer = this.getFileBuffer(); @@ -144,11 +149,6 @@ private void regenerateAtlas() { this.fileBuffer = null; } - private static ByteBuffer readFont(FontFace fontFace) { - byte[] data = Utils.readBytes(fontFace.toStream()); - return BufferUtils.createByteBuffer(data.length).put(data).flip(); - } - private CharData getCharData(int codepoint) { return charMap.computeIfAbsent(codepoint, c -> { ByteBuffer fileBuffer = this.getFileBuffer(); @@ -157,11 +157,7 @@ private CharData getCharData(int codepoint) { STBTTPackRange.Buffer packRange = STBTTPackRange.create(1); STBTTPackedchar.Buffer packedCharBuffer = STBTTPackedchar.create(1); - IntBuffer charBuffer = BufferUtils.createIntBuffer(1); - charBuffer.put(codepoint); - charBuffer.flip(); - - packRange.put(STBTTPackRange.create().set(height, 0, charBuffer, 1, packedCharBuffer, (byte) 0, (byte) 0)); + packRange.put(STBTTPackRange.create().set(height, codepoint, null, 1, packedCharBuffer, (byte) 0, (byte) 0)); packRange.flip(); // pack and upload From 57706a4eefc02a575a94488d140fafe013cc6b2f Mon Sep 17 00:00:00 2001 From: Crosby <32882447+crosby-moe@users.noreply.github.com> Date: Wed, 14 Jan 2026 14:57:04 -0500 Subject: [PATCH 5/6] correctly handle surrogate pairs Co-authored-by: Nora <50046813+noramibu@users.noreply.github.com> --- .../meteorclient/renderer/text/Font.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/meteordevelopment/meteorclient/renderer/text/Font.java b/src/main/java/meteordevelopment/meteorclient/renderer/text/Font.java index 30196b4f43..3df4180a64 100644 --- a/src/main/java/meteordevelopment/meteorclient/renderer/text/Font.java +++ b/src/main/java/meteordevelopment/meteorclient/renderer/text/Font.java @@ -186,10 +186,11 @@ private CharData getCharData(int codepoint) { public double getWidth(String string, int length) { double width = 0; - for (int i = 0; i < length; i++) { - int cp = string.charAt(i); + for (int i = 0; i < length;) { + int cp = string.codePointAt(i); CharData c = this.getCharData(cp); width += c.xAdvance(); + i += Character.charCount(cp); } return width; @@ -205,8 +206,8 @@ public double render(MeshBuilder mesh, String string, double x, double y, Color int length = string.length(); mesh.ensureCapacity(length * 4, length * 6); - for (int i = 0; i < length; i++) { - int cp = string.charAt(i); + for (int i = 0; i < length;) { + int cp = string.codePointAt(i); CharData c = this.getCharData(cp); mesh.quad( mesh.vec2(x + c.x0() * scale, y + c.y0() * scale).vec2(c.u0(), c.v0()).color(color).next(), @@ -216,6 +217,7 @@ public double render(MeshBuilder mesh, String string, double x, double y, Color ); x += c.xAdvance() * scale; + i += Character.charCount(cp); } return x; From d9f023f37c1023165ceb6a2a28267a6b5dfc9535 Mon Sep 17 00:00:00 2001 From: Crosby <32882447+crosby-moe@users.noreply.github.com> Date: Thu, 15 Jan 2026 15:46:33 -0500 Subject: [PATCH 6/6] use `NativeImage` for font atlas --- .../meteorclient/renderer/text/Font.java | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/main/java/meteordevelopment/meteorclient/renderer/text/Font.java b/src/main/java/meteordevelopment/meteorclient/renderer/text/Font.java index 3df4180a64..a15906244c 100644 --- a/src/main/java/meteordevelopment/meteorclient/renderer/text/Font.java +++ b/src/main/java/meteordevelopment/meteorclient/renderer/text/Font.java @@ -5,8 +5,7 @@ package meteordevelopment.meteorclient.renderer.text; -import com.mojang.blaze3d.opengl.GlConst; -import com.mojang.blaze3d.opengl.GlStateManager; +import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.blaze3d.textures.FilterMode; import com.mojang.blaze3d.textures.TextureFormat; import it.unimi.dsi.fastutil.ints.*; @@ -14,7 +13,7 @@ import meteordevelopment.meteorclient.renderer.Texture; import meteordevelopment.meteorclient.utils.Utils; import meteordevelopment.meteorclient.utils.render.color.Color; -import net.minecraft.client.texture.GlTexture; +import net.minecraft.client.texture.NativeImage; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.lwjgl.BufferUtils; @@ -35,7 +34,7 @@ public class Font { private final float ascent; private final Int2ObjectOpenHashMap charMap = new Int2ObjectOpenHashMap<>(); private final STBTTPackContext packContext; - private final ByteBuffer fontAtlasBuffer; + private final NativeImage fontAtlas; private @Nullable ByteBuffer fileBuffer; private WeakReference fileBufferRef; @@ -53,10 +52,10 @@ public Font(FontFace fontFace, @NotNull ByteBuffer buffer, int height) { scale = STBTruetype.stbtt_ScaleForPixelHeight(fontInfo, height); // initialize font info & zero out texture - fontAtlasBuffer = BufferUtils.createByteBuffer(size * size); + fontAtlas = new NativeImage(NativeImage.Format.LUMINANCE, size, size, false); packContext = STBTTPackContext.create(); - STBTruetype.stbtt_PackBegin(packContext, fontAtlasBuffer, size, size, 0 ,1); + STBTruetype.nstbtt_PackBegin(packContext.address(), fontAtlas.imageId(), size, size, 0, 1, MemoryUtil.NULL); // Get font vertical ascent try (MemoryStack stack = MemoryStack.stackPush()) { @@ -71,12 +70,7 @@ public Font(FontFace fontFace, int height) { } private void upload() { - GlStateManager._bindTexture(((GlTexture) this.texture.getGlTexture()).getGlId()); - GlStateManager._pixelStore(GlConst.GL_UNPACK_ROW_LENGTH, size); - GlStateManager._pixelStore(GlConst.GL_UNPACK_SKIP_PIXELS, 0); - GlStateManager._pixelStore(GlConst.GL_UNPACK_SKIP_ROWS, 0); - GlStateManager._pixelStore(GlConst.GL_UNPACK_ALIGNMENT, 1); - GlStateManager._texSubImage2D(GlConst.GL_TEXTURE_2D, 0, 0, 0, size, size, GlConst.GL_RED, GlConst.GL_UNSIGNED_BYTE, MemoryUtil.memAddress0(this.fontAtlasBuffer)); + RenderSystem.getDevice().createCommandEncoder().writeToTexture(this.texture.getGlTexture(), this.fontAtlas); } private ByteBuffer getFileBuffer() { @@ -235,6 +229,7 @@ public Texture getTexture() { public void close() { this.texture.close(); + this.fontAtlas.close(); STBTruetype.stbtt_PackEnd(this.packContext); }