一组纹理有八页,画面每次只用其中一页,GPU 是否需要始终保存全部八页?可以只留三个槽,缺哪页就上传哪页。但省下的容量要用上传和调度来交换;相机不断回来访问刚被淘汰的页时,这种交换可能很差。

本篇只研究纹理页流送。沿用第 25、26 篇的上传、绘制与读回方法,以及第 34、35 篇的固定输入、画质前置检查和重复测量,不扩建完整引擎。全部源页已在 CPU 内存中,因此实验不包括磁盘、网络、图片解码或压缩纹理转码。

先固定画面需要哪些字节

实验生成八张 64×64 的 RGBA8 页,每页 16,384 字节,透明度固定为 255。RGB 由页号与整数像素坐标生成,颜色只作为诊断码值,没有光照、过滤、mip、混合或额外的颜色转换。

程序生成的八页诊断纹理,按两行排列

把八页视为一条虚拟纹理带,相机每帧停在一个整数页中心,输出正好是这一页的 64×64 像素。这个“相机”只是确定页号的有界模型,没有透视变换或跨页过滤。两条轨迹都只访问前四页:

1
2
局部性:0 0 0 0  1 1 1 1  2 2 2 2  3 3 3 3
抖动: 0 1 2 3 0 1 2 3 0 1 2 3 0 1 2 3

两条路径的着色器相同,均用页表查出 texture array 层号,再以整数坐标执行 textureLoad。常驻方案提前上传全部八页;流送方案只有三个层,使用最近最少使用策略 LRU。每次命中更新使用次序,缺页时选择不属于当前需求集合、且最久未使用的层。

当前主实验每帧只需一页。调度器仍先检查整帧需求:若一帧需要四个不同页,容量三无法同时满足,程序在修改缓存前拒绝请求。重复页号只占一个槽;当前帧需要的页不能被另一项缺页淘汰。这些条件比“找一个空层”更完整。

页表正确,才知道画的是哪一页

虚拟页号与物理层号不能混用。页 3 可以装进层 0,着色器必须先读取 mapping[3]。淘汰旧页时,旧页表项置为无效值,再让新页指向该层。缺页上传整张纹理,同时更新完整的 32 字节页表。

实际上传使用 writeTexture,目标用途为 COPY_DST 与 TEXTURE_BINDING;源行距为 64×4=256 字节,上传层由 origin.z 指定。每帧另写 16 字节控制缓冲,告诉着色器本帧页号。writeTexture 的目标区域、源数据布局和复制大小是独立参数,可对照 GPUQueue.writeTexture 接口。

层复用还要满足时间顺序。若旧绘制仍在读取层 0,就不能把它覆盖成新页。本实验最多一帧在途:等上一帧完成,再选择、上传、提交新绘制并等待完成。它用同步等待换取明确的生命周期,没有测量多帧流水线、异步预取或后台加载收益。

画质检查先于计时。32 对画面分别完整读回常驻与流送结果,每张都有 4096 像素、16,384 个 RGBA 字节;两臂及独立 C++ 参考逐字节一致。正式计时帧也在接受样本前检查全部字节,只是读回不计入下面的端到端区间。

故意把页 0 映射到错误层后,4096 个像素全部不同,负对照被检出。容量超限也确实返回失败。黑色差图或“页面没有报错”都不能替代这些检查。

少五个层,不等于测出了显存节省

按无 mip 的 RGBA8 载荷计算,P 页常驻与 C 页缓存分别需要:

Bresident=4PWH=131072 B,Bcache=4CWH=49152 B.B_{\rm resident}=4PWH=131072\ \text{B},\qquad B_{\rm cache}=4CWH=49152\ \text{B}.

缓存的逻辑纹理载荷是常驻的 37.5%,减少 81,920 字节。两臂各有 32 字节页表、16 字节控制缓冲,还各有 16,384 字节输出纹理和同样大小的读回缓冲。本次支持时间戳查询,另分配 256 字节查询解析缓冲与 16 字节查询读回缓冲。

这些数说明程序请求了哪些资源,不能直接相加后声称测得物理 VRAM。纹理内部布局、对齐、驱动暂存等没有被测量;而且本实验的两臂同时存活,便于交替对照,并没有释放常驻纹理再测设备总占用。CPU 也始终保留八页,共 131,072 字节源载荷。

初始化单列:常驻上传八页、131,072 字节纹理及 32 字节页表,分配与完成等待约 6.0 ms;缓存初始化未上传页面,约 1.7 ms。两数包含资源创建,不能当成纯复制耗时,也不混入正式轨迹样本。

局部性决定上传多少次

每个正式样本都从冷的流送缓存元数据开始。reset 在计时外清空页号、年龄和页表;物理层不重新分配,旧内容在缺页时被覆盖。常驻路径始终保留已上传的八页。因此这里的预热指代码与运行环境预热,不意味着流送样本沿用上一轮已命中的页面。

16 帧轨迹 流送命中/缺页 淘汰数 纹理上传字节 页表上传字节
局部性 12 / 4 1 65,536 128
抖动 0 / 16 13 262,144 512

常驻路径两组均无缺页与轨迹内纹理上传,仍每帧更新控制缓冲。局部性轨迹对同页连续使用四次,第一次之后均命中。抖动轨迹每次回来之前已经访问另外三页,容量三恰好把即将再用的页淘汰,导致每帧都上传。

抖动时上传了逻辑八页总量的两倍,却始终只占三个纹理层,画质仍相同。这个失败是复用机会不足造成的上传放大,不能被改写成漏页或降低清晰度。缓存容量与累计传输量回答的是两个不同问题。

三种时间各自测到什么

上传专用轨迹先排空队列,在纹理与页表写入前开始计时,等 onSubmittedWorkDone 完成后停止,不绘制也不写帧控制缓冲。这个 Promise 表示调用前提交的队列工作完成,定义见 GPUQueue.onSubmittedWorkDone。墙钟还包含 API、暂存、调度与通知开销。

有效上传吞吐取纹理载荷除以这段完成时间,单位 MB/s 使用十进制。分子不包含单列的页表字节;零上传报告不适用,不报告无限吞吐。它不是 PCIe、DRAM 或复制引擎硬件带宽。常驻无上传的完成时间记零,仅是这个指标的定义。

渲染轨迹另从冷元数据重放。端到端时间从页选择开始,到上传、绘制及查询结果复制的队列完成通知为止,不含像素复制/映射与时间戳映射。CPU 选择、上传 API 调用、编码提交时间分别保留。GPU 时间戳只覆盖 render pass 起止,不含之前的上传,参见 GPURenderPassTimestampWrites。

每条轨迹预热四对,再测 30 对,奇偶对交替常驻→流送、流送→常驻。共 120 个正式样本,每样本合计 16 帧;上传专用与渲染各重放一次。所有慢样本保留。分位定义为排序后的零基索引 floor(p(n−1)),30 个数的 median 取第 15 个,不平均中间两项;这与第 34、35 篇的线性插值定义不同,不能混算。

轨迹/方案 端到端 p10 / median / p90,ms GPU 渲染合计 median,ms 上传完成合计 median,ms
局部性/常驻 8.4 / 11.6 / 20.9 1.180 0
局部性/流送 8.6 / 11.6 / 20.0 1.376 1.1
抖动/常驻 8.5 / 9.8 / 10.7 1.180 0
抖动/流送 8.6 / 10.2 / 11.9 1.180 4.6

表中每项都是 16 帧合计,不能把约 11 ms 当成单帧耗时。流送/常驻的成对端到端比值中位数分别为 1.076、1.037,p10 到 p90 分别为 0.779 至 1.413、0.892 至 1.264,均跨过 1;这组数据不支持稳定加速结论。

流送有效吞吐中位数为局部性 40.96、抖动 56.99 MB/s。后者吞吐值较大,同时上传更多、完成等待更久,不能据此称抖动更好。各指标先逐样本计算再取分位,不能用两个汇总中位数相除代替吞吐分位。

本次真实回执日期为 2026-10-01,浏览器 Chrome/154,适配器 apple/metal-3,具体设备字段为空。虽然支持 timestamp-query,1920 个正式渲染帧中仍有 1356 个时间差为零;短任务受到计时分辨率影响,零值不证明没有执行成本。CPU 选择时间中位数为零也应按同样边界理解。

独立 rAF 检查每组丢弃四次预热回调,保留 16 个间隔,四组中位数均约 8.3 ms。它等待 GPU 完成和完整读回后才请求下一回调,包含验证与调度开销,既不是显示延迟,也不能取倒数宣称游戏 FPS。

复跑与自检

打开完整实验,保持页面可见,点击“运行完整对照”。页面绘出实际 GPU 回读的最后一页,并可下载完整 JSON。设备丢失、隐藏页面或验证错误都会使本次运行失败。

仓库根目录先生成参考并检查纯逻辑,再启动本地服务:

1
2
3
4
make -C examples/computer-graphics build/streamE08_check
examples/computer-graphics/build/streamE08_check examples/computer-graphics/build
node examples/computer-graphics/gpuE08_check.mjs
python3 -m http.server 8772 --bind 127.0.0.1 --directory examples/computer-graphics

打开 http://127.0.0.1:8772/gpuE08.html。CPU 页数据、请求与上传 CSV、完整 GPU 回执保留复查输入和全部样本。Node 检查器还可接受回执路径,重新核对完整像素、顺序、缓存计数和统计;它验证保存的数据,不会重新执行 GPU。

  1. 容量三足以装下一帧的一页,为什么四页循环仍每次缺页?因为满足瞬时需求不等于满足跨帧复用距离;下次访问前已有另外三页更新为更近使用。
  2. 逻辑纹理少了 81,920 字节,能否报告设备显存减少同样大小?不能。两臂同时存在,资源布局与驱动开销未测量,这只是各策略的逻辑纹理载荷差。
  3. GPU render pass 时间没有包含上传,能否用它判断流送是否拖慢整条路径?不能。需要同时看缺页/字节、上传完成时间和端到端时间;rAF 另含读回开销,也不能替代其中任一项。

系列入口 · 上一篇:三维高斯多视图拟合。这是选修最后一篇,完整系列导航回到入口。