TESR 实例化管线(rendering/tesr 核心)

基本信息

属性 值
包 com.gtnewhorizons.angelica.rendering.tesr
本条目覆盖 12 个文件(目录共 29 个)
总行数 603 + 355 + 204 + 168 + 67 + 44 + 39 + 30 + 27 + 18 + 12 + 7 = 1574
主题 批处理资格判定、绘制状态描述、模板实例化、状态变更兜底、生命周期与混合作用域

rendering/tesr/ 共 29 个文件,是本条目与 TESR 网格捕获、TESR 保留组 三篇共同覆盖的对象。

# 文件 行数 可见性
1 TesrBatchRenderer.java 603 public final(INSTANCE 单例)
2 InstancedTemplateRenderer.java 355 public final
3 DrawState.java 204 public final
4 BatchEligibility.java 168 public final
5 InstanceRing.java 67 public final
6 TesrProviderDispatch.java 44 public final
7 BatchStateFallback.java 39 包私有 final class
8 TesrBlendScope.java 30 public final
9 PassRebindGate.java 27 public final
10 TesrLifecycle.java 18 public final
11 TesrInstancingPipeline.java 12 public interface
12 TesrAttribution.java 7 public final

TesrInstancingPipeline:3 方法接口

12 行,只 3 个方法:

方法 行
rebindCurrentPass() :7
hasInstancedVariant(Instancing kind) :9
bindInstancedVariant(Instancing kind) :11

Instancing 是 GLSM 的枚举(com.gtnewhorizons.angelica.glsm.ffp.Instancing),其定义在 GLSM 子项目,本条目不可判定其有哪些值。

实现方:PassRebindGate.rebindIfDirty()(:20-23)持有 WorldRenderingPipeline 并调 pipeline.rebindCurrentPass() —— 内嵌 Iris 的 WorldRenderingPipeline 实现了这个接口(net.coderbot.iris.pipeline)。所以这是 Angelica 接口 ← Iris 实现 的方向,与其它条目里「Angelica 调 Iris」相反。

⚠️ bindInstancedVariant 的实现方在本包内不可见(grep 不到实现类)。若 Iris 的 pipeline 未实现,调用会 ClassCastException 或 AbstractMethodError。本仓库无法判定谁提供实现。

TesrAttribution:7 行的全局槽

public final class TesrAttribution {         // :3
    private TesrAttribution() {}              // :4
    public static Class<?> currentRenderable; // :6
}

public static 可写字段,无 volatile、无封装、无注释。 用途是「当前正在渲染的 renderer 类」,供 BatchEligibility 判断状态变更是否发生在 renderer 内部。

三个复位点:

位置 动作
RenderRecovery.resetAfterCrash()(RenderRecovery.java:39) TesrAttribution.currentRenderable = null —— 直接写字段,不是调方法
BatchEligibility 作用域内保存/恢复
正常渲染路径 由 mixin 设置

⚠️ 这是全仓「用 static 字段当跨类传参槽」最典型的一例,与 WitherArmorState.pendingInflate 同类。三个复位点分散在三个文件,任一路径漏复位都会让后续判定错乱。

⚠️ 字段是 Class<?> 强引用,会持有 renderer 类的 Class 对象 —— 阻止类卸载。renderer 类通常随 mod 常驻,影响可忽略。

TesrBlendScope:GLSM 状态的 push/pop 栈

30 行,把「进入混合作用域」的 GLSM 深度压进一个可扩容的 int 数组。

成员 行 说明
depths :12 int[4] 初值
depth :13 当前深度
public static void enter() {                                    // :15-20
    if (depth == depths.length) depths = Arrays.copyOf(depths, depth * 2);
    depths[depth++] = GLStateManager.pushState(StateSet.BLEND);
}
public static void exit() {                                     // :22-25
    if (depth == 0) return;
    GLStateManager.popStateTo(depths[--depth]);
}
public static void reset() { depth = 0; }                        // :27-29

⚠️ 只 push StateSet.BLEND 一个状态集。若作用域内改了矩阵、纹理、深度等,这些不在 push 范围内。

⚠️ exit() 在 depth == 0 时静默 return(:23)—— 不匹配的 enter/exit 不报错。这掩盖了配对错误。

⚠️ reset() 只置 depth = 0,不清 depths 内容(:28)。崩溃复位后 depths 里是过期的 GLSM 深度值。若复位后有人误调 exit(),depth == 0 会挡住 —— 这一层保护是有效的。

⚠️ 扩容用 Arrays.copyOf(堆分配)(:17),与 InstanceRing / MeshBuffer 用直接内存不同。enter() 在渲染热路径,深度超过 4 时会分配新数组。深度 4 对应 4 层嵌套混合作用域,实际几乎不会到。

PassRebindGate:按「世代」跳过重复 rebind

27 行,用两个世代计数器判断 Iris pass 是否需要重绑。

private static int lastProgramGen = -1;    // :9
private static int lastFbGen = -1;         // :10

rebindIfDirty()(:14-26):

步 行 动作
1 :15 !Iris.enabled → 直接返回
2 :16-19 program 代数 与 draw FBO 代数都没变 → 返回
3 :20-23 取 pipeline,非 null 时 rebindCurrentPass()
4 :24-25 记录两个新世代

GLSM 侧的两个计数器:GLStateManager.getProgramGeneration() 与 GLStateManager.getDrawFramebufferGeneration()。这两个「世代」在 GLSM 中的递增规则本条目不可判定(GLSM 子项目),只能确认它们是 int 且在 program / FBO 变更时递增。

⚠️ 两个世代初值都是 -1(:9-10),GLStateManager 的初值是 0,所以首次调用必然走 rebind。正确。

⚠️ 第 3 步的 pipeline == null 时不更新世代(:24-25 在 if (pipeline != null) 之外)—— 每次调用都会重查。正确但略浪费。

⚠️ lastProgramGen / lastFbGen 无 volatile。若 rebind 在渲染线程、program 变更在其它线程,世代读取可能过期。实际都在渲染线程。

调用方:PlayerReflectionCapture.flush()(PlayerReflectionCapture.java:214)—— 即护甲/玩家反射条目里提到的那个「三重前置检查」之一。

BatchEligibility:三态资格判定

168 行,判定「当前能否批处理」。核心是一个 3 值 byte 枚举。

常量 值 行 语义
UNKNOWN 0 :11 未判定
SAFE 1 :12 可批处理
DENIED 2 :13 不可批处理

⚠️ byte 值 1/2 与 DrawState.CULL_BACK = 1 / CULL_FRONT = 2 / CULL_BOTH = 3 数值巧合但语义无关。两套常量都在本包,不要混用。

入口与出口

方法 行 说明
begin(byte state, long drawCount) :84 返回 boolean
end(byte state, long drawCount) :97 返回新的 byte
beginIsolated(byte state, long drawCount) :68 隔离作用域
endIsolated(byte state, long drawCount) :76 返回 byte
batchingAllowed() :111
insideRenderer() (BatchStateFallback.java:19 用到) 是否在 renderer 内部

⚠️ begin 返回 boolean 而 end 返回 byte —— 不对称。调用方必须把 begin 的 boolean 转回 byte 存起来,再传给 end。这个不对称是接口设计的坑。

SavedScope:5 字段快照(:30-34)

private static final class SavedScope {
    int depth, bracketDepth, parts;         // 3 个 int
    boolean allowed, uncapturedState;       // 2 个 boolean
}

save(long drawCount)(:36)/ restore(long drawCount)(:51)成对。isolatedScopes 是 ArrayList<SavedScope>(:27),栈式。

⚠️ save / restore 都接收 drawCount 参数 —— 说明它们还会断言「作用域内 draw call 数量是否符合预期」,但本条目未读这两个方法的完整实现。beginExpectedDraws(:147)/ endExpectedDraws(:153)是一对显式的预期 draw 计数接口。

计数器回调

方法 行 何时调
onPartQueued() :136 有 ModelPart 入队
onPartFallback(drawsBefore, drawsNow) :140 回落到非批处理路径,参数是前后 draw 数
onUncapturedState() (BatchStateFallback.java:27 调) 检测到未捕获的状态变更
onBatchFlushed(long) (BatchStateFallback.java:36 调) flush 完成,参数是本次的 draw 数增量
onStateChange(Object renderer, byte state) :159 状态变更,带 renderer 引用

⚠️ onBatchFlushed(GLStateManager.drawCalls - before)(BatchStateFallback.java:36)—— flush 过程中若有第三方插入额外 draw call,计数会被污染,导致 onPartFallback 的启发式判断失准。

BatchStateFallback:未捕获状态变更的兜底 flush

39 行,包私有,是全包唯一包私有的类。类注释(:8):

Uncaptured state changes draw the queued batch before the mutation takes effect.

安装机制(:10-12)

private static final Runnable FLUSH = BatchStateFallback::flush;
static void install() { BatchStateGuard.flush = FLUSH; }

⚠️ 它通过写 GLSM 的 BatchStateGuard.flush 静态字段来接管回调(com.gtnewhorizons.angelica.glsm.hooks.BatchStateGuard)。这是一个全局单槽回调 —— 任何其它代码写 BatchStateGuard.flush 都会覆盖本安装。GLSM 侧的字段可见性与初始化顺序本条目不可判定(GLSM 子项目)。

⚠️ install() 是唯一的安装入口,调用方在本包外不可见。若 GLSM 初始化晚于 Angelica 安装,会被 GLSM 自己的默认值覆盖。本仓库无法判定安装顺序。

flush 的三道提前返回(:17、:19)

if (!models.isActive() && !tesrs.hasPendingGeometry()) return;                    // :17
if (!models.hasQueuedGeometry() && !tesrs.hasQueuedGeometry()
    && !BatchEligibility.insideRenderer()) return;                                // :19

第 2 行的注释(:18)说明了它的必要性:

Pass setup may establish defaults after beginPass, before any renderer or queued draw.

即「pass 建立默认值可能发生在 beginPass 之后、任何 renderer 或入队绘制之前」—— 此时的 flush 是多余的。

唯一的诊断输出(:20-24)

if (models.entityPassActive()) {
    final boolean inside = BatchEligibility.insideRenderer();
    GLStateManager.warnOnce("batch-fallback-entities:" + inside,
        "Uncaptured state change {} a renderer flushed the entity batch early",
        inside ? "inside" : "between", new Throwable());
}

warnOnce 的 key 是 "batch-fallback-entities:" + inside —— 只有 2 个可能的 key(true / false)。即每种情况一生只警告一次,带完整 new Throwable() 栈。

⚠️ 只诊断 models.entityPassActive() 这一种情况。若 flush 发生在非实体 pass(阴影、光照),静默无日志。这是最大的可诊断性缺口。

状态保存/恢复(:25-37)

final int mode = GLStateManager.getMatrixMode().getMode();     // :25
final int unit = GLStateManager.getActiveTextureUnit();         // :26
...
try {
    GLStateManager.glMatrixMode(GL11.GL_MODELVIEW);             // :30
    models.flushForStateChange();
    tesrs.flushForStateChange();
} finally {
    GLStateManager.glMatrixMode(mode);                          // :34
    GLStateManager.glActiveTexture(GL13.GL_TEXTURE0 + unit);    // :35
    BatchEligibility.onBatchFlushed(GLStateManager.drawCalls - before);  // :36
}

只保存/恢复 2 个状态:矩阵模式与活动纹理单元。⚠️ flush 内部若改了其它 GL 状态(program、VAO、FBO、混合),不会被恢复 —— 依赖 flushForStateChange 自己处理。

⚠️ onBatchFlushed 在 finally 里调,用的是 drawCalls - before(:28)。若 flushForStateChange 抛异常,onBatchFlushed 仍会执行(finally 语义),但计数不完整。

DrawState:绘制状态的描述与 interning

204 行,public final,是绘制状态的值对象 + 驻留表。

两组枚举常量

剔除模式(4 值)(:14-17):

常量 值 语义
DISABLED 0 不剔除
CULL_BACK 1 剔背面
CULL_FRONT 2 剔正面
CULL_BOTH 3 双面剔除

混合模式(5 值)(:19-23):

常量 值 语义
OPAQUE 0
TRANSLUCENT 1
ADDITIVE 2
ADDITIVE_ALPHA 3
GLINT 4

⚠️ 剔除是 4 值(0…3,两位),混合是 5 值(0…4)。packCull(boolean enabled, int faceMode)(:93)把两者打包,剔除占 2 位、混合占 3 位(共 5 位)。:56 的 int h = cull; 是哈希计算的起点。

驻留表(:25)

private static final ObjectOpenCustomHashSet<DrawState> CACHE = new ObjectOpenCustomHashSet<>(HashStrategy.INSTANCE);

fastutil 的 ObjectOpenCustomHashSet + 自定义 HashStrategy.INSTANCE。这是 Angelica 自己实现的 HashStrategy(不在本条目覆盖的文件里,属 api/ 或其它包 —— 见 API 层)。

of(cull, blend, depthEqual, depthWrite, colorWrite, alphaCutoff, lit, offsetFactor, offsetUnits)(:68)是 9 参数工厂。⚠️ 9 个参数里 4 个是 boolean —— 工厂调用点极易发生参数错位(相邻 boolean 传反不报错)。源码用长参数列表单行承载(:68),可读性差。

forMaterial(TesrMaterial material, int cull, boolean lit, float offsetFactor, float offsetUnits)(:72)从 TesrMaterial 派生,blendFor(TesrMaterial.Transparency)(:83)做 5 值映射。

liveCull()(:102)与 liveLit(TesrMaterial)(:106)读当前实时 GL 状态而非驻留值。

apply()(:110-~173,本文件最大方法)把状态写回 GLSM。equals(:175)/ hashCode(:180)/ toString(:185)三者齐备。

⚠️ 驻留表 CACHE 是静态且无上限。of() 每产生一个新状态组合就 intern 一个。9 个参数的组合空间很大,理论上是无界的内存增长。TesrLifecycle.reset() 调 DrawState.clearInterning()(:16)—— 复位入口存在。

TesrBatchRenderer:单例总控

603 行,public static final TesrBatchRenderer INSTANCE(:51)。全仓最重的渲染协调器之一。

三个 Pass 槽(:53-56)

常量 值 行
PASS_MAIN_0 0 :53
PASS_MAIN_1 1 :54
PASS_SHADOW 2 :55
PASS_COUNT 3 :56(私有)

⚠️ PASS_MAIN_0 / PASS_MAIN_1 是两个主 pass(多 DrawBuffer / 渲染到纹理的两级主渲染),不是「主 pass 的第 0/1 项」。1.7.10 的主渲染只有 1 个 FBO,所以这两个槽在无 Iris 时只有一个会被用。

5 个 Tracy Zone(:60-64)

Zone 名 常量 行
tesrOpaque Z_TESR_OPAQUE :60
tesrBatch Z_TESR_BATCH :61
tesrInstanced Z_TESR_INSTANCED :62
tesrText Z_TESR_TEXT :63
tesrDeferred Z_TESR_DEFERRED :64

全部 Tracy.COLOR_CLIENT。这是本包中 Tracy 标注最密集的文件 —— 说明 5 条路径是独立可观测的。

⚠️ Z_TESR_TEXT(文字)是一个独立路径 —— 说明 TESR 的 TileEntitySpecialRenderer 里可能有文本渲染(1.7.10 的 TileEntitySign 等)。

SWEEP_INTERVAL_MS(:58)

private static final long SWEEP_INTERVAL_MS = 5_000L —— 5 秒做一次清扫(清理过期模板 / 组)。无配置项。

LayerKey(:195)

static final class LayerKey —— 内部类,包私有。是渲染层的键。

对外方法(本条目已确认的)

方法 来源 语义
clearRetained() TesrLifecycle.java:11 清保留组
hasPendingGeometry() BatchStateFallback.java:17、:19 有待画几何
hasQueuedGeometry() BatchStateFallback.java:19 有已入队几何
flushForStateChange() BatchStateFallback.java:32 状态变更前 flush
entityPassActive() BatchStateFallback.java:20 实体 pass 是否活跃

⚠️ clearRetained / hasPendingGeometry / hasQueuedGeometry / flushForStateChange / entityPassActive 全部是包私有(BatchStateFallback 在同包才能调)。外部 mod 无法直接操作 TESR 批处理。

InstancedTemplateRenderer:模板 + 实例环

355 行,模板网格的 GPU 实例化绘制。

常量 值 行
TEMPLATE_TTL_MS AngelicaTesrMeshCache.LRU_TIMEOUT_MS :35
TEMPLATE_STRIDE VERTEX_SIZE * 4 :37
TEMPLATE_FLAGS COLOR_BIT | TEXTURE_BIT | NORMAL_BIT :38

⚠️ TEMPLATE_TTL_MS 与 RetainedTesrGroups.GROUP_TTL_MS 都直接等于 AngelicaTesrMeshCache.LRU_TIMEOUT_MS(RetainedTesrGroups.java:37)—— 三处共用一个 LRU 超时常量,改一处即改三处。这是有意的单一真源(不是重复),但也意味着改 TTL 会同时影响 3 个子系统。

⚠️ TEMPLATE_STRIDE = VERTEX_SIZE * 4 —— VERTEX_SIZE 未在本文件 import 列表中出现(应来自 GLSM 或静态导入),其值本条目不可判定。

TemplateMesh(:42)与 Bucket(:51)是两个私有内部类。Bucket 与 TemplateBuffer.bucketEpoch / bucketIndex(TemplateBuffer.java:9-10)对应 —— 模板按 bucket 分组,bucket 有世代。

InstanceRing:实例数据环

67 行,public final,是固定容量的实例数据环形缓冲。

方法 行 语义
upload(ByteBuffer data, int stride) :17 返回 long(上传的字节数?地址?)
keptUploadsEpoch() :38 返回 int
bufferId() :42 返回 int
postDraw() :46 绘制后
delete() :53

⚠️ upload 返回 long,本条目未读其实现,无法判定返回值语义(可能是上传字节数,也可能是地址)。

postDraw() 由 ParticleInstancer.endLayer() 调用(ParticleInstancer.java:240 附近的 ring().postDraw())—— 粒子与 TESR 共用同一套实例环机制(GLSM 的 Instancing)。

⚠️ delete() 的调用方在本包外不可见。若从不调用,GL buffer 泄漏。

TesrProviderDispatch:对外 provider 分发

44 行,接入 com.gtnewhorizons.angelica.api.tesr.TesrMeshProvider —— 这是 api/ 包,见 API 层。

resolveBlockEntityId(:16-25)

5 段 null 检查,逐层降级返回 0:

检查 行 返回
te == null :17 0
te.getBlockType() == null :18 0
matches == null :21 0
metaMap == null :23 0
正常 :24 Math.max(0, metaMap.get(metadata))

⚠️ 最后一步 Math.max(0, ...) —— 若 metaMap 里有负 id(Iris 的 BlockMaterialMapping 可能用 -1 表示「无匹配」),被夹到 0。0 与「无匹配」在 Iris 侧可能不是同一个语义(FallingBlockRendering.blockMaterialId 用的就是 -1 表示无匹配,见 Tessellator 条目)。同一概念在两处的哨兵值不同(0 vs -1)。

tryRender(:27-43)—— 教科书级 try/finally

if (!(renderer instanceof TesrMeshProvider provider)) return false;   // :28
final Object key = provider.angelica$meshKey(te);                      // :29
if (key == null) return false;                                        // :30

CapturedRenderingState.INSTANCE.pushCurrentBlockEntity();             // :32
CapturedRenderingState.INSTANCE.setCurrentBlockEntity(te == null ? null : te.getBlockType(),
                                                       te == null ? 0 : te.getBlockMetadata());   // :33
GLStateManager.glPushMatrix();                                         // :34
try {
    provider.angelica$transform(te, x, y, z);
    AngelicaTesrMeshCache.INSTANCE.renderCached(key, provider.angelica$meshDirty(te), provider, te);
} finally {
    GLStateManager.glPopMatrix();                                      // :39
    CapturedRenderingState.INSTANCE.popCurrentBlockEntity();          // :40
}
return true;

⚠️ te == null 的三重检查冗余 —— 第 30 行 key == null 时若 te 必为 null(meshKey(te) 的实现决定的),第 33 行的 te == null ? ... 就多余。反之若 te 可为 null 且 key 非 null,第 33 行是必需的。源码保留了防御,但没有说明 te 是否真的可为 null。

⚠️ 两处 glPushMatrix / glPopMatrix 正确配对,CapturedRenderingState 的 push/pop 也配对。这是全仓异常处理最规范的方法之一,可作为 DeferredEntityOverlay.recycle()(缺 finally)与 RenderRecovery.resetAfterCrash()(13 步无保护)的对照。

⚠️ return true 在 finally 之后(:42)。若 renderCached 抛异常,return true 不执行,异常向上传播 —— 正确(不吞异常)。

TesrLifecycle:崩溃复位

18 行,5 步复位(无 try/finally):

public static void reset() {                    // :10-17
    TesrBatchRenderer.INSTANCE.clearRetained(); // :11
    ModelPartBatcher.INSTANCE.clear();          // :12
    ParticleInstancer.clear();                  // :13
    RenderLayer.clearInterningAndHooks();       // :15
    DrawState.clearInterning();                 // :16
}

被 RenderRecovery.resetAfterCrash() 的第 9 步调用(RenderRecovery.java:37)。TesrBlendScope.reset() 是第 10 步(:38),TesrAttribution.currentRenderable = null 是第 11 步(:39)。

⚠️ TesrLifecycle.reset() 本身 5 步无异常保护,与 RenderRecovery 的 13 步同样问题。第 11 步 ParticleInstancer.clear()(:13)会 ParticleQuadMesh.delete() 与 ParticleDescriptorRegistry.clearCache()(见 粒子条目)—— 跨子系统的连锁清理。

⚠️ 5 步的顺序不可换:先清缓存(1-3)再清驻留表(4-5)。若 DrawState.clearInterning() 先执行而 ModelPartBatcher.clear() 仍持有 DrawState 引用,会出现「已清空的表被再次访问」。

已知问题 / 风险

  1. TesrAttribution.currentRenderable 是裸 public static 可写字段(:6),三个复位点分散在 RenderRecovery / BatchEligibility / 正常路径,任一漏复位即判定错乱。
  2. BatchStateFallback.install() 通过写 GLSM 的 BatchStateGuard.flush 全局单槽回调(:12),安装顺序无保障,且会被任何其它写入覆盖。
  3. BatchStateFallback.flush 的诊断只覆盖 entityPassActive() 一种情况(:20),非实体 pass 的提前 flush 完全静默。
  4. BatchStateFallback.flush 只保存/恢复矩阵模式与纹理单元(:25-26),flush 期间的其它 GL 状态变更不被恢复。
  5. BatchEligibility.begin 返回 boolean 而 end 返回 byte(:84、:97),接口不对称,调用方必须手工转换。
  6. DrawState.of 是 9 参数工厂,其中 4 个相邻 boolean(:68),参数错位不报错。
  7. DrawState 的 CACHE 驻留表静态无上限(:25),9 参数组合空间大,理论内存增长无界。
  8. TesrLifecycle.reset() 与 RenderRecovery.resetAfterCrash() 均无异常聚合(TesrLifecycle.java:10-17),任一步抛异常后续全部跳过。
  9. TesrBlendScope.exit() 在 depth == 0 时静默返回(:23),掩盖 enter/exit 配对错误。
  10. TesrBlendScope 只 push StateSet.BLEND(:19),作用域内其它状态不受保护。
  11. resolveBlockEntityId 的哨兵值 0 与 FallingBlockRendering.blockMaterialId 的 -1 不一致(TesrProviderDispatch.java:24),同一「无匹配」概念两处不同。
  12. TEMPLATE_TTL_MS / GROUP_TTL_MS / LRU_TIMEOUT_MS 三处共用一个常量(改一处改三处)。
  13. BatchEligibility 的 UNKNOWN/SAFE/DENIED = 0/1/2 与 DrawState.CULL_* = 0/1/2/3 数值巧合(同包内两套无关常量)。
  14. PassRebindGate 的两个世代计数器无 volatile(:9-10),pipeline == null 时每次重查。
  15. InstanceRing.upload 返回 long,语义未在公开 API 文档中说明;InstanceRing.delete() 的调用方在包外不可见。
  16. TesrInstancingPipeline.bindInstancedVariant 的实现方在本仓库不可见 —— 接口由内嵌 Iris 实现,但 Iris 侧是否真的实现本仓库无法判定。
  17. InstanceRing 被粒子与 TESR 共用(ParticleInstancer.endLayer 调 ring().postDraw()),一条链路的异常可能影响另一条。

相关条目