13.3 跨语言逐位一致的验证
13.1 证明了「定点与浮点必然分歧」,13.2 把共享边归属钉成了整数判据。这一章把两条线索合起来,回答本卷最硬的那个问题:同一份场景规格,在 Python 与 TypeScript 下,能不能给出逐字节相同的像素缓冲? 站内的 《渲染引擎》
讲的是 TypeScript 引擎的架构与后端抽象;本节和它分工不同:那套书关心「引擎怎么搭」,本节关心同一算法在两个宿主上是不是同一个字节序列——判据就是一个 sha256。
验证方式不是「看起来一样」,而是摘要相等 + 扰动分叉两件事同时成立。摘要相等说明忠实复现;扰动分叉说明这个摘要真的对运算细节敏感——一个对任何改动都无所谓的摘要,证明不了任何东西。
一条可参数化的定点管线
要验证「敏感」,最干净的办法是把管线写成可参数化的:同一条公式,三个旋钮分别控制三处最容易出错的口径。
- 降精度:默认用算术右移
>>;P1 把它换成向零截断除法。两者在正数上一致,在负数上差 1。 - 定点除法:默认用向零截断(
fdiv);P2 把它换成 floor 除法。这正是 Python//与 JSBigInt/的分水岭。 - 子像素位数:默认 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 深度图与法线图 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。