Skia 正确性测试工具 DM 完整指南:从构建、运行到回归比对
2026/9/24 15:33:30 网站建设 项目流程
  • 图形学

【免费下载链接】skia

Skia is a complete 2D graphic library for drawing Text, Geometries, and Images. See documentation for contribution instructions.

项目地址:https://gitcode.com/gh_mirrors/ski/skia
点击查看免费下载

Skia 的官方文档 Correctness Testing 指出,Skia 的正确性测试主要由一个名为DM(Delta Master)的工具承担。本文基于该文档并结合仓库源码,完整讲解如何构建并运行 DM、如何解读它那铺天盖地的日志输出、如何理解 srcs/sinks/config 等核心概念,以及如何用-r对比模式和skdiff做回归校验。读完本文,你将能够独立搭建起一套 Skia 正确性测试流程,并从源码层面理解 DM 的任务调度、哈希比对与结果落盘机制。

DM 是什么:Skia 正确性测试的发动机

DM 不是普通的单元测试框架,它是一个“源 × 汇”矩阵式的绘图验证工具。从仓库结构看,DM 的全部实现集中在 dm/ 目录下:

  • dm/DM.cpp:主程序,负责命令行解析、任务收集、调度与结果汇总;
  • dm/DMSrcSink.h 与 dm/DMSrcSink.cpp:定义了Src(绘图源)与Sink(绘制目标/后端)两类抽象,以及大量具体实现;
  • dm/DMJsonWriter.cpp:负责把每次运行的结果(含 MD5 摘要)写入dm.json
  • dm/DMGpuTestProcs.cpp:GPU 测试相关的辅助逻辑。

在 BUILD.gn 中,test_app("dm")目标由上述源文件组成,因此构建产物是一个独立的可执行程序dm

DM 的工作模型可以概括为:把一批“源”分别画进一批“汇”里,逐像素记录结果摘要,再与历史基线比对。这正是"正确性测试"的落脚点——不是简单地断言返回值,而是验证每一种后端、每一种配置下绘制出的图像是否与预期一致。

快速开始:四步跑通 DM

在仓库根目录依次执行以下命令即可完成构建并启动一次测试(命令来自官方文档):

python3 tools/git-sync-deps bin/gn gen out/Debug ninja -C out/Debug dm out/Debug/dm -v -w dm_output

各步骤的作用:

  1. python3 tools/git-sync-deps:同步第三方依赖。该脚本(tools/git-sync-deps)会解析仓库根目录的 DEPS 文件,并逐一git checkout其中的依赖仓库,为后续构建准备好完整的源码树。
  2. bin/gn gen out/Debug:用 GN 生成构建文件到out/Debug目录。
  3. ninja -C out/Debug dm:编译 DM 可执行程序。
  4. out/Debug/dm -v -w dm_output:以 verbose 模式运行 DM,并将输出产物(PNG 图像与dm.json)写入dm_output目录。

注意:-w--writePath)是必填的输出参数;而-v--verbose)只是让 DM 以每任务一行的方式打印详细日志。此外,首次运行时如果资源缺失,DM 会提示Some resources are missing. Do you need to set --resourcePath?,此时需要设置资源路径。

运行行为:为什么 CPU 先打满再回落

当你运行 DM 时,会观察到 CPU 先飙到 100% 一段时间,然后逐渐回落到只有 1~2 个核在活跃。官方文档明确说明这是有意设计的:DM 高度多线程化,但部分工作(尤其是 GPU 后端的工作)仍被迫在单线程上执行。

从源码看,DM 的并行调度基于 Skia 自带的SkTaskGroup(见 dm/DM.cpp):

  • 并行任务通过parallel.add(...)提交到线程池,每个可用的硬件线程同时跑一个任务;
  • 标记为serial的源或汇,以及全部 CPU 串行测试(gCPUSerialTests),在主线程上串行执行,以避免与并行测试产生竞态;
  • 线程池大小由--threads(短选项-j)控制,其默认值是"每个核一个额外线程"(见 dm/DM.cpp)。

如果机器的 CPU 相对内存比较富裕(或者说内存相对紧张),可以通过--threads N限制并发线程数:

out/Debug/dm -w dm_output --threads 4

解读输出:Skipping 行、任务总数与状态行

启动期的 Skipping 行,不必惊慌

运行开始时你会看到大量类似下面的行:

Skipping nonrendering: Don't understand 'nonrendering'. Skipping angle: Don't understand 'angle'. Skipping nvprmsaa4: Could not create a surface.

官方文档提示:这些行是"仅供参考"(FYI)性质的提示。DM 支持非常多的测试配置,但并非所有配置都适用于每台机器。出现Skipping xxx: Don't understand 'xxx'.通常意味着该配置在当前构建中不可用(如未编译对应后端),而Could not create a surface.则意味着运行环境(如 GPU 能力)不满足该配置的要求。只有当某个你预期应该跑起来的配置被跳过时,才需要认真排查。

从源码看,这些行来自gather_sinks()中对每个配置创建Sink失败时的打印(dm/DM.cpp)。

另外,如果出现skps: Couldn't read skps.也不必担心——默认构建并不自带.skp文件。如果需要测试 SKP 录制文件,可以用 bin/fetch-skps 单独下载。

任务总数:srcs × sinks + tests

接下来 DM 会打印一行总览:

492 srcs * 3 sinks + 382 tests == 1858 tasks

这行字对应源码 dm/DM.cpp 中的计算:

gPending = gSrcs->size() * gSinks->size() + testCount; info("%d srcs * %d sinks + %d tests == %d tasks\n", ...);

其中:

  • srcs(源):可以被绘制的输入,来自三类:
    • GM 集成测试:代码位于 gm/ 目录,每个 GM 用DEF_GM宏注册(见 gm/gm.h);
    • 图像文件:来自--images,默认目录是resources(仓库中的 resources/images 就存放着大量测试用图,例如文档示例中出现的 mandrill 系列);
    • .skp 文件:来自--skps,默认目录是skps
    • 源类型由--src控制,默认值是"tests gm skp mskp lottie rive svg image colorImage"(见 dm/DM.cpp)。
  • sinks(汇):绘制目标,即"用什么后端、画到什么表面"的配置组合,由--config控制(详见下文)。
  • tests(单元测试):通过DEF_TESTDEF_SERIAL_TESTDEF_GRAPHITE_TEST等宏注册(见 tests/Test.h),代码位于 tests/ 目录。单元测试不遵循 src-sink 模型,因此单独计数。

DM 总是把所有源画进所有汇(除非被veto--skip排除),这就是"492 × 3"的来历。几千个任务是非常正常的数量。

状态行:一条日志的四个字段

( 25MB 1857) 1.36ms 8888 image mandrill_132x132_12x12.astc-5-subsets [1] [2] [3] [4]

逐字段解读(官方文档原意 + 源码印证):

  1. 25MB:DM 进程迄今为止用到的峰值内存(高水位标记,而非当前内存)。它主要服务于 CI 构建机器人——有些机器离系统内存上限非常近,需要跟踪峰值。源码中通过sk_tools::getMaxResidentSetSizeMB()获取(dm/DM.cpp),并会一并写入dm.jsonmax_rss_MB字段。
  2. 1857:尚未完成的任务数(正在运行 + 等待运行)。通常每个硬件线程同时跑一个任务,所以典型笔记本上大约同时有 4 或 8 个任务在跑。文档特别说明:启动初期计数看起来乱序是无害的,不影响运行正确性。
  3. 1.36ms:该任务的实际耗时,计时精度约 1 微秒。这个数字纯粹供参考,主要用于发现慢测试
  4. 8888 image mandrill_132x132_12x12.astc-5-subsets<config> <srcType> <srcOptions> <name>。这里就是把名为mandrill_132x132_12x12.astc-5-subsets的 image 源画进了8888汇。

如果觉得日志太吵,去掉-v后 DM 会把进度压缩到单行刷新(配合--quiet/-q甚至可以完全静默)。

深入理解 config:8888 与 gl 之外的世界

文档指出,Linux 上默认的--config"8888 gl nonrendering",其中:

  • 8888:用软件后端(software rasterizer)绘制到 32 位 RGBA 位图;
  • gl:用 OpenGL 后端(Ganesh,Skia 的 GPU 渲染引擎)绘制到 32 位 RGBA 位图;
  • nonrendering:非渲染配置,常因当前环境不支持而被跳过。

从源码看,默认配置定义在 tools/flags/CommonFlagsConfig.cpp:

static const char defaultConfigs[] = "8888 " DEFAULT_GPU_CONFIG " nonrendering " #if SK_ANGLE && defined(SK_BUILD_FOR_WIN) " angle_d3d11_es2" #endif ;

其中DEFAULT_GPU_CONFIG在 Android/iOS 上为"gles",其他平台为"gl"(见同文件第 23-28 行)——这印证了文档所说"默认配置与操作系统相关"。

8888gl只是冰山一角。同一个文件中的gPredefinedConfigs表(tools/flags/CommonFlagsConfig.cpp)预定义了上百种配置,例如:

配置名底层后端说明
gl/glesGPUOpenGL / OpenGL ES
glf16/glsrgba/gl1010102GPU不同颜色格式(半浮点 / sRGB / 10-10-10-2)
glmsaa4/glmsaa8GPU4x / 8x 多重采样抗锯齿
glbetex/glbertGPU后端纹理 / 渲染目标表面变体
gldmsaaGPU动态 MSAA
angle_d3d11_es2GPU通过 ANGLE 在 D3D11/D3D9/Metal 上跑 ES2/ES3
glddlGPU使用 DDL(Deferred Display List)Sink
gltestpersistentcacheGPU测试持久化着色器缓存

这些预定义配置用--config指定即可,例如:

out/Debug/dm -w dm_output --config 8888 glmsaa4 glf16

文档特别提醒:DM 偶尔会把这些概念叫做 "configs" 或 "sinks",二者指的是同一回事;日常关注最多的就是8888gl

输出产物:dm.json 与按目录组织的 PNG

运行结束后,dm_output目录里会有一个dm.json文件,以及按config/srcType嵌套的图像目录:

$ ls dm_output 8888 dm.json gl $ find dm_output -name '*.png' dm_output/8888/gm/3x3bitmaprect.png dm_output/8888/gm/aaclip.png dm_output/8888/gm/aarectmodes.png ...

目录结构为先按汇类型(--config),再按源类型(--src,最后以源名称命名图像文件。例如状态行中的任务8888 image mandrill_132x132_12x12.astc-5-subsets,其输出位于:

dm_output/8888/image/mandrill_132x132_12x12.astc-5-subsets.png

这一落盘逻辑可以在 dm/DM.cpp 的WriteToDisk()中看到:它逐级创建<writePath>/<config>/<srcType>/[<srcOptions>/]<name>.<ext>。若启用--nameByHash,则会改为按 MD5 内容寻址存储:<writePath>/<md5>.<ext>,且文件已存在时直接跳过(内容寻址去重)。

dm.json是自动化测试系统的接口文件,它记录了每次运行每个任务的结果摘要。从 dm/DMJsonWriter.cpp 可以看到其结构:

  • 顶层:propertieskey(用于标识 builder 的键值对,来自--key/--properties)、max_rss_MBresults数组;
  • 每个 result 包含:
    • keyname(测试名)、config(汇)、source_type(源类型)、source_options(源选项,如存在);
    • optionsext(文件扩展名)、gamut(色域,如 sRGB/P3/Adobe)、transfer_fn(传递函数)、color_typealpha_typecolor_depth
    • md5:图像摘要。

这些options字段在 dm/DM.cpp 中通过识别位图的色彩空间(identify_gamut/identify_transfer_fn)等生成,方便在 Gold(Skia 的自动图像比对服务)中按色彩特征归类。

Digest 细节:哈希的不是 PNG 文件,而是原始像素

文档特意强调了一个"枯燥但重要"的技术细节:dm.json中的 checksum(MD5)并不是对.png文件本身做哈希,而是对生成该 PNG 的原始像素做哈希。这意味着两种配置可能生成字节完全相同的.png,但它们的 checksum 却不同。

源码印证:在 dm/DM.cpp 中,DM 用SkMD5对两种数据源之一做摘要:

  • 如果data流有内容(如 PDF、MSKP 等非位图产物),直接对数据流做哈希;
  • 否则通过HashAndEncode(见 tools/HashAndEncode.h)把SkBitmap原始像素喂给哈希器,再交给hashAndEncode->encodePNG(...)写 PNG。

由于先算像素摘要、再编码 PNG,checksum 反映的是"画出来的结果"而非"磁盘上的文件",这正是回归比对能跨配置、跨机器生效的前提。

单元测试:通过时静默,失败时全量汇报

单元测试通过时通常只输出一条状态更新;一旦失败,DM 会在失败发生时立即打印断言失败信息,并在全部任务结束后把所有失败再汇总打印一遍Failures:段落),同时这些失败也会写入dm.json

源码逻辑:DMReporter::reportFailed()调用全局的fail()(dm/DM.cpp),把错误压入gFailures数组并立即SkDebugf输出;主流程结束时(dm/DM.cpp)再遍历gFailures汇总输出,且只要存在任何失败,DM 就以退出码 1 结束。

测试本身通过DEF_TEST(name, reporter)等宏注册,例如:

DEF_TEST(ExampleTest, reporter) { REPORTER_ASSERT(reporter, 1 + 1 == 2); }

REPORTER_ASSERT宏(tests/Test.h)在断言失败时记录失败信息,与TestRegistry(tests/Test.h)配合实现静态注册,DM 启动时通过skiatest::TestRegistry::Range()收集全部测试(dm/DM.cpp)。

回归比对:-r 对比模式与 skdiff

方式一:-r直接对比上一次运行

DM 内置了简单的历史结果比对能力:

ninja -C out/Debug dm out/Debug/dm -w good # 第一次运行,产出基线 good/ # ... 修改或重构代码 ... ninja -C out/Debug dm out/Debug/dm -r good -w bad # 第二次运行,与 good 对比后输出 bad/

使用-r--readPath)时,只要某个测试没有产生与good运行完全相同的图像,DM 就会报告失败。

其实现机制:gather_gold()(dm/DM.cpp)读取good/dm.json中所有(config, sourceType, sourceOptions, name, md5)五元组并存入gGold哈希集合;运行中每个任务算出的 MD5 若不在gGold中,则通过fail()报错(dm/DM.cpp)。这解释了为什么 checksum 必须基于原始像素——只有跨运行的稳定摘要,才能作为可靠的比对键。

方式二:skdiff 做更精细的差异分析

需要更复杂、可交互的差异分析时,用skdiff

ninja -C out/Debug dm out/Debug/dm -w good # do some work ninja -C out/Debug dm out/Debug/dm -w bad ninja -C out/Debug skdiff mkdir diff out/Debug/skdiff good bad diff # open diff/index.html in your web browser

skdiff是仓库中的一个独立工具(源码位于 tools/skdiff,GN 目标见 BUILD.gn)。它在 skdiff_main.cpp 中的功能说明很清晰:

  • 接收三个目录:前两个分别视为基线图集变体图集,期望其中存在同名文件;
  • 对每一对文件生成一张差异图(diff image)写入第三个目录;
  • 在第三个目录生成index.html,方便在浏览器中逐对对比不一致的图像;
  • 默认递归遍历子目录(可用--norecurse关闭);
  • 若所有图像完全一致,则返回退出码 0

相比-r的"全有或全无",skdiff能直观展示像素差异的位置与程度,适合人工审视渲染回退。

常用 Flags 速查

DM 支持大量命令行开关,最常用的整理如下(源码定义见 dm/DM.cpp):

out/Debug/dm --help # 打印所有 flag、默认值与简要说明 out/Debug/dm --src tests # 只运行单元测试 out/Debug/dm --nocpu # 只测试 GPU 后端工作 out/Debug/dm --nogpu # 只测试 CPU 后端工作 out/Debug/dm --match blur # 只运行名称包含 "blur" 的工作 out/Debug/dm --dryRun # 不真正执行,仅打印将要运行的任务

几个值得展开的开关:

  • --match(短选项-m:支持[~][^]substring[$]语法,可空格分隔多个模式;~表示反向匹配(跳过),^/$限定开头/结尾,两者同时使用即精确匹配(dm/DM.cpp)。
  • --skip:以config/src/srcOptions/name四元组跳过特定任务,_匹配任意值、~取反。例如--skip gpu skp _ _跳过所有画进 gpu 配置的 SKP(dm/DM.cpp)。
  • --threads N(短选项-j:限制线程池大小,详见上文"运行行为"一节。
  • --shards N/--shard i:把任务集分片,适合多机并行测试(dm/DM.cpp)。
  • --list:收集并打印所有源与汇后退出,方便确认配置是否生效([dm/DM.cpp](https://link.gitcode.com/i/087e038de69e3020bed20c7669692cf2#L169-L171, L1727-L1731))。
  • --config:覆盖默认汇配置;--src:覆盖默认源类型集合。
  • --key/--properties:向dm.json追加键值对,用于标识 builder 或运行环境。
  • --nameByHash:按 MD5 内容寻址输出文件。

小结

DM 是 Skia 正确性测试的核心设施:它以"所有源 × 所有汇 + 单元测试"的矩阵模型,把每一种后端、每一种颜色配置下的绘制结果都沉淀为可复现的像素摘要(dm.json+ PNG 目录),并提供-rskdiff两级回归比对手段。理解它的运行日志、配置体系与哈希细节,是深入 Skia 开发、排查渲染回退的第一步。更多一手细节,可以继续阅读 site/docs/dev/testing/testing.md 与 dm/DM.cpp 中的 flag 注释。

  • 图形学

【免费下载链接】skia

Skia is a complete 2D graphic library for drawing Text, Geometries, and Images. See documentation for contribution instructions.

项目地址:https://gitcode.com/gh_mirrors/ski/skia
点击查看免费下载

相关推荐

上一篇:3大创新突破解决Android位置模拟难题:FakeLocation技术原理解析与场景落地指南
下一篇:Pydantic AI 实时语音 Agent 的前端音频接入拓扑怎么选

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询