🖥️ 第01课:GPU渲染概述

阶段一:图形基础 第1/30课

学习目标:理解GPU渲染管线的整体架构,掌握从3D数据到2D像素的完整流程,建立渲染管线的系统认知。

✅Verilator验证通过gpu_pipeline_top.v 已通过 Verilator --lint-only 检查。源文件:verilog/gpu_pipeline_top.v

📋 渲染管线全景图

GPU渲染管线(Rendering Pipeline)是将3D场景数据转换为2D屏幕图像的一系列处理阶段。理解这条管线是掌握GPU编程的基石。现代GPU渲染管线通常包含以下核心阶段:

顶点数据
Vertex Data
顶点着色器
Vertex Shader
图元装配
Primitive Assembly
光栅化
Rasterization
片段着色器
Fragment Shader
帧缓冲
Framebuffer

🔍 各阶段详解

1. 顶点数据(Vertex Data)

3D场景的基础是顶点。每个顶点包含位置坐标(x,y,z,w)、法线、纹理坐标、颜色等属性。在硬件层面,这些数据存储在顶点缓冲区(Vertex Buffer)中,通过总线传输到GPU。

顶点数据结构

// 一个典型顶点的属性组成
struct Vertex {
    float x, y, z;     // 位置坐标 (12 bytes)
    float nx, ny, nz;  // 法线向量 (12 bytes)
    float u, v;        // 纹理坐标 (8 bytes)
    uint8_t r, g, b, a;// 顶点颜色 (4 bytes)
    // 总计: 36 bytes per vertex
};

2. 顶点着色器(Vertex Shader)

顶点着色器是管线的第一个可编程阶段。它对每个顶点独立执行,主要完成:

3. 图元装配(Primitive Assembly)

将顶点组装成图元(点、线、三角形)。GPU需要知道哪些顶点构成一个图元,这由索引缓冲区(Index Buffer)指定。

4. 光栅化(Rasterization)

将图元转换为片段(Fragment)的过程。每个片段对应屏幕上的一个像素位置,包含从顶点属性插值而来的数据。光栅化是GPU最核心的固定功能阶段。

5. 片段着色器(Fragment Shader)

对每个片段执行着色计算,决定最终颜色。这是另一个可编程阶段,负责纹理采样、光照计算等。

6. 帧缓冲输出(Framebuffer Output)

经过深度测试、模板测试、混合等操作后,片段颜色写入帧缓冲区,最终显示在屏幕上。

🔧 Verilog实现:渲染管线状态机

我们从最基础的管线状态机开始,用Verilog描述渲染管线的各个阶段流转。这个模块将作为整个课程的框架,后续课程会逐步填充每个阶段的具体实现。

//====================================================================
// gpu_pipeline_top.v - GPU渲染管线顶层状态机
// 第01课:渲染管线概述
// 功能:定义管线各阶段的状态流转,协调各模块工作
//====================================================================

module gpu_pipeline_top #(
    parameter DATA_WIDTH    = 32,    // 数据位宽
    parameter COORD_WIDTH   = 16,    // 坐标位宽(定点数)
    parameter COLOR_WIDTH   = 24,    // 颜色位宽(RGB各8位)
    parameter FB_WIDTH      = 640,   // 帧缓冲宽度
    parameter FB_HEIGHT     = 480,   // 帧缓冲高度
    parameter VERTEX_SIZE   = 32     // 顶点属性大小(bytes)
)(
    input  wire                     clk,        // 系统时钟
    input  wire                     rst_n,      // 异步复位,低有效
    
    // 顶点数据输入接口
    input  wire                     vdata_valid,// 顶点数据有效
    input  wire [DATA_WIDTH-1:0]    vdata_in,   // 顶点数据输入
    output reg                      vdata_ready,// 准备接收顶点数据
    
    // 帧缓冲写入接口
    output reg  [COORD_WIDTH-1:0]   fb_x,       // 帧缓冲x坐标
    output reg  [COORD_WIDTH-1:0]   fb_y,       // 帧缓冲y坐标
    output reg  [COLOR_WIDTH-1:0]   fb_color,   // 帧缓冲颜色
    output reg                      fb_wen,     // 帧缓冲写使能
    
    // 状态输出
    output reg  [3:0]               pipeline_stage,// 当前管线阶段
    output reg                      frame_done  // 一帧渲染完成
);

    //================================================================
    // 管线阶段定义
    //================================================================
    localparam STAGE_IDLE       = 4'd0;  // 空闲状态
    localparam STAGE_VERTEX     = 4'd1;  // 顶点处理
    localparam STAGE_ASSEMBLY   = 4'd2;  // 图元装配
    localparam STAGE_CLIP       = 4'd3;  // 裁剪
    localparam STAGE_RASTER     = 4'd4;  // 光栅化
    localparam STAGE_FRAGMENT   = 4'd5;  // 片段着色
    localparam STAGE_DEPTH      = 4'd6;  // 深度测试
    localparam STAGE_BLEND      = 4'd7;  // 混合
    localparam STAGE_FRAMEBUFFER= 4'd8;  // 帧缓冲写入
    localparam STAGE_DONE       = 4'd9;  // 渲染完成

    //================================================================
    // 内部寄存器
    //================================================================
    reg [COORD_WIDTH-1:0] vertex_x, vertex_y, vertex_z;
    reg [COLOR_WIDTH-1:0] vertex_color;
    reg [DATA_WIDTH-1:0]  vertex_attr_buf [0:7]; // 顶点属性缓冲
    reg [2:0]             attr_count;             // 属性计数器
    
    // 图元装配缓冲(3个顶点构成一个三角形)
    reg [COORD_WIDTH-1:0] prim_v0_x, prim_v0_y, prim_v0_z;
    reg [COORD_WIDTH-1:0] prim_v1_x, prim_v1_y, prim_v1_z;
    reg [COORD_WIDTH-1:0] prim_v2_x, prim_v2_y, prim_v2_z;
    reg [COLOR_WIDTH-1:0] prim_v0_color, prim_v1_color, prim_v2_color;
    reg [1:0]             vertex_count;  // 当前图元的顶点计数
    
    // 光栅化相关
    reg [COORD_WIDTH-1:0] raster_x, raster_y;
    reg [COORD_WIDTH-1:0] raster_x_end, raster_y_end;
    
    // 片段数据
    reg [COLOR_WIDTH-1:0] frag_color;
    reg [COORD_WIDTH-1:0] frag_depth;
    
    // 统计计数器
    reg [31:0] vertices_processed;
    reg [31:0] primitives_drawn;
    reg [31:0] fragments_shaded;
    reg [31:0] pixels_written;

    //================================================================
    // 管线状态机 - 主状态转移
    //================================================================
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            pipeline_stage   <= STAGE_IDLE;
            vdata_ready      <= 1'b1;
            fb_wen           <= 1'b0;
            frame_done       <= 1'b0;
            attr_count       <= 3'd0;
            vertex_count     <= 2'd0;
            vertices_processed <= 32'd0;
            primitives_drawn   <= 32'd0;
            fragments_shaded   <= 32'd0;
            pixels_written     <= 32'd0;
            raster_x         <= {COORD_WIDTH{1'b0}};
            raster_y         <= {COORD_WIDTH{1'b0}};
        end else begin
            fb_wen      <= 1'b0;  // 默认不写帧缓冲
            frame_done  <= 1'b0;  // 默认帧未完成
            
            case (pipeline_stage)
                //------------------------------------------------
                // 空闲状态:等待顶点数据
                //------------------------------------------------
                STAGE_IDLE: begin
                    vdata_ready <= 1'b1;
                    if (vdata_valid) begin
                        pipeline_stage <= STAGE_VERTEX;
                        vdata_ready    <= 1'b0;
                        attr_count     <= 3'd0;
                    end
                end
                
                //------------------------------------------------
                // 顶点处理阶段:接收并处理顶点属性
                //------------------------------------------------
                STAGE_VERTEX: begin
                    if (vdata_valid) begin
                        // 将顶点属性存入缓冲
                        vertex_attr_buf[attr_count] <= vdata_in;
                        attr_count <= attr_count + 3'd1;
                        
                        // 8个属性接收完毕(位置xyz + 法线xyz + uv + 颜色)
                        if (attr_count == 3'd7) begin
                            // 解析顶点位置(简化:假设已为屏幕坐标)
                            vertex_x     <= vertex_attr_buf[0][COORD_WIDTH-1:0];
                            vertex_y     <= vertex_attr_buf[1][COORD_WIDTH-1:0];
                            vertex_z     <= vertex_attr_buf[2][COORD_WIDTH-1:0];
                            vertex_color <= vdata_in[COLOR_WIDTH-1:0];
                            
                            vertices_processed <= vertices_processed + 32'd1;
                            pipeline_stage <= STAGE_ASSEMBLY;
                        end
                    end
                end
                
                //------------------------------------------------
                // 图元装配:将顶点组装为三角形
                //------------------------------------------------
                STAGE_ASSEMBLY: begin
                    case (vertex_count)
                        2'd0: begin
                            prim_v0_x     <= vertex_x;
                            prim_v0_y     <= vertex_y;
                            prim_v0_z     <= vertex_z;
                            prim_v0_color <= vertex_color;
                            vertex_count  <= 2'd1;
                            vdata_ready   <= 1'b1;
                            pipeline_stage <= STAGE_VERTEX; // 继续接收下一个顶点
                        end
                        2'd1: begin
                            prim_v1_x     <= vertex_x;
                            prim_v1_y     <= vertex_y;
                            prim_v1_z     <= vertex_z;
                            prim_v1_color <= vertex_color;
                            vertex_count  <= 2'd2;
                            vdata_ready   <= 1'b1;
                            pipeline_stage <= STAGE_VERTEX;
                        end
                        2'd2: begin
                            prim_v2_x     <= vertex_x;
                            prim_v2_y     <= vertex_y;
                            prim_v2_z     <= vertex_z;
                            prim_v2_color <= vertex_color;
                            vertex_count  <= 2'd0;
                            primitives_drawn <= primitives_drawn + 32'd1;
                            pipeline_stage <= STAGE_CLIP;
                        end
                    endcase
                end
                
                //------------------------------------------------
                // 裁剪阶段:判断图元是否在可视区域内
                //------------------------------------------------
                STAGE_CLIP: begin
                    // 简化裁剪:检查三角形是否完全在屏幕外
                    // 如果三个顶点都在同一侧屏幕外,则丢弃
                    if ((prim_v0_x >= FB_WIDTH && prim_v1_x >= FB_WIDTH && prim_v2_x >= FB_WIDTH) ||
                        (prim_v0_y >= FB_HEIGHT && prim_v1_y >= FB_HEIGHT && prim_v2_y >= FB_HEIGHT)) begin
                        // 完全裁剪,回到空闲
                        pipeline_stage <= STAGE_IDLE;
                        vdata_ready    <= 1'b1;
                    end else begin
                        // 部分或全部在屏幕内,进入光栅化
                        pipeline_stage <= STAGE_RASTER;
                        raster_x       <= {COORD_WIDTH{1'b0}};
                        raster_y       <= {COORD_WIDTH{1'b0}};
                    end
                end
                
                //------------------------------------------------
                // 光栅化阶段:遍历三角形包围盒内的像素
                //------------------------------------------------
                STAGE_RASTER: begin
                    // 简化光栅化:逐像素扫描包围盒
                    // 实际实现需使用重心坐标判断点是否在三角形内
                    frag_color <= prim_v0_color; // 简化:使用顶点0的颜色
                    
                    if (raster_y < FB_HEIGHT) begin
                        if (raster_x < FB_WIDTH) begin
                            raster_x <= raster_x + 16'd1;
                            pipeline_stage <= STAGE_FRAGMENT;
                        end else begin
                            raster_x <= {COORD_WIDTH{1'b0}};
                            raster_y <= raster_y + 16'd1;
                        end
                    end else begin
                        pipeline_stage <= STAGE_DONE;
                    end
                end
                
                //------------------------------------------------
                // 片段着色阶段
                //------------------------------------------------
                STAGE_FRAGMENT: begin
                    fragments_shaded <= fragments_shaded + 32'd1;
                    pipeline_stage <= STAGE_DEPTH;
                end
                
                //------------------------------------------------
                // 深度测试阶段
                //------------------------------------------------
                STAGE_DEPTH: begin
                    // 简化:始终通过深度测试
                    pipeline_stage <= STAGE_BLEND;
                end
                
                //------------------------------------------------
                // 混合阶段
                //------------------------------------------------
                STAGE_BLEND: begin
                    // 简化:直接替换,不混合
                    pipeline_stage <= STAGE_FRAMEBUFFER;
                end
                
                //------------------------------------------------
                // 帧缓冲写入阶段
                //------------------------------------------------
                STAGE_FRAMEBUFFER: begin
                    fb_x     <= raster_x;
                    fb_y     <= raster_y;
                    fb_color <= frag_color;
                    fb_wen   <= 1'b1;
                    pixels_written <= pixels_written + 32'd1;
                    pipeline_stage <= STAGE_RASTER; // 继续光栅化
                end
                
                //------------------------------------------------
                // 渲染完成
                //------------------------------------------------
                STAGE_DONE: begin
                    frame_done     <= 1'b1;
                    pipeline_stage <= STAGE_IDLE;
                    vdata_ready    <= 1'b1;
                end
                
                default: pipeline_stage <= STAGE_IDLE;
            endcase
        end
    end

    //================================================================
    // 管线统计输出(调试用)
    //================================================================
    // 统计信息可通过JTAG或UART读取
    // vertices_processed: 已处理顶点数
    // primitives_drawn:   已绘制图元数
    // fragments_shaded:   已着色片段数
    // pixels_written:     已写入像素数

endmodule

🧪 仿真验证:Testbench

//====================================================================
// tb_gpu_pipeline_top.sv - 渲染管线顶层测试台
// 第01课:验证管线状态机流转
//====================================================================

`timescale 1ns/1ps

module tb_gpu_pipeline_top;

    // 参数
    parameter DATA_WIDTH  = 32;
    parameter COORD_WIDTH = 16;
    parameter COLOR_WIDTH = 24;
    parameter FB_WIDTH    = 640;
    parameter FB_HEIGHT   = 480;
    parameter CLK_PERIOD  = 10;  // 100MHz时钟

    // 信号
    reg                     clk;
    reg                     rst_n;
    reg                     vdata_valid;
    reg  [DATA_WIDTH-1:0]   vdata_in;
    wire                    vdata_ready;
    wire [COORD_WIDTH-1:0]  fb_x;
    wire [COORD_WIDTH-1:0]  fb_y;
    wire [COLOR_WIDTH-1:0]  fb_color;
    wire                    fb_wen;
    wire [3:0]              pipeline_stage;
    wire                    frame_done;

    // 实例化DUT
    gpu_pipeline_top #(
        .DATA_WIDTH  (DATA_WIDTH),
        .COORD_WIDTH (COORD_WIDTH),
        .COLOR_WIDTH (COLOR_WIDTH),
        .FB_WIDTH    (FB_WIDTH),
        .FB_HEIGHT   (FB_HEIGHT)
    ) dut (
        .clk            (clk),
        .rst_n          (rst_n),
        .vdata_valid    (vdata_valid),
        .vdata_in       (vdata_in),
        .vdata_ready    (vdata_ready),
        .fb_x           (fb_x),
        .fb_y           (fb_y),
        .fb_color       (fb_color),
        .fb_wen         (fb_wen),
        .pipeline_stage (pipeline_stage),
        .frame_done     (frame_done)
    );

    // 时钟生成
    initial clk = 1'b0;
    always #(CLK_PERIOD/2) clk = ~clk;

    // 管线阶段名称
    reg [128*8-1:0] stage_name;
    always @(*) begin
        case (pipeline_stage)
            4'd0: stage_name = "IDLE";
            4'd1: stage_name = "VERTEX";
            4'd2: stage_name = "ASSEMBLY";
            4'd3: stage_name = "CLIP";
            4'd4: stage_name = "RASTER";
            4'd5: stage_name = "FRAGMENT";
            4'd6: stage_name = "DEPTH";
            4'd7: stage_name = "BLEND";
            4'd8: stage_name = "FRAMEBUFFER";
            4'd9: stage_name = "DONE";
            default: stage_name = "UNKNOWN";
        endcase
    end

    // 发送一个顶点的8个属性
    task send_vertex;
        input [COORD_WIDTH-1:0] vx;
        input [COORD_WIDTH-1:0] vy;
        input [COORD_WIDTH-1:0] vz;
        input [COLOR_WIDTH-1:0] vcolor;
        integer i;
        reg [DATA_WIDTH-1:0] attrs [0:7];
        begin
            attrs[0] = {{(DATA_WIDTH-COORD_WIDTH){1'b0}}, vx};
            attrs[1] = {{(DATA_WIDTH-COORD_WIDTH){1'b0}}, vy};
            attrs[2] = {{(DATA_WIDTH-COORD_WIDTH){1'b0}}, vz};
            attrs[3] = 32'd0; // nx
            attrs[4] = 32'd0; // ny
            attrs[5] = 32'd0; // nz
            attrs[6] = 32'd0; // u
            attrs[7] = {{(DATA_WIDTH-COLOR_WIDTH){1'b0}}, vcolor};
            for (i = 0; i < 8; i = i + 1) begin
                @(posedge clk);
                vdata_valid = 1'b1;
                vdata_in    = attrs[i];
                @(posedge clk);
            end
            vdata_valid = 1'b0;
        end
    endtask

    // 主测试流程
    initial begin
        // 初始化
        rst_n       = 1'b0;
        vdata_valid = 1'b0;
        vdata_in    = 32'd0;
        
        // 复位
        #(CLK_PERIOD * 5);
        rst_n = 1'b1;
        #(CLK_PERIOD * 2);
        
        $display("========================================");
        $display("  GPU渲染管线状态机测试");
        $display("========================================");
        
        // 测试1:发送一个三角形(3个顶点)
        $display("\n--- 测试1:发送三角形 ---");
        
        // 顶点0: (100, 100, 0) 红色
        $display("发送顶点0: (%0d, %0d, %0d) 颜色=%h", 100, 100, 0, 24'hff0000);
        send_vertex(16'd100, 16'd100, 16'd0, 24'hff0000);
        #(CLK_PERIOD * 2);
        
        // 顶点1: (200, 100, 0) 绿色
        $display("发送顶点1: (%0d, %0d, %0d) 颜色=%h", 200, 100, 0, 24'h00ff00);
        send_vertex(16'd200, 16'd100, 16'd0, 24'h00ff00);
        #(CLK_PERIOD * 2);
        
        // 顶点2: (150, 200, 0) 蓝色
        $display("发送顶点2: (%0d, %0d, %0d) 颜色=%h", 150, 200, 0, 24'h0000ff);
        send_vertex(16'd150, 16'd200, 16'd0, 24'h0000ff);
        #(CLK_PERIOD * 10);
        
        // 测试2:发送屏幕外顶点(应被裁剪)
        $display("\n--- 测试2:屏幕外顶点(裁剪测试) ---");
        send_vertex(16'd700, 16'd100, 16'd0, 24'hffffff); // x超出屏幕
        #(CLK_PERIOD * 2);
        send_vertex(16'd750, 16'd100, 16'd0, 24'hffffff);
        #(CLK_PERIOD * 2);
        send_vertex(16'd725, 16'd200, 16'd0, 24'hffffff);
        #(CLK_PERIOD * 10);
        
        $display("\n========================================");
        $display("  测试完成!");
        $display("  最终管线阶段: %s", stage_name);
        $display("========================================");
        
        #(CLK_PERIOD * 20);
        $finish;
    end

    // 监控管线阶段变化
    always @(pipeline_stage) begin
        $display("[%0t] 管线阶段 → %s (stage=%0d)", $time, stage_name, pipeline_stage);
    end

    // 监控帧缓冲写入
    always @(posedge clk) begin
        if (fb_wen) begin
            $display("[%0t] 写帧缓冲: (%0d, %0d) 颜色=%h", $time, fb_x, fb_y, fb_color);
        end
    end

endmodule
💡 设计要点:

📊 GPU vs CPU:为什么需要专用硬件

特性CPUGPU
核心数量4-64核数千核
核心类型复杂ALU,高频率简单ALU,高吞吐
并行模式MIMDSIMD/SIMT
缓存大容量多级缓存小容量共享缓存
内存带宽~50 GB/s~900 GB/s
适用场景分支密集逻辑数据并行计算

GPU渲染管线的核心优势在于数据并行——同一时刻处理成千上万个顶点或片段。这与CPU的任务并行理念截然不同。在Verilog中,我们可以通过实例化多个处理单元来模拟这种并行性。

🔬 固定管线 vs 可编程管线

固定功能管线(Fixed-Function Pipeline)

早期GPU(如OpenGL 1.x / DirectX 7时代)使用固定管线,渲染行为由硬件寄存器配置决定:

可编程管线(Programmable Pipeline)

现代GPU(OpenGL 3.3+ / DirectX 10+)提供可编程着色器:

本课程的实现策略

我们将用Verilog实现固定功能管线的核心阶段,每个阶段用独立的模块实现,通过状态机协调。这既是理解管线原理的最佳方式,也是实际GPU设计的基础。后续课程将逐步添加可编程特性。

📐 渲染管线数据流

让我们追踪一个三角形从输入到输出的完整数据流:

// 数据流追踪:一个三角形的渲染旅程
//
// 1. 输入:3个顶点
//    V0 = {x:100, y:100, z:0, color:0xFF0000}  // 红色
//    V1 = {x:200, y:100, z:0, color:0x00FF00}  // 绿色
//    V2 = {x:150, y:200, z:0, color:0x0000FF}  // 蓝色
//
// 2. 顶点着色器输出(本课简化,直接透传)
//    V0' = V0, V1' = V1, V2' = V2
//
// 3. 图元装配
//    Triangle = {V0', V1', V2'}
//
// 4. 光栅化(简化扫描)
//    对包围盒[100,100]~[200,200]内的每个像素
//    判断是否在三角形内 → 生成片段
//
// 5. 片段着色
//    每个片段的颜色 = 重心坐标插值(V0',V1',V2'的颜色)
//
// 6. 帧缓冲写入
//    pixel(x,y) = frag_color

🧩 练习题

练习1:理解管线阶段

请画出完整的GPU渲染管线流程图,标注每个阶段的输入和输出数据类型。思考:如果去掉裁剪阶段,会出现什么问题?

练习2:状态机扩展

在本课的管线状态机基础上,添加一个背面剔除阶段(在裁剪之后、光栅化之前)。背面剔除的判断条件是:如果三角形的绕序为逆时针(从相机方向看),则丢弃该三角形。提示:使用叉积判断绕序。

练习3:性能计算

假设屏幕分辨率为1920×1080,一个三角形覆盖了屏幕的1/4面积。如果GPU主频为1GHz,每个片段着色需要10个时钟周期,请计算:

  1. 该三角形会产生多少个片段?
  2. 渲染这个三角形需要多少时间?
  3. 如果要达到60FPS,每个片段着色最多能用多少个时钟周期?
📖 练习3参考答案
  1. 片段数 ≈ 1920 × 1080 / 4 = 518,400
  2. 时间 = 518,400 × 10 / 10⁹ = 5.184ms
  3. 60FPS每帧时间 ≈ 16.67ms,总片段预算 ≈ 2,073,600(满屏),每片段 ≈ 16.67ms × 10⁹ / 2,073,600 ≈ 8个周期

🏗️ 历史回顾:GPU架构演进

第一代:固定管线(1995-2000)

3dfx Voodoo、NVIDIA RIVA系列。硬件只能执行固定的变换和光照计算。开发者通过OpenGL/Direct3D API配置渲染状态。

第二代:可编程着色器(2001-2006)

NVIDIA GeForce 3引入可编程顶点着色器。GeForce FX进一步支持可编程片段着色器。开发者可以用类似汇编的语言编写着色程序。

第三代:统一着色器(2006-至今)

NVIDIA GeForce 8800(G80)引入统一着色架构。顶点、几何、片段着色器使用相同的处理单元,GPU可根据负载动态分配计算资源。CUDA也应运而生。

第四代:光线追踪与AI(2018-至今)

NVIDIA RTX系列集成光线追踪(RT)核心和Tensor核心,支持实时光线追踪和DLSS超分辨率。AMD RDNA2和Intel Arc也加入了硬件光线追踪支持。

🎯 本课小结

🏆 成就解锁:管线初学者

你已掌握GPU渲染管线的整体架构!你了解了从3D顶点数据到2D屏幕像素的完整流程,并亲手用Verilog实现了管线状态机的骨架。