14.3 一致性的判据

把三类判据摆在同一帧上:跨语言逐字节一致(sha256 相等,0 容忍)、跨路径像素掩码一致(光追命中掩码对光栅化覆盖掩码的对称差等于 4 个像素)、跨宿主声明容差(MAE ≤ 1.0 / PSNR ≥ 30 dB / 覆盖差 ≤ 20‰)。再给出反直觉的结论:判据一抓不到「两侧同错」。

14.3 一致性的判据

站内 /graphics-api-modern-comparison/ 那篇关心的是用哪个 API;这一节关心的是怎么证明你的实现是对的。前两节量出了两条路径的一致程度(对称差 4 个像素)与三类可归因的差异,但「量出来」不等于「判据成立」——判据要能自动跑、要能拒绝错的东西、还要说清楚它抓不到什么。这一节把三类判据摆在同一帧 320×240 上逐条验。

三类判据并排

判据比什么阈值能抓到抓不到
C1 跨语言逐字节一致同一路径两个语言实现的像素缓冲sha256 完全相等(0 容忍)实现分歧:求交顺序、归一化时机、BVH 遍历次序两侧同时犯的同一个错
C2 跨路径像素掩码一致光追命中掩码 vs 光栅化覆盖掩码对称差 = 4 像素相机基、投影约定、剔除口径的系统性错两侧都覆盖但颜色都错
C3 跨宿主声明容差WebGL2(float32)vs CPU 定点(Q6/Q16.16)MAE ≤ 1.0、PSNR ≥ 30 dB、覆盖差 ≤ 20‰跨宿主无法逐位一致时的可用性边界比容差还小的系统性偏差

三者的参照物完全不同:C1 的参照物是「另一个语言实现」,C2 的参照物是「另一条独立路径」,C3 的参照物是「一个事先声明的数」。参照物选错,判据就会变成自我循环。

判据一:跨语言逐字节一致

Python 与 TypeScript 两份实现各自逐像素渲染同一帧,落盘像素缓冲的 sha256 必须完全相等。实测:行口径两侧都是 213b413069ca66c9…(= 卷三基线),逐字节相同;灵敏度也够——只把第 0 个像素的红色通道异或 1,摘要立刻分叉。

判据二:跨路径像素掩码一致

光追命中掩码与光栅化覆盖掩码的对称差必须等于 4 个像素。这是两条路径唯一能直接相减的对象:它们不共用任何中间量,连「颜色」的定义都不一样(一个算辐射度,一个插值法线)。行口径下 43731 个命中像素、对称差 4(命中数的 0.0915‰),C2 接受。

判据三:跨宿主声明容差

浏览器 WebGL2 的 float32 与 CPU 的 Q6/Q16.16 逐位一致在数学上不可能,只能按事先声明的容差比对。实测 GPU 仿射对 CPU 定点仿射:MAE 0.5067 ≤ 1.0、PSNR 47.03 ≥ 30 dB、覆盖归属差 0.0781‰ ≤ 20‰,三条全过。下面的配置给每条判据都同时写明了 catches 与 blindTo——一个判据的价值一半在于它抓不到什么,不把盲区写下来的判据,用起来就是在赌。

{
  "name": "lantern-consistency-criteria",
  "criteria": [
    { "id": "C1", "kind": "cross-language-byte-exact",
      "object": "同一条路径的两个语言实现的像素缓冲", "threshold": "sha256 完全相等(0 容忍)",
      "catches": "两个实现之间的任何分歧:求交顺序、归一化时机、BVH 遍历次序",
      "blindTo": "两侧同时犯的同一个错(互为镜像)" },
    { "id": "C2", "kind": "cross-path-pixel-mask",
      "object": "光追命中掩码 vs 光栅化覆盖掩码", "thresholdPixels": 4,
      "catches": "相机基、投影约定、剔除口径一类的系统性错",
      "blindTo": "两侧都覆盖但颜色都错" },
    { "id": "C3", "kind": "cross-host-tolerance",
      "object": "浏览器 WebGL2(float32)vs CPU 定点(Q6/Q16.16)",
      "maeMax": 1.0, "psnrMinDb": 30.0, "coverageDiffPermilleMax": 20.0,
      "catches": "跨宿主无法逐位一致时的可用性边界",
      "blindTo": "比容差小的系统性偏差" }
  ],
  "rowBasisSha256": "213b413069ca66c9f0cc091bce5328eac0f540d075afa3bab37788aea335ba71",
  "expectedRowHitPixels": 43731,
  "expectedColumnHitPixels": 21519,
  "expectedRowMaskDiffPixels": 4,
  "expectedColumnMaskDiffPixels": 22212,
  "note": "列口径是已修正的真 bug:mat_look_at 返回行主序,取列会让 up.z 与 back.y 各反号。C1 抓不到它——两个语言实现同时犯同一个错时 sha256 照样相等。"
}

完整脚本

#!/usr/bin/env python3
# file: lantern/sim/cg_trace_14_3_criteria.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/cg_trace_14_3_criteria.py
"""14.3《一致性的判据》:把三类判据摆在同一帧上,并给出一个反直觉的结论。

  **C1 跨语言逐字节一致**——同一条路径的两个语言实现(Python / TypeScript)落盘的像素缓冲
  sha256 必须**完全相等**。0 容忍,任何一处求交顺序、归一化时机、BVH 遍历次序不同都会分叉。
  **C2 跨路径像素掩码一致**——光追命中掩码与光栅化覆盖掩码的对称差必须等于 **4** 个像素。
  掩码是两条路径唯一能直接相减的对象:它们不共用任何中间量,连「颜色」的定义都不一样。
  **C3 跨宿主声明容差**——浏览器 WebGL2(float32)与 CPU 定点(Q6 + Q16.16)逐位一致在数学
  上不可能,只能按声明容差比对:MAE ≤ 1.0、PSNR ≥ 30 dB、覆盖归属差异 ≤ 20‰。

反直觉的结论:**C1 抓不到「两侧同错」**。本脚本把相机基向量从行口径改成列口径(`mat_look_at`
返回行主序,取列会让 up.z 与 back.y 各反一个号),这是本卷已修正的一个真 bug。列口径下
Python 与 TS 两个实现**照样逐字节相等**(C1 通过——两个实现犯的是同一个错),而命中像素从
43731 掉到 21519、掩码对称差 22212(C2 当场拒绝)。产物见 `out/cg_trace_14_3_*.{json,rgb}`。
"""

import hashlib
import json
import os
import sys
import time

import numpy as np

HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
sys.path.insert(0, HERE)
import cglib  # noqa: E402
import cgtrace  # noqa: E402
import render_trace  # noqa: E402

CONFIG = os.path.join(ROOT, "config", "cg_trace_14_3_criteria.json")
OUT = os.path.join(ROOT, "out")


def render_basis(spec, width, height, max_depth, shade_factory, ctx, basis):
    """`cgtrace.render_trace` 的可换基向量版本:只把 right/up/back 的取法换掉。

    `basis="row"` 取视图矩阵的第 0/1/2 **行**(正确口径);`basis="col"` 取第 0/1/2 **列**
    (错误口径,本卷已修正的真 bug)。其余逐字与 `cgtrace.render_trace` 相同。
    """
    cam = spec["camera"]
    eye = tuple(cam["eyeQ16"])
    view = cglib.mat_look_at(eye, tuple(cam["targetQ16"]), tuple(cam["upQ16"]))
    if basis == "row":
        right, up, back = ((view[0], view[1], view[2]), (view[4], view[5], view[6]),
                           (view[8], view[9], view[10]))
    else:
        right, up, back = ((view[0], view[4], view[8]), (view[1], view[5], view[9]),
                           (view[2], view[6], view[10]))
    tan_half = cam["tanHalfFovYQ16"]
    tan_x = cglib.fmul(tan_half, cglib.fdiv(cglib.f_from_int(width), cglib.f_from_int(height)))

    ctx.setdefault("background", tuple(spec["backgroundQ8"]))
    ctx.setdefault("tmax", cam["farQ16"])
    ctx["spec"] = spec
    stats = {"visited": 0, "tested": 0, "rays": 0, "hits": 0}
    ctx["bvh_stats"] = stats
    ctx["shade"] = shade_factory(ctx)
    nodes, order = ctx["bvh"]

    color = np.empty((height, width, 3), dtype=np.uint8)
    half_w, half_h = width // 2, height // 2
    for py in range(height):
        sy = cglib.fdiv(cglib.f_from_int(2 * (half_h - py) - 1), cglib.f_from_int(height))
        ay = cglib.fmul(sy, tan_half)
        for px in range(width):
            sx = cglib.fdiv(cglib.f_from_int(2 * (px - half_w) + 1), cglib.f_from_int(width))
            ax = cglib.fmul(sx, tan_x)
            dirv = cglib.v_norm(tuple(cglib.fmul(ax, right[k]) + cglib.fmul(ay, up[k]) - back[k]
                                      for k in range(3)))
            stats["rays"] += 1
            color[py, px] = cgtrace.trace(nodes, order, ctx["tris"], eye, dirv, 0, ctx)
    return color, stats


def hit_mask(color, bg):
    """由像素缓冲推出命中掩码:与背景色不同即命中。"""
    return ~((color[:, :, 0] == bg[0]) & (color[:, :, 1] == bg[1]) & (color[:, :, 2] == bg[2]))


def sha_of(rgb):
    return hashlib.sha256(np.asarray(rgb, dtype=np.uint8).tobytes()).hexdigest()


def main():
    spec = cglib.load_spec()
    cfg = json.load(open(CONFIG, encoding="utf-8"))
    width, height = spec["width"], spec["height"]
    bg = tuple(spec["backgroundQ8"])
    os.makedirs(OUT, exist_ok=True)
    t0 = time.time()

    # 光栅化覆盖掩码(第 14.1 节落盘):C2 的外部参照
    raster_cov = np.frombuffer(open(os.path.join(OUT, "cg_trace_01_2_coverage.bin"), "rb").read(),
                               dtype=np.uint8).reshape(height, width) != 0

    row, _rs = render_basis(spec, width, height, 1, render_trace.make_shader,
                            {"stats": {"shadow_rays": 0, "shadow_hits": 0}}, "row")
    row_digest, row_mask = sha_of(row), hit_mask(row, bg)
    row_diff = int((row_mask ^ raster_cov).sum())
    col, _cs = render_basis(spec, width, height, 1, render_trace.make_shader,
                            {"stats": {"shadow_rays": 0, "shadow_hits": 0}}, "col")
    col_digest, col_mask = sha_of(col), hit_mask(col, bg)
    col_diff = int((col_mask ^ raster_cov).sum())
    for name, img in (("row", row), ("column", col)):
        with open(os.path.join(OUT, f"cg_trace_14_3_{name}.rgb"), "wb") as fh:
            fh.write(img.tobytes())

    # C1 的灵敏度:只改一个像素,摘要立刻变
    tweak = row.copy()
    tweak[0, 0] = (int(tweak[0, 0, 0]) ^ 1, int(tweak[0, 0, 1]), int(tweak[0, 0, 2]))
    tweak_digest = sha_of(tweak)

    # 跨语言对账:读 P 层落盘的 TS 像素缓冲(若已跑过)
    ts_files = {"row": os.path.join(OUT, "trace_hollow_ts.rgb"),
                "col": os.path.join(OUT, "cg_trace_14_3_column_ts.rgb")}
    cross_lang = {}
    for k, path in ts_files.items():
        if os.path.isfile(path):
            ts = np.frombuffer(open(path, "rb").read(), dtype=np.uint8)
            cross_lang[k] = {"tsSha256": hashlib.sha256(ts.tobytes()).hexdigest(), "present": True}
        else:
            cross_lang[k] = {"tsSha256": None, "present": False}
    cross_lang["row"]["pySha256"] = row_digest
    cross_lang["col"]["pySha256"] = col_digest
    for k in ("row", "col"):
        c = cross_lang[k]
        c["byteExact"] = bool(c["present"] and c["tsSha256"] == c["pySha256"])

    # C3:跨宿主声明容差(第 14.2 节的 W 层结果)
    w_path = os.path.join(OUT, "cg_trace_14_2_w.json")
    w = json.load(open(w_path, encoding="utf-8")) if os.path.isfile(w_path) else None
    c3 = None
    if w:
        a = w["class3_affine"]
        c3 = {"mae": a["mae"], "psnrDb": a["psnr_db"],
              "coverageDiffPermille": a["coverage_diff_permille"],
              "maeMax": w["thresholds"]["mae_max"], "psnrMinDb": w["thresholds"]["psnr_min_db"],
              "coverageDiffPermilleMax": w["thresholds"]["coverage_diff_permille_max"],
              "pass": bool(w["verdict"]["class3_mae_ok"] and w["verdict"]["class3_psnr_ok"]
                           and w["verdict"]["class3_coverage_ok"]),
              "backend": w["backend"]}

    n_row, n_col = int(row_mask.sum()), int(col_mask.sum())
    stats = {
        "width": width, "height": height, "rasterCoveredPixels": int(raster_cov.sum()),
        "C1_crossLanguage": {
            "object": "同一条路径的两个语言实现的像素缓冲",
            "rowBasisSha256": row_digest, "expectedBaselineSha256": cfg["rowBasisSha256"],
            "rowMatchesBaseline": row_digest == cfg["rowBasisSha256"],
            "columnBasisSha256": col_digest, "columnEqualsRow": col_digest == row_digest,
            "sensitivityOnePixel": {"tweakedSha256": tweak_digest,
                                    "detects": tweak_digest != row_digest},
            "crossLanguage": cross_lang, "verdictRow": row_digest == cfg["rowBasisSha256"],
            "verdictColumn": cross_lang["col"]["byteExact"] if cross_lang["col"]["present"] else None,
        },
        "C2_crossPathMask": {
            "object": "光追命中掩码 vs 光栅化覆盖掩码",
            "rowHitPixels": n_row, "columnHitPixels": n_col,
            "rowMaskDiffPixels": row_diff, "columnMaskDiffPixels": col_diff,
            "rowMaskDiffPermilleOfHit": round(row_diff * 1000.0 / n_row, 4),
            "columnMaskDiffPermilleOfHit": round(col_diff * 1000.0 / n_row, 4),
            "expectedRowMaskDiffPixels": cfg["expectedRowMaskDiffPixels"],
            "verdictRow": row_diff == cfg["expectedRowMaskDiffPixels"],
            "verdictColumn": col_diff != cfg["expectedRowMaskDiffPixels"],
            "rowHitMatchesExpected": n_row == cfg["expectedRowHitPixels"],
            "columnHitMatchesExpected": n_col == cfg["expectedColumnHitPixels"],
        },
        "C3_crossHostTolerance": c3, "ms": int((time.time() - t0) * 1000),
    }
    with open(os.path.join(OUT, "cg_trace_14_3_criteria.json"), "w", encoding="utf-8") as fh:
        json.dump(stats, fh, ensure_ascii=False, indent=2)
        fh.write("\n")

    print("== 14.3 一致性的判据 ==")
    print(f"光栅化覆盖掩码      : {int(raster_cov.sum())} 像素(外部参照)")
    print("C1 跨语言逐字节一致(0 容忍):")
    print(f"   行口径 Python     : {row_digest}")
    print(f"   卷三基线          : {cfg['rowBasisSha256']}"
          f"  相同 {stats['C1_crossLanguage']['rowMatchesBaseline']}")
    print(f"   TS 行口径         : {cross_lang['row']['tsSha256']}"
          f"({'已跑 P 层' if cross_lang['row']['present'] else '未跑 P 层'})"
          f"  逐字节相同 {cross_lang['row']['byteExact']}")
    print(f"   列口径 Python     : {col_digest}")
    print(f"   TS 列口径         : {cross_lang['col']['tsSha256']}"
          f"  逐字节相同 {cross_lang['col']['byteExact']}")
    print(f"   列口径 == 行口径  : {stats['C1_crossLanguage']['columnEqualsRow']}"
          "  ← 同一个 bug 换了写法")
    print("C2 跨路径像素掩码一致:")
    print(f"   行口径命中像素    : {n_row}  掩码对称差 {row_diff} 像素"
          f"(命中数的 {stats['C2_crossPathMask']['rowMaskDiffPermilleOfHit']}‰)"
          f"  C2 接受 {stats['C2_crossPathMask']['verdictRow']}")
    print(f"   列口径命中像素    : {n_col}  掩码对称差 {col_diff} 像素"
          f"(命中数的 {stats['C2_crossPathMask']['columnMaskDiffPermilleOfHit']}‰)"
          f"  C2 拒绝 {stats['C2_crossPathMask']['verdictColumn']}")
    if c3:
        print("C3 跨宿主声明容差:")
        print(f"   WebGL2 后端       : {c3['backend']}")
        print(f"   MAE {c3['mae']} ≤ {c3['maeMax']}   PSNR {c3['psnrDb']} ≥ {c3['psnrMinDb']} dB"
              f"   覆盖差 {c3['coverageDiffPermille']}‰ ≤ {c3['coverageDiffPermilleMax']}‰"
              f"   C3 接受 {c3['pass']}")
    print("结论                : C1 通过但 C2 拒绝 —— C1 抓不到两侧同错;"
          "C2 才是锚在外部参照上的判据")
    return 0


if __name__ == "__main__":
    sys.exit(main())

跑一次:

# snippet: 本机实跑(工作区 lantern/)
cd lantern
/opt/homebrew/bin/python3 sim/cg_trace_14_3_criteria.py
== 14.3 一致性的判据 ==
光栅化覆盖掩码      : 43731 像素(外部参照)
C1 跨语言逐字节一致(0 容忍):
   行口径 Python     : 213b413069ca66c9f0cc091bce5328eac0f540d075afa3bab37788aea335ba71
   卷三基线          : 213b413069ca66c9f0cc091bce5328eac0f540d075afa3bab37788aea335ba71  相同 True
   TS 行口径         : 213b413069ca66c9f0cc091bce5328eac0f540d075afa3bab37788aea335ba71(已跑 P 层)  逐字节相同 True
   列口径 Python     : 4d2416f903de02e56d3d1303a147dcf9440c24992d4e4c5e7534af994e3b7dbe
   TS 列口径         : 4d2416f903de02e56d3d1303a147dcf9440c24992d4e4c5e7534af994e3b7dbe  逐字节相同 True
   列口径 == 行口径  : False  ← 同一个 bug 换了写法
C2 跨路径像素掩码一致:
   行口径命中像素    : 43731  掩码对称差 4 像素(命中数的 0.0915‰)  C2 接受 True
   列口径命中像素    : 21519  掩码对称差 22212 像素(命中数的 507.9234‰)  C2 拒绝 True
C3 跨宿主声明容差:
   WebGL2 后端       : ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (LLVM 10.0.0) (0x0000C0DE)), SwiftShader driver)
   MAE 0.5067 ≤ 1   PSNR 47.03 ≥ 30 dB   覆盖差 0.0781‰ ≤ 20‰   C3 接受 True
结论                : C1 通过但 C2 拒绝 —— C1 抓不到两侧同错;C2 才是锚在外部参照上的判据

反直觉的结论:C1 抓不到「两侧同错」

把相机基向量从行口径改成列口径(mat_look_at 返回行主序,取列会让 up.z 与 back.y 各反一个号)——这是本卷修正过的一个真 bug。列口径下命中像素从 43731 掉到 21519(少了 49.2%),画面明显是错的。但 C1 照样通过:Python 与 TS 两份实现都犯同一个错,落盘缓冲逐字节相等,都是 4d2416f903de02e5…。C1 比的是「两个实现是否互为镜像」,不是「实现是否正确」——共享同一段被误解的口径时,sha256 相等反而是必然的。C2 才拒绝它:掩码对称差 22212 像素(命中数的 507.9234‰),比被接受的 4 大了三个数量级。所以判据必须锚在外部参照上——另一条不共享代码的路径、或者一个独立测得的事实;「两个实现互相验证」在算法层面几乎不提供信息。

跨语言核对

P 层用 BigInt 重算两种口径的整帧像素缓冲,落盘后由 S 侧读回比对,断言的不只是「行口径相等」,还有「列口径也相等、但 C2 必须拒绝它」:

// snippet: 节选自 lantern/core/test/cg_trace_14_3_criteria.test.ts
test("C1 抓不到「两侧同错」:列口径在 TS 与 Python 两侧也逐字节相同", () => {
  assert.equal(colDigest, golden.C1_crossLanguage.columnBasisSha256);
  const py = new Uint8Array(readFileSync(join(out, "cg_trace_14_3_column.rgb")));
  assert.equal(sha(py), colDigest, "两个语言实现犯同一个错时 C1 照样通过");
});
test("C2 抓得到:列口径命中像素 21519、掩码对称差 22212", () => {
  assert.equal(maskDiff(colMask), 22212);
  assert.ok(maskDiff(colMask) > 100 * maskDiff(rowMask), "C2 的拒绝幅度必须是数量级的");
});

这两条断言把「C1 通过」与「C2 拒绝」钉在同一个错误上:同一个 bug,一条判据放行,另一条拦住。

阅读导航:上一节:14.2 CPU 与 GPU 的差异 · 下一节:15.1 帧序列与动画输出 。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「cg」更多文章

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