上一篇准备好了内存,这一篇解决 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 同步

测试里高频出现的其实就 NOPWRITE_DATADMA_DATAWAIT_REG_MEMATOMIC_MEMDISPATCH_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;

逐字读这段:

  1. headerWRITE_DATA,count = 2 + payload_dw。这个 2 对应后面「控制 dword + 地址低32 + 地址高32」里除去 header 的固定开销(body = 控制 + 地址lo + 地址hi + N 个数据,count 编码的是 body-1,这里按该 packet 约定算出)。
  2. 控制 dwordDST_SEL(5) = 异步内存目标,WR_CONFIRM = 写完确认。
  3. 地址:把 GPU VA(bo_mc,就是上一篇 mc_address)拆成高低 32 位。
  4. 数据:循环写入 func->deadbeaf
  5. 最后回填 *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)有两条路,取舍很清楚:

适合

适合

拼 PM4 的两种写法

裸 pm4[i++]
手动下标 + 手写 guard

cmd_base emit
游标托管 + 自带越界断言

ip_funcs 里短小固定的 packet
如 write_linear/copy_linear

较长、需对齐/回填/拼接外部数据的序列
如 compute dispatch 组包

  • ip_funcs 里那些短小的 write_linear/copy_linear/const_fill,用裸数组 + 断言就够直观。
  • 稍长、需要对齐填充、需要先占位再回填、或要把外部数据块拼进来的场景,用 cmd_baseemit_* 系列更省心,也不容易越界。

八、附:SDMA packet —— 另一套独立编码

前面讲的 PACKET3CP(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 位扩展字段 eamd_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_SELWR_CONFIRMDMA_DATA_*…),可读性远胜裸魔数。
  • amdgpu_cmd_base 是带游标的命令缓冲抽象,emit 系列自带越界断言;attach_buf 可直接写进最终要提交的 IB BO,避免多一次拷贝。
  • 短固定 packet 用裸数组,长/需对齐回填的序列用 cmd_base——拼好的这段内存,就是下一环命令提交要打包成 IB 的东西。
  • PM4 只是 CP/GFX/Compute 的包格式;SDMA 用 SDMA_PACKET 另有一套 opcode 与位域,opcode 空间独立、跨代还会变——不同引擎的 packet 不能混用。
Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐