渲染崩溃恢复(RenderRecovery / RenderFailures)
基本信息
| 属性 | 值 |
|---|---|
| 包 | com.gtnewhorizons.angelica.rendering |
| 本条目覆盖 | 2 个文件:RenderRecovery.java(58 行)、RenderFailures.java(22 行) |
| 可见性 | 两者均 public final,构造器私有,全静态 |
| 单元测试 | src/test/java/.../rendering/RenderFailuresTest.java 存在(覆盖 RenderFailures 全部 3 个方法) |
| 触发点 | compat/bettercrashes/BetterCrashesCompat.java:32 |
这是一个崩溃后把 GL / Iris / 字体 / HUD 缓存状态全部复位的子系统。存在的理由很直接:Angelica 在一帧里做了大量 GL 状态跟踪(GLSM)、display list 编译(Iris)、字体批处理缓存、HUD 缓存 —— 其中任何一步抛异常,若不复位,1.7.10 的 Minecraft.runGameLoop 不会自动清理,下一帧就会带着脏状态继续跑,故障现场会被掩盖。
resetAfterCrash 的 12 步复位
RenderRecovery.resetAfterCrash()(:24-40)无条件、无 try/catch 顺序执行:
| # | 行 | 动作 | 复位对象 |
|---|---|---|---|
| 1 | :25 |
DisplayListManager.abortCompilation() |
GLSM display list 编译中断 |
| 2 | :26-29 |
Iris.getPipelineManagerNullable() → 非 null 时 destroyPipeline() |
内嵌 Iris 渲染管线 |
| 3 | :30 |
GLStateManager.reset() |
GLSM 全状态 |
| 4 | :31 |
GLDebug.resetGroupStack() |
GLSM debug group 栈 |
| 5 | :33 |
resetFont(mc.fontRenderer) |
字体批处理缓存 |
| 6 | :34 |
resetFont(mc.standardGalacticFontRenderer) |
同上(第二个字体渲染器) |
| 7 | :35 |
HUDCaching.resetAfterCrash() |
HUD 缓存 |
| 8 | :36 |
mc.getFramebuffer().bindFramebuffer(false) |
绑定 0 号 FBO |
| 9 | :37 |
TesrLifecycle.reset() |
TESR 实例化生命周期 |
| 10 | :38 |
TesrBlendScope.reset() |
TESR 混合作用域 |
| 11 | :39 |
TesrAttribution.currentRenderable = null |
TESR 归因(直接写字段) |
| 12 | :40 |
CapturedRenderingState.INSTANCE.setCurrentBlockEntity(0) |
内嵌 Iris 捕获的渲染状态 |
| 13 | :41 |
CTMUtils.clearCurrentCompact() |
MCPatcher 紧凑纹理 |
实际是 13 步。
⚠️ 第 11 步是唯一直接赋值私有静态字段的地方:TesrAttribution.currentRenderable = null。该字段是 public(TesrAttribution.java 只有 7 行,字段可见性需要单独确认),这不是通过 setter,而是跨类直接写。若 TesrAttribution 改字段名,这里编译不过;更糟的是若该字段语义从「当前渲染目标」变成别的含义,这里会静默复位错东西。
跨子系统依赖(本条目是耦合度最高的文件之一)
| 依赖 | 来源包 | 是否 Angelica 自有 |
|---|---|---|
DisplayListManager、GLStateManager、GLDebug |
com.gtnewhorizons.angelica.glsm |
是(同仓子项目,见 Subprojects) |
FontRendererAccessor |
com.gtnewhorizons.angelica.mixins.interfaces |
是(Angelica 自己的 mixin 接口) |
Iris、PipelineManager、CapturedRenderingState |
net.coderbot.iris |
否 —— 内嵌 Iris 上游 |
CTMUtils |
com.prupe.mcpatcher.ctm |
否 —— MCPatcher 上游 |
HUDCaching |
com.gtnewhorizons.angelica.hudcaching |
是(见 HUDCaching) |
TesrLifecycle、TesrBlendScope、TesrAttribution |
com.gtnewhorizons.angelica.rendering.tesr |
是(见 TESR 条目) |
RenderRecovery.java 一共 import 17 个类,其中 3 个来自非 Angelica 包。注意 net.coderbot.iris 是内嵌上游,不是 Angelica 的代码 —— 但这个文件本身是 Angelica 原创的复位逻辑。
resetFont:唯一的抽象点
// :43-45
private static void resetFont(FontRenderer fr) {
if (fr instanceof FontRendererAccessor a && a.angelica$getBatcher() != null) a.angelica$getBatcher().resetAfterCrash();
}
用 instanceof + mixin accessor 拿批处理器,两重 null 检查:
fr是不是 Angelica 增强过的FontRenderer(即 mixin 是否生效)angelica$getBatcher()是否为 null(批处理器是否已创建)
resetAfterCrash() 是 FontRenderer 批处理器上的方法,定义在 mixin 侧 —— 见 Angelica mixin 分组。
⚠️ 两个检查都是「失败就静默跳过」。若 mixin 因其它 mod 冲突未生效,resetFont 什么都不做也不会报错,字体缓存的脏状态会留到下次 —— 这正是最难排查的一类故障。
崩溃自测(crashTest)
RenderRecovery 内含一对故意制造崩溃的方法,用于验证复位路径本身是否有效:
// :47-50
public static void armCrashTest() { crashTestArmed = true; }
// :52-58
public static void throwIfCrashTestArmed() {
if (crashTestArmed) {
crashTestArmed = false;
throw new IllegalStateException("angelica crashtest");
}
}
| 项 | 值 |
|---|---|
| 触发源 | commands/AngelicaCommand.java:167 RenderRecovery.armCrashTest() |
| 抛出处 | src/mixin/java/.../mixins/early/shaders/MixinTileEntityRendererDispatcher.java:21 RenderRecovery.throwIfCrashTestArmed() |
| 标志位 | crashTestArmed,private static boolean,非 volatile |
| 语义 | 一次性:抛出前先清 false(:54),不会反复崩 |
armCrashTest / throwIfCrashTestArmed 都是 public static,但 crashTestArmed 字段本身是私有的 —— 外部只能通过这两个方法操作。
抛出点在 MixinTileEntityRendererDispatcher(shaders mixin 组),也就是说崩溃发生在方块实体渲染派发时,而不是任意位置。这是为了让复位路径覆盖到 TESR 相关的脏状态(步骤 9-11)。
RenderFailures:异常合并三件套
22 行,三个静态方法,全部是 try-with-resources / lambda 场景下的辅助工具:
| 方法 | 行 | 语义 |
|---|---|---|
suppress(Throwable failure, Throwable cleanup) |
:5-8 |
failure == null → 返回 cleanup;failure != cleanup → failure.addSuppressed(cleanup) 后返回 failure;否则返回 failure |
<T extends Throwable> void rethrow(Throwable failure) throws T |
:10-12 |
failure != null 时 throw (T) failure(无检查转换) |
rethrowWrapped(Throwable failure) |
:14-19 |
RuntimeException 原样抛 → Error 原样抛 → 其它包装成 new RuntimeException(failure) |
两个必须注意的实现细节
-
suppress的 self-suppress 防护(:7)。failure != cleanup这个判断防止t.addSuppressed(t)—— JDK 的addSuppressed会直接抛IllegalArgumentException("Self-suppression not permitted"),在异常清理路径上再抛一次会把原始异常吃掉。这是必需的防御,不是冗余判断。RenderFailuresTest.java:17专门测了这一点。 -
rethrow的无检查转换(:11)。throw (T) failure里T是方法类型参数,由调用点推断。RenderFailuresTest.java:19用assertThrows(Exception.class, () -> RenderFailures.rethrow(primary))让T推断为Exception。这依赖调用点的捕获类型;如果调用方只 catch 了具体异常而failure实际是别的类型,异常会直接逃逸出该 catch 块。这是有意的 sneaky-throw 模式,不是漏洞,但审计时容易误判。 -
rethrowWrapped不是rethrow的替代品 —— 前者会把受检异常包成RuntimeException,后者原样抛(靠类型擦除骗过编译器)。两者的使用场景不同,不要混用。
已知问题 / 风险
resetAfterCrash无异常保护。13 步任一步抛异常,后续 12 步全部跳过。第 2 步(destroyPipeline)和第 8 步(bindFramebuffer)最可能抛 —— 前者依赖 Iris 管线处于可销毁状态,后者依赖 GL 上下文仍然有效。没有 finally 链,没有聚合异常(尽管同包的RenderFailures.suppress正是干这个的,这里却没用上)。这是最值得指出的结构性缺陷。resetAfterCrash依赖 GL 上下文存活。它自己调用glBindFramebuffer/GLStateManager.reset(),这些在 GL 上下文已丢失(如窗口被系统回收)时会二次失败。crashTestArmed非 volatile。armCrashTest()在命令线程,throwIfCrashTestArmed()在渲染线程。private static boolean无内存屏障 —— 严格按 JMM,渲染线程不保证能看到命令线程写入的true。实践中单线程游戏循环 + 命令在主线程执行使其能工作,但这是未同步的跨线程可见性依赖。同理true→false的清除也不保证对其它线程可见。resetFont双重静默跳过(见上),mixin 失效时无任何诊断输出。RenderFailures本身没有缺陷,但RenderRecovery.resetAfterCrash没用它(见问题 1)。对比ParticleInstancer.endLayer()(particles/ParticleInstancer.java:208、:216、:224、:237)与TesrBatchRenderer(:594、:598)—— 这两处都正确地用suppress聚合了 finally 里的多个异常,是仓库内RenderFailures的正确用法样板。resetAfterCrash缺同样的聚合。
RenderFailures 的生产调用点(集合运算,20 处)
| 调用方 | 位置 | 用到的方法 | 性质 |
|---|---|---|---|
net/coderbot/batchedentityrendering/impl/AngelicaBufferSource.java |
:241、:244、:296、:303、:307、:318、:325、:330、:335、:343 |
suppress × 7、rethrow × 3 |
⚠️ 文件在内嵌上游包 net/coderbot.batchedentityrendering 内,但类名带 Angelica 前缀 |
com/gtnewhorizons/angelica/rendering/particles/ParticleInstancer.java |
:208、:216、:224、:237 |
suppress × 3、rethrowWrapped × 1 |
Angelica 自有 |
com/gtnewhorizons/angelica/rendering/tesr/TesrBatchRenderer.java |
:594、:598 |
suppress、rethrow |
Angelica 自有 |
com/gtnewhorizons/angelica/compat/mojang/RenderLayer.java |
:221、:228、:246、:250 |
suppress × 3、rethrow × 1 |
Angelica 自有 |
⚠️ AngelicaBufferSource 的归属需要留意:它的包路径是内嵌上游 net.coderbot.batchedentityrendering,但类名是 AngelicaBufferSource。包在 vendored 命名空间、类名带 mod 前缀 —— 这种「上游包内的 mod 私有类」是内嵌改写的常见手法,但也意味着它可能只被本仓 mixin 注入调用,无法从包名判断生命周期。
⚠️ RenderFailures 唯一未被使用的是 rethrowWrapped 的受检异常包装语义(只有 ParticleInstancer.java:237 一处用),其余全部用 rethrow 或 suppress。
相关条目
- AngelicaCommand -
armCrashTest()的触发点 - HUDCaching - 复位步骤 7
- TESR 实例化管线 - 复位步骤 9-11
- Subprojects(内嵌子项目) - GLSM 所在
- Iris(内嵌) - 复位步骤 2、12 的来源
- Compat Runtime -
BetterCrashesCompat的归属 - 帧节流内核 - 同属
rendering/顶层的另一子系统