第 5 章 通用层与 per-IP 层:函数指针解耦硬件差异

本章目标:先讲清 amdgpu 应对"十几代硬件"的核心武器------分层 + 函数指针 ;再以这套机制为尺标,横向扫一遍 v4 → v5 → v6 → v7 各代 SDMA 的实际差异。前半是"机制(怎么解耦)",后半是"实证(差在哪)",一体两面。

本章源码:amdgpu_sdma.c(通用层)、sdma_v4_0.c(per-IP 主线),以及 sdma_v5_0.c / v6_0.c / v7_0.clsdma_v6_0.c 等对比参照。

5.1 问题:一份代码,十几代硬件

回顾第 1 章的版本表:SDMA 从 v2_4、v3_0、v4_0,一路到 v6_0、v7_1,还有 LSDMA。每代硬件的寄存器布局、packet 细节、doorbell 方式都可能不同。如果每处都写 if (是 Vega) ... else if (是 Navi) ...,代码会彻底失控。

amdgpu 的解法是经典的面向接口编程思想,在 C 里用**函数指针表(ops table)**实现:

通用层只定义"要做什么"(接口)和"通用流程";每代硬件提供一张函数指针表,填入"具体怎么做"(实现)。运行时通过指针调用,自动分发到当前硬件的实现。

5.2 两层分工

复制代码
┌───────────────────────────────────────────────────────────┐
│ 通用层(generic,与硬件无关)                                 │
│   amdgpu_sdma.c / amdgpu_ring.c / amdgpu_vm_sdma.c ...    │
│   - 定义接口结构体:amdgpu_ring_funcs / buffer_funcs ...     │
│   - 提供通用 helper:get_instance_from_ring、init_microcode │
│   - 编排通用流程:提交、调度、fence 等待                       │
└──────────────────────────┬────────────────────────────────┘
                           │ 通过函数指针调用
┌──────────────────────────┴──────────────────────────────┐
│ per-IP 层(每代硬件一份实现)                               │
│   sdma_v4_0.c / sdma_v5_2.c / sdma_v6_0.c ...           │
│   - 填充函数指针表:sdma_v4_0_ring_funcs = { ... }         │
│   - 实现具体动作:get_wptr / emit_ib / emit_fence ...      │
└─────────────────────────────────────────────────────────┘

一句话:"通用层定义 what,per-IP 层实现 how。"

5.3 四张关键的函数指针表

每个 sdma_v*.c 本质上就是在填四张表。以 v4.0 为例:

函数指针表 定义在通用层 作用 v4.0 里的实例
amd_ip_funcs 通用 IP 生命周期(init/fini/suspend...) sdma_v4_0_ip_funcs
amdgpu_ring_funcs 通用 一条 ring 的全部操作 sdma_v4_0_ring_funcs / ..._page_ring_funcs
amdgpu_buffer_funcs 通用 拷贝/填充能力 sdma_v4_0_buffer_funcs
amdgpu_vm_pte_funcs 通用 页表更新能力 sdma_v4_0_vm_pte_funcs

后续章节各有对应:ip_funcs第 7 章)、ring_funcs第 8 章)、buffer_funcs

第 11 章)、vm_pte_funcs第 12 章)。本章先看它们如何"挂上去"。

5.4 ring_funcs:一条 ring 的操作接口

amdgpu_ring_funcs 是最核心的一张表------它定义了"对一条 ring 能做的所有操作"。看 v4.0的填法(节选):

c 复制代码
// sdma_v4_0.c
static const struct amdgpu_ring_funcs sdma_v4_0_ring_funcs = {
	.type = AMDGPU_RING_TYPE_SDMA,        // 这是一条 SDMA 类型的 ring
	.align_mask = 0xff,
	.nop = SDMA_PKT_NOP_HEADER_OP(SDMA_OP_NOP),
	.support_64bit_ptrs = true,
	.get_rptr = sdma_v4_0_ring_get_rptr,  // ← 第 2 章见过
	.get_wptr = sdma_v4_0_ring_get_wptr,  // ← 第 2 章见过
	.set_wptr = sdma_v4_0_ring_set_wptr,  // ← 第 2 章见过
	.emit_ib = sdma_v4_0_ring_emit_ib,        // ← 第 3 章见过
	.emit_fence = sdma_v4_0_ring_emit_fence,  // ← 第 3 章见过
	.emit_vm_flush = sdma_v4_0_ring_emit_vm_flush,
	.emit_hdp_flush = sdma_v4_0_ring_emit_hdp_flush,
	.test_ring = sdma_v4_0_ring_test_ring,    // ← 第 3 章见过
	.test_ib = sdma_v4_0_ring_test_ib,
	.insert_nop = sdma_v4_0_ring_insert_nop,
	.pad_ib = sdma_v4_0_ring_pad_ib,
	...
};

注意一个重要细节 :GFX queue 和 Page queue 用的是两张不同的表

c 复制代码
static const struct amdgpu_ring_funcs sdma_v4_0_ring_funcs = { ... };       // GFX
static const struct amdgpu_ring_funcs sdma_v4_0_page_ring_funcs = { ... };  // Page

对比会发现,两张表绝大部分一样,唯独 set_wptr / get_wptr 不同------GFX 用sdma_v4_0_ring_get_wptr,Page 用 sdma_v4_0_page_ring_get_wptr原因正是第 2 章说的:两条队列走不同的寄存器组(SDMA0_GFX_* vs SDMA0_PAGE_*)。 数据结构、硬件模型、函数指针在这里完美对上了。

5.5 表是怎么"挂"到实例上的:set_ring_funcs

光定义表还不够,要把表指针赋给每个实例的 ring.funcs / page.funcs。这由set_ring_funcs 完成:

c 复制代码
// sdma_v4_0.c: sdma_v4_0_set_ring_funcs()
for (i = 0; i < adev->sdma.num_instances; i++) {
	adev->sdma.instance[i].ring.funcs = &sdma_v4_0_ring_funcs;   // GFX 挂 GFX 表
	adev->sdma.instance[i].ring.me = i;
	if (adev->sdma.has_page_queue) {                             // 有 page 队列才挂
		adev->sdma.instance[i].page.funcs = &sdma_v4_0_page_ring_funcs;
		adev->sdma.instance[i].page.me = i;
	}
}

看到 has_page_queue 了吗?第 4 章那个"是否启用 page 队列"的总开关,在这里第一次发挥作用------只有为真时才给 page.funcs 赋值。

类似地还有:

  • sdma_v4_0_set_buffer_funcs() → 挂 adev->mman.buffer_funcs
  • sdma_v4_0_set_vm_pte_funcs() → 挂 adev->vm_manager.vm_pte_funcs
  • sdma_v4_0_set_irq_funcs() → 挂各中断源的处理函数

这些 set_*_funcs 全部在 early_init 阶段被调用(第 7 章)。"early_init 挂表,之后全程通过指针调用",是 amdgpu 每个 IP 的统一套路。

5.5.1 表里也能按版本再分叉

函数指针不是"一代一张"死板对应,同一份 per-IP 文件里也能按细分版本选不同表:

c 复制代码
// sdma_v4_0.c: sdma_v4_0_set_buffer_funcs()
if (amdgpu_ip_version(adev, SDMA0_HWIP, 0) >= IP_VERSION(4, 4, 0))
	amdgpu_sdma_set_buffer_funcs_scheds(adev, &sdma_v4_4_buffer_funcs);  // 4.4+
else
	amdgpu_sdma_set_buffer_funcs_scheds(adev, &sdma_v4_0_buffer_funcs);  // 4.0

两张 buffer_funcs 的差异仅在单次拷贝上限(1<<22 vs 1<<30),却能用同一套emit_copy_buffer 实现。这就是函数指针的弹性:共享大部分实现,只在必要处分叉。

5.6 通用层的 helper:与硬件无关的公共逻辑

amdgpu_sdma.c 里则是不依赖任何具体硬件的公共函数,谁都能用。几个代表:

从 ring 反查实例(双队列都认)

c 复制代码
// amdgpu_sdma.c
struct amdgpu_sdma_instance *amdgpu_sdma_get_instance_from_ring(struct amdgpu_ring *ring)
{
	struct amdgpu_device *adev = ring->adev;
	for (int i = 0; i < adev->sdma.num_instances; i++)
		if (ring == &adev->sdma.instance[i].ring ||   // 匹配 GFX queue
		    ring == &adev->sdma.instance[i].page)     // 也匹配 Page queue
			return &adev->sdma.instance[i];
	return NULL;
}

这个函数是"双 ring 设计"最好的注脚:给任意一条 ring,都能找回它属于哪个实例,无论它是 GFX 还是 Page 队列。中断处理、复位等场景大量用到它。

上下文保存区地址(抢占用)

c 复制代码
// amdgpu_sdma.c
uint64_t amdgpu_sdma_get_csa_mc_addr(struct amdgpu_ring *ring, unsigned int vmid)
{
	// SR-IOV / vmid==0 / 未开启 mcbp 时返回 0(不启用抢占 CSA)
	// 否则按实例 index 算出 CSA 在显存里的地址
}

CSA(Context Save Area)用于队列被抢占时保存/恢复上下文。这类逻辑与具体寄存器无关,所以放通用层。

其它通用 helper(后续章节展开)

  • amdgpu_sdma_init_microcode() / amdgpu_sdma_destroy_inst_ctx():固件加载/清理(第 9 章
  • amdgpu_sdma_ras_late_init() / amdgpu_sdma_process_ecc_irq():RAS/ECC(第 9 章
  • amdgpu_sdma_reset_engine():引擎复位(第 914 章)

5.7 一次调用的分发过程(把机制串起来)

以"发一个 IB"为例,看通用层如何分发到 v4.0 实现:

复制代码
上层代码
  amdgpu_ring_emit_ib(ring, ...)          // 通用层(amdgpu_ring.h 宏)
       │  展开为 ring->funcs->emit_ib(...)
       ▼
  ring->funcs == &sdma_v4_0_ring_funcs     // early_init 时挂上的
       │  .emit_ib == sdma_v4_0_ring_emit_ib
       ▼
  sdma_v4_0_ring_emit_ib(ring, ...)        // per-IP 层:真正写 INDIRECT packet

上层永远调用统一的 ring->funcs->emit_ib,运行时自动落到当前硬件的实现。换一代硬件,只需换一张表,上层代码一行不动。这就是 amdgpu 能优雅支撑十几代硬件的秘密。

5.8 横向:一份代码,十几代硬件(版本一览)

上面讲的是"机制",现在换个视角看"实证":正因为有这套函数指针分层,本仓库 drivers/gpu/drm/amd/amdgpu/ 才能用一致的骨架容纳十几代 SDMA。快速对照:

文件 硬件代 对应产品(示例)
si_dma.c / cik_sdma.c SI / CIK(legacy) Tahiti / Hawaii
sdma_v2_4.c / v3_0.c GFX8 Carrizo / Fiji、Polaris
sdma_v4_0.c GFX9(本系列主线) Vega、Raven
sdma_v4_4_2.c GFX9.4(MI 系) Aldebaran、Arcturus
sdma_v5_0.c / v5_2.c GFX10 / 10.3 Navi1x / Navi2x
sdma_v6_0.c / v7_0.c / v7_1.c GFX11 / GFX12 RDNA3 / RDNA4
lsdma_v6_0.c LSDMA 配套 GFX11/12

万变不离其宗:每个文件都在填 5.3 那四张表。 而且 opcode 语义(INDIRECT/FENCE/TRAP/...)跨代稳定------emit_ib 永远发 SDMA_OP_INDIRECTemit_fence 永远是 FENCE + TRAP。这正是 SDMA 抽象能长期复用的根基。

5.9 各代差异的三个观察点

带着"四张表哪里不一样"的问题去读,差异集中在三处:

  1. 寄存器命名与 packet 头宏 :GFX9 用 mmSDMA0_GFX_RB_* / mmSDMA0_PAGE_RB_*SDMA_PKT_HEADER_OP(...);GFX11+ 改成 regSDMA0_QUEUE0_RB_* 这类"统一队列"命名和 SDMA_PKT_COPY_LINEAR_HEADER_OP(...) 新宏。只是命名与布局的演进,指针管理语义(RPTR 归硬件、WPTR 归驱动 + doorbell)完全一致第 2 章)。
  2. page queue 的去留 (最值得注意的架构差异):本树里只有 sdma_v4_0.c(GFX9)和 sdma_v4_4_2.c(GFX9.4)置 has_page_queue = true,即"一实例两队列"的经典形态;GFX10+(v5/v6/v7)不再依赖常驻内核 page ring,随 MES / user queue 架构演进走不同调度路径。但 SDMA"拷贝 + 页表更新"两大职责没变(直接影响第 12 章页表更新走哪条 ring)。
  3. 多实例 + 分区 :消费级(Navi/RDNA)通常 1~2 个实例;数据中心 GPU(sdma_v4_4_2.c)则 num_instances 可达十几个,实例与 AID/XCC 分区 绑定(aid_id/xcc_id),这是它比 v4.0 复杂的主要来源(第 4 章)。

另外两个"边角"知道定位即可:LSDMAlsdma_v6_0.c 起)是轻量、同步、寄存器编程(PIO)的辅助拷贝引擎,用于复位等底层场景,与主 SDMA 不是一回事;legacy 的 si_dma.c / cik_sdma.c 结构思想一脉相承,理解 v4.0 后再看会觉得"就是简化版"。

5.10 速读陌生 sdma_vX.c 的五问套路

拿到一个没读过的版本,按这五问就能十几分钟摸清骨架(对应回调细节由后续章节展开):

  1. _ip_funcs:生命周期回调有无新增/改名(第 7 章)。
  2. _ring_funcs:重点比 get/set_wptr(寄存器变了没)和 emit_*(packet 宏变了没)(第 8 章)。
  3. has_page_queue:判断是不是双 ring(影响第 12 章)。
  4. _buffer_funcscopy_max_bytes:单次拷贝上限(影响 TTM 迁移分片,第 11 章)。
  5. _vm_pte_funcs:确认页表操作实现(第 12 章)。

5.11 本章小结

  • amdgpu 用 分层 + 函数指针 应对多代硬件:通用层定 what,per-IP 层实现 how
  • 每个 sdma_v*.c 主要就是填四张表:ip_funcs / ring_funcs / buffer_funcs / vm_pte_funcs
  • GFX 和 Page 队列用两张 ring_funcs ,差异集中在 get_wptr / set_wptr,根源是两套寄存器------硬件、结构、函数指针三者一致。
  • set_*_funcs(在 early_init 调用)负责把表挂到实例/设备上;has_page_queue 决定是否挂 page 表。
  • amdgpu_sdma.c 提供与硬件无关的通用 helper,如 get_instance_from_ring(双队列都认)。
  • 各代 SDMA 共享同一套四张表骨架 ,opcode 语义跨代稳定;主要差异在寄存器/宏命名 (GFX9 mmSDMA0_GFX/PAGE_* → GFX11+ regSDMA0_QUEUE0_*)。
  • page queue 是 GFX9(v4.x)的经典形态 (本树仅 v4_0、v4_4_2 置 has_page_queue=true);GFX10+ 随 MES/user queue 演进。多实例 + AID/XCC 分区 是 MI 系复杂度的主来源;LSDMA 是轻量、同步、PIO 的辅助引擎。

下一章预告 :既然 SDMA 对外是一条条 ring,那它是怎么接入 amdgpu 通用的ring / IB / GPU scheduler / fence 体系的?第 6 章我们理清 amdgpu_ringamdgpu_ibamdgpu_job 三者的关系,看一次提交从调度器到硬件的完整数据流。