16.3 交付与后续
站内的 图形学与 GPU 编程专题 是三卷的入口页;本节是它的收尾。交付物不是「十五个能跑的脚本」,而是一份写清哪里已经封死、哪里留给后续的账本——字段、量纲、不变量、失效模式,一样不能少。卷三封住的是整条离线光追链路:从主光线生成到像素缓冲的 sha256,中间每一级的出口都被另一个产物复核过。
交付范围:审计而不是重算
交付前最后一道工序是交叉审计:不重算渲染,只读已落盘的产物,核对它们之间是否自洽。交付物要能被下游接手,靠的不是「跑通了一次」,而是「每一句声明都能指到一个产物、每一个产物都能被另一处复核」。审计分三类:三卷基线 .rgb 的 sha256 是否等于声明值;计数恒等式是否成立;同一批计数在多份产物里是否同值。
#!/usr/bin/env python3
# file: lantern/sim/cg_trace_16_3_delivery.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/cg_trace_16_3_delivery.py
"""16.3《交付与后续》:把三卷的产物做一次交叉审计,并写成一份可交接的账本。
本脚本**不重算渲染**——它读已落盘的产物,核对它们之间是否自洽。交付物要能被下游接手,
靠的不是「跑通了一次」,而是「每一句声明都能指到一个产物、每一个产物都能被另一处复核」。
审计三类:
① 摘要:三卷基线 .rgb 的 sha256 是否等于声明值(三卷各自的封版摘要);
② 计数恒等式:主光线 == 宽×高、BVH 访问 == 主光线 + 阴影光线、BVH 测试 == 两者之和;
③ 跨节一致:同一批计数(命中 43731 / 阴影 77211 / 遮挡 1357)在多份产物里是否同值。
同时落一张六档可运行性账本(S/P/W/D/X/B),把「哪一档证明了什么、靠哪个产物」写死。
产物 out/cg_trace_16_3_delivery.json 与 out/cg_trace_16_3_ledger.csv。
"""
import hashlib
import json
import os
import sys
import time
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
sys.path.insert(0, HERE)
OUT = os.path.join(ROOT, "out")
DECLARED = {
"卷一 顶点光栅化": ("raster_hollow.rgb",
"bac659770178037ad3f6a6ad184945a96ff37a7fe4d754ef1ebc5b0aaa538ced"),
"卷二 逐像素光栅化": ("shade_hollow.rgb",
"0d997343ca547ed5cada5e4b1f75ce1782d79e524dba817278853de77307a6de"),
"卷三 光线追踪": ("trace_hollow.rgb",
"213b413069ca66c9f0cc091bce5328eac0f540d075afa3bab37788aea335ba71"),
}
def load(name):
with open(os.path.join(OUT, name), encoding="utf-8") as fh:
return json.load(fh)
def main():
t0 = time.time()
base = load("cg_trace_baseline.json")
prof = load("cg_trace_15_2_profile.json")
c13 = load("cg_trace_13_3_cross_language.json")
c14 = load("cg_trace_14_1_two_paths.json")
c143 = load("cg_trace_14_3_criteria.json")
shade = load("cg_shade_baseline.json")
warn = load("cg_trace_15_3_warnings.json")
# ① 摘要:三卷基线的像素缓冲
digests = []
for label, (fname, declared) in DECLARED.items():
path = os.path.join(OUT, fname)
got = hashlib.sha256(open(path, "rb").read()).hexdigest() if os.path.isfile(path) else None
digests.append({"volume": label, "artifact": f"out/{fname}", "sha256": got,
"declared": declared, "match": got == declared})
# 卷二的 per_pixel 与卷三基线也落在各自的摘要 JSON 里,一并核对
assert shade["gouraud"]["sha256"] == DECLARED["卷一 顶点光栅化"][1]
assert shade["per_pixel"]["sha256"] == DECLARED["卷二 逐像素光栅化"][1]
assert prof["sha256"] == DECLARED["卷三 光线追踪"][1]
counts = prof["counts"]
w, h = base["width"], base["height"]
identities = [
("主光线 == 宽×高", counts["primaryRays"], w * h),
("BVH 访问 == 主光线 + 阴影光线", counts["bvhVisitedTotal"],
counts["bvhVisitedPrimary"] + counts["bvhVisitedShadow"]),
("BVH 测试 == 主光线 + 阴影光线", counts["bvhTestedTotal"],
counts["bvhTestedPrimary"] + counts["bvhTestedShadow"]),
("命中掩码 vs 光栅化覆盖:对称差", c14["maskDiffPixels"], 4),
("BVH 节点", base["bvhNodes"], 511),
("三角形", base["triangles"], 896),
]
identities = [{"name": n, "value": v, "expected": e, "match": v == e}
for n, v, e in identities]
cross = []
for field, path in (("hitPixels", "cg_trace_baseline.json"), ("hitPixels", "cg_trace_13_3_cross_language.json"),
("hitPixels", "cg_trace_15_2_profile.json")):
doc = load(path)
val = doc[field] if field in doc else doc["counts"][field]
cross.append({"field": field, "source": path, "value": val})
for field, path in (("shadowRays", "cg_trace_13_3_cross_language.json"),
("shadowHits", "cg_trace_13_3_cross_language.json")):
cross.append({"field": field, "source": path, "value": load(path)[field]})
cross.append({"field": "rasterCoveredPixels", "source": "cg_trace_14_1_two_paths.json",
"value": c14["rasterCoveredPixels"]})
hit_vals = {c["value"] for c in cross if c["field"] == "hitPixels"}
cross_ok = (len(hit_vals) == 1 and c13["shadowRays"] == base["shadowRays"]
and c13["shadowHits"] == base["shadowHits"]
and c14["rasterCoveredPixels"] == base["hitPixels"])
ledger = [
{"tier": "S", "data": "config/scene_spec.json", "proves": "离线全链可跑:76800 主光线 → 像素缓冲 sha256",
"artifact": "out/trace_hollow.rgb", "reproducible": True},
{"tier": "P", "data": "同一份 spec", "proves": "BigInt 内核跨语言逐字节复现同一摘要",
"artifact": "out/trace_hollow_ts.rgb", "reproducible": True},
{"tier": "W", "data": "同一份 spec 内联进单文件 HTML", "proves": "像素缓冲在 WebGL2 纹理管线里往返后逐字节不变",
"artifact": "out/cg_trace_16_3_w.json", "reproducible": True},
{"tier": "D", "data": "场景规格 + 21 元组记录 + BVH 节点布局",
"proves": "字段表、量纲、不变量清单", "artifact": "附录 A 常数表", "reproducible": True},
{"tier": "X", "data": "真实 GPU", "proves": "真实驱动的浮点差异与性能剖析",
"artifact": "无(命令行不可复现)", "reproducible": False},
{"tier": "B", "data": "可扰动参数(分辨率 / 强度 / 半径)", "proves": "收敛与相变阈值",
"artifact": "out/cg_trace_16_3_perturbation.json", "reproducible": True},
]
proven = [
"同一份场景规格在三种宿主(Python 定点 / TypeScript BigInt / WebGL2)上可跑通并各自落盘",
"S↔P 像素缓冲逐字节相同(行口径),列口径同样逐字节相同但被 C2 拒绝",
"光追命中掩码与光栅化覆盖掩码对称差 4 像素(两条路径唯一能直接相减的对象)",
"BVH 计数可分解:访问 3238333 = 主光线 1347708 + 阴影光线 1890625",
]
not_proven = [
"真实 GPU 上的浮点差异与性能:本机 headless 的 WebGL2 后端是 ANGLE/SwiftShader 软件实现,X 档",
"生产级材质与真实场景规模:本卷只有 Lambert + 两个光源 + 896 三角形",
"硬件光追 API(DXR / Vulkan RT / Metal)路径:本卷全程 CPU 定点",
"跨架构(x86 / ARM)逐字节一致:只在 Apple M1 Pro 上实测过",
"多线程与并行下的确定性:本卷的 S 层是单线程 Python",
]
ok = (all(d["match"] for d in digests) and all(i["match"] for i in identities) and cross_ok)
out = {"canvas": [w, h], "digests": digests, "identities": identities, "crossChecks": cross,
"crossChecksAgree": cross_ok, "ledger": ledger, "proven": proven, "notProven": not_proven,
"bWarnings": warn["warnings"], "bExitCode": warn["exitCode"],
"criteria": c143["C1_crossLanguage"]["crossLanguage"], "allPass": ok,
"ms": int((time.time() - t0) * 1000)}
with open(os.path.join(OUT, "cg_trace_16_3_delivery.json"), "w", encoding="utf-8") as fh:
json.dump(out, fh, ensure_ascii=False, indent=2)
fh.write("\n")
with open(os.path.join(OUT, "cg_trace_16_3_ledger.csv"), "w", encoding="utf-8") as fh:
fh.write("tier,data,proves,artifact,reproducible\n")
for r in ledger:
fh.write(f"{r['tier']},{r['data']},{r['proves']},{r['artifact']},{r['reproducible']}\n")
print("== 16.3 交付账本审计 ==")
for d in digests:
print(f"{d['volume']:<12s} {d['artifact']:<24s} {'OK' if d['match'] else 'FAIL'} {d['sha256']}")
print("计数恒等式:")
for i in identities:
print(f" {'OK ' if i['match'] else 'FAIL'} {i['name']}: {i['value']} == {i['expected']}")
print(f"跨节一致 : {'OK' if cross_ok else 'FAIL'}(命中值集合 {sorted(hit_vals)})")
print("六档账本:")
for r in ledger:
print(f" {r['tier']} {r['proves']} <- {r['artifact']}")
print(f"已证明 {len(proven)} 条 / 未证明 {len(not_proven)} 条")
print(f"B 档软告警 {len(warn['warnings'])} 条,exit {warn['exitCode']}")
print(f"总判据全过 {ok} 耗时 {out['ms']} ms")
return 0
if __name__ == "__main__":
sys.exit(main())
实跑输出(脚本里那一行 B 档告警读的是第 15.3 节落盘的告警表,2 条;本节自己的扰动扫描另出 3 条,见下):
== 16.3 交付账本审计 ==
卷一 顶点光栅化 out/raster_hollow.rgb OK bac659770178037ad3f6a6ad184945a96ff37a7fe4d754ef1ebc5b0aaa538ced
卷二 逐像素光栅化 out/shade_hollow.rgb OK 0d997343ca547ed5cada5e4b1f75ce1782d79e524dba817278853de77307a6de
卷三 光线追踪 out/trace_hollow.rgb OK 213b413069ca66c9f0cc091bce5328eac0f540d075afa3bab37788aea335ba71
计数恒等式:
OK 主光线 == 宽×高: 76800 == 76800
OK BVH 访问 == 主光线 + 阴影光线: 3238333 == 3238333
OK BVH 测试 == 主光线 + 阴影光线: 1119804 == 1119804
OK 命中掩码 vs 光栅化覆盖:对称差: 4 == 4
OK BVH 节点: 511 == 511
OK 三角形: 896 == 896
跨节一致 : OK(命中值集合 [43731])
六档账本:
S 离线全链可跑:76800 主光线 → 像素缓冲 sha256 <- out/trace_hollow.rgb
P BigInt 内核跨语言逐字节复现同一摘要 <- out/trace_hollow_ts.rgb
W 像素缓冲在 WebGL2 纹理管线里往返后逐字节不变 <- out/cg_trace_16_3_w.json
D 字段表、量纲、不变量清单 <- 附录 A 常数表
X 真实驱动的浮点差异与性能剖析 <- 无(命令行不可复现)
B 收敛与相变阈值 <- out/cg_trace_16_3_perturbation.json
已证明 4 条 / 未证明 5 条
B 档软告警 2 条,exit 0
总判据全过 True 耗时 23 ms
六档可运行性账本
| 档 | 数据源 | 证明什么 | 产物 | 可复现 |
|---|---|---|---|---|
| S | config/scene_spec.json | 离线全链可跑:76800 主光线 → 像素缓冲 sha256 | out/trace_hollow.rgb | 是 |
| P | 同一份 spec | BigInt 内核跨语言逐字节复现同一摘要 | out/trace_hollow_ts.rgb | 是 |
| W | 同一份 spec 内联进单文件 HTML | 像素缓冲经 WebGL2 纹理管线往返后逐字节不变 | out/cg_trace_16_3_w.json | 是 |
| D | 场景规格 + 21 元组记录 + BVH 节点布局 | 字段表、量纲、不变量清单 | 附录 A 常数表 | 是 |
| X | 真实 GPU | 真实驱动的浮点差异与性能剖析 | 无(命令行不可复现) | 否 |
| B | 可扰动参数(分辨率 / 强度 / 半径) | 收敛与相变阈值 | out/cg_trace_16_3_perturbation.json | 是 |
X 档那一行必须留着并且留空:本机 headless 的 WebGL2 后端是 ANGLE/SwiftShader 软件实现,不是真实 GPU,所以本卷没有任何一条结论可以归因于真实硬件。
W 层:像素缓冲经纹理管线搬运后逐字节不变
第 14.2 节让 GPU 重算几何,逐位一致在数学上不可能,只能按声明容差比。本节要问的是交付层面的另一件事:已经算好的像素缓冲,经 GPU 纹理管线搬运一次之后会不会被改写。这里能用「逐字节相等」是因为全程没有舍入点——采样对齐纹素中心、NEAREST 过滤、RGBA8 帧缓冲。
// snippet: 16.3 的 W 层判据(摘自 lantern/web/verify_trace16.mjs)
const rows = [];
let pass = data.gl.error === 0;
for (const k of ["v1", "v2", "v3"]) {
const src = readFileSync(join(OUT, DECLARED[k].file));
const back = Buffer.from(data.results[k].out, "base64");
const row = {
volume: k,
sourceSha256: sha(src),
roundTripSha256: sha(back),
declaredSha256: DECLARED[k].sha,
sourceMatchesDeclared: sha(src) === DECLARED[k].sha,
byteExact: sha(back) === sha(src),
lengthMatch: back.length === src.length,
};
if (!row.sourceMatchesDeclared || !row.byteExact || !row.lengthMatch) pass = false;
rows.push(row);
}
const badSha = sha(Buffer.from(data.results.bad.out, "base64"));
// 判据必须配负例:同一比对若不拒绝「颜色减半」的着色器,PASS 就没有意义
const negativeRejected = rows.every((r) => badSha !== r.sourceSha256);
if (!negativeRejected) pass = false;
canvasDrawn : true
GL 后端 : ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (LLVM 10.0.0) (0x0000C0DE)), SwiftShader driver)
GL 版本 : WebGL 2.0 (OpenGL ES 3.0 Chromium) glError : 0
v1 out/raster_hollow.rgb 源 sha256 bac659770178037a
往返后 sha256 bac659770178037a 逐字节相同 true 长度一致 true 与声明基线相同 true
v2 out/shade_hollow.rgb 源 sha256 0d997343ca547ed5
往返后 sha256 0d997343ca547ed5 逐字节相同 true 长度一致 true 与声明基线相同 true
v3 out/trace_hollow.rgb 源 sha256 213b413069ca66c9
往返后 sha256 213b413069ca66c9 逐字节相同 true 长度一致 true 与声明基线相同 true
负例(颜色 ×0.5): 9fb18b692534fe49 与三者都不同 true
8 位值域往返 : 256/256 个值还原
判据 : {"threeByteExact":true,"sourceMatchesDeclared":true,"negativeRejected":true,"glClean":true,"rampFull":true}
负例把颜色乘 0.5,摘要立刻变成 9fb18b69…,说明这条比对不是摆设。另外在 GPU 上把 0..255 的灰度斜坡经 float32 归一化再写回 RGBA8,256 个值全部还原——「1/255 的步长在 float32 里够用」这句在 GPU 侧也成立。
B 层:三条扰动轴
A 分辨率(命中率与每像素耗时是否随分辨率漂移):
80x60 主光线 4800 命中 2734(569.5833‰) 93.306 us/px(漂移 +119.8646‰) 447.9 ms
160x120 主光线 19200 命中 10933(569.4271‰) 83.113 us/px(漂移 -2.4724‰) 1595.8 ms
240x180 主光线 43200 命中 24606(569.5833‰) 82.953 us/px(漂移 -4.3928‰) 3583.6 ms
320x240 主光线 76800 命中 43731(569.4141‰) 83.319 us/px(漂移 +0.0000‰) 6398.9 ms
每像素耗时中位数 83.319 us 命中率极差 0.1692‰
…
B 方向光强度(基线 intensityQ16 = 58982,80x60):
偏移 +1 变像素 10( 2.0833‰) MAE 0.0007 阴影光线 4836
偏移 +4096 变像素 2660(554.1667‰) MAE 2.0114 阴影光线 4836
首次可见差异(升)= 1 LSB,(降)= -1 LSB,MAE>=1 的最小升幅 = 4096
…
C 点光源作用半径(基线 rangeQ16 = 1048576,80x60):
×1 rangeQ16 1048576 阴影光线 4836 遮挡 77 414.0 ms
×2 rangeQ16 2097152 阴影光线 5405 遮挡 110 466.7 ms
饱和档 = ×2(基线 ×1 的阴影光线 4836)
…
软告警 3 条,exit 0
WARN 方向光强度偏移 +4096(Q16.16)MAE 2.0114 >= 1.0
WARN 方向光强度偏移 -4096(Q16.16)MAE 2.0165 >= 1.0
WARN 点光源半径 ×2 才饱和,声明半径 ×1 落在敏感带内
耗时 26065 ms
命中率在四个分辨率上的极差只有 0.1692‰,每像素耗时中位数 83.319 μs;最小的 80×60 档漂移 +119.9‰,那是每帧固定开销摊不薄所致,不是算法随分辨率变差。方向光强度偏移 1 个 Q16.16 单位就产生首次可见差异(10 个像素),而 MAE 到 1.0 需要 4096 单位;点光源作用半径在 ×2 处即判定饱和(阴影光线 4836 → 5405,再往上 5407 并保持不变)。三条轴一共 3 条软告警,进程 exit 0——扫描的目的是把失效的参数位置标出来,不是让脚本变红。
未证明什么
- 真实 GPU 上的浮点差异与性能:本机后端是 ANGLE/SwiftShader 软件实现,属 X 档,命令行不可复现。
- 生产级材质与真实场景规模:本卷只有 Lambert + 两个光源 + 896 个三角形,没有纹理、没有法线贴图、没有真实资产。
- 硬件光追 API(DXR / Vulkan RT / Metal)路径:本卷全程 CPU 定点,一次都没碰过这些 API。
- 跨架构(x86 / ARM)逐字节一致:只在 Apple M1 Pro 上实测过,换架构未测。
- 多线程与并行下的确定性:S 层是单线程 Python,并行下的求交顺序与累加顺序都没验证过。
后续的入口就写在这张清单里:要往真实硬件走,先补 X 档的剖析;要往生产场景走,先扩材质与资产;要往并行走,先证明 BVH 遍历在乱序下仍给出同一个 best_t。
阅读导航:上一节:16.2 三卷一致性总检 · 下一节:回到目录 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。