intel gpu ppgtt虚拟地址

937 阅读12分钟

intel gpu 支持多种映射方式:

  1. Global GTT with 32b virtual addressing: Global GTT usage is similar to previous generations withextended capability of increasing virtual address (VA) up to 4GB (from 2GB) and use a standard 64b PTE format. The breakdown of the PTE for global GTT is given in later sections and allows 1-level page walk where the 20b index is used to select the 64b PTE from memory.
  2. Legacy 32b VA with ppGTT: This is a mode where ppGTT page tables are considered private and managed via GFX sotfware (driver) where context is tagged as Legacy 32b VA. Each page walk is managed via 9b of the virtual address and 20b index to address 4GB memory space is broken into 3 parts. In order to optimize the walks and make it look like previous generations, GFX sotfware provides 4 pointers to page tables (called 4 PDP entries) all guest physical address. GPU uses the four pointers and fetches the 4x4KB into h/w (for render and media) before the context execution starts. The optimization limits the dynamic (on demand) page walks to 1-level only.
  3. Legacy 48b VA with ppGTT: GFX address expansion beyond 4GB is added to address 48b virtual address space. 48b VA requires 36b indexing (4x9b) translating into 4-levels of page walk. To reduce the overhead of 4 level walk, GPU will cache the entire content of PML4 (4kB) to limit the on-demand walks to 3 levels. The caching happens as part of the initial demand where no further replacements required.
  4. Advanced 48b VA with IA32e support via IOMMU: 48b addressing in advanced mode is managed via IOMMU settings where the base of the page table shall be found after the root /context tables using bus/device/function values. PASID# is used as an index in PASID table to find page table pointer to start the 4-level page walk. Rest of the mechanism is similar to Legacy 48b VA mode, GPU has the capability to cache entire content of PML4 and try to limit the dynamicpage walks to 3-level

ppgtt

intel从gen6 开始支持ppgtt(Per-Process Graphics Translation Table)支持32位虚拟地址和48位虚拟地址映射。和cpu 虚拟地址类似,gpu 侧的地址映射也是多级映射,建立转换表,gpu 的叫做 graphics translation table。 每个进程的叫做ppgtt。

虚拟地址转换流程图

ppgtt 32位虚拟地址映射4K页面地址转换流程图:

image.png ppgtt 32位虚拟地址映射64K页面地址转换流程图:

image.png

32位地址需要2级转换表。

ppgtt 48位虚拟地址映射4K页面:

image.png

ppgtt 48位虚拟地址映射64K页面:

image.png

48位虚拟地址映射使用了4级页表。 32位和48位虚拟地址 转换表的基地址寄存器也不一样。 32位地址使用了PDP0-3. 48位虚拟地址使用了PML4

/*
 * GEN8 32b style address is defined as a 3 level page table:
 * 31:30 | 29:21 | 20:12 |  11:0
 * PDPE  |  PDE  |  PTE  | offset
 * The difference as compared to normal x86 3 level page table is the PDPEs are
 * programmed via register.
 *
 * GEN8 48b style address is defined as a 4 level page table:
 * 47:39 | 38:30 | 29:21 | 20:12 |  11:0
 * PML4E | PDPE  |  PDE  |  PTE  | offset
 */

进程gtt映射表基地址写入

用户在OpenGL会创建一个context,在intel 用户态驱动mesa中,用户context 会对应创建一个gem context。 内核驱动根据硬件有多少个引擎(video,render,blitter等等) 创建多少个intel context(驱动内部使用)。那个引擎对应一个intel context。 每个intel context 会创建出一个Logical Context, Logical Context 保存了在硬件上切换任务时候的上下文,保存了一些硬件的寄存器状态。函数intel_engine_context_size 用来获取intel 不同genX Logical Context的大小,最新gen12 Logical Context 14个page size。Logical Context 包含三个部分:

• Per-Process HW Status Page (4K) • Ring Context (Ring Buffer Control Registers, Page Directory Pointers, etc.) • Engine Context ( PipelineState, Non-pipelineState, Statistics, MMIO)

Ring Context 中保存了Page Directory Pointers,也就是用来保存转换表的基地址。 ppgtt 转换分配在内存中。 在驱动中 i915_gem_create_context ->i915_ppgtt_create->gen8_ppgtt_create。 也就是一个用户态的gem context 有一个ppgtt 的映射转换表。 驱动中存在gen6 和gen8 两种ppgtt 接口实现。 gen8之后都使用的gen8 ppgtt接口。

static struct i915_gem_context *
i915_gem_create_context(struct drm_i915_private *i915, unsigned int flags)
{
    struct i915_gem_context *ctx;
     ......
      ctx = __create_context(i915);
  
        ppgtt = i915_ppgtt_create(&i915->gt); //创建ppgtt 图形地址映射表
        if (IS_ERR(ppgtt)) {
                drm_dbg(&i915->drm, "PPGTT setup failed (%ld)\n",
                        PTR_ERR(ppgtt));
                context_close(ctx);
                return ERR_CAST(ppgtt);
        }

        mutex_lock(&ctx->mutex);
        __assign_ppgtt(ctx, &ppgtt->vm);//将创建ppgtt 时候初始化的vm 赋值给ctx->vm
        mutex_unlock(&ctx->mutex);
.....

ppgtt 指向的内存 是一个在system 上分配的buffer

/*
 * GEN8 legacy ppgtt programming is accomplished through a max 4 PDP registers
 * with a net effect resembling a 2-level page table in normal x86 terms. Each
 * PDP represents 1GB of memory 4 * 512 * 512 * 4096 = 4GB legacy 32b address
 * space.
 *
 */
struct i915_ppgtt *gen8_ppgtt_create(struct intel_gt *gt)
{
	struct drm_i915_private *i915 = gt->i915;
	struct i915_ppgtt *ppgtt;
	int err;

	ppgtt = kzalloc(sizeof(*ppgtt), GFP_KERNEL);
	if (!ppgtt)
		return ERR_PTR(-ENOMEM);

	ppgtt_init(ppgtt, gt);
	ppgtt->vm.top = i915_vm_is_4lvl(&ppgtt->vm) ? 3 : 2;
        
        .........省略

i915_vm_is_4lvl 用来判断当前的vm 是否支持超过32位的,这个根据代码中的ppgtt_size 写死读取,例如gen8

#define GEN8_FEATURES \
	G75_FEATURES, \
	GEN(8), \
	.has_logical_ring_contexts = 1, \
	.dma_mask_size = 39, \
	.ppgtt_type = INTEL_PPGTT_FULL, \
	.ppgtt_size = 48, \
	.has_64bit_reloc = 1, \
	.has_reset_engine = 1

ppgtt_init函数 初始化了vm 同时绑定了vma 的操作函数。

void ppgtt_init(struct i915_ppgtt *ppgtt, struct intel_gt *gt,
		unsigned long lmem_pt_obj_flags)
{
	struct drm_i915_private *i915 = gt->i915;

	ppgtt->vm.gt = gt;
	ppgtt->vm.i915 = i915;
	ppgtt->vm.dma = i915->drm.dev;
	ppgtt->vm.total = BIT_ULL(RUNTIME_INFO(i915)->ppgtt_size);
	ppgtt->vm.lmem_pt_obj_flags = lmem_pt_obj_flags;

	dma_resv_init(&ppgtt->vm._resv);
	i915_address_space_init(&ppgtt->vm, VM_CLASS_PPGTT);

	ppgtt->vm.vma_ops.bind_vma    = ppgtt_bind_vma;
	ppgtt->vm.vma_ops.unbind_vma  = ppgtt_unbind_vma;
}

初始化了i915中的vm(i915_address_space)

struct i915_address_space {
	struct kref ref;
	struct rcu_work rcu;

	**struct drm_mm mm;**
	struct intel_gt *gt;
	struct drm_i915_private *i915;
	struct device *dma;
	/*
	 * Every address space belongs to a struct file - except for the global
	 * GTT that is owned by the driver (and so @file is set to NULL). In
	 * principle, no information should leak from one context to another
	 * (or between files/processes etc) unless explicitly shared by the
	 * owner. Tracking the owner is important in order to free up per-file
	 * objects along with the file, to aide resource tracking, and to
	 * assign blame.
	 */
	struct drm_i915_file_private *file;
	u64 total;		/* size addr space maps (ex. 2GB for ggtt) */
	u64 reserved;		/* size addr space reserved */
        
        ..... 省略

i915 中的i915_address_space 类似于cpu 侧 linux 下的mm_struct. i915_address_space 相关的mm 红黑树在struct drm_mm mm 中维护。和cpu 侧类似,i915虚拟地址中也有vma,i915_vma。i915 vma 地址空间主要是使用drm_mm_node

	struct drm_mm_node node;

	struct i915_address_space *vm;
	const struct i915_vma_ops *ops;

         。。。。。。省略

前面也提到过一个应用可以打开多个引擎,比如opengl 会使用rcs 和blitter 引擎。使用每个引擎会创建单独与引擎关联的intel context。一个gem context 结构绑定了多个intel context(每个引擎一个intel context),intel context 初始化的时候将ppgtt 地址 写入到自己的logical ring context 对应的buffer 地址中。 i915_gem_create_context->__create_context->default_engines->intel_context_create 创建intel context

static struct i915_gem_engines *default_engines(struct i915_gem_context *ctx)
{
   .....
	for_each_engine(engine, gt, id) {
		struct intel_context *ce;

		if (engine->legacy_idx == INVALID_ENGINE)
			continue;

		GEM_BUG_ON(engine->legacy_idx >= I915_NUM_ENGINES);
		GEM_BUG_ON(e->engines[engine->legacy_idx]);

		ce = intel_context_create(engine);
		if (IS_ERR(ce)) {
			__free_engines(e, e->num_engines + 1);
			return ERR_CAST(ce);
		}

		intel_context_set_gem(ce, ctx);

		e->engines[engine->legacy_idx] = ce;
		e->num_engines = max(e->num_engines, engine->legacy_idx);
	}
 ...
}

intel i915驱动中使用context,vma,vm 等等都要先pin再unpin。pin引用计数加1 并且做一些初始化。 在i915的驱动中intel_context_pin函数将会检测context 上下文保存的buffer 有没有分配。没有的话调用 state 分配函数,分配上下文保存的buffer后将ppgtt 写入特定位置。

intel_context_pin->execlists_context_pin->lrc_pin->lrc_init_state ->__lrc_init_regs->init_ppgtt_regs

static void init_ppgtt_regs(u32 *regs, const struct i915_ppgtt *ppgtt)
{
	if (i915_vm_is_4lvl(&ppgtt->vm)) { //48位虚拟地址使用
		/* 64b PPGTT (48bit canonical)
		 * PDP0_DESCRIPTOR contains the base address to PML4 and
		 * other PDP Descriptors are ignored.
		 */
		ASSIGN_CTX_PML4(ppgtt, regs);
	} else {
		ASSIGN_CTX_PDP(ppgtt, regs, 3);
		ASSIGN_CTX_PDP(ppgtt, regs, 2);
		ASSIGN_CTX_PDP(ppgtt, regs, 1);
		ASSIGN_CTX_PDP(ppgtt, regs, 0);
	}
}

32位虚拟地址 :ASSIGN_CTX_PDP 0到3 对应上面地址转换流程图中的PDPE0-3 48位虚拟地址: PML4寄存器

PDP0和PML4是同一个寄存器实现。 38到12bit 用来指向基地址。

image.png

PDP1寄存器

image.png

PDP2 PDP3 和之前寄存器一样, 四个寄存器一个表示1G空间,4个正好32位的4G空间。

drm mm 结构

intel 虚拟地址分配 继承自DRM MM Range Allocator。vm和vma分别对应drm_mm 和drm_mm_node结构。 drm提供了一个简单的地址空间分配器,drm_mm是此功能主要维护结构。drm_mm_node 维护一个已经被分配出去的node 节点。 node 中包含了一块连续的内存地址。这段被分配出去连续的内存地址也称为空洞。

struct drm_mm {
	struct list_head hole_stack; // 所有含有空洞的节点的链表
	struct drm_mm_node head_node; // 所有的已分配的节点的链表, 按照起始地址升序排序
	struct list_head unused_nodes; // drm_mm_node结构体的cache链表
	int num_unused; // cache链表中的个数
	spinlock_t unused_lock; // 用于保护cache链表
	unsigned int scan_check_range : 1; // 用户是否指定地址的范围
	unsigned scan_alignment; // 检查的对齐要求
	unsigned long scan_color; // 检查的color设置
	unsigned long scan_size; // 希望的空洞的大小
	unsigned long scan_hit_start; // 标记找到的空洞的起始地址
	unsigned long scan_hit_end; // 标记找到的空洞的尾地址
	unsigned scanned_blocks; // 记录经过检查的节点的个数
	unsigned long scan_start; // 用户指定的范围的开始地址
	unsigned long scan_end; // 用户指定的范围的结束地址
	struct drm_mm_node *prev_scanned_node; // 所有经过检查的节点的链表

	void (*color_adjust)(struct drm_mm_node *node, unsigned long color,
			     unsigned long *start, unsigned long *end); // 对符合的条件的节点进行可选的地址范围的调整
};
struct drm_mm_node {
	struct list_head node_list; // 在drm_mm的全局链表中的位置
	struct list_head hole_stack; // 若该节点后面存在空洞,在表示在drm_mm的hole_stack的位置
	unsigned hole_follows : 1; // 该节点后面是否存在空洞
	unsigned scanned_block : 1; // 检查器是否检查过该节点
	unsigned scanned_prev_free : 1; // 暂时没用
	unsigned scanned_next_free : 1; // 暂时没用
	unsigned scanned_preceeds_hole : 1; // 保存在检查之前该节点的前一个节点是否有空洞,用于状态恢复
	unsigned allocated : 1; // 该节点是否被分配使用
	unsigned long color; // 用于地址调整的颜色值
	unsigned long start; // 占用的地址范围的开始
	unsigned long size; // 占用的地址范围的结束
	struct drm_mm *mm; // 指向所属的分配器
};

drm_mm_reserve_node 执行分配node 虚拟地址的start 和size 如果此地址已经被分配给其他人则返回失败。 drm_mm_insert_node_in_range 随机选择一个满足size 的空洞 然后插入node到mm 的树中

来自blog.totorow.xyz/posts/gfx_d…

地址插入页表

在驱动中分配的一个buffer 都是一个obj。 读取和写入obj时候 需要给obj 分配vma(这个vma 是给gpu 访问使用), 将obj 对应的物理page 转换成虚拟地址。首先是创建一个vma,vma_create 函数创建一个vma 同时把vma 和obj,vm 等绑定起来。 i915_vma_pin 函数, pin vma 时候先判断当前的obj 是否已经获取到页表了,然后根据vm 分配一个vma 虚拟地址空间, 将这块range 插入到vm 中维护起来。

int i915_vma_pin(struct i915_vma *vma, u64 size, u64 alignment, u64 flags)
{
	struct i915_vma_work *work = NULL;
	intel_wakeref_t wakeref = 0;
	unsigned int bound;
	int err;

       ........
	err = vma_get_pages(vma);//obj 物理页没分配先分配物理page
	if (err)
		return err;
      .......

	err = i915_active_acquire(&vma->active);
	if (err)
		goto err_unlock;

	if (!(bound & I915_VMA_BIND_MASK)) {
		err = i915_vma_insert(vma, size, alignment, flags); //分配一个node 插入到vm中
	.............
	}

	GEM_BUG_ON(!vma->pages);
	err = i915_vma_bind(vma,
			    vma->obj ? vma->obj->cache_level : 0,
			    flags, work);
.............

drm mm 插入node

i915_vma_insert 首先判断vm 的大小是否满足要求,如果size 大于vm 总大小直接返回错误。然后根据有没有设置PIN_OFFSET_FIXED 标志,如果设置了此标志 必须使用执行的起始地址位置分配一个vma 空间。mesa 用户态分配的bo 会自己分配个虚拟地址,exec 命令下发batch buffer 会强制执行驱动使用用户态分配的虚拟地址分配vma。 设置了PIN_OFFSET_FIXED 标志 会调用i915_gem_gtt_reserve 然后先执行 drm_mm_reserve_node 如果空间已经被人使用 会调用调用i915_gem_evict_for_vma 把这个空间的vma 驱逐掉。然后再执行drm_mm_reserve_node 插入一个包含start end 的node到vm中。

int i915_gem_evict_for_node(struct i915_address_space *vm,
			    struct drm_mm_node *target,
			    unsigned int flags)
{
	LIST_HEAD(eviction_list);
	struct drm_mm_node *node;
	u64 start = target->start;
	u64 end = start + target->size;
        。。。。。

	if (i915_vm_has_cache_coloring(vm)) { // ggtt 有 pgtt 没有
		/* Expand search to cover neighbouring guard pages (or lack!) */
		if (start)
			start -= I915_GTT_PAGE_SIZE;

		/* Always look at the page afterwards to avoid the end-of-GTT */
		end += I915_GTT_PAGE_SIZE;
	}
	GEM_BUG_ON(start >= end);

	drm_mm_for_each_node_in_range(node, &vm->mm, start, end) {
		/* If we find any non-objects (!vma), we cannot evict them */
		if (node->color == I915_COLOR_UNEVICTABLE) {//I915_COLOR_UNEVICTABLE 设置不可驱逐标志的直接返回
			ret = -ENOSPC;
			break;
		}

		GEM_BUG_ON(!drm_mm_node_allocated(node));
		vma = container_of(node, typeof(*vma), node);

	        。。。。
		if (flags & PIN_NONBLOCK && //设置不阻塞标志 不等待vma 绑定的rq 完成
		    (i915_vma_is_pinned(vma) || i915_vma_is_active(vma))) {
			ret = -ENOSPC;
			break;
		}

		/* Overlap of objects in the same batch? */
		if (i915_vma_is_pinned(vma)) {
			ret = -ENOSPC;
			if (vma->exec_flags &&
			    *vma->exec_flags & EXEC_OBJECT_PINNED)
				ret = -EINVAL;
			break;
		}

            。。。。。。
		__i915_vma_pin(vma);
		list_add(&vma->evict_link, &eviction_list);
	}

	list_for_each_entry_safe(vma, next, &eviction_list, evict_link) {
		__i915_vma_unpin(vma);
		if (ret == 0)
			ret = __i915_vma_unbind(vma); //使用vma sync 等待对应的request任务完成释放vma 空闲处这块空间
	}

	return ret;
}

没有设置FIXD 固定start 位置时候调用 drm_mm_insert_node_in_range 随机选择一个位置。

ppgtt 页表填充

i915_vma_pin 执行完i915_vma_insert 给drm_mm 的node 插入后, 执行 i915_vma_bind 函数填充ppgtt 的pte。 前面执行完i915_vma_insert 是drm mm struct 使用红黑树维护地址空间,和cpu 使用mm struct 一样。 i915_vma_bind 会调用 ppgtt_bind_vma->gen8_ppgtt_insert 如果vma 对应的obj 的page 大于4K 就调用大页的方法gen8_ppgtt_insert_huge否则使用gen8_ppgtt_insert_pte. 无论哪种方式都需要设置pd pte 除了地址空间位 其他的位。 和cpu 地址映射一样剩余的位会用来设置读写权限或者cache 层等等

static u64 gen8_pte_encode(dma_addr_t addr,
			   enum i915_cache_level level,
			   u32 flags)
{
	gen8_pte_t pte = addr | GEN8_PAGE_PRESENT | GEN8_PAGE_RW;
	if (unlikely(flags & PTE_READ_ONLY))
		pte &= ~GEN8_PAGE_RW;
	if (flags & PTE_LM)
		pte |= GEN12_PPGTT_PTE_LM;
	switch (level) {
	case I915_CACHE_NONE:
		pte |= PPAT_UNCACHED;
		break;
	case I915_CACHE_WT:
		pte |= PPAT_DISPLAY_ELLC;
		break;
	default:
		pte |= PPAT_CACHED;
		break;
	}
	return pte;
}

image.png

image.png

建立页表映射和cpu 侧一样, 先通过虚拟地址找到pd pte 相关的index,然后通过sg 物理散列表找到物理page 将物理page 地址填充到pte 的entry中

static __always_inline u64
gen8_ppgtt_insert_pte(struct i915_ppgtt *ppgtt,
		      struct i915_page_directory *pdp,
		      struct sgt_dma *iter,
		      u64 idx,
		      enum i915_cache_level cache_level,
		      u32 flags)
{
	struct i915_page_directory *pd;
	const gen8_pte_t pte_encode = gen8_pte_encode(0, cache_level, flags);
	gen8_pte_t *vaddr;

	pd = i915_pd_entry(pdp, gen8_pd_index(idx, 2));
	vaddr = px_vaddr(i915_pt_entry(pd, gen8_pd_index(idx, 1)));
	do {
		GEM_BUG_ON(sg_dma_len(iter->sg) < I915_GTT_PAGE_SIZE);
		vaddr[gen8_pd_index(idx, 0)] = pte_encode | iter->dma;

		iter->dma += I915_GTT_PAGE_SIZE;
		if (iter->dma >= iter->max) {
			iter->sg = __sg_next(iter->sg);
			if (!iter->sg || sg_dma_len(iter->sg) == 0) {
				idx = 0;
				break;
			}

			iter->dma = sg_dma_address(iter->sg);
			iter->max = iter->dma + sg_dma_len(iter->sg);
		}

		if (gen8_pd_index(++idx, 0) == 0) {
			if (gen8_pd_index(idx, 1) == 0) {
				/* Limited by sg length for 3lvl */
				if (gen8_pd_index(idx, 2) == 0)
					break;

				pd = pdp->entry[gen8_pd_index(idx, 2)];
			}

			drm_clflush_virt_range(vaddr, PAGE_SIZE);
			vaddr = px_vaddr(i915_pt_entry(pd, gen8_pd_index(idx, 1)));
		}
	} while (1);
	drm_clflush_virt_range(vaddr, PAGE_SIZE);

	return idx;
}

参考文档: intel-gfx-prm-osrc-dg1-vol08-command_stream blog.totorow.xyz/posts/gfx_d… docs.kernel.org/gpu/drm-mm.…