16.3 交付与后续

卷三的交付账本:三卷基线 .rgb 的 sha256 逐个复核,六条计数恒等式(主光线 76800、BVH 访问 3238333 = 主光线 + 阴影光线、掩码对称差 4)逐条对账,六档可运行性各写清证明了什么;另给出已证明 4 条与未证明 5 条(真实 GPU、生产级材质、硬件光追 API 等),以及三条扰动轴的软告警。

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

六档可运行性账本

档数据源证明什么产物可复现
Sconfig/scene_spec.json离线全链可跑:76800 主光线 → 像素缓冲 sha256out/trace_hollow.rgb是
P同一份 specBigInt 内核跨语言逐字节复现同一摘要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 三卷一致性总检 · 下一节:回到目录 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「cg」更多文章

  1. 16.3 交付卷三
  2. 16.2 三种消费的联调
  3. 16.1 着色管线的装配