资讯动态

6.4 命令字组装:PM4 与 cmd_base

发布时间:2026/8/23 18:46:52 来源:尧图企业网站定制
上一篇准备好了内存这一篇解决 6.1 里的第一个难题GPU 命令不是函数调用而是一段按格式排布的二进制 packet。要让 GPU 干活你得往一段内存里逐个 32-bit dword 拼出 PM4Programmable Machine 4packet再把这段内存当成 IB 提交。这一篇讲两件事amd_PM4.h提供的 packet 编码宏以及amdgpu_cmd_base这个「往 buffer 里安全追加 dword」的小抽象。一、PM4 packet 长什么样PM4 命令流由一个个 packet 组成每个 packet 第一个 dword 是 header高 2 位是 packet type。测试里几乎只用 TYPE3带 opcode 的引擎命令#definePACKET_TYPE00#definePACKET_TYPE33#definePACKET3(op,n)((PACKET_TYPE330)|\(((op)0xFF)8)|\((n)0x3FFF)16)一个 TYPE3 header 编码三件事typebit 30-31、opcodebit 8-15、以及body dword 数减一的 countbit 16-29。也就是说PACKET3(op, n)里的n是「后面还有 n1 个 body dword」。这个「1 偏移」是最常见的踩坑点。常用 opcode 都有宏例如#definePACKET3_NOP0x10#definePACKET3_WRITE_DATA0x37#definePACKET3_DMA_DATA0x50amd_PM4.h一共给了 21 个 TYPE3 opcode 宏按用途归类如下opcode 宏值用途归类PACKET3_NOP0x10空操作 / 占位对齐通用PKT3_CLEAR_STATE0x12清引擎状态状态PACKET3_DISPATCH_DIRECT0x15直接派发 compute grid计算PACKET3_ATOMIC_MEM0x1E对显存做原子操作如 CMPSWAP内存PKT3_CONTEXT_CONTROL0x28控制上下文加载/影子状态PACKET3_DRAW_INDEX_AUTO0x2D自动索引绘制图形PACKET3_WRITE_DATA0x37往寄存器/内存写数据内存PACKET3_WAIT_REG_MEM0x3C轮询寄存器/内存直到条件满足同步PACKET3_INDIRECT_BUFFER0x3F跳转执行另一段 IB通用PACKET3_EVENT_WRITE0x46触发/记录 GPU 事件同步PACKET3_DMA_DATA0x50CP DMA 拷贝/填充内存PACKET3_ACQUIRE_MEM0x58cache flush/invalidate 屏障同步PACKET3_SET_CONTEXT_REG0x69写 context 寄存器状态PKT3_SET_SH_REG0x76写 SHshader寄存器状态PKT3_SET_SH_REG_INDEX0x9B带 index 写 SH 寄存器状态PACKET3_SET_UCONFIG_REG0x79写 uconfig 寄存器状态PACKET3_INCREMENT_CE_COUNTER0x84CE 计数器自增CE/DE 同步PACKET3_WAIT_ON_CE_COUNTER0x86DE 等待 CE 计数器CE/DE 同步PACKET3_SET_CE_DE_COUNTERS0x89设置 CE/DE 计数器CE/DE 同步PACKET3_PROTECTED_FENCE_SIGNAL0xD0受保护 fence 置位同步PACKET3_FENCE_WAIT_MULTI0xD1等待多个 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为例#defineWRITE_DATA_DST_SEL(x)((x)8)/* 0reg 1mem(sync) 5mem(async) ... */#defineWR_ONE_ADDR(116)#defineWR_CONFIRM(120)/* 等写完成再继续 */#defineWRITE_DATA_ENGINE_SEL(x)((x)30)/* 0me 1pfp 2ce */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,2ring_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(jring_context-write_length/4)ring_context-pm4[i]func-deadbeaf;*pm4_dwi;逐字读这段headerWRITE_DATAcount 2 payload_dw。这个 2 对应后面「控制 dword 地址低32 地址高32」里除去 header 的固定开销body 控制 地址lo 地址hi N 个数据count 编码的是 body-1这里按该 packet 约定算出。控制 dwordDST_SEL(5) 异步内存目标WR_CONFIRM 写完确认。地址把 GPU VAbo_mc就是上一篇mc_address拆成高低 32 位。数据循环写入func-deadbeaf。最后回填*pm4_dw 实际用了多少个 dword供提交阶段知道 IB 长度。注意它前面还有 DWORD 对齐和缓冲越界的断言守护——手拼 packet 最怕的就是长度算错、越界这些 guard 是必要的自我保护。四、cmd_base把「往 buffer 追加 dword」抽象出来上面的例子用裸pm4[i]手动维护下标。更结构化的写法是struct amdgpu_cmd_base——一个带游标的命令缓冲抽象让你emit而不是自己管下标structamdgpu_cmd_base{uint32_tcdw;/* 已用 dword 数游标 */uint32_tmax_dw;/* 容量 */uint32_t*buf;/* 底层缓冲 */bool is_assigned_buf;int(*allocate_buf)(structamdgpu_cmd_base*base,uint32_tsize);int(*attach_buf)(structamdgpu_cmd_base*base,void*ptr,uint32_tsize_bytes);void(*emit)(structamdgpu_cmd_base*base,uint32_tvalue);void(*emit_aligned)(structamdgpu_cmd_base*base,uint32_tmask,uint32_tvalue);void(*emit_repeat)(structamdgpu_cmd_base*base,uint32_tvalue,uint32_tn);void(*emit_at_offset)(structamdgpu_cmd_base*base,uint32_tvalue,uint32_toff);void(*emit_buf)(structamdgpu_cmd_base*base,constvoid*ptr,uint32_toff,uint32_tsz);};核心就是cdw游标emit追加。emit的实现极简但带越界断言staticvoidcmd_emit(structamdgpu_cmd_base*base,uint32_tvalue){assert(base-cdwbase-max_dw);base-buf[base-cdw]value;}其它 emit 变体各有用途方法作用emit追加一个 dword游标 1emit_aligned反复 emit 直到游标满足对齐掩码填 NOP 用emit_repeat连续 emit 同一个值 N 次emit_at_offset往「游标 偏移」处写值但不移动游标回填占位用emit_bufmemcpy 一段外部数据进缓冲五、这些回调由谁实现结构体里这一堆函数指针实现全部在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()一次性绑定并返回一个初始化好的实例structamdgpu_cmd_base*get_cmd_base(void){structamdgpu_cmd_base*basecalloc(1,sizeof(*base));/* cdw/max_dw/buf 清零is_assigned_buffalse */base-allocate_bufcmd_allocate_buf;base-attach_bufcmd_attach_buf;base-emitcmd_emit;base-emit_alignedcmd_emit_aligned;base-emit_repeatcmd_emit_repeat;base-emit_at_offsetcmd_emit_at_offset;base-emit_bufcmd_emit_buf;returnbase;}调用方从不自己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 还是 attachcmd_base的缓冲可以「自己分配」或「借用外部内存」这决定谁负责 free/* 自己 callocfree 时由 cmd_base 释放 */staticintcmd_allocate_buf(structamdgpu_cmd_base*base,uint32_tsize_dw);/* 借用外部指针如已映射的 IB BOis_assigned_buftruecmd_base 不 free */staticintcmd_attach_buf(structamdgpu_cmd_base*base,void*ptr,uint32_tsize_bytes);attach_buf特别有用直接把上一篇分配的 IB BO 的 CPU 映射地址挂上来之后所有emit就直接写进那块最终要提交的 GPU 内存省掉一次拷贝。释放逻辑据此区分voidfree_cmd_base(structamdgpu_cmd_base*base){if(base){if(base-bufbase-is_assigned_buffalse)free(base-buf);free(base);}}is_assigned_buf为真借用外部时只释放结构体本身不碰那块外部缓冲——因为它的所有权在别处。七、两种写法怎么选同一件事拼 PM4有两条路取舍很清楚适合适合拼 PM4 的两种写法裸 pm4[i]手动下标 手写 guardcmd_base emit游标托管 自带越界断言ip_funcs 里短小固定的 packet如 write_linear/copy_linear较长、需对齐/回填/拼接外部数据的序列如 compute dispatch 组包ip_funcs里那些短小的write_linear/copy_linear/const_fill用裸数组 断言就够直观。稍长、需要对齐填充、需要先占位再回填、或要把外部数据块拼进来的场景用cmd_base的emit_*系列更省心也不容易越界。八、附SDMA packet —— 另一套独立编码前面讲的PACKET3是CPCommand Processor即 GFX/Compute 引擎专用的 TYPE3 包。但 SDMA拷贝引擎并不认 PM4它有自己一套完全独立的包格式——同一个 opcode 数字在两套体系里含义毫不相干。amd_sdma.h定义了 SDMA 的 header 宏#defineSDMA_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_NOP0x0—空操作 / 对齐占位SDMA_OPCODE_COPY1SDMA_COPY_SUB_OPCODE_LINEAR 0linear 拷贝SDMA_OPCODE_WRITE2SDMA_WRITE_SUB_OPCODE_LINEAR 0 /SDMA_WRTIE_SUB_OPCODE_TILED 1写数据linear / tiledSDMA_OP_INDIRECT0x4—跳转执行另一段 IBSDMA_OP_PROTECTED_FENCE0x5SDMA_SUB_OP_PROTECTED_FENCE 0x3受保护 fenceSDMA_OP_POLL_REGMEM8—轮询寄存器/内存直到条件满足SDMA_OPCODE_ATOMIC10无独立 sub_opTC 原子 op 用SDMA_ATOMIC_OPCODE(x)编在 body原子操作如 CMPSWAPSDMA_OPCODE_CONSTANT_FILL11无 sub_opbyte/DW fill 用SDMA_CONSTANT_FILL_EXTRA_SIZE(x)选常量填充和PACKET3对比一下位域布局差异一目了然PM4 TYPE3CPSDMAheader 宏PACKET3(op, n)SDMA_PACKET(op, sub_op, e)opcode 位置bit 8-15bit 0-7子操作无用不同 opcodebit 8-15 的 sub_op长度字段header 里的 countbody-1无统一 count长度由各 opcode 自定 bodyopcode 空间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_SI0xfSDMA_OPCODE_COPY_SI3SDMA_OPCODE_CONSTANT_FILL_SI13同一个COPY新架构是 1、SI 是 3——说明连同一个 SDMA 引擎跨代包格式都会变。此外 byte-count 编码也随代际不同AI 及更新的架构编码「字节数减一」更老的编码原始字节数。九、关键结论PM4 命令是二进制 packetTYPE3 header 用PACKET3(op, n)编码 type/opcode/countn是 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 不能混用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价