- 图形学
【免费下载链接】skia
Skia is a complete 2D graphic library for drawing Text, Geometries, and Images. See documentation for contribution instructions.
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各步骤的作用:
python3 tools/git-sync-deps:同步第三方依赖。该脚本(tools/git-sync-deps)会解析仓库根目录的 DEPS 文件,并逐一git checkout其中的依赖仓库,为后续构建准备好完整的源码树。bin/gn gen out/Debug:用 GN 生成构建文件到out/Debug目录。ninja -C out/Debug dm:编译 DM 可执行程序。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)。
- GM 集成测试:代码位于 gm/ 目录,每个 GM 用
- sinks(汇):绘制目标,即"用什么后端、画到什么表面"的配置组合,由
--config控制(详见下文)。 - tests(单元测试):通过
DEF_TEST、DEF_SERIAL_TEST、DEF_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]逐字段解读(官方文档原意 + 源码印证):
25MB:DM 进程迄今为止用到的峰值内存(高水位标记,而非当前内存)。它主要服务于 CI 构建机器人——有些机器离系统内存上限非常近,需要跟踪峰值。源码中通过sk_tools::getMaxResidentSetSizeMB()获取(dm/DM.cpp),并会一并写入dm.json的max_rss_MB字段。1857:尚未完成的任务数(正在运行 + 等待运行)。通常每个硬件线程同时跑一个任务,所以典型笔记本上大约同时有 4 或 8 个任务在跑。文档特别说明:启动初期计数看起来乱序是无害的,不影响运行正确性。1.36ms:该任务的实际耗时,计时精度约 1 微秒。这个数字纯粹供参考,主要用于发现慢测试。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 行)——这印证了文档所说"默认配置与操作系统相关"。
8888和gl只是冰山一角。同一个文件中的gPredefinedConfigs表(tools/flags/CommonFlagsConfig.cpp)预定义了上百种配置,例如:
| 配置名 | 底层后端 | 说明 |
|---|---|---|
gl/gles | GPU | OpenGL / OpenGL ES |
glf16/glsrgba/gl1010102 | GPU | 不同颜色格式(半浮点 / sRGB / 10-10-10-2) |
glmsaa4/glmsaa8 | GPU | 4x / 8x 多重采样抗锯齿 |
glbetex/glbert | GPU | 后端纹理 / 渲染目标表面变体 |
gldmsaa | GPU | 动态 MSAA |
angle_d3d11_es2等 | GPU | 通过 ANGLE 在 D3D11/D3D9/Metal 上跑 ES2/ES3 |
glddl | GPU | 使用 DDL(Deferred Display List)Sink |
gltestpersistentcache | GPU | 测试持久化着色器缓存 |
这些预定义配置用--config指定即可,例如:
out/Debug/dm -w dm_output --config 8888 glmsaa4 glf16文档特别提醒:DM 偶尔会把这些概念叫做 "configs" 或 "sinks",二者指的是同一回事;日常关注最多的就是8888和gl。
输出产物: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 可以看到其结构:
- 顶层:
properties、key(用于标识 builder 的键值对,来自--key/--properties)、max_rss_MB、results数组; - 每个 result 包含:
key:name(测试名)、config(汇)、source_type(源类型)、source_options(源选项,如存在);options:ext(文件扩展名)、gamut(色域,如 sRGB/P3/Adobe)、transfer_fn(传递函数)、color_type、alpha_type、color_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 browserskdiff是仓库中的一个独立工具(源码位于 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 目录),并提供-r与skdiff两级回归比对手段。理解它的运行日志、配置体系与哈希细节,是深入 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.
相关推荐
miniblink49 内置 Skia 的图形正确性测试:DM 工具从构建到黄金图对比实战指南
miniblink49 内置 Skia 的图形正确性测试:DM 工具从构建到黄金图对比实战指南 miniblink49 作为基于 Blink 的轻量级浏览器内核
前端桌面应用Skia GN 构建工作流完全指南:从 `dm` 正确性测试到 `nanobench` 性能基准与 `viewer` 可视化
Skia GN 构建工作流完全指南:从 dm 正确性测试到 nanobench 性能基准与 viewer 可视化 Skia 是基于 GN 构建系统与 Ninja
图形学Skia dm 工具使用指南:GMs 图形测试与单元测试的正确性验证实战
Skia dm 工具使用指南:GMs 图形测试与单元测试的正确性验证实战 dm 是 Skia 主要的 正确性(correctness)测试工具 ,负责运行 GM
图形学
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考