14.2 CPU 与 GPU 的差异

把 CPU 定点与浏览器 WebGL2 之间的差异拆成三类可归因的来源:光栅化规则(顶左规则与子像素精度)、插值方式(屏幕空间仿射对透视校正)、浮点精度(Q6/Q16.16 对 float32)。每类配一个只动一个变量的实验,并用 MAE ≤ 1.0、PSNR ≥ 30 dB、覆盖归属差异 ≤ 20‰ 三条声明容差加一个必须被拒绝的负例来收口。

14.2 CPU 与 GPU 的差异

站内 /graphics-api-modern-comparison/ 那篇从工程与生态的角度比 API;这一节要的是数值:同一份几何、同一个光照公式、同一套顶点属性,把它交给 CPU 定点与交给浏览器 WebGL2,结果会差多少?差在哪儿?只说「GPU 与 CPU 有差异」是没有信息量的——差异必须能归因,否则既不能修,也不能声明容差。

三类差异与三个可分离实验

差异可以拆成三类,每一类配一个只动一个变量的实验,其余全部冻结:

类别唯一变量可分离实验量什么
① 光栅化规则覆盖判据 / 屏幕坐标位数三边 >= 0 ↔ 三边 > 0 ↔ 顶左规则;Q6 逐级量化到 1/2^k px覆盖差、重复覆盖、2×2 微四边形
② 插值方式仿射 ↔ 透视校正同一组边函数权重,只换插值公式法线差、MAE、PSNR
③ 浮点精度Q6/Q16.16 ↔ float32几何、覆盖、插值方式全同,只换数的表示MAE、PSNR、覆盖归属差

「可分离」的含义是:任何一个实验里,只有一行代码在变。做不到这一点,量出来的差值就是三个原因混在一起的总和,无法归因。

实验一:覆盖规则与子像素精度

真实场景上三种规则给出的覆盖完全相同(各 43731 像素,覆盖差 0),差别只在「重复覆盖」的计数:>= 0 与顶左规则都是 3538 个像素被两个三角形覆盖过,> 0 是 3537。真实场景量不出覆盖差别,是因为没有一条共享边正好穿过像素格心——所以还要一个 2×2 像素的微四边形,把对角线故意放在两个格心上:inclusive 覆盖 4 个像素但 2 个重复覆盖,exclusive 覆盖 2 个、留下 2 个空洞,只有顶左规则给出 4 个覆盖、0 重复、0 空洞。

子像素精度是另一根轴:屏幕 Q6 坐标每少一位,覆盖就往「量化后的位置」漂一点。1/1 px 差 179 个像素,1/16 px(OpenGL 只保证这个)差 10 个,1/64 px(本节点位)差 0。

实验二与实验三:插值方式、数的表示

透视校正的公式是 a = Σ(w_i·a_i/w_i) / Σ(w_i/w_i),屏幕空间仿射是 a = Σ(w_i·a_i) / Σ w_i。两者只在近处大三角形上分得出差别:本场景的裁剪空间 w 跨度 18.08×,仿射与透视的法线平均差 299.5 Q16.16(约 0.0046),最大差 3635(0.0555)。这是一类真实存在的、不来自精度的差异——即使两侧都是无限精度的实数,它也在。

第三类才是数的表示。CPU 走 Q6(屏幕)+ Q16.16(深度与法线)+ 8 位通道,中间量全部整数、移位截断;GPU 的顶点与片元着色器走 float32。两者的几何与覆盖完全一样(共用同一份边函数权重),唯一变量是浮点格式。逐位一致在数学上不可能,所以这一类只能用声明容差收口。

{
  "name": "lantern-host-difference",
  "subpixelSweepBits": [0, 1, 2, 3, 4, 5, 6],
  "subpixelOpenGlGuaranteeBits": 4,
  "tolerance": {
    "maeMax": 1.0,
    "psnrMinDb": 30.0,
    "coverageDiffPermilleMax": 20.0,
    "significantChannelDiff": 2
  },
  "negativeHalfColor": true,
  "note": "阈值口径取自 BRIEF §10.1:MAE <= 1.0 / PSNR >= 30 dB / 覆盖归属差异 <= 20‰"
}

三条容差不是拍的:MAE ≤ 1.0 卡住「平均每个通道差不到 1 个灰阶」,PSNR ≥ 30 dB 卡住「差异不集中在少数像素上」,覆盖归属差异 ≤ 20‰ 卡住「可见性本身有没有漂」。significantChannelDiff = 2 是「显著差异」的门槛——1 个灰阶的差在 8 位量化里本来就无法避免。

完整脚本

#!/usr/bin/env python3
# file: lantern/sim/cg_trace_14_2_difference.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/cg_trace_14_2_difference.py
"""14.2《CPU 与 GPU 的差异》:把「同一场景、同一光照公式、同一套顶点属性」在 CPU 定点与
浏览器 WebGL2 之间的差异**拆成可归因的三类**,每类配一个只动一个变量的可分离实验。

  ① **光栅化规则**——覆盖判据从三边 `>= 0` 换成三边 `> 0`(无补偿的半开区间)与按顶左
     规则逐边选,真实场景上量覆盖差,再用一个 2×2 像素的微四边形(对角线正好穿过格心)
     把三种规则逼出来;子像素上把屏幕 Q6 坐标逐级量化到 1/2^k 像素后重算覆盖,k=6 是本
     节点位(1/64 px),k=4 是 OpenGL 只保证的 1/16 px。
  ② **插值方式**——屏幕空间仿射 `Σ w_i·n_i / Σ w_i` 对透视校正 `Σ (w_i·n_i/w_i) / Σ (w_i/w_i)`。
  ③ **浮点精度**——CPU 定点(Q6 + Q16.16 + `>> 16`)对 float32:几何、覆盖、插值方式全同。

三条声明容差取自 BRIEF §10.1:MAE ≤ 1.0、PSNR ≥ 30 dB、覆盖归属差异 ≤ 20‰,并配负例
(片元颜色 ×1/2)证明判据有区分力。产物见 `out/cg_trace_14_2_*.{json,csv,rgb}`。
"""

import json
import os
import sys

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 cgshade  # noqa: E402
import render_shade  # noqa: E402
import cg_trace_14_common as C  # noqa: E402

CONFIG = os.path.join(ROOT, "config", "cg_trace_14_2_difference.json")
OUT = os.path.join(ROOT, "out")
# 卷二逐像素基线(out/cg_shade_baseline.json):本节 CPU 仿射参考必须逐字节复现它
SHADE_BASELINE = "0d997343ca547ed5cada5e4b1f75ce1782d79e524dba817278853de77307a6de"


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

    tris, ws, _total, dropped = C.build_scene(spec, width, height)
    assert tris == render_shade.build_scene_shaded(spec)[0], "场景构建与卷二必须逐字同构"

    # CPU 定点 · 屏幕空间仿射:必须逐字节复现卷二逐像素基线
    shade = C.make_shader(spec)
    ref, _rd, ref_cov, _rc = cgshade.rasterize_shaded(tris, width, height, bg, shade)
    ref_digest = C.digest_u8(ref)

    covered, owner, W0, W1, W2, _depth, cover_count, culled = C.rasterize(tris, width, height)
    cov_match = bool(np.array_equal(covered, ref_cov))
    base_covered = int(covered.sum())

    n_aff, a_aff = C.attributes_for_pixels(tris, ws, owner, W0, W1, W2, "affine")
    n_per, a_per = C.attributes_for_pixels(tris, ws, owner, W0, W1, W2, "persp")
    img_affine = C.shade_normals(spec, n_aff, a_aff, owner, bg)
    img_persp = C.shade_normals(spec, n_per, a_per, owner, bg)
    img_f32 = C.shade_float32(spec, tris, owner, W0, W1, W2, bg)
    assert C.digest_u8(img_affine) == ref_digest, "自研光栅化器 + 逐像素着色必须复现卷二基线"

    m = owner >= 0
    d_n = np.abs(n_aff - n_per)[m]
    ndiff = {"meanAbsQ16": round(float(d_n.mean()), 4), "maxAbsQ16": int(d_n.max()),
             "maxAbsOfOne": round(float(d_n.max()) / C.ONE, 6),
             "pixelsWithAnyDiff": int((d_n.max(axis=1) > 0).sum())}

    # ---- ① 光栅化规则 ----
    cov_excl, _oe, *_e, cnt_excl, _ce = C.rasterize(tris, width, height, rule="exclusive")
    cov_tl, own_tl, *_t, cnt_tl, _ct = C.rasterize(tris, width, height, rule="topleft")
    rules = {
        "inclusive": {"covered": base_covered, "doubleCovered": int((cover_count > 1).sum())},
        "exclusive": {"covered": int(cov_excl.sum()),
                      "coverageDiffPixels": int((cov_excl ^ covered).sum()),
                      "doubleCovered": int((cnt_excl > 1).sum())},
        "topleft": {"covered": int(cov_tl.sum()),
                    "coverageDiffPixels": int((cov_tl ^ covered).sum()),
                    "ownerDiffPixels": int((own_tl != owner).sum()),
                    "doubleCovered": int((cnt_tl > 1).sum())},
        "microQuad": C.micro_quad(),
    }

    # ---- ① 子像素精度扫描 ----
    sweep = []
    for k in cfg["subpixelSweepBits"]:
        cov_k, *_r, cnt_k, _ck = C.rasterize(tris, width, height, keep_bits=k)
        diff = int((cov_k ^ covered).sum())
        sweep.append({"keepBits": k, "resolutionPx": f"1/{1 << k}", "covered": int(cov_k.sum()),
                      "coverageDiffPixels": diff,
                      "coverageDiffPermilleOfCovered": round(diff * 1000.0 / base_covered, 4),
                      "doubleCovered": int((cnt_k > 1).sum())})
    og = cfg["subpixelOpenGlGuaranteeBits"]
    cov_gl = C.rasterize(tris, width, height, keep_bits=og)[0]
    sub_at_gl = next(r for r in sweep if r["keepBits"] == og)

    # 第 14.1 节那 4 个分歧像素在两类实验里的表现
    probe = os.path.join(OUT, "cg_trace_14_1_two_paths.json")
    diverg = json.load(open(probe, encoding="utf-8"))["divergent"] if os.path.isfile(probe) else []
    probes = [{"px": r["px"], "py": r["py"],
               "exclusiveFlipped": bool(cov_excl[r["py"], r["px"]] != covered[r["py"], r["px"]]),
               "topLeftFlipped": bool(cov_tl[r["py"], r["px"]] != covered[r["py"], r["px"]]),
               "subpixel1_16Flipped": bool(cov_gl[r["py"], r["px"]] != covered[r["py"], r["px"]])}
              for r in diverg]

    # ---- ② / ③ 度量 ----
    metrics = {"affine_vs_persp": C.compare(img_affine, img_persp, bg),
               "affine_vs_float32": C.compare(img_affine, img_f32, bg),
               "persp_vs_float32": C.compare(img_persp, img_f32, bg)}
    metrics["affine_vs_halfcolor"] = C.compare(img_affine, np.clip(img_affine // 2, 0, 255), bg)
    tol = cfg["tolerance"]
    af, ap = metrics["affine_vs_float32"], metrics["affine_vs_persp"]
    neg = metrics["affine_vs_halfcolor"]
    wvals = [w for wv in ws for w in wv]
    stats = {
        "width": width, "height": height,
        "scene": {"trianglesDrawn": len(tris), "dropped": dropped, "culled": int(culled)},
        "clipWRange": {"minQ16": min(wvals), "maxQ16": max(wvals),
                       "ratio": round(max(wvals) / min(wvals), 4)},
        "cpuAffine": {"coveredPixels": base_covered,
                      "coveragePermilleOfCanvas": round(base_covered * 1000.0 / (width * height), 4),
                      "sha256": ref_digest, "matchesVolume2Baseline": ref_digest == SHADE_BASELINE,
                      "coverageMatchesOwnerRasterizer": cov_match},
        "normalDifferenceAffineVsPersp": ndiff,
        "class1_rasterRules": {"rules": rules, "subpixelSweep": sweep, "divergentPixelProbes": probes},
        "metrics": metrics, "tolerance": tol,
        "verdict": {
            "affine_vs_float32_within_mae": af["mae"] <= tol["maeMax"],
            "affine_vs_float32_within_psnr": af["psnrDb"] >= tol["psnrMinDb"],
            "affine_vs_float32_within_coverage": af["coverageDiffPermille"] <= tol["coverageDiffPermilleMax"],
            "affine_vs_persp_within_mae": ap["mae"] <= tol["maeMax"],
            "affine_vs_persp_within_psnr": ap["psnrDb"] >= tol["psnrMinDb"],
            "negative_rejected": (neg["mae"] > tol["maeMax"] and neg["psnrDb"] < tol["psnrMinDb"]),
        },
    }
    with open(os.path.join(OUT, "cg_trace_14_2_difference.json"), "w", encoding="utf-8") as fh:
        json.dump(stats, fh, ensure_ascii=False, indent=2)
        fh.write("\n")
    for name, img in (("affine", img_affine), ("persp", img_persp), ("f32", img_f32)):
        C.save_rgb(os.path.join(OUT, f"cg_trace_14_2_{name}.rgb"), img)

    # W 层页面用的顶点属性:每顶点 10 个整数(屏幕 Q6 + 深度 + 法线 + albedo + 1/w)
    with open(os.path.join(OUT, "cg_trace_14_2_rtris.json"), "w", encoding="utf-8") as fh:
        json.dump(C.w_page(spec, tris, ws, width, height, bg), fh, separators=(",", ":"))

    cglib.write_csv(
        os.path.join(OUT, "cg_trace_14_2_metrics.csv"),
        ["pair", "diff_pixels", "diff_permille", "significant_pixels", "mae", "psnr_db",
         "max_channel_diff", "coverage_diff_pixels", "coverage_diff_permille"],
        [[n, v["diffPixels"], v["diffPermille"], v["significantPixels"], v["mae"], v["psnrDb"],
          v["maxChannelDiff"], v["coverageDiffPixels"], v["coverageDiffPermille"]]
         for n, v in metrics.items()])
    cglib.write_csv(
        os.path.join(OUT, "cg_trace_14_2_subpixel.csv"),
        ["keep_bits", "resolution_px", "covered", "coverage_diff_pixels",
         "coverage_diff_permille_of_covered", "double_covered"],
        [[r["keepBits"], r["resolutionPx"], r["covered"], r["coverageDiffPixels"],
          r["coverageDiffPermilleOfCovered"], r["doubleCovered"]] for r in sweep])
    cglib.write_csv(
        os.path.join(OUT, "cg_trace_14_2_rules.csv"),
        ["rule", "covered", "coverage_diff_pixels", "owner_diff_pixels", "double_covered"],
        [[k, v["covered"], v.get("coverageDiffPixels", 0), v.get("ownerDiffPixels", 0),
          v["doubleCovered"]] for k, v in rules.items() if k != "microQuad"])

    print("== 14.2 CPU 与 GPU 的差异 ==")
    print(f"几何                : {len(tris)} 个三角形绘制(剔除 {culled},丢弃 {dropped})")
    print(f"裁剪空间 w          : {min(wvals)} ~ {max(wvals)} Q16.16"
          f"(跨度 {stats['clipWRange']['ratio']}×)")
    print(f"CPU 仿射基线        : {base_covered} 覆盖  sha256 {ref_digest[:16]}…"
          f"  复现卷二基线 {stats['cpuAffine']['matchesVolume2Baseline']}"
          f"  自研光栅器覆盖一致 {cov_match}")
    print("① 光栅化规则(真实场景 320×240):")
    print(f"   三边 >= 0        : 覆盖 {base_covered}  重复覆盖 {rules['inclusive']['doubleCovered']}")
    print(f"   三边 > 0         : 覆盖 {rules['exclusive']['covered']}"
          f"  覆盖差 {rules['exclusive']['coverageDiffPixels']}"
          f"  重复覆盖 {rules['exclusive']['doubleCovered']}")
    print(f"   顶左规则         : 覆盖 {rules['topleft']['covered']}"
          f"  覆盖差 {rules['topleft']['coverageDiffPixels']}"
          f"  归属差 {rules['topleft']['ownerDiffPixels']}"
          f"  重复覆盖 {rules['topleft']['doubleCovered']}")
    print("① 光栅化规则(2×2 微四边形,对角线穿过格心):")
    for k, v in rules["microQuad"].items():
        print(f"   {k:10s} 覆盖 {v['covered']}  空洞 {v['holes']}"
              f"  重复覆盖 {v['doubleCovered']}  覆盖次数网格 {v['coverGrid']}")
    print(f"① 子像素精度扫描(覆盖差 / {base_covered} 个覆盖像素):")
    print("   " + " | ".join(f"1/{1 << r['keepBits']} px {r['coverageDiffPixels']}"
                            f"({r['coverageDiffPermilleOfCovered']:.4f}‰)" for r in sweep[:4]))
    print("   " + " | ".join(f"1/{1 << r['keepBits']} px {r['coverageDiffPixels']}"
                            f"({r['coverageDiffPermilleOfCovered']:.4f}‰)" for r in sweep[4:]))
    print(f"   OpenGL 只保证 1/16 px:差 {sub_at_gl['coverageDiffPixels']} 像素;"
          f"14.1 的 4 个分歧像素翻面情况 " +
          " ".join(f"({r['px']},{r['py']})={r['subpixel1_16Flipped']}" for r in probes))
    print(f"② 法线(仿射 vs 透视): 平均差 {ndiff['meanAbsQ16']} Q16.16,最大 {ndiff['maxAbsQ16']}"
          f"({ndiff['maxAbsOfOne']} of 1.0),有差像素 {ndiff['pixelsWithAnyDiff']}")
    for name, v in metrics.items():
        print(f"   {name:22s}: 差 {v['diffPixels']:5d} 像素({v['diffPermille']:8.4f}‰)"
              f" 显著(>2) {v['significantPixels']:5d}  MAE {v['mae']:8.4f}"
              f"  PSNR {v['psnrDb']:7.2f} dB  最大通道差 {v['maxChannelDiff']:3d}"
              f"  覆盖差 {v['coverageDiffPixels']}")
    print(f"判据                : {json.dumps(stats['verdict'], ensure_ascii=False)}")
    return 0


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

跑一次:

# snippet: 本机实跑(工作区 lantern/)
cd lantern
/opt/homebrew/bin/python3 sim/cg_trace_14_2_difference.py
== 14.2 CPU 与 GPU 的差异 ==
几何                : 896 个三角形绘制(剔除 290,丢弃 0)
裁剪空间 w          : 167561 ~ 3029109 Q16.16(跨度 18.0776×)
CPU 仿射基线        : 43731 覆盖  sha256 0d997343ca547ed5…  复现卷二基线 True  自研光栅器覆盖一致 True
① 光栅化规则(真实场景 320×240):
   三边 >= 0        : 覆盖 43731  重复覆盖 3538
   三边 > 0         : 覆盖 43731  覆盖差 0  重复覆盖 3537
   顶左规则         : 覆盖 43731  覆盖差 0  归属差 0  重复覆盖 3538
① 光栅化规则(2×2 微四边形,对角线穿过格心):
   inclusive  覆盖 4  空洞 0  重复覆盖 2  覆盖次数网格 [2, 1, 1, 2]
   exclusive  覆盖 2  空洞 2  重复覆盖 0  覆盖次数网格 [0, 1, 1, 0]
   topleft    覆盖 4  空洞 0  重复覆盖 0  覆盖次数网格 [1, 1, 1, 1]
① 子像素精度扫描(覆盖差 / 43731 个覆盖像素):
   1/1 px 179(4.0932‰) | 1/2 px 76(1.7379‰) | 1/4 px 48(1.0976‰) | 1/8 px 30(0.6860‰)
   1/16 px 10(0.2287‰) | 1/32 px 2(0.0457‰) | 1/64 px 0(0.0000‰)
   OpenGL 只保证 1/16 px:差 10 像素;14.1 的 4 个分歧像素翻面情况 (142,94)=False (177,94)=False (69,105)=False (307,114)=True
② 法线(仿射 vs 透视): 平均差 299.5069 Q16.16,最大 3635(0.055466 of 1.0),有差像素 43730
   affine_vs_persp       : 差 32830 像素(427.4740‰) 显著(>2)  2166  MAE   0.4008  PSNR   56.07 dB  最大通道差   5  覆盖差 0
   affine_vs_float32     : 差 43402 像素(565.1302‰) 显著(>2)     0  MAE   0.4950  PSNR   54.24 dB  最大通道差   2  覆盖差 0
   persp_vs_float32      : 差 39795 像素(518.1641‰) 显著(>2)  5873  MAE   0.6754  PSNR   51.54 dB  最大通道差   5  覆盖差 0
   affine_vs_halfcolor   : 差 76800 像素(1000.0000‰) 显著(>2) 76800  MAE  25.0472  PSNR   20.16 dB  最大通道差 110  覆盖差 33069
判据                : {"affine_vs_float32_within_mae": true, "affine_vs_float32_within_psnr": true, "affine_vs_float32_within_coverage": true, "affine_vs_persp_within_mae": true, "affine_vs_persp_within_psnr": true, "negative_rejected": true}

五个数字怎么读

① 的两种规则在真实场景上覆盖差 0,只在微四边形上分出胜负。 这恰恰说明「覆盖规则」这一类差异平时是隐形的:896 个三角形、43731 个覆盖像素里没有一个共享边穿过格心,直到你人为造出穿过格心的对角线,>= 0 与顶左规则的重复覆盖才从 2 掉到 0。判据要能测出「平时测不出」的差别,就必须造出极端样例,否则容差只是自我安慰。

② 的仿射对透视:MAE 0.4008、PSNR 56.07 dB、最大通道差 5。 差 32830 个像素(427‰)却有 43730 个像素的法线不同——说明大量像素差在 1 个灰阶以内,被 8 位量化吃掉了。透视校正不是「更准一点点」,它在近处是定性的差别:w 跨度 18.08×,仿射插值会把近处的纹理/法线明显拉错。

③ 的定点对 float32:MAE 0.4950、PSNR 54.24 dB、最大通道差只有 2,显著像素 0。 差 43402 个像素(565‰),但没有一个像素的通道差超过 2——分布是「遍地 1 个灰阶」,不是「少数像素差很多」。这正是定点舍入的特征,也是为什么容差里 MAE 与 PSNR 要同时给:单看 MAE 会以为差异很大,单看 PSNR 会以为差异很小。

负例:片元颜色整体 ×1/2,MAE 25.0472、PSNR 20.16 dB、覆盖差 33069。 同一组容差(MAE ≤ 1.0、PSNR ≥ 30 dB)当场拒绝它。判据必须能拒绝一个明显错的输入,否则「通过」不说明任何事。

CPU 仿射基线逐字节复现卷二:0d997343ca547ed5…,覆盖与自研光栅器一致。 这一步是整节的锚:如果自研光栅化器不能逐字节复现卷二,那么后面所有「差异」都可能只是我自己写错了。

W 层核对:浏览器里的同一条管线

W 层把同一份顶点属性(S 层落盘的 out/cg_trace_14_2_rtris.json)喂给 WebGL2,顶点着色器把 n/w、albedo/w、1/w 作为 varying 交给硬件做仿射插值,片元里再做一次除法重建透视校正——这样顶点位置与 CPU 逐位同源,硬件插值的差异被隔离到「数的表示」这一项:

// snippet: 节选自 lantern/web/verify_trace14.mjs
const p3 = stats(gl, cpuAffine, covGl);        // ③ 只差数的表示(两侧都是仿射)
const p2 = stats(gl, glP, covGlP);             // ② 只差插值方式(两侧都在 GPU)
const p3iso = stats(gl, cpuF32, covGl);        // ③ 隔离项:GPU float32 vs CPU float32
// 本机 headless 后端是 ANGLE/SwiftShader **软件**实现,不是真实 GPU:
// 差异不得归因于硬件。逐位一致在数学上不可能,故只用对舍入不敏感的统计量。

实测:GPU 仿射对 CPU 定点仿射 MAE 0.5067、PSNR 47.03 dB、覆盖归属差 6 像素(0.0781‰),三条容差全过;隔离项 GPU float32 对 CPU float32 的 MAE 掉到 0.0191,证明残余差异确实来自数的表示而不是着色器写错了。25 个显著差异像素里有 19 个是「两边都覆盖、但选中的三角形不同」——这是深度并列时的裁决差,不是着色差。负例(片元颜色 ×0.5)MAE 21.3528、PSNR 18.90 dB,被同一组容差拒绝。后端如实打印为 ANGLE/SwiftShader 软件实现,本节的任何结论都不涉及真实硬件。

阅读导航:上一节:14.1 两条路径的等价与分歧 · 下一节:14.3 一致性的判据 。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「cg」更多文章

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