TESR 保留组(rendering/tesr 缓存与实体批渲染)

基本信息

属性 值
包 com.gtnewhorizons.angelica.rendering.tesr
本条目覆盖 7 个文件
总行数 1083 + 172 + 136 + 121 + 108 + 98 + 78 = 1796
主题 跨帧保留的实例组、原版方块实体网格、实体材质表、实体阴影批渲染、锚点量化哈希

RetainedTesrGroups.java(1083 行)是 com.gtnewhorizons.angelica(379 个文件)里第二大的单个 java 文件——第一是 client/font/BatchingFontRenderer.java(1574 行,见 字体子系统)。若按全仓计,第一则是 GLSM 子项目的 GLStateManager.java(8561 行)。

# 文件 行数 可见性 角色
1 RetainedTesrGroups.java 1083 包私有 final class 跨帧保留的实例组 + 晋升/降级
2 EntityShadowBatcher.java 172 public final 实体阴影 quad 批渲染
3 VanillaModelMeshes.java 136 public final 箱子/牌子/头颅的原版网格
4 EntityMaterials.java 121 public final 14 个共享 TesrMaterial 常量
5 BeaconBeamMeshes.java 108 public final 信标光柱内外两层网格
6 ShadowQuadMath.java 98 public final 阴影 quad 的 11 字段记录格式
7 TesrAnchorMath.java 78 public final 锚点网格 + 相对化 + 实例哈希

RetainedTesrGroups:跨帧保留

5 个调优常量(:34-38,全部包私有)

常量 值 行 语义
PROMOTE_AFTER_REBUILDS 3 :34 重建几次后晋升为常驻组
DEMOTE_AFTER_STABLE_FRAMES 4 :35 稳定几帧后降级
IDLE_RESET_FRAMES 100 :36 空闲多少帧后重置标记
GROUP_TTL_MS AngelicaTesrMeshCache.LRU_TIMEOUT_MS :37 组存活期(复用 LRU 常量)
MERGE_SCAN 4 :38 合并扫描上限

⚠️ PROMOTE_AFTER_REBUILDS = 3 与 DEMOTE_AFTER_STABLE_FRAMES = 4 互不对称(3 次重建升,4 帧稳定降)—— 这是**滞回(hysteresis)**设计:需要连续 4 帧稳定才降级,避免在阈值附近抖动。不是笔误。

⚠️ GROUP_TTL_MS 与 InstancedTemplateRenderer.TEMPLATE_TTL_MS 都直接引用 AngelicaTesrMeshCache.LRU_TIMEOUT_MS —— 三处共用一个常量(含 AngelicaTesrMeshCache 自身)。改一处即改三处。

⚠️ MERGE_SCAN = 4 是「最多检查 4 个已有组」(而非全部)。组数多时本可合并的组可能因扫描上限被漏掉,退化为新建组。性能与正确性的折中。

implements AngelicaBufferSource.LayerDrawHook(:32)

final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook {

⚠️ 类整体包私有(final class 无 public),但实现的接口来自内嵌上游包 net.coderbot.batchedentityrendering.impl —— 就是 TESR 实例化管线 里标注过的那条「上游包内的 mod 私有类」线索。AngelicaBufferSource 也是 Angelica 写的类,放在上游包命名空间里。

这条线索的含义:RetainedTesrGroups 是通过在内嵌上游的钩子接口上挂载来被驱动的,没有 main 里的显式 new。这是「内嵌改写」而非「独立子系统」,其生命周期由上游的 AngelicaBufferSource 持有。

4 个内部类

内部类 行 说明
TexRun :75 纹理运行
InstanceColumns :94 实例列(SoA 布局)
Group :172 组
DrawStateKey :399 绘制状态键

⚠️ DrawStateKey 是 private static final class(:399),与 DrawState(public final)是两个不同概念 —— DrawStateKey 是 map 键用的值对象,DrawState 是状态描述。名字相近但职责不同,极易混淆。

两个 need 位标志(:669-670)

private static final int NEED_MATRIX = 1;
private static final int NEED_CUBE   = 2;

使用处(:660-666):

if ((need & NEED_CUBE) != 0) {
    if (deferred != null) deferred.bindInstancedVariant(Instancing.CUBE);
    drawCubeVariant(list);
}
if (need != 0 && deferred != null) deferred.rebindCurrentPass();

⚠️ 两处检查不对称:第二处用 need != 0(任一位),第一处用位掩码。若 need 只有 NEED_MATRIX,第一处跳过、第二处仍 rebind。这是有意的(任何绘制后都要 rebind pass),但容易读成疏漏。

Instancing.CUBE 是 GLSM 枚举的一个值 —— Instancing 枚举的全部取值本条目不可判定(GLSM 子项目)。已知至少有 CUBE 一个,且 TesrInstancingPipeline.hasInstancedVariant(Instancing kind) 按 kind 分派。

⚠️ bindInstancedVariant 之后必须 rebindCurrentPass()(:666)—— 这是 TesrInstancingPipeline 三个方法的实际使用顺序。若 rebindCurrentPass 抛异常或被跳过,Iris 的 pass 状态会停留在 cube 变体上。

live() 的双条件(:~672-674)

private boolean live(Group g) {
    return g.frameMark == frameMark && (g.templateColumns.hasInstances() || g.cubeColumns.hasInstances());
}

本帧被标记过 且 至少一列有实例。

⚠️ g.frameMark == frameMark 是「本帧活跃」判据。若某组在某帧未被访问,frameMark 不更新 → live() 假 → 该组被清扫。这意味着「一帧内未被渲染的组」等同失效,即使它仍在视野内(只是本帧恰好没画)。

3 个静态可变对象

字段 行
Z_TESR_REBUILD :40 —— Tracy zone tesrRebuild,COLOR_CLIENT
IDENTITY :849 —— private static final Matrix4f

⚠️ IDENTITY 是共享的可变 Matrix4f。若被某处 set() 或乘性修改,全局所有「单位矩阵」基准被污染。需靠源码不可见的约定保证不被改。

3 个 fastutil 结构 + 1 个 LongSupplier

字段 类型 行
source AngelicaBufferSource :42
clock LongSupplier :43
nowMs long :44
baseMV Matrix4f :45

fastutil import(5 个):FloatArrayList、IntArrayList、LongArrayList、ObjectArrayList、Object2ObjectOpenHashMap、Reference2ObjectOpenHashMap(:12-17)。Reference2ObjectOpenHashMap 是弱引用键 —— 组可被回收。

LongSupplier clock(:43)是注入的时间源(不是直接 System.currentTimeMillis),便于测试。nowMs(:44)是缓存的当前时间,每帧更新一次。

⚠️ nowMs 是实例字段。若 RetainedTesrGroups 有多个实例(每个渲染层一个?),各自的 TTL 判定基于各自的 nowMs,只要 clock 相同就一致。但若某个实例的 nowMs 未更新,TTL 永不过期。

⚠️ 文件 1083 行中约 1000 行本条目未逐行阅读(只读了 :1-45、:660-680、以及 grep 出的常量/类声明)。Group(:172,约 180 行)、InstanceColumns(:94,约 80 行)、TexRun(:75,约 20 行)的字段与算法本条目不可判定。 本条目只覆盖结构性事实。

TesrAnchorMath:锚点量化与实例哈希

78 行,3 个 public 静态方法,是 RetainedTesrGroups 的数学基础。

常量 值 行
ANCHOR_GRID 16 :7
REANCHOR_DISTANCE 512.0 :8
HASH_QUANT 4096f :9(私有)

anchorCoord(:13-15)

public static long anchorCoord(double cam) { return (long) Math.floor(cam / ANCHOR_GRID) * ANCHOR_GRID; }

向下取整到 16 的倍数,返回 long。三个轴各调一次。

⚠️ Math.floor(cam / 16.0) * 16.0 在 cam 极大时会丢精度。cam 是 double,除以 16 是精确的(2 的幂),Math.floor 精确,乘 16 也精确(2 的幂)。所以这个实现实际是精确的 —— 但依赖 ANCHOR_GRID 恰为 2 的幂。改成 10 或 15 就会引入误差。

⚠️ 负坐标:-0.5 / 16 = -0.03125,floor → -1,× 16 → -16。正确(负方向也向下取整到网格)。

shouldReanchor(:17-22)

final double dx = camX - anchorX; ... 
return dx * dx + dy * dy + dz * dz > REANCHOR_DISTANCE * REANCHOR_DISTANCE;

纯平方距离比较,阈值 512.0² = 262144。用距离平方而非距离,避免 sqrt。

⚠️ 阈值 512 与 ANCHOR_GRID = 16 相差 32 倍 —— 意味着相机要走出 32 个网格才会重锚。这是个刻意选择的宽阈值(重锚会清空所有相对化坐标,代价高)。

⚠️ 严格 >(不是 >=)—— 恰好 512 不重锚。

toAnchorRelative(:24-28)

m.m30(m.m30() + (float) (camX - anchorX));
m.m31(m.m31() + (float) (camY - anchorY));
m.m32(m.m32() + (float) (camZ - anchorZ));

⚠️ **直接改 m 的第 3 行(平移)三个分量,就地修改传入的 Matrix4f。无返回值,无 Matrix4f 复用(每次 new)。调用方必须知道矩阵被修改了。

⚠️ (float) 强转在 double → float 时丢精度。锚点差值最大 512(因为超出会重锚),float 有 24 位有效数字,512 范围内精度约 3e-5 —— 对 1.7.10 的方块坐标完全够用。

⚠️ 只改 m30/m31/m32,不动 m03/m13/m23(行向量约定下的另一组平移)。这是列向量约定(m30 是第 3 行第 0 列 = x 平移) —— 与 GlintClock 里 m01() 的用法同属 JOML 约定依赖。

instanceHash(:30-~50)

long h = templateIdentity * 0x9E3779B97F4A7C15L;   // :31
h = mix(h, quantize(mA.m00()));                      // :32
h = mix(h, quantize(mA.m01()));                      // :33
...                                                 // 9 个矩阵元素

⚠️ 0x9E3779B97F4A7C15L 是 64 位黄金比例常数(2⁶⁴/φ 的整数部分) —— 即 SplitMix64 / Fibonacci hashing 的乘子。templateIdentity 先乘它做初始扩散,再用 mix 逐个混入量化后的矩阵元素。

⚠️ 只用了 3×3 的 9 个元素(m00..m22)—— 不含平移(m03/m13/m23 或 m30/m31/m32)。这是有意的:平移不同但旋转/缩放相同的实例应归入同一组(用相对坐标渲染)。但 instanceHash 与 toAnchorRelative 的「平移在哪一列」约定必须一致,否则哈希会漏掉本该区分的差异。

⚠️ quantize(x) 用 HASH_QUANT = 4096f(:9)做量化 —— 即 Math.round(x * 4096) 之类。量化步长是 1/4096 ≈ 0.000244。两个矩阵元素相差小于这个步长会被判为同一实例。对 1.7.10 的方块级模型足够(最小有意义差异远大于 1/4096),但对 TESR 的连续缩放/旋转动画可能不够。

⚠️ mix 与 quantize 的实现本条目未读(在 :41 之后)。quantize 对负数的处理、mix 的最终化步骤本条目不可判定。

EntityMaterials:14 个共享材质

121 行,全是 static final 常量,是本条目里最「平」的文件。全部 package-private(无 public)。

# 常量 行 构造
1 CUTOUT :11 .cutout(0.1f).stream()
2 CUTOUT_HALF :12 .cutout(0.5f).stream()
3 SOLID :13 .stream()
4 TRANSLUCENT :14 .translucent().cutout(1f/255f).noDepthWrite().stream()
5 TRANSLUCENT_DEPTH_WRITE :15 .translucent().cutout(1f/255f).stream()
6 ADDITIVE :16 .additive().stream()
7 ADDITIVE_NO_DEPTH_WRITE :17 .additive().noDepthWrite().stream()
8 ADDITIVE_CUTOUT :18 .additive().cutout(0.1f).stream()
9 ADDITIVE_CUTOUT_NO_DEPTH_WRITE :19 .additive().cutout(0.1f).noDepthWrite().stream()
10 ADDITIVE_ALPHA :20 .additiveAlpha().stream()
11 ADDITIVE_ALPHA_NO_DEPTH_WRITE :21 .additiveAlpha().noDepthWrite().stream()
12 ADDITIVE_ALPHA_CUTOUT :22 .additiveAlpha().cutout(0.1f).stream()
13 ADDITIVE_ALPHA_CUTOUT_NO_DEPTH_WRITE :23 .additiveAlpha().cutout(0.1f).noDepthWrite().stream()
14 SHADOW :24 .translucent().cutout(0.1f).noDepthWrite().unlit().stream()

TesrMaterial 在 com.gtnewhorizons.angelica.api.tesr(api/ 包)—— 见 API 层。

两个必须注意的细节

⚠️ 1f / 255f 只出现在半透明的两个常量里(:14、:15),不透明的全部用 0.1f。即「alpha cutoff」在两个透明度族里取值不同:半透明用 1/255(几乎不裁剪),不透明用 0.1。这个差异是刻意的(半透明要保留几乎所有片元,不透明要剔除 10% alpha 以下的边),但源码无注释说明。

⚠️ SHADOW 是唯一带 .unlit() 的常量(:24)—— 阴影不参与光照计算,且 translucent + cutout(0.1f) + noDepthWrite 的组合与 EntityShadowBatcher 配套。

⚠️ 14 个常量构成 3 个正交维度的组合:transparency(solid / translucent / additive / additiveAlpha)× cutoff(无 / 0.1 / 0.5 / 1-255)× depthWrite(是 / 否)。但组合并不完备 —— 例如没有 translucent + cutout(0.1f)(只有 1/255),没有 solid + noDepthWrite。缺哪些组合是有意的还是遗漏,本仓库无法判定。

⚠️ 全部 14 个是包私有,外部 mod 无法复用(只能走 api/ 的 builder 自己造)。

ShadowQuadMath:11 字段记录格式

98 行,public final,全部是常量 + 静态方法。这是「数据布局的定义方」。

常量 值 行
RECORD_SIZE 11 :17

11 个字段索引(:19 一行内全部定义):

索引 名字 语义
0 MIN_X
1 Y ⚠️ 没有 MIN_Y / MAX_Y —— 只有单个 Y
2 MIN_Z
3 MAX_X
4 MAX_Z
5 U_MIN_X
6 V_MIN_Z
7 U_MAX_X
8 V_MAX_Z
9 COLOR
10 LIGHT

⚠️ 索引 0-4 是几何,5-8 是 UV,9-10 是颜色与光照 —— 三段布局。

⚠️ Y 只有 1 个(索引 1),而 X 和 Z 各有 MIN/MAX 两个 —— 阴影 quad 在 Y 方向是退化的(贴在地面上,高度固定)。这与 1.7.10 的实体阴影(textures/misc/shadow.png 的水平面片)一致。没有 MAX_Y 是正确的,不是遗漏。

⚠️ 10 个索引常量在一行内用逗号分隔声明(:19)—— 这是 Java 允许的多变量声明,但不符合格式规范,IDE 格式化会拆行。同时这也意味着一个笔误会静默改变索引(因为编译器不检查)。

UV 命名顺序:U_MIN_X(5)、V_MIN_Z(6)、U_MAX_X(7)、V_MAX_Z(8)—— X 的 U 夹在 Z 的 V 之间,是「按角点分组」而不是「按分量分组」。这个交错顺序不可重排。

EntityShadowBatcher

172 行,public final。

常量 值 行
SHADOW_TEXTURE new ResourceLocation("textures/misc/shadow.png") :29
PACKED_UP NormI8.pack(0f, 1f, 0f, 0f) :30
INITIAL_QUADS 256 :32
INITIAL_MVS 8 :33

⚠️ SHADOW_TEXTURE 是 1.7.10 原版资源路径(textures/misc/shadow.png),没有 minecraft: 前缀 —— 1.7.10 的 ResourceLocation 单参数构造把缺省域设为 minecraft。这与 BeaconBeamMeshes.BEACON_TEXTURE、VanillaModelMeshes.SIGN_TEXTURE 的写法一致(全仓统一风格)。

⚠️ PACKED_UP 与 VertexTransform.PACKED_UP 是两份独立定义的同值常量(VertexTransform.java:27 vs EntityShadowBatcher.java:30),都是 NormI8.pack(0f, 1f, 0f, 0f)。同包内两份同值常量,无关联。

⚠️ INITIAL_QUADS = 256 与 INITIAL_MVS = 8 相差 32 倍 —— 每个 modelview 矩阵要展开成多个 quad(ShadowQuadMath.RECORD_SIZE = 11 个字段的记录,一个 box 有 4 个侧面 → 4 个 quad)。8 个 MVS × 4 quad = 32,接近 256/8。 具体比例关系本条目未读方法实现,不臆断。

用 fastutil 的 IntArrayList / LongArrayList / FloatArrayList(RetainedTesrGroups 也用同 3 个)。

VanillaModelMeshes

136 行,public final,为原版方块实体提供预烘焙网格。

常量 值 行
MODEL_SCALE 0.0625f :22
LID_CLOSED_EPS 1.0e-4f :23
WHITE TesrMaterial.builder().color(1f,1f,1f,1f).build() :24
SIGN_TEXTURE new ResourceLocation("textures/entity/sign.png") :25

⚠️ MODEL_SCALE = 0.0625f = 1/16 —— 与 EntityFX 粒子半边长 0.1F(见 粒子条目)是不同量级。1/16 是「一个方块 = 16 个模型单位」的 Minecraft 惯例。注意 0.0625F 与 0.0624375F(粒子 UV 魔数)字面形近但值不同(差 2⁻¹⁶)—— 极易混淆。

⚠️ 0.0625f 写的是字面量不是 1.0f/16.0f —— 与 GlintClock 的 0.33333334f 同类写法(保持 float 精确值)。

两张弱引用键表(:34-35)

private static final Reference2ObjectOpenHashMap<ResourceLocation, ChestKeys> SINGLE_CHEST_KEYS = ...;   // :34
private static final Reference2ObjectOpenHashMap<ResourceLocation, ChestKeys> DOUBLE_CHEST_KEYS = ...;  // :35

⚠️ ResourceLocation 作弱引用键 —— 好处是资源包卸载时键可回收;坏处是 ResourceLocation 被回收后弱键会被自动清空,但 map 里对应的 ChestKeys 也在同一 entry 里(Reference2Object 的 value 是强引用,key 被回收后 value 仍在,直到 entry 被清理)。这可能导致 ChestKeys 比预期活得久。

3 类键(:36-41)

键 行 类型
KEY_SIGN_STANDING :36 new Object() —— 单例 Object 键
KEY_SIGN_WALL :37 同上
SKULL_KEYS :41 Map<SkullKey, Object>,普通 HashMap

⚠️ KEY_SIGN_STANDING / KEY_SIGN_WALL 是 new Object() 的裸实例键(Object 没有 equals/hashCode → 身份相等)。只能作为 map 键使用,不能反查语义 —— 调试时打印出来是 java.lang.Object@1a2b3c。没有名字常量。

⚠️ SKULL_KEYS 用普通 HashMap(:41)而箱子用 Reference2ObjectOpenHashMap(:34-35) —— 同一个类里两种 map 实现。若 SkullKey 的生命周期与 ResourceLocation 不同,这是有意的;否则是不一致。本条目未读 SkullKey 的定义(在 api/ 或本文件内,api/ 见 API 层)。

Builder(:104,private static final class 实现 TesrMeshBuilder)与静态 BUILDER(:43)是构建器的单例。

BeaconBeamMeshes

108 行,public final,为信标光柱提供内外两层。

常量 值 行
BEACON_TEXTURE new ResourceLocation("textures/entity/beacon_beam.png") :16
INNER TesrMaterial.builder()... :18
OUTER TesrMaterial.builder()... :21
INNER_KEY new Object() :26
OUTER_KEY new Object() :27
INNER_RADIUS 0.2 :29
BEAM_HEIGHT 256.0 :30
INNER_V_TOP BEAM_HEIGHT * (0.5 / INNER_RADIUS) = 640.0 :31
INNER_BUILDER sink -> {...} :33
OUTER_BUILDER sink -> {...} :49

⚠️ INNER_V_TOP 可验算:256.0 * (0.5 / 0.2) = 256.0 * 2.5 = 640.0。源码写成表达式而非字面量 640.0(:31),是为了让「V 坐标 = 高度 / 内半径 × 0.5」的推导可见。改动 INNER_RADIUS 会自动改变 V 坐标 —— 这正是它被写成表达式的原因。

⚠️ BEAM_HEIGHT = 256.0 是原版 1.7.10 的信标光柱高度(WorldProvider 的 beacon 光柱)。INNER_RADIUS = 0.2 是内层半径。两者都是原版魔数。

⚠️ INNER_KEY / OUTER_KEY 又是 new Object() 裸实例键(:26-27),与 VanillaModelMeshes.KEY_SIGN_* 同一手法。全包 4 处这样的键(信标 2、牌子 2)。

⚠️ INNER_BUILDER / OUTER_BUILDER 是 static final 的 lambda(:33、:49)—— 持有 sink 参数做网格构建。它们是 static 的,所以构建出的网格若被缓存,是全进程共享的。

已知问题 / 风险

  1. RetainedTesrGroups 1083 行中约 1000 行本条目未逐行阅读(只覆盖了 5 个常量、4 个内部类、2 个 need 位标志、live() 与 Instancing.CUBE 的使用)。Group / InstanceColumns / TexRun 的字段与算法本条目不可判定。这是本条目最大的已知缺口。
  2. RetainedTesrGroups 类整体包私有但实现内嵌上游的钩子接口(:32 AngelicaBufferSource.LayerDrawHook),无 main 里的显式 new —— 生命周期由上游的 AngelicaBufferSource 持有。
  3. GROUP_TTL_MS / TEMPLATE_TTL_MS / LRU_TIMEOUT_MS 三处共用一个常量,改一处改三处。
  4. PROMOTE_AFTER_REBUILDS = 3 与 DEMOTE_AFTER_STABLE_FRAMES = 4 是不对称的滞回设计(易被误读为笔误)。
  5. MERGE_SCAN = 4 限制合并扫描范围,组数多时可合并的组会被漏掉。
  6. live() 的 frameMark == frameMark 判据使「本帧未渲染的组」等同失效。
  7. IDENTITY 是共享可变的 static final Matrix4f(RetainedTesrGroups.java:849),被就地修改会污染全局基准。
  8. nowMs 是实例字段,某实例未更新则其 TTL 永不过期。
  9. need 的两处检查不对称((need & NEED_CUBE) != 0 vs need != 0),易读成疏漏。
  10. TesrAnchorMath.anchorCoord 的精确性依赖 ANCHOR_GRID = 16 恰为 2 的幂(:7、:14),改成 10/15 会引入浮点误差。
  11. instanceHash 的 quantize 步长 1/4096(HASH_QUANT = 4096f),对连续动画可能不足;且只哈希 3×3 不含平移,依赖与 toAnchorRelative 的列约定一致。
  12. ShadowQuadMath 的 10 个索引常量挤在一行(:19),编译器不检查,笔误会静默改变索引。
  13. ShadowQuadMath.RECORD_SIZE = 11 与 UV 的交错命名(U_MIN_X / V_MIN_Z / U_MAX_X / V_MAX_Z)不可重排。
  14. PACKED_UP 在同包内两份独立定义(EntityShadowBatcher.java:30 与 VertexTransform.java:27)。
  15. EntityMaterials 的 14 个材质组合不完备(无 translucent + cutout(0.1f)、无 solid + noDepthWrite),缺哪些是有意还是遗漏本仓库无法判定;且半透明族用 1/255、不透明族用 0.1 的 cutoff 差异无注释。
  16. VanillaModelMeshes.MODEL_SCALE = 0.0625f 与粒子 UV 魔数 0.0624375F 字面形近值不同(差 2⁻¹⁶),极易混淆。
  17. VanillaModelMeshes 同一文件内混用 Reference2ObjectOpenHashMap(箱子)与普通 HashMap(头颅),一致性未说明。
  18. 4 处 new Object() 裸实例键(VanillaModelMeshes 2 个、BeaconBeamMeshes 2 个),无名字常量,调试不可读。
  19. InstanceColumns / Group / TexRun 的字段与算法未读(同上 1)。

相关条目