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..a15906244c 100644 --- a/src/main/java/meteordevelopment/meteorclient/renderer/text/Font.java +++ b/src/main/java/meteordevelopment/meteorclient/renderer/text/Font.java @@ -5,109 +5,186 @@ package meteordevelopment.meteorclient.renderer.text; +import com.mojang.blaze3d.systems.RenderSystem; 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 net.minecraft.client.texture.NativeImage; +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.lang.ref.WeakReference; import java.nio.ByteBuffer; 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; - - public Font(ByteBuffer buffer, int height) { + private final STBTTPackContext packContext; + private final NativeImage fontAtlas; + 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; - // Initialize font + // allocate data STBTTFontinfo fontInfo = STBTTFontinfo.create(); STBTruetype.stbtt_InitFont(fontInfo, buffer); - // 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 - }; - - // create and initialise packing context - STBTTPackContext packContext = STBTTPackContext.create(); - STBTruetype.stbtt_PackBegin(packContext, bitmap, size, size, 0 ,1); - - // 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 - packRange.flip(); - - // write and finish - STBTruetype.stbtt_PackFontRanges(packContext, buffer, 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); + // initialize font info & zero out texture + fontAtlas = new NativeImage(NativeImage.Format.LUMINANCE, size, size, false); + + packContext = STBTTPackContext.create(); + STBTruetype.nstbtt_PackBegin(packContext.address(), fontAtlas.imageId(), size, size, 0, 1, MemoryUtil.NULL); + // 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); + } - 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() - )); + private void upload() { + RenderSystem.getDevice().createCommandEncoder().writeToTexture(this.texture.getGlTexture(), this.fontAtlas); + } + + private ByteBuffer getFileBuffer() { + if (this.fileBuffer == null) { + 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; } } + 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(); + + // Allocate buffers + int chars = charMap.size(); + + STBTTPackRange.Buffer packRange = STBTTPackRange.create(1); + STBTTPackedchar.Buffer packedCharBuffer = STBTTPackedchar.create(chars); + + // create the pack range, populate with the specific packing ranges + IntBuffer charBuffer = BufferUtils.createIntBuffer(chars); + for (int c : charMap.keySet()) charBuffer.put(c); + charMap.clear(); + charBuffer.flip(); + + packRange.put(STBTTPackRange.create().set(height, 0, charBuffer, chars, packedCharBuffer, (byte) 0, (byte) 0)); + packRange.flip(); + + // create and initialise packing context + + // pack and upload + STBTruetype.stbtt_PackFontRanges(packContext, fileBuffer, 0, packRange); + + this.upload(); + + // 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() + )); + } + + // clear + this.fileBuffer = null; + } + + private CharData getCharData(int codepoint) { + return charMap.computeIfAbsent(codepoint, c -> { + ByteBuffer fileBuffer = this.getFileBuffer(); + + // allocate buffers + STBTTPackRange.Buffer packRange = STBTTPackRange.create(1); + STBTTPackedchar.Buffer packedCharBuffer = STBTTPackedchar.create(1); + + packRange.put(STBTTPackRange.create().set(height, codepoint, null, 1, packedCharBuffer, (byte) 0, (byte) 0)); + packRange.flip(); + + // pack and upload + STBTruetype.stbtt_PackFontRanges(packContext, 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() + ); + }); + } + public double getWidth(String string, int length) { double width = 0; - 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; + 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; @@ -123,23 +200,38 @@ 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); - CharData c = charMap.get(cp); - if (c == null) c = charMap.get(32); - + 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(), - 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() + 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; + x += c.xAdvance() * scale; + i += Character.charCount(cp); } return x; } + public Texture getTexture() { + // flush updates + if (this.fileBuffer != null) { + this.upload(); + this.fileBuffer = null; + } + + return this.texture; + } + + public void close() { + this.texture.close(); + this.fontAtlas.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) {} } 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(); } } }