阴影体素化 Sink(rendering/voxelization)

基本信息

属性 值
包 com.gtnewhorizons.angelica.rendering.voxelization
本条目覆盖 1 个文件:SdlShadowVoxelizationSink.java(92 行)
目录内另 1 个 ShadowVoxelizer.java —— 已被其它条目提及(见 Celeritas)
可见性 public final implements ShadowVoxelizer.Sink
依赖 内嵌 Iris(1 类)+ 内嵌 Embeddium/Sodium(3 类)+ GLSM(3 类)

⚠️ rendering/voxelization/ 只有 2 个文件,其中 ShadowVoxelizer(接口的持有方)已在上轮被覆盖,故本条目只写实现方。

它解决的问题

ShadowVoxelizer.Sink 是一个回调接口,本类实现它,把「地形顶点缓冲」喂给 GLSM SDL 端的体素化后端。

体素化(voxelization) 是现代阴影技术:把场景几何编码进 3D 体素纹理,让阴影贴图获得高精度遮挡。1.7.10 没有这个机制,着色器包(Iris)期待它存在。本类是 Angelica 补的适配层。

SSBO 绑定槽来自 Iris(:17)

private static final int SSBO_BINDING = RwImageStoreExtractor.VG_VBUF_SSBO_BINDING;

⚠️ ⚠️ 绑定槽号不是字面量,而是引用 net.coderbot.iris.pipeline.transform.RwImageStoreExtractor(内嵌 Iris)的静态常量(:6 import)。

这是本文件最重要的一行。 它意味着:

事实 含义
槽号由内嵌 Iris 决定 Iris 改这个常量,Angelica 跟随 —— 编译期一致,语义由 Iris 定
用 RwImageStoreExtractor 的槽 说明这个槽原本是给 Iris 的「渲染目标顶点缓冲 SSBO」用的,体素化复用同一槽
Iris 若移除该常量 编译失败(好过静默用错槽)

⚠️ 「复用 Iris 的 SSBO 槽」意味着槽位冲突风险 —— 若 Iris 同时也在用 VG_VBUF_SSBO_BINDING,两者的绑定会互相覆盖。⚠️ finish()(:85-91)只调 endVoxelizationBatch,不调 glBindBufferBase(..., 0) 解绑 —— 解绑责任在 GLSM 的 endVoxelizationBatch 内(GLSM 子项目,本条目不可判定它是否解绑)。这是本文件最需要确认的一点。

5 个「一次性告警」标志与 3 个静态计数器

字段 行 类型 语义
LOG :16 Logger LogManager.getLogger("Angelica")
encodersThisFrame :19 static int 本帧成功创建的 encoder 数
dispatchesThisFrame :20 static int 本帧的 dispatch 次数
regionsThisFrame :21 static int 本帧的 region 数
warnedBindFailure :23 boolean(实例)
warnedBeginFailure :24 boolean(实例)
pass :25 long 当前 encoder pass 句柄
pendingVertexBuffer :26 int 待绑定的 GL buffer handle
pendingX/Y/Z :27-29 float region 偏移

⚠️ 两个告警标志是实例字段(private boolean),3 个计数器是静态字段 —— 混用。⚠️ 告警「一次性」的语义是「每个 sink 实例每种失败只警告一次」 —— 若 ShadowVoxelizer 每帧新建 sink,告警会每帧打。⚠️ sink 的创建/复用策略本条目不可判定(ShadowVoxelizer 已在上轮覆盖,但创建点未核实)。

⚠️ 两个告警的文案都是「不带修复指引」的(:56、:68):

LOG.warn("shadow voxelization: could not bind region vertex buffer {} at SSBO slot {}", ...);   // :56
LOG.warn("shadow voxelization: compute encoder refused; no voxel writes this pass");             // :68

⚠️ :56 有两个 {} 占位符与两个实参(正确),:68 是纯字符串无占位符。⚠️ 两条都不含「请检查 X」的建议,与 RenderPassIndex.indexOf 那条含修复指引的异常消息(见 GPU 视锥剔除)形成对比。

⚠️ encodersThisFrame / dispatchesThisFrame / regionsThisFrame 是 static int 非 volatile(:19-21)—— 与 DroppedItemInstancer 的 11 个静态可变字段同类问题(见 掉落物品实例化)。

两个 Sink 回调方法

region(:35-47)—— 登记 region,不立即绑定

final RenderRegion.DeviceResources resources = region.getResources();     // :37
if (resources == null) return false;                                      // :38
final GlBuffer vertexBuffer = resources.getVertexBuffer();                // :39
if (vertexBuffer == null || vertexBuffer.handle() == 0) return false;     // :40
pendingVertexBuffer = vertexBuffer.handle();                              // :41
pendingX = offsetX; pendingY = offsetY; pendingZ = offsetZ;               // :42-44
regionsThisFrame++;                                                       // :45
return true;

5 个检查点(:38、:40 的两个),只存不绑 —— 实际绑定推迟到 range(:51-61)。

⚠️ vertexBuffer.handle() == 0 的检查(:40)—— handle() 返回 int 的 GL buffer name,0 是默认/无效值。⚠️ GL 没有「buffer 0 无效」的标准 —— glBindBufferBase 绑 0 是合法的(读零长度)。这里把 0 当无效是本实现的选择,若上游某天用 buffer 0 做真实缓冲,会被误拒。

⚠️ region 只设置 pending*,不验证多个 region 的连续性 —— 若调用方连续 region(A)、region(A)、region(B),A 的登记被覆盖。⚠️ 接口契约要求 region 后紧跟 range,本文件无断言。这是隐式跨文件契约。

⚠️ regionsThisFrame++ 在 return true 之前(:45)—— 计数的是「成功登记」而非「成功编码」。

range(:49-76)—— 两阶段:先绑 SSBO,再 dispatch

第一阶段:绑定 region 顶点缓冲(:51-61)

if (pendingVertexBuffer != 0) {
    GLStateManager.glBindBufferBase(GL43.GL_SHADER_STORAGE_BUFFER, SSBO_BINDING, pendingVertexBuffer);   // :52
    if (!sdl().bindVoxelizationRegion(SSBO_BINDING, pass, pendingX, pendingY, pendingZ)) {                  // :53
        if (!warnedBindFailure) { warnedBindFailure = true; LOG.warn(...); }                                // :54-57
        return false;                                                                                       // :58
    }
    pendingVertexBuffer = 0;                                                                                 // :60
}

⚠️ glBindBufferBase(GL_SHADER_STORAGE_BUFFER, ...) 用的是 GL43(:12 import)—— 需要 GL 4.3。无版本检查 —— 在不支持 SSBO 的上下文里会 GL_INVALID_OPERATION(不崩,静默失败),随后 bindVoxelizationRegion 大概返回 false,触发一次性告警。这实际上是一个「优雅降级」。

⚠️ ⚠️ pass 在第一阶段被用作 bindVoxelizationRegion 的参数(:53),但 pass 此时可能还是 0(首次调用时第二阶段还没执行 :63)。⚠️ 即「首次 range 调用时,绑定用的 pass 是 0」。这要么是 GLSM 侧容忍 pass == 0(把绑定登记下来,等 beginVoxelizationBatch 返回真实 pass 后再用),要么是顺序缺陷。本条目无法判定 GLSM 侧行为 —— 但这是本文件最需要向 GLSM 侧确认的一行。

⚠️ 绑定失败时 pendingVertexBuffer 不清零(:58 直接 return,:60 未执行)—— 下一次 range 会重试绑定。这是正确的重试语义(因为失败可能是暂时的)。

第二阶段:懒创建 encoder pass(:62-72)

if (pass == 0) {
    pass = sdl().beginVoxelizationBatch(SSBO_BINDING);       // :63
    if (pass != 0) encodersThisFrame++;                       // :64
    if (pass == 0) {
        if (!warnedBeginFailure) { warnedBeginFailure = true; LOG.warn(...); }   // :66-69
        return false;                                          // :70
    }
}

⚠️ pass 是 long(不是 int)(:25、:63)—— GLSM 的 pass 句柄是 64 位,与 RenderPassIndex 的 int 索引(见 GPU 视锥剔除)是两种不同的「pass」概念,不要混用。

⚠️ beginVoxelizationBatch 返回 0 表示失败(:65 的 if (pass == 0))—— 0 是失败哨兵。⚠️ 与 pendingVertexBuffer 的 0 哨兵(:40、:51、:60)不同语义但同样用 0。

⚠️ encodersThisFrame++ 只在 pass != 0 时(:64)—— 统计「成功创建的 encoder 数」,且因为 pass 一旦非 0 就走 if (pass == 0) 的假分支,每个 sink 实例每帧只创建 1 个 encoder(直到 finish() 清零)。⚠️ 但 encodersThisFrame 是静态的 —— 多个 sink 实例共享计数。

第三阶段:dispatch(:73-75)

sdl().voxelizeRange(pass, vertexOffset, vertexCount);   // :73
dispatchesThisFrame++;                                 // :74
return true;

⚠️ voxelizeRange 的返回值被丢弃(:73)—— 若它返回失败状态,无检查。⚠️ 而 bindVoxelizationRegion / beginVoxelizationBatch 的返回值都检查了(:53、:65)。三个 GLSM 调用里唯一一个不检查返回值 —— 这是本文件最可能出静默失败的一行。

finish 与三个 takeXxx(:78-91)

public static int takeEncoders()  { final int n = encodersThisFrame;  encodersThisFrame = 0;  return n; }   // :78
public static int takeDispatches(){ final int n = dispatchesThisFrame; dispatchesThisFrame = 0; return n; }   // :80
public static int takeRegions()   { final int n = regionsThisFrame;   regionsThisFrame = 0;   return n; }   // :82

@Override public void finish() {                    // :85
    if (pass != 0) { sdl().endVoxelizationBatch(pass); pass = 0; }   // :86-89
    pendingVertexBuffer = 0;                         // :90
}

3 个 takeXxx 是「读并清零」(read-and-reset),典型的帧统计模式。

⚠️ 三个 takeXxx 各自独立清零自己的字段 —— 若调用方只取一个,另两个会累积到下一帧。⚠️ regionsThisFrame 只在 region() 成功时累加,takeRegions 不检查是否有对应 dispatch —— 「登记了 region 但 range 全失败」时 takeRegions > 0 而 takeEncoders == 0。这三个计数器不保证自洽。

⚠️ ⚠️ takeEncoders 等三个是 public static 且直接读写非 volatile 的静态字段(:19-21、:78-82)—— 跨线程读/清零无 happens-before。⚠️ 若 Tracy 线程调用 takeXxx 而渲染线程累加,会丢计数。这三个是本文件最可能被外部线程调用的 public API。

⚠️ finish() 不调 takeXxx —— 结束体素化批次与取统计是分离的。⚠️ 若调用方忘记 finish(),pass 非 0 保持到下次 range,而 pendingVertexBuffer 已清零(:90) → 下次 range 走 if (pass == 0) 假分支,用旧 pass dispatch 新 region 的数据。⚠️ 这是真实的状态泄漏路径(接口契约要求 finish() 必被调用,本文件无 AutoCloseable 保障)。

⚠️ finish() 无 try/finally —— endVoxelizationBatch 抛异常则 pass 不清零(下次继续用坏 pass)。

依赖清单(10 个 import)

包 类 行
com.gtnewhorizons.angelica.glsm GLStateManager :3
com.gtnewhorizons.angelica.glsm.backend BackendManager、RenderBackend :4-5
net.coderbot.iris.pipeline.transform RwImageStoreExtractor :6
org.embeddedt.embeddium.impl.gl.attribute GlVertexFormat :7
org.embeddedt.embeddium.impl.gl.buffer GlBuffer :8
org.apache.logging.log4j LogManager、Logger :9-10
org.embeddedt.embeddium.impl.render.chunk.region RenderRegion :11
org.lwjgl.opengl GL43 :12

⚠️ GL43(1.7.10 时代不存在的 GL 版本常量)出现在一个为 1.7.10 服务的 mod 里 —— LWJGL 2.9 提供 org.lwjgl.opengl.GL43 的常量定义(只是常量,不需要运行时的 GL 4.3 上下文)。⚠️ 常量可用 ≠ 功能可用 —— 实际 SSBO 支持取决于 GLSM 所用的 GL 后端(SDL/GLX/WGL)。无版本查询代码。

⚠️ sdl() 静态辅助(:31-33)只是 BackendManager.RENDER_BACKEND 的 getter —— 名字叫 sdl() 但返回的是抽象 RenderBackend(可能不是 SDL 后端)。⚠️ 类名 SdlShadowVoxelizationSink 与方法名 sdl() 暗示 SDL 专用,实际是通用后端。这是命名与实现的偏差。

⚠️ GLSM 的 4 个体素化方法(bindVoxelizationRegion、beginVoxelizationBatch、voxelizeRange、endVoxelizationBatch)是 GLSM 扩展的标准 GL 之外的方法,定义在 RenderBackend(GLSM 子项目)。本条目不可判定它们的实现。

已知问题 / 风险

  1. SSBO 槽号复用内嵌 Iris 的 RwImageStoreExtractor.VG_VBUF_SSBO_BINDING(:17)—— 槽位冲突风险取决于 Iris 何时用该槽。
  2. finish() 不解绑 SSBO(:85-91),解绑责任在 GLSM 的 endVoxelizationBatch 内(本条目不可判定)。
  3. ⚠️ 首次 range 调用时 bindVoxelizationRegion 收到的 pass 是 0(:53 vs :63 的顺序)—— 是 GLSM 侧容忍还是顺序缺陷,本条目无法判定。这是最需要向 GLSM 侧确认的一行。
  4. ⚠️ voxelizeRange 的返回值被丢弃(:73)—— 三个 GLSM 调用里唯一不检查返回值的,静默失败风险最高。
  5. glBindBufferBase 用 GL43.GL_SHADER_STORAGE_BUFFER,无 GL 版本检查(:52)—— 不支持时 GL_INVALID_OPERATION(静默),靠后续 bindVoxelizationRegion 返回 false 降级。
  6. vertexBuffer.handle() == 0 被当作无效(:40)—— GL 语义上 buffer 0 可用,是本实现的约定。
  7. region 只设置 pending* 不验证连续性(:41-44),多次 region 不跟 range 会静默丢弃前面的登记 —— 接口契约无断言。
  8. 两个告警标志是实例字段、3 个计数器是静态字段(:19-24)—— 混用;若 sink 每帧新建,告警每帧打。
  9. 两条告警消息都不含修复建议(:56、:68),对比 RenderPassIndex 含指引的风格不一致。
  10. 3 个静态计数器非 volatile 且由 public static takeXxx() 直接读写(:19-21、:78-82)—— 跨线程取统计会丢计数。
  11. 3 个计数器不保证自洽(takeRegions 不检查是否有对应 dispatch)—— 「登记了但全失败」时 regions > 0 而 encoders == 0。
  12. finish() 被漏调用会导致 pass 残留(:86-89)—— 下次 range 用旧 pass dispatch 新数据。无 AutoCloseable 保障。
  13. finish() 无 try/finally(:86-89)—— endVoxelizationBatch 抛异常则 pass 不清零。
  14. 类名 Sdl* 与 sdl() 方法暗示 SDL 专用,实际用通用 RenderBackend(:31-33)—— 命名与实现偏差。
  15. GL43 常量在 1.7.10 语境下的可用性依赖 LWJGL 2.9,且常量可用 ≠ 功能可用,无版本查询。

相关条目