primitive_assembler.v 已通过 Verilator --lint-only 检查。源文件:verilog/primitive_assembler.v学习目标:理解图元装配的工作原理,掌握三角形绕序判断和索引缓冲机制,实现Verilog图元装配器。
图元装配(Primitive Assembly)是将离散顶点组装成几何图元的过程。GPU支持多种图元类型:
| 图元类型 | OpenGL枚举 | 顶点数 | 说明 |
|---|---|---|---|
| 点 | GL_POINTS | 1 | 单独顶点 |
| 线段 | GL_LINES | 2 | 两个顶点连线 |
| 三角形 | GL_TRIANGLES | 3 | 基本渲染图元 |
| 三角带 | GL_TRIANGLE_STRIP | n | 共享边连续三角形 |
| 三角扇 | GL_TRIANGLE_FAN | n | 共享顶点扇形 |
实际3D模型中多三角形共享顶点极普遍。一个立方体8个顶点但36个索引(12三角形×3),索引缓冲大幅减少顶点变换计算量。
// 立方体:8个唯一顶点,36个索引
vertices[0]=(-1,-1,-1) vertices[1]=(+1,-1,-1)
vertices[2]=(+1,+1,-1) vertices[3]=(-1,+1,-1)
vertices[4]=(-1,-1,+1) vertices[5]=(+1,-1,+1)
vertices[6]=(+1,+1,+1) vertices[7]=(-1,+1,+1)
indices = [0,1,2, 2,3,0, // 前面
4,6,5, 6,4,7, // 后面
0,4,5, 5,1,0, // 底面
3,2,6, 6,7,3, // 顶面
0,3,7, 7,4,0, // 左面
1,5,6, 6,2,1] // 右面屏幕空间绕序决定面朝向,是背面剔除依据:
//====================================================================
// primitive_assembler.v - 图元装配器
// 第03课:图元装配
// 功能:将顶点流组装为三角形图元,判断绕序
//====================================================================
module primitive_assembler #(
parameter COORD_WIDTH = 16,
parameter COLOR_WIDTH = 24,
parameter INDEX_WIDTH = 16
)(
input wire clk,
input wire rst_n,
input wire vert_in_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 [INDEX_WIDTH-1:0] vert_in_idx,
output reg vert_in_ready,
input wire idx_valid,
input wire [INDEX_WIDTH-1:0] idx_data,
output reg idx_ready,
output reg prim_valid,
output reg signed [COORD_WIDTH-1:0] prim_v0_x, prim_v0_y, prim_v0_z,
output reg signed [COORD_WIDTH-1:0] prim_v1_x, prim_v1_y, prim_v1_z,
output reg signed [COORD_WIDTH-1:0] prim_v2_x, prim_v2_y, prim_v2_z,
output reg [COLOR_WIDTH-1:0] prim_v0_color, prim_v1_color, prim_v2_color,
output reg prim_ccw,
input wire prim_ready
);
reg signed [COORD_WIDTH-1:0] vbuf_x [0:2];
reg signed [COORD_WIDTH-1:0] vbuf_y [0:2];
reg signed [COORD_WIDTH-1:0] vbuf_z [0:2];
reg [COLOR_WIDTH-1:0] vbuf_color [0:2];
reg [1:0] slot;
reg signed [2*COORD_WIDTH-1:0] cross_product;
wire signed [2*COORD_WIDTH-1:0] edge1_x = vbuf_x[1] - vbuf_x[0];
wire signed [2*COORD_WIDTH-1:0] edge1_y = vbuf_y[1] - vbuf_y[0];
wire signed [2*COORD_WIDTH-1:0] edge2_x = vbuf_x[2] - vbuf_x[0];
wire signed [2*COORD_WIDTH-1:0] edge2_y = vbuf_y[2] - vbuf_y[0];
always @(*) cross_product = edge1_x * edge2_y - edge1_y * edge2_x;
localparam S_COLLECT = 2'd0, S_OUTPUT = 2'd1;
reg [1:0] state;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= S_COLLECT; slot <= 2'd0;
vert_in_ready <= 1'b1; prim_valid <= 1'b0;
end else begin
prim_valid <= 1'b0;
case (state)
S_COLLECT: begin
vert_in_ready <= 1'b1;
if (vert_in_valid) begin
vbuf_x[slot] <= vert_in_x;
vbuf_y[slot] <= vert_in_y;
vbuf_z[slot] <= vert_in_z;
vbuf_color[slot] <= vert_in_color;
if (slot == 2'd2) begin
state <= S_OUTPUT; vert_in_ready <= 1'b0;
end else slot <= slot + 2'd1;
end
end
S_OUTPUT: begin
prim_ccw <= (cross_product > 0) ? 1'b1 : 1'b0;
prim_v0_x <= vbuf_x[0]; prim_v0_y <= vbuf_y[0];
prim_v0_z <= vbuf_z[0]; prim_v0_color <= vbuf_color[0];
prim_v1_x <= vbuf_x[1]; prim_v1_y <= vbuf_y[1];
prim_v1_z <= vbuf_z[1]; prim_v1_color <= vbuf_color[1];
prim_v2_x <= vbuf_x[2]; prim_v2_y <= vbuf_y[2];
prim_v2_z <= vbuf_z[2]; prim_v2_color <= vbuf_color[2];
prim_valid <= 1'b1;
if (prim_ready) begin
vbuf_x[0] <= vbuf_x[1]; vbuf_y[0] <= vbuf_y[1];
vbuf_z[0] <= vbuf_z[1]; vbuf_color[0] <= vbuf_color[1];
vbuf_x[1] <= vbuf_x[2]; vbuf_y[1] <= vbuf_y[2];
vbuf_z[1] <= vbuf_z[2]; vbuf_color[1] <= vbuf_color[2];
slot <= 2'd2; state <= S_COLLECT;
end
end
default: state <= S_COLLECT;
endcase
end
end
endmodule
`timescale 1ns/1ps
module tb_primitive_assembler;
parameter CLK_PERIOD=10; reg clk,rst_n;
reg vert_in_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_idx;
wire vert_in_ready, prim_valid; wire prim_ccw;
wire signed [15:0] prim_v0_x,prim_v0_y,prim_v0_z;
wire signed [15:0] prim_v1_x,prim_v1_y,prim_v1_z;
wire signed [15:0] prim_v2_x,prim_v2_y,prim_v2_z;
wire [23:0] prim_v0_color,prim_v1_color,prim_v2_color;
reg prim_ready;
primitive_assembler dut(.*);
initial clk=0; always #(5) clk=~clk;
function signed [15:0] q; input real v; begin q=$rtoi(v*4096); end endfunction
initial begin
rst_n=0; vert_in_valid=0; prim_ready=1; #(50); rst_n=1; #20;
$display("=== 逆时针三角形测试 ===");
wait(vert_in_ready); vert_in_valid=1;
vert_in_x=q(0); vert_in_y=q(0); vert_in_z=0; vert_in_color=24'hff0000;
#10; vert_in_x=q(10); vert_in_y=q(0); vert_in_color=24'h00ff00;
#10; vert_in_x=q(5); vert_in_y=q(10); vert_in_color=24'h0000ff;
#10; vert_in_valid=0;
wait(prim_valid); #10;
$display("绕序: %s", prim_ccw?"CCW":"CW");
#200; $display("=== 测试完成 ==="); $finish;
end
endmodule
练习1:三角带装配
修改装配器支持GL_TRIANGLE_STRIP。给定V0,V1,V2,V3,V4,输出哪3个三角形?
练习2:退化三角形检测
添加面积为0的三角形检测,|cross_product| < threshold时丢弃。
在实际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在实际GPU中,图元装配器是固定功能单元,不可编程。其硬件设计需要考虑:
三角带(TRIANGLE_STRIP)中,偶数三角形的绕序会自动反转:
// 输入顶点: V0, V1, V2, V3, V4, V5
// 输出三角形:
// T0 = (V0, V1, V2) → CCW
// T1 = (V1, V3, V2) → CCW (注意V2和V3交换!)
// T2 = (V2, V3, V4) → CCW
// T3 = (V3, V5, V4) → CCW
//
// 规则: 偶数三角形(i%2==0)正常顺序
// 奇数三角形(i%2==1)交换后两个顶点
三角带中使用退化三角形(面积为0)来连接不连续的三角形条带。退化三角形会被后续阶段自动丢弃,无需特殊处理。
// 连接两个条带: [V0,V1,V2,V3] 和 [V4,V5,V6,V7]
// 插入退化三角形: V3,V3,V4
// 完整序列: V0,V1,V2,V3,V3,V4,V4,V5,V6,V7
// 生成的三角形: (V0,V1,V2), (V1,V3,V2), (V2,V3,V3)退化, (V3,V4,V3)退化, (V3,V4,V4)退化, (V4,V5,V4)...
| 场景 | 非索引顶点数 | 索引顶点数 | 节省比例 |
|---|---|---|---|
| 立方体 | 36 | 8 | 77.8% |
| 球体(16段) | 1536 | 289 | 81.2% |
| 地形(64×64) | 24576 | 4225 | 82.8% |
| 角色模型 | ~100K | ~20K | ~80% |
索引缓冲平均可节省约80%的顶点变换计算量,这是GPU性能优化的关键手段之一。
练习3:三角扇模式
实现GL_TRIANGLE_FAN模式的图元装配。所有三角形共享第一个顶点V0,后续每两个相邻顶点与V0构成三角形:T0=(V0,V1,V2), T1=(V0,V2,V3), T2=(V0,V3,V4)...
练习4:索引缓冲查找
在图元装配器中添加索引缓冲支持:当idx_valid有效时,使用idx_data作为顶点缓冲的索引来查找顶点,而非按顺序接收顶点。