Repository navigation
Optimize font startup for 1.21.1 优化字体首次加载与图集上传 - #269
Merged
PigeonNian merged 1 commit intoNov 13, 2026
Merged
Conversation
- 分离字体度量和字形生成,提前预热常用样式并按需生成字形 - 使用有界后台任务队列处理字形,缩短图集发布锁并保持文字宽度稳定 - 复用字体纹理并只上传变化区域,避免整页转换和重复创建,及时释放临时图像 - 保留完整文本布局缓存,补充异步生成、并发请求及增量上传回归测试
🌿 Roseau API Breaking Change Report
|
PigeonNian
approved these changes
Nov 13, 2026
Gugle2308
reviewed
Nov 13, 2026
Gugle2308
left a comment
There was a problem hiding this comment.
结论:内容正确、可以合并(审查期间已由 0e2171db squash 合入 dev/1.21.1,2026-11-13T15:30:35Z);8 文件 / +293 −138 与 patch-diff 逐条对得上,CI build / build 通过,无阻塞缺陷。以下为合并后追记,重点是几个建议后续清理的冗余与死钩子。
✅ 一、已验证
| 项 | 证据 |
|---|---|
| 合并状态与范围 | API:state=closed、merged=true、merge_commit_sha=0e2171db305cfcb0ffd412a152c39ed25a07fd7d = 当前 dev/1.21.1 tip,squash 提交标题 perf(font): 优化字体首次加载与图集上传 (#269);changed_files=8、+293/−138、commits=1,与 pr269.diff(8 个 diff --git、md5 49b971c4…)一致 |
| 分页增量上传的方向 | 反汇编 com.mojang.blaze3d.platform.NativeImage(1.21.1 混淆名 faj):_upload 用 _pixelStore(3316 /*SKIP_PIXELS*/, iload 4)、_pixelStore(3315 /*SKIP_ROWS*/, iload 5)、_pixelStore(3314 /*ROW_LENGTH*/, iload 6 == getWidth() ? 0 : getWidth()),而 _texSubImage2D 的偏移取 iload_2/iload_3 ⇒ 形参为 (level, xOffset, yOffset, x, y, width, height, …),x/y 是源图原点、xOffset/yOffset 是贴图落点。SdfAtlasTexture.java:81 的 image.copyRect(backing, 0, 0, x, y, w, h, false, false) 正是把补丁写进 backing 的 (x,y),:83 再 backing.upload(0, x, y, x, y, w, h, …) 从 backing 的 (x,y) 取 w×h 贴到纹理 (x,y) —— 方向正确,且 ROW_LENGTH 取的是 backing 的 1024 而不是补丁的 w,行跨距也对 |
| 上传参数不会引入 mipmap/翻转 | 同一方法里 iload 8/iload 10 进私有静态 a(ZZ)V(MIN_FILTER= LINEAR / LINEAR_MIPMAP_LINEAR、MAG_FILTER=LINEAR),iload 9 控制 WRAP_S/WRAP_T = GL_CLAMP_TO_EDGE(33071),iload 11 控制上传后 close();faj.a(IIIIIIIZZ)V(9 参便捷重载)对前两个布尔恒压 iconst_0, iconst_0。本 PR 传的 true, true, false, false ⇒ LINEAR + CLAMP + 不请求 mipmap + 不在这里 close,与构造器 setFilter(true, false) 自洽(翻转语义在 copyRect 的布尔上,不在这里) |
| 脏矩形与补丁尺寸 | SdfGlyphPage.java:37-46 的脏盒恰为 (x, y, x+size, y+size)(size = paddedCellSize),:67-70 按 width/height 逐行拷贝;FontStartupTest.java:83 断言补丁长度 == paddedCellSize²、:85-89 与 page.image 逐像素相等,:84 还断言 < 整页/16 —— 与实现一致 |
| 并发发布无丢失 | SdfGlyphAtlas.java:191-193 CAS + :209-212 finally 里先 set(false) 再查队列,逐序推演不存在丢唤醒(唯一失活序列要求 workerScheduled 为 false 而 CAS 失败,矛盾);SdfGlyphPage 的 pixels/image 全在 synchronized(this) 内写,snapshot/uploaded 同一监视器 ⇒ 与 placeGlyph 之间有 happens-before;dirty 为 volatile,:76 用 version 比对避免把新字形标成已上传 |
| 纹理关闭后可恢复 | faj/gpy 字节码:DynamicTexture.close() 置空其 NativeImage、upload() 在 image 为 null 时只打警告不再 GL 调用;因此 :30 的 `create = entry == null |
| 新测试真的在 CI 里跑 | .github/modules.json 含 "module": "font";gradle/scripts/publication-projects.gradle:7-8 由它生成构建目标,ci.gradle:21 dependsOn(":${target.path}:build") → check → test(build.gradle:296-303 已接 junit-bom 5.10.2 + useJUnitPlatform() + addModdingDependenciesTo(sourceSets.test),测试里用到 ResourceLocation 所以后者必要);check 另挂 fontRegressionTest(:291-293)。sourceSets.test 显式 exclude FontRegressionTest.java、fontRegression 只 include 它,两个 source set 不重叠 |
| 度量/栅格拆分与宽度稳定 | :235-237 measureCodepoint = advances.computeIfAbsent(codepoint, fontMetrics::charWidth),measureText 不再调 atlas.glyph(...) ⇒ 度量不产生页面(FontStartupTest:33,37 的 pageCount()==0 成立)。旧实现 base:221 对未生成字形回落 cellSize/2 占位宽,新实现直接用 AWT 真值 ⇒ 这是本 PR 修掉的首帧抖动,也是对外的宽度行为变更(同一字符在字形就绪前后宽度一致了) |
💡 二、建议(均为合并后清理项,不构成缺陷)
SdfGlyphPage.image已成为「只写 + 只用来取宽高」的 1 MiB 冗余(SdfGlyphPage.java:14):git grep "\.image"在module.font/src/main只剩:38的getRaster().setRect(...)写、SdfTextLayout.java:107-110,130的getWidth()/getHeight()读,以及测试FontStartupTest.java:87的逐像素比对 —— 宽高就是常量SdfGlyphAtlas.PAGE_SIZE。现在每个字形既要setRect拷一遍、又要System.arraycopy写pixels,还常驻一张 1024×1024 的TYPE_BYTE_GRAY。建议删字段、宽高改静态常量、测试改为与pixels对账,能同时省掉一次整格光栅拷贝与每页 1 MiB。SdfAtlasTexture.java:33的早退返回了一个从未注册的ResourceLocation:if (upload == null) return id;里id是本地拼出来的名字,此时既没register也没写page.textureId,调用方(:64的ensureUploaded忽略返回值)才没出问题;一旦有人按 javadoc「returning its texture location」拿去用就会拿到绑不上的 id。实际路径上不可达(snapshot(false)只在!dirty且脏盒为空时返回 null,二者与 version 不匹配不共存),建议改成return entry != null ? entry.id : id,或直接返回null并同步改注释。- 两个公开钩子已成死代码:
SdfGlyphPage.updateHash()(SdfGlyphPage.java:54-60)全仓 0 调用点(base 上有 7 处),且它会把脏盒重置成整页并version++—— 后人误用一次就是一次整页 4 MiB 重传;SdfTextLayout.invalidateAtlas(String)(SdfTextLayout.java:62-64)同样 0 调用点(base 上 3 处)。两者都在@ApiStatus.Internal类上,建议直接删除,或保留则补@Deprecated+ 说明「仅用于外部强制失效,会让下一帧整页重传」。 - 布局缓存从「会失效」变成「只增」(
SdfTextLayout.java:42-57、:73-83):字形位置不可变,缓存条目确实永远有效,但也意味着再没有任何东西回收它们 —— 会稳定涨到MAX_CACHE_SIZE = 1024后每次插入都从ConcurrentHashMap.keySet()迭代器里任意摘掉一半(在渲染线程上),动态文本多的场景(F3、聊天、数字)会一直在这个阈值上抖动。建议改 LRU 或按atlasKey设上限。 - 预热规模值得写进注释(
AnvilLibFont.java:28-32、SdfGlyphAtlas.java:260-263):base + 3 个样式 = 4 个图集 × 95 个 ASCII = 380 个格子全部塞进共享的 1~2 线程GLYPH_EXECUTOR。按ATLAS_FONT_SIZE = 64估:cellSize ≈ 79、padding ≈ 13、paddedCellSize ≈ 105⇒ 每页 9×9 = 81 格 ⇒ 每样式约 2 页、合计约 5–8 页;每页常驻pixels1 MiB +image1 MiB + GPU 侧 4 MiB(SdfAtlasTexture.CACHE与SdfGlyphAtlas.CACHE都没有淘汰)≈ 6 MiB/页。同时首帧请求的 CJK 单字会排在 380 个预热项之后(单队列 FIFO)——这是本 PR 有意拿「首帧延迟」换「启动不卡」的取舍,建议在preWarmAscii上注明,避免后人以为是按优先级调度。 module.font/build.gradle尾部排版(:294-303):两个空行 + 第二个顶层dependencies { },且neoForge { … }/test { … }被放到tasks.named('check')之后,而不是像module.math/build.gradle:114,117,127那样分别归位到已有的neoForge/test/dependencies块。功能无碍(CI 绿),纯一致性问题。FontStartupTest.concurrentRequestsPublishEachCellOnceAcrossPages的revisions == codepoints.length(:121)依赖两个隐含前提:该图集没被preWarmAscii()过、且updateHash()没人调用(两者都会白增version)。测试图集是直接getOrCreate得来的所以成立,但一旦将来构造器恢复内联预热(或有人手调updateHash),这条断言会以「多出 95 次 revision」这种看不懂的方式挂掉。建议加一行注释点明前提,或改断言revisions <= codepoints.length并单独断言每格唯一(:113-117已经在做)。
验证边界
本轮为静态审查:patch-diff 与合并后 dev/1.21.1 tip 的 blob 逐文件对账、module.font 全部生产代码 + 新测试全文阅读、原版 NativeImage/DynamicTexture/TextureManager 的 1.21.1 字节码(纯 Python 常量池 + 操作码解析,本环境无 JDK/javap)判读、CI build / build = success。未在本机执行 Gradle/runClient,因此「首帧是否仍有可感知的空白字形」「380 个预热格子实际占多少显存」属于推算而非实测;合并后如需继续跟进,建议以真实客户端的 F3 内存曲线与字体首次渲染帧为准。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
perf(font): 优化字体首次加载与图集上传