From 4ccf0ba44866e11b18a0cc495ee1c06cf0ec0942 Mon Sep 17 00:00:00 2001 From: Gugle Date: Sat, 20 Jun 2026 18:31:25 +0800 Subject: [PATCH 01/16] =?UTF-8?q?feat(gradle):=20=E9=9B=86=E6=88=90=20Rose?= =?UTF-8?q?au=20=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 roseau.yaml 配置文件定义排除规则 - 在 gradle 脚本中注册 roseauConfig 变量 - 将配置参数传递给 roseauCheck 任务 - 定义废弃和实验性注解的排除列表 - 配置报告输出格式和路径 - 设置基础版本依赖和执行配置 --- gradle/scripts/roseau.gradle | 2 ++ roseau.yaml | 9 +++++++++ 2 files changed, 11 insertions(+) create mode 100644 roseau.yaml diff --git a/gradle/scripts/roseau.gradle b/gradle/scripts/roseau.gradle index ec0ca4e6..7a427414 100644 --- a/gradle/scripts/roseau.gradle +++ b/gradle/scripts/roseau.gradle @@ -42,6 +42,7 @@ def roseauMvnCoord = "${roseauGroup}:${roseauArtifact}:${roseauBaselineVersion}" def roseauBaselineDep = dependencies.create(group: roseauGroup, name: roseauArtifact, version: roseauBaselineVersion) def roseauBaselineConfig = configurations.detachedConfiguration(roseauBaselineDep) def roseauBaselineJarProvider = provider { roseauBaselineConfig.singleFile } +def roseauConfig = rootProject.file("roseau.yaml").absolutePath tasks.register('roseauCheck', JavaExec) { group = 'verification' @@ -71,6 +72,7 @@ tasks.register('roseauCheck', JavaExec) { '--fail-on-bc', '--report', "HTML=${new File(reportsDir, 'report.html').absolutePath}", '--report', "CSV=${new File(reportsDir, 'report.csv').absolutePath}", + '--config', roseauConfig ) } } diff --git a/roseau.yaml b/roseau.yaml new file mode 100644 index 00000000..1f196728 --- /dev/null +++ b/roseau.yaml @@ -0,0 +1,9 @@ +common: + excludes: + annotations: + - name: java.lang.Deprecated + - name: com.google.common.annotations.Beta + - name: org.jetbrains.annotations.ApiStatus$Experimental + - name: org.jetbrains.annotations.ApiStatus$Internal + - name: org.jetbrains.annotations.ApiStatus$Obsolete + - name: org.jetbrains.annotations.ApiStatus$ScheduledForRemoval From ac156026ff8f5ef0db23c6144d31b9ca45b33439 Mon Sep 17 00:00:00 2001 From: Gugle Date: Sat, 20 Jun 2026 18:49:23 +0800 Subject: [PATCH 02/16] =?UTF-8?q?refactor(font):=20=E4=BC=98=E5=8C=96SDF?= =?UTF-8?q?=E5=AD=97=E4=BD=93=E6=B8=B2=E6=9F=93=E6=80=A7=E8=83=BD=E5=92=8C?= =?UTF-8?q?=E5=BC=82=E6=AD=A5=E5=AD=97=E5=BD=A2=E5=88=9B=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现异步字形创建,避免渲染线程阻塞 - 添加布局缓存机制,减少重复计算开销 - 修复基线偏移问题,解决降部字母裁剪 - 修正cellSize计算,确保完整容纳字形 - 使用ConcurrentHashMap替代HashMap提高并发性能 - 添加单线程执行器处理后台字形创建任务 - 实现字形页同步机制避免读写冲突 - 优化纹理上传时的图像数据转换 - 添加字形布局缓存失效和清理机制 - 修正awtHeight返回实际字体度量高度 --- module.font/TODO.md | 68 ++++++++++++++++ .../lib/v2/font/sdf/SdfAtlasTexture.java | 8 +- .../lib/v2/font/sdf/SdfGlyphAtlas.java | 78 +++++++++++++++---- .../lib/v2/font/sdf/SdfGlyphPage.java | 6 +- .../lib/v2/font/sdf/SdfTextLayout.java | 76 ++++++++++++++++-- .../lib/v2/font/sdf/SdfTextRenderer.java | 14 ++-- .../v2/font/sdf/state/SdfTextRenderState.java | 12 ++- 7 files changed, 229 insertions(+), 33 deletions(-) create mode 100644 module.font/TODO.md diff --git a/module.font/TODO.md b/module.font/TODO.md new file mode 100644 index 00000000..14e85fc3 --- /dev/null +++ b/module.font/TODO.md @@ -0,0 +1,68 @@ +# module.font Performance & Rendering Fixes + +## P0 - Critical (causes visible stutter & missing text) + +### 1. Layout Cache -- DONE +- **File:** `SdfTextLayout.java`, `SdfTextRenderer.java`, `SdfTextRenderState.java` +- **Problem:** Every frame, the entire text string is re-laid-out: codepoint -> glyph lookup, UV calculation, quad bucketing per page, new object allocations (LinkedHashMap, ArrayList, GlyphQuad, SdfTextRenderState) +- **Fix:** Cache `SdfTextLayout` results keyed by `(atlasKey, text, scale)`. Store quad positions relative to origin; apply offset during `buildVertices()`. + +### 2. Async Glyph Creation -- DONE +- **File:** `SdfGlyphAtlas.java`, `SdfAtlasTexture.java` +- **Problem:** `glyph()` calls `createGlyph()` synchronously on the render thread when a glyph is not yet in the atlas. For CJK text, hundreds of glyphs may need creation, each involving AWT rendering + EDT distance transform (~O(n^2) per glyph), blocking the render loop for multiple frames. +- **Fix:** Return null for not-yet-created codepoints, enqueue async creation on a background single-threaded executor. Added `pendingGlyphs` set to prevent duplicate creation requests. `synchronized` on atlas for glyph creation, synchronized on page for texture upload. + +### 3. Fix `quadY` Baseline Offset -- DONE +- **File:** `SdfTextRenderer.java` +- **Problem:** `quadY = y - 2` is a hardcoded magic number. Doesn't account for actual atlas baseline position or scale, causing text below baseline (descenders: g, j, p, q, y) to be clipped. +- **Fix:** `quadY = y - Math.round((atlas.awtAscent() + 2) * scale)` + +## P1 - Significant improvement + +### 4. Fix `cellSize` to Fit Full Glyph -- DONE +- **File:** `SdfGlyphAtlas.java:55` +- **Problem:** `cellSize = Math.max(24, font.getSize() + 12)` - 64pt font -> cellSize=76. But 64pt ascent+descent can exceed 76px, clipping ascenders and descenders. +- **Fix:** `cellSize = Math.max(24, awtAscent + awtDescent + 4)` + +### 5. Remove Baseline Clamp in `renderMask()` -- DONE +- **File:** `SdfGlyphAtlas.java:224` +- **Problem:** `Math.min(this.cellSize - 4, this.awtAscent + 2)` clamps baseline when ascent is large, cutting off descenders. +- **Fix:** `g.drawString(s, 2, this.awtAscent + 2)` - no clamping needed if cellSize is large enough (see #4). + +### 6. Fix `awtHeight()` to Return Actual Metrics Height -- DONE +- **File:** `SdfGlyphAtlas.java` +- **Problem:** `awtHeight()` returned `font.getSize()` (point size, e.g., 64), not the actual pixel height of the font. This affected `scaleFor()` calculation. +- **Fix:** Now returns `awtHeight` (actual FontMetrics height = ascent + descent + leading). + +### 7. Replace `hashImage()` with Monotonic Version Number +- **File:** `SdfAtlasTexture.java:103-111`, `SdfGlyphPage.java` +- **Problem:** `hashImage()` iterates all 1,048,576 pixels every time a new glyph is added. Called from `SdfGlyphPage.updateHash()` after each glyph creation and after `measureText()`. +- **Fix:** Use an atomic version counter: `page.version++` on mutation, compare version instead of content hash. + +## P2 - Nice to have + +### 8. Optimize `toNativeImage()` with Bulk Copy +- **File:** `SdfAtlasTexture.java:75-84` +- **Problem:** Per-pixel `getRGB()`/`setPixel()` loop over 1M pixels during texture upload. +- **Fix:** Use `NativeImage` bulk write or `BufferedImage.getRaster().getDataElements()` for batch transfer. + +### 9. Object Pooling for Layout Temporaries +- **File:** `SdfTextLayout.java`, `SdfTextRenderer.java` +- **Problem:** Each draw call allocates new `LinkedHashMap`, multiple `ArrayList`, `GlyphQuad` records, `SdfTextRenderState` records, causing GC pressure. +- **Fix:** Thread-local pools for `ArrayList`, `StringBuilder`, and vertex buffers. Reuse layout intermediates. + +--- + +## Summary + +| # | Task | Status | Impact | Effort | +|---|-------------------------|--------|-----------------------------------|---------| +| 1 | Layout Cache | DONE | Eliminates repeated CPU work | Medium | +| 2 | Async Glyph Creation | DONE | Eliminates render-thread blocking | Medium | +| 3 | Fix quadY offset | DONE | Fixes descender clipping | Trivial | +| 4 | Fix cellSize | DONE | Fixes glyph clipping in atlas | Small | +| 5 | Remove baseline clamp | DONE | Fixes descender clipping | Trivial | +| 6 | Fix awtHeight() | DONE | Corrects text scaling | Trivial | +| 7 | Version instead of hash | TODO | ~1M fewer pixel reads per glyph | Small | +| 8 | Bulk NativeImage copy | TODO | Faster texture upload | Small | +| 9 | Object pooling | TODO | Reduced GC pauses | Medium | diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfAtlasTexture.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfAtlasTexture.java index 11ef20a1..a3b161ee 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfAtlasTexture.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfAtlasTexture.java @@ -34,7 +34,13 @@ public static Identifier uploadPage(SdfGlyphAtlas atlas, int pageIndex) { if (entry != null && entry.hash == hash) return entry.id; Identifier id = Identifier.fromNamespaceAndPath("anvillib_font", "dynamic/sdf_atlas/" + sanitize(key)); - SdfTexture texture = new SdfTexture(toNativeImage(page.image)); + // Synchronize on page to avoid reading image data while the async + // glyph creation thread is writing to it. + NativeImage nativeImage; + synchronized (page) { + nativeImage = toNativeImage(page.image); + } + SdfTexture texture = new SdfTexture(nativeImage); Minecraft.getInstance().getTextureManager().register(id, texture); if (entry != null) entry.texture.close(); diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java index 8f263ebe..5939de02 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java @@ -1,6 +1,8 @@ package dev.anvilcraft.lib.v2.font.sdf; import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.awt.Color; import java.awt.Font; @@ -9,7 +11,6 @@ import java.awt.RenderingHints; import java.awt.image.BufferedImage; import java.util.ArrayList; -import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; @@ -17,6 +18,8 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.function.Consumer; /** @@ -26,6 +29,7 @@ * pre-warmed; all other codepoints are rendered lazily on first use. */ public final class SdfGlyphAtlas { + private static final Logger LOGGER = LoggerFactory.getLogger(SdfGlyphAtlas.class); static final int PAGE_SIZE = 1024; private static final int FIRST_CHAR = 32; private static final int LAST_CHAR = 126; @@ -36,28 +40,36 @@ public final class SdfGlyphAtlas { private static final Map> CACHE = new ConcurrentHashMap<>(); + /** + * Single-threaded executor for background glyph creation. + * All glyph creation (including SDF computation) happens on this thread + * to avoid blocking the render loop. + */ + private static final ExecutorService GLYPH_EXECUTOR = Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r, "AnvilLib-SDF-Glyph"); + t.setDaemon(true); + return t; + }); + private final String key; private final Font font; final int cellSize; final int padding; final int paddedCellSize; final float sdfRadius; - private int awtAscent; - private int awtHeight; + private final int awtAscent; + private final int awtHeight; private final FontMetrics fontMetrics; private final List pages = new ArrayList<>(); - private final Map glyphMap = new HashMap<>(); + private final Map glyphMap = new ConcurrentHashMap<>(); + private final Set pendingGlyphs = ConcurrentHashMap.newKeySet(); private SdfGlyphAtlas(String key, Font font) { this.key = key; this.font = font; - this.cellSize = Math.max(24, font.getSize() + 12); - this.sdfRadius = Math.max(12, font.getSize() * 0.25f); - this.padding = Math.max(4, this.cellSize / 6); - this.paddedCellSize = this.cellSize + 2 * this.padding; - // Capture font metrics + // Capture font metrics first — needed by cellSize calculation BufferedImage tmp = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB); Graphics2D g = tmp.createGraphics(); try { @@ -69,6 +81,15 @@ private SdfGlyphAtlas(String key, Font font) { g.dispose(); } + // Cell must accommodate full ascent + descent above and below baseline, + // plus 2px margin on each side. Previously used font.getSize()+12, which + // could clip descenders or ascenders for fonts with large extents. + int awtDescent = this.fontMetrics.getDescent(); + this.cellSize = Math.max(24, this.awtAscent + awtDescent + 4); + this.sdfRadius = Math.max(12, font.getSize() * 0.25f); + this.padding = Math.max(4, this.cellSize / 6); + this.paddedCellSize = this.cellSize + 2 * this.padding; + preWarmAscii(); } @@ -115,7 +136,7 @@ public Font font() { } public int awtHeight() { - return this.font.getSize(); + return this.awtHeight; } public int awtAscent() { @@ -131,12 +152,40 @@ public SdfGlyphPage page(int index) { } /** - * Get (or lazily create) the glyph entry for a codepoint. + * Get the glyph entry for a codepoint. If the glyph is not yet in the + * atlas, requests async creation and returns {@code null}. The caller + * should handle the missing glyph gracefully (e.g. advance by a default + * width); the glyph will be available on the next call. */ public @Nullable GlyphEntry glyph(int codepoint, Consumer pageConsumer) { GlyphEntry entry = this.glyphMap.get(codepoint); if (entry != null) return entry; - return createGlyph(codepoint, pageConsumer); + // Trigger async creation on background thread + if (this.pendingGlyphs.add(codepoint)) { + GLYPH_EXECUTOR.submit(() -> createGlyphAsync(codepoint)); + } + return null; + } + + /** + * Background-thread entry point for glyph creation. + * Synchronized to ensure only one glyph is created at a time per atlas, + * avoiding races on page state (nextCol, nextRow, image pixels). + */ + private void createGlyphAsync(int codepoint) { + try { + synchronized (this) { + // Double-check: may have been created since we were enqueued + if (this.glyphMap.containsKey(codepoint)) return; + createGlyph(codepoint, SdfGlyphPage::updateHash); + } + // Invalidate cached layouts so they pick up the new glyph + SdfTextLayout.invalidateAtlas(this.key); + } catch (Exception e) { + LOGGER.error("Failed to create SDF glyph for codepoint {} (U+{})", codepoint, Integer.toHexString(codepoint).toUpperCase(), e); + } finally { + this.pendingGlyphs.remove(codepoint); + } } public int measureText(String text) { @@ -169,9 +218,8 @@ public int measureCodepoint(int codepoint) { // ── Glyph creation ────────────────────────────────────────── - private @Nullable GlyphEntry createGlyph(int codepoint, Consumer pageConsumer) { + private GlyphEntry createGlyph(int codepoint, Consumer pageConsumer) { BufferedImage mask = renderMask(codepoint); - if (mask == null) return null; int pageIdx = findOrCreatePageIndex(); SdfGlyphPage page = this.pages.get(pageIdx); @@ -221,7 +269,7 @@ private BufferedImage renderMask(int codepoint) { g.setColor(new Color(0, 0, 0, 0)); g.fillRect(0, 0, this.cellSize, this.cellSize); g.setColor(Color.WHITE); - g.drawString(s, 2, Math.min(this.cellSize - 4, this.awtAscent + 2)); + g.drawString(s, 2, this.awtAscent + 2); } finally { g.dispose(); } diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphPage.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphPage.java index 955143fe..b78bb5d6 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphPage.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphPage.java @@ -14,7 +14,7 @@ public final class SdfGlyphPage { final int cols, rows; int nextCol, nextRow; Identifier textureId; - boolean dirty = true; + volatile boolean dirty = true; SdfGlyphPage(int paddedCellSize) { this.cols = SIZE / paddedCellSize; @@ -26,7 +26,7 @@ boolean hasSpace() { return nextRow < rows; } - SdfGlyphAtlas.GlyphEntry placeGlyph(SdfGlyphAtlas atlas, BufferedImage mask) { + synchronized SdfGlyphAtlas.GlyphEntry placeGlyph(SdfGlyphAtlas atlas, BufferedImage mask) { int col = nextCol, row = nextRow; int padX = col * atlas.paddedCellSize; int padY = row * atlas.paddedCellSize; @@ -42,7 +42,7 @@ SdfGlyphAtlas.GlyphEntry placeGlyph(SdfGlyphAtlas atlas, BufferedImage mask) { return new SdfGlyphAtlas.GlyphEntry(0, innerX, innerY, atlas.cellSize, atlas.cellSize, 0); } - void fillPaddingForCell(SdfGlyphAtlas atlas, SdfGlyphAtlas.GlyphEntry e) { + synchronized void fillPaddingForCell(SdfGlyphAtlas atlas, SdfGlyphAtlas.GlyphEntry e) { int col = (e.atlasX() - atlas.padding) / atlas.paddedCellSize; int row = (e.atlasY() - atlas.padding) / atlas.paddedCellSize; SdfGlyphAtlas.fillPadding( diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextLayout.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextLayout.java index 434825ad..9ff9639b 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextLayout.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextLayout.java @@ -7,12 +7,21 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; /** * CPU-side glyph layout output for SDF text rendering, grouped by atlas page. + *

+ * Layouts are cached by {@code (atlasKey, text, scale)} to avoid redundant + * computation each frame. Quads are stored with positions relative to the + * layout origin; the renderer supplies the screen-space offset. */ public final class SdfTextLayout { + private static final int MAX_CACHE_SIZE = 1024; + private static final Map LAYOUT_CACHE = new ConcurrentHashMap<>(); + private final List pages; private final int width; private final int height; @@ -23,12 +32,60 @@ private SdfTextLayout(List pages, int width, int height) { this.height = height; } + /** + * Compute glyph layout for a string. The returned quads have positions + * relative to (0, 0); add {@code x}/{@code y} when rendering. + *

+ * Result is cached when all glyphs are available in the atlas. + */ public static SdfTextLayout fromAtlas(SdfGlyphAtlas atlas, @Nullable String text, int x, int y, float scale) { if (text == null || text.isEmpty()) - return new SdfTextLayout(List.of(), 0, 0); + return EMPTY; + + int scaleInt = Math.round(scale * 1000f); + CacheKey key = new CacheKey(atlas.key(), text, scaleInt); + SdfTextLayout cached = LAYOUT_CACHE.get(key); + if (cached != null) return cached; + + SdfTextLayout layout = computeLayout(atlas, text, scale, scaleInt); + if (layout != null && layout.allGlyphsAvailable) { + evictIfNeeded(); + LAYOUT_CACHE.put(key, layout); + } + return layout; + } + + /** + * Invalidate cached layouts for a specific atlas (e.g. after glyph additions). + */ + public static void invalidateAtlas(String atlasKey) { + LAYOUT_CACHE.keySet().removeIf(k -> k.atlasKey.equals(atlasKey)); + } + + /** + * Clear all cached layouts. + */ + public static void clearCache() { + LAYOUT_CACHE.clear(); + } + + private static void evictIfNeeded() { + if (LAYOUT_CACHE.size() >= MAX_CACHE_SIZE) { + // Evict half the entries (simple random-ish eviction via iterator) + var it = LAYOUT_CACHE.keySet().iterator(); + int toRemove = MAX_CACHE_SIZE / 2; + for (int i = 0; i < toRemove && it.hasNext(); i++) { + it.next(); + it.remove(); + } + } + } - int penX = x, maxHeight = 0, totalWidth = 0; + @Nullable + private static SdfTextLayout computeLayout(SdfGlyphAtlas atlas, String text, float scale, int scaleInt) { + int penX = 0, maxHeight = 0; java.util.Map> buckets = new java.util.LinkedHashMap<>(); + boolean allAvailable = true; Set glyphPages = new HashSet<>(); for (int ci = 0; ci < text.length(); ) { @@ -37,6 +94,7 @@ public static SdfTextLayout fromAtlas(SdfGlyphAtlas atlas, @Nullable String text SdfGlyphAtlas.GlyphEntry glyph = atlas.glyph(cp, glyphPages::add); if (glyph == null) { + allAvailable = false; penX += Math.round(Math.max(6, atlas.font().getSize() / 2) * scale); continue; } @@ -53,7 +111,8 @@ public static SdfTextLayout fromAtlas(SdfGlyphAtlas atlas, @Nullable String text int w = Math.round(glyph.width() * scale); int h = Math.round(glyph.height() * scale); - GlyphQuad quad = new GlyphQuad(penX, y, penX + w, y + h, u0, v0, u1, v1, (char) cp); + // Positions relative to layout origin (0, 0) + GlyphQuad quad = new GlyphQuad(penX, 0, penX + w, h, u0, v0, u1, v1, (char) cp); buckets.computeIfAbsent(glyph.pageIndex(), _ -> new ArrayList<>()).add(quad); penX += Math.max(1, Math.round(glyph.advance() * scale)); @@ -69,14 +128,21 @@ public static SdfTextLayout fromAtlas(SdfGlyphAtlas atlas, @Nullable String text pages.add(new PageQuads(pi, tex, page.image.getWidth(), page.image.getHeight(), entry.getValue())); } - totalWidth = Math.max(0, penX - x); - return new SdfTextLayout(pages, totalWidth, maxHeight); + SdfTextLayout layout = new SdfTextLayout(pages, penX, maxHeight); + layout.allGlyphsAvailable = allAvailable; + return layout; } + private boolean allGlyphsAvailable = true; + + private static final SdfTextLayout EMPTY = new SdfTextLayout(List.of(), 0, 0); + public List pages() { return Collections.unmodifiableList(this.pages); } public int width() { return this.width; } public int height() { return this.height; } + private record CacheKey(String atlasKey, String text, int scaleInt) {} + public record PageQuads(int pageIndex, @Nullable Identifier atlasTexture, int pageWidth, int pageHeight, List quads) {} diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java index 42bffae2..cb356cc6 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java @@ -50,12 +50,12 @@ public void drawString( private void drawStringWithAtlas(GuiGraphicsExtractor graphics, SdfGlyphAtlas atlas, String text, int x, int y, int color) { float scale = scaleFor(atlas); - int quadY = y - 2; + int quadY = y - Math.round((atlas.awtAscent() + 2) * scale); SdfTextLayout layout = SdfTextLayout.fromAtlas(atlas, text, x, quadY, scale); if (layout.pages().isEmpty()) return; SdfAtlasTexture.ensureUploaded(atlas); for (SdfTextLayout.PageQuads pq : layout.pages()) { - drawAtlasPipeline(graphics, pq, this.diffuseSampler, color); + drawAtlasPipeline(graphics, pq, this.diffuseSampler, color, x, quadY); } } @@ -148,12 +148,12 @@ private int flushFormattedSegment(GuiGraphicsExtractor graphics, @Nullable Font SdfGlyphAtlas atlas = SdfGlyphAtlas.getIfReady(font); if (atlas == null) return x; float scale = scaleFor(atlas); - int quadY = y - 2; + int quadY = y - Math.round((atlas.awtAscent() + 2) * scale); SdfTextLayout layout = SdfTextLayout.fromAtlas(atlas, text, x, quadY, scale); SdfAtlasTexture.ensureUploaded(atlas); for (SdfTextLayout.PageQuads pq : layout.pages()) { if (pq.atlasTexture() != null && !pq.quads().isEmpty()) { - drawAtlasPipeline(graphics, pq, this.diffuseSampler, color); + drawAtlasPipeline(graphics, pq, this.diffuseSampler, color, x, quadY); } } return x + layout.width(); @@ -237,7 +237,9 @@ private static void drawAtlasPipeline( GuiGraphicsExtractor graphics, SdfTextLayout.PageQuads pq, GpuSampler diffuseSampler, - int color + int color, + int originX, + int originY ) { if (pq.atlasTexture() == null || pq.quads().isEmpty()) return; SdfTextRenderState state = new SdfTextRenderState( @@ -248,6 +250,8 @@ private static void drawAtlasPipeline( pq.pageWidth(), pq.pageHeight(), color, + originX, + originY, graphics.peekScissorStack() ); graphics.submitGuiElementRenderState(state); diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/state/SdfTextRenderState.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/state/SdfTextRenderState.java index 64e1141f..7e628ad7 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/state/SdfTextRenderState.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/state/SdfTextRenderState.java @@ -32,6 +32,8 @@ public record SdfTextRenderState( int atlasWidth, int atlasHeight, int color, + int originX, + int originY, @Nullable ScreenRectangle scissorArea ) implements LibGuiElementRenderState { private static final Logger LOGGER = LoggerFactory.getLogger(SdfTextRenderState.class); @@ -55,14 +57,16 @@ public void executeDrawAfterSetPipeline(RenderPass renderPass) { @Override public void buildVertices(VertexConsumer consumer) { + int ox = this.originX; + int oy = this.originY; for (SdfTextLayout.GlyphQuad quad : this.glyphs) { // Build quad vertices in screen space // The pose matrix will handle transformation to clip space - float x0 = quad.x0(); - float y0 = quad.y0(); - float x1 = quad.x1(); - float y1 = quad.y1(); + float x0 = quad.x0() + ox; + float y0 = quad.y0() + oy; + float x1 = quad.x1() + ox; + float y1 = quad.y1() + oy; float u0 = quad.u0(); float v0 = quad.v0(); From 2f61da73bbd2603edf4a94f70d365d05d4aef1b2 Mon Sep 17 00:00:00 2001 From: Gugle Date: Sat, 20 Jun 2026 19:04:25 +0800 Subject: [PATCH 03/16] =?UTF-8?q?refactor(font):=20=E4=BC=98=E5=8C=96SDF?= =?UTF-8?q?=E5=AD=97=E5=BD=A2=E5=9B=BE=E9=9B=86=E7=9A=84=E5=BC=82=E6=AD=A5?= =?UTF-8?q?=E5=88=9B=E5=BB=BA=E5=92=8C=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将SdfGlyphAtlas.getOrCreate方法改为使用专用线程池执行异步任务 - 在AnvilLibFont初始化时预先启动SDF图集构建以避免首帧文字不可见 - 当构建失败时移除失败的Future并重新开始构建以防止瞬态错误永久禁用图集 - 为SdfTextRenderer添加辅助方法以统一获取准备就绪的图集 - 修复多线程环境下的内存可见性和同步问题 --- module.font/TODO.md | 6 ++++++ .../dev/anvilcraft/lib/v2/font/ALFont.java | 4 ++-- .../anvilcraft/lib/v2/font/AnvilLibFont.java | 10 +++++++++- .../lib/v2/font/sdf/SdfGlyphAtlas.java | 11 ++++++++-- .../lib/v2/font/sdf/SdfTextRenderer.java | 20 +++++++++++++++---- 5 files changed, 42 insertions(+), 9 deletions(-) diff --git a/module.font/TODO.md b/module.font/TODO.md index 14e85fc3..d8197bc1 100644 --- a/module.font/TODO.md +++ b/module.font/TODO.md @@ -8,6 +8,12 @@ - **Fix:** Cache `SdfTextLayout` results keyed by `(atlasKey, text, scale)`. Store quad positions relative to origin; apply offset during `buildVertices()`. ### 2. Async Glyph Creation -- DONE +- **Follow-up fixes:** + - `SdfGlyphPage.dirty` made `volatile` — cross-thread visibility for texture re-upload + - `placeGlyph()`/`fillPaddingForCell()` synchronized — memory barrier with `uploadPage` + - `createGlyphAsync` now calls `SdfGlyphPage::updateHash` — prevents stale hash from skipping upload + - `getIfReady` retries on failed futures — transient errors don't permanently disable atlas + - `AnvilLibFont.getSelectFont()` preloads atlas eagerly — avoids first-frame text invisibility - **File:** `SdfGlyphAtlas.java`, `SdfAtlasTexture.java` - **Problem:** `glyph()` calls `createGlyph()` synchronously on the render thread when a glyph is not yet in the atlas. For CJK text, hundreds of glyphs may need creation, each involving AWT rendering + EDT distance transform (~O(n^2) per glyph), blocking the render loop for multiple frames. - **Fix:** Return null for not-yet-created codepoints, enqueue async creation on a background single-threaded executor. Added `pendingGlyphs` set to prevent duplicate creation requests. `synchronized` on atlas for glyph creation, synchronized on page for texture upload. diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFont.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFont.java index 67eaecfd..d7c68792 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFont.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFont.java @@ -26,8 +26,8 @@ public ALFont(Font font) { public Font awtFont() { return this.font; } - private @Nullable SdfGlyphAtlas atlas() { return SdfGlyphAtlas.getIfReady(this.font); } - private SdfGlyphAtlas atlasBlocking() { return SdfGlyphAtlas.getOrCreate(this.font).join(); } + @Nullable + private SdfGlyphAtlas atlas() { return SdfGlyphAtlas.getIfReady(this.font); } private float scale() { SdfGlyphAtlas a = atlas(); diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/AnvilLibFont.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/AnvilLibFont.java index a5f44c4b..0faae1e8 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/AnvilLibFont.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/AnvilLibFont.java @@ -1,6 +1,7 @@ package dev.anvilcraft.lib.v2.font; import dev.anvilcraft.lib.v2.font.screen.FontConfigScreen; +import dev.anvilcraft.lib.v2.font.sdf.SdfGlyphAtlas; import net.minecraft.resources.Identifier; import net.neoforged.api.distmarker.Dist; import net.neoforged.fml.ModContainer; @@ -20,10 +21,17 @@ public class AnvilLibFont { public AnvilLibFont(ModContainer container) { AnvilLibFontConfig.AnvilLibFontConfigManager.readConfig(AnvilLibFont.CONFIG); container.registerExtensionPoint(IConfigScreenFactory.class, FontConfigScreen::new); + // Start building the SDF atlas on a background thread at mod init time. + // By the time the player opens any GUI the atlas is already uploaded. + SdfGlyphAtlas.getOrCreate(getSelectFont()); } public static Font getSelectFont() { - return FontManager.INSTANCE.getFont(AnvilLibFont.CONFIG.getFont()); + Font font = FontManager.INSTANCE.getFont(AnvilLibFont.CONFIG.getFont()); + // Eagerly start building the SDF atlas on a background thread so it is + // ready (or nearly ready) by the time the first text is rendered. + SdfGlyphAtlas.getOrCreate(font); + return font; } public static Identifier of(String path) { diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java index 5939de02..2766e672 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java @@ -101,11 +101,15 @@ private SdfGlyphAtlas(String key, Font font) { public static CompletableFuture getOrCreate(@Nullable Font font) { Font resolved = resolveFont(font); String key = resolved.getFontName(Locale.ENGLISH) + "." + resolved.getStyle() + "." + resolved.getSize(); - return CACHE.computeIfAbsent(key, _ -> CompletableFuture.supplyAsync(() -> new SdfGlyphAtlas(key, resolved))); + return CACHE.computeIfAbsent(key, _ -> CompletableFuture.supplyAsync(() -> new SdfGlyphAtlas(key, resolved), GLYPH_EXECUTOR)); } /** * Return the atlas if fully built, or {@code null} if still constructing. + *

+ * If the previous build failed (future completed exceptionally), the + * failed future is removed and a fresh build is started so that a + * transient error does not permanently disable the atlas. */ public static @Nullable SdfGlyphAtlas getIfReady(@Nullable Font font) { Font resolved = resolveFont(font); @@ -114,7 +118,10 @@ public static CompletableFuture getOrCreate(@Nullable Font font) if (f != null && f.isDone()) { try { return f.get(); - } catch (Exception ignored) { + } catch (Exception e) { + LOGGER.error("SDF atlas build failed for key={}, retrying", key, e); + CACHE.remove(key, f); + f = null; } } if (f == null) getOrCreate(font); diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java index cb356cc6..bdbb3b10 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java @@ -32,6 +32,18 @@ public final class SdfTextRenderer { public SdfTextRenderer() { } + /** + * Get the SDF atlas for a font, or {@code null} if still building. + *

+ * The atlas is pre-built at mod init, so this should almost always + * return non-null. If it returns null the caller skips rendering + * gracefully; the atlas will be ready the next frame. + */ + @Nullable + private static SdfGlyphAtlas getAtlas(@Nullable Font font) { + return SdfGlyphAtlas.getIfReady(font); + } + public void drawString( GuiGraphicsExtractor graphics, @Nullable Font font, @@ -42,7 +54,7 @@ public void drawString( boolean dropShadow ) { if (text == null || text.isEmpty()) return; - SdfGlyphAtlas atlas = SdfGlyphAtlas.getIfReady(font); + SdfGlyphAtlas atlas = getAtlas(font); if (atlas == null) return; drawStringWithAtlas(graphics, atlas, text, x, y, color); } @@ -145,7 +157,7 @@ public void drawFormatted( } private int flushFormattedSegment(GuiGraphicsExtractor graphics, @Nullable Font font, String text, int x, int y, int color) { - SdfGlyphAtlas atlas = SdfGlyphAtlas.getIfReady(font); + SdfGlyphAtlas atlas = getAtlas(font); if (atlas == null) return x; float scale = scaleFor(atlas); int quadY = y - Math.round((atlas.awtAscent() + 2) * scale); @@ -193,7 +205,7 @@ public void drawWrapped( int color, boolean dropShadow ) { - SdfGlyphAtlas atlas = SdfGlyphAtlas.getIfReady(font); + SdfGlyphAtlas atlas = getAtlas(font); if (atlas == null) return; float scale = scaleFor(atlas); List lines = wrapLines(atlas, text.getString(), width, scale); @@ -209,7 +221,7 @@ public void drawCentered(GuiGraphicsExtractor graphics, @Nullable Font font, Com public void drawCentered(GuiGraphicsExtractor graphics, @Nullable Font font, FormattedCharSequence text, int x, int y, int color) { String value = flattenToString(text); - SdfGlyphAtlas atlas = SdfGlyphAtlas.getIfReady(font); + SdfGlyphAtlas atlas = getAtlas(font); if (atlas == null) return; float scale = scaleFor(atlas); int drawX = x - Math.round(atlas.measureText(value) * scale) / 2; From b5457d66b9dd38fcf7f33ffc6fccc150eabb4fb0 Mon Sep 17 00:00:00 2001 From: Gugle Date: Sat, 20 Jun 2026 19:22:12 +0800 Subject: [PATCH 04/16] =?UTF-8?q?refactor(font):=20=E4=BC=98=E5=8C=96SDF?= =?UTF-8?q?=E5=AD=97=E4=BD=93=E5=9B=BE=E9=9B=86=E7=9A=84=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=E5=92=8C=E6=B8=B2=E6=9F=93=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除atlas方法的空值检查,改为直接阻塞等待图集就绪 - 在模组初始化时预构建基础字体及常用样式变体的SDF图集 - 修复配置界面中字体下拉框的设置方法调用 - 添加详细的日志记录以跟踪SDF图集的构建过程 - 简化渲染器中的空值检查逻辑,确保图集就绪后再进行渲染 - 调整图集上传时机以避免渲染线程阻塞 --- .../dev/anvilcraft/lib/v2/font/ALFont.java | 12 +++------- .../anvilcraft/lib/v2/font/AnvilLibFont.java | 12 +++++++--- .../lib/v2/font/screen/FontConfigScreen.java | 2 +- .../lib/v2/font/sdf/SdfGlyphAtlas.java | 20 ++++++++++++++--- .../lib/v2/font/sdf/SdfTextRenderer.java | 22 ++++++------------- 5 files changed, 37 insertions(+), 31 deletions(-) diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFont.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFont.java index d7c68792..0dcc22d0 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFont.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFont.java @@ -26,19 +26,16 @@ public ALFont(Font font) { public Font awtFont() { return this.font; } - @Nullable - private SdfGlyphAtlas atlas() { return SdfGlyphAtlas.getIfReady(this.font); } + private SdfGlyphAtlas atlas() { return SdfGlyphAtlas.getOrCreate(this.font).join(); } private float scale() { - SdfGlyphAtlas a = atlas(); - return a == null ? 1f : (float) lineHeight / a.awtHeight(); + return (float) lineHeight / atlas().awtHeight(); } // ── Width measurement ─────────────────────────────────────── public int width(String str) { - SdfGlyphAtlas a = atlas(); - return a == null ? 0 : Mth.ceil(a.measureText(str) * scale()); + return Mth.ceil(atlas().measureText(str) * scale()); } public int width(FormattedText text) { return width(text.getString()); } @@ -58,7 +55,6 @@ public String plainSubstrByWidth(String str, int maxWidth) { private String plainHeadByWidth(String str, int maxWidth) { SdfGlyphAtlas a = atlas(); - if (a == null) return str; float s = scale(); if (a.measureText(str) * s <= maxWidth) return str; int lo = 0, hi = str.length(); @@ -72,7 +68,6 @@ private String plainHeadByWidth(String str, int maxWidth) { private String plainTailByWidth(String str, int maxWidth) { SdfGlyphAtlas a = atlas(); - if (a == null) return str; float s = scale(); if (a.measureText(str) * s <= maxWidth) return str; int lo = 0, hi = str.length(); @@ -92,7 +87,6 @@ public FormattedText substrByWidth(FormattedText text, int maxWidth) { public List split(FormattedText input, int maxWidth) { SdfGlyphAtlas a = atlas(); - if (a == null) return List.of(FormattedCharSequence.forward(input.getString(), Style.EMPTY)); float s = scale(); List lines = wrapLines(a, input.getString(), maxWidth, s); List result = new ArrayList<>(lines.size()); diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/AnvilLibFont.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/AnvilLibFont.java index 0faae1e8..f3fad55f 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/AnvilLibFont.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/AnvilLibFont.java @@ -10,6 +10,7 @@ import net.neoforged.neoforge.client.gui.IConfigScreenFactory; import java.awt.Font; +import java.util.List; @EventBusSubscriber @Mod(value = AnvilLibFont.MOD_ID, dist = Dist.CLIENT) @@ -21,9 +22,14 @@ public class AnvilLibFont { public AnvilLibFont(ModContainer container) { AnvilLibFontConfig.AnvilLibFontConfigManager.readConfig(AnvilLibFont.CONFIG); container.registerExtensionPoint(IConfigScreenFactory.class, FontConfigScreen::new); - // Start building the SDF atlas on a background thread at mod init time. - // By the time the player opens any GUI the atlas is already uploaded. - SdfGlyphAtlas.getOrCreate(getSelectFont()); + // Start building the SDF atlas for the base font and all common style + // variants on background threads at mod init time. This avoids render- + // thread blocking (visible lag) when bold/italic text is first drawn. + Font base = getSelectFont(); + SdfGlyphAtlas.getOrCreate(base); + for (int style : List.of(Font.BOLD, Font.ITALIC, Font.BOLD | Font.ITALIC)) { + SdfGlyphAtlas.getOrCreate(base.deriveFont(style)); + } } public static Font getSelectFont() { diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/FontConfigScreen.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/FontConfigScreen.java index d7f99f0c..26dd890d 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/FontConfigScreen.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/FontConfigScreen.java @@ -96,7 +96,7 @@ protected void init() { this.refreshFontOptions(this.familyDropdown.getValueId(), AnvilLibFont.CONFIG.getFont(), false); - this.familyDropdown.setValue(AnvilLibFont.CONFIG.getFont()); + this.fontDropdown.setValue(AnvilLibFont.CONFIG.getFont()); this.addRenderableWidget(this.testBtn); this.addRenderableWidget(this.fontDropdown); diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java index 2766e672..46634cdc 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java @@ -68,6 +68,7 @@ public final class SdfGlyphAtlas { private SdfGlyphAtlas(String key, Font font) { this.key = key; this.font = font; + LOGGER.info("SdfGlyphAtlas building for key={}, thread={}", key, Thread.currentThread().getName()); // Capture font metrics first — needed by cellSize calculation BufferedImage tmp = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB); @@ -89,8 +90,10 @@ private SdfGlyphAtlas(String key, Font font) { this.sdfRadius = Math.max(12, font.getSize() * 0.25f); this.padding = Math.max(4, this.cellSize / 6); this.paddedCellSize = this.cellSize + 2 * this.padding; + LOGGER.info("SdfGlyphAtlas cellSize={} ascent={} descent={} key={}", this.cellSize, this.awtAscent, awtDescent, key); preWarmAscii(); + LOGGER.info("SdfGlyphAtlas ready for key={}, pages={}", key, this.pages.size()); } // ── Public API ────────────────────────────────────────────── @@ -101,7 +104,14 @@ private SdfGlyphAtlas(String key, Font font) { public static CompletableFuture getOrCreate(@Nullable Font font) { Font resolved = resolveFont(font); String key = resolved.getFontName(Locale.ENGLISH) + "." + resolved.getStyle() + "." + resolved.getSize(); - return CACHE.computeIfAbsent(key, _ -> CompletableFuture.supplyAsync(() -> new SdfGlyphAtlas(key, resolved), GLYPH_EXECUTOR)); + CompletableFuture result = CACHE.computeIfAbsent(key, _ -> { + LOGGER.info("Starting SDF atlas build for key={}", key); + return CompletableFuture.supplyAsync(() -> new SdfGlyphAtlas(key, resolved), GLYPH_EXECUTOR); + }); + if (result.isDone()) { + LOGGER.info("SDF atlas build done for key={}, done={}, cancelled={}", key, result.isDone(), result.isCancelled()); + } + return result; } /** @@ -117,14 +127,18 @@ public static CompletableFuture getOrCreate(@Nullable Font font) CompletableFuture f = CACHE.get(key); if (f != null && f.isDone()) { try { - return f.get(); + SdfGlyphAtlas atlas = f.get(); + return atlas; } catch (Exception e) { LOGGER.error("SDF atlas build failed for key={}, retrying", key, e); CACHE.remove(key, f); f = null; } } - if (f == null) getOrCreate(font); + if (f == null) { + LOGGER.info("No atlas future for key={}, starting build", key); + getOrCreate(font); + } return null; } diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java index bdbb3b10..31d94b6c 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java @@ -33,15 +33,12 @@ public SdfTextRenderer() { } /** - * Get the SDF atlas for a font, or {@code null} if still building. - *

- * The atlas is pre-built at mod init, so this should almost always - * return non-null. If it returns null the caller skips rendering - * gracefully; the atlas will be ready the next frame. + * Get the SDF atlas for a font, blocking until it is ready on first access. + * The atlas is cached forever, so only the very first call for a particular + * font blocks (typically ~200-400 ms while the glyph atlas is built). */ - @Nullable private static SdfGlyphAtlas getAtlas(@Nullable Font font) { - return SdfGlyphAtlas.getIfReady(font); + return SdfGlyphAtlas.getOrCreate(font).join(); } public void drawString( @@ -54,18 +51,16 @@ public void drawString( boolean dropShadow ) { if (text == null || text.isEmpty()) return; - SdfGlyphAtlas atlas = getAtlas(font); - if (atlas == null) return; - drawStringWithAtlas(graphics, atlas, text, x, y, color); + drawStringWithAtlas(graphics, getAtlas(font), text, x, y, color); } private void drawStringWithAtlas(GuiGraphicsExtractor graphics, SdfGlyphAtlas atlas, String text, int x, int y, int color) { float scale = scaleFor(atlas); int quadY = y - Math.round((atlas.awtAscent() + 2) * scale); + SdfAtlasTexture.ensureUploaded(atlas); SdfTextLayout layout = SdfTextLayout.fromAtlas(atlas, text, x, quadY, scale); if (layout.pages().isEmpty()) return; - SdfAtlasTexture.ensureUploaded(atlas); for (SdfTextLayout.PageQuads pq : layout.pages()) { drawAtlasPipeline(graphics, pq, this.diffuseSampler, color, x, quadY); } @@ -158,11 +153,10 @@ public void drawFormatted( private int flushFormattedSegment(GuiGraphicsExtractor graphics, @Nullable Font font, String text, int x, int y, int color) { SdfGlyphAtlas atlas = getAtlas(font); - if (atlas == null) return x; float scale = scaleFor(atlas); int quadY = y - Math.round((atlas.awtAscent() + 2) * scale); - SdfTextLayout layout = SdfTextLayout.fromAtlas(atlas, text, x, quadY, scale); SdfAtlasTexture.ensureUploaded(atlas); + SdfTextLayout layout = SdfTextLayout.fromAtlas(atlas, text, x, quadY, scale); for (SdfTextLayout.PageQuads pq : layout.pages()) { if (pq.atlasTexture() != null && !pq.quads().isEmpty()) { drawAtlasPipeline(graphics, pq, this.diffuseSampler, color, x, quadY); @@ -206,7 +200,6 @@ public void drawWrapped( boolean dropShadow ) { SdfGlyphAtlas atlas = getAtlas(font); - if (atlas == null) return; float scale = scaleFor(atlas); List lines = wrapLines(atlas, text.getString(), width, scale); int lineHeight = Minecraft.getInstance().font.lineHeight; @@ -222,7 +215,6 @@ public void drawCentered(GuiGraphicsExtractor graphics, @Nullable Font font, Com public void drawCentered(GuiGraphicsExtractor graphics, @Nullable Font font, FormattedCharSequence text, int x, int y, int color) { String value = flattenToString(text); SdfGlyphAtlas atlas = getAtlas(font); - if (atlas == null) return; float scale = scaleFor(atlas); int drawX = x - Math.round(atlas.measureText(value) * scale) / 2; this.drawFormatted(graphics, font, text, drawX, y, color, false); From 5079f4b61dd6ece3f14e737950fea5d4ed72aa8c Mon Sep 17 00:00:00 2001 From: Gugle Date: Sat, 20 Jun 2026 19:23:31 +0800 Subject: [PATCH 05/16] =?UTF-8?q?refactor(font):=20=E5=B0=86SDF=E5=AD=97?= =?UTF-8?q?=E5=BD=A2=E5=88=9B=E5=BB=BA=E4=BB=8E=E5=8D=95=E7=BA=BF=E7=A8=8B?= =?UTF-8?q?=E6=89=A7=E8=A1=8C=E5=99=A8=E5=88=87=E6=8D=A2=E5=88=B0=E8=99=9A?= =?UTF-8?q?=E6=8B=9F=E7=BA=BF=E7=A8=8B=E6=89=A7=E8=A1=8C=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除单线程执行器,改用每个任务一个虚拟线程的执行器 - 利用现有的synchronized块提供必要的互斥保护 - 使用虚拟线程提高字形创建的并发性能 - 保持字形或批次处理的独立线程隔离 --- .../anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java index 46634cdc..4ca7d0b0 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java @@ -41,15 +41,14 @@ public final class SdfGlyphAtlas { private static final Map> CACHE = new ConcurrentHashMap<>(); /** - * Single-threaded executor for background glyph creation. - * All glyph creation (including SDF computation) happens on this thread - * to avoid blocking the render loop. + * Per-task virtual-thread executor for background glyph creation. + * Each glyph (or batch) gets its own virtual thread; the existing + * {@code synchronized} blocks on the atlas and page objects already + * provide the necessary mutual exclusion. */ - private static final ExecutorService GLYPH_EXECUTOR = Executors.newSingleThreadExecutor(r -> { - Thread t = new Thread(r, "AnvilLib-SDF-Glyph"); - t.setDaemon(true); - return t; - }); + private static final ExecutorService GLYPH_EXECUTOR = Executors.newThreadPerTaskExecutor( + Thread.ofVirtual().name("AnvilLib-SDF-Glyph-", 0).factory() + ); private final String key; private final Font font; From 8b4aedfa0495cf3bc87fe78363f6a1d2f043727a Mon Sep 17 00:00:00 2001 From: Gugle Date: Sat, 20 Jun 2026 19:33:00 +0800 Subject: [PATCH 06/16] =?UTF-8?q?fix(font):=20=E4=BF=AE=E5=A4=8D=E5=AD=97?= =?UTF-8?q?=E4=BD=93=E6=B8=B2=E6=9F=93=E4=B8=AD=E7=9A=84=E6=96=87=E6=9C=AC?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F=E5=92=8C=E4=BD=8D=E7=BD=AE=E8=AE=A1=E7=AE=97?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为绿色文本添加删除线样式 - 为紫色斜体文本添加下划线样式 - 添加混淆文字测试用例 - 修复SDF文本渲染器中的Y轴位置计算偏差 - 简化文本布局和绘制管道的坐标传递逻辑 --- .../lib/v2/font/screen/FontTestScreen.java | 12 ++++++++++-- .../anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java | 10 ++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/FontTestScreen.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/FontTestScreen.java index 058c07b6..ad6bdb85 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/FontTestScreen.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/FontTestScreen.java @@ -30,7 +30,7 @@ public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mo ); graphics.anvillib$centeredText( AnvilLibFont.getSelectFont(), - Component.literal("请不要关闭你的计算机").withStyle(ChatFormatting.GREEN), + Component.literal("请不要关闭你的计算机").withStyle(Style.EMPTY.withColor(ChatFormatting.GREEN).withStrikethrough(true)), offsetX, offsetY + this.font.lineHeight, 0xFFFFFFFF @@ -60,7 +60,7 @@ public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mo graphics.anvillib$centeredText( AnvilLibFont.getSelectFont(), Component.literal("The quick brown fox jumped over the lazy dog.") - .withStyle(Style.EMPTY.withItalic(true).withColor(ChatFormatting.DARK_PURPLE)), + .withStyle(Style.EMPTY.withItalic(true).withColor(ChatFormatting.DARK_PURPLE).withUnderlined(true)), offsetX, offsetY + this.font.lineHeight * 5, 0xFFFFFFFF @@ -87,6 +87,14 @@ public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mo offsetY + this.font.lineHeight * 8, 0xFFFFFFFF ); + graphics.anvillib$centeredText( + AnvilLibFont.getSelectFont(), + Component.literal("混淆文字测试") + .withStyle(Style.EMPTY.withItalic(true).withBold(true).withObfuscated(true)), + offsetX, + offsetY + this.font.lineHeight * 9, + 0xFFFFFFFF + ); } @Override diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java index 31d94b6c..915479c5 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java @@ -57,12 +57,11 @@ public void drawString( private void drawStringWithAtlas(GuiGraphicsExtractor graphics, SdfGlyphAtlas atlas, String text, int x, int y, int color) { float scale = scaleFor(atlas); - int quadY = y - Math.round((atlas.awtAscent() + 2) * scale); SdfAtlasTexture.ensureUploaded(atlas); - SdfTextLayout layout = SdfTextLayout.fromAtlas(atlas, text, x, quadY, scale); + SdfTextLayout layout = SdfTextLayout.fromAtlas(atlas, text, x, y, scale); if (layout.pages().isEmpty()) return; for (SdfTextLayout.PageQuads pq : layout.pages()) { - drawAtlasPipeline(graphics, pq, this.diffuseSampler, color, x, quadY); + drawAtlasPipeline(graphics, pq, this.diffuseSampler, color, x, y); } } @@ -154,12 +153,11 @@ public void drawFormatted( private int flushFormattedSegment(GuiGraphicsExtractor graphics, @Nullable Font font, String text, int x, int y, int color) { SdfGlyphAtlas atlas = getAtlas(font); float scale = scaleFor(atlas); - int quadY = y - Math.round((atlas.awtAscent() + 2) * scale); SdfAtlasTexture.ensureUploaded(atlas); - SdfTextLayout layout = SdfTextLayout.fromAtlas(atlas, text, x, quadY, scale); + SdfTextLayout layout = SdfTextLayout.fromAtlas(atlas, text, x, y, scale); for (SdfTextLayout.PageQuads pq : layout.pages()) { if (pq.atlasTexture() != null && !pq.quads().isEmpty()) { - drawAtlasPipeline(graphics, pq, this.diffuseSampler, color, x, quadY); + drawAtlasPipeline(graphics, pq, this.diffuseSampler, color, x, y); } } return x + layout.width(); From c6fe2cb47b152ec08e047366f7d6ef6646943c55 Mon Sep 17 00:00:00 2001 From: Gugle Date: Sat, 20 Jun 2026 19:45:18 +0800 Subject: [PATCH 07/16] =?UTF-8?q?refactor(font):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E5=AD=97=E4=BD=93=E6=A8=A1=E5=9D=97=E6=97=A5=E5=BF=97=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E5=92=8C=E4=BB=A3=E7=A0=81=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用 Lombok @Slf4j 注解替换手动创建 Logger 实例 - 移除冗余的 import org.slf4j 语句 - 优化方法参数格式化以提高可读性 - 调整数组初始化语法以符合编码规范 - 将部分 info 级别日志调整为 debug 级别以减少运行时输出 --- .../anvilcraft/lib/v2/font/ALFPipelines.java | 8 +++--- .../lib/v2/font/sdf/SdfGlyphAtlas.java | 25 ++++++++----------- .../lib/v2/font/sdf/SdfTextRenderer.java | 17 +++++++------ .../v2/font/sdf/state/SdfTextRenderState.java | 4 --- 4 files changed, 22 insertions(+), 32 deletions(-) diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFPipelines.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFPipelines.java index e379e075..27e093b7 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFPipelines.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFPipelines.java @@ -6,12 +6,11 @@ import com.mojang.blaze3d.shaders.UniformType; import com.mojang.blaze3d.vertex.VertexFormat; import com.mojang.blaze3d.vertex.VertexFormatElement; +import lombok.extern.slf4j.Slf4j; import net.neoforged.api.distmarker.Dist; import net.neoforged.bus.api.SubscribeEvent; import net.neoforged.fml.common.EventBusSubscriber; import net.neoforged.neoforge.client.event.RegisterRenderPipelinesEvent; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Font module render pipeline registration. @@ -19,10 +18,9 @@ *

The pipeline is ready for SDF text quad rendering and will be consumed by the * runtime text render state in a follow-up step.

*/ +@Slf4j @EventBusSubscriber(modid = AnvilLibFont.MOD_ID, value = Dist.CLIENT) public final class ALFPipelines { - private static final Logger LOGGER = LoggerFactory.getLogger(ALFPipelines.class); - public static final VertexFormat SDF_TEXT_FORMAT = VertexFormat.builder() .add("Position", VertexFormatElement.POSITION) .add("Color", VertexFormatElement.COLOR) @@ -47,7 +45,7 @@ private ALFPipelines() { @SubscribeEvent public static void on(RegisterRenderPipelinesEvent event) { event.registerPipeline(SDF_TEXT); - LOGGER.info("Registered SDF_TEXT pipeline: {}", SDF_TEXT); + log.info("Registered SDF_TEXT pipeline: {}", SDF_TEXT); } } diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java index 4ca7d0b0..2c122cf6 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java @@ -1,8 +1,7 @@ package dev.anvilcraft.lib.v2.font.sdf; +import lombok.extern.slf4j.Slf4j; import org.jspecify.annotations.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.awt.Color; import java.awt.Font; @@ -28,8 +27,8 @@ * Glyphs are packed into fixed-size 1024×1024 pages. ASCII 32-126 is * pre-warmed; all other codepoints are rendered lazily on first use. */ +@Slf4j public final class SdfGlyphAtlas { - private static final Logger LOGGER = LoggerFactory.getLogger(SdfGlyphAtlas.class); static final int PAGE_SIZE = 1024; private static final int FIRST_CHAR = 32; private static final int LAST_CHAR = 126; @@ -67,7 +66,7 @@ public final class SdfGlyphAtlas { private SdfGlyphAtlas(String key, Font font) { this.key = key; this.font = font; - LOGGER.info("SdfGlyphAtlas building for key={}, thread={}", key, Thread.currentThread().getName()); + log.info("SdfGlyphAtlas building for key={}, thread={}", key, Thread.currentThread().getName()); // Capture font metrics first — needed by cellSize calculation BufferedImage tmp = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB); @@ -89,10 +88,10 @@ private SdfGlyphAtlas(String key, Font font) { this.sdfRadius = Math.max(12, font.getSize() * 0.25f); this.padding = Math.max(4, this.cellSize / 6); this.paddedCellSize = this.cellSize + 2 * this.padding; - LOGGER.info("SdfGlyphAtlas cellSize={} ascent={} descent={} key={}", this.cellSize, this.awtAscent, awtDescent, key); + log.info("SdfGlyphAtlas cellSize={} ascent={} descent={} key={}", this.cellSize, this.awtAscent, awtDescent, key); preWarmAscii(); - LOGGER.info("SdfGlyphAtlas ready for key={}, pages={}", key, this.pages.size()); + log.info("SdfGlyphAtlas ready for key={}, pages={}", key, this.pages.size()); } // ── Public API ────────────────────────────────────────────── @@ -103,14 +102,10 @@ private SdfGlyphAtlas(String key, Font font) { public static CompletableFuture getOrCreate(@Nullable Font font) { Font resolved = resolveFont(font); String key = resolved.getFontName(Locale.ENGLISH) + "." + resolved.getStyle() + "." + resolved.getSize(); - CompletableFuture result = CACHE.computeIfAbsent(key, _ -> { - LOGGER.info("Starting SDF atlas build for key={}", key); + return CACHE.computeIfAbsent(key, _ -> { + log.debug("Starting SDF atlas build for key={}", key); return CompletableFuture.supplyAsync(() -> new SdfGlyphAtlas(key, resolved), GLYPH_EXECUTOR); }); - if (result.isDone()) { - LOGGER.info("SDF atlas build done for key={}, done={}, cancelled={}", key, result.isDone(), result.isCancelled()); - } - return result; } /** @@ -129,13 +124,13 @@ public static CompletableFuture getOrCreate(@Nullable Font font) SdfGlyphAtlas atlas = f.get(); return atlas; } catch (Exception e) { - LOGGER.error("SDF atlas build failed for key={}, retrying", key, e); + log.error("SDF atlas build failed for key={}, retrying", key, e); CACHE.remove(key, f); f = null; } } if (f == null) { - LOGGER.info("No atlas future for key={}, starting build", key); + log.info("No atlas future for key={}, starting build", key); getOrCreate(font); } return null; @@ -202,7 +197,7 @@ private void createGlyphAsync(int codepoint) { // Invalidate cached layouts so they pick up the new glyph SdfTextLayout.invalidateAtlas(this.key); } catch (Exception e) { - LOGGER.error("Failed to create SDF glyph for codepoint {} (U+{})", codepoint, Integer.toHexString(codepoint).toUpperCase(), e); + log.error("Failed to create SDF glyph for codepoint {} (U+{})", codepoint, Integer.toHexString(codepoint).toUpperCase(), e); } finally { this.pendingGlyphs.remove(codepoint); } diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java index 915479c5..5ff9f53e 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java @@ -10,13 +10,11 @@ import net.minecraft.network.chat.FormattedText; import net.minecraft.network.chat.Style; import net.minecraft.util.FormattedCharSequence; +import org.joml.Matrix3x2f; import org.jspecify.annotations.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.awt.Font; import java.util.List; -import org.joml.Matrix3x2f; /** * SDF text renderer that draws strings via the SDF text render pipeline. @@ -25,8 +23,6 @@ * sampled by a custom fragment shader for smooth anti-aliased text. */ public final class SdfTextRenderer { - private static final Logger LOGGER = LoggerFactory.getLogger(SdfTextRenderer.class); - private final GpuSampler diffuseSampler = RenderSystem.getSamplerCache().getClampToEdge(FilterMode.LINEAR); public SdfTextRenderer() { @@ -54,8 +50,10 @@ public void drawString( drawStringWithAtlas(graphics, getAtlas(font), text, x, y, color); } - private void drawStringWithAtlas(GuiGraphicsExtractor graphics, SdfGlyphAtlas atlas, - String text, int x, int y, int color) { + private void drawStringWithAtlas( + GuiGraphicsExtractor graphics, SdfGlyphAtlas atlas, + String text, int x, int y, int color + ) { float scale = scaleFor(atlas); SdfAtlasTexture.ensureUploaded(atlas); SdfTextLayout layout = SdfTextLayout.fromAtlas(atlas, text, x, y, scale); @@ -109,7 +107,10 @@ public void drawFormatted( int color, boolean dropShadow ) { - int[] pen = {x, x}; + int[] pen = { + x, + x + }; StringBuilder buf = new StringBuilder(); int[] segColor = {color}; boolean[] segBold = {false}; diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/state/SdfTextRenderState.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/state/SdfTextRenderState.java index 7e628ad7..f292a1ea 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/state/SdfTextRenderState.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/state/SdfTextRenderState.java @@ -16,8 +16,6 @@ import org.jspecify.annotations.Nullable; import java.util.List; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Render state for SDF text rendering. @@ -36,8 +34,6 @@ public record SdfTextRenderState( int originY, @Nullable ScreenRectangle scissorArea ) implements LibGuiElementRenderState { - private static final Logger LOGGER = LoggerFactory.getLogger(SdfTextRenderState.class); - @Override public RenderPipeline pipeline() { return ALFPipelines.SDF_TEXT; From 9da6c7ad886b081d1fc1098de72cb9c4241ea588 Mon Sep 17 00:00:00 2001 From: Gugle Date: Sat, 20 Jun 2026 19:47:19 +0800 Subject: [PATCH 08/16] =?UTF-8?q?fix(gradle):=20=E7=A6=81=E7=94=A8=20rosea?= =?UTF-8?q?u=20=E5=9F=BA=E7=BA=BF=E4=BE=9D=E8=B5=96=E7=9A=84=E4=BC=A0?= =?UTF-8?q?=E9=80=92=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 设置roseauBaselineConfig.transitive = false以阻止传递依赖 - 防止不必要的依赖项被引入到项目中 --- gradle/scripts/roseau.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/gradle/scripts/roseau.gradle b/gradle/scripts/roseau.gradle index 7a427414..99f63b4b 100644 --- a/gradle/scripts/roseau.gradle +++ b/gradle/scripts/roseau.gradle @@ -41,6 +41,7 @@ def roseauBaselineVersion = project.findProperty('roseauBaselineVersion') ?: { def roseauMvnCoord = "${roseauGroup}:${roseauArtifact}:${roseauBaselineVersion}" def roseauBaselineDep = dependencies.create(group: roseauGroup, name: roseauArtifact, version: roseauBaselineVersion) def roseauBaselineConfig = configurations.detachedConfiguration(roseauBaselineDep) +roseauBaselineConfig.transitive = false def roseauBaselineJarProvider = provider { roseauBaselineConfig.singleFile } def roseauConfig = rootProject.file("roseau.yaml").absolutePath From 82e470d8b4e6d498b9f6ea68062aafb4375fef4c Mon Sep 17 00:00:00 2001 From: Gugle Date: Sat, 20 Jun 2026 19:53:44 +0800 Subject: [PATCH 09/16] =?UTF-8?q?feat(font):=20=E6=B7=BB=E5=8A=A0API?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E6=B3=A8=E8=A7=A3=E4=BB=A5=E6=A0=87=E8=AE=B0?= =?UTF-8?q?=E5=86=85=E9=83=A8=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在ALFont类上添加@Beta注解标记为实验性功能 - 在多个字体模块核心组件上添加@ApiStatus.Internal注解 - 包括ALFPipelines、AnvilLibFontConfigManager等关键类 - 为Sdf相关的纹理、图集、渲染器等添加适当的API状态标记 - 统一管理内部实现与公共接口的可见性边界 --- .../src/main/java/dev/anvilcraft/lib/v2/font/ALFPipelines.java | 2 ++ .../src/main/java/dev/anvilcraft/lib/v2/font/ALFont.java | 3 ++- .../java/dev/anvilcraft/lib/v2/font/AnvilLibFontConfig.java | 2 ++ .../src/main/java/dev/anvilcraft/lib/v2/font/FontManager.java | 2 ++ .../java/dev/anvilcraft/lib/v2/font/data/AnvilLibFontData.java | 2 ++ .../lib/v2/font/extension/GuiGraphicsExtractorExtension.java | 2 ++ .../lib/v2/font/mixin/GuiGraphicsExtractorMixin.java | 2 ++ .../dev/anvilcraft/lib/v2/font/screen/FontConfigScreen.java | 2 ++ .../java/dev/anvilcraft/lib/v2/font/screen/FontTestScreen.java | 3 ++- .../dev/anvilcraft/lib/v2/font/screen/widget/Dropdown.java | 2 ++ .../java/dev/anvilcraft/lib/v2/font/sdf/SdfAtlasTexture.java | 2 ++ .../java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java | 2 ++ .../main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphPage.java | 2 ++ .../java/dev/anvilcraft/lib/v2/font/sdf/SdfTextLayout.java | 2 ++ .../java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java | 2 ++ .../anvilcraft/lib/v2/font/sdf/state/SdfTextRenderState.java | 2 ++ 16 files changed, 32 insertions(+), 2 deletions(-) diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFPipelines.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFPipelines.java index 27e093b7..58d52082 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFPipelines.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFPipelines.java @@ -11,6 +11,7 @@ import net.neoforged.bus.api.SubscribeEvent; import net.neoforged.fml.common.EventBusSubscriber; import net.neoforged.neoforge.client.event.RegisterRenderPipelinesEvent; +import org.jetbrains.annotations.ApiStatus; /** * Font module render pipeline registration. @@ -18,6 +19,7 @@ *

The pipeline is ready for SDF text quad rendering and will be consumed by the * runtime text render state in a follow-up step.

*/ +@ApiStatus.Internal @Slf4j @EventBusSubscriber(modid = AnvilLibFont.MOD_ID, value = Dist.CLIENT) public final class ALFPipelines { diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFont.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFont.java index 0dcc22d0..7c15cbb0 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFont.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/ALFont.java @@ -1,5 +1,6 @@ package dev.anvilcraft.lib.v2.font; +import com.google.common.annotations.Beta; import dev.anvilcraft.lib.v2.font.sdf.SdfGlyphAtlas; import net.minecraft.client.Minecraft; import net.minecraft.network.chat.FormattedText; @@ -10,12 +11,12 @@ import java.awt.Font; import java.util.ArrayList; import java.util.List; -import org.jspecify.annotations.Nullable; /** * Wraps {@link Font} with text measurement and layout utilities that * delegate to {@link SdfGlyphAtlas} for glyph metrics. */ +@Beta public class ALFont { public final int lineHeight = Minecraft.getInstance().font.lineHeight; private final Font font; diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/AnvilLibFontConfig.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/AnvilLibFontConfig.java index 011cad80..9e455568 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/AnvilLibFontConfig.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/AnvilLibFontConfig.java @@ -6,6 +6,7 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; import net.neoforged.fml.loading.FMLLoader; +import org.jetbrains.annotations.ApiStatus; import org.jspecify.annotations.Nullable; import java.io.File; @@ -41,6 +42,7 @@ public void setFont(String font) { AnvilLibFontConfigManager.saveConfig(this); } + @ApiStatus.Internal @Slf4j public static class AnvilLibFontConfigManager { public static final Gson GSON = new GsonBuilder().disableHtmlEscaping().setPrettyPrinting().create(); diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/FontManager.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/FontManager.java index aee93ee6..071e3c43 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/FontManager.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/FontManager.java @@ -1,5 +1,6 @@ package dev.anvilcraft.lib.v2.font; +import com.google.common.annotations.Beta; import com.google.common.collect.Multimap; import com.google.common.collect.MultimapBuilder; import lombok.Getter; @@ -19,6 +20,7 @@ import java.util.TreeSet; import javax.swing.UIManager; +@Beta @Slf4j @Getter public class FontManager { diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/data/AnvilLibFontData.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/data/AnvilLibFontData.java index 5228bb2e..77d28408 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/data/AnvilLibFontData.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/data/AnvilLibFontData.java @@ -8,7 +8,9 @@ import net.neoforged.fml.common.EventBusSubscriber; import net.neoforged.neoforge.common.data.LanguageProvider; import net.neoforged.neoforge.data.event.GatherDataEvent; +import org.jetbrains.annotations.ApiStatus; +@ApiStatus.Internal @EventBusSubscriber(modid = AnvilLibFont.MOD_ID) public class AnvilLibFontData { @SubscribeEvent diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/extension/GuiGraphicsExtractorExtension.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/extension/GuiGraphicsExtractorExtension.java index 30928279..e5709c00 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/extension/GuiGraphicsExtractorExtension.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/extension/GuiGraphicsExtractorExtension.java @@ -1,5 +1,6 @@ package dev.anvilcraft.lib.v2.font.extension; +import com.google.common.annotations.Beta; import dev.anvilcraft.lib.v2.font.sdf.SdfTextRenderer; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; @@ -11,6 +12,7 @@ import java.awt.Font; +@Beta public interface GuiGraphicsExtractorExtension { default GuiGraphicsExtractor self() { return (GuiGraphicsExtractor) this; diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/mixin/GuiGraphicsExtractorMixin.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/mixin/GuiGraphicsExtractorMixin.java index 004f6514..4477cacc 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/mixin/GuiGraphicsExtractorMixin.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/mixin/GuiGraphicsExtractorMixin.java @@ -3,9 +3,11 @@ import dev.anvilcraft.lib.v2.font.extension.GuiGraphicsExtractorExtension; import dev.anvilcraft.lib.v2.font.sdf.SdfTextRenderer; import net.minecraft.client.gui.GuiGraphicsExtractor; +import org.jetbrains.annotations.ApiStatus; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Unique; +@ApiStatus.Internal @Mixin(GuiGraphicsExtractor.class) public abstract class GuiGraphicsExtractorMixin implements GuiGraphicsExtractorExtension { @Unique diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/FontConfigScreen.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/FontConfigScreen.java index 26dd890d..1142e2fe 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/FontConfigScreen.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/FontConfigScreen.java @@ -8,12 +8,14 @@ import net.minecraft.client.gui.screens.Screen; import net.minecraft.network.chat.Component; import net.neoforged.fml.ModContainer; +import org.jetbrains.annotations.ApiStatus; import org.jspecify.annotations.Nullable; import java.util.ArrayList; import java.util.Comparator; import java.util.List; +@ApiStatus.Internal public class FontConfigScreen extends Screen { protected final Screen lastScreen; private Dropdown.@Nullable Shielding shielding; diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/FontTestScreen.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/FontTestScreen.java index ad6bdb85..8ea2d4b5 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/FontTestScreen.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/FontTestScreen.java @@ -6,8 +6,9 @@ import net.minecraft.client.gui.screens.Screen; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Style; -import org.joml.Matrix3x2fStack; +import org.jetbrains.annotations.ApiStatus; +@ApiStatus.Internal public class FontTestScreen extends Screen { protected final Screen lastScreen; diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/widget/Dropdown.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/widget/Dropdown.java index 15f9ce24..3a72f47b 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/widget/Dropdown.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/widget/Dropdown.java @@ -10,6 +10,7 @@ import net.minecraft.client.input.MouseButtonEvent; import net.minecraft.network.chat.Component; import net.minecraft.util.Util; +import org.jetbrains.annotations.ApiStatus; import org.jspecify.annotations.Nullable; import java.util.ArrayList; @@ -18,6 +19,7 @@ import java.util.function.Consumer; import java.util.function.Supplier; +@ApiStatus.Internal public class Dropdown extends AbstractWidget { private final List allows = new ArrayList<>(); private final Minecraft minecraft = Minecraft.getInstance(); diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfAtlasTexture.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfAtlasTexture.java index a3b161ee..e5973d63 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfAtlasTexture.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfAtlasTexture.java @@ -8,6 +8,7 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.texture.AbstractTexture; import net.minecraft.resources.Identifier; +import org.jetbrains.annotations.ApiStatus; import java.awt.image.BufferedImage; import java.util.Map; @@ -16,6 +17,7 @@ /** * Uploads SDF glyph atlas pages to GPU textures with LINEAR filtering. */ +@ApiStatus.Internal public final class SdfAtlasTexture { private static final Map CACHE = new ConcurrentHashMap<>(); diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java index 2c122cf6..8db7b9a9 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphAtlas.java @@ -1,6 +1,7 @@ package dev.anvilcraft.lib.v2.font.sdf; import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.ApiStatus; import org.jspecify.annotations.Nullable; import java.awt.Color; @@ -27,6 +28,7 @@ * Glyphs are packed into fixed-size 1024×1024 pages. ASCII 32-126 is * pre-warmed; all other codepoints are rendered lazily on first use. */ +@ApiStatus.Internal @Slf4j public final class SdfGlyphAtlas { static final int PAGE_SIZE = 1024; diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphPage.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphPage.java index b78bb5d6..77f680f6 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphPage.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphPage.java @@ -1,12 +1,14 @@ package dev.anvilcraft.lib.v2.font.sdf; import net.minecraft.resources.Identifier; +import org.jetbrains.annotations.ApiStatus; import java.awt.image.BufferedImage; /** * A single 1024×1024 atlas page holding packed glyphs. */ +@ApiStatus.Internal public final class SdfGlyphPage { private static final int SIZE = SdfGlyphAtlas.PAGE_SIZE; final BufferedImage image; diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextLayout.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextLayout.java index 9ff9639b..8c7b207c 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextLayout.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextLayout.java @@ -1,6 +1,7 @@ package dev.anvilcraft.lib.v2.font.sdf; import net.minecraft.resources.Identifier; +import org.jetbrains.annotations.ApiStatus; import org.jspecify.annotations.Nullable; import java.util.ArrayList; @@ -18,6 +19,7 @@ * computation each frame. Quads are stored with positions relative to the * layout origin; the renderer supplies the screen-space offset. */ +@ApiStatus.Internal public final class SdfTextLayout { private static final int MAX_CACHE_SIZE = 1024; private static final Map LAYOUT_CACHE = new ConcurrentHashMap<>(); diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java index 5ff9f53e..19fff9d2 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java @@ -1,5 +1,6 @@ package dev.anvilcraft.lib.v2.font.sdf; +import com.google.common.annotations.Beta; import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.blaze3d.textures.FilterMode; import com.mojang.blaze3d.textures.GpuSampler; @@ -22,6 +23,7 @@ * Uses a CPU-generated SDF glyph atlas uploaded to a GPU texture, * sampled by a custom fragment shader for smooth anti-aliased text. */ +@Beta public final class SdfTextRenderer { private final GpuSampler diffuseSampler = RenderSystem.getSamplerCache().getClampToEdge(FilterMode.LINEAR); diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/state/SdfTextRenderState.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/state/SdfTextRenderState.java index f292a1ea..a13b702e 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/state/SdfTextRenderState.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/state/SdfTextRenderState.java @@ -12,6 +12,7 @@ import net.minecraft.client.gui.render.TextureSetup; import net.minecraft.client.renderer.texture.AbstractTexture; import net.minecraft.resources.Identifier; +import org.jetbrains.annotations.ApiStatus; import org.joml.Matrix3x2f; import org.jspecify.annotations.Nullable; @@ -22,6 +23,7 @@ * *

Coordinates are in screen space and will be transformed via the pose matrix.

*/ +@ApiStatus.Internal public record SdfTextRenderState( Matrix3x2f pose, List glyphs, From 2b11adb558ca2eb07027495089f163a026148ac1 Mon Sep 17 00:00:00 2001 From: Gugle Date: Sat, 20 Jun 2026 20:13:30 +0800 Subject: [PATCH 10/16] =?UTF-8?q?fix(font):=20=E4=BF=AE=E5=A4=8D=E4=B8=8B?= =?UTF-8?q?=E6=8B=89=E8=8F=9C=E5=8D=95=E6=9C=80=E5=A4=A7=E9=AB=98=E5=BA=A6?= =?UTF-8?q?=E8=AE=A1=E7=AE=97=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 计算最大高度时向下舍入到行高的倍数 - 确保最后一个可见行填满整个区域而不留空白 - 避免下拉菜单出现尾随空白空间的问题 --- .../dev/anvilcraft/lib/v2/font/screen/widget/Dropdown.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/widget/Dropdown.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/widget/Dropdown.java index 3a72f47b..9b1db96d 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/widget/Dropdown.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/screen/widget/Dropdown.java @@ -166,7 +166,10 @@ protected void extractWidgetRenderState(GuiGraphicsExtractor guiGraphicsExtracto public int calcMaxHeight() { int startHeight = this.getY() + this.getHeight(); - return Math.clamp((long) this.allows.size() * this.getHeight(), 0, this.screenHeight - startHeight - 10); + int maxHeight = Math.clamp((long) this.allows.size() * this.getHeight(), 0, this.screenHeight - startHeight - 10); + // Round down to a multiple of the row height so the last visible row + // fills the entire area without trailing blank space. + return (maxHeight / this.getHeight()) * this.getHeight(); } private int visibleRowCount() { From 718ebf30ac0fbcdbc94b8bb416db6fb36ba8c3ee Mon Sep 17 00:00:00 2001 From: Gugle Date: Sat, 20 Jun 2026 20:16:15 +0800 Subject: [PATCH 11/16] =?UTF-8?q?perf(font):=20=E4=BC=98=E5=8C=96SDF?= =?UTF-8?q?=E5=AD=97=E4=BD=93=E7=BA=B9=E7=90=86=E4=B8=8A=E4=BC=A0=E6=80=A7?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将图片哈希计算替换为原子版本号,避免每像素遍历 - 使用getRaster().getDataElements()批量复制像素数据 - 缓存图片宽高减少重复查询 - SdfGlyphPage使用AtomicInteger版本计数器 - 更新TODO文档说明性能优化详情 --- module.font/TODO.md | 151 ++++++++++-------- .../lib/v2/font/sdf/SdfAtlasTexture.java | 34 ++-- .../lib/v2/font/sdf/SdfGlyphPage.java | 5 +- 3 files changed, 97 insertions(+), 93 deletions(-) diff --git a/module.font/TODO.md b/module.font/TODO.md index d8197bc1..d10f6917 100644 --- a/module.font/TODO.md +++ b/module.font/TODO.md @@ -1,74 +1,83 @@ -# module.font Performance & Rendering Fixes - -## P0 - Critical (causes visible stutter & missing text) - -### 1. Layout Cache -- DONE -- **File:** `SdfTextLayout.java`, `SdfTextRenderer.java`, `SdfTextRenderState.java` -- **Problem:** Every frame, the entire text string is re-laid-out: codepoint -> glyph lookup, UV calculation, quad bucketing per page, new object allocations (LinkedHashMap, ArrayList, GlyphQuad, SdfTextRenderState) -- **Fix:** Cache `SdfTextLayout` results keyed by `(atlasKey, text, scale)`. Store quad positions relative to origin; apply offset during `buildVertices()`. - -### 2. Async Glyph Creation -- DONE -- **Follow-up fixes:** - - `SdfGlyphPage.dirty` made `volatile` — cross-thread visibility for texture re-upload - - `placeGlyph()`/`fillPaddingForCell()` synchronized — memory barrier with `uploadPage` - - `createGlyphAsync` now calls `SdfGlyphPage::updateHash` — prevents stale hash from skipping upload - - `getIfReady` retries on failed futures — transient errors don't permanently disable atlas - - `AnvilLibFont.getSelectFont()` preloads atlas eagerly — avoids first-frame text invisibility -- **File:** `SdfGlyphAtlas.java`, `SdfAtlasTexture.java` -- **Problem:** `glyph()` calls `createGlyph()` synchronously on the render thread when a glyph is not yet in the atlas. For CJK text, hundreds of glyphs may need creation, each involving AWT rendering + EDT distance transform (~O(n^2) per glyph), blocking the render loop for multiple frames. -- **Fix:** Return null for not-yet-created codepoints, enqueue async creation on a background single-threaded executor. Added `pendingGlyphs` set to prevent duplicate creation requests. `synchronized` on atlas for glyph creation, synchronized on page for texture upload. - -### 3. Fix `quadY` Baseline Offset -- DONE -- **File:** `SdfTextRenderer.java` -- **Problem:** `quadY = y - 2` is a hardcoded magic number. Doesn't account for actual atlas baseline position or scale, causing text below baseline (descenders: g, j, p, q, y) to be clipped. -- **Fix:** `quadY = y - Math.round((atlas.awtAscent() + 2) * scale)` - -## P1 - Significant improvement - -### 4. Fix `cellSize` to Fit Full Glyph -- DONE -- **File:** `SdfGlyphAtlas.java:55` -- **Problem:** `cellSize = Math.max(24, font.getSize() + 12)` - 64pt font -> cellSize=76. But 64pt ascent+descent can exceed 76px, clipping ascenders and descenders. -- **Fix:** `cellSize = Math.max(24, awtAscent + awtDescent + 4)` - -### 5. Remove Baseline Clamp in `renderMask()` -- DONE -- **File:** `SdfGlyphAtlas.java:224` -- **Problem:** `Math.min(this.cellSize - 4, this.awtAscent + 2)` clamps baseline when ascent is large, cutting off descenders. -- **Fix:** `g.drawString(s, 2, this.awtAscent + 2)` - no clamping needed if cellSize is large enough (see #4). - -### 6. Fix `awtHeight()` to Return Actual Metrics Height -- DONE -- **File:** `SdfGlyphAtlas.java` -- **Problem:** `awtHeight()` returned `font.getSize()` (point size, e.g., 64), not the actual pixel height of the font. This affected `scaleFor()` calculation. -- **Fix:** Now returns `awtHeight` (actual FontMetrics height = ascent + descent + leading). - -### 7. Replace `hashImage()` with Monotonic Version Number -- **File:** `SdfAtlasTexture.java:103-111`, `SdfGlyphPage.java` -- **Problem:** `hashImage()` iterates all 1,048,576 pixels every time a new glyph is added. Called from `SdfGlyphPage.updateHash()` after each glyph creation and after `measureText()`. -- **Fix:** Use an atomic version counter: `page.version++` on mutation, compare version instead of content hash. - -## P2 - Nice to have - -### 8. Optimize `toNativeImage()` with Bulk Copy -- **File:** `SdfAtlasTexture.java:75-84` -- **Problem:** Per-pixel `getRGB()`/`setPixel()` loop over 1M pixels during texture upload. -- **Fix:** Use `NativeImage` bulk write or `BufferedImage.getRaster().getDataElements()` for batch transfer. - -### 9. Object Pooling for Layout Temporaries -- **File:** `SdfTextLayout.java`, `SdfTextRenderer.java` -- **Problem:** Each draw call allocates new `LinkedHashMap`, multiple `ArrayList`, `GlyphQuad` records, `SdfTextRenderState` records, causing GC pressure. -- **Fix:** Thread-local pools for `ArrayList`, `StringBuilder`, and vertex buffers. Reuse layout intermediates. +# 模块字体性能与渲染修复 + +## P0 - 关键问题(导致可见卡顿及文字缺失) + +### 1. 布局缓存 -- 已修复 + +- **文件:** `SdfTextLayout.java`, `SdfTextRenderer.java`, `SdfTextRenderState.java` +- **问题:** 每帧都重新计算整个文本字符串布局:码点转字形查询、UV计算、按页面分桶、新建对象(LinkedHashMap、ArrayList、GlyphQuad、SdfTextRenderState) +- **修复:** 根据`(atlasKey, text, scale)`缓存`SdfTextLayout`结果。存储四边形相对原点的位置;在`buildVertices()`时应用偏移量。 + +### 2. 异步字形创建 -- 已修复 + +- **后续修复:** + - `SdfGlyphPage.dirty`设为`volatile`——确保跨线程纹理重传可见性 + - `placeGlyph()`/`fillPaddingForCell()`同步化——与`uploadPage`建立内存屏障 + - `createGlyphAsync`现在调用`SdfGlyphPage::updateHash`——防止因哈希过时跳过上传 + - `getIfReady`对失败future进行重试——临时错误不会永久禁用图集 + - `AnvilLibFont.getSelectFont()`预加载图集——避免首帧文字不可见 +- **文件:** `SdfGlyphAtlas.java`, `SdfAtlasTexture.java` +- **问题:** 渲染线程同步调用`createGlyph()`时若字形未创建,对CJK文本可能需创建数百个字形(每个涉及AWT渲染+EDT距离变换,约O(n²)/字形),导致渲染循环阻塞多帧 +- **修复:** 对未创建码点返回null,在单线程后台执行器异步创建。添加`pendingGlyphs`集合防重复请求。字形创建时同步图集,纹理上传时同步页面。 + +### 3. 修复`quadY`基线偏移 -- 已修复 + +- **文件:** `SdfTextRenderer.java` +- **问题:** `quadY = y - 2`是硬编码魔法值。未考虑实际图集基线位置和缩放,导致基线下方文字(如gjpqy等降部字母)被裁剪 +- **修复:** `quadY = y - Math.round((atlas.awtAscent() + 2) * scale)` + +## P1 - 显著改进 + +### 4. 调整`cellSize`适配完整字形 -- 已修复 + +- **文件:** `SdfGlyphAtlas.java:55` +- **问题:** `cellSize = Math.max(24, font.getSize() + 12)`——64pt字体→cellSize=76。但64pt字体的升部+降部可能超过76px,导致裁剪 +- **修复:** `cellSize = Math.max(24, awtAscent + awtDescent + 4)` + +### 5. 移除`renderMask()`中的基线截断 -- 已修复 + +- **文件:** `SdfGlyphAtlas.java:224` +- **问题:** `Math.min(this.cellSize - 4, this.awtAscent + 2)`在升部较大时截断基线,导致降部缺失 +- **修复:** `g.drawString(s, 2, this.awtAscent + 2)`——若cellSize足够大(见#4)则无需截断 + +### 6. 修正`awtHeight()`返回实际度量高度 -- 已修复 + +- **文件:** `SdfGlyphAtlas.java` +- **问题:** 原先返回`font.getSize()`(磅值如64),非字体实际像素高度。影响`scaleFor()`计算 +- **修复:** 现返回`awtHeight`(实际FontMetrics高度=升部+降部+行间距) + +### 7. 用单调版本号取代`hashImage()` -- 已修复 + +- **文件:** `SdfAtlasTexture.java`, `SdfGlyphPage.java` +- **问题:** 每次新增字形时`hashImage()`遍历所有1,048,576像素。在字形创建后和`measureText()`后调用 +- **修复:** `SdfGlyphPage`使用`AtomicInteger version`原子版本计数器,修改时`version.incrementAndGet()`,`uploadPage()`比较版本号而非内容哈希。已移除`hashImage()`方法。 + +## P2 - 优化项 + +### 8. 使用批量复制优化`toNativeImage()` -- 已修复 + +- **文件:** `SdfAtlasTexture.java` +- **问题:** 纹理上传时通过逐像素`image.getRGB(x, y)`处理百万级像素,每次getRGB调用都涉及Java2D颜色模型转换 +- **修复:** 使用`image.getRaster().getDataElements()`一次性获取所有像素字节数组,消除逐像素getRGB开销 + +### 9. 布局临时对象池化 -- 暂缓 + +- **文件:** `SdfTextLayout.java`, `SdfTextRenderer.java` +- **问题:** 每次绘制都新建`LinkedHashMap`、多个`ArrayList`、`GlyphQuad`、`SdfTextRenderState`,增加GC压力 +- **状态:** 因#1布局缓存已大幅降低布局计算频率(相同字符串仅计算一次),且`SdfTextRenderState`等记录对象生命周期极短(单帧),GC在ZGC/Generational ZGC下可高效处理。暂不实施对象池化,若后续profiling显示此部分仍为热点再考虑。 --- -## Summary - -| # | Task | Status | Impact | Effort | -|---|-------------------------|--------|-----------------------------------|---------| -| 1 | Layout Cache | DONE | Eliminates repeated CPU work | Medium | -| 2 | Async Glyph Creation | DONE | Eliminates render-thread blocking | Medium | -| 3 | Fix quadY offset | DONE | Fixes descender clipping | Trivial | -| 4 | Fix cellSize | DONE | Fixes glyph clipping in atlas | Small | -| 5 | Remove baseline clamp | DONE | Fixes descender clipping | Trivial | -| 6 | Fix awtHeight() | DONE | Corrects text scaling | Trivial | -| 7 | Version instead of hash | TODO | ~1M fewer pixel reads per glyph | Small | -| 8 | Bulk NativeImage copy | TODO | Faster texture upload | Small | -| 9 | Object pooling | TODO | Reduced GC pauses | Medium | +## 汇总 + +| # | 任务 | 状态 | 影响 | 工作量 | +|---|-----------------|-----|-----------------|-----| +| 1 | 布局缓存 | 已完成 | 消除重复CPU计算 | 中等 | +| 2 | 异步字形创建 | 已完成 | 消除渲染线程阻塞 | 中等 | +| 3 | 修复quadY偏移 | 已完成 | 修复降部裁剪 | 简单 | +| 4 | 修正cellSize | 已完成 | 修复图集内字形裁剪 | 较小 | +| 5 | 移除基线截断 | 已完成 | 修复降部裁剪 | 简单 | +| 6 | 修正awtHeight() | 已完成 | 正确文本缩放 | 简单 | +| 7 | 版本号替代哈希 | 已完成 | 单字形减少约100万次像素读取 | 较小 | +| 8 | NativeImage批量复制 | 已完成 | 加速纹理上传 | 较小 | +| 9 | 对象池化 | 暂缓 | 减少GC暂停 | 中等 | diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfAtlasTexture.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfAtlasTexture.java index e5973d63..e77651b2 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfAtlasTexture.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfAtlasTexture.java @@ -30,10 +30,10 @@ private SdfAtlasTexture() { public static Identifier uploadPage(SdfGlyphAtlas atlas, int pageIndex) { SdfGlyphPage page = atlas.page(pageIndex); String key = atlas.key() + ".p" + pageIndex; - int hash = page.hash; + int version = page.version.get(); PageEntry entry = CACHE.get(key); - if (entry != null && entry.hash == hash) return entry.id; + if (entry != null && entry.version == version) return entry.id; Identifier id = Identifier.fromNamespaceAndPath("anvillib_font", "dynamic/sdf_atlas/" + sanitize(key)); // Synchronize on page to avoid reading image data while the async @@ -46,7 +46,7 @@ public static Identifier uploadPage(SdfGlyphAtlas atlas, int pageIndex) { Minecraft.getInstance().getTextureManager().register(id, texture); if (entry != null) entry.texture.close(); - CACHE.put(key, new PageEntry(id, texture, hash)); + CACHE.put(key, new PageEntry(id, texture, version)); page.textureId = id; page.dirty = false; return id; @@ -81,10 +81,14 @@ static final class SdfTexture extends AbstractTexture { } static NativeImage toNativeImage(BufferedImage image) { - NativeImage ni = new NativeImage(NativeImage.Format.RGBA, image.getWidth(), image.getHeight(), false); - for (int y = 0; y < image.getHeight(); y++) { - for (int x = 0; x < image.getWidth(); x++) { - int gray = image.getRGB(x, y) & 0xFF; + int w = image.getWidth(); + int h = image.getHeight(); + NativeImage ni = new NativeImage(NativeImage.Format.RGBA, w, h, false); + byte[] pixels = (byte[]) image.getRaster().getDataElements(0, 0, w, h, null); + int idx = 0; + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + int gray = pixels[idx++] & 0xFF; ni.setPixel(x, y, (0xFF << 24) | (gray << 16) | (gray << 8) | gray); } } @@ -108,25 +112,15 @@ private static String sanitize(String key) { return sb.toString(); } - public static int hashImage(BufferedImage image) { - int hash = 1; - for (int y = 0; y < image.getHeight(); y++) { - for (int x = 0; x < image.getWidth(); x++) { - hash = 31 * hash + image.getRGB(x, y); - } - } - return hash; - } - private static final class PageEntry { final Identifier id; final SdfTexture texture; - final int hash; + final int version; - PageEntry(Identifier id, SdfTexture texture, int hash) { + PageEntry(Identifier id, SdfTexture texture, int version) { this.id = id; this.texture = texture; - this.hash = hash; + this.version = version; } } } diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphPage.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphPage.java index 77f680f6..03fbc16b 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphPage.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfGlyphPage.java @@ -4,6 +4,7 @@ import org.jetbrains.annotations.ApiStatus; import java.awt.image.BufferedImage; +import java.util.concurrent.atomic.AtomicInteger; /** * A single 1024×1024 atlas page holding packed glyphs. @@ -12,7 +13,7 @@ public final class SdfGlyphPage { private static final int SIZE = SdfGlyphAtlas.PAGE_SIZE; final BufferedImage image; - int hash = 1; + final AtomicInteger version = new AtomicInteger(); final int cols, rows; int nextCol, nextRow; Identifier textureId; @@ -58,6 +59,6 @@ synchronized void fillPaddingForCell(SdfGlyphAtlas atlas, SdfGlyphAtlas.GlyphEnt } public void updateHash() { - this.hash = SdfAtlasTexture.hashImage(this.image); + this.version.incrementAndGet(); } } From 7ec5af51a842400c9f1e1dbf75279197b86135c2 Mon Sep 17 00:00:00 2001 From: Gugle Date: Sat, 20 Jun 2026 20:29:03 +0800 Subject: [PATCH 12/16] =?UTF-8?q?fix(font):=20=E4=BF=AE=E5=A4=8DSDF?= =?UTF-8?q?=E5=9B=BE=E9=9B=86=E7=BA=B9=E7=90=86=E4=B8=8A=E4=BC=A0=E4=B8=AD?= =?UTF-8?q?=E7=9A=84=E7=AB=9E=E6=80=81=E6=9D=A1=E4=BB=B6=E5=92=8C=E9=A2=9C?= =?UTF-8?q?=E8=89=B2=E7=A9=BA=E9=97=B4=E8=BD=AC=E6=8D=A2=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在同步块中统一读取版本号和图像数据,避免异步字形创建线程的竞态访问 - 重新实现toNativeImage方法,将逐像素getRGB调用回滚到逐像素处理方式 - 更新TODO文档记录NativeImage批量复制优化的回滚原因 - 修正纹理脏检查逻辑,确保版本变更时正确触发上传 - 修复因Java2D颜色空间转换差异导致的SDF纹理GPU采样残影问题 --- module.font/TODO.md | 29 ++++++++------- .../lib/v2/font/sdf/SdfAtlasTexture.java | 37 ++++++++++--------- 2 files changed, 35 insertions(+), 31 deletions(-) diff --git a/module.font/TODO.md b/module.font/TODO.md index d10f6917..37c29dbe 100644 --- a/module.font/TODO.md +++ b/module.font/TODO.md @@ -54,11 +54,12 @@ ## P2 - 优化项 -### 8. 使用批量复制优化`toNativeImage()` -- 已修复 +### 8. 使用批量复制优化`toNativeImage()` -- 待修复 - **文件:** `SdfAtlasTexture.java` -- **问题:** 纹理上传时通过逐像素`image.getRGB(x, y)`处理百万级像素,每次getRGB调用都涉及Java2D颜色模型转换 -- **修复:** 使用`image.getRaster().getDataElements()`一次性获取所有像素字节数组,消除逐像素getRGB开销 +- **问题:** 纹理上传时逐像素`image.getRGB(x, y)`调用涉及Java2D颜色空间转换,处理百万级像素较慢 +- **尝试:** 使用`image.getRaster().getDataElements()`一次性获取所有像素字节数组,消除逐像素getRGB开销 +- **回滚:** `getDataElements()`返回的原始byte数组与`getRGB()`的sRGB转换值存在微妙差异(TYPE_BYTE_GRAY的颜色空间→sRGB转换路径),导致SDF纹理在GPU采样时产生"淡淡残影"效果。已回滚至逐像素getRGB方案,待后续用NativeImage内部API(如copyFromBuffer)正确实现 ### 9. 布局临时对象池化 -- 暂缓 @@ -70,14 +71,14 @@ ## 汇总 -| # | 任务 | 状态 | 影响 | 工作量 | -|---|-----------------|-----|-----------------|-----| -| 1 | 布局缓存 | 已完成 | 消除重复CPU计算 | 中等 | -| 2 | 异步字形创建 | 已完成 | 消除渲染线程阻塞 | 中等 | -| 3 | 修复quadY偏移 | 已完成 | 修复降部裁剪 | 简单 | -| 4 | 修正cellSize | 已完成 | 修复图集内字形裁剪 | 较小 | -| 5 | 移除基线截断 | 已完成 | 修复降部裁剪 | 简单 | -| 6 | 修正awtHeight() | 已完成 | 正确文本缩放 | 简单 | -| 7 | 版本号替代哈希 | 已完成 | 单字形减少约100万次像素读取 | 较小 | -| 8 | NativeImage批量复制 | 已完成 | 加速纹理上传 | 较小 | -| 9 | 对象池化 | 暂缓 | 减少GC暂停 | 中等 | +| # | 任务 | 状态 | 影响 | 工作量 | +|---|-----------------|-----|--------------------|-----| +| 1 | 布局缓存 | 已完成 | 消除重复CPU计算 | 中等 | +| 2 | 异步字形创建 | 已完成 | 消除渲染线程阻塞 | 中等 | +| 3 | 修复quadY偏移 | 已完成 | 修复降部裁剪 | 简单 | +| 4 | 修正cellSize | 已完成 | 修复图集内字形裁剪 | 较小 | +| 5 | 移除基线截断 | 已完成 | 修复降部裁剪 | 简单 | +| 6 | 修正awtHeight() | 已完成 | 正确文本缩放 | 简单 | +| 7 | 版本号替代哈希 | 已完成 | 单字形减少约100万次像素读取 | 较小 | +| 8 | NativeImage批量复制 | 回滚 | 加速纹理上传(颜色空间差异导致残影) | 较小 | +| 9 | 对象池化 | 暂缓 | 减少GC暂停 | 中等 | diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfAtlasTexture.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfAtlasTexture.java index e77651b2..c73459e0 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfAtlasTexture.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfAtlasTexture.java @@ -30,25 +30,27 @@ private SdfAtlasTexture() { public static Identifier uploadPage(SdfGlyphAtlas atlas, int pageIndex) { SdfGlyphPage page = atlas.page(pageIndex); String key = atlas.key() + ".p" + pageIndex; - int version = page.version.get(); - - PageEntry entry = CACHE.get(key); - if (entry != null && entry.version == version) return entry.id; - Identifier id = Identifier.fromNamespaceAndPath("anvillib_font", "dynamic/sdf_atlas/" + sanitize(key)); - // Synchronize on page to avoid reading image data while the async - // glyph creation thread is writing to it. + // Synchronize on page to read the version and image data atomically + // with respect to the glyph creation thread, which also synchronizes + // on page for placeGlyph / fillPaddingForCell. NativeImage nativeImage; + int version; synchronized (page) { + version = page.version.get(); nativeImage = toNativeImage(page.image); + page.dirty = false; } + + PageEntry entry = CACHE.get(key); + if (entry != null && entry.version == version) return entry.id; + SdfTexture texture = new SdfTexture(nativeImage); Minecraft.getInstance().getTextureManager().register(id, texture); if (entry != null) entry.texture.close(); CACHE.put(key, new PageEntry(id, texture, version)); page.textureId = id; - page.dirty = false; return id; } @@ -58,7 +60,12 @@ public static Identifier uploadPage(SdfGlyphAtlas atlas, int pageIndex) { public static void ensureUploaded(SdfGlyphAtlas atlas) { for (int i = 0; i < atlas.pageCount(); i++) { SdfGlyphPage page = atlas.page(i); - if (page.dirty || page.textureId == null) { + String key = atlas.key() + ".p" + i; + PageEntry entry = CACHE.get(key); + if (page.textureId == null + || entry == null + || entry.version != page.version.get() + || page.dirty) { uploadPage(atlas, i); } } @@ -81,14 +88,10 @@ static final class SdfTexture extends AbstractTexture { } static NativeImage toNativeImage(BufferedImage image) { - int w = image.getWidth(); - int h = image.getHeight(); - NativeImage ni = new NativeImage(NativeImage.Format.RGBA, w, h, false); - byte[] pixels = (byte[]) image.getRaster().getDataElements(0, 0, w, h, null); - int idx = 0; - for (int y = 0; y < h; y++) { - for (int x = 0; x < w; x++) { - int gray = pixels[idx++] & 0xFF; + NativeImage ni = new NativeImage(NativeImage.Format.RGBA, image.getWidth(), image.getHeight(), false); + for (int y = 0; y < image.getHeight(); y++) { + for (int x = 0; x < image.getWidth(); x++) { + int gray = image.getRGB(x, y) & 0xFF; ni.setPixel(x, y, (0xFF << 24) | (gray << 16) | (gray << 8) | gray); } } From f5b67574bc2073a2df84236689c9b06d86987b2d Mon Sep 17 00:00:00 2001 From: Gugle Date: Sat, 20 Jun 2026 20:32:04 +0800 Subject: [PATCH 13/16] =?UTF-8?q?docs(font):=20=E7=A7=BB=E9=99=A4=E5=B7=B2?= =?UTF-8?q?=E5=AE=8C=E6=88=90=E7=9A=84TODO=E4=BA=8B=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- module.font/TODO.md | 84 --------------------------------------------- 1 file changed, 84 deletions(-) delete mode 100644 module.font/TODO.md diff --git a/module.font/TODO.md b/module.font/TODO.md deleted file mode 100644 index 37c29dbe..00000000 --- a/module.font/TODO.md +++ /dev/null @@ -1,84 +0,0 @@ -# 模块字体性能与渲染修复 - -## P0 - 关键问题(导致可见卡顿及文字缺失) - -### 1. 布局缓存 -- 已修复 - -- **文件:** `SdfTextLayout.java`, `SdfTextRenderer.java`, `SdfTextRenderState.java` -- **问题:** 每帧都重新计算整个文本字符串布局:码点转字形查询、UV计算、按页面分桶、新建对象(LinkedHashMap、ArrayList、GlyphQuad、SdfTextRenderState) -- **修复:** 根据`(atlasKey, text, scale)`缓存`SdfTextLayout`结果。存储四边形相对原点的位置;在`buildVertices()`时应用偏移量。 - -### 2. 异步字形创建 -- 已修复 - -- **后续修复:** - - `SdfGlyphPage.dirty`设为`volatile`——确保跨线程纹理重传可见性 - - `placeGlyph()`/`fillPaddingForCell()`同步化——与`uploadPage`建立内存屏障 - - `createGlyphAsync`现在调用`SdfGlyphPage::updateHash`——防止因哈希过时跳过上传 - - `getIfReady`对失败future进行重试——临时错误不会永久禁用图集 - - `AnvilLibFont.getSelectFont()`预加载图集——避免首帧文字不可见 -- **文件:** `SdfGlyphAtlas.java`, `SdfAtlasTexture.java` -- **问题:** 渲染线程同步调用`createGlyph()`时若字形未创建,对CJK文本可能需创建数百个字形(每个涉及AWT渲染+EDT距离变换,约O(n²)/字形),导致渲染循环阻塞多帧 -- **修复:** 对未创建码点返回null,在单线程后台执行器异步创建。添加`pendingGlyphs`集合防重复请求。字形创建时同步图集,纹理上传时同步页面。 - -### 3. 修复`quadY`基线偏移 -- 已修复 - -- **文件:** `SdfTextRenderer.java` -- **问题:** `quadY = y - 2`是硬编码魔法值。未考虑实际图集基线位置和缩放,导致基线下方文字(如gjpqy等降部字母)被裁剪 -- **修复:** `quadY = y - Math.round((atlas.awtAscent() + 2) * scale)` - -## P1 - 显著改进 - -### 4. 调整`cellSize`适配完整字形 -- 已修复 - -- **文件:** `SdfGlyphAtlas.java:55` -- **问题:** `cellSize = Math.max(24, font.getSize() + 12)`——64pt字体→cellSize=76。但64pt字体的升部+降部可能超过76px,导致裁剪 -- **修复:** `cellSize = Math.max(24, awtAscent + awtDescent + 4)` - -### 5. 移除`renderMask()`中的基线截断 -- 已修复 - -- **文件:** `SdfGlyphAtlas.java:224` -- **问题:** `Math.min(this.cellSize - 4, this.awtAscent + 2)`在升部较大时截断基线,导致降部缺失 -- **修复:** `g.drawString(s, 2, this.awtAscent + 2)`——若cellSize足够大(见#4)则无需截断 - -### 6. 修正`awtHeight()`返回实际度量高度 -- 已修复 - -- **文件:** `SdfGlyphAtlas.java` -- **问题:** 原先返回`font.getSize()`(磅值如64),非字体实际像素高度。影响`scaleFor()`计算 -- **修复:** 现返回`awtHeight`(实际FontMetrics高度=升部+降部+行间距) - -### 7. 用单调版本号取代`hashImage()` -- 已修复 - -- **文件:** `SdfAtlasTexture.java`, `SdfGlyphPage.java` -- **问题:** 每次新增字形时`hashImage()`遍历所有1,048,576像素。在字形创建后和`measureText()`后调用 -- **修复:** `SdfGlyphPage`使用`AtomicInteger version`原子版本计数器,修改时`version.incrementAndGet()`,`uploadPage()`比较版本号而非内容哈希。已移除`hashImage()`方法。 - -## P2 - 优化项 - -### 8. 使用批量复制优化`toNativeImage()` -- 待修复 - -- **文件:** `SdfAtlasTexture.java` -- **问题:** 纹理上传时逐像素`image.getRGB(x, y)`调用涉及Java2D颜色空间转换,处理百万级像素较慢 -- **尝试:** 使用`image.getRaster().getDataElements()`一次性获取所有像素字节数组,消除逐像素getRGB开销 -- **回滚:** `getDataElements()`返回的原始byte数组与`getRGB()`的sRGB转换值存在微妙差异(TYPE_BYTE_GRAY的颜色空间→sRGB转换路径),导致SDF纹理在GPU采样时产生"淡淡残影"效果。已回滚至逐像素getRGB方案,待后续用NativeImage内部API(如copyFromBuffer)正确实现 - -### 9. 布局临时对象池化 -- 暂缓 - -- **文件:** `SdfTextLayout.java`, `SdfTextRenderer.java` -- **问题:** 每次绘制都新建`LinkedHashMap`、多个`ArrayList`、`GlyphQuad`、`SdfTextRenderState`,增加GC压力 -- **状态:** 因#1布局缓存已大幅降低布局计算频率(相同字符串仅计算一次),且`SdfTextRenderState`等记录对象生命周期极短(单帧),GC在ZGC/Generational ZGC下可高效处理。暂不实施对象池化,若后续profiling显示此部分仍为热点再考虑。 - ---- - -## 汇总 - -| # | 任务 | 状态 | 影响 | 工作量 | -|---|-----------------|-----|--------------------|-----| -| 1 | 布局缓存 | 已完成 | 消除重复CPU计算 | 中等 | -| 2 | 异步字形创建 | 已完成 | 消除渲染线程阻塞 | 中等 | -| 3 | 修复quadY偏移 | 已完成 | 修复降部裁剪 | 简单 | -| 4 | 修正cellSize | 已完成 | 修复图集内字形裁剪 | 较小 | -| 5 | 移除基线截断 | 已完成 | 修复降部裁剪 | 简单 | -| 6 | 修正awtHeight() | 已完成 | 正确文本缩放 | 简单 | -| 7 | 版本号替代哈希 | 已完成 | 单字形减少约100万次像素读取 | 较小 | -| 8 | NativeImage批量复制 | 回滚 | 加速纹理上传(颜色空间差异导致残影) | 较小 | -| 9 | 对象池化 | 暂缓 | 减少GC暂停 | 中等 | From 3500f1921e68e6278574896ff69b1088f1495ac7 Mon Sep 17 00:00:00 2001 From: Gugle Date: Sat, 20 Jun 2026 22:12:26 +0800 Subject: [PATCH 14/16] =?UTF-8?q?fix(ci):=20=E4=BF=AE=E5=A4=8D=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E6=B5=81=E4=B8=ADgradlew=E6=89=A7=E8=A1=8C=E6=9D=83?= =?UTF-8?q?=E9=99=90=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为gradlew脚本添加执行权限 - 修改roseauCheck步骤以确保正确的文件权限 - 保持继续执行错误的配置选项 --- .github/workflows/roseau_check.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/roseau_check.yml b/.github/workflows/roseau_check.yml index a3a3d052..c07d50b8 100644 --- a/.github/workflows/roseau_check.yml +++ b/.github/workflows/roseau_check.yml @@ -48,7 +48,9 @@ jobs: - name: Run roseauCheck id: roseau continue-on-error: true - run: ./gradlew :${{ inputs.module_id }}-neoforge-26.1:roseauCheck + run: | + chmod +x ./gradlew + ./gradlew :${{ inputs.module_id }}-neoforge-26.1:roseauCheck - name: Collect result id: result From 3505dbb330b920047db07db8d818025e4ccca940 Mon Sep 17 00:00:00 2001 From: Gugle Date: Sat, 20 Jun 2026 22:22:55 +0800 Subject: [PATCH 15/16] =?UTF-8?q?fix(render):=20=E4=BF=AE=E5=A4=8DSDF?= =?UTF-8?q?=E6=96=87=E6=9C=AC=E6=B8=B2=E6=9F=93=E4=B8=AD=E7=9A=84=E4=B8=8B?= =?UTF-8?q?=E5=88=92=E7=BA=BF=E5=92=8C=E5=88=A0=E9=99=A4=E7=BA=BF=E4=B8=8B?= =?UTF-8?q?=E6=A0=87=E4=BD=8D=E7=BD=AE=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 引入Matrix3x2fStack用于矩阵变换操作 - 添加矩阵推入和缩放操作以支持正确的坐标变换 - 修正删除线下标计算公式,确保在缩放后正确显示 - 修正下划线位置计算公式,避免渲染位置偏移 - 在绘制完成后弹出矩阵以恢复变换状态 --- .../dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java index 19fff9d2..187e0d38 100644 --- a/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java +++ b/module.font/src/main/java/dev/anvilcraft/lib/v2/font/sdf/SdfTextRenderer.java @@ -12,6 +12,7 @@ import net.minecraft.network.chat.Style; import net.minecraft.util.FormattedCharSequence; import org.joml.Matrix3x2f; +import org.joml.Matrix3x2fStack; import org.jspecify.annotations.Nullable; import java.awt.Font; @@ -180,14 +181,18 @@ private static void drawDecorations( ) { if (x1 <= x0) return; int lh = Minecraft.getInstance().font.lineHeight; + Matrix3x2fStack pose = graphics.pose(); + pose.pushMatrix(); + pose.scale(1.0f, 0.5f); if (strikethrough) { - int sy = y + lh / 2; + int sy = (y + lh / 2) * 2; graphics.fill(x0, sy, x1, sy + 1, color); } if (underline) { - int sy = y + lh; + int sy = (y + lh - 2) * 2; graphics.fill(x0, sy, x1, sy + 1, color); } + pose.popMatrix(); } public void drawWrapped( From cae7caa5d90b66580739b617959f2d123e88c28a Mon Sep 17 00:00:00 2001 From: Gugle Date: Sat, 20 Jun 2026 22:41:55 +0800 Subject: [PATCH 16/16] =?UTF-8?q?feat(ci):=20=E6=B7=BB=E5=8A=A0=20Roseau?= =?UTF-8?q?=20=E6=8A=A5=E5=91=8A=E4=B8=8B=E8=BD=BD=E5=92=8C=E8=AF=A6?= =?UTF-8?q?=E7=BB=86=E5=8F=98=E6=9B=B4=E4=BF=A1=E6=81=AF=E5=B1=95=E7=A4=BA?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 下载 Roseau 报告到本地 roseau-reports 目录 - 为每个受影响模块追加详细的破坏性变更详情 - 在 PR 评论中显示具体的变更类型、符号和兼容性状态 - 更新文档链接指向完整的 CSV 报告文件 --- .github/workflows/pull_request.yml | 49 +++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 33dc5af0..b52eb1a9 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -363,6 +363,12 @@ jobs: MAIN_BC: ${{ needs.main-roseau.outputs.has_bc }} MAIN_N: ${{ needs.main-roseau.outputs.bc_count }} steps: + - name: Download Roseau reports + uses: actions/download-artifact@v4 + with: + pattern: roseau-* + path: roseau-reports + - name: Generate and post PR comment run: | row() { @@ -398,9 +404,50 @@ jobs: row "wheel" "$WHEEL_BC" "$WHEEL_N" row "main" "$MAIN_BC" "$MAIN_N" echo "" - echo "> Detailed reports: see the *Artifacts* section of [this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})." } > /tmp/roseau-body.md + # Append breaking change details for each affected module + append_details() { + local mod=$1 bc=$2 count=$3 + if [ "$bc" != "true" ]; then return; fi + local csv="roseau-reports/roseau-${mod}/report.csv" + if [ ! -f "$csv" ]; then return; fi + { + echo "### 🔴 ${mod} — ${count} breaking change(s)" + echo "" + while IFS=';' read -r type symbol kind nature location newSymbol binaryBreaking sourceBreaking; do + echo "#### \`${location}\`" + echo '```' + echo "${type}" + echo "${symbol}" + echo "${kind}" + [ "$binaryBreaking" = "true" ] && echo "✗ binary-breaking" || echo "✓ binary-compatible" + [ "$sourceBreaking" = "true" ] && echo "✗ source-breaking" || echo "✓ source-compatible" + echo '```' + echo "" + done < <(tail -n +2 "$csv") + } >> /tmp/roseau-body.md + } + + append_details "codec" "$CODEC_BC" "$CODEC_N" + append_details "collision" "$COLLISION_BC" "$COLLISION_N" + append_details "config" "$CONFIG_BC" "$CONFIG_N" + append_details "font" "$FONT_BC" "$FONT_N" + append_details "integration" "$INTEGRATION_BC" "$INTEGRATION_N" + append_details "moveable-entity-block" "$MEB_BC" "$MEB_N" + append_details "multiblock" "$MULTIBLOCK_BC" "$MULTIBLOCK_N" + append_details "network" "$NETWORK_BC" "$NETWORK_N" + append_details "recipe" "$RECIPE_BC" "$RECIPE_N" + append_details "registrum" "$REGISTRUM_BC" "$REGISTRUM_N" + append_details "rendering" "$RENDERING_BC" "$RENDERING_N" + append_details "space-select" "$SPACESELECT_BC" "$SPACESELECT_N" + append_details "sync" "$SYNC_BC" "$SYNC_N" + append_details "util" "$UTIL_BC" "$UTIL_N" + append_details "wheel" "$WHEEL_BC" "$WHEEL_N" + append_details "main" "$MAIN_BC" "$MAIN_N" + + echo "> Full CSVs: see the *Artifacts* section of [this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})." >> /tmp/roseau-body.md + - name: Find existing Roseau comment id: find-comment uses: peter-evans/find-comment@v3