渲染崩溃恢复(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 检查:

  1. fr 是不是 Angelica 增强过的 FontRenderer(即 mixin 是否生效)
  2. 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)

两个必须注意的实现细节

  1. suppress 的 self-suppress 防护(:7)。failure != cleanup 这个判断防止 t.addSuppressed(t) —— JDK 的 addSuppressed 会直接抛 IllegalArgumentException("Self-suppression not permitted"),在异常清理路径上再抛一次会把原始异常吃掉。这是必需的防御,不是冗余判断。RenderFailuresTest.java:17 专门测了这一点。

  2. rethrow 的无检查转换(:11)。throw (T) failure 里 T 是方法类型参数,由调用点推断。RenderFailuresTest.java:19 用 assertThrows(Exception.class, () -> RenderFailures.rethrow(primary)) 让 T 推断为 Exception。这依赖调用点的捕获类型;如果调用方只 catch 了具体异常而 failure 实际是别的类型,异常会直接逃逸出该 catch 块。这是有意的 sneaky-throw 模式,不是漏洞,但审计时容易误判。

  3. rethrowWrapped 不是 rethrow 的替代品 —— 前者会把受检异常包成 RuntimeException,后者原样抛(靠类型擦除骗过编译器)。两者的使用场景不同,不要混用。

已知问题 / 风险

  1. resetAfterCrash 无异常保护。13 步任一步抛异常,后续 12 步全部跳过。第 2 步(destroyPipeline)和第 8 步(bindFramebuffer)最可能抛 —— 前者依赖 Iris 管线处于可销毁状态,后者依赖 GL 上下文仍然有效。没有 finally 链,没有聚合异常(尽管同包的 RenderFailures.suppress 正是干这个的,这里却没用上)。这是最值得指出的结构性缺陷。
  2. resetAfterCrash 依赖 GL 上下文存活。它自己调用 glBindFramebuffer / GLStateManager.reset(),这些在 GL 上下文已丢失(如窗口被系统回收)时会二次失败。
  3. crashTestArmed 非 volatile。armCrashTest() 在命令线程,throwIfCrashTestArmed() 在渲染线程。private static boolean 无内存屏障 —— 严格按 JMM,渲染线程不保证能看到命令线程写入的 true。实践中单线程游戏循环 + 命令在主线程执行使其能工作,但这是未同步的跨线程可见性依赖。同理 true → false 的清除也不保证对其它线程可见。
  4. resetFont 双重静默跳过(见上),mixin 失效时无任何诊断输出。
  5. 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。

相关条目