vertex_processor.v 已通过 Verilator --lint-only 检查。源文件:verilog/vertex_processor.v学习目标:深入理解GPU顶点处理阶段,掌握定点数运算在Verilog中的实现,实现顶点坐标变换与属性传递。
顶点处理是渲染管线的第一个处理阶段,对每个顶点独立执行。其主要任务包括:
在FPGA/ASIC实现中,浮点运算开销大。我们采用Q格式定点数来表示坐标和颜色。Q格式记为Qm.n,其中m为整数位数,n为小数位数。
// Q4.12格式: 4位整数 + 12位小数 = 16位
// 范围: -8.0 ~ +7.999756
// 精度: 1/4096 ≈ 0.000244
// 示例:
// 1.0 = 16'h1000 = 4096
// 0.5 = 16'h0800 = 2048
// -1.0 = 16'hF000 = -4096 (二进制补码)
// 2.5 = 16'h2800 = 10240
// 3.14159 ≈ 16'h3243 = 12867//====================================================================
// vertex_processor.v - 顶点处理单元
// 第02课:顶点处理
// 功能:接收原始顶点数据,执行坐标变换,输出变换后顶点
//====================================================================
module vertex_processor #(
parameter COORD_WIDTH = 16,
parameter COLOR_WIDTH = 24,
parameter FRAC_BITS = 12
)(
input wire clk,
input wire rst_n,
input wire vert_valid,
input wire signed [COORD_WIDTH-1:0] vert_in_x,
input wire signed [COORD_WIDTH-1:0] vert_in_y,
input wire signed [COORD_WIDTH-1:0] vert_in_z,
input wire [COLOR_WIDTH-1:0] vert_in_color,
input wire [COORD_WIDTH-1:0] vert_in_u,
input wire [COORD_WIDTH-1:0] vert_in_v,
output reg vert_ready,
input wire signed [COORD_WIDTH-1:0] mat_col0 [0:3],
input wire signed [COORD_WIDTH-1:0] mat_col1 [0:3],
input wire signed [COORD_WIDTH-1:0] mat_col2 [0:3],
input wire signed [COORD_WIDTH-1:0] mat_col3 [0:3],
output reg vert_out_valid,
output reg signed [COORD_WIDTH-1:0] vert_out_x,
output reg signed [COORD_WIDTH-1:0] vert_out_y,
output reg signed [COORD_WIDTH-1:0] vert_out_z,
output reg signed [COORD_WIDTH-1:0] vert_out_w,
output reg [COLOR_WIDTH-1:0] vert_out_color,
output reg [COORD_WIDTH-1:0] vert_out_u,
output reg [COORD_WIDTH-1:0] vert_out_v
);
function signed [COORD_WIDTH-1:0] qmul;
input signed [COORD_WIDTH-1:0] a, b;
reg signed [2*COORD_WIDTH-1:0] product;
begin
product = a * b;
qmul = product[2*COORD_WIDTH-FRAC_BITS-1:COORD_WIDTH-FRAC_BITS];
end
endfunction
function signed [COORD_WIDTH-1:0] qadd;
input signed [COORD_WIDTH-1:0] a, b;
begin qadd = a + b; end
endfunction
reg signed [COORD_WIDTH-1:0] mul_x0, mul_x1, mul_x2, mul_x3;
reg signed [COORD_WIDTH-1:0] mul_y0, mul_y1, mul_y2, mul_y3;
reg signed [COORD_WIDTH-1:0] mul_z0, mul_z1, mul_z2, mul_z3;
reg signed [COORD_WIDTH-1:0] sum_x01, sum_x23;
reg signed [COORD_WIDTH-1:0] sum_y01, sum_y23;
reg signed [COORD_WIDTH-1:0] sum_z01, sum_z23;
reg signed [COORD_WIDTH-1:0] result_x, result_y, result_z, result_w;
reg [COLOR_WIDTH-1:0] pass_color;
reg [COORD_WIDTH-1:0] pass_u, pass_v;
localparam PIPE_IDLE = 2'd0;
localparam PIPE_MUL = 2'd1;
localparam PIPE_SUM = 2'd2;
localparam PIPE_OUT = 2'd3;
reg [1:0] pipe_stage;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
pipe_stage <= PIPE_IDLE; vert_ready <= 1'b1;
vert_out_valid <= 1'b0;
end else begin
vert_out_valid <= 1'b0;
case (pipe_stage)
PIPE_IDLE: begin
vert_ready <= 1'b1;
if (vert_valid) begin
vert_ready <= 1'b0;
pass_color <= vert_in_color;
pass_u <= vert_in_u; pass_v <= vert_in_v;
mul_x0 <= qmul(mat_col0[0], vert_in_x);
mul_x1 <= qmul(mat_col1[0], vert_in_y);
mul_x2 <= qmul(mat_col2[0], vert_in_z);
mul_x3 <= mat_col3[0];
mul_y0 <= qmul(mat_col0[1], vert_in_x);
mul_y1 <= qmul(mat_col1[1], vert_in_y);
mul_y2 <= qmul(mat_col2[1], vert_in_z);
mul_y3 <= mat_col3[1];
mul_z0 <= qmul(mat_col0[2], vert_in_x);
mul_z1 <= qmul(mat_col1[2], vert_in_y);
mul_z2 <= qmul(mat_col2[2], vert_in_z);
mul_z3 <= mat_col3[2];
pipe_stage <= PIPE_MUL;
end
end
PIPE_MUL: begin
sum_x01 <= qadd(mul_x0, mul_x1);
sum_x23 <= qadd(mul_x2, mul_x3);
sum_y01 <= qadd(mul_y0, mul_y1);
sum_y23 <= qadd(mul_y2, mul_y3);
sum_z01 <= qadd(mul_z0, mul_z1);
sum_z23 <= qadd(mul_z2, mul_z3);
pipe_stage <= PIPE_SUM;
end
PIPE_SUM: begin
result_x <= qadd(sum_x01, sum_x23);
result_y <= qadd(sum_y01, sum_y23);
result_z <= qadd(sum_z01, sum_z23);
result_w <= 16'h1000;
pipe_stage <= PIPE_OUT;
end
PIPE_OUT: begin
vert_out_x <= result_x; vert_out_y <= result_y;
vert_out_z <= result_z; vert_out_w <= result_w;
vert_out_color <= pass_color;
vert_out_u <= pass_u; vert_out_v <= pass_v;
vert_out_valid <= 1'b1;
pipe_stage <= PIPE_IDLE; vert_ready <= 1'b1;
end
default: pipe_stage <= PIPE_IDLE;
endcase
end
end
endmodule
`timescale 1ns/1ps
module tb_vertex_processor;
parameter COORD_WIDTH = 16; parameter COLOR_WIDTH = 24;
parameter FRAC_BITS = 12; parameter CLK_PERIOD = 10;
reg clk, rst_n, vert_valid;
reg signed [15:0] vert_in_x, vert_in_y, vert_in_z;
reg [23:0] vert_in_color;
reg [15:0] vert_in_u, vert_in_v;
wire vert_ready, vert_out_valid;
wire signed [15:0] vert_out_x, vert_out_y, vert_out_z, vert_out_w;
wire [23:0] vert_out_color;
wire [15:0] vert_out_u, vert_out_v;
reg signed [15:0] mat_col0 [0:3], mat_col1 [0:3], mat_col2 [0:3], mat_col3 [0:3];
vertex_processor #(.COORD_WIDTH(16),.COLOR_WIDTH(24),.FRAC_BITS(12)) dut (.*);
initial clk=0; always #(5) clk=~clk;
function signed [15:0] q; input real v; begin q=$rtoi(v*4096); end endfunction
function real fq; input signed [15:0] v; begin fq=real'(v)/4096.0; end endfunction
initial begin
rst_n=0; vert_valid=0; #(50); rst_n=1; #20;
// 单位矩阵
mat_col0[0]=q(1.0); mat_col0[1]=0; mat_col0[2]=0; mat_col0[3]=0;
mat_col1[0]=0; mat_col1[1]=q(1.0); mat_col1[2]=0; mat_col1[3]=0;
mat_col2[0]=0; mat_col2[1]=0; mat_col2[2]=q(1.0); mat_col2[3]=0;
mat_col3[0]=0; mat_col3[1]=0; mat_col3[2]=0; mat_col3[3]=q(1.0);
$display("=== 测试1:单位矩阵 ===");
wait(vert_ready); vert_valid=1;
vert_in_x=q(1.5); vert_in_y=q(2.0); vert_in_z=q(0.5);
vert_in_color=24'hff0000; vert_in_u=q(0.0); vert_in_v=q(0.0);
#10; vert_valid=0; wait(vert_out_valid); #10;
$display("输入(1.5,2.0,0.5) 输出(%.3f,%.3f,%.3f,%.3f)",
fq(vert_out_x),fq(vert_out_y),fq(vert_out_z),fq(vert_out_w));
// 平移矩阵
mat_col3[0]=q(3.0); mat_col3[1]=q(1.0);
$display("\n=== 测试2:平移(3,1,0) ===");
wait(vert_ready); vert_valid=1;
vert_in_x=q(1.0); vert_in_y=q(2.0); vert_in_z=q(0.0);
#10; vert_valid=0; wait(vert_out_valid); #10;
$display("期望(4.0,3.0,0.0) 输出(%.3f,%.3f,%.3f)",
fq(vert_out_x),fq(vert_out_y),fq(vert_out_z));
#100; $display("\n=== 测试完成 ==="); $finish;
end
endmodule
顶点着色器输出的属性需要在光栅化阶段进行插值。最常用的是透视正确插值:
练习1:定点数转换
将以下浮点数转换为Q4.12格式:(a) 2.5 (b) -1.75 (c) 0.333 (d) 7.999
练习2:矩阵乘法时序
画出顶点处理器的流水线时序图。100MHz时钟下处理一个顶点的延迟是多少?
Q格式定点数是FPGA/ASIC设计中最常用的数值表示方法。选择Q4.12格式的原因:
| 格式 | 整数位 | 小数位 | 范围 | 精度 | 适用场景 |
|---|---|---|---|---|---|
| Q4.12 | 4 | 12 | ±8.0 | 0.00024 | 3D坐标 |
| Q1.14 | 1 | 14 | ±2.0 | 0.00006 | 法线/方向 |
| Q8.8 | 8 | 8 | ±256 | 0.0039 | 纹理坐标 |
| Q0.16 | 0 | 16 | 0~1 | 0.000015 | 归一化值 |
顶点处理器采用3级流水线设计,这是高性能数字设计的经典模式:
// 流水线时序图 (100MHz, 10ns周期)
// 周期: 1 2 3 4 5 6 7
// V0: MUL SUM OUT - - - -
// V1: - MUL SUM OUT - - -
// V2: - - MUL SUM OUT - -
// V3: - - - MUL SUM OUT -
//
// 延迟: 3周期 (30ns)
// 吞吐率: 1顶点/周期 (100MHz下1千万顶点/秒)
流水线设计的三个关键原则:
4×4矩阵乘4维向量需要16次乘法和12次加法。在硬件中有多种优化策略:
| 策略 | 乘法器数 | 延迟 | 面积 |
|---|---|---|---|
| 全并行 | 16 | 1周期 | 最大 |
| 行并行 | 4 | 4周期 | 中等 |
| 串行 | 1 | 16周期 | 最小 |
| 本课方案 | 12 | 3周期 | 较大 |
本课采用的方案在面积和性能之间取得平衡:12个并行乘法器在PIPE_MUL阶段同时计算,2级加法树在PIPE_SUM阶段完成求和,PIPE_OUT阶段输出结果。
顶点处理器是渲染管线的入口,其性能直接影响整个管线的吞吐率。现代GPU的顶点处理特点:
在我们的Verilog实现中,顶点处理器作为独立模块运行,通过valid/ready握手协议与前后模块通信,这与实际GPU的接口设计一致。
Q4.12格式加减法可能溢出,需要检测和处理:
// 溢出检测逻辑
wire overflow_add = (a[15] == b[15]) && (result[15] != a[15]);
wire overflow_mul = (product[2*COORD_WIDTH-1:2*COORD_WIDTH-2] != 0) &&
(product[2*COORD_WIDTH-1:2*COORD_WIDTH-2] != 2'b11);
// 饱和处理
assign saturated = overflow_add ? (a[15] ? 16'h8000 : 16'h7FFF) : result;
在实际GPU芯片设计中,顶点处理模块面临以下挑战:
| 资源 | 本课模块 | 占比 |
|---|---|---|
| LUT | ~800 | ~1.2% |
| FF | ~400 | ~0.6% |
| DSP | 12 | ~5.5% |
| BRAM | 0 | 0% |
| 平台 | 实现方式 | 性能 | 开发难度 |
|---|---|---|---|
| FPGA | Verilog/HLS | 100MHz+ | 中高 |
| ASIC | RTL设计 | 1GHz+ | 极高 |
| GPU着色器 | GLSL/HLSL | 可变 | 低 |
| CPU软件 | C/C++ | 受限于核心数 | 低 |
对顶点处理模块的验证应包含以下方面:
在现代FPGA平台上的典型性能指标:
// 性能基准(Artix-7 @ 100MHz)
// - 顶点吞吐率: 10M vertices/s (单核)
// - 延迟: 3周期 (30ns)
// - DSP占用: 12个 (Q4.12乘法)
// - 最大时钟: ~150MHz (时序约束后)
// - 功耗: ~50mW (动态功耗估算)
khronos.org/openglveripool.org/verilator