🖥️ 第02课:顶点处理

阶段一:图形基础 第2/30课
✅Verilator验证通过vertex_processor.v 已通过 Verilator --lint-only 检查。源文件:verilog/vertex_processor.v

学习目标:深入理解GPU顶点处理阶段,掌握定点数运算在Verilog中的实现,实现顶点坐标变换与属性传递。

📋 顶点处理的核心任务

顶点处理是渲染管线的第一个处理阶段,对每个顶点独立执行。其主要任务包括:

🔍 定点数运算基础

在FPGA/ASIC实现中,浮点运算开销大。我们采用Q格式定点数来表示坐标和颜色。Q格式记为Qm.n,其中m为整数位数,n为小数位数。

Q4.12定点数格式

// 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

🔧 Verilog实现:顶点处理单元

//====================================================================
// 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

🧪 仿真验证:Testbench

`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

📐 顶点属性插值原理

顶点着色器输出的属性需要在光栅化阶段进行插值。最常用的是透视正确插值

(p/z)_interp = (1-t)(p₀/z₀) + t(p₁/z₁)
1/z_interp = (1-t)(1/z₀) + t(1/z₁)
p_final = (p/z)_interp / (1/z)_interp

🧩 练习题

练习1:定点数转换

将以下浮点数转换为Q4.12格式:(a) 2.5 (b) -1.75 (c) 0.333 (d) 7.999

练习2:矩阵乘法时序

画出顶点处理器的流水线时序图。100MHz时钟下处理一个顶点的延迟是多少?

📖 练习1参考答案

🎯 本课小结

📊 Q格式定点数深入分析

Q格式定点数是FPGA/ASIC设计中最常用的数值表示方法。选择Q4.12格式的原因:

不同Q格式对比

格式整数位小数位范围精度适用场景
Q4.12412±8.00.000243D坐标
Q1.14114±2.00.00006法线/方向
Q8.888±2560.0039纹理坐标
Q0.160160~10.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千万顶点/秒)

流水线设计的三个关键原则:

  1. 阶段均衡:每个阶段的处理时间应大致相等
  2. 数据无冲突:不同阶段之间通过寄存器隔离,避免数据竞争
  3. 控制简单化:流水线控制逻辑应尽量简单,避免复杂的停顿和刷新

📐 矩阵-向量乘法的硬件优化

4×4矩阵乘4维向量需要16次乘法和12次加法。在硬件中有多种优化策略:

优化策略对比

策略乘法器数延迟面积
全并行161周期最大
行并行44周期中等
串行116周期最小
本课方案123周期较大

本课采用的方案在面积和性能之间取得平衡:12个并行乘法器在PIPE_MUL阶段同时计算,2级加法树在PIPE_SUM阶段完成求和,PIPE_OUT阶段输出结果。

🏗️ 顶点处理器在GPU中的位置

顶点处理器是渲染管线的入口,其性能直接影响整个管线的吞吐率。现代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芯片设计中,顶点处理模块面临以下挑战:

FPGA资源估算(Xilinx Artix-7)

资源本课模块占比
LUT~800~1.2%
FF~400~0.6%
DSP12~5.5%
BRAM00%

🌐 跨平台实现对比

平台实现方式性能开发难度
FPGAVerilog/HLS100MHz+中高
ASICRTL设计1GHz+极高
GPU着色器GLSL/HLSL可变
CPU软件C/C++受限于核心数

🧪 验证策略

对顶点处理模块的验证应包含以下方面:

  1. 功能验证:基本输入输出的正确性
  2. 边界测试:零值、最大值、最小值、溢出情况
  3. 随机测试:大量随机输入的回归测试
  4. 对比验证:与软件参考模型的输出对比
  5. 时序验证:流水线停顿、反压等场景

📈 性能基准

在现代FPGA平台上的典型性能指标:

// 性能基准(Artix-7 @ 100MHz)
// - 顶点吞吐率: 10M vertices/s (单核)
// - 延迟: 3周期 (30ns)
// - DSP占用: 12个 (Q4.12乘法)
// - 最大时钟: ~150MHz (时序约束后)
// - 功耗: ~50mW (动态功耗估算)

🔗 与其他课程的关联

知识图谱

📖 推荐资源

🏆 成就解锁:顶点变换师