diff --git a/src/main/java/meteordevelopment/meteorclient/renderer/Fonts.java b/src/main/java/meteordevelopment/meteorclient/renderer/Fonts.java index e67d60cd72f..e5bf4239295 100644 --- a/src/main/java/meteordevelopment/meteorclient/renderer/Fonts.java +++ b/src/main/java/meteordevelopment/meteorclient/renderer/Fonts.java @@ -17,14 +17,36 @@ import meteordevelopment.meteorclient.utils.render.FontUtils; import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import java.util.Locale; +import java.util.Optional; import static meteordevelopment.meteorclient.MeteorClient.mc; public class Fonts { public static final String[] BUILTIN_FONTS = {"JetBrains Mono", "Comfortaa", "Tw Cen MT", "Pixelation"}; + private static final List FALLBACK_FONT_FAMILIES = List.of( + "Noto Sans CJK SC", + "Noto Sans SC", + "Microsoft YaHei", + "Microsoft YaHei UI", + "DengXian", + "SimHei", + "SimSun", + "KaiTi", + "Microsoft JhengHei", + "Malgun Gothic", + "Yu Gothic", + "MS Gothic", + "Source Han Sans SC", + "WenQuanYi Zen Hei", + "PingFang SC", + "Hiragino Sans GB" + ); public static String DEFAULT_FONT_FAMILY; public static FontFace DEFAULT_FONT; @@ -59,14 +81,12 @@ public static void refresh() { } public static void load(FontFace fontFace) { - if (RENDERER != null) { - if (RENDERER.fontFace.equals(fontFace)) return; - else RENDERER.destroy(); - } + CustomTextRenderer previous = RENDERER; + if (previous != null && previous.fontFace.equals(fontFace)) return; + CustomTextRenderer replacement; try { - RENDERER = new CustomTextRenderer(fontFace); - MeteorClient.EVENT_BUS.post(CustomFontChangedEvent.get()); + replacement = new CustomTextRenderer(fontFace); } catch (Exception e) { if (fontFace.equals(DEFAULT_FONT)) { throw new RuntimeException("Failed to load default font: " + fontFace, e); @@ -74,8 +94,13 @@ public static void load(FontFace fontFace) { MeteorClient.LOG.error("Failed to load font: {}", fontFace, e); load(Fonts.DEFAULT_FONT); + return; } + RENDERER = replacement; + if (previous != null) previous.destroy(); + MeteorClient.EVENT_BUS.post(CustomFontChangedEvent.get()); + if (mc.screen instanceof WidgetScreen widgetScreen && Config.get().customFont.get()) { widgetScreen.invalidate(); } @@ -90,4 +115,66 @@ public static FontFamily getFamily(String name) { return null; } + + public static Optional getFallbackFont(FontFace primary) { + for (String familyName : FALLBACK_FONT_FAMILIES) { + FontFace font = findFallbackFont(familyName, primary); + if (font != null) return Optional.of(font); + } + + return Optional.empty(); + } + + public static List readFontBuffers(FontFace primary) throws IOException { + List buffers = new ArrayList<>(); + buffers.add(primary.readToDirectByteBuffer()); + + getFallbackFont(primary) + .flatMap(Fonts::readFallbackFont) + .ifPresent(buffers::add); + + return List.copyOf(buffers); + } + + private static Optional readFallbackFont(FontFace font) { + try { + return Optional.of(font.readToDirectByteBuffer()); + } catch (IOException e) { + MeteorClient.LOG.warn("Failed to load fallback font: {}", font, e); + return Optional.empty(); + } + } + + private static FontFace findFallbackFont(String familyName, FontFace primary) { + FontFamily family = getFamily(familyName); + + if (family == null) { + String needle = familyName.toLowerCase(Locale.ROOT); + + for (FontFamily fontFamily : FONT_FAMILIES) { + if (fontFamily.getName().toLowerCase(Locale.ROOT).contains(needle)) { + family = fontFamily; + break; + } + } + } + + if (family == null) return null; + if (family.getName().equalsIgnoreCase(primary.info.family())) return null; + + FontFace styleMatch = family.get(primary.info.type()); + if (styleMatch != null) return styleMatch; + + FontFace regular = family.get(FontInfo.Type.Regular); + if (regular != null) return regular; + + for (FontInfo.Type type : FontInfo.Type.values()) { + if (type == primary.info.type() || type == FontInfo.Type.Regular) continue; + + FontFace font = family.get(type); + if (font != null) return font; + } + + return null; + } } diff --git a/src/main/java/meteordevelopment/meteorclient/renderer/text/CustomTextRenderer.java b/src/main/java/meteordevelopment/meteorclient/renderer/text/CustomTextRenderer.java index c2e3d307bab..dbe2cef9e16 100644 --- a/src/main/java/meteordevelopment/meteorclient/renderer/text/CustomTextRenderer.java +++ b/src/main/java/meteordevelopment/meteorclient/renderer/text/CustomTextRenderer.java @@ -5,6 +5,7 @@ package meteordevelopment.meteorclient.renderer.text; +import meteordevelopment.meteorclient.renderer.Fonts; import meteordevelopment.meteorclient.renderer.MeshBuilder; import meteordevelopment.meteorclient.renderer.MeshRenderer; import meteordevelopment.meteorclient.renderer.MeteorRenderPipelines; @@ -13,30 +14,38 @@ import java.io.IOException; import java.nio.ByteBuffer; +import java.util.List; public class CustomTextRenderer implements TextRenderer { public static final Color SHADOW_COLOR = new Color(60, 60, 60, 180); private final MeshBuilder mesh = new MeshBuilder(MeteorRenderPipelines.UI_TEXT); + private final Color shadowColor = new Color(SHADOW_COLOR); public final FontFace fontFace; + private final List fontBuffers; private final Font[] fonts; private Font font; private boolean building; private boolean scaleOnly; + private boolean destroyed; private double fontScale = 1; private double scale = 1; public CustomTextRenderer(FontFace fontFace) throws IOException { this.fontFace = fontFace; - - ByteBuffer buffer = fontFace.readToDirectByteBuffer(); - - 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))); + this.fontBuffers = Fonts.readFontBuffers(fontFace); + + this.fonts = new Font[5]; + try { + for (int i = 0; i < fonts.length; i++) { + fonts[i] = createFont((int) Math.round(27 * ((i * 0.5) + 1))); + } + } catch (RuntimeException | Error e) { + closeFonts(); + throw e; } } @@ -47,6 +56,7 @@ public void setAlpha(double a) { @Override public void begin(double scale, boolean scaleOnly, boolean big) { + if (destroyed) throw new IllegalStateException("CustomTextRenderer has already been destroyed."); if (building) throw new RuntimeException("CustomTextRenderer.begin() called twice"); if (!scaleOnly) mesh.begin(); @@ -94,13 +104,10 @@ public double render(String text, double x, double y, Color color, boolean shado double width; if (shadow) { - int preShadowA = SHADOW_COLOR.a; - SHADOW_COLOR.a = (int) (color.a / 255.0 * preShadowA); + shadowColor.a = (int) (color.a / 255.0 * SHADOW_COLOR.a); - width = font.render(mesh, text, x + fontScale * scale / 1.5, y + fontScale * scale / 1.5, SHADOW_COLOR, scale / 1.5); + width = font.render(mesh, text, x + fontScale * scale / 1.5, y + fontScale * scale / 1.5, shadowColor, scale / 1.5); font.render(mesh, text, x, y, color, scale / 1.5); - - SHADOW_COLOR.a = preShadowA; } else { width = font.render(mesh, text, x, y, color, scale / 1.5); } @@ -118,24 +125,40 @@ public boolean isBuilding() { public void end() { if (!building) throw new RuntimeException("CustomTextRenderer.end() called without calling begin()"); - if (!scaleOnly) { - mesh.end(); - - MeshRenderer.begin() - .attachments(Minecraft.getInstance().getMainRenderTarget()) - .pipeline(MeteorRenderPipelines.UI_TEXT) - .mesh(mesh) - .sampler("u_Texture", font.texture.getTextureView(), font.texture.getSampler()) - .end(); + try { + if (!scaleOnly) { + mesh.end(); + font.uploadPendingGlyphs(); + + MeshRenderer.begin() + .attachments(Minecraft.getInstance().getMainRenderTarget()) + .pipeline(MeteorRenderPipelines.UI_TEXT) + .mesh(mesh) + .sampler("u_Texture", font.texture.getTextureView(), font.texture.getSampler()) + .end(); + } + } finally { + building = false; + scaleOnly = false; + scale = 1; } + } - building = false; - scale = 1; + public Font createFont(int height) { + if (destroyed) throw new IllegalStateException("CustomTextRenderer has already been destroyed."); + return new Font(fontBuffers, height); } public void destroy() { - for (Font font : this.fonts) { - font.texture.close(); + if (destroyed) return; + + closeFonts(); + destroyed = true; + } + + private void closeFonts() { + for (Font font : fonts) { + if (font != null) 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 78866100142..666acd247a3 100644 --- a/src/main/java/meteordevelopment/meteorclient/renderer/text/Font.java +++ b/src/main/java/meteordevelopment/meteorclient/renderer/text/Font.java @@ -7,6 +7,7 @@ import com.mojang.blaze3d.textures.FilterMode; import com.mojang.blaze3d.textures.TextureFormat; +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import meteordevelopment.meteorclient.renderer.MeshBuilder; import meteordevelopment.meteorclient.renderer.Texture; @@ -17,24 +18,58 @@ import java.nio.ByteBuffer; import java.nio.IntBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class Font implements AutoCloseable { + private static final int ATLAS_SIZE = 2048; + private static final int GLYPH_PADDING = 2; + private static final int SPACE_CODE_POINT = ' '; -public class Font { public final Texture texture; + private final int height; private final float scale; private final float ascent; + private final ByteBuffer bitmap; + private final List fonts; private final Int2ObjectOpenHashMap charMap = new Int2ObjectOpenHashMap<>(); - private static final int size = 2048; + private final Int2ObjectOpenHashMap metricsMap = new Int2ObjectOpenHashMap<>(); + private final IntOpenHashSet missingGlyphs = new IntOpenHashSet(); + private final IntOpenHashSet unpackableGlyphs = new IntOpenHashSet(); + + private int packX; + private int packY; + private int rowHeight; + private boolean dirty; + private boolean closed; public Font(ByteBuffer buffer, int height) { + this(List.of(buffer), height); + } + + public Font(List buffers, int height) { + if (height <= 0) throw new IllegalArgumentException("Font height must be positive."); + if (buffers.isEmpty()) throw new IllegalArgumentException("At least one font buffer is required."); + this.height = height; - // Initialize font - STBTTFontinfo fontInfo = STBTTFontinfo.create(); - STBTruetype.stbtt_InitFont(fontInfo, buffer); + List loadedFonts = new ArrayList<>(buffers.size()); + FontData primaryFont = createFontData(buffers.get(0), height); + if (primaryFont == null) throw new IllegalArgumentException("The primary font buffer is invalid."); + + loadedFonts.add(primaryFont); + for (int i = 1; i < buffers.size(); i++) { + FontData fallbackFont = createFontData(buffers.get(i), height); + if (fallbackFont != null) loadedFonts.add(fallbackFont); + } + + fonts = List.copyOf(loadedFonts); + STBTTFontinfo fontInfo = primaryFont.info; // Allocate buffers - ByteBuffer bitmap = BufferUtils.createByteBuffer(size * size); + bitmap = BufferUtils.createByteBuffer(ATLAS_SIZE * ATLAS_SIZE); STBTTPackedchar.Buffer[] cdata = { STBTTPackedchar.create(95), // Basic Latin STBTTPackedchar.create(96), // Latin 1 Supplement @@ -46,7 +81,10 @@ public Font(ByteBuffer buffer, int height) { // create and initialise packing context STBTTPackContext packContext = STBTTPackContext.create(); - STBTruetype.stbtt_PackBegin(packContext, bitmap, size, size, 0 ,1); + if (!STBTruetype.stbtt_PackBegin(packContext, bitmap, ATLAS_SIZE, ATLAS_SIZE, 0, 1)) { + throw new IllegalStateException("Failed to initialize the font atlas packer."); + } + STBTruetype.stbtt_PackSetSkipMissingCodepoints(packContext, true); // create the pack range, populate with the specific packing ranges STBTTPackRange.Buffer packRange = STBTTPackRange.create(cdata.length); @@ -59,13 +97,18 @@ public Font(ByteBuffer buffer, int height) { packRange.flip(); // write and finish - STBTruetype.stbtt_PackFontRanges(packContext, buffer, 0, packRange); - STBTruetype.stbtt_PackEnd(packContext); + try { + // A false return value means at least one glyph did not fit. Successfully packed + // glyphs are still usable; the remaining ones are loaded lazily below. + STBTruetype.stbtt_PackFontRanges(packContext, primaryFont.buffer, 0, packRange); + } finally { + STBTruetype.stbtt_PackEnd(packContext); + } // Create texture object and get font scale - texture = new Texture(size, size, TextureFormat.RED8, FilterMode.LINEAR, FilterMode.LINEAR); + texture = new Texture(ATLAS_SIZE, ATLAS_SIZE, TextureFormat.RED8, FilterMode.LINEAR, FilterMode.LINEAR); texture.upload(bitmap); - scale = STBTruetype.stbtt_ScaleForPixelHeight(fontInfo, height); + scale = primaryFont.scale; // Get font vertical ascent try (MemoryStack stack = MemoryStack.stackPush()) { @@ -74,17 +117,22 @@ public Font(ByteBuffer buffer, int height) { this.ascent = ascent.get(0); } + int usedY = 0; 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++) { + int codePoint = j + offset; + if (STBTruetype.stbtt_FindGlyphIndex(primaryFont.info, codePoint) == 0) continue; + STBTTPackedchar packedChar = cbuf.get(j); + if (!isPacked(packedChar)) continue; - float ipw = 1f / size; // pixel width and height - float iph = 1f / size; + float ipw = 1f / ATLAS_SIZE; // pixel width and height + float iph = 1f / ATLAS_SIZE; - charMap.put(j + offset, new CharData( + charMap.put(codePoint, new CharData( packedChar.xoff(), packedChar.yoff(), packedChar.xoff2(), @@ -95,19 +143,24 @@ public Font(ByteBuffer buffer, int height) { packedChar.y1() * iph, packedChar.xadvance() )); + + usedY = Math.max(usedY, packedChar.y1()); } } + + packY = Math.min(usedY + GLYPH_PADDING, ATLAS_SIZE); } public double getWidth(String string, int length) { - double width = 0; + ensureOpen(); - for (int i = 0; i < length; i++) { - int cp = string.charAt(i); - CharData c = charMap.get(cp); - if (c == null) c = charMap.get(32); + double width = 0; + int end = Math.min(length, string.length()); - width += c.xAdvance; + for (int i = 0; i < end; ) { + int cp = codePointAt(string, i, end); + width += getAdvance(cp); + i += Character.charCount(cp); } return width; @@ -117,29 +170,210 @@ public int getHeight() { return height; } + public void uploadPendingGlyphs() { + ensureOpen(); + if (!dirty) return; + + texture.upload(bitmap); + dirty = false; + } + public double render(MeshBuilder mesh, String string, double x, double y, Color color, double scale) { + ensureOpen(); y += ascent * this.scale * scale; 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 = 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() - ); + if (c != null && c.hasBitmap()) { + 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; + x += (c != null ? c.xAdvance : getAdvance(cp)) * scale; + i += Character.charCount(cp); } return x; } - private record CharData(float x0, float y0, float x1, float y1, float u0, float v0, float u1, float v1, float xAdvance) {} + @Override + public void close() { + if (closed) return; + + texture.close(); + closed = true; + } + + private CharData getCharData(int cp) { + CharData c = charMap.get(cp); + if (c != null) return c; + if (unpackableGlyphs.contains(cp)) return null; + + GlyphMetrics metrics = getGlyphMetrics(cp); + if (metrics == null) return null; + + c = loadGlyph(cp, metrics); + if (c != null) { + charMap.put(cp, c); + } else { + unpackableGlyphs.add(cp); + } + + return c; + } + + private float getAdvance(int cp) { + CharData data = charMap.get(cp); + if (data != null) return data.xAdvance; + + GlyphMetrics metrics = getGlyphMetrics(cp); + if (metrics != null) return metrics.xAdvance; + + if (cp != SPACE_CODE_POINT) return getAdvance(SPACE_CODE_POINT); + return 0; + } + + private GlyphMetrics getGlyphMetrics(int cp) { + GlyphMetrics metrics = metricsMap.get(cp); + if (metrics != null) return metrics; + if (missingGlyphs.contains(cp)) return null; + + FontData font = findFont(cp); + if (font == null) { + missingGlyphs.add(cp); + return null; + } + + try (MemoryStack stack = MemoryStack.stackPush()) { + IntBuffer advanceWidth = stack.mallocInt(1); + STBTruetype.stbtt_GetCodepointHMetrics(font.info, cp, advanceWidth, null); + + metrics = new GlyphMetrics(font, advanceWidth.get(0) * font.scale); + metricsMap.put(cp, metrics); + return metrics; + } + } + + private CharData loadGlyph(int cp, GlyphMetrics metrics) { + FontData font = metrics.font; + + try (MemoryStack stack = MemoryStack.stackPush()) { + IntBuffer x0 = stack.mallocInt(1); + IntBuffer y0 = stack.mallocInt(1); + IntBuffer x1 = stack.mallocInt(1); + IntBuffer y1 = stack.mallocInt(1); + + STBTruetype.stbtt_GetCodepointBitmapBox(font.info, cp, font.scale, font.scale, x0, y0, x1, y1); + + int bitmapWidth = x1.get(0) - x0.get(0); + int bitmapHeight = y1.get(0) - y0.get(0); + + if (bitmapWidth <= 0 || bitmapHeight <= 0) { + return new CharData(0, 0, 0, 0, 0, 0, 0, 0, metrics.xAdvance); + } + + if (!reserve(bitmapWidth, bitmapHeight)) return null; + + int glyphX = packX + GLYPH_PADDING; + int glyphY = packY + GLYPH_PADDING; + + ByteBuffer glyphTarget = bitmap.duplicate(); + glyphTarget.position(glyphY * ATLAS_SIZE + glyphX); + STBTruetype.stbtt_MakeCodepointBitmap(font.info, glyphTarget.slice(), bitmapWidth, bitmapHeight, ATLAS_SIZE, font.scale, font.scale, cp); + + float ipw = 1f / ATLAS_SIZE; + float iph = 1f / ATLAS_SIZE; + + CharData charData = new CharData( + x0.get(0), + y0.get(0), + x1.get(0), + y1.get(0), + glyphX * ipw, + glyphY * iph, + (glyphX + bitmapWidth) * ipw, + (glyphY + bitmapHeight) * iph, + metrics.xAdvance + ); + + packX += bitmapWidth + GLYPH_PADDING * 2; + rowHeight = Math.max(rowHeight, bitmapHeight + GLYPH_PADDING * 2); + + dirty = true; + return charData; + } + } + + private boolean reserve(int bitmapWidth, int bitmapHeight) { + int requiredWidth = bitmapWidth + GLYPH_PADDING * 2; + int requiredHeight = bitmapHeight + GLYPH_PADDING * 2; + + if (requiredWidth > ATLAS_SIZE || requiredHeight > ATLAS_SIZE) return false; + + if (packX + requiredWidth > ATLAS_SIZE) { + packX = 0; + packY += rowHeight; + rowHeight = 0; + } + + if (packY + requiredHeight > ATLAS_SIZE) return false; + + return true; + } + + private FontData findFont(int cp) { + for (FontData font : fonts) { + if (STBTruetype.stbtt_FindGlyphIndex(font.info, cp) != 0) return font; + } + + return null; + } + + private static FontData createFontData(ByteBuffer source, int height) { + ByteBuffer buffer = Objects.requireNonNull(source, "Font buffer cannot be null.").duplicate(); + STBTTFontinfo info = STBTTFontinfo.create(); + if (!STBTruetype.stbtt_InitFont(info, buffer)) return null; + + return new FontData(buffer, info, STBTruetype.stbtt_ScaleForPixelHeight(info, height)); + } + + private static boolean isPacked(STBTTPackedchar packedChar) { + return packedChar.x0() != packedChar.x1() + || packedChar.y0() != packedChar.y1() + || packedChar.xadvance() != 0; + } + + private void ensureOpen() { + if (closed) throw new IllegalStateException("Font has already been closed."); + } + + private static int codePointAt(String string, int index, int end) { + char c = string.charAt(index); + + if (Character.isHighSurrogate(c) && index + 1 < end) { + char c2 = string.charAt(index + 1); + if (Character.isLowSurrogate(c2)) return Character.toCodePoint(c, c2); + } + + return c; + } + + private record FontData(ByteBuffer buffer, STBTTFontinfo info, float scale) {} + + private record GlyphMetrics(FontData font, float xAdvance) {} + + private record CharData(float x0, float y0, float x1, float y1, float u0, float v0, float u1, float v1, float xAdvance) { + private boolean hasBitmap() { + return x0 != x1 && y0 != y1; + } + } } diff --git a/src/main/java/meteordevelopment/meteorclient/systems/hud/HudRenderer.java b/src/main/java/meteordevelopment/meteorclient/systems/hud/HudRenderer.java index 4f13c5bcbac..4a2e18dde23 100644 --- a/src/main/java/meteordevelopment/meteorclient/systems/hud/HudRenderer.java +++ b/src/main/java/meteordevelopment/meteorclient/systems/hud/HudRenderer.java @@ -8,6 +8,7 @@ import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; +import com.google.common.cache.RemovalCause; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import meteordevelopment.meteorclient.MeteorClient; @@ -28,8 +29,6 @@ import org.joml.Quaternionf; import org.joml.Vector3f; -import java.io.IOException; -import java.nio.ByteBuffer; import java.time.Duration; import java.util.ArrayList; import java.util.Iterator; @@ -44,17 +43,22 @@ public class HudRenderer { private final Hud hud = Hud.get(); private final List postTasks = new ArrayList<>(); + private final Color shadowColor = new Color(CustomTextRenderer.SHADOW_COLOR); private final Int2ObjectMap fontsInUse = new Int2ObjectOpenHashMap<>(); private final LoadingCache fontCache = CacheBuilder.newBuilder() .maximumSize(4) .expireAfterAccess(Duration.ofMinutes(10)) .removalListener(notification -> { - if (notification.wasEvicted()) + if (notification.getCause() != RemovalCause.EXPLICIT) ((FontHolder) notification.getValue()).destroy(); }) .build(CacheLoader.from(HudRenderer::loadFont)); + private boolean frameActive; + private boolean customFontForFrame; + private boolean fontResetPending; + public GuiGraphicsExtractor graphics; public double delta; @@ -63,52 +67,69 @@ private HudRenderer() { } public void begin(GuiGraphicsExtractor graphics) { + if (frameActive) throw new IllegalStateException("HudRenderer.begin() called twice"); + Renderer2D.COLOR.begin(); this.graphics = graphics; this.delta = Utils.frameTime; + this.customFontForFrame = hud.hasCustomFont(); graphics.nextStratum(); - if (!hud.hasCustomFont()) { + if (!customFontForFrame) { VanillaTextRenderer.INSTANCE.scaleIndividually = true; VanillaTextRenderer.INSTANCE.begin(); } + + frameActive = true; } public void end() { - Renderer2D.COLOR.render(); - - if (hud.hasCustomFont()) { - // Render fonts that were visited this frame and move to cache which weren't visited - for (Iterator it = fontsInUse.values().iterator(); it.hasNext(); ) { - FontHolder fontHolder = it.next(); - - if (fontHolder.visited) { - MeshRenderer.begin() - .attachments(mc.getMainRenderTarget()) - .pipeline(MeteorRenderPipelines.UI_TEXT) - .mesh(fontHolder.getMesh()) - .sampler("u_Texture", fontHolder.font.texture.getTextureView(), fontHolder.font.texture.getSampler()) - .end(); - } else { - it.remove(); - fontCache.put(fontHolder.font.getHeight(), fontHolder); - } + if (!frameActive) throw new IllegalStateException("HudRenderer.end() called without calling begin()"); - fontHolder.visited = false; + try { + Renderer2D.COLOR.render(); + + if (customFontForFrame) { + // Render fonts that were visited this frame and move to cache which weren't visited + for (Iterator it = fontsInUse.values().iterator(); it.hasNext(); ) { + FontHolder fontHolder = it.next(); + + if (fontHolder.visited) { + fontHolder.font.uploadPendingGlyphs(); + + MeshRenderer.begin() + .attachments(mc.getMainRenderTarget()) + .pipeline(MeteorRenderPipelines.UI_TEXT) + .mesh(fontHolder.getMesh()) + .sampler("u_Texture", fontHolder.font.texture.getTextureView(), fontHolder.font.texture.getSampler()) + .end(); + } else { + it.remove(); + fontCache.put(fontHolder.font.getHeight(), fontHolder); + } + + fontHolder.visited = false; + } + } else { + VanillaTextRenderer.INSTANCE.end(); + VanillaTextRenderer.INSTANCE.scaleIndividually = false; } - } else { - VanillaTextRenderer.INSTANCE.end(); - VanillaTextRenderer.INSTANCE.scaleIndividually = false; - } - for (Runnable task : postTasks) task.run(); - postTasks.clear(); + for (Runnable task : postTasks) task.run(); - graphics.nextStratum(); + graphics.nextStratum(); + } finally { + postTasks.clear(); + graphics = null; + frameActive = false; - graphics = null; + if (fontResetPending) { + resetFonts(); + fontResetPending = false; + } + } } public void line(double x1, double y1, double x2, double y2, Color color) { @@ -136,7 +157,7 @@ public void texture(Identifier id, double x, double y, double width, double heig public double text(String text, double x, double y, Color color, boolean shadow, double scale) { if (scale == -1) scale = hud.getTextScale(); - if (!hud.hasCustomFont()) { + if (!usesCustomFont()) { VanillaTextRenderer.INSTANCE.scale = scale * 2; return VanillaTextRenderer.INSTANCE.render(text, x, y, color, shadow); } @@ -149,13 +170,10 @@ public double text(String text, double x, double y, Color color, boolean shadow, double width; if (shadow) { - int preShadowA = CustomTextRenderer.SHADOW_COLOR.a; - CustomTextRenderer.SHADOW_COLOR.a = (int) (color.a / 255.0 * preShadowA); + shadowColor.a = (int) (color.a / 255.0 * CustomTextRenderer.SHADOW_COLOR.a); - width = font.render(mesh, text, x + 1, y + 1, CustomTextRenderer.SHADOW_COLOR, scale); + width = font.render(mesh, text, x + 1, y + 1, shadowColor, scale); font.render(mesh, text, x, y, color, scale); - - CustomTextRenderer.SHADOW_COLOR.a = preShadowA; } else { width = font.render(mesh, text, x, y, color, scale); } @@ -170,7 +188,7 @@ public double text(String text, double x, double y, Color color, boolean shadow) public double textWidth(String text, boolean shadow, double scale) { if (text.isEmpty()) return 0; - if (hud.hasCustomFont()) { + if (usesCustomFont()) { double width = getFont(scale).getWidth(text, text.length()); return (width + (shadow ? 1 : 0)) * (scale == -1 ? hud.getTextScale() : scale) + (shadow ? 1 : 0); } @@ -192,7 +210,7 @@ public double textWidth(String text) { } public double textHeight(boolean shadow, double scale) { - if (hud.hasCustomFont()) { + if (usesCustomFont()) { double height = getFont(scale).getHeight() + 1; return (height + (shadow ? 1 : 0)) * (scale == -1 ? hud.getTextScale() : scale); } @@ -289,8 +307,21 @@ private Font getFont(double scale) { return getFontHolder(scale, false).font; } + private boolean usesCustomFont() { + return frameActive ? customFontForFrame : hud.hasCustomFont(); + } + @EventHandler private void onCustomFontChanged(CustomFontChangedEvent event) { + if (frameActive) { + fontResetPending = true; + return; + } + + resetFonts(); + } + + private void resetFonts() { // Need to destroy both fonts in use and in cache because they were not evicted from the cache automatically for (FontHolder fontHolder : fontsInUse.values()) fontHolder.destroy(); for (FontHolder fontHolder : fontCache.asMap().values()) fontHolder.destroy(); @@ -301,12 +332,7 @@ private void onCustomFontChanged(CustomFontChangedEvent event) { } private static FontHolder loadFont(int height) { - try { - ByteBuffer buffer = Fonts.RENDERER.fontFace.readToDirectByteBuffer(); - return new FontHolder(new Font(buffer, height)); - } catch (IOException e) { - throw new RuntimeException("Failed to load font: " + Fonts.RENDERER.fontFace, e); - } + return new FontHolder(Fonts.RENDERER.createFont(height)); } private static class FontHolder { @@ -326,7 +352,7 @@ public MeshBuilder getMesh() { } public void destroy() { - font.texture.close(); + font.close(); } } }