6.4 命令字组装:PM4 与 cmd_base
上一篇准备好了内存,这一篇解决 6.1 里的第一个难题:GPU 命令不是函数调用,而是一段按格式排布的二进制 packet。要让 GPU 干活,你得往一段内存里逐个 32-bit dword 拼出 PM4(Programmable Machine 4)packet,再把这段内存当成 IB 提交。这一篇讲两件事:amd_PM4.h 提供的 packet 编码宏,以及 amdgpu_cmd_base 这个「往 buffer 里安全追加 dword」的小抽象。
一、PM4 packet 长什么样
PM4 命令流由一个个 packet 组成,每个 packet 第一个 dword 是 header,高 2 位是 packet type。测试里几乎只用 TYPE3(带 opcode 的引擎命令):
#define PACKET_TYPE0 0
#define PACKET_TYPE3 3
#define PACKET3(op, n) ((PACKET_TYPE3 << 30) | \
(((op) & 0xFF) << 8) | \
((n) & 0x3FFF) << 16)
一个 TYPE3 header 编码三件事:type(bit 30-31)、opcode(bit 8-15)、以及body dword 数减一的 count(bit 16-29)。也就是说 PACKET3(op, n) 里的 n 是「后面还有 n+1 个 body dword」。这个「+1 偏移」是最常见的踩坑点。
常用 opcode 都有宏,例如:
#define PACKET3_NOP 0x10
#define PACKET3_WRITE_DATA 0x37
#define PACKET3_DMA_DATA 0x50
amd_PM4.h 一共给了 21 个 TYPE3 opcode 宏,按用途归类如下:
| opcode 宏 | 值 | 用途 | 归类 |
|---|---|---|---|
PACKET3_NOP |
0x10 | 空操作 / 占位对齐 | 通用 |
PKT3_CLEAR_STATE |
0x12 | 清引擎状态 | 状态 |
PACKET3_DISPATCH_DIRECT |
0x15 | 直接派发 compute grid | 计算 |
PACKET3_ATOMIC_MEM |
0x1E | 对显存做原子操作(如 CMPSWAP) | 内存 |
PKT3_CONTEXT_CONTROL |
0x28 | 控制上下文加载/影子 | 状态 |
PACKET3_DRAW_INDEX_AUTO |
0x2D | 自动索引绘制 | 图形 |
PACKET3_WRITE_DATA |
0x37 | 往寄存器/内存写数据 | 内存 |
PACKET3_WAIT_REG_MEM |
0x3C | 轮询寄存器/内存直到条件满足 | 同步 |
PACKET3_INDIRECT_BUFFER |
0x3F | 跳转执行另一段 IB | 通用 |
PACKET3_EVENT_WRITE |
0x46 | 触发/记录 GPU 事件 | 同步 |
PACKET3_DMA_DATA |
0x50 | CP DMA 拷贝/填充 | 内存 |
PACKET3_ACQUIRE_MEM |
0x58 | cache flush/invalidate 屏障 | 同步 |
PACKET3_SET_CONTEXT_REG |
0x69 | 写 context 寄存器 | 状态 |
PKT3_SET_SH_REG |
0x76 | 写 SH(shader)寄存器 | 状态 |
PKT3_SET_SH_REG_INDEX |
0x9B | 带 index 写 SH 寄存器 | 状态 |
PACKET3_SET_UCONFIG_REG |
0x79 | 写 uconfig 寄存器 | 状态 |
PACKET3_INCREMENT_CE_COUNTER |
0x84 | CE 计数器自增 | CE/DE 同步 |
PACKET3_WAIT_ON_CE_COUNTER |
0x86 | DE 等待 CE 计数器 | CE/DE 同步 |
PACKET3_SET_CE_DE_COUNTERS |
0x89 | 设置 CE/DE 计数器 | CE/DE 同步 |
PACKET3_PROTECTED_FENCE_SIGNAL |
0xD0 | 受保护 fence 置位 | 同步 |
PACKET3_FENCE_WAIT_MULTI |
0xD1 | 等待多个 fence | 同步 |
测试里高频出现的其实就 NOP、WRITE_DATA、DMA_DATA、WAIT_REG_MEM、ATOMIC_MEM、DISPATCH_DIRECT 这几个,剩下的多是图形管线或 CE/DE 双引擎场景才用到。
二、header 之外,body 也全是位域宏
光有 header 不够,body 里每个控制 dword 也是一堆位域。amd_PM4.h 把它们都给了名字。以 WRITE_DATA 为例:
#define WRITE_DATA_DST_SEL(x) ((x) << 8) /* 0=reg 1=mem(sync) 5=mem(async) ... */
#define WR_ONE_ADDR (1 << 16)
#define WR_CONFIRM (1 << 20) /* 等写完成再继续 */
#define WRITE_DATA_ENGINE_SEL(x) ((x) << 30) /* 0=me 1=pfp 2=ce */
DMA_DATA 更复杂,src/dst 的 cache policy、SRC_SEL/DST_SEL、CP_SYNC、BYTE_COUNT 全是位域宏。这些宏的意义在于:你写 WRITE_DATA_DST_SEL(5) | WR_CONFIRM 时,读代码的人立刻知道「异步内存目标、写完确认」,而不是盯着一个 0x00100500 猜。
三、一个真实例子:拼一条 WRITE_DATA
把 header 宏和 body 宏拼起来,就是一条完整命令。这是 gfx_ring_write_linear 的核心(往一段内存写若干个 deadbeef,用于验证 GFX ring 通路):
ring_context->pm4[i++] = PACKET3(PACKET3_WRITE_DATA, 2 + ring_context->write_length / 4);
ring_context->pm4[i++] = WRITE_DATA_DST_SEL(5) | WR_CONFIRM;
ring_context->pm4[i++] = lower_32_bits(ring_context->bo_mc);
ring_context->pm4[i++] = upper_32_bits(ring_context->bo_mc);
while (j++ < ring_context->write_length / 4)
ring_context->pm4[i++] = func->deadbeaf;
*pm4_dw = i;
逐字读这段:
- header:
WRITE_DATA,count =2 + payload_dw。这个 2 对应后面「控制 dword + 地址低32 + 地址高32」里除去 header 的固定开销(body = 控制 + 地址lo + 地址hi + N 个数据,count 编码的是 body-1,这里按该 packet 约定算出)。 - 控制 dword:
DST_SEL(5)= 异步内存目标,WR_CONFIRM= 写完确认。 - 地址:把 GPU VA(
bo_mc,就是上一篇mc_address)拆成高低 32 位。 - 数据:循环写入
func->deadbeaf。 - 最后回填
*pm4_dw= 实际用了多少个 dword,供提交阶段知道 IB 长度。
注意它前面还有 DWORD 对齐和缓冲越界的断言守护——手拼 packet 最怕的就是长度算错、越界,这些 guard 是必要的自我保护。
四、cmd_base:把「往 buffer 追加 dword」抽象出来
上面的例子用裸 pm4[i++] 手动维护下标。更结构化的写法是 struct amdgpu_cmd_base——一个带游标的命令缓冲抽象,让你 emit 而不是自己管下标:
struct amdgpu_cmd_base {
uint32_t cdw; /* 已用 dword 数(游标) */
uint32_t max_dw; /* 容量 */
uint32_t *buf; /* 底层缓冲 */
bool is_assigned_buf;
int (*allocate_buf)(struct amdgpu_cmd_base *base, uint32_t size);
int (*attach_buf)(struct amdgpu_cmd_base *base, void *ptr, uint32_t size_bytes);
void (*emit)(struct amdgpu_cmd_base *base, uint32_t value);
void (*emit_aligned)(struct amdgpu_cmd_base *base, uint32_t mask, uint32_t value);
void (*emit_repeat)(struct amdgpu_cmd_base *base, uint32_t value, uint32_t n);
void (*emit_at_offset)(struct amdgpu_cmd_base *base, uint32_t value, uint32_t off);
void (*emit_buf)(struct amdgpu_cmd_base *base, const void *ptr, uint32_t off, uint32_t sz);
};
核心就是 cdw(游标)+ emit(追加)。emit 的实现极简,但带越界断言:
static void
cmd_emit(struct amdgpu_cmd_base *base, uint32_t value)
{
assert(base->cdw < base->max_dw);
base->buf[base->cdw++] = value;
}
其它 emit 变体各有用途:
| 方法 | 作用 |
|---|---|
emit |
追加一个 dword,游标 +1 |
emit_aligned |
反复 emit 直到游标满足对齐掩码(填 NOP 用) |
emit_repeat |
连续 emit 同一个值 N 次 |
emit_at_offset |
往「游标 + 偏移」处写值,但不移动游标(回填占位用) |
emit_buf |
memcpy 一段外部数据进缓冲 |
五、这些回调由谁实现
结构体里这一堆函数指针,实现全部在 amd_ip_blocks.c 里的一组 static 函数(cmd_allocate_buf/cmd_attach_buf/cmd_emit/cmd_emit_aligned/cmd_emit_repeat/cmd_emit_at_offset/cmd_emit_buf),由唯一的工厂函数 get_cmd_base() 一次性绑定并返回一个初始化好的实例:
struct amdgpu_cmd_base *
get_cmd_base(void)
{
struct amdgpu_cmd_base *base = calloc(1, sizeof(*base));
/* cdw/max_dw/buf 清零,is_assigned_buf=false */
base->allocate_buf = cmd_allocate_buf;
base->attach_buf = cmd_attach_buf;
base->emit = cmd_emit;
base->emit_aligned = cmd_emit_aligned;
base->emit_repeat = cmd_emit_repeat;
base->emit_at_offset = cmd_emit_at_offset;
base->emit_buf = cmd_emit_buf;
return base;
}
调用方从不自己 new,一律 struct amdgpu_cmd_base *base = get_cmd_base(); 拿实例,用完 free_cmd_base(base)。
这里有个容易被误读的点:那些 cmd_* 实现全是 static 的,对外不可见。所以用函数指针并非为了多实现分派(当前只有一套),真正的目的是三件事——一是封装,把实现藏在 .c 文件里、只暴露头文件里的 struct amdgpu_cmd_base 接口,改实现不动调用方;二是自带 receiver 的对象式写法,每个回调第一个参数都是 base 自己,游标 cdw/容量 max_dw/缓冲 buf 全挂在实例上,天然支持多个命令缓冲并存互不干扰;三是留出将来替换实现的口子(比如换一套带 trace/校验的 emit)。至于「为什么这么多个」,是因为拼 PM4 本就需要这几类原子动作——追加、连写、对齐填充、占位回填、拷贝外部数据——缺一不可。
六、两种缓冲来源:allocate 还是 attach
cmd_base 的缓冲可以「自己分配」或「借用外部内存」,这决定谁负责 free:
/* 自己 calloc,free 时由 cmd_base 释放 */
static int cmd_allocate_buf(struct amdgpu_cmd_base *base, uint32_t size_dw);
/* 借用外部指针(如已映射的 IB BO),is_assigned_buf=true,cmd_base 不 free */
static int cmd_attach_buf(struct amdgpu_cmd_base *base, void *ptr, uint32_t size_bytes);
attach_buf 特别有用:直接把上一篇分配的 IB BO 的 CPU 映射地址挂上来,之后所有 emit 就直接写进那块最终要提交的 GPU 内存,省掉一次拷贝。释放逻辑据此区分:
void free_cmd_base(struct amdgpu_cmd_base *base)
{
if (base) {
if (base->buf && base->is_assigned_buf == false)
free(base->buf);
free(base);
}
}
is_assigned_buf 为真(借用外部)时只释放结构体本身,不碰那块外部缓冲——因为它的所有权在别处。
七、两种写法怎么选
同一件事(拼 PM4)有两条路,取舍很清楚:
ip_funcs里那些短小的write_linear/copy_linear/const_fill,用裸数组 + 断言就够直观。- 稍长、需要对齐填充、需要先占位再回填、或要把外部数据块拼进来的场景,用
cmd_base的emit_*系列更省心,也不容易越界。
八、附:SDMA packet —— 另一套独立编码
前面讲的 PACKET3 是 CP(Command Processor,即 GFX/Compute 引擎)专用的 TYPE3 包。但 SDMA(拷贝引擎)并不认 PM4,它有自己一套完全独立的包格式——同一个 opcode 数字在两套体系里含义毫不相干。amd_sdma.h 定义了 SDMA 的 header 宏:
#define SDMA_PACKET(op, sub_op, e) ((((e) & 0xFFFF) << 16) | \
(((sub_op) & 0xFF) << 8) | \
(((op) & 0xFF) << 0))
header 三段:低 8 位 op、中间 8 位 sub_op、高 16 位扩展字段 e。amd_sdma.h 里给出的 op / sub_op 如下:
| opcode 宏 | 值 | sub_op(宏 = 值) | 用途 |
|---|---|---|---|
SDMA_NOP |
0x0 | — | 空操作 / 对齐占位 |
SDMA_OPCODE_COPY |
1 | SDMA_COPY_SUB_OPCODE_LINEAR = 0 |
linear 拷贝 |
SDMA_OPCODE_WRITE |
2 | SDMA_WRITE_SUB_OPCODE_LINEAR = 0 / SDMA_WRTIE_SUB_OPCODE_TILED = 1 |
写数据(linear / tiled) |
SDMA_OP_INDIRECT |
0x4 | — | 跳转执行另一段 IB |
SDMA_OP_PROTECTED_FENCE |
0x5 | SDMA_SUB_OP_PROTECTED_FENCE = 0x3 |
受保护 fence |
SDMA_OP_POLL_REGMEM |
8 | — | 轮询寄存器/内存直到条件满足 |
SDMA_OPCODE_ATOMIC |
10 | 无独立 sub_op;TC 原子 op 用 SDMA_ATOMIC_OPCODE(x) 编在 body |
原子操作(如 CMPSWAP) |
SDMA_OPCODE_CONSTANT_FILL |
11 | 无 sub_op;byte/DW fill 用 SDMA_CONSTANT_FILL_EXTRA_SIZE(x) 选 |
常量填充 |
和 PACKET3 对比一下位域布局,差异一目了然:
| PM4 TYPE3(CP) | SDMA | |
|---|---|---|
| header 宏 | PACKET3(op, n) |
SDMA_PACKET(op, sub_op, e) |
| opcode 位置 | bit 8-15 | bit 0-7 |
| 子操作 | 无(用不同 opcode) | bit 8-15 的 sub_op |
| 长度字段 | header 里的 count(body-1) | 无统一 count,长度由各 opcode 自定 body |
| opcode 空间 | PACKET3_* / IT_* |
SDMA_OPCODE_*(独立编号) |
注意 COPY 在 CP 和 SDMA 里都可能出现,但编号和 body 布局完全不同——不能跨引擎混用。
老架构(SI)位域整个不同
amd_sdma.h 里还有一套 SI 专用宏 SDMA_PACKET_SI(op, b, t, s, cnt),opcode 落在 bit 28,且编号自成一体:
| SI opcode 宏 | 值 |
|---|---|
SDMA_NOP_SI |
0xf |
SDMA_OPCODE_COPY_SI |
3 |
SDMA_OPCODE_CONSTANT_FILL_SI |
13 |
同一个 COPY,新架构是 1、SI 是 3——说明连同一个 SDMA 引擎,跨代包格式都会变。此外 byte-count 编码也随代际不同:AI 及更新的架构编码「字节数减一」,更老的编码原始字节数。
九、关键结论
- PM4 命令是二进制 packet:TYPE3 header 用
PACKET3(op, n)编码 type/opcode/count,n是 body dword 数减一,这个「+1 偏移」最容易算错。 - body 的每个控制位都有具名宏(
WRITE_DATA_DST_SEL、WR_CONFIRM、DMA_DATA_*…),可读性远胜裸魔数。 amdgpu_cmd_base是带游标的命令缓冲抽象,emit系列自带越界断言;attach_buf可直接写进最终要提交的 IB BO,避免多一次拷贝。- 短固定 packet 用裸数组,长/需对齐回填的序列用
cmd_base——拼好的这段内存,就是下一环命令提交要打包成 IB 的东西。 - PM4 只是 CP/GFX/Compute 的包格式;SDMA 用
SDMA_PACKET另有一套 opcode 与位域,opcode 空间独立、跨代还会变——不同引擎的 packet 不能混用。
更多推荐



所有评论(0)