计算机图形学 E08:三页缓存怎样显示八页纹理
一组纹理有八页,画面每次只用其中一页,GPU 是否需要始终保存全部八页?可以只留三个槽,缺哪页就上传哪页。但省下的容量要用上传和调度来交换;相机不断回来访问刚被淘汰的页时,这种交换可能很差。
本篇只研究纹理页流送。沿用第 25、26 篇的上传、绘制与读回方法,以及第 34、35 篇的固定输入、画质前置检查和重复测量,不扩建完整引擎。全部源页已在 CPU 内存中,因此实验不包括磁盘、网络、图片解码或压缩纹理转码。
先固定画面需要哪些字节
实验生成八张 64×64 的 RGBA8 页,每页 16,384 字节,透明度固定为 255。RGB 由页号与整数像素坐标生成,颜色只作为诊断码值,没有光照、过滤、mip、混合或额外的颜色转换。
把八页视为一条虚拟纹理带,相机每帧停在一个整数页中心,输出正好是这一页的 64×64 像素。这个“相机”只是确定页号的有界模型,没有透视变换或跨页过滤。两条轨迹都只访问前四页:
1 | |
两条路径的着色器相同,均用页表查出 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 页缓存分别需要:
缓存的逻辑纹理载荷是常驻的 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 | |
打开 http://127.0.0.1:8772/gpuE08.html。CPU 页数据、请求与上传 CSV、完整 GPU 回执保留复查输入和全部样本。Node 检查器还可接受回执路径,重新核对完整像素、顺序、缓存计数和统计;它验证保存的数据,不会重新执行 GPU。
- 容量三足以装下一帧的一页,为什么四页循环仍每次缺页?因为满足瞬时需求不等于满足跨帧复用距离;下次访问前已有另外三页更新为更近使用。
- 逻辑纹理少了 81,920 字节,能否报告设备显存减少同样大小?不能。两臂同时存在,资源布局与驱动开销未测量,这只是各策略的逻辑纹理载荷差。
- GPU render pass 时间没有包含上传,能否用它判断流送是否拖慢整条路径?不能。需要同时看缺页/字节、上传完成时间和端到端时间;rAF 另含读回开销,也不能替代其中任一项。
系列入口 · 上一篇:三维高斯多视图拟合。这是选修最后一篇,完整系列导航回到入口。







