HLOD 细化屏障:子分块齐备前必须保留父级代理
开放世界相机进入城市分区时,流送器会把一个低细节父级代理替换为多个高细节子分块。若任一子分块完成就立即隐藏父级,尚未到达的区域会暴露空洞;若失败后仍提交已就绪部分,画面又会同时缺块与重叠。证据标识 2026-08-04-1930 的最小 C# 探针把一次细化定义为带代际的完整子集事务。
// .tmp/daily-labs/2026-08-04-1930/HlodRefinementGate.cs
// RefinementTicket / HlodRefinementGate fields
public readonly record struct RefinementTicket(
int Generation,
string Parent,
string[] Children
);
public sealed class HlodRefinementGate
{
private int generation;
private RefinementTicket? pending;
private readonly HashSet<string> ready = [];
public string VisibleSet { get; private set; } = "CityBlock_L2";
}
所有权边界
HlodRefinementGate 唯一拥有当前可见集合、待提交票据与就绪子集。流送工作线程只报告资源准备完成,渲染世界只消费已提交集合;下载、解压、GPU 上传和遮挡裁剪都在边界外。两个不变量是:父级代理在全部子分块可用前持续可见;一次提交只能发布父级或完整子集,不能发布混合集合。这里的“就绪”指 Mesh、材质与绑定资源已经能够进入渲染世界,而不是仅完成磁盘读取。

票据代际
每次 Begin 清空旧就绪集并增加代际,新票据因此取得唯一提交权。异步任务可能在相机离开后才返回,MarkReady 必须先比较代际,再验证子分块属于当前票据。仅比较分块名称并不充分:相机往返同一区域会产生同名但不同生命周期的请求,旧完成项仍可能污染新事务。
// .tmp/daily-labs/2026-08-04-1930/HlodRefinementGate.cs
// Begin / MarkReady
public RefinementTicket Begin(string parent, params string[] children)
{
ready.Clear();
pending = new RefinementTicket(
++generation,
parent,
children
);
return pending.Value;
}
public bool MarkReady(RefinementTicket ticket, string child)
{
if (pending is not { } current ||
current.Generation != ticket.Generation)
return false;
if (!current.Children.Contains(child)) return false;
ready.Add(child);
return true;
}
原子提交
正常时序中,NW_L1、NE_L1 和 S_L1 可以任意顺序完成,但前两次只改变门内就绪集。TryCommit 检查全部成员后才替换 VisibleSet,随后清除票据。父级隐藏与子级显示在同一提交点发生,渲染侧无需推断准备进度,也不会观察到半完成状态。
// .tmp/daily-labs/2026-08-04-1930/HlodRefinementGate.cs
// TryCommit / Fail
public bool TryCommit(RefinementTicket ticket)
{
if (pending is not { } current ||
current.Generation != ticket.Generation)
return false;
if (current.Children.Any(child => !ready.Contains(child)))
return false;
VisibleSet = string.Join("+", current.Children);
pending = null;
ready.Clear();
return true;
}
public bool Fail(RefinementTicket ticket)
{
if (pending is not { } current ||
current.Generation != ticket.Generation)
return false;
pending = null;
ready.Clear();
return true;
}

失败回归
失败时序固定让一个子分块完成后终止事务。正确结果不是展示已到达的 NW_L1,而是清除准备状态并继续显示 CityBlock_L2。另一个回归先创建旧票据,再以新票据覆盖它,验证旧完成不能进入当前就绪集。这两个分支共同约束“失败无可见副作用”和“陈旧完成无状态副作用”。
// .tmp/daily-labs/2026-08-04-1930/HlodRefinementGate.cs
// failed child preserves parent / stale completion cannot publish
Test("failed child preserves parent", () =>
{
var gate = new HlodRefinementGate();
var ticket = gate.Begin(
"CityBlock_L2", "NW_L1", "NE_L1", "S_L1"
);
True(gate.MarkReady(ticket, "NW_L1"));
True(gate.Fail(ticket));
Equal("CityBlock_L2", gate.VisibleSet);
Equal(false, gate.TryCommit(ticket));
});
Test("stale completion cannot publish", () =>
{
var gate = new HlodRefinementGate();
var old = gate.Begin("CityBlock_L2", "NW_L1", "NE_L1", "S_L1");
var current = gate.Begin("CityBlock_L2", "NW_L1", "NE_L1", "S_L1");
Equal(false, gate.MarkReady(old, "NW_L1"));
foreach (var child in current.Children)
True(gate.MarkReady(current, child));
True(gate.TryCommit(current));
});
运行证据
探针使用 .NET 9 标准库且没有外部包。新项目首次以 --no-restore 构建时因缺少资产清单退出,执行标准还原后复跑成功:Release 构建 0 警告、0 错误;三个用例通过,失败 0、跳过 0,测试逻辑耗时 117.918 毫秒,完整运行进程耗时 2.22 秒,退出码 0。时间只证明本次证据执行完成,不支持帧耗或吞吐结论。
$ cd "$WORK_DIR"
$ dotnet restore \
.tmp/daily-labs/2026-08-04-1930/HlodRefinementProbe.csproj
Restored HlodRefinementProbe.csproj (in 163 ms).
$ dotnet build \
.tmp/daily-labs/2026-08-04-1930/HlodRefinementProbe.csproj \
-c Release --no-restore
Build succeeded.
0 Warning(s)
0 Error(s)
$ /usr/bin/time -p dotnet run \
--project .tmp/daily-labs/2026-08-04-1930/HlodRefinementProbe.csproj \
-c Release --no-build --no-restore
PASS all children commit atomically
PASS failed child preserves parent
PASS stale completion cannot publish
tests=3 passed=3 failed=0 skipped=0 duration_ms=117.918
real 2.22
user 1.05
sys 0.49
exit_code=0
{
"evidenceId": "2026-08-04-1930",
"partialCommit": false,
"before": {
"generation": 1,
"VisibleSet": "CityBlock_L2",
"ready": ["NE_L1", "NW_L1"]
},
"finalCommit": true,
"after": {
"generation": 1,
"VisibleSet": "NW_L1+NE_L1+S_L1",
"pending": null,
"ready": []
}
}
容量边界
当前查验用 HashSet 聚合就绪项,单次报告均摊为 O(1),提交扫描 C 个子分块,空间为 O(C)。它适用于单线程提交入口和一个父级对应有限子集;不处理跨层级同时细化,也不承担显存驱逐。逐块交叉淡化看似能隐藏跳变,却仍要求父子两套资源同时驻留,并不能修复缺块语义。当前取舍优先保证可见集合完整,允许齐备时出现一次 LOD 切换。
当前合同:单父级、单待提交票据、单线程提交入口
失败恢复:清空票据与就绪集,继续显示父级代理
失效信号:等待时间超预算、失败率升高、父子双驻留超显存预算
演进触发:跨层级并行细化或 C 持续超过 64
下一结构:region key + epoch + dependency bitset + residency budget
当子分块数持续超过 64、多个层级并发细化,或双驻留显存超过预算时,应把集合替换为依赖位图,并由驻留预算器决定可启动事务;但提交仍必须保持完整子集原子性。HLOD 细化的关键不是让高细节尽早出现,而是让父级代理负责遮蔽准备过程,直到唯一所有者能够证明替代集合完整、当前且可渲染。