高级 GLSL、UBO 与 std140 对齐

高级 GLSL、UBO 与 std140 对齐:保留 LearnOpenGL 3.3 Core 正文机制,以 context—资源—结果合同、GPU 轨迹和章专属单故障完成可重放验收。

学习目标

  • 能实现基于 gl_FragCoord 的分屏效果,且按 framebuffer 像素分辨率而非 NDC 归一化
  • 能实现接口块匹配、检查 UBO block index,并把多个 program 接到同一 binding point
  • 能计算 std140 的成员起始偏移,区分 vec3 后接 float 与后接 vec2/数组的不同结果
  • 能设计 bindBufferRange 的对齐范围,并检查 UBO 大小与 binding-point 上限
  • 能判断写 gl_FragDepth 是否会妨碍 early depth,并选择无深度写的替代路径

为什么 GLSL 不该靠重复 uniform 传递数据

把渲染想成一条工厂流水线:顶点在前面的工位被摆好位置,到了后面的工位被一个个像素地上色。流水线很贴心——每到一个工位,它都顺手塞给你几块免费的工牌和小仪表:一块告诉你「你现在处理的这个像素,落在画布的哪个格子上」,一块告诉你「你看到的这一面,是物体的正面还是背面」。你不用自己费劲去算,低头看一眼工牌就知道。

可光有工牌还不够。流水线上同时跑着好几台机器(好几个着色器程序),它们经常要用到同一份配料表——比如「相机怎么看这个世界」的那两个矩阵。要是每台机器都自己抄一份、改的时候挨个去改,既费事又容易漏掉一台,画面就花了。更好的办法是在车间墙上挂一块公告板,配料表只写一份贴上去,所有机器都抬头看同一块板,改一次全体同步。

这一章讲的就是这两件事:怎么读流水线发的那些工牌(内建变量),以及怎么用一块公告板(Uniform 缓冲对象)让多台机器共享同一份数据。没有它们,你也能画——只是会写很多本可以省掉的重复代码,还得自己算一堆本该流水线告诉你的信息。

内建变量:流水线免费发的「工牌」

GLSL 里有一批你不用声明、拿来就能用的特殊变量,统称 (built-in variable)——名字都以 gl_ 开头,就像流水线在每个工位发的工牌。有的是输出(你往上写,告诉流水线结果),有的是输入(流水线填好给你读)。先把本章会碰到的几块工牌过一遍:

  • gl_Position(输出):顶点着色器里写的顶点最终位置(裁剪坐标),前面「你好三角形」「坐标系统」章已经天天在用,本章不展开;
  • gl_PointSize(输出):用 GL_POINTS 画点时,控制每个点画多大;
  • gl_VertexID(输入):当前正在处理的是第几个顶点(一个整数索引);
  • gl_FragCoord(输入,本章重点):当前片段在画布上的窗口像素坐标
  • gl_FrontFacing(输入):当前片段属于物体的正面还是背面
  • gl_FragDepth(输出):让你在片段着色器里改写这个片段的深度值

gl_Position / gl_PointSize / gl_VertexID 这三块都很直白,看名字即知用途,下面重点拆三块最有用、也最容易踩坑的:gl_FragCoordgl_FrontFacinggl_FragDepth

gl_FragCoord:这个片段落在屏幕的哪个像素

片段着色器对每个像素各跑一遍。跑的时候,它怎么知道「我正在画的是画布上哪个位置的像素」?答案就是读这块工牌:。它的 xy 就是这个片段的屏幕像素坐标:原点在画布左下角,向右数像素是 x、向上数像素是 yz 分量则是这个片段的深度值。

这里有个小白最容易栽的坑,先用图掐死它:gl_FragCoord.xy像素值——x 从 0 到画布宽、y 从 0 到画布高,不是 -1 到 1 的那种归一化坐标(NDC)。所以想把它变成 0..1 的比例,得除以画布分辨率

最经典的玩法就是拿它做屏幕分屏:判断 gl_FragCoord.x 落在画布左半还是右半,左半涂一色、右半涂另一色——画面立刻被切成两块。本章的主 Demo 就是这个,等会儿你能亲手拖那条分界线。

gl_FrontFacing:你看到的是正面还是背面

还记得「面剔除」章里「正面 / 背面」的概念吗?片段着色器也能查这块工牌:。它是个 bool:当前片段在正面就是 true、在背面就是 false

它有什么用?最直观的就是给正反两面上不同的颜色或纹理。比如一个箱子,从外面看(正面)贴木纹、走进它内部看到的背面贴另一种图,只要一句 gl_FrontFacing ? frontColor : backColor 就分开了。注意:要看得到背面,通常得先把「面剔除」关掉(不然背面早被剔了),这点 §6 会提。

gl_FragDepth:自己改写片段的深度

正常情况下,一个片段的深度由流水线生成。确有特效需要时,可写 覆盖它。

gl_FragDepth 通常会妨碍 GPU 在片段着色器前进行 early depth,因为实现无法预知最终深度;某些保守深度声明或专用布局可保留部分优化,但不能默认指望它。无须改深度时,保留管线生成的深度值才是稳定的默认路径。

接口块:把一组 in/out 打包整组传

着色器章讲过,两段着色器之间靠一对对同名的 in/out 传数据。可当要传的东西变多——纹理坐标、法线、世界坐标……一根根 out、再一根根 in,又长又乱,还容易把名字写错对不上。GLSL 给了个整洁的写法:(interface block)——把一组 inout 装进一个有名字的盒子,整组打包传。

写法是顶点端 out VS_OUT { ... } vs_out;、片段端 in VS_OUT { ... } fs_in;。注意两个名字的分工:VS_OUT块名,两端必须一致(靠它配对);vs_out / fs_in实例名,只是这个块在本段着色器里的代号,两端可以不同。块内用 vs_out.TexCoords 这样的点号访问。下面这张图把「散装」和「打包」并排对照:

接口块不改变数据怎么流(还是顶点 → 片段、还是会被插值),只是把声明组织得更整洁——传的东西越多,它越省心。

Uniform 缓冲对象:墙上那块共享的「公告板」

最后一块大头,也是直觉引入里那块公告板。前面我们传 uniform,都是「一个着色器程序一份、一个一个 gl.uniform* 上传」。可像投影矩阵、视图矩阵这类数据,好几个着色器程序都要用同一份——难道每个程序都各传一遍?改相机时挨个去改?太蠢了。(UBO)就是来解决这个的:把这组共享数据放进一块缓冲,让多个程序都连到它,改一次、全体同步。

它怎么「连到多个程序」?靠一个 (binding point)——可以理解成一个带编号的「插座」。UBO 缓冲插在 0 号插座上,每个要共享它的程序也都声明「我连 0 号插座」,于是它们就连到同一块缓冲了。下面这张图就是公告板的全貌(左右滑看「不用 UBO 各传一遍」和「用 UBO 一处生效」的差别):

还有一个绕不开的细节:UBO 这块缓冲里的数据,CPU 和 GPU 必须遵守同一套摆放方式。这套约定最常用的叫 。关键是“起始对齐”:vec3 必须从 16 的倍数开始,但它后面若是 float,这个 float 可以放进同一个 16B 槽;若后面是 vec2、数组或矩阵,则按它们自己的更高规则向前跳。

动手:用 gl_FragCoord 把画面切成两半

猜一猜:下面这块画布用 gl_FragCoord 判断每个像素落在分界线左边还是右边,左边涂一色、右边涂另一色。如果你把那条分界线一路滑到最右边,画面会变成什么样?是左色铺满、右色铺满,还是没变化?先动手拖一拖,再看下面的解释。

这块画布在片段着色器里读 gl_FragCoord.x、除以画布宽得到「这个像素在水平方向的 0..1 比例」,再和你拖的 uSplit 比较:比它小(在左边)涂左色、比它大(在右边)涂右色,分界线那一道高亮成品牌紫。盯着拖 uSplit 时分界线怎么走:

uSplit 滑到最右边(1),几乎每个像素的 norm 都比它小、都判进「左边」,于是画面几乎被左色铺满;滑到**最左边(0)**则反过来,几乎全是右色;停在 0.5 附近,左右两色各占一半。这整套切割,靠的就是片段着色器从 gl_FragCoord 这块工牌上读到「我是哪个像素」——这正是 gl_FragCoord 最朴素的用法:让每个片段知道自己在屏幕上的位置

UBO 怎么让多个着色器共享同一块数据:单步走一遍

猜一猜:场景里有三个着色器程序,都要用同一份投影 / 视图矩阵。如果不用 UBO,你改一次相机,得往几个程序传矩阵?用了 UBO 之后又变几次?想一想,再单步走下面三步。

下面把「为什么要 UBO、它怎么共享、内存怎么对齐」拆成三步,每一步都画出那一刻的数据怎么流:

分步1 / 3

① 不用 UBO:每个程序各传一遍

不用 UBO 时,每个着色器程序里都各自存一份 projection / view 的 uniform。想改一次相机,CPU 就得挨个程序各上传一遍——三个程序传三遍,五个传五遍,又冗余又容易漏掉某一个,漏了的那个画面就和别人对不上。

走完三步答案就清楚了:不用 UBO 改一次相机要传 N 遍,用了 UBO 只传 1 遍。代价是你得照 std140 的对齐规则把数据摆进缓冲——下面 §6 就把这两端的代码摊开。

代码逐段拆解

把上面用到的东西摊开看:读内建变量、用接口块打包、用 UBO 共享。前两段是纯 GLSL,C++/OpenGL 与 WebGL2 写法完全一样;第三段(UBO 的 CPU 侧)两端 API 差异较大,重点对照。

内建变量:读 gl_FragCoord 做分屏、用 gl_FrontFacing 分正反

片段着色器里直接读这两块工牌即可,不用声明。下面这段把上面 Demo 的分屏逻辑、和「正反面给不同色」拼在一起:

#version 330 core
out vec4 FragColor;
uniform vec2 uResolution;
void main() {
  // gl_FragCoord.xy 是窗口像素坐标(不是 NDC!),除以分辨率归一化
  float norm = gl_FragCoord.x / uResolution.x;
  vec3 col = norm < 0.5 ? vec3(1.0, 0.0, 0.0) : vec3(0.0, 1.0, 0.0);
  // gl_FrontFacing:正面 true / 背面 false,给正反不同色
  if (!gl_FrontFacing) col = col * 0.3;  // 背面压暗
  FragColor = vec4(col, 1.0);
}

两端一字不差——内建变量是 GLSL 语言本身的东西,和桌面 / WebGL2 无关。唯一差异还是老两样:版本声明和精度声明。gl_FragCoord 那行是关键,别当 NDC 用:

接口块:把一组 out / in 打包

顶点端用 out 块名 { ... } 实例名、片段端用同名 in 块名 { ... } 实例名。块名两端一致、实例名可不同。这段也是两端通用的纯 GLSL:

// —— 顶点着色器:打包成一个 out 块 ——
out VS_OUT {
  vec2 TexCoords;
  vec3 Normal;
} vs_out;                 // vs_out 是本段的实例名
void main() {
  // …gl_Position 照常…
  vs_out.TexCoords = aTexCoords;   // 用「实例名.成员」访问
}
// —— 片段着色器:同名 in 块接住 ——
in VS_OUT {
  vec2 TexCoords;
  vec3 Normal;
} fs_in;                  // 块名 VS_OUT 必须一致,实例名可不同
// main 里:texture(tex, fs_in.TexCoords);

接口块只是组织方式的改进——它不改变数据怎么流(顶点 → 片段、照样插值),只在传一大组变量时让命名更整洁、不易把 in/out 名字写岔。

UBO:让两个着色器共享投影 / 视图矩阵

这才是本节重点,分两半:GLSL 端声明 uniform 块(两端通用),和 CPU 端建缓冲 + 接绑定点 + 填数据(API 差异大)。先看 GLSL 端——两段着色器都写同一个 std140 uniform 块:

// 顶点着色器里(两端 GLSL 完全一样)
layout (std140) uniform Matrices {
  mat4 projection;   // 偏移 0:mat4 = 4 列 × 16 字节
  mat4 view;         // 偏移 64:紧接在 projection 之后
};
uniform mat4 model;  // model 各物体不同,仍走普通 uniform
// main 里直接用:gl_Position = projection * view * model * vec4(aPos, 1.0);

注意块里成员直接当全局变量用(不用写实例名前缀)。std140 声明了内存布局——mat4 当 4 个 vec4 算、各占 16 字节,所以 view 紧跟在偏移 64。接着是 CPU 端,这里两端 API 差异最大:

// 1) 建缓冲,开 2 个 mat4 的空间(128 字节)
unsigned int ubo;
glGenBuffers(1, &ubo);
glBindBuffer(GL_UNIFORM_BUFFER, ubo);
glBufferData(GL_UNIFORM_BUFFER, 2 * sizeof(glm::mat4), NULL, GL_STATIC_DRAW);
// 2) 把缓冲接到 0 号绑定点
glBindBufferBase(GL_UNIFORM_BUFFER, 0, ubo);
// 3) 把每个程序的 Matrices 块也指到 0 号绑定点,并检查块存在
unsigned int idx = glGetUniformBlockIndex(shaderA, "Matrices");
if (idx == GL_INVALID_INDEX) throw std::runtime_error("Matrices missing");
glUniformBlockBinding(shaderA, idx, 0);   // 对 shaderB 同样来一遍
// 4) 按 std140 偏移填数据:projection 在 0、view 在 64
glBindBuffer(GL_UNIFORM_BUFFER, ubo);
glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(glm::mat4), &projection[0][0]);
glBufferSubData(GL_UNIFORM_BUFFER, 64, sizeof(glm::mat4), &view[0][0]);

四步逻辑一一对应:建缓冲 → 缓冲接 0 号绑定点(bindBufferBase)→ 每个程序的块也指 0 号绑定点(先检查 block index)→ 按偏移填数据。bindBufferBase 绑定整只 buffer;若要复用一只大 buffer 的子范围,使用 bindBufferRange,其 offset 必须满足 UNIFORM_BUFFER_OFFSET_ALIGNMENT。开始前也应查询 MAX_UNIFORM_BLOCK_SIZEMAX_UNIFORM_BUFFER_BINDINGS,避免跨设备超限。

写好后,CPU 每帧(或相机一变)只往这块缓冲 bufferSubData 写一次 projection / view,连着 0 号绑定点的所有着色器程序就全都更新了——这正是 §5 第②步那张图的实况。完整可运行源码将在章末提供(M5)。

容易踩的坑

小结

  • 内建变量是流水线发的「免费工牌」:gl_FragCoord(片段的窗口像素坐标)、gl_FrontFacing(正/背面)、gl_FragDepth(改写深度,慎用)
  • gl_FragCoord.xy像素值不是 NDC,要除以 uResolution 才归一化到 0..1——这是 §5 分屏 Demo 的关键
  • 接口块把一组 in/out 打包成有名字的块整组传:块名两端一致、实例名可不同,传一大组数据时更整洁
  • UBO 是块共享「公告板」:一块缓冲经绑定点接到多个着色器程序,改一次全体生效,省掉逐个程序各传一遍
  • std140 按成员起始对齐排布:vec3 从 16B 边界开始,后接 float 可复用第 4 槽,数组/矩阵有至少 16B 步长;绑定点与范围偏移都要核对

练习

问题 1(改 Demo 代码题) 在上面的 gl_FragCoord 分屏 Demo 里,把「左右分屏」改成「上下分屏」:画面上半一色、下半另一色,分界线水平、可由 uSplit 控制高低。提示:换一个 gl_FragCoord 的分量、除以对应的分辨率分量。

问题 2(std140 对齐题) 一个 UBO 块按 std140 布局,依次有:float a; vec3 b; vec2 c;。写出 abc 三个成员各自的起始字节偏移,并说明为什么 b 不是从偏移 4 开始。

问题 3(问答题) 某人用 UBO 共享投影 / 视图矩阵,CPU 侧写了 glBindBufferBase(GL_UNIFORM_BUFFER, 0, ubo),又对着色器写了 glUniformBlockBinding(shader, idx, 1)。运行后画面里物体位置完全不对。问题出在哪?怎么改?

名词解释

本章出现的专业名词,用大白话再讲一遍。

内建变量

GLSL 预先定义好、有固定含义、不用你声明就能直接用的特殊变量,名字都以 gl_ 开头。它们由渲染流水线在对应阶段自动填好——比如顶点的最终位置 gl_Position、片段的屏幕坐标 gl_FragCoord、正反面标志 gl_FrontFacing。读对应的「工牌」就能拿到流水线算好的信息,不必自己算。详见本章「内建变量」一节。

gl_FragCoord

片段着色器里的只读内建变量,存当前片段在画布上的「窗口像素坐标」。xy 是像素位置(原点在画布左下角,向右是 x、向上是 y),z 是这个片段的深度。关键xy 是像素值(x 从 0 到画布宽、y 从 0 到画布高),不是 -1..1 的 NDC,要除以分辨率才能归一化到 0..1。详见本章「gl_FragCoord」一节与 §5。

gl_FrontFacing

片段着色器里的只读布尔内建变量,告诉你当前片段属于物体的正面true)还是背面false)。常用来给正反两面上不同的颜色或纹理(从外面看一种、看到背面换另一种)。判正反和「面剔除」同一套环绕顺序规则;想看到背面通常得先关掉面剔除。详见本章「gl_FrontFacing」一节。

gl_FragDepth

片段着色器里的可写深度输出,覆盖管线生成的片段深度。动态写入通常会妨碍 early depth,因为实现无法预知最终值;某些保守深度限定是例外,但需在目标平台实测。详见本章「gl_FragDepth」一节。

接口块

把一组要在着色器之间传递的 inout 变量,用一个有名字的「块」打包起来整组传,而不是一个个零散声明。顶点端写 out 块名 { ... } 实例名、片段端写同名 in 块名 { ... } 实例名块名两端必须一致(靠它配对),实例名(块在本段里的代号)两端可以不同。传一大组数据时命名整洁、不易写错。详见本章「接口块」一节。

Uniform 缓冲对象

一块能被多个着色器程序共享的 uniform 数据缓冲(英文 Uniform Buffer Object,简称 UBO)。把一组常用 uniform(如投影矩阵、视图矩阵)放进这一块缓冲,经一个「绑定点」同时接到多个程序上;改一次缓冲,所有连着它的程序立刻都读到新值,不必逐个程序各传一遍。详见本章「Uniform 缓冲对象」一节与 §5、§6。

绑定点

一个带编号的「插座」。UBO 缓冲插在这个编号上,多个着色器程序也都声明连到同一个编号,于是它们就共享了这块缓冲。CPU 侧用 glBindBufferBase / glBindBufferRange 把缓冲接到某个编号,再用 glUniformBlockBinding 把每个程序里的 uniform 块也指到同一个编号——编号对齐,连接才成立;编号不一致就会读到错数据。详见本章「Uniform 缓冲对象」一节与 §6。

std140 布局

UBO 缓冲里成员怎么摆放(每个成员从第几个字节开始)的一套对齐规则。核心:成员要按各自的「对齐量」对齐——标量(float/int)对齐 4 字节、vec2 对齐 8、vec3vec4起始位置对齐 16;数组和结构体的步长至少 16,mat4 当成 4 个 vec4vec3 后接 float 时,该 float 可复用第 4 个槽;后接 vec2/数组/矩阵则按其对齐量向前跳。CPU 往缓冲写数据时必须照真实偏移摆,GPU 才能读对。详见本章「Uniform 缓冲对象」一节与 §6。

版本、来源与运行边界

本章以 Joey de Vries 的 LearnOpenGL 原章 为授权改编依据,教学运行基线是 OpenGL 3.3 Core Profile。Khronos 当前发布的规范参照是 OpenGL 4.6 Core Profile;这里用 4.6 规范核查术语和状态合同,但不把 4.6 API 偷偷倒填为原教程内容。GLFW、GLAD、Assimp 与驱动版本都属于运行环境,不能拿“编译通过”替代对 context、资源和 framebuffer 结果的验证。

正式概念与状态责任

  • gl_pointsize:在“高级 GLSL、UBO 与 std140 对齐”中由shader interface、uniform block 与绑定点负责解释其输入、受控状态和可观察结果;运行时以active block、uniform offsets/strides、binding、buffer bytes 与多 program 输出定位它的第一处变化。
  • interface block:在“高级 GLSL、UBO 与 std140 对齐”中由shader interface、uniform block 与绑定点负责解释其输入、受控状态和可观察结果;运行时以active block、uniform offsets/strides、binding、buffer bytes 与多 program 输出定位它的第一处变化。
  • uniform buffer object:在“高级 GLSL、UBO 与 std140 对齐”中由shader interface、uniform block 与绑定点负责解释其输入、受控状态和可观察结果;运行时以active block、uniform offsets/strides、binding、buffer bytes 与多 program 输出定位它的第一处变化。
  • std140:在“高级 GLSL、UBO 与 std140 对齐”中由shader interface、uniform block 与绑定点负责解释其输入、受控状态和可观察结果;运行时以active block、uniform offsets/strides、binding、buffer bytes 与多 program 输出定位它的第一处变化。

章专属 OpenGL 状态实验

先预测“查询 block,绑定到同一 UBO binding point,按规范 offset 写入矩阵”发生后,shader interface、uniform block 与绑定点应怎样改变block index/binding、std140 offset/stride、CPU 字节布局和多个 program 的引用;再操作三个实验。实验不生成变化率或正确率等虚构总分,只显示真实 GL 状态、资源、命令和可观察结果。

实验一:Context—资源—结果合同

选择任一正式概念与基线/单故障场景,核对它是否进入本章状态合同。正式概念只有同时出现在解释、可视状态和交付证据中才算覆盖。

Context · resource · observable result

高级 GLSL、UBO 与 std140 对齐:状态合同

验证 GLSL 接口块、gl_PointSize、内建变量与 std140 UBO 的跨 program 共享布局

验证场景

官方教程正式概念

logl-25 · 基线帧

gl_pointsize固定 context、资源内容与输入事件,执行“查询 block,绑定到同一 UBO binding point,按规范 offset 写入矩阵”

状态所有者shader interface、uniform block 与绑定点
受控状态/资源block index/binding、std140 offset/stride、CPU 字节布局和多个 program 的引用
触发命令查询 block,绑定到同一 UBO binding point,按规范 offset 写入矩阵

冻结输入:gl_pointsize

shader interface、uniform block 与绑定点记录block index/binding、std140 offset/stride、CPU 字节布局和多个 program 的引用

结果:得到可重复的初始 GL 状态与资源身份

观测:active block、uniform offsets/strides、binding、buffer bytes 与多 program 输出中的初始快照

预期:shader interface、uniform block 与绑定点得到可复查结果,并持续满足“CPU 写入 offset 来自 std140 规则或驱动查询,不假定 C++ struct 紧密布局”

实验二:CPU 命令到 GPU 结果的五段轨迹

逐段执行“查询 block,绑定到同一 UBO binding point,按规范 offset 写入矩阵”,在每一步记录资源身份、状态变化与第一个可观察结果,并持续核对“CPU 写入 offset 来自 std140 规则或驱动查询,不假定 C++ struct 紧密布局”。

CPU command · GL state · GPU result

高级 GLSL、UBO 与 std140 对齐:五段轨迹

选择一段命令—资源—结果1 / 5

当前观测:active block、uniform offsets/strides、binding、buffer bytes 与多 program 输出中的初始快照

不变量:CPU 写入 offset 来自 std140 规则或驱动查询,不假定 C++ struct 紧密布局

实验三:单故障与同输入恢复

注入“把 vec3 后的 float 写在 offset 12,但 std140 中下一成员按 16 字节边界开始”,保存首个分岔;撤销后沿用完全相同的 context、资源内容、uniform 和 draw 输入重放。只有active block、uniform offsets/strides、binding、buffer bytes 与多 program 输出一起恢复才算修复。

Single fault · first divergence · replay

高级 GLSL、UBO 与 std140 对齐:反例与恢复

故障:把 vec3 后的 float 写在 offset 12,但 std140 中下一成员按 16 字节边界开始

1. 冻结输入一致

第 1 次沿用同一 context、资源、uniform 与 draw 输入

2. 注入单故障一致

保持其余输入不变,仅注入“把 vec3 后的 float 写在 offset 12,但 std140 中下一成员按 16 字节边界开始”

3. 定位首差一致

CPU 写入 offset 来自 std140 规则或驱动查询,不假定 C++ struct 紧密布局

4. 清理并重放一致

active block、uniform offsets/strides、binding、buffer bytes 与多 program 输出

最小可重放检查

unit: logl-25
owner: shader interface、uniform block 与绑定点
state_or_resource: block index/binding、std140 offset/stride、CPU 字节布局和多个 program 的引用
command: 查询 block,绑定到同一 UBO binding point,按规范 offset 写入矩阵
pass_invariant: CPU 写入 offset 来自 std140 规则或驱动查询,不假定 C++ struct 紧密布局
single_fault: 把 vec3 后的 float 写在 offset 12,但 std140 中下一成员按 16 字节边界开始
required_evidence: active block、uniform offsets/strides、binding、buffer bytes 与多 program 输出

复核者先仅依据以上合同写出预期,再运行基线、单故障和清理后重放。若两次基线的资源身份、首个状态变化或 framebuffer 结果不同,必须保留差异,不能用最终截图相似掩盖中间状态错误。

出处声明

本文为改编重写,改编自 Joey de Vries 的 LearnOpenGL。原文:learnopengl.com

原作及译作以 CC BY-NC 4.0 协议授权,本改编版同样遵循该协议(署名—非商业性使用)。

讨论

评论区加载中…