Chapter 85: Debugging and Extensions
OpenGL 调试的主问题是:当一帧出现黑屏、错误纹理、深度异常、blend 结果异常或状态泄漏时,如何把视觉现象追踪回 API 调用、对象状态、shader 输入输出、扩展能力和驱动消息。读完本章后,读者应能建立一条可复用的证据链:先让 OpenGL 自己产生错误信号,再给对象和 pass 加上可读标签,再用 frame debugger 检查 draw state,最终根据扩展能力决定修复路径和 fallback 设计。
本章使用一个贯穿案例:一个旧 OpenGL renderer 在资源系统重构后,主模型从正常贴图变成黑色。场景里有一个 geometry pass、一个 lighting pass 和一个 UI pass;模型使用 sampler2DArray,深度写入打开,blend 在 UI pass 中打开。代码层面没有崩溃,swap buffer 也正常完成。这个案例适合串起本章的所有对象:glGetError 能给出基础错误枚举,debug output 能提供带 source、type、severity、id 的消息,glObjectLabel 和 debug group 能把数字对象名映射回工程语义,extension 查询能判断当前 context 是否具备所需能力,RenderDoc 或 GLIntercept 能把单帧状态展开成可检查证据。
OpenGL 的困难来自状态机模型。一次 draw call 的结果由当前 context 中的 program、VAO、buffer binding、texture unit、sampler、framebuffer、viewport、depth、stencil、blend、cull、scissor 等状态共同决定。视觉结果异常时,错误来源可能在 draw call 之前几十个 API 调用中产生。调试流程要把“看起来黑了”转换成“哪个 pass 的哪个 draw 在什么输入状态下产生了什么输出”。
本章的结论可以先明确:OpenGL 调试应以 debug output 和 frame capture 为主线,以 glGetError 作为局部哨兵,以 extension profile 作为能力边界,以 object label 和 debug group 作为定位索引。这样处理后,OpenGL 的隐式状态会被整理成可观察的 frame、pass、draw、resource 和 message。
85.1 OpenGL 调试机制与错误查询机制
OpenGL 的错误查询机制负责回答一个局部问题:从上一次查询到现在,当前 context 是否记录了可检测的 API 错误。glGetError 返回并清除一个错误标志;当多个错误标志存在时,应循环调用直到返回 GL_NO_ERROR,这个行为可在 docs.gl 的 glGetError 页面 中查到。它适合作为小范围断点,例如资源创建、framebuffer 组装、shader link、draw call 前后的哨兵。
glGetError 的能力边界同样清楚。它返回的是 GL_INVALID_ENUM、GL_INVALID_VALUE、GL_INVALID_OPERATION、GL_INVALID_FRAMEBUFFER_OPERATION、GL_OUT_OF_MEMORY 等枚举,不携带工程对象名、pass 名称、调用栈、driver 解释或性能提示。对于本章的黑屏案例,如果 draw 之后只看到 GL_INVALID_OPERATION,读者仍需回答:这个错误来自绑定了错误 texture target,来自 program 与 sampler 类型不匹配,来自 framebuffer 状态,还是来自上一轮 pass 的状态泄漏。
基础错误哨兵可以写得很短。这个函数的目的只是把 OpenGL 错误从“沉默状态”转成日志记录,位置应放在可疑区间边界,保持低频、明确、可定位。
static void DrainGlErrors(const char* scope) {
for (;;) {
GLenum error = glGetError();
if (error == GL_NO_ERROR) {
break;
}
LogError("GL error in %s: 0x%04x", scope, error);
}
}
DrainGlErrors("before geometry pass");
DrawMainMesh();
DrainGlErrors("after geometry pass");
这段代码证明的判断是:glGetError 适合缩小时间区间。若 before geometry pass 干净而 after geometry pass 出现 GL_INVALID_OPERATION,错误就集中在 geometry pass 的 draw 准备和 draw 执行阶段。它仍然缺少对象语义,所以后续要引入 debug output、label 和 frame capture。
Debug output 解决的是错误信号的表达问题。Khronos OpenGL Wiki 的 Debug Output 页面把它描述为 driver 向应用返回文本消息、应用插入自定义消息、给 GL 对象添加可读名字的机制。它在 OpenGL 4.3 进入 core,也可通过 KHR_debug 等扩展获得。使用时通常需要 debug context,并开启 GL_DEBUG_OUTPUT;需要把消息尽量绑定到原始调用栈时,再打开 GL_DEBUG_OUTPUT_SYNCHRONOUS。
一个实用的初始化路径如下。代码重点是把 driver 消息变成带严重性、类型和来源的日志,同时把对象标签和 debug group 接入工程 pass。
static void GLAPIENTRY OnGlDebugMessage(
GLenum source,
GLenum type,
GLuint id,
GLenum severity,
GLsizei length,
const GLchar* message,
const void* userParam) {
LogGlMessage(source, type, id, severity, std::string(message, length));
}
void EnableGlDebugOutput() {
glEnable(GL_DEBUG_OUTPUT);
glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
glDebugMessageCallback(OnGlDebugMessage, nullptr);
glDebugMessageControl(
GL_DONT_CARE,
GL_DONT_CARE,
GL_DEBUG_SEVERITY_NOTIFICATION,
0,
nullptr,
GL_FALSE);
}
void LabelSceneObjects(GLuint meshVao, GLuint albedoArray, GLuint geometryFbo) {
glObjectLabel(GL_VERTEX_ARRAY, meshVao, -1, "main mesh vao");
glObjectLabel(GL_TEXTURE, albedoArray, -1, "main albedo texture array");
glObjectLabel(GL_FRAMEBUFFER, geometryFbo, -1, "geometry gbuffer fbo");
}
glDebugMessageCallback 注册回调,消息会携带 source、type、id、severity 和字符串内容;glObjectLabel 给 buffer、shader、program、VAO、texture、framebuffer 等对象添加可读标签;glPushDebugGroup 和 glPopDebugGroup 把一组命令标记为一个 pass 或一次材质提交。这些接口在 docs.gl 的 glDebugMessageCallback 页面、glObjectLabel 页面 和 glPushDebugGroup 页面 中有参数定义和版本边界。
对本章黑屏案例,正确的第一步是把 geometry pass、lighting pass 和 UI pass 都包进 debug group,并给 texture、framebuffer、program、VAO 打 label。这样一条 driver 消息可以从“纹理绑定操作非法”升级为“geometry pass 中 main albedo texture array 与当前 program 的 sampler 期望冲突”。OpenGL 的数字对象名因此进入工程语义空间。
85.2 Debug Callback as Error Signal Path
Debug callback 应被理解成错误信号路径。它的输入来自 OpenGL implementation、shader compiler、window system、application 或 third party;输出是应用侧的日志、断点、统计计数和调试 UI。这个路径回答的问题是:错误、性能警告、deprecated behavior、undefined behavior 和自定义 marker 如何从 driver 传播到 renderer 的诊断系统。
回调消息的五个字段各自承担不同判断功能。source 说明消息来自 API、shader compiler、window system、application 还是第三方拦截层;type 说明它是 error、performance、portability、deprecated behavior、undefined behavior、marker、push group 或 pop group;severity 决定处理优先级;id 适合做过滤和统计;message 提供 driver 文本。OpenGL Wiki 对这些字段和同步回调规则有集中说明,尤其指出同步 debug output 会在同一 context 线程和触发消息的 GL 调用范围内调用回调。
工程上应把 debug callback 的输出归入四类处理。高严重性 error 进入断点或测试失败;medium 级 shader 编译、link 或 major performance warning 进入构建日志和性能记录;low 级冗余状态变化用于优化 state cache;notification 级 marker 和 debug group 用于串联 pass。这样分类后,callback 输出会变成 renderer 的诊断总线,日志也会保持可分类、可过滤、可统计。
下图展示了本章贯穿案例中 debug message 从 GL 调用传播到定位结论的路径。图的边界是单个 context 内的一帧;它关注错误信号和工程语义的结合,不覆盖 GPU vendor 私有 profiler 计数器。
图中最关键的路径是 Draw 调用 → OpenGL 状态校验 → Debug callback → Pass 与对象标签 → Frame capture 复查。callback 给出错误信号,debug group 给出 pass 范围,object label 给出资源语义,frame capture 给出完整 draw state。四者合并后,黑屏就从视觉现象变成可复核的状态差异。
回调函数内部应保持轻量。同步回调处在 GL 调用范围内,适合记录消息、触发断点、写入 lock-free 队列或设置诊断标志。复杂资源查询、窗口系统调用、重新进入 GL API 的操作会扩大时序风险。异步回调的线程和顺序约束更弱,日志系统要能接受消息乱序和跨线程进入。稳定做法是把回调当成信号采集点,把重分析放到帧尾或工具侧完成。
消息过滤也应成为工程策略。glDebugMessageControl 可以按 source、type、severity 或具体 id 打开和关闭消息。开发阶段可以保留 error、undefined behavior、portability 和 medium 以上 performance warning;大规模自动测试可以提升过滤强度,只把高严重性错误转成失败。对于驱动长期输出的低价值 notification,应按 id 记录白名单和说明,防止日志量遮蔽真实错误。
本章案例中,如果 callback 收到 GL_DEBUG_TYPE_ERROR 且 severity 为 high,日志中同时包含 geometry pass 和 main albedo texture array,排查顺序就应从 geometry pass 的 texture binding、sampler uniform、program link 状态和 texture target 开始。若消息来源为 shader compiler,重点转向 shader 编译和 link log;若类型为 performance,重点转向 redundant state、texture format、sync 或 slow path。
85.3 ARB and EXT Extensions as Capability Boundaries
OpenGL extension 是能力边界。Khronos OpenGL Wiki 的 OpenGL Extension 页面说明,extension 用来让 OpenGL implementation 提供 core 尚未包含的新功能或扩展功能,并且可由单个 vendor 或多个 implementation 支持。对工程而言,ARB、EXT、KHR、NV、AMD、APPLE 等前缀的主要价值是提示标准化程度、平台覆盖范围和 fallback 风险。
ARB 通常表示经过 OpenGL Architecture Review Board 路径标准化的扩展,很多 core 功能曾以 ARB 形式出现。EXT 通常表示多个实现共享的扩展,标准化程度和行为边界需阅读对应 extension spec。KHR 与 Khronos 维护的跨 API 或现代扩展生态关系更紧,例如 KHR_debug。Vendor 前缀表示某个硬件或驱动生态暴露的能力,使用时要把它放进平台 profile 和 fallback 表,draw path 读取归一化后的能力结果。
能力查询要分成三层。第一层查询 OpenGL version、profile、vendor 和 renderer,用于建立运行时环境记录。第二层查询 extension 字符串和 loader function pointer,用于确认功能入口存在。第三层查询 limits、formats、feature-specific state,用于确认参数规模、格式支持和资源上限。只有 extension 字符串存在还不足以说明路径安全,函数入口和具体 limits 同样要进入 profile。
一个最小的 extension profile 可以这样组织。代码示例的目标是把“当前机器是否具备某条路径能力”转成结构化数据,供渲染管线和 fallback 系统读取。
struct GlFeatureProfile {
int major = 0;
int minor = 0;
std::string vendor;
std::string renderer;
std::unordered_set<std::string> extensions;
bool hasDebugOutput = false;
bool hasTextureStorage = false;
bool hasTextureView = false;
bool hasDirectStateAccess = false;
};
static std::unordered_set<std::string> CollectExtensions() {
GLint count = 0;
glGetIntegerv(GL_NUM_EXTENSIONS, &count);
std::unordered_set<std::string> result;
for (GLint index = 0; index < count; ++index) {
const char* name = reinterpret_cast<const char*>(glGetStringi(GL_EXTENSIONS, index));
if (name != nullptr) {
result.insert(name);
}
}
return result;
}
GlFeatureProfile BuildGlFeatureProfile() {
GlFeatureProfile profile;
glGetIntegerv(GL_MAJOR_VERSION, &profile.major);
glGetIntegerv(GL_MINOR_VERSION, &profile.minor);
profile.vendor = reinterpret_cast<const char*>(glGetString(GL_VENDOR));
profile.renderer = reinterpret_cast<const char*>(glGetString(GL_RENDERER));
profile.extensions = CollectExtensions();
auto has = [&](const char* name) {
return profile.extensions.contains(name);
};
profile.hasDebugOutput = profile.major > 4 ||
(profile.major == 4 && profile.minor >= 3) || has("GL_KHR_debug");
profile.hasTextureStorage = profile.major > 4 ||
(profile.major == 4 && profile.minor >= 2) || has("GL_ARB_texture_storage");
profile.hasTextureView = profile.major > 4 ||
(profile.major == 4 && profile.minor >= 3) || has("GL_ARB_texture_view");
profile.hasDirectStateAccess = profile.major > 4 ||
(profile.major == 4 && profile.minor >= 5) || has("GL_ARB_direct_state_access");
return profile;
}
这段代码体现了三个判断。版本号可以让一部分 extension 功能进入 core 路径;extension 名称可以打开额外路径;最终仍要通过函数加载器绑定入口,并在使用前查询相关 limits。对于 core profile,glGetStringi(GL_EXTENSIONS, index) 是更稳定的扩展枚举方式;旧 compatibility 路径可能还会遇到空格分隔的 glGetString(GL_EXTENSIONS)。
对本章黑屏案例,extension profile 能回答两个问题。第一,debug output 是否可用,决定使用 callback 还是退回 glGetError 与工具捕获。第二,texture array、texture storage、texture view 或 sampler object 是否可用,决定资源系统能否使用统一 texture array 路径。如果某平台缺少相关扩展,fallback 应在资源创建阶段切换到普通 sampler2D 或 atlas 路径,并把材质变体、shader define 和绑定布局同时切换。
扩展的工程风险来自“功能存在”和“行为适合”之间的距离。某个 extension 可能只在特定 profile、特定 driver 版本或特定 format 上可用;某些功能在移动、WebGL、macOS OpenGL、旧 Windows 驱动和 Mesa 驱动中的可用性差异很大。稳定的 renderer 会把 extension 决策集中在 feature profile、capability table 和 fallback path 中,draw code 只读取已经归一化的能力结果。
85.4 RenderDoc and GLIntercept Debugging Workflow
Frame debugger 负责回答 glGetError 和 callback 无法独立回答的问题:当前帧的某个 draw call 具体绑定了哪些 buffer、texture、sampler、shader、uniform、framebuffer、depth/blend state,并产生了什么中间图像。RenderDoc 是常用的开源 frame debugger,官方资料和项目文档说明它可以捕获 OpenGL、OpenGL ES、Vulkan、D3D 等 API 的单帧并检查 pipeline state、resources、textures、mesh 和 shader 相关信息;GLIntercept 则是 Windows 上的 OpenGL function call interceptor,适合需要记录 API 调用序列的旧项目或工具链。
对本章案例,RenderDoc 工作流应从捕获“黑屏那一帧”开始。打开 capture 后,不要先在代码中猜测;应在 Event Browser 中定位 geometry pass 的主模型 draw,检查该 draw 的 framebuffer attachment、viewport、scissor、depth test、blend state、program、VAO、index buffer、vertex attributes、texture bindings 和 sampler state。每一项都要对应一个可能导致黑屏的因果路径。
检查 draw state 时,先确认输出目标。Framebuffer attachment 如果缺少 color attachment、draw buffer 指向错误 attachment、viewport 尺寸为零、scissor 裁掉全部区域,fragment shader 即便输出颜色也无法出现在目标纹理中。深度状态如果沿用 shadow pass 的 depth func 或 depth mask,主模型可能被全部丢弃。blend state 如果从 UI pass 泄漏到 geometry pass,并且 color mask 或 blend func 异常,输出也会变成透明或黑色。
再检查输入资源。VAO 中 position、normal、UV、instance attribute 的 stride、offset、format 和 divisor 应与 vertex shader 输入匹配。Texture viewer 应看到 main albedo texture array 的内容、format、mip level 和 array layer。Shader resource 面板应确认 sampler uniform 指向正确 texture unit,sampler 类型与 GLSL 声明一致。若 shader 声明 sampler2DArray,但绑定的是 GL_TEXTURE_2D 或普通 sampler 状态,RenderDoc 中通常能看到 target、view 或 binding 与预期分离。
接着检查 shader 输入输出。Mesh viewer 可以验证顶点是否进入屏幕空间,UV 和 layer index 是否有合理范围。Shader viewer 或 pipeline state 可以确认 program link 状态、uniform 数值、texture unit 映射和 fragment output。若 mesh viewer 显示模型在屏幕外,问题在 vertex transform;若 mesh 正常但 fragment output 黑,问题更可能在 texture sample、material 参数、normal/light 输入或 color space;若 fragment output 有颜色但 render target 黑,问题转向 depth/blend/write mask/framebuffer。
GLIntercept 的位置不同。它更像调用序列记录器和拦截层,适合回答“哪一段代码按什么顺序调用了 OpenGL”。在旧 Windows OpenGL 项目中,如果 RenderDoc capture 受到注入、context 或驱动限制,GLIntercept 可以记录 API 调用、参数、shader、texture 和错误日志,帮助把状态泄漏定位到具体调用顺序。它的限制也应明确:它的生态和平台边界较窄,现代项目通常优先使用 RenderDoc,再把 GLIntercept 作为特定旧项目的补充工具。
一条完整 frame debugging workflow 可以压缩成五步:定位异常 draw,检查输出目标,检查输入资源,检查 shader I/O,回到 API 调用序列。这个顺序的好处是把黑屏问题拆成可排除的渲染管线阶段。读者在工具中看到的每个状态都要回到工程对象:哪个 pass 设置它,哪个 wrapper 持有它,哪个 resource system 创建它,哪个 feature profile 决定它可用。
85.5 定位复杂渲染错误的调试流程
复杂渲染错误的定位流程应从视觉症状进入管线阶段,再从管线阶段进入具体状态。黑屏、错误纹理、深度异常、blend 错误和状态泄漏看起来都可能是“画面异常”,但它们对应的优先检查点不同。稳定流程的目标是缩小搜索空间,让 shader、资源和 API 状态按阶段接受检查。
黑屏问题先分清“没有 draw”“draw 无覆盖”“fragment 被丢弃”“输出被覆盖”四类。没有 draw 时,Event Browser 中 draw count、instance count 或 index count 可能为零。draw 无覆盖时,mesh viewer 会显示模型在视锥外、viewport 尺寸异常或 scissor 裁切异常。fragment 被丢弃时,depth/stencil/cull/discard/alpha test 风格逻辑是重点。输出被覆盖时,render target 顺序、clear、blend、color mask 和后续 pass 会成为证据。
错误纹理问题先检查 resource identity,再检查 sample path。resource identity 包括 texture object、target、view、format、mip、array layer 和绑定 texture unit;sample path 包括 sampler type、sampler object、wrap/filter、LOD、shader uniform 和 UV 范围。本章案例中,sampler2DArray 的 layer index 如果来自错误 vertex attribute,texture viewer 里资源内容正常,fragment shader 仍可能采样到错误 layer 或返回边界颜色。
深度异常要按写入、比较、范围和清理四个对象检查。写入检查 depth attachment、depth mask 和 depth format;比较检查 depth func、reversed-Z 约定和 projection matrix;范围检查 near/far、clip control、viewport depth range 和 NDC 深度映射;清理检查 clear value 和 pass 顺序。如果 geometry pass 的 depth func 从 shadow pass 泄漏,主模型可能在同一帧内被全部裁掉。
Blend 错误要同时看 render target format、color space、premultiplied alpha 约定、blend func、blend equation 和 color write mask。UI pass 中常见的 blend state 如果泄漏到不透明 geometry pass,G-buffer 或 forward target 的颜色可能被异常混合。这里的证据通常在 RenderDoc pipeline state、render target history 和 draw 前后 target inspection 中出现。
状态泄漏是 OpenGL 工程中最常见的复杂错误之一。它的典型形态是某个 pass 修改了全局状态,后续 pass 沿用该状态产生异常。处理它的稳定策略是把每个 pass 的入口状态定义为显式 contract:program、VAO、framebuffer、viewport、scissor、depth、stencil、blend、cull、color mask、active texture、bound textures、samplers 都由 pass setup 写完整。State cache 可以减少重复提交,但 pass contract 要能在 debug build 中被验证。
下面的排查顺序可以作为本章黑屏案例的收束流程:
- 记录症状:截取异常帧,标记预期输出和实际输出。
- 打开信号:启用 debug context、debug callback、object label 和 debug group。
- 定位 draw:在 RenderDoc 中找到产生主模型的 draw call。
- 检查输出:确认 framebuffer、viewport、scissor、depth、blend 和 color mask。
- 检查输入:确认 VAO、buffer、texture、sampler、uniform 和 shader program。
- 对照能力:读取 feature profile,确认 texture array、debug output 和相关 extension 可用。
- 最小复现:保留一个 mesh、一个 material、一个 texture、一个 pass,验证修复是否稳定。
- 固化规则:把问题对应的状态 contract、capability check 或 debug assertion 写进 renderer。
这个流程的关键是每一步都产生可保存证据。日志记录 callback message;工具保存 capture;feature profile 保存 vendor、renderer、version 和 extension;最小复现保存输入资源;修复提交保存状态 contract 或 fallback 逻辑。复杂 OpenGL bug 的修复质量,取决于这些证据能否解释同类问题,并能支持跨平台复查。
对本章贯穿案例,最终可能得到这样的结论:debug callback 报告 geometry pass 中存在 texture binding 相关 high severity error;RenderDoc 显示 shader 期望 sampler2DArray,但资源系统在某个平台 fallback 后仍绑定 GL_TEXTURE_2D;feature profile 显示 texture array 路径未被统一封装;修复方案是把材质变体、texture target、sampler uniform 和 resource creation 绑定到同一个 capability branch,并在 pass 入口加入 debug assertion。这样,黑屏从一次偶发画面问题变成资源能力与状态 contract 的设计问题。
最小自检任务
给定一个 OpenGL renderer:模型在 geometry pass 中变成黑色,debug callback 输出一条 high severity API error;RenderDoc 中该 draw 的 framebuffer、viewport、depth test 都正常,mesh viewer 显示顶点和 UV 正常;texture viewer 能看到 albedo 纹理内容,但 shader resource 面板显示 fragment shader 声明 sampler2DArray,绑定资源 target 为 GL_TEXTURE_2D。请说明排查顺序、核心结论和修复边界。
答案要点
排查顺序应先确认错误信号来自 geometry pass 的主模型 draw,再用 object label 和 debug group 把 callback message 绑定到具体 texture、program 和 draw。接着在 RenderDoc 中排除 framebuffer、viewport、depth、mesh transform 和 UV,再检查 shader resource 面板中的 sampler 类型、texture target、texture unit 和 sampler object。由于 framebuffer 与几何输入正常,且 texture viewer 中资源内容存在,核心问题集中在 sample path:shader 期望 sampler2DArray,实际绑定为 GL_TEXTURE_2D,二者的 target 与采样语义不匹配。
核心结论是:这类黑屏由资源能力分支和绑定状态分离引起。修复应把 feature profile、材质变体、shader define、texture creation、texture target、sampler uniform 和 draw binding 统一到同一个 capability branch。若当前平台支持 texture array,则创建并绑定 GL_TEXTURE_2D_ARRAY,shader 使用 sampler2DArray,并提供 layer index;若平台走 fallback,则 shader、材质和绑定路径同时切到 sampler2D 或 atlas 方案。修复后应保留 debug assertion:draw 前检查 program sampler 类型、texture target 和材质资源描述一致。
本章知识点总结
- 错误哨兵:
glGetError适合缩小可疑 API 区间,但它只返回错误枚举和清除状态。 - 调试输出:debug output 把 driver error、performance warning、shader compiler message 和 application marker 统一送入日志路径。
- 同步回调:
GL_DEBUG_OUTPUT_SYNCHRONOUS让 callback 更接近触发消息的 GL 调用范围,适合开发期断点定位。 - 对象标签:
glObjectLabel把 OpenGL 数字对象名映射成工程资源名,使 driver message 可追踪到 pass、texture、program 或 framebuffer。 - 调试分组:
glPushDebugGroup和glPopDebugGroup把一组命令标记为 pass 或 draw 范围,帮助 frame debugger 与日志对齐。 - 扩展边界:ARB、EXT、KHR 和 vendor 扩展应进入 feature profile,用于决定能力路径和 fallback 设计。
- 能力查询:稳定 profile 同时记录 version、vendor、renderer、extension、function pointer、limits 和 format 支持。
- 帧捕获:RenderDoc 通过单帧 capture 展开 draw state、resource binding、shader input/output 和 render target 结果。
- 调用拦截:GLIntercept 适合旧 Windows OpenGL 项目的 API 调用序列记录,可作为 RenderDoc 工作流的补充。
- 黑屏排查:黑屏应拆成没有 draw、draw 无覆盖、fragment 被丢弃和输出被覆盖四类证据路径。
- 纹理排查:错误纹理要同时检查 resource identity 和 sample path,包括 target、format、mip、layer、sampler type 和 uniform。
- 深度排查:深度异常应按 attachment、depth mask、depth func、projection、depth range 和 clear value 检查。
- Blend 排查:blend 异常要检查 render target format、alpha 约定、blend equation、blend func 和 color write mask。
- 状态契约:每个 pass 的入口应显式设置 program、VAO、framebuffer、viewport、depth、blend、texture 和 sampler 等关键状态。
- 修复闭环:复杂 OpenGL bug 的修复应留下 callback log、frame capture、feature profile、最小复现和状态 contract。