From 28d7b02350afab88c44103fdaf70236ff28e7ddd Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 12 Mar 2026 17:26:26 +0200 Subject: [PATCH] OMG BEST FONT RENDERING EVER FUUUUCK --- .../java/noppes/npcs/config/GlyphCache.java | 90 ++++++++++++++++--- .../java/noppes/npcs/config/StringCache.java | 84 ++++++++++++----- 2 files changed, 139 insertions(+), 35 deletions(-) diff --git a/src/main/java/noppes/npcs/config/GlyphCache.java b/src/main/java/noppes/npcs/config/GlyphCache.java index 5379cb1b9..76e74fb2d 100644 --- a/src/main/java/noppes/npcs/config/GlyphCache.java +++ b/src/main/java/noppes/npcs/config/GlyphCache.java @@ -1,6 +1,7 @@ package noppes.npcs.config; import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.ScaledResolution; import net.minecraft.client.renderer.GLAllocation; import net.minecraft.util.ResourceLocation; import org.lwjgl.opengl.GL11; @@ -91,6 +92,21 @@ public class GlyphCache { */ private boolean antiAliasEnabled = false; + /** + * The current Minecraft GUI scale factor (1-4). Glyphs are rasterized at fontSize * guiScaleFactor + * so the atlas has enough texel density for the actual display resolution. This prevents + * GL_LINEAR texture filtering from diluting alpha values at high resolutions. + */ + private int guiScaleFactor = 1; + + /** + * Return the GUI scale factor used for glyph rasterization. StringCache uses this + * to compute an effective render scale that compensates for the larger atlas images. + */ + int getGuiScaleFactor() { + return guiScaleFactor; + } + /** * Temporary image for rendering a string to and then extracting the glyph images from. @@ -103,6 +119,7 @@ public class GlyphCache { private Graphics2D stringGraphics; + /** * All font glyphs are packed inside this image and are then loaded from here into an OpenGL texture. */ @@ -280,6 +297,7 @@ public class GlyphCache { fontSize = size; antiAliasEnabled = antiAlias; + guiScaleFactor = queryGuiScaleFactor(); setRenderingHints(); } @@ -295,10 +313,50 @@ public class GlyphCache { fontSize = size; antiAliasEnabled = antiAlias; + guiScaleFactor = queryGuiScaleFactor(); setRenderingHints(); } } + private static int queryGuiScaleFactor() { + try { + Minecraft mc = Minecraft.getMinecraft(); + if (mc != null && mc.displayWidth > 0 && mc.displayHeight > 0) { + ScaledResolution sr = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + return Math.max(1, sr.getScaleFactor()); + } + } catch (Exception ignored) { + } + return 1; + } + + /** + * Check if the GUI scale factor has changed (e.g. due to window resize) and invalidate the glyph + * cache if so. Glyphs must be re-rasterized at the new density to prevent GL_LINEAR filtering + * from diluting alpha at high display resolutions. + * + * @return true if the scale factor changed and glyphs were invalidated + */ + boolean checkAndUpdateScaleFactor() { + int newScale = queryGuiScaleFactor(); + if (newScale != guiScaleFactor) { + guiScaleFactor = newScale; + invalidateGlyphCache(); + return true; + } + return false; + } + + private void invalidateGlyphCache() { + glyphCache.clear(); + fontCache.clear(); + cachePosX = GLYPH_BORDER; + cachePosY = GLYPH_BORDER; + cacheLineHeight = 0; + allocateGlyphCacheTexture(); + allocateStringImage(STRING_WIDTH, STRING_HEIGHT); + } + int fontHeight(String s) { Font font = lookupFont(s.toCharArray(), 0, s.length(), 0); return (int) (font.getLineMetrics(s, this.fontRenderContext).getHeight()); @@ -338,6 +396,9 @@ public class GlyphCache { * @return an OpenType font capable of displaying at least the first character at the start position in text */ Font lookupFont(char text[], int start, int limit, int style) { + /* Rasterize at scaled size so atlas texel density matches display resolution */ + int renderSize = fontSize * guiScaleFactor; + /* Try using an already known base font; the first font in usedFonts list is the one set with setDefaultFont() */ Iterator iterator = usedFonts.iterator(); while (iterator.hasNext()) { @@ -345,7 +406,7 @@ public class GlyphCache { Font font = iterator.next(); if (font.canDisplayUpTo(text, start, limit) != start) { /* Return a font instance of the proper point size and style; usedFonts has only 1pt sized plain style fonts */ - return font.deriveFont(style, fontSize); + return font.deriveFont(style, renderSize); } } @@ -360,7 +421,7 @@ public class GlyphCache { usedFonts.add(font); /* Return a font instance of the proper point size and style; allFonts has only 1pt sized plain style fonts */ - return font.deriveFont(style, fontSize); + return font.deriveFont(style, renderSize); } } @@ -368,7 +429,7 @@ public class GlyphCache { Font font = usedFonts.get(0); /* Return a font instance of the proper point size and style; usedFonts only 1pt sized plain style fonts */ - return font.deriveFont(style, fontSize); + return font.deriveFont(style, renderSize); } /** @@ -460,7 +521,7 @@ public class GlyphCache { /* Expand bounds to account for hinting overflow */ vectorBounds.grow(GLYPH_PADDING, GLYPH_PADDING); - /* Enlage the stringImage if it is too small to store the entire rendered string */ + /* Enlarge the stringImage if it is too small to store the entire rendered string */ if (stringImage == null || vectorBounds.width > stringImage.getWidth() || vectorBounds.height > stringImage.getHeight()) { int width = Math.max(vectorBounds.width, stringImage.getWidth()); int height = Math.max(vectorBounds.height, stringImage.getHeight()); @@ -615,16 +676,19 @@ public class GlyphCache { stringGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antiAliasEnabled ? RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF); - if (antiAliasEnabled) { - stringGraphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, - RenderingHints.VALUE_TEXT_ANTIALIAS_GASP); - stringGraphics.setRenderingHint(RenderingHints.KEY_RENDERING, - RenderingHints.VALUE_RENDER_QUALITY); - } else { - stringGraphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, - RenderingHints.VALUE_TEXT_ANTIALIAS_OFF); - } +// if (antiAliasEnabled) { +// stringGraphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, +// RenderingHints.VALUE_TEXT_ANTIALIAS_ON); +// stringGraphics.setRenderingHint(RenderingHints.KEY_RENDERING, +// RenderingHints.VALUE_RENDER_QUALITY); +// } else { +// stringGraphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, +// RenderingHints.VALUE_TEXT_ANTIALIAS_OFF); +// } + stringGraphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, + RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + stringGraphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); stringGraphics.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_OFF); fontRenderContext = stringGraphics.getFontRenderContext(); diff --git a/src/main/java/noppes/npcs/config/StringCache.java b/src/main/java/noppes/npcs/config/StringCache.java index 773e0680c..e8d67ee32 100644 --- a/src/main/java/noppes/npcs/config/StringCache.java +++ b/src/main/java/noppes/npcs/config/StringCache.java @@ -73,8 +73,9 @@ public class StringCache { */ private static final int STRIKETHROUGH_THICKNESS = 2; - // BetterFonts renders glyphs at 2x internal resolution; this scale converts to GUI coords - private static final float RENDER_SCALE = 0.5F; + // BetterFonts base scale: maps internal 2x glyph coords to GUI coords. + // With scale-aware rasterization, the effective scale becomes BASE_RENDER_SCALE / guiScaleFactor. + private static final float BASE_RENDER_SCALE = 0.5F; private static final boolean FONT_DEBUG_LOG = Boolean.getBoolean("cnpc.font.debug"); private static final boolean FONT_DEBUG_DISABLE_INK_OFFSET = Boolean.getBoolean("cnpc.font.debug.disableInkOffset"); @@ -90,8 +91,14 @@ public class StringCache { return c == '"' ? "\\\"" : String.valueOf(c); } - private static float pixelSnap(float coord) { - return (float) Math.floor(coord * 2.0F + 0.5F) * RENDER_SCALE; + private float renderScale() { + return BASE_RENDER_SCALE / glyphCache.getGuiScaleFactor(); + } + + private float pixelSnap(float coord) { + float scale = renderScale(); + float invScale = 1.0F / scale; + return (float) Math.floor(coord * invScale + 0.5F) * scale; } private int baselineOffset = BASELINE_OFFSET; @@ -468,7 +475,8 @@ public class StringCache { if (g.y > maxY) maxY = g.y; } - fontHeight = (int) ((maxY + height) / 2.0F - minY / 2.0F); + int scaleFactor = glyphCache.getGuiScaleFactor(); + fontHeight = (int) (((maxY + height) - minY) / 2.0F / scaleFactor); updateFontMetrics(); } @@ -477,11 +485,36 @@ public class StringCache { float ascent = lm.getAscent(); float descent = lm.getDescent(); - baselineOffset = (int) (ascent * RENDER_SCALE + 0.5f); + /* + * baselineOffset is added directly to startY (GUI pixels) at render time, so it must be a + * scale-invariant GUI-pixel value. The constant BASELINE_OFFSET (7) is correct for the default + * 18pt font and remains stable across all GUI scale factors because it operates in GUI coordinate + * space — independent of the glyph rasterization scale. + * + * A dynamic formula like (ascent - padding) * renderScale is fragile: ascent comes from the + * scaled font (fontSize * scaleFactor) and rounding/font-metric variations across scales can + * produce values 1-2 pixels too large, visibly shifting text down within its highlight box. + */ + baselineOffset = BASELINE_OFFSET; + + /* + * underlineOffset and strikethroughOffset are multiplied by renderScale() at draw time + * (same as glyph.x/glyph.y), so they must be in glyph-space pixels. These scale correctly + * because ascent/descent are proportional to the rasterization size. + */ underlineOffset = (int) (descent * 0.25f + 0.5f); strikethroughOffset = -(int) (ascent * 0.35f + 0.5f); } + private void checkForScaleChange() { + if (glyphCache.checkAndUpdateScaleFactor()) { + weakRefCache.clear(); + stringCache.clear(); + cacheDightGlyphs(); + updateHeight(); + } + } + /** * Pre-cache the ASCII digits to allow for fast glyph substitution. Called once from the constructor and called any time the font selection * changes at runtime via setDefaultFont(). @@ -518,6 +551,8 @@ public class StringCache { return 0; } + /* Detect window resize: if GUI scale factor changed, invalidate all caches and re-rasterize */ + checkForScaleChange(); /* Make sure the entire string is cached before rendering and return its glyph representation */ Entry entry = cacheString(str); @@ -615,10 +650,11 @@ public class StringCache { if (FONT_DEBUG_DISABLE_INK_OFFSET) { glyphX = glyph.x; } - float x1 = pixelSnap(startX + (glyphX) * RENDER_SCALE); - float x2 = pixelSnap(startX + (glyphX + texture.width) * RENDER_SCALE); - float y1 = pixelSnap(startY + (glyph.y) * RENDER_SCALE); - float y2 = pixelSnap(startY + (glyph.y + texture.height) * RENDER_SCALE); + float rs = renderScale(); + float x1 = pixelSnap(startX + (glyphX) * rs); + float x2 = pixelSnap(startX + (glyphX + texture.width) * rs); + float y1 = pixelSnap(startY + (glyph.y) * rs); + float y2 = pixelSnap(startY + (glyph.y + texture.height) * rs); if (FONT_DEBUG_LOG && isSpacingDebugGlyph(c)) { LogWriter.info("[StringCache glyph-debug] char='" + debugGlyphLabel(c) + "' idx=" + glyph.stringIndex + @@ -667,10 +703,11 @@ public class StringCache { /* Draw underline under glyph if the style is enabled */ if ((renderStyle & ColorCode.UNDERLINE) != 0) { /* The divide by 2.0F is needed to align with the scaled GUI coordinate system; startX/startY are already scaled */ - float x1 = pixelSnap(startX + (glyph.x - glyphSpace) * RENDER_SCALE); - float x2 = pixelSnap(startX + (glyph.x + glyph.advance) * RENDER_SCALE); - float y1 = pixelSnap(startY + (underlineOffset) * RENDER_SCALE); - float y2 = pixelSnap(startY + (underlineOffset + UNDERLINE_THICKNESS) * RENDER_SCALE); + float urs = renderScale(); + float x1 = pixelSnap(startX + (glyph.x - glyphSpace) * urs); + float x2 = pixelSnap(startX + (glyph.x + glyph.advance) * urs); + float y1 = pixelSnap(startY + (underlineOffset) * urs); + float y2 = pixelSnap(startY + (underlineOffset + UNDERLINE_THICKNESS) * urs); tessellator.addVertex(x1, y1, 0); tessellator.addVertex(x1, y2, 0); @@ -681,10 +718,11 @@ public class StringCache { /* Draw strikethrough in the middle of glyph if the style is enabled */ if ((renderStyle & ColorCode.STRIKETHROUGH) != 0) { /* The divide by 2.0F is needed to align with the scaled GUI coordinate system; startX/startY are already scaled */ - float x1 = pixelSnap(startX + (glyph.x - glyphSpace) * RENDER_SCALE); - float x2 = pixelSnap(startX + (glyph.x + glyph.advance) * RENDER_SCALE); - float y1 = pixelSnap(startY + (strikethroughOffset) * RENDER_SCALE); - float y2 = pixelSnap(startY + (strikethroughOffset + STRIKETHROUGH_THICKNESS) * RENDER_SCALE); + float srs = renderScale(); + float x1 = pixelSnap(startX + (glyph.x - glyphSpace) * srs); + float x2 = pixelSnap(startX + (glyph.x + glyph.advance) * srs); + float y1 = pixelSnap(startY + (strikethroughOffset) * srs); + float y2 = pixelSnap(startY + (strikethroughOffset + STRIKETHROUGH_THICKNESS) * srs); tessellator.addVertex(x1, y1, 0); tessellator.addVertex(x1, y2, 0); @@ -700,7 +738,7 @@ public class StringCache { /* Return total horizontal advance (slightly wider than the bounding box, but close enough for centering strings) */ - return entry.advance / 2; + return (int) (entry.advance * renderScale()); } /** @@ -715,11 +753,13 @@ public class StringCache { return 0; } + checkForScaleChange(); + /* Make sure the entire string is cached and rendered since it will probably be used again in a renderString() call */ Entry entry = cacheString(str); /* Return total horizontal advance (slightly wider than the bounding box, but close enough for centering strings) */ - return entry.advance / 2; + return (int) (entry.advance * renderScale()); } /** @@ -738,8 +778,8 @@ public class StringCache { return 0; } - /* Convert the width from GUI coordinate system to pixels */ - width += width; + /* Convert the width from GUI coordinate system to internal glyph pixels */ + width = (int) (width / renderScale()); /* The glyph array for a string is sorted by the string's logical character position */ Glyph glyphs[] = cacheString(str).glyphs;