2.1 向量运算的定点化
站内的 图形数学基础 用浮点向量与矩阵库搭起渲染所需的线性代数;本节关心的是另一件事——同一串向量运算在两台机器、两种语言上是否给出逐位相同的结果。为此我们把向量运算全部搬进 Q16.16 定点整数,先钉死加、减、点积、叉积的舍入语义,再用 sha256 证明 Python 与 TypeScript 两侧逐位一致。
Q16.16 的表示与范围
Q16.16 用一个整数表示实数:低 16 位是小数,ONE = 1 << 16 = 65536 代表 1.0。把它塞进 32 位有符号整数,可表示范围是 [-32768.00000, 32767.99998]。Python 的 int 与 JS 的 BigInt 都是任意精度,故顶点层不受这个范围约束——但约定仍是 Q16.16,因为逐像素层的量级已被证明 ≤ 2^49.6,可以在 float64 的整数精确区里用 Number 跑,无需 BigInt。
加减为什么精确
加减是「同分母」的整数运算:两个 Q16.16 相加就是把两个整数相加,分母不变,没有舍入。实测 1.5 + 2.25 = 98304 + 147456 = 245760,还原成实数正好是 3.75;2.25 - 1.5 = 49152,正好是 0.75。唯一的风险是溢出——32 位下越过 ±32768 就翻车,这正是顶点层坚持任意精度整数的理由。
乘积必须降精度,且只能右移
两个 Q16.16 相乘,小数位变成 32 位,必须右移 16 位砍回去:fmul(a, b) = (a * b) >> 16。这一步只能右移,不能除。理由是一条实测结论:
Python -1 >> 16 : -1 (算术右移 = floor)
Python -1 // 65536 : -1 (整除 = floor)
JS BigInt -1n / 65536n : 0 (向零截断)
JS BigInt -1n >> 16n : -1 (算术右移 = floor)
Python 的 // 是 floor,JS BigInt 的 / 是向零截断,两者在负数上分叉;而 >> 在两侧都是 floor。定点内核里只要有一处用 / 或 // 降精度,跨语言的 sha256 就会立刻不一致。点积与叉积都由乘积组成,故每一项都走 fmul;叉积是「乘积之差」,中间结果同样只做 >>。
点积的误差上界
每次 fmul 引入的误差是 真值 - floor(真值) ∈ [0, 1) ulp——floor 只会向下,故误差恒非负。点积是三项之和,于是误差落在 [0, 3) ulp,这是一个单边界:定点点积永远不大于精确值。用 64 个确定性向量实测,误差落在 [0.365311, 2.615829],确实在界内。
两份实现,一个 sha256
下面是 S 层脚本,复用 sim/cglib.py 的 fmul / v_dot / v_cross,不重写内核:
#!/usr/bin/env python3
# file: lantern/sim/cg_raster_02_1_fixed_point.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/cg_raster_02_1_fixed_point.py
"""2.1 向量运算的定点化:Q16.16 的表示、精确的加减、必须降精度的点积与叉积。
实测结论:
1. 加/减是同分母整数运算,无精度损失(只可能溢出)。
2. 乘积有 32 位小数,必须 `>> 16` 降精度;且只能用算术右移 `>>`——
Python 的 `//` 是 floor,JS BigInt 的 `/` 向零截断,负数上不一致。
3. 点积误差恒在 [0, 3) ulp:每项 floor 误差在 [0, 1),三项之和非负且 < 3。
产物:out/cg_raster_02_1_stats.json
"""
import json
import os
import sys
from fractions import Fraction
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import cglib # noqa: E402
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ONE = cglib.ONE
MASK = 0x7FFFFFFF
def lcg(state):
return (1103515245 * state + 12345) & MASK
def gen_vec(state):
"""从 LCG 生成一个三分量 Q16.16 向量,分量落在 [-4.0, 4.0)。"""
comps = []
for _ in range(3):
state = lcg(state)
comps.append(((state >> 8) % (8 * ONE)) - 4 * ONE)
return state, tuple(comps)
def exact_dot(a, b):
"""点积的精确有理值(单位:ulp,即 1.0 = ONE)。"""
return Fraction(a[0] * b[0] + a[1] * b[1] + a[2] * b[2], ONE)
def main():
print(f"ONE : {ONE}")
print(f"int32 Q16.16 范围 : [{(1 << 31) / -ONE:.5f}, {((1 << 31) - 1) / ONE:.5f}]")
x, y = 3 * ONE // 2, 9 * ONE // 4 # 1.5 与 2.25
print(f"1.5 + 2.25 : {x + y} ({(x + y) / ONE:.5f})")
print(f"2.25 - 1.5 : {y - x} ({(y - x) / ONE:.5f})")
assert x + y == 15 * ONE // 4 and y - x == 3 * ONE // 4
print(f"Python -1 >> 16 : {-1 >> 16}")
print(f"Python -1 // 65536 : {-1 // 65536}")
print("JS BigInt -1n / 65536n : 0 (向零截断)")
print("JS BigInt -1n >> 16n : -1 (算术右移)")
assert (-1 >> 16) == -1 and (-1 // 65536) == -1
p = cglib.fmul(3 * ONE // 2, 5 * ONE // 2)
print(f"fmul(1.5, 2.5) : {p} ({p / ONE:.5f})")
assert p == 15 * ONE // 4
print(f"0.1 的 Q16.16 表示 : 6553 (真值 {0.1 * ONE:.1f})")
N = 64
state = 20261015
vecs = []
for _ in range(N):
state, v = gen_vec(state)
vecs.append(v)
max_err, min_err = Fraction(0), None
for i in range(N):
a, b = vecs[i], vecs[(i + 1) % N]
e = exact_dot(a, b) - cglib.v_dot(a, b)
if e > max_err:
max_err = e
if min_err is None or e < min_err:
min_err = e
assert 0 <= e < 3, f"点积误差 {e} 越界 [0,3)"
print(f"点积样本数 : {N}")
print(f"误差范围 (ulp) : [{float(min_err):.6f}, {float(max_err):.6f}] (< 3)")
a, b = vecs[0], vecs[1]
print(f"样本 0 a={a} b={b}")
print(f" v_dot={cglib.v_dot(a, b)} exact={float(exact_dot(a, b)):.6f}")
print(f"v_cross(a, b) : {cglib.v_cross(a, b)}")
out = os.path.join(ROOT, "out")
os.makedirs(out, exist_ok=True)
stats = {
"one": ONE,
"samples": N,
"dot_err_min_ulp": float(min_err),
"dot_err_max_ulp": float(max_err),
"dot_err_bound_ulp": 3,
"sample0_dot": cglib.v_dot(a, b),
"sample0_exact": float(exact_dot(a, b)),
}
with open(os.path.join(out, "cg_raster_02_1_stats.json"), "w", encoding="utf-8") as fh:
json.dump(stats, fh, ensure_ascii=False, indent=2)
print("out : cg_raster_02_1_stats.json")
return 0
if __name__ == "__main__":
sys.exit(main())
P 层是同一套确定性 LCG 的 TypeScript 版本:它生成同样的 64 个向量,断言点积与叉积序列的 sha256 与 Python 侧相同,并用一个「改用截断除法」的负例证明判据有区分力。
// file: lantern/core/test/cg_raster_02_1_fixed_point.test.ts
// tier: P
// verify: node --test 'core/test/cg_raster_02_1_fixed_point.test.ts'
// 2.1 的 P 层:与 sim/cg_raster_02_1_fixed_point.py 用同一套确定性 LCG 生成同样的向量,
// 断言两侧的点积与叉积逐位相同(sha256 比对),并锁定 `>>` 的舍入语义。
//
// 金标准由 S 侧独立产出:
// /opt/homebrew/bin/python3 sim/cg_raster_02_1_fixed_point.py
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { test } from "node:test";
import { ONE, fmul } from "../src/lantern/fixed.ts";
import { type Vec3, vCross, vDot } from "../src/lantern/vec.ts";
const MASK = 0x7fffffffn;
function lcg(state: bigint): bigint {
return (1103515245n * state + 12345n) & MASK;
}
function genVec(state: bigint): [bigint, Vec3] {
const comps: bigint[] = [];
let s = state;
for (let i = 0; i < 3; i += 1) {
s = lcg(s);
comps.push(((s >> 8n) % (8n * ONE)) - 4n * ONE);
}
return [s, [comps[0], comps[1], comps[2]]];
}
// 由 S 侧 sim/cg_raster_02_1_fixed_point.py 产出
const GOLDEN_DOT_SHA = "2774812048ca6c619b971d17f7b27b3c7a4e3c57c1954109ebabe77a881dfe37";
const GOLDEN_CROSS_SHA = "280d567b9ca694157c11a05fdeefae435cddfa3f71b802c85df7ddfaac71dc5f";
const GOLDEN_DOT0 = -585784n;
const GOLDEN_DOT_LAST = 161286n;
const N = 64;
const vecs: Vec3[] = [];
{
let state = 20261015n;
for (let i = 0; i < N; i += 1) {
const [s, v] = genVec(state);
state = s;
vecs.push(v);
}
}
test("舍入语义:算术右移 >> 与 BigInt / 在负数上分叉", () => {
// BigInt 的 / 向零截断 -> 0;>> 是 floor -> -1。Python 的 // 也是 floor(= -1)。
assert.equal(-1n / 65536n, 0n, "BigInt / 向零截断");
assert.equal(-1n >> 16n, -1n, "BigInt >> 是 floor,与 Python // 同义");
assert.notEqual(-1n / 65536n, -1n >> 16n, "两者在负数上必须分叉,故降精度只能用 >>");
});
test("fmul 金标准:1.5 * 2.5 = 3.75 逐位精确", () => {
const p = fmul(3n * ONE / 2n, 5n * ONE / 2n);
assert.equal(p, 15n * ONE / 4n);
assert.equal(p, 245760n);
});
test("【核心】点积序列 sha256 与 Python 侧逐位相同", () => {
const dots: bigint[] = [];
for (let i = 0; i < N; i += 1) {
dots.push(vDot(vecs[i], vecs[(i + 1) % N]));
}
assert.equal(dots[0], GOLDEN_DOT0);
assert.equal(dots[N - 1], GOLDEN_DOT_LAST);
const digest = createHash("sha256").update(dots.join(",")).digest("hex");
assert.equal(digest, GOLDEN_DOT_SHA);
});
test("叉积序列 sha256 与 Python 侧逐位相同", () => {
const crs: string[] = [];
for (let i = 0; i < N; i += 1) {
const c = vCross(vecs[i], vecs[(i + 1) % N]);
crs.push(`${c[0]},${c[1]},${c[2]}`);
}
const digest = createHash("sha256").update(crs.join(",")).digest("hex");
assert.equal(digest, GOLDEN_CROSS_SHA);
});
test("负例:改用截断除法降精度 -> 序列 sha256 必须分叉", () => {
const truncMul = (a: bigint, b: bigint): bigint => {
const p = a * b;
return p / ONE; // 向零截断,而不是 floor
};
const dots: bigint[] = [];
for (let i = 0; i < N; i += 1) {
const a = vecs[i];
const b = vecs[(i + 1) % N];
dots.push(truncMul(a[0], b[0]) + truncMul(a[1], b[1]) + truncMul(a[2], b[2]));
}
const digest = createHash("sha256").update(dots.join(",")).digest("hex");
assert.notEqual(digest, GOLDEN_DOT_SHA, "换成截断除法后必须与金标准分叉");
});
真跑:
# snippet: 复现命令(在 lantern 工作区根执行)
cd $HOME/.cgbook/lantern
/opt/homebrew/bin/python3 sim/cg_raster_02_1_fixed_point.py
node --test 'core/test/cg_raster_02_1_fixed_point.test.ts'
S 侧输出(节选):
ONE : 65536
int32 Q16.16 范围 : [-32768.00000, 32767.99998]
1.5 + 2.25 : 245760 (3.75000)
2.25 - 1.5 : 49152 (0.75000)
fmul(1.5, 2.5) : 245760 (3.75000)
0.1 的 Q16.16 表示 : 6553 (真值 6553.6)
点积样本数 : 64
误差范围 (ulp) : [0.365311, 2.615829] (< 3)
样本 0 a=(-144774, -89454, 74686) b=(235959, 177850, 156392)
v_dot=-585784 exact=-585782.306732
v_cross(a, b) : (-416150, 614385, -70810)
P 侧结果:
ℹ tests 5
ℹ pass 5
ℹ fail 0
点积序列的 sha256 是 2774812048ca6c61…,叉积是 280d567b9ca69415…,两侧完全相同。这就是「可证伪」的含义:任何一处舍入改动——哪怕只把一次 >> 换成 /——都会让 sha256 立刻变掉,负例正是这样被抓住的。
阅读导航:上一节:1.3 最小渲染器的骨架 · 下一节:2.2 点与向量的区别 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。