学习目标:理解GPU渲染管线的整体架构,掌握从3D数据到2D像素的完整流程,建立渲染管线的系统认知。
gpu_pipeline_top.v 已通过 Verilator --lint-only 检查。源文件:verilog/gpu_pipeline_top.vGPU渲染管线(Rendering Pipeline)是将3D场景数据转换为2D屏幕图像的一系列处理阶段。理解这条管线是掌握GPU编程的基石。现代GPU渲染管线通常包含以下核心阶段:
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
};
顶点着色器是管线的第一个可编程阶段。它对每个顶点独立执行,主要完成:
将顶点组装成图元(点、线、三角形)。GPU需要知道哪些顶点构成一个图元,这由索引缓冲区(Index Buffer)指定。
将图元转换为片段(Fragment)的过程。每个片段对应屏幕上的一个像素位置,包含从顶点属性插值而来的数据。光栅化是GPU最核心的固定功能阶段。
对每个片段执行着色计算,决定最终颜色。这是另一个可编程阶段,负责纹理采样、光照计算等。
经过深度测试、模板测试、混合等操作后,片段颜色写入帧缓冲区,最终显示在屏幕上。
我们从最基础的管线状态机开始,用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
//====================================================================
// 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
| 特性 | CPU | GPU |
|---|---|---|
| 核心数量 | 4-64核 | 数千核 |
| 核心类型 | 复杂ALU,高频率 | 简单ALU,高吞吐 |
| 并行模式 | MIMD | SIMD/SIMT |
| 缓存 | 大容量多级缓存 | 小容量共享缓存 |
| 内存带宽 | ~50 GB/s | ~900 GB/s |
| 适用场景 | 分支密集逻辑 | 数据并行计算 |
GPU渲染管线的核心优势在于数据并行——同一时刻处理成千上万个顶点或片段。这与CPU的任务并行理念截然不同。在Verilog中,我们可以通过实例化多个处理单元来模拟这种并行性。
早期GPU(如OpenGL 1.x / DirectX 7时代)使用固定管线,渲染行为由硬件寄存器配置决定:
现代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个时钟周期,请计算:
3dfx Voodoo、NVIDIA RIVA系列。硬件只能执行固定的变换和光照计算。开发者通过OpenGL/Direct3D API配置渲染状态。
NVIDIA GeForce 3引入可编程顶点着色器。GeForce FX进一步支持可编程片段着色器。开发者可以用类似汇编的语言编写着色程序。
NVIDIA GeForce 8800(G80)引入统一着色架构。顶点、几何、片段着色器使用相同的处理单元,GPU可根据负载动态分配计算资源。CUDA也应运而生。
NVIDIA RTX系列集成光线追踪(RT)核心和Tensor核心,支持实时光线追踪和DLSS超分辨率。AMD RDNA2和Intel Arc也加入了硬件光线追踪支持。
你已掌握GPU渲染管线的整体架构!你了解了从3D顶点数据到2D屏幕像素的完整流程,并亲手用Verilog实现了管线状态机的骨架。