【原创】Linux环境下的图形系统和AMD R600显卡编程(10)——R600显卡的3D引擎编程
3D图形处理流水线需要流经多个硬件单元才能得到最后的渲染结果,流水线上的所有的硬件单元必须被正确编程,才能得到正确的结果。
总体上看,从图形处理流水线的源头开始,需要准备好vertex和index,在立即模式下,index可以直接编程在命令中,通过配置寄存器告诉GPU vertex buffer的位置,在启动GPU流水线之前,还需要将vertex shader程序和pixel shader程序加载到vram 中,并通过配置寄存器告示GPU shader程序的位置,在vertex shader和pixel shader之间还需要配置光栅化部件以及semantic table,在pixel shader的输出端配置render target,这样整个GPU的编程就算完成了。
本节使用的代码是radeon exa的copy过程,copy过程即将显存上某个区域的内容移动到另外一片区域,GPU在目的区域绘制一个矩形,将源作为纹理贴到这个矩形上。
1. R600 3D引擎基本状态编程
本节使用exa驱动的代码详细说明如何对3D引擎进行编程。前面已经说过,R600显卡上不包含单独的2D单元,2D加速也是用3D部件完成的,这里选取exa代码的原因在于exa代码量比mesa dri驱动代码量小很多,分析起来容易一些。
EXA驱动挂接在xorg可加载驱动中。在启用KMS的情况下,Xorg的R600驱动按照如下的调用路径加载EXA驱动:RADEONScreenInit_ KMS ---> RADEONAccelInit ---> R600DrawInit。R600DrawInit函数中先为radeon_accel_state结构体中的ExaDriverPtr结构体的加速回调函数赋值,这些函数就是实现EXA加速所必须的函数,这些2D加速包括填充矩形(Solid)、块拷贝(Copy)以及混合(Composite)。调用exaDriverInit将exa驱动注册到Xorg系统中。
然后是调用R600AllocShaders为Shader分配显存,Shaders是GPU上运行的程序必须放在VRAM内存上:
accel_state->shaders_bo = radeon_bo_open(info->bufmgr, 0, size, 0,
RADEON_GEM_DOMAIN_VRAM, 0);
加载Shader程序,将为Shader分配的VRAM映射到用户空间,然后以直接写内存的方式将Shader程序加载到VRAM上。然后取消映射:
ret = radeon_bo_map(accel_state->shaders_bo, 1);
......
accel_state->solid_vs_offset = 0;
R600_solid_vs(ChipSet, shader + accel_state->solid_vs_offset / 4);
......
radeon_bo_unmap(accel_state->shaders_bo);
在EXA的每一个加速过程都注册了3个函数,分别是R600PrepareXXX、R600XXX和R600DoneXXX,这3个函数分别进行3D 引擎初始化、输入数据触发3D引擎以及3D引擎的清理工作。
接下来通过Copy加速过程描述3D引擎的编程过程。(前面讨论命令包格式的时候使用R500显卡做过copy操作,R500上使用2D部件进行操作,和这里区别很大)。
首先是3D引擎的初始化R600PrepareCopy,Copy操作涉及源和目的。获取源和目的的图像的pitch值,这里的pitch是图像一行的像素数目(包括可能的空白对齐区域,注意有些时候pitch值指以字节计一行图像占用的内存大小):
dst_obj.pitch = exaGetPixmapPitch(pDst) / (pDst->drawable.bitsPerPixel / 8);
src_obj.pitch = exaGetPixmapPitch(pSrc) / (pSrc->drawable.bitsPerPixel / 8);
获取代表图像显存的显存对象:
src_obj.bo = radeon_get_pixmap_bo(pSrc);
dst_obj.bo = radeon_get_pixmap_bo(pDst);
获取源和目的图像的格式信息:
src_obj.width = pSrc->drawable.width;
src_obj.height = pSrc->drawable.height;
src_obj.bpp = pSrc->drawable.bitsPerPixel;
src_obj.domain = RADEON_GEM_DOMAIN_VRAM | RADEON_GEM_DOMAIN_GTT;
dst_obj.width = pDst->drawable.width;
dst_obj.height = pDst->drawable.height;
dst_obj.bpp = pDst->drawable.bitsPerPixel;
dst_obj.domain = RADEON_GEM_DOMAIN_VRAM;
注意到上面代码的domain域,源可能来自VRAM或者GTT,但是dst是显示区域,一定只可能是VRAM。后面还有一段代码用于计算图片的高度和大小。
调用R600SetAccelState函数,该函数只是做了一些软件上的赋值操作,注意copy_vs_offset和copy_ps_offset都是GPU虚拟地址。
if (!R600SetAccelState(pScrn,
&src_obj,
NULL,
&dst_obj,
accel_state->copy_vs_offset, accel_state->copy_ps_offset,
rop, planemask))
return FALSE;
主要的3D引擎初始化工作在R600DoPrepareCopy函数调用里面“R600DoPrepareCopy(pScrn);”,后面将同时结合“Radeon R6xx/R7xx 3D Register Reference Guide”、“Radeon R6xx/R7xx Acceleration”两份文档进行说明。“Radeon R6xx/R7xx Acceleration”的“3D Engine Programming”部分较为完整的描述了需要编程的部分(虽然不详细)。
radeon_vbo_check函数获取可用的vertex buffer,如果没有可用的vertex buffer,将会重新分配一个“radeon_vbo_check(pScrn, &accel_state->vbo, 16);”。
radeon_cp_start用于清空当前命令流中的命令:
radeon_cp_start(pScrn):
if (info->cs) {
if (CS_FULL(info->cs)) {
radeon_cs_flush_indirect(pScrn);
}
accel_state->ib_reset_op = info->cs->cdw;
}
r600_set_default_state设置3D引擎的初始化状态,对R600硬件的配置主要在这个地方。
void r600_set_default_state(ScrnInfoPtr pScrn, drmBufPtr ib):
if (accel_state->XInited3D)
return;
r600_start_3d(pScrn, accel_state->ib);
sq_conf.ps_prio = 0;
sq_conf.vs_prio = 1;
sq_conf.gs_prio = 2;
sq_conf.es_prio = 3;
switch (info->ChipFamily) {
case CHIP_FAMILY_R600:
sq_conf.num_ps_gprs = 192;
sq_conf.num_vs_gprs = 56;
sq_conf.num_temp_gprs = 4;
sq_conf.num_gs_gprs = 0;
sq_conf.num_es_gprs = 0;
sq_conf.num_ps_threads = 136;
sq_conf.num_vs_threads = 48;
sq_conf.num_gs_threads = 4;
sq_conf.num_es_threads = 4;
sq_conf.num_ps_stack_entries = 128;
sq_conf.num_vs_stack_entries = 128;
sq_conf.num_gs_stack_entries = 0;
sq_conf.num_es_stack_entries = 0;
break;
r600_sq_setup(pScrn, ib, &sq_conf);
accel_state中的XInited3D域记录了3D引擎是否已经初始化过,如果EXA驱动已经初始化过3D引擎,则无需再度初始化。调用r600_start_3d函数启动3D引擎的,然后调用r600_sq_setup对3D引擎的Sequencer进行配置。r600_start_3d函数启动3D引擎的命令环,在GPU核心小于RV770的情况下,执行如下命令:
void r600_start_3d(ScrnInfoPtr pScrn, drmBufPtr ib):
BEGIN_BATCH(5);
PACK3(ib, IT_START_3D_CMDBUF, 1);
E32(ib, 0);
PACK3(ib, IT_CONTEXT_CONTROL, 2);
E32(ib, 0x80000000);
E32(ib, 0x80000000);
END_BATCH();
上面的代码如果转换成相应的内核代码会是这样的:
radeon_ring_lock(rdev, 5);
radeon_ring_write(rdev, CP_PACKET3(START_3D_CMDBUF, 1));
radeon_ring_write(rdev, 0x0);
radeon_ring_write(rdev, CP_PACKET3(IT_CONTEXT_CONTROL, 2));
radeon_ring_write(rdev, 0x80000000);
radeon_ring_write(rdev, 0x80000000);
手册上并没有给出START_3D_CMDBUF和IT_CONTEXT_CONTROL的具体含义【猜测这里START\_3D\_CMDBUF是将关闭掉Command Buffer。】
接下来是Sequencer的配置,Sequencer可以认为是GPU的一个控制逻辑单元,Sequence控制Shader程序的运行。Sequencer相关的寄存器参见《Radeon R6xx/R7xx 3D Register Reference Guide》第三节“General Shader Registers”。
static void r600_sq_setup(ScrnInfoPtr pScrn, drmBufPtr ib, sq_config_t *sq_conf):
......
BEGIN_BATCH(8);
PACK0(ib, SQ_CONFIG, 6);
E32(ib, sq_config);
E32(ib, sq_gpr_resource_mgmt_1);
E32(ib, sq_gpr_resource_mgmt_2);
E32(ib, sq_thread_resource_mgmt);
E32(ib, sq_stack_resource_mgmt_1);
E32(ib, sq_stack_resource_mgmt_2);
END_BATCH();
注意到这里写寄存器的方式,注意到PACK0的定义:
#define PACK0(ib, reg, num) \
do { \
if ((reg) >= SET_CONFIG_REG_offset && (reg) < SET_CONFIG_REG_end) { \
PACK3((ib), IT_SET_CONFIG_REG, (num) + 1); \
E32((ib), ((reg) - SET_CONFIG_REG_offset) >> 2); \
} else if ((reg) >= SET_CONTEXT_REG_offset && (reg) < SET_CONTEXT_REG_end) { \
PACK3((ib), IT_SET_CONTEXT_REG, (num) + 1); \
E32((ib), ((reg) - SET_CONTEXT_REG_offset) >> 2); \
} else if ((reg) >= SET_ALU_CONST_offset && (reg) < SET_ALU_CONST_end) { \
PACK3((ib), IT_SET_ALU_CONST, (num) + 1); \
E32((ib), ((reg) - SET_ALU_CONST_offset) >> 2); \
}
......
else { \
E32((ib), CP_PACKET0 ((reg), (num) - 1)); \
} \
} while (0)
地址小于SET_CONFIG_REG_offset(0x8000)的寄存器必须使用3型命令包写,这些寄存器只有GPU自己能够访问,只能使用命令的方式,不能使用通常直接写寄存器的方式(是否正确??),上面这段代码改写成:
radeon_ring_lock(rdev, 8);
radeon_ring_write(rdev, CP_PACKET3(IT_SET_CONFIG_REG, 7));
radeon_ring_write(rdev, (SQ_CONFIG - IT_SET_CONFIG_REG) >> 2);
radeon_ring_write(rdev, sq_config);
radeon_ring_write(rdev, sq_gpr_resource_mgmt_1);
radeon_ring_write(rdev, sq_gpr_resource_mgmt_2);
radeon-ring_write(rdev, sq_thread_resource_mgmt);
radeon_ring_write(rdev, sq_stack_resource_mgmt_1);
radeon_ring_write(rdev, sq_stack_resource_mgmt_2);
radeon_ring_unlock(rdev);
这是一个3型命令包,包头IT_SET_CONFIG_REG表明这个命令用于“Write Register Data to a Location on Chip”,即写片上的寄存器,这个命令包的第第二个DWORD是寄存器相对于IT_SET_CONFIG_REG(0x8000)的偏移(以DWORD计),命令包后续的内容是往offset 开始的连续几个寄存器里面写入的值,这里总共写了6个寄存器。这里配置的是从地址0x8c00(SQ_CONFIG)开始的几个寄存器。在寄存器手册搜索地址为0x8c00、0x8c04、0x8c08、0x8c0c、0x8c10、0x8c12的这几个寄存器可以查看详细说明,这里对SQ设置的参数同时需要考虑硬件的能力和软件的需求。对SQ的配置包括通用寄存器数目的配置、线程数目的配置以及堆栈资源的分配。
目前为止,碰到的寄存器有三类,使用的时候要注意区分:
- CPU可访问的寄存器,这类寄存器可以映射到进程地址空间,驱动程序可以直接访问
- GPU内部寄存器,这类寄存器不能被驱动程序直接访问,只能由GPU的命令处理器访问,驱动程序只能通过写GPU命令的方式写这些寄存器
- Shader通用寄存器,这类寄存器用于执行Shader程序的时候使用,类似x86上的eax、ebx这样的寄存器
sq_conf.num_ps_gprs = 192;
sq_conf.num_vs_gprs = 56;
sq_conf.num_temp_gprs = 4;
以上代码为每个【确认per simd 是什么意思,似乎不是这个意思,所有运行的thread实例共能够使用这么多通用寄存器?】PS程序分配的寄存器数目为192个,为VS程序分配的通用寄存器数目为56。
sq_conf.num_ps_threads = 136;
sq_conf.num_vs_threads = 48;
同时【??】运行ps程序的线程为136个,同时运行vs程序的线程数目为48个。EXA程序中不需要使用GS和ES,Shader也不需要使用堆栈,对于GS、ES和堆栈的配置跳过。
PACK0(ib, SQ_VTX_BASE_VTX_LOC, 2);
E32(ib, 0);
E32(ib, 0);
上面代码向SQ_VTX_START_INST_LOC和SQ_VTX_BASE_VTX_LOC两个寄器中写入0,这两个寄存器的含义涉及到GPU取顶点数据的寻址过程,GPU取顶点数据的时候按照下面的方式寻址:
fetch_addr = (index + index_offset) * stride + base + offset
其中index是通用寄存器中的值,为顶点的索引值,后面讨论GPU指令的时候会涉及到;index_offset中的值即来自SQ_VTX_START_INST_LOC和SQ_VTX_BASE_VTX_LOC两个寄存器,可以根据Shader程序中的相应位选择使用哪个寄存器的值,stride是设置顶点资源的时候设置的,为一个顶点的全部数据占的字节数。base也是设置顶点资源的时候配置的,为顶点内存(vertex buffer)的地址(GPU虚拟地址),offset 指明取顶点的哪部分数据,后面讨论GPU指令集的时候还会讨论。
接下来是对Depth test、Stencil test和alpha test的配置:
EREG(ib, DB_DEPTH_CONTROL, 0);
PACK0(ib, DB_RENDER_CONTROL, 2);
E32(ib, STENCIL_COMPRESS_DISABLE_bit | DEPTH_COMPRESS_DISABLE_bit);
E32(ib, FORCE_SHADER_Z_ORDER_bit);
EREG(ib, DB_ALPHA_TO_MASK,((2 << ALPHA_TO_MASK_OFFSET0_shift) |
(2 << ALPHA_TO_MASK_OFFSET1_shift) |
(2 << ALPHA_TO_MASK_OFFSET2_shift) |
(2 << ALPHA_TO_MASK_OFFSET3_shift)));
EREG(ib, DB_SHADER_CONTROL, ((1 << Z_ORDER_shift) | /* EARLY_Z_THEN_LATE_Z */
DUAL_EXPORT_ENABLE_bit)); /* Only useful if no depth export */
PACK0(ib, DB_STENCIL_CLEAR, 2);
E32(ib, 0); // DB_STENCIL_CLEAR
E32(ib, 0); // DB_DEPTH_CLEAR
PACK0(ib, DB_STENCILREFMASK, 3);
E32(ib, 0); // DB_STENCILREFMASK
E32(ib, 0); // DB_STENCILREFMASK_BF
E32(ib, 0); // SX_ALPHA_REF
往寄存器DB_DEPTH_CONTROL写入0将关闭depth test和stencil test。其他几个寄存器请自行参阅寄存器手册。
对viewport、windows、clip、scissor等的配置【待查阅】。
对光栅化部件的编程【查阅相关资料详细描述】:
PACK0(ib, PA_SC_LINE_CNTL, 9);
E32(ib, 0); // PA_SC_LINE_CNTL
E32(ib, 0); // PA_SC_AA_CONFIG
E32(ib, ((2 << PA_SU_VTX_CNTL__ROUND_MODE_shift) | PIX_CENTER_bit | // PA_SU_VTX_CNTL
(5 << QUANT_MODE_shift))); /* Round to Even, fixed point 1/256 */
EFLOAT(ib, 1.0); // PA_CL_GB_VERT_CLIP_ADJ
EFLOAT(ib, 1.0); // PA_CL_GB_VERT_DISC_ADJ
EFLOAT(ib, 1.0); // PA_CL_GB_HORZ_CLIP_ADJ
EFLOAT(ib, 1.0); // PA_CL_GB_HORZ_DISC_ADJ
E32(ib, 0); // PA_SC_AA_SAMPLE_LOCS_MCTX
E32(ib, 0); // PA_SC_AA_SAMPLE_LOCS_8S_WD1_M
EREG(ib, PA_SC_AA_MASK, 0xFFFFFFFF);
对semantic table的编程:
/* default Interpolator setup */
EREG(ib, SPI_VS_OUT_ID_0, ((0 << SEMANTIC_0_shift) |
(1 << SEMANTIC_1_shift)));
PACK0(ib, SPI_PS_INPUT_CNTL_0 + (0 << 2), 2);
/* SPI_PS_INPUT_CNTL_0 maps to GPR[0] - load with semantic id 0 */
E32(ib, ((0 << SEMANTIC_shift) |
(0x01 << DEFAULT_VAL_shift) |
SEL_CENTROID_bit));
/* SPI_PS_INPUT_CNTL_1 maps to GPR[1] - load with semantic id 1 */
E32(ib, ((1 << SEMANTIC_shift) |
(0x01 << DEFAULT_VAL_shift) |
SEL_CENTROID_bit));
关于semantic table稍微多说一点,图形处理流水线上,每一个过程都是相对独立的,这里有一些过程的数据格式(定点、法向量、颜色等的相对位置)是由应用程序(或者驱动程序)决定的,比如输入数据的格式,顶点数据根据具体应用的不同会有不同,比如最简单的应用只包含顶点坐标,复杂一点的可能还包含顶点颜色、纹理坐标、法向量数据等,再比如可编程处理器的Shader程序是应用程序或者驱动决定的,输出数据的格式是由应用程序或者驱动程序决定的,必须通过某种方式将这些"可编程"部分流出的数据格式告诉下一个阶段的部件。
先看输入的顶点数据和顶点处理器流出的数据。在早期的固定流水线的显卡上,应用程序或者驱动必须告诉显卡输入数据具体包含哪些分量,比如R100/R200显卡是通过VAP_VTX_FMT_0/1(关于这两个寄存器,可以参考R500手册)这两个寄存器配置顶点数据的格式的,这两个寄存器都有很多位,每一位都对应某一属性数据,如果这一位被置1,则表明输入数据包含这一属性,而且输入数据的各个属性之间必须按照规定的先后顺序排列。到R300以后,可编程处理器被引入,此时可编程处理器是不需要知道数据属性的,因为顶点处理程序是由程序员或者编译器编写的,程序员或者编译器是知道数据的格式的,此时VAP_VTX_FMT_0/1这两个寄存器就被废除了,取而代之的是一个表明顶点数据数据大小(占用的字节数)的寄存器,取顶点数据的硬件只需根据每个顶点数据的大小从顶点数据缓冲区中取出一个顶点的全部数据然后跳到下一个顶点取下一个顶点的数据就可以了,顶点数据送到Shader运行,编写Shader的程序员或者编译器知道数据的格式,然后对数据进行处理。
对R600,在没有Fetch shader的情况下,R600对顶点数据格式的处理和R300是类似的,后面设置顶点数据资源的时候将会看到某个寄存器的Stride位记录的就是顶点数据的大小。如果有Fetch shader,这必须配置一个semantic table,EXA驱动里面没有使用Fetch Shader,但是后面的Vertex shader和光栅化部件以及Pixel Shader有类似的semantic table,上面一段程序的semantic table就是针对这个设计的。
图1
图1显示了一个完整的semantic的数据转化过程,SPI_VS_OUT_ID寄存器共有10个,每个寄存器有4个域 ,每个域里代表semantic table中的一项,根据SPI_VS_OUT_CONFIG 寄存器的配置,SPI_VS_OUT_ID系列寄存器的每一个域代表一个向量或者向量的一个分量,这里配置的是代表一个向量,EXA驱动用到的顶点属性比较少,因此需要的semantic table也不大。SPI_VS_OUT_ID_0的第0个域代表Parameter Cache中的第0个向量,第1个域代表Parameter Cache中的第1个域,以此类推。为了能够更清晰的说明问题,图中显示的内容和EXA驱动稍微有点差异,SPI_VS_OUT_ID_0寄存器的第0个域填充的是1,表明需要查询semantic table的第1项,semantic table是由SPI_PS_INPUT_CNTL一系列共32个寄存器组成的,每个寄存器代表semantic table的一项,SPI_PS_INPUT_CNTL_1寄存器的SEMANTIC位的内容为1,对应Pixel Shader的GPR1,因此光栅化部件处理完,将像素的纹理信息放到Pixel Shader的GPR1中,Pixel Shader程序从GPR1中可以取到纹理数据,同样的,法向量信息被放置到GPR0中。【semantic table还需要包含Position的相关条项??查阅相关寄存器】
和semantic table有关还有一处代码,
r600_set_spi(pScrn, accel_state->ib, (1 - 1), 1);
void
r600_set_spi(ScrnInfoPtr pScrn, drmBufPtr ib, int vs_export_count, int num_interp)
{
RADEONInfoPtr info = RADEONPTR(pScrn);
BEGIN_BATCH(8);
/* Interpolator setup */
EREG(ib, SPI_VS_OUT_CONFIG, (vs_export_count << VS_EXPORT_COUNT_shift));
PACK0(ib, SPI_PS_IN_CONTROL_0, 3);
E32(ib, (num_interp << NUM_INTERP_shift));
E32(ib, 0);
E32(ib, 0);
END_BATCH();
}
首先是写SPI\_VS\_OUT\_CONFIG寄存器,这里VS\_PER\_COMPONENT位为0,表明一个向量在semantic中占一个条目,VS\_EXPORT\_COUNT为0,表明Vertex Shader输出了(0+1)=1个向量,在BLT过程中,就是输出了一个纹理坐标这一个向量。然后是写SPI_PS_IN_CONTROL_0、SPI_PS_IN_CONTROL_1和SPI_INTERP_CONTROL_0四个寄存器,SPI_PS_IN_CONTROL_0的NUM_INTERP表明有一个属性需要进行差值,【手册上说需要包括位置信息,那么这里应该有位置坐标和纹理坐标两个属性需要差值??】这个寄存器里面还有一个比较重要的位POSITION_ENA,表明位置信息是否也加载到PS中。【如果这一位开启的话,是否SPI\_PS\_IN\_CONTROL\_0的NUM\_INTERP位和semantic table都需要修改??】
接下来是对雾效果的处理,EXA驱动中并没有使用雾效果,因此这里相关配置全部填充的是0。
然后是对Fetch Shader的的配置:“r600_fs_setup(pScrn, ib, &fs_conf, RADEON_GEM_DOMAIN_VRAM);”。前面有代码将fs_conf变量置为0:“memset(&fs_conf, 0, sizeof(shader_config_t));”。调用r600_fs_setup实际上是将FS的配置清空为0,EXA驱动中不使用Fetch Shader,Fetch Shader的配置和Vertex Shader、Pixel Shader等的配置基本相同,后面使用的时候还会提到Vertex Shader。
然后是VGT的配置,VGT是最早接触顶点数据的部件,VGT中的信息在图元组装的时候也会被使用,VGT包含了两方面的内容:一部分是Vertex Group,另外一部分是Tesselator,关于VGT的配置的细节,这里不详细论述,读者可以参考寄存器手册。其中PA_SU_POINT_SIZE和PA_SU_LINE_CNTL分别用于控制点的大小和线的粗细。
至此r600_set_default_state函数已经全部完成。
设置裁剪范围:
r600_set_generic_scissor(pScrn, accel_state->ib, 0, 0, accel_state->dst_obj.width,
accel_state->dst_obj.height);
r600_set_screen_scissor(pScrn, accel_state->ib, 0, 0, accel_state->dst_obj.width,
accel_state->dst_obj.height);
r600_set_window_scissor(pScrn, accel_state->ib, 0, 0, accel_state->dst_obj.width,
accel_state->dst_obj.height);
虽然由于笔者能力有限以及手册有些地方没有明确说明导致上文还有很多地方语焉不详,但是大体上把整个过程走完了,如果对图形渲染流水线有足够的认识,在把R600的手册多读几遍,还是能够有不少收获的。
2. Shader程序的配置
接下来进入的是r600_vs_setup函数对Vertex Shader的配置:
void
r600_vs_setup(ScrnInfoPtr pScrn, drmBufPtr ib, shader_config_t *vs_conf, uint32_t domain)
{
RADEONInfoPtr info = RADEONPTR(pScrn);
uint32_t sq_pgm_resources;
sq_pgm_resources = ((vs_conf->num_gprs << NUM_GPRS_shift) |
(vs_conf->stack_size << STACK_SIZE_shift));
if (vs_conf->dx10_clamp)
sq_pgm_resources |= SQ_PGM_RESOURCES_VS__DX10_CLAMP_bit;
if (vs_conf->fetch_cache_lines)
sq_pgm_resources |= (vs_conf->fetch_cache_lines << FETCH_CACHE_LINES_shift);
if (vs_conf->uncached_first_inst)
sq_pgm_resources |= UNCACHED_FIRST_INST_bit;
/* flush SQ cache */
r600_cp_set_surface_sync(pScrn, ib, SH_ACTION_ENA_bit,
vs_conf->shader_size, vs_conf->shader_addr,
vs_conf->bo, domain, 0);
BEGIN_BATCH(3 + 2);
EREG(ib, SQ_PGM_START_VS, vs_conf->shader_addr >> 8);
RELOC_BATCH(vs_conf->bo, domain, 0);
END_BATCH();
BEGIN_BATCH(6);
EREG(ib, SQ_PGM_RESOURCES_VS, sq_pgm_resources);
EREG(ib, SQ_PGM_CF_OFFSET_VS, 0);
END_BATCH();
}
Shader程序配置的相关寄存器的详细信息见寄存器手册的“Shader Program Setup Registers”章节,所有Shader程序的配置都需要配置三个寄存器:
- SQ_PGM_START_xx Shader程序的起始地址
- SQ_PGM_RESOURCES_xx 为Shader程序分配的资源情况
- SQ_PGM_CF_OFFSET_xx Shader程序第一条CF指令相对Shader程序起始地址的偏移
上面代码是对Vertex Shader程序的配置,SQ_PGM_START_VS寄存器中写入的是Vertex Shader程序的GPU地址,SQ_PGM_CF_OFFSET_VS写入的是0,Shader程序的第一条CF指令在程序起始地址处。SQ_PGM_RESOURCES_VS寄存器中包含有NUM_GPRS【和SQ配置处的GPR有什么关系?】,必须配置足够的寄存器数目才能使程序运行,如果这里配置的寄存器数目NUM_GPRS为5,那么Shader程序中可访问的寄存器为GPR[0...4],如果访问GPR5,程序会运行出错。
r600_ps_setup(pScrn, accel_state->ib, &ps_conf, RADEON_GEM_DOMAIN_VRAM);
PS程序的配置和VS程序的配置类似,不再赘述。
3. 资源的配置
这里的资源(Resource)包括:顶点资源、纹理资源和常量资源。
1)纹理资源的配置
先看纹理资源的设置。函数调用进入了r600_set_tex_resource函数,纹理配置相关的寄存器在寄存器手册“Shader Vertex Resource Constants”章节。总共7个寄存器,
- SQ_VTX_CONSTANT_WORD0 配置纹理图的维度、PITCH值以及纹理图宽度,
- SQ_TEX_RESOURCE_WORD1 记录了纹理图的高度、深度和像素格式,
- SQ_TEX_RESOURCE_WORD2 记录纹理图在显存中的地址(GPU地址),
- SQ_TEX_RESOURCE_WORD3 记录mipmap在显存中的地址,EXA驱动进行BLIT操作的时候,纹理图是以原图的大小贴上去的,实际上不使用mipmap,这个寄存器中写入的值和SQ_TEX_RESOURCE_WORD2写入的值是一样的,
- SQ_TEX_RESOURCE_WORD4 配置纹理图像素格式
- SQ_TEX_RESOURCE_WORD5 【功能暂不清楚】
- SQ_TEX_RESOURCE_WORD6 【功能暂不清楚】
这里需要注意到纹理资源的id问题,配置以上寄存器的时候并没有指定纹理id,寄存器地址实际上已经表明了id了,在寄存器手册上上面几个寄存器都有一个后缀_0,这个0 就已经表明了纹理的id号了,从r600_set_tex_resource函数的代码能够清楚看到这一点:
PACK0(ib, SQ_TEX_RESOURCE + tex_res->id * SQ_TEX_RESOURCE_offset, 7);
这里的寄存器地址是在SQ_VTX_CONSTANT_WORD0_0的基础上加了一个偏移,关于RESOURCE_ID的问题,后面设置顶点资源的时候也有,【是否应该这样理解:vs 中有多个自己的顶点资源,这些资源都被编号,ps 也有自己的顶点资源(ps 要顶点资源干嘛?还是因为为了和 texture 统一才这样做,texture 在 vs 和 ps 中都是可用的,可是 fs 呢,fs 为什么会有自己的资源),也被编号,并且这些资源可以共享,比如一个 tex 资源在 vs 中使用,也在 ps 中使用,则在 ps 中一个编号为 0 的 tex 资源,在 vs中有一个编号为 160 的 tex 资源,都是指向同一个 tex 资源??】
在EXA驱动中可以看到如下的定义:
SQ_TEX_RESOURCE = SQ_TEX_RESOURCE_WORD0_0,/* 160 PS, 160 VS, 16 FS, 160 GS */
SQ_TEX_RESOURCE_ps_num = 160,
SQ_TEX_RESOURCE_vs_num = 160,
SQ_TEX_RESOURCE_fs_num = 16,
SQ_TEX_RESOURCE_gs_num = 160,
SQ_TEX_RESOURCE_all_num = 496,
SQ_TEX_RESOURCE_offset = 28,
SQ_TEX_RESOURCE_ps = 0,
SQ_TEX_RESOURCE_vs = SQ_TEX_RESOURCE_ps + SQ_TEX_RESOURCE_ps_num,
SQ_TEX_RESOURCE_fs = SQ_TEX_RESOURCE_vs + SQ_TEX_RESOURCE_vs_num,
SQ_TEX_RESOURCE_gs = SQ_TEX_RESOURCE_fs + SQ_TEX_RESOURCE_fs_num,
SQ_TEX_RESOURCE_WORD0_0为这一系列寄存器的起始地址,每一个资源对应7个寄存器,故偏移量为7*4 = 28(SQ_TEX_RESOURCE_offset)。PS的第一块纹理的id号为0,写入寄存器地址为0x38000~0x38018,PS第二块纹理的id号为1,写入寄存器地址为0x38000+28 ~ 0x38018+28,VS的第一块纹理的id号为160,写入的寄存器地址为0x38000+28*160 ~ 0x38018+28*160。
接下来是设置采样器,可以参考寄存器手册上的介绍,这里不详细描述。
后面还有设置顶点资源的代码test_r600_set_vtx_resource,和纹理资源的设置类似。
2)顶点资源和索引的配置
在R500显卡中,输入数据的属性可以是按照向量的方式写入到VAP_PORT_DATA寄存器然后到达IVM中,顶点位置和其他属性数据的排列方式可以是任意的,驱动程序只需要通过VAP_VTX_SIZE寄存器告知硬件顶点数据的大小。也可以使用索引方式,将顶点数据放置到VRAM的某一个位置,然后通过VAP_PORT_IDX寄存器将索引写入,到显卡上。
R600显卡只支持索引模式,用户必须先将顶点数据放置到vertex buffer中,vertex buffer是驱动从GTT上分配出来的用于放置顶点数据的一片内存,然后将索引写入显卡,因此vertex shader的输入都是索引,因此在vertex shader中必须调用事先编程好的fetch shader或者直接使用取顶点数据指令将顶点位置和顶点属性取出来【从主存DMA到显卡上】【细节还需要详细理解】,然后才能对这些数据进行处理【R3xx索引模式下是似乎不需要这样做,似乎无论使用索引模式还是立即模式,硬件都会将数据准备好,因此vertex shader看到的就是顶点数据,确认??】。
和R500显卡一样,R600显卡输入的顶点属性数据顺序也是可以由驱动自己定义的,在使用fetch shader的情况下,必须编程一个semantic table告知硬件顶点属性的排列顺序【semantic table如何编程】,如果直接是在vertex shader程序中取顶点数据,则编写vertex shader的用户或者编译器知道数据的格式,vertex shader和输入数据之间不需要semantic table(参考Radeon R6xx/R7xx Acceleration 2.5 Shader Linkage)。
EXA驱动中关于vertex buffer的分配
R600DoPrepareCopy中有使用vertex buffer的分配过程,在尚未分配vertex buffer的时候,vertex buffer按照如调用下过程分配:
R600DoPrepareCopy -> radeon_vbo_check -> r600_vb_no_space -> radeon_vbo_get -> radeon_vbo_get_bo.
最后的分配过程是这样的:
dma_bo->bo = radeon_bo_open(info->bufmgr, 0, VBO_SIZE,
0, RADEON_GEM_DOMAIN_GTT, 0);
这里使用的是GTT内存。
R600Copy函数调用了R600AppendCopyVertex,R600AppendCopyVertex函数中表明了vertex buffer的使用过程:
static void
R600AppendCopyVertex(ScrnInfoPtr pScrn, int srcX, int srcY, int dstX, int dstY, int w, int h)
{
float *vb;
vb = radeon_vbo_space(pScrn, 16);
vb[0] = (float)dstX;
vb[1] = (float)dstY;
vb[2] = (float)srcX;
vb[3] = (float)srcY;
vb[4] = (float)dstX;
vb[5] = (float)(dstY + h);
vb[6] = (float)srcX;
vb[7] = (float)(srcY + h);
vb[8] = (float)(dstX + w);
vb[9] = (float)(dstY + h);
vb[10] = (float)(srcX + w);
vb[11] = (float)(srcY + h);
radeon_vbo_commit(pScrn);
}
radeon_vbo_space(pScrn, 16)从vertex buffer中获取16(应该是出于对齐的原因)个dword大的内存并获取到指向申请到的这块内存的指针(具体过程大致是如果vertex buffer不够用了,就重新申请,如果还没有做映射,则做一次映射,然后根据整个bo的起始映射地址加上偏移得到可以使用的vertex buffer的地址)即上面代码中的vb指针,然后填上顶点数据。
这里是要做copy操作,源内存中的内容被看做是纹理,在目的地址处绘制矩形,然后将纹理应用到这个矩形上(这里是绘制矩形,但是只是用了三个顶点的坐标,似乎是自动计算了第四个坐标,如果改成绘制QUAD_LIST就可以指定完整的四个坐标)。
radeon_vbo_commit(pScrn)计算了一下当前剩余的空间,并将指针移动到剩余的空间上(有点类似前面的radeon_ring_lock和radeon_ring_commit的过程)。
R600DoCopy函数按照如下的调用顺序完成硬件顶点资源的设置:
R600DoCopy -> r600_finish_op -> set_vtx_resource,
/* Vertex buffer setup */
accel_state->vb_size = accel_state->vb_offset - accel_state->vb_start_op;
vtx_res.id = SQ_VTX_RESOURCE_vs;
vtx_res.vtx_size_dw = vtx_size / 4;
vtx_res.vtx_num_entries = accel_state->vb_size / 4;
vtx_res.mem_req_size = 1;
vtx_res.vb_addr = accel_state->vb_mc_addr + accel_state->vb_start_op;
vtx_res.bo = accel_state->vb_bo;
这里的id号和纹理资源的id号类似。EXA驱动里面只有一个顶点资源,被编号为SQ_VTX_RESOURCE_vs (160),顶点大小为16 (x、y 坐标加上s、t纹理坐标共4x4=16 字节)。vtx_res.vb_addr为vertex buffer的地址。
将上面这些信息设置到硬件里:
set_vtx_resource(pScrn, accel_state->ib, &vtx_res, RADEON_GEM_DOMAIN_GTT);
void set_vtx_resource(ScrnInfoPtr pScrn, drmBufPtr ib, vtx_resource_t *res, uint32_t domain)
{
RADEONInfoPtr info = RADEONPTR(pScrn);
uint32_t sq_vtx_constant_word2;
sq_vtx_constant_word2 = ((((res->vb_addr) >> 32) & BASE_ADDRESS_HI_mask) |
((res->vtx_size_dw << 2) << SQ_VTX_CONSTANT_WORD2_0__STRIDE_shift) |
(res->format << SQ_VTX_CONSTANT_WORD2_0__DATA_FORMAT_shift) |
(res->num_format_all << SQ_VTX_CONSTANT_WORD2_0__NUM_FORMAT_ALL_shift) |
(res->endian << SQ_VTX_CONSTANT_WORD2_0__ENDIAN_SWAP_shift));
if (res->clamp_x)
sq_vtx_constant_word2 |= SQ_VTX_CONSTANT_WORD2_0__CLAMP_X_bit;
if (res->format_comp_all)
sq_vtx_constant_word2 |= SQ_VTX_CONSTANT_WORD2_0__FORMAT_COMP_ALL_bit;
if (res->srf_mode_all)
sq_vtx_constant_word2 |= SQ_VTX_CONSTANT_WORD2_0__SRF_MODE_ALL_bit;
BEGIN_BATCH(9 + 2);
PACK0(ib, SQ_VTX_RESOURCE + res->id * SQ_VTX_RESOURCE_offset, 7);
E32(ib, res->vb_addr & 0xffffffff); // 0: BASE_ADDRESS
E32(ib, (res->vtx_num_entries << 2) - 1); // 1: SIZE
E32(ib, sq_vtx_constant_word2); // 2: BASE_HI, STRIDE, CLAMP, FORMAT, ENDIAN
E32(ib, res->mem_req_size << MEM_REQUEST_SIZE_shift); // 3: MEM_REQUEST_SIZE ?!?
E32(ib, 0); // 4: n/a
E32(ib, 0); // 5: n/a
E32(ib, SQ_TEX_VTX_VALID_BUFFER << SQ_VTX_CONSTANT_WORD6_0__TYPE_shift); // 6: TYPE
RELOC_BATCH(res->bo, domain, 0);
END_BATCH();
}
以上写的是Radeon R6xx/R7xx 3D Register Reference Guide第6部分 Shader Vertex Resource Constants的几个寄存器
SQ_VTX_CONSTANT_WORD0_0 0x38000
SQ_VTX_CONSTANT_WORD1_0 0x38004
SQ_VTX_CONSTANT_WORD2_0 0x38008
SQ_VTX_CONSTANT_WORD3_0 0x3800c
SQ_VTX_CONSTANT_WORD6_0 0x38018
往第0个寄存器中写入的是vertex buffer地址(GPU虚拟地址)的高32位,往第1 个寄存器里面写入的全部顶点占用的内存大小(以DWORD计),往第2个寄存器中写入的是vertex buffer地址的低32位、stride值、endian等信息。往第3个寄存器写入的值暂不清楚其含义(手册并未给出)。中间还有两个寄存器未定义,编程的时候需要往这两个寄存器里面写0。
3)常量资源的配置
Copy过程中没有使用常量资源,但是Solid过程使用了常量,将颜色作为常量传递给Shader。
颜色应该是作为顶点属性放置在顶点输入数据里面的,但是由于Solid操作整个矩形内填充的是同一种颜色,因此可以将颜色作为常量传递给Shader。
关于如何设置常量,以及常量如何被访问,读者可以参考EXA驱动中的r600_set_alu_consts函数和Solid的Shader程序代码。
4 输出
r600_set_render_target(pScrn, accel_state->ib, &cb_conf, accel_state->dst_o
Render target称为渲染目标,在早期的显卡上,渲染的结果只能输出在显示区域上,后来的显卡引入了Multiple render target的概念,多目标渲染,即渲染结果可以同时输出到显存的多个位置,在R600上最多可以配置8个render target,也就是说一次渲染的结果可以输出到显存上的最多8个不同的位置。
这部相关的寄存器在寄存器手册"Color Buffer Registers"章节,这里的很多寄存器都有八套,目前我们只使用一个render target,只是用于显示输出。
首先关心的是输出地址,EXA驱动向CB_COLOR0_BASE寄存器中写入的就是framebuffer的地址,CB_COLOR0_SIZE寄存器中存放的是Render target的大小,这里的大小是按照TILE来计算的,TILE大小为8*8,CB_COLOR0_SIZE寄存器PITCH_TILE_MAX域放置的是一行的TILE数目减1(width/8),SLICE_TILE_MAX域存放的是整个render target的tile数目减1(width*height/(8*8) -1)。
CB_COLOR_INFO寄存器配置render target的格式,目前比较重要的位有:
ENDIAN 指定输出是否进行大小端转换,ARRAY_MODE指定render target的tiling格式,FORMAT指定输出像素的格式。
至此,已经将全部的设置走了一遍。GPU流水线启动之后,就按照上一篇文章描述的过程处理数据,并将最后的结果输出到render target中。