AIGC标识 01基础概念


一、Init 阶段:Buffer 的创建与填充

核心认知:
init 阶段就是围绕「创建 buffer → 绑定 buffer → 往 buffer 里塞数据」这三步展开的。

标准流程(以顶点缓冲为例):

    glGenBuffers(1, &vbo);              // 1. 创建:向驱动申请一个 buffer 对象,拿到句柄 id
    glBindBuffer(GL_ARRAY_BUFFER, vbo); // 2. 绑定:把它挂到当前上下文的 GL_ARRAY_BUFFER 槽位上
    glBufferData(                       // 3. 填数据:把 CPU 侧的顶点数组上传(拷贝)进去
        GL_ARRAY_BUFFER,
        sizeof(positions),
        positions,
        GL_STATIC_DRAW                  // 使用提示:一次性上传、多次使用
    );

补充:为什么要先绑定再操作?
OpenGL 是「状态机」设计,后续所有针对 buffer 的操作(填数据、配置属性指针)都作用在
「当前绑定在该 target 槽位上的那个对象」上,所以必须先 glBind 再做后续动作。
另外在 Core Profile 下,顶点属性还必须通过 VAO(Vertex Array Object)来记录
(glVertexAttribPointer 的状态存储在当前绑定的 VAO 里)。

补充:每一个 buffer 都要有一个类型(target),常见类型有:

    GL_ARRAY_BUFFER          顶点属性数据(VBO)
    GL_ELEMENT_ARRAY_BUFFER  索引数据(IBO/EBO)
    GL_UNIFORM_BUFFER        统一变量块(UBO)
    GL_SHADER_STORAGE_BUFFER 可读写着色器存储块(SSBO)
    GL_PIXEL_UNPACK_BUFFER   纹理上传的像素数据
    GL_TRANSFORM_FEEDBACK_BUFFER  捕获顶点着色器输出

二、ShaderInfo:做了什么,在哪个阶段

ShaderInfo 是《OpenGL 编程指南》(红宝书)辅助库 InitShader.cpp 里的结构体,
用来描述「一个着色器阶段」的三要素:

    typedef struct {
        GLenum       type;      // 着色器阶段:GL_VERTEX_SHADER / GL_FRAGMENT_SHADER 等
        const char  *filename;  // GLSL 源码文件路径
        GLuint       shader;    // 编译后生成的着色器对象句柄(由驱动分配)
    } ShaderInfo;

它所处的阶段:InitShader() 在 初始化阶段(程序启动 / 渲循环之前) 被调用,
内部依次完成:

    读文件 → glCreateShader(type)          创建着色器对象
           → glShaderSource + glCompileShader   编译(出错时打印日志)
           → glCreateProgram()              创建程序对象
           → glAttachShader × N             把各阶段挂到 program 上
           → glLinkProgram(program)         链接(统一各 stage 的 varying/uniform)
           → glDeleteShader × N             编译产物已并入 program,源对象可删
           → 返回 program 句柄

即:ShaderInfo 只是「把着色器阶段的描述组织成表」,真正的创建/编译/链接
全部发生在 init 阶段;渲染循环里只剩 glUseProgram + draw。


三、GPU 实际操作 shader 是怎么个流程?

以 glUseProgram(prog) 的实际实现为例,一次 GL 调用走过的完整链路:

你的代码 (glUseProgram(prog))
↓
GL 加载器 (GLAD/GLEW 拿到的函数指针)
↓
opengl32.dll (系统只内置到 OpenGL 1.1)
↓
显卡驱动的 ICD (Installable Client Driver, 如 nvoglv64.dll)
↓
驱动内部维护的 GL 状态机
↓
(真正的 GPU 命令在后续 draw call 时才下发)

驱动内部的典型逻辑(伪代码)——查表 → 校验 → 改状态 → 标脏:

void glUseProgram(GLuint program)
    {
        GLcontext *ctx = get_current_context();  // 线程局部存储里取当前上下文

        // 1. 查对象表:把句柄翻译成驱动内部结构
        gl_program *prog = lookup_object(ctx, program);
        if (prog == NULL) {
            set_error(GL_INVALID_VALUE);  // 注意:错误只是记录,不会崩
            return;
        }

        // 2. 合法性检查:必须成功 link 过
        if (program != 0 && prog->link_status != GL_LINKED) {
            set_error(GL_INVALID_OPERATION);
            return;
        }

        // 3. 真正的动作:把当前 program 状态指向它
        ctx->current_program = prog;

        // 4. 通知后端(硬件相关)
        driver_backend->dirty_state(PROGRAM_STATE);
        // 到这里就返回了,GPU 什么都没干!
    }

新手最常见的误解是「调了 glUseProgram,shader 就在 GPU 上跑了」。实际上:

  • glUseProgram 只是 CPU 侧改一个状态指针,开销极小;
  • shader 真正被「装进」GPU 管线是在下一次 draw call(glDrawArrays 等)时——
    驱动在 draw 时检查所有脏状态,把当前 program 对应的编译好的 GPU 指令
    (不同硬件格式不同,NVIDIA 是 SASS 类指令、AMD 是 GCN/RDNA 指令)
    通过命令缓冲提交给 GPU;
  • GPU 硬件本身有可编程 stage(VS/FS/GS...),驱动做的事是把你的 GLSL
    编译结果绑定到这些 stage 的状态寄存器/命令流上。

三个关键调用的时间点与开销对比:

    glCreateProgram + glLinkProgram → GLSL 被驱动编译成 GPU 指令(CPU 侧,最耗时)
    glUseProgram                    → 记住「下次用这个」(微秒级)
    glDrawArrays                    → 才真正下发命令,shader 开跑

四、DrawCall 整体流程

前提认知:
PS(Pixel/Fragment Shader)执行的前提是光栅化,在此之前还有 culling
(背面剔除、视锥剔除等)——被剔除的图元根本不会产生片元,也就不会跑 FS。

一次 glDrawArrays 在 GPU 上的完整路径:

    glDrawArrays
      → 顶点着色器(同样在 SM 上跑,同样按 warp)
      → 光栅化(固定功能硬件,SM 之外)
      → 生成 2x2 quad,插值 varying(专门的插值硬件,或者 FS 自己插)
      → quad 打包成 warp,塞进 SM 队列
      → SM 锁步执行你编译后的 GPU 指令
      → depth test(通常提前到 FS 之前执行,Early-Z)
      → 结果写入 Render Target(经过 ROP)

名词补充:

  • SM(Streaming Multiprocessor):NVIDIA GPU 的通用计算单元,着色器指令在它上面执行;
  • quad:光栅化以 2x2 像素为最小单位生成片元,方便计算屏幕空间导数(ddx/ddy);
  • warp:32 个线程组成的执行组,同一条指令锁步执行(SIMD);
  • Early-Z:深度测试若能提前通过/剔除,就避免白白跑一遍 FS;
    (但 FS 里若 discard / 写深度,Early-Z 会失效,退回 FS 之后测试)
  • ROP(Render Output Unit):负责深度测试、颜色混合、写回显存。

五、片元着色器的本质

一句话总结:

片元着色器 = 把你的 GLSL 编译成 GPU 指令后,由 SM 上的 warp 以
32 线程锁步 SIMD 的方式,对海量片元做数据并行计算,
靠海量 warp 轮转隐藏内存延迟,最终目标是填满屏幕像素。


六、绑定的本质

所有 glBind* 系列调用(glBindBuffer / glBindTexture / glBindVertexArray ...)
的本质都一样:

    glBind*(target, id)
      = 查名字表拿到驱动内部对象指针
      + 写进当前上下文的对应槽位
      + 标脏,等 draw 时才真正编码进硬件命令

七、glFinish:同步与计时

GL 命令是异步下发的(先进命令缓冲),glFinish() 会阻塞 CPU,
直到「命令缓冲里所有命令被 GPU 全部执行完毕」才返回。

用途:可以用于测试渲染流水线所有阶段完成的总时长:

    glFinish();                 // 保证起点干净
    t0 = now();
    ... draw calls ...
    glFinish();                 // 等 GPU 真正做完
    t1 = now();
    gpuTime = t1 - t0;

(区别:glFlush 只是「把命令缓冲刷给 GPU」,不等待执行完成。)

posted @ 2026-11-01 23:25  doreenxuu  阅读(1)  评论(0)    收藏  举报