【原创】Linux环境下的图形系统和AMD R600显卡编程(5)——AMD显卡显命令处理机制
通常通过读写设备寄存器对设备进行编程,在X86系统上,有专门的IO指令进行编程,在其他诸如MIPS、SPARC这类系统上,通过将设备的寄存器映射到内存地址空间直接使用读写内存的方式对设备进行编程。
Radeon显卡提供两种方式对硬件进行编程,一种称为“推模式”(push mode)即直接写寄存器的方式,另一种称为拉模式,这篇blog讨论拉模式,这也是驱动中使用的模式。
在拉模式下,驱动使用命令流(Command Stream)的形式进行对显卡编程:驱动程序将需要对显卡进行配置的一连串命令写入命令缓冲区,写完之后进入让出处理器,显卡按照命令写入的顺序执行这些命令,执行完成后触发中断通知驱动。CPU将这些命令放入一个称为命令环的环形缓冲区中,命令环是GTT内存中分出来的一片内存,驱动程序往命令环中填充命令,填充完后通知GPU命令已经写入命令,GPU的命令处理器CP(Command Processor)。上一篇博客即是通过ring环内存的使用来说明如何在系统中分配内存以及建立映射关系的。
驱动写入的命令流由命令处理器CP进行解析,具体来说,CP完成以下工作:
- 接收驱动程序的命令流。驱动程序将命令流先写入系统内存,然后由CP通过总线主设备访问方式进行获取,当前支持三种命令流,除了前面说的环形缓冲命令流,还有间接缓冲1命令流和间接缓冲2命令流;
- 解析命令流,将解析后的数据传输给图形控制器的其他模块,包括3D图形处理器、2D图形处理器、视频处理器。
命令环缓冲区
在拉模式下,驱动程序在系统内存中为命令流申请一块缓冲区。GPU会根据这些命令流去执行屏幕绘图等操作。这种命令缓冲区按照环形方式进行管理,是CPU和GPU 共享的一片系统主存,CPU负责写入命令包,GPU负责读取和解析命令包。因为CPU和GPU 看到的环形缓冲区状态必须是一致性,所以CPU和GPU都要共同维护和管理环形缓冲区的状态:基地址、长度、写指针和读指针。为了使Ring Buffer能够正常工作,CPU和GPU 必须维护这种状态的一致性。Ring Buffer基地址和大小是在系统第一次启动时已经初始化好的,之后一般也不会改变。当操作Ring Buffer时, 读指针和写指针的修改非常频繁。为了维护环形缓冲区的状态一致性,当写操作者(CPU)更新写指针时,它必须将写指针告诉GPU。同样的,当读操作者(GPU)更新读指针时,它必须将读指针告知CPU。无论是CPU还是GPU都是从低地址开始进行填写或抽取操作的,一旦到了环形缓冲区的结束处,又从环形缓冲区起始处继续。
图1
整个过程如图1示,左边的Host(CPU)和右边的GPU各自记录了命令环的起始地址,并各自保存了一份读写指针,CPU写之前首先查询读指针,确认有空闲空间之后写入内容并更新写指针,GPU读取了命令之后更新读指针。
间接缓冲
在系统主存中,除了环形缓冲区之外,CP还可以从间接缓冲1和间接缓冲2中获取命令包。这个过程是这样完成的:在主命令流中(ring buffer)有一个设置CP的间接缓冲1地址和大小的寄存器。写间接缓冲1的寄存器触发CP从提供的地址处取间接缓冲区1的命令流。主命令的最后一个命令包设置间接缓冲1地址和大小;然后CP开始从间接缓冲1中取数据。间接缓冲1的数命令流可能使用间接缓冲区2。和之前的过程一样,写间接缓冲1的寄存器触发CP从间接缓冲区2中获取新的命令流。间接缓冲1流中的最后一个包设置间接缓冲2的地址和大小。CP从间接缓冲2取命令直到全部去完;执行完间接缓冲区2的命令后返回到间接缓冲1的命令流。CP从间接缓冲1中取剩余的命令一直到间接缓冲1的末尾,返回到主命令流中。
这个过程有点类似函数调用。程序在运行过程中遇到函数调用,则会使用跳转指令跳到被调用函数入口,执行完函数后跳回到原来的程序位置继续执行。这的最大调用“深度”为2。
在Linux内核radeon驱动中有一个ring test过程用于验证ring buffer是否工作正常,如果ring test通过,那么GPU和CPU交互的部分已经配置正确,可以正常工作了。
Ring buffer机制几乎在所有类型的芯片上都是一样的,区别只是r600以后的芯片ring buffer GPU端读写指针的寄存器地址发生了变化。Linux内核驱动针对不同GPU核实现ring buffer机制以及ring test过程的代码几乎是完全相同的。
从内核中拿出ring test过程的代码:
2287 int r600_ring_test(struct radeon_device *rdev)
2288 {
2289 uint32_t scratch;
2290 uint32_t tmp = 0;
2291 unsigned i;
2292 int r;
2293
2294 r = radeon_scratch_get(rdev, &scratch);
2295 if (r) {
2296 DRM_ERROR("radeon: cp failed to get scratch reg (%d).\n", r);
2297 return r;
2298 }
2299 WREG32(scratch, 0xCAFEDEAD);
2300 r = radeon_ring_lock(rdev, 3);
2301 if (r) {
2302 DRM_ERROR("radeon: cp failed to lock ring (%d).\n", r);
2303 radeon_scratch_free(rdev, scratch);
2304 return r;
2305 }
2306 radeon_ring_write(rdev, PACKET3(PACKET3_SET_CONFIG_REG, 1));
2307 radeon_ring_write(rdev, ((scratch - PACKET3_SET_CONFIG_REG_OFFSET) >> 2));
2308 radeon_ring_write(rdev, 0xDEADBEEF);
2309 radeon_ring_unlock_commit(rdev);
2310 for (i = 0; i < rdev->usec_timeout; i++) {
2311 tmp = RREG32(scratch);
2312 if (tmp == 0xDEADBEEF)
2313 break;
2314 DRM_UDELAY(1);
2315 }
2316 if (i < rdev->usec_timeout) {
2317 DRM_INFO("ring test succeeded in %d usecs\n", i);
2318 } else {
2319 DRM_ERROR("radeon: ring test failed (scratch(0x%04X)=0x%08X)\n",
2320 scratch, tmp);
2321 r = -EINVAL;
2322 }
2323 radeon_scratch_free(rdev, scratch);
2324 return r;
2325 }
2294行获取一个可用的scratch寄存器,scratch寄存器是功能未定义的寄存器,由(驱动)软件定义其功能。
2299行使用mmio的方式直接向寄存器中写入值“0xCAFEDEAD”,此时该scratch寄存器的内容为0xCAFEDEAD。
2300行向内核驱动中的ring buffer机制申请3个dword(gpu命令都是以4字节为单位计的),同时由于会有多个程序并发访问ring buffer,这里还会对ring buffer加锁。
2306-2308行代码向刚才申请到的ring buffer内存中写入3个dword的命令,关于GPU命令在下一章会详细介绍,这里的命令的意思是向刚才的scratch寄存器中写入值“0xDEADBEEF”。
2309行提交命令,上面三行代码写的命令写入ring buffer后并不会被执行,直到调用radeon_ring_unlock_commit之后命令才会被执行。
2310-2314行是一个通过轮询的方式检查scratch寄存器的过程,如果上面的命令正常运行,那么scratch寄存器的值将会是“0xDEADBEEF”,否则命令没有正常运行,ring test 失败。
从上面的示例代码中可以看到,在radeon内核驱动使用了下面三个函数就可以操作ring buffer了:
API接口函数 | 功能 | 参数 |
radeon_ring_lock | 申请ring buffer内存并锁住ring buffer,如果ring buffer被用完,则更新CPU端的读指针 | N为申请的dwords数目 |
radeon_ring_write | 向ring buffer写入命令和命令参数,这里只更新CPU端的写指针 | |
radeon_ring_commit | 更新GPU端的写指针,释放ring buffer锁 |
需要提及的是scratch寄存器,scratch寄存器是GPU预留给软件使用的寄存器,r300以前的显卡只5个scratch寄存器,以后的显卡有7个寄存器,GPU本身并不依赖这些寄存器对其进行配置,软件可以自定义其功能。上面这段代码仅仅用于验证命令是否正确执行,然而后面的轮询过程却对我们有所启发:软件发送了命令之后什么时候直到命令被执行完成了?可以按照这里面的做法,在命令尾部再添加一条写scratch寄存器的命令(当然必须保证往scratch寄存器写入的值和scratch寄存器原来的值不一样),而后轮询该scratch寄存器,如果这个寄存器被写入了我们要求其写入的值,那么就可以确定命令已经执行完了。这里实际上定义了一个软硬件同步的机制,后面中断机制的章节会讨论驱动中fence机制的实现,fence机制是使用中断实现的,但是那里面使用了我们上面提到的思想。
经过上面描述之后,阅读ring buffer的实现代码应该不难读懂了。
Linux内核中完成ring test后,会有一个indirect buffer test过程。这个过程和ring test过程完成的操作一样,写scratch寄存器。
2660 int r600_ib_test(struct radeon_device *rdev)
2661 {
2662 struct radeon_ib *ib;
2663 uint32_t scratch;
2664 uint32_t tmp = 0;
2665 unsigned i;
2666 int r;
2667
2668 r = radeon_scratch_get(rdev, &scratch);
......
2673 WREG32(scratch, 0xCAFEDEAD);
2674 r = radeon_ib_get(rdev, &ib);
......
2679 ib->ptr[0] = PACKET3(PACKET3_SET_CONFIG_REG, 1);
2680 ib->ptr[1] = ((scratch - PACKET3_SET_CONFIG_REG_OFFSET) >> 2);
2681 ib->ptr[2] = 0xDEADBEEF;
2682 ib->ptr[3] = PACKET2(0);
2683 ib->ptr[4] = PACKET2(0);
2684 ib->ptr[5] = PACKET2(0);
2685 ib->ptr[6] = PACKET2(0);
2686 ib->ptr[7] = PACKET2(0);
2687 ib->ptr[8] = PACKET2(0);
2688 ib->ptr[9] = PACKET2(0);
2689 ib->ptr[10] = PACKET2(0);
2690 ib->ptr[11] = PACKET2(0);
2691 ib->ptr[12] = PACKET2(0);
2692 ib->ptr[13] = PACKET2(0);
2693 ib->ptr[14] = PACKET2(0);
2694 ib->ptr[15] = PACKET2(0);
2695 ib->length_dw = 16;
2696 r = radeon_ib_schedule(rdev, ib);
......
2703 r = radeon_fence_wait(ib->fence, false);
......
2708 for (i = 0; i < rdev->usec_timeout; i++) {
2709 tmp = RREG32(scratch);
2710 if (tmp == 0xDEADBEEF)
2711 break;
2712 DRM_UDELAY(1);
2713 }
.....
2721 radeon_scratch_free(rdev, scratch);
2722 radeon_ib_free(rdev, &ib);
2723 return r;
2724 }
2668-2673行的内容和ring test的过程一样。
2674行从系统中获取一个indirect buffer,ib->ptr中记录了indirect buffer在内存中的位置。
2679-2694向indirect buffer中填充命令和参数,这里填写的命令和参数与ring test 中填写的命令和参数是相同的,当然这里也有对齐要求。
2696 行将填写好的indirect buffer添加到调度队列中。
2703行涉及fence机制,在中断机制一节中我们将详细介绍。
同样读懂indirect buffer机制的代码也不会有太大困难。
Indirect buffer要能够正常运行,必须将其插入到ring buffer的代码中去,这就类似在汇编代码中插入"call xx"指令进行函数调用一样。radeon_ring_ib_execute函数添加的命令就相当于函数调用时使用的call指令。
下一篇将描述这些命令的格式,并给出一些例子。
参考资料:
这部分的描述的内容基本上来自“Radeon R5xx Acceleration”文档。
“Graphic Engine Resource Management”对命令的调度有一些改进,可以作为进一步学习的参考。