13.3 跨语言逐位一致的验证

把定点管线写成可参数化形式:baseline 复现 S 层金标准 sha256,再把 `>>` 换成向零截断、把截断除法换成 floor 除法、把子像素位数 6 改成 5,三类扰动各给出一个不同的 sha256(覆盖 43730 / 43734 / 43731,分歧像素 153 / 173 / 646),并由 TypeScript 侧逐字节复现全部四个摘要。

13.3 跨语言逐位一致的验证

13.1 证明了「定点与浮点必然分歧」,13.2 把共享边归属钉成了整数判据。这一章把两条线索合起来,回答本卷最硬的那个问题:同一份场景规格,在 Python 与 TypeScript 下,能不能给出逐字节相同的像素缓冲? 站内的 《渲染引擎》 讲的是 TypeScript 引擎的架构与后端抽象;本节和它分工不同:那套书关心「引擎怎么搭」,本节关心同一算法在两个宿主上是不是同一个字节序列——判据就是一个 sha256。

验证方式不是「看起来一样」,而是摘要相等 + 扰动分叉两件事同时成立。摘要相等说明忠实复现;扰动分叉说明这个摘要真的对运算细节敏感——一个对任何改动都无所谓的摘要,证明不了任何东西。

一条可参数化的定点管线

要验证「敏感」,最干净的办法是把管线写成可参数化的:同一条公式,三个旋钮分别控制三处最容易出错的口径。

  • 降精度:默认用算术右移 >>;P1 把它换成向零截断除法。两者在正数上一致,在负数上差 1。
  • 定点除法:默认用向零截断(fdiv);P2 把它换成 floor 除法。这正是 Python // 与 JS BigInt / 的分水岭。
  • 子像素位数:默认 6(1 像素 = 64 单位);P3 改成 5(1 像素 = 32 单位),屏幕坐标整体被重新量化。

三个旋钮都在顶点变换层生效——矩阵链与中间量用 Python int(任意精度),只有屏幕坐标落到逐像素层才降到 numpy int64。这条分层与 13.1 完全一致:分歧的源头在顶点变换,所以旋钮也放在那里。

四个模式,四个摘要

baseline 用全部默认旋钮,必须复现 S 层金标准 bac659…;其余三个模式各扰动一个旋钮。实跑输出:

canvas           : 320x240  顶点 487
S 层金标准 sha256 : bac659770178037ad3f6a6ad184945a96ff37a7fe4d754ef1ebc5b0aaa538ced
baseline 复现金标准 : True
  baseline             sha256 bac659770178037ad3f6a6ad184945a96ff37a7fe4d754ef1ebc5b0aaa538ced  覆盖 43731  剔除 290
  P1_shift_to_trunc    sha256 b2a44adad98354c11ca830e4987b5953865f83d43293b0e77af19f6ba7aaea46  覆盖 43730  剔除 290  分歧像素 153
  P2_trunc_to_floor    sha256 6d5271b7b6b5e2a4d81e7a0dfcc52147571eba1906198a868ee9232881ca7d22  覆盖 43734  剔除 291  分歧像素 173
  P3_subpixel_5        sha256 85eb940d237d1b20f82f9a430cb6b7b1a65f07b4de553844bcd4b5fecc7067c1  覆盖 43731  剔除 290  分歧像素 646

baseline 复现金标准 : True 是第一层证据——参数化实现没有引入任何偏差。四个 sha256 两两不同是第二层证据——摘要对三处口径都敏感。

三类扰动各自分叉

三行的分歧像素数不一样,背后的机制也不同:

  • P1(>> → 向零截断):分歧 153 像素、覆盖 43730。右移与截断只在负数上差 1,而顶点变换里大量中间量(法线、相机空间 z)可能是负的,于是一部分顶点的屏幕坐标被挪动了 1 个 Q6 单位,边缘像素随之翻面。
  • P2(截断 → floor):分歧 173 像素、覆盖 43734、剔除数从 290 涨到 291。floor 与截断在负数上差 1,这条差异甚至改变了一个三角形的朝向符号(area 由正变负),于是它被背面剔除——覆盖数反而变多,是因为别的三角形补上了。
  • P3(子像素 6 → 5):分歧 646 像素、覆盖仍 43731。位数少 1 意味着量化步长翻倍,边缘被更粗糙地量化,分歧像素最多;但覆盖总数恰好不变,因为像素中心的位置只是被重新取整。

这三行合起来说明:同一个 sha256 判定对运算顺序、舍入方向、子像素精度都敏感。这正是它能当「逐字节一致」判据的原因。

S 层驱动:四个模式一次跑完

下面的驱动脚本调用 sim/cg_raster_13_pipeline.py 的可参数化管线,跑四个模式、与 cglib 的金标准对照,并把结果写进 out/cg_raster_13_3_cross_language.json。

#!/usr/bin/env python3
# file: lantern/sim/cg_raster_13_3_cross_language.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/cg_raster_13_3_cross_language.py
"""13.3 跨语言逐位一致的验证(驱动)。

参数化定点管线在 cg_raster_13_pipeline.py。四个模式各产出一个 sha256:
  baseline          Q16.16 算术右移 + 向零截断除法 + 6 位子像素 —— 必须复现 S 层金标准
  P1 shift->trunc   把 `>>` 换成向零截断除法(负数上差 1)
  P2 trunc->floor   把向零截断的定点除法换成 floor 除法
  P3 subpixel 5     把子像素位数从 6 改成 5(1 像素 = 32 单位)

baseline 复现金标准 = 参数化实现忠实;三类扰动各给一个**不同**的 sha256 = 摘要可分叉。
TypeScript 侧(core/src/lantern/cg_raster_13_pipeline.ts)独立实现同一套公式,
`node --test` 断言四个摘要与 Python 侧逐字节相同。

产物:out/cg_raster_13_3_cross_language.json、out/cg_raster_13_3_baseline.rgb。
"""

import hashlib
import json
import os
import sys

import numpy as np

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import cglib  # noqa: E402
import render_raster  # noqa: E402
import cg_raster_13_pipeline as pl  # noqa: E402

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

MODES = {
    "baseline": {},
    "P1_shift_to_trunc": {"sar": False},
    "P2_trunc_to_floor": {"floor_div": True},
    "P3_subpixel_5": {"sub": 5},
}


def digest(color):
    return hashlib.sha256(np.clip(color, 0, 255).astype(np.uint8).tobytes()).hexdigest()


def golden_render(spec, verts, tris, vcolors):
    """用 cglib 的既有实现独立渲染一次,作为 S 层金标准。"""
    width, height = spec["width"], spec["height"]
    cam = spec["camera"]
    aspect = cglib.fdiv(cglib.f_from_int(width), cglib.f_from_int(height))
    view = cglib.mat_look_at(tuple(cam["eyeQ16"]), tuple(cam["targetQ16"]), tuple(cam["upQ16"]))
    proj = cglib.mat_perspective(cam["tanHalfFovYQ16"], aspect, cam["nearQ16"], cam["farQ16"])
    mvp = cglib.mat_mul(proj, view)
    screen = [cglib.project(mvp, v) for v in verts]
    rtris = []
    for a, b, c in tris:
        sa, sb, sc = screen[a], screen[b], screen[c]
        if sa is None or sb is None or sc is None:
            continue
        pa = cglib.to_screen(sa[0], sa[1], width, height)
        pb = cglib.to_screen(sb[0], sb[1], width, height)
        pc = cglib.to_screen(sc[0], sc[1], width, height)
        rtris.append((pa[0], pa[1], sa[2], vcolors[a][0], vcolors[a][1], vcolors[a][2],
                      pb[0], pb[1], sb[2], vcolors[b][0], vcolors[b][1], vcolors[b][2],
                      pc[0], pc[1], sc[2], vcolors[c][0], vcolors[c][1], vcolors[c][2]))
    return cglib.rasterize(rtris, width, height, spec["backgroundQ8"])


def main():
    spec = cglib.load_spec()
    verts, tris, vcolors = render_raster.build_scene(spec)

    results = {}
    colors = {}
    for name, mode in MODES.items():
        color, covered, culled = pl.make_pipeline(mode)(spec, verts, tris, vcolors)
        colors[name] = color
        results[name] = {"sha256": digest(color), "covered": int(covered.sum()), "culled": culled}

    golden_color, _gcov, _gcovered, _gculled = golden_render(spec, verts, tris, vcolors)
    golden = digest(golden_color)

    diffs = {
        name: int((colors["baseline"] != colors[name]).any(axis=2).sum())
        for name in MODES if name != "baseline"
    }

    out = os.path.join(ROOT, "out")
    with open(os.path.join(out, "cg_raster_13_3_baseline.rgb"), "wb") as fh:
        fh.write(np.clip(colors["baseline"], 0, 255).astype(np.uint8).tobytes())

    record = {
        "canvas": [spec["width"], spec["height"]],
        "vertices": len(verts),
        "golden_sha256": golden,
        "baseline_matches_golden": results["baseline"]["sha256"] == golden,
        "modes": {k: {**v, "divergent_vs_baseline": diffs.get(k, 0)} for k, v in results.items()},
    }
    with open(os.path.join(out, "cg_raster_13_3_cross_language.json"), "w", encoding="utf-8") as fh:
        json.dump(record, fh, ensure_ascii=False, indent=2)

    print(f"canvas           : {spec['width']}x{spec['height']}  顶点 {len(verts)}")
    print(f"S 层金标准 sha256 : {golden}")
    print(f"baseline 复现金标准 : {record['baseline_matches_golden']}")
    for name in MODES:
        r = record["modes"][name]
        tag = "" if name == "baseline" else f"  分歧像素 {r['divergent_vs_baseline']}"
        print(f"  {name:20s} sha256 {r['sha256']}  覆盖 {r['covered']}  剔除 {r['culled']}{tag}")
    print(f"rgb              : {os.path.getsize(os.path.join(out, 'cg_raster_13_3_baseline.rgb'))} bytes")
    return 0


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

三个旋钮长什么样

旋钮的实现很短,但每一行都对应一条数值纪律:

# snippet: 三个旋钮如何改变同一条定点管线(摘自 lantern/sim/cg_raster_13_pipeline.py)
def shr(a, bits):                       # 降精度:默认算术右移;P1 换成向零截断
    if sar:
        return a >> bits
    q = abs(a) >> bits
    return -q if a < 0 else q

def fdiv(a, b):                         # 定点除法:默认向零截断;P2 换成 floor
    if floor_div:
        return (a << 16) // b
    q = (abs(a) << 16) // abs(b)
    return -q if (a < 0) != (b < 0) else q

SUB1 = 1 << mode.get("sub", 6)          # 子像素位数:默认 6(1 像素=64);P3 改成 5

shr 默认走 a >> bits(算术右移,负数保持 floor 语义);fdiv 默认用 abs 加符号位拼出向零截断。这两处正是 13.1 里反复强调的那条铁律——降精度用 >>、除法显式截断——的可参数化形态。

跨语言断言:四个摘要逐字节相同

S 层产出四个摘要后,TypeScript 侧用独立实现(core/src/lantern/cg_raster_13_pipeline.ts,顶点链用 BigInt、逐像素层用 Number)重算,逐个比对。

// file: lantern/core/test/cg_raster_13_3_cross_language.test.ts
// tier: P
// verify: node --test 'core/test/cg_raster_13_3_cross_language.test.ts'
// 13.3 的跨语言断言:TypeScript 侧的可参数化管线必须与 Python 侧
// sim/cg_raster_13_3_cross_language.py 给出**逐字节相同**的 sha256,
// 且三类扰动各自分叉。金标准与四个摘要由 S 侧独立产出。

import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { test } from "node:test";
import { fileURLToPath } from "node:url";

import { type SceneSpec, renderScene } from "../src/lantern/pipeline.ts";
import { type Mode, renderParam } from "../src/lantern/cg_raster_13_pipeline.ts";

const root = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
const spec = JSON.parse(readFileSync(join(root, "config", "scene_spec.json"), "utf8")) as SceneSpec;
const g = JSON.parse(
  readFileSync(join(root, "out", "cg_raster_13_3_cross_language.json"), "utf8"),
) as {
  golden_sha256: string;
  baseline_matches_golden: boolean;
  modes: Record<string, { sha256: string; covered: number; culled: number; divergent_vs_baseline: number }>;
};

const sha = (rgb: Uint8Array): string => createHash("sha256").update(Buffer.from(rgb)).digest("hex");

const MODES: Record<string, Mode> = {
  baseline: {},
  P1_shift_to_trunc: { sar: false },
  P2_trunc_to_floor: { floorDiv: true },
  P3_subpixel_5: { sub: 5 },
};

test("金标准:主管线(pipeline.ts)复现 S 层 sha256", () => {
  const frame = renderScene(spec);
  assert.equal(sha(frame.result.rgb), g.golden_sha256);
  assert.equal(g.golden_sha256, "bac659770178037ad3f6a6ad184945a96ff37a7fe4d754ef1ebc5b0aaa538ced");
  assert.ok(g.baseline_matches_golden, "S 侧参数化 baseline 必须复现金标准");
});

test("baseline:可参数化管线在默认旋钮下复现金标准", () => {
  const r = renderParam(spec, MODES.baseline);
  assert.equal(sha(r.rgb), g.golden_sha256);
  assert.equal(r.covered, g.modes.baseline.covered);
  assert.equal(r.culled, g.modes.baseline.culled);
});

test("三类扰动:TypeScript 摘要与 Python 侧逐字节相同", () => {
  for (const name of ["P1_shift_to_trunc", "P2_trunc_to_floor", "P3_subpixel_5"]) {
    const r = renderParam(spec, MODES[name]);
    assert.equal(sha(r.rgb), g.modes[name].sha256, `${name} 摘要与 Python 侧不符`);
    assert.equal(r.covered, g.modes[name].covered, `${name} 覆盖数不符`);
    assert.equal(r.culled, g.modes[name].culled, `${name} 剔除数不符`);
  }
});

test("负例:每类扰动都必须让摘要分叉,且四者两两不同", () => {
  const digests = Object.keys(MODES).map((name) => sha(renderParam(spec, MODES[name]).rgb));
  for (const name of ["P1_shift_to_trunc", "P2_trunc_to_floor", "P3_subpixel_5"]) {
    assert.notEqual(g.modes[name].sha256, g.golden_sha256, `${name} 未分叉`);
  }
  assert.equal(new Set(digests).size, digests.length, "四个摘要必须两两不同");
});

node --test 'core/test/cg_raster_13_3_cross_language.test.ts' 的实测输出:

✔ 金标准:主管线(pipeline.ts)复现 S 层 sha256
✔ baseline:可参数化管线在默认旋钮下复现金标准
✔ 三类扰动:TypeScript 摘要与 Python 侧逐字节相同
✔ 负例:每类扰动都必须让摘要分叉,且四者两两不同
ℹ tests 4  ℹ pass 4  ℹ fail 0

4 条全绿。注意最后一条负例:它断言四类摘要两两不同——如果某个扰动其实是恒等变换,这条会立刻失败。这正是「逐字节相同」这条纪律的完整形态:Python 与 TypeScript 之间是逐字节相同(可硬断言),而判据本身经过扰动分叉的反向检验。至此,本卷的可复现性主张从「同一算法」升级成了「同一字节序列」——下一章转进中间缓冲,看深度图与法线图如何把这条一致性延伸到像素之外。

阅读导航:上一节:13.2 边函数规则的整数化 · 下一节:14.1 深度图与法线图 。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「cg」更多文章

  1. 14.3 中间缓冲的可视化
  2. 14.2 覆盖率与线框
  3. 14.1 深度图与法线图