xorg 硬件加速浅析 系列 kdrive的xvide的加速的实现

来源:互联网 发布:手写字体软件下载 编辑:程序博客网 时间:2024/05/01 20:18

原文:http://blog.chinaunix.net/u1/40978/showart_1972799.html

 

kdrive的xvide的加速的实现。

首先xvideo本来也是在驱动里面实现的,实际上我们先要做一个驱动。
kdrive硬件加速驱动的实现,其实就是kaa的实现,kaa分xserver这边和driver这边,xserver这边会提供一种机制
这个机制就是当gc操作的时候会判断一下回调函数是否注册,如果注册就使用回调函数,如果没有注册就使用软件的实现,
而注册这个回调函数就是在kdrive的驱动里面来实现的。因为kdrive不能动态装载驱动,所以驱动都是直接编译到Xserver
里面的,也就是说编译出来的Xfbdev是直接带了驱动的。

我们先看看代码的布局,之前是在xserver 1.5.x的基础上来实现的,大概就以1.5.3来讲吧
关于kdrive和xserver的关系之前说过,kdrive是xserver的一种,xserver在xorg的代码树里面有好几种实现,但是大体的代码都是共用的
比如基本的事件处理,clip等等代码都是共用的。hw目录下面就是不同的xserver的实现。xorg只是其中一种,如前所说,kdrive和xorg有很多
代码是共用的,所以xorg的代码更新过快,kdrive更新过慢,导致kdrive的有很多事件处理有问题。

本来应该先说kaa的实现的,但是因为xivdeo的实现比较简单,所以先说。

我们先看看xvideo的工作流程,就是说xvideo到底是怎么发挥作用的。


xvideo的目的是为了把video或者frame输出直接输出到屏幕上面,尽量少的转存,尽量多的利用硬件加速,而xorg是管理最后输出的(目前是这样)
所以需要在xserver里面有一个扩展这个就是Xv的扩展了,

这里需要一个图片说明

我们看到最原始的数据无非是一个视频文件,我们先假定这个文件是一个mpeg4的文件。然后通过demux之后就有不同的数据流,比如音频,视频,还有其他的,
但是我们这里先只关注视频,视频流出来之后就经过解码,解码完成之后就成了一帧一帧的图片,和放电影的原理是一样的。然后我们就要把这个图片显示
到屏幕上面,最终的输出函数一般都是XPutImage,XvPutImage,XvShmPutImage类似这样的函数。
当然使用shm的版本会比不使用shm的版本要快很多了,Xshm本来也是Xserver的一个扩展,这个主要是为了减少数据的copy
一般来说写的好的播放器都会判断一些xserver是否支持shm扩展,如果支持,就会使用shm的函数。比如mplayer,gstreamer(xvimagesink)
因为Xserver这个框架是C/S模式的,说这个很好理解,但是很多人还是知其然不知其所以然,我们从原始的架构来理解。
原始的X是可以通过网络来传输的,但是因为这种模式我们现在用的不是很多了,除了ssh -X abc@192.168.1.100这样的用法之外,可能我们
很少会用到这样的远程传输,我们本机一般用的是unix socket。也就是说所有的请求,实际上是这样的,

我们编写应用程序里面会有XPutImage的函数,实际上这个函数是由libX11来提供的(如果我没有记错的话),XvSHmPutImage这样的函数是由libXext来实现的。
这个里面的代码实际上就是封装一个X的请求,然后通过Xsend发送出去。然后Xserver那边会dispatch这些消息,由对应的扩展或者函数来处理,因为这些Xext初始化
的时候会告诉Xserver这类消息由自己的扩展来处理。

所以想想如果是一个很大帧的数据,都走socket的话是很慢的,所以有了Xshm就好很多了,shm,和我们普通的shm是一样的,就是共享内存了,就是用户程序和
xserver共享一个内存空间,当用户程序填充帧的时候实际上就直接写到xserver的内存空间了(需要图片说明),然后xserver可以直接显示出去,这样会节省很多空间
Ximage这个数据接口是有data指针的,但是如果是shm的版本就没有,里面会带一个地址。XshmImage.这些内容都是凭记忆写的,可能会有漏误


话说回来,Xvideo在xserver这边对应的处理流程。
xserver无非也会接收到XshmPutimage的请求,这个在Xserver里面xorg-server-1.5.3/Xext/shm.c里面实现的

static int
SProcShmPutImage(client)
    ClientPtr client;
{
    register int n;
    REQUEST(xShmPutImageReq);
    swaps(&stuff->length, n);
    REQUEST_SIZE_MATCH(xShmPutImageReq);
    swapl(&stuff->drawable, n);
    swapl(&stuff->gc, n);
    swaps(&stuff->totalWidth, n);
    swaps(&stuff->totalHeight, n);
    swaps(&stuff->srcX, n);
    swaps(&stuff->srcY, n);
    swaps(&stuff->srcWidth, n);
    swaps(&stuff->srcHeight, n);
    swaps(&stuff->dstX, n);
    swaps(&stuff->dstY, n);
    swapl(&stuff->shmseg, n);
    swapl(&stuff->offset, n);
    return ProcShmPutImage(client);
}



ProcShmPutImage这个函数会调用下面的函数下面调用的是XPutImage的注册回调函数了,
    (*pGC->ops->PutImage) (pDraw, pGC, stuff->depth,
                   stuff->dstX, stuff->dstY,
                   stuff->totalWidth, stuff->srcHeight,
                   stuff->srcX, stuff->format,
                   shmdesc->addr + stuff->offset +
                   (stuff->srcY * length));





从dispatch开始看
Xext/xvdisp.c
#define XVCALL(name) Xv##name

ProcXvPutImage
最后调用的是这个,
 return XVCALL(diPutImage)(client, pDraw, pPort, pGC,
                stuff->src_x, stuff->src_y,
                stuff->src_w, stuff->src_h,
                stuff->drw_x, stuff->drw_y,
                stuff->drw_w, stuff->drw_h,
                pImage, (unsigned char*)(&stuff[1]), FALSE,
                stuff->width, stuff->height);

实际上是下面的函数XvdiPutImage
Xext/xvmain.c这个函数是xvideo的具体实现,当然xorg和kdrive实际上都用这里的代码。
XvdiPutImage
调用下面的回调函数了
 return (* pPort->pAdaptor->ddPutImage)(client, pDraw, pPort, pGC,
                       src_x, src_y, src_w, src_h,
                       drw_x, drw_y, drw_w, drw_h,
                       image, data, sync, width, height);

我们要实现的自然就是ddPutImage了。
这个函数就是gc注册的了,这里我们使用kdrive的框架,自然就是kaa去注册的了。
然后kaa的具体实现,也就是我们的驱动,会注册kaa的函数。

所有kdrive的函数都在
xorg-server-1.5.3/hw/kdrive
ailantian@vax:/mnt/sdb1/ubd/soft/xorg/temp/xorg-server-1.5.3/hw/kdrive$ ls
ati    ephyr  fake   i810   mach64       Makefile.in  neomagic  pm2   sdl     smi  vesa
chips  epson  fbdev  linux  Makefile.am  mga          nvidia    r128  sis300  src  via
ailantian@vax:/mnt/sdb1/ubd/soft/xorg/temp/xorg-server-1.5.3/hw/kdrive$ pwd
这下面基本每个目录都是一个driver,比如有intel 810, ati,等等,很多驱动了。
kaa这个框架是在hw/kdrive/kaa.c里面实现的,至于xvideo的框架的实现就是在kxv里面来实现的了。

我们的驱动里面通过一定的机制实际上会调用到下面这个函数,
KdXVInitAdaptors

      pa->pScreen = pScreen;
      pa->ddAllocatePort = KdXVAllocatePort;
      pa->ddFreePort = KdXVFreePort;
      pa->ddPutVideo = KdXVPutVideo;
      pa->ddPutStill = KdXVPutStill;
      pa->ddGetVideo = KdXVGetVideo;
      pa->ddGetStill = KdXVGetStill;
      pa->ddStopVideo = KdXVStopVideo;
      pa->ddPutImage = KdXVPutImage;
      pa->ddSetPortAttribute = KdXVSetPortAttribute;
      pa->ddGetPortAttribute = KdXVGetPortAttribute;
      pa->ddQueryBestSize = KdXVQueryBestSize;
      pa->ddQueryImageAttributes = KdXVQueryImageAttributes;


实际上我们只关注
      pa->ddPutImage = KdXVPutImage;

这个函数是在xserver的xvideo扩展里面被调用的。
也就是说kxv实际上是一个框架,然后会有人来填充KdXVPutImage这样的回调函数的,这个就是我们kaa驱动里面要做的事情了。

其实i810等等这些驱动可以直接参考,我们可以直接copy一下一个目录,然后作为自己驱动开发的基础,然后改改makefile就行了。
这里还是以ati作为例子来讲解吧,我自己的代码写的比较乱,可能还有其他一些问题,所以不便贴出来。
一般来说xvideo的实现都是在xxx_video.c里面实现的,比如我们看ati的,就是在ati_video.c里面了。
其实代码量比较小。
其实输出出去,最终还是这个函数,
ATIPutImage,然后这个函数一般会调用DisplayVideo这样的函数,不过这个只是开发者的习惯而已。
那么先看看框架。
以ati为例,注册回调函数从哪里开始,
grep一下代码就知道了,实际上是这个。
ATISetupImageVideo

ATIInitVideo调用ATISetupImageVideo
看个人的喜好了,反正是要注册这些回调函数了,
下面是关键代码,所谓的框架都在这里了。
static KdVideoAdaptorPtr
ATISetupImageVideo(ScreenPtr pScreen)
{
    KdScreenPriv(pScreen);
    ATIScreenInfo(pScreenPriv);
    KdVideoAdaptorPtr adapt;
    ATIPortPrivPtr pPortPriv;
    int i;

    atis->num_texture_ports = 16;

    adapt = xcalloc(1, sizeof(KdVideoAdaptorRec) + atis->num_texture_ports *
        (sizeof(ATIPortPrivRec) + sizeof(DevUnion)));
    if (adapt == NULL)
        return NULL;

    adapt->type = XvWindowMask | XvInputMask | XvImageMask;
    adapt->flags = VIDEO_CLIP_TO_VIEWPORT;
    adapt->name = "ATI Texture Video";
    adapt->nEncodings = 1;
    adapt->pEncodings = DummyEncoding;
    adapt->nFormats = NUM_FORMATS;
    adapt->pFormats = Formats;
    adapt->nPorts = atis->num_texture_ports;
    adapt->pPortPrivates = (DevUnion*)(&adapt[1]);

    pPortPriv =
        (ATIPortPrivPtr)(&adapt->pPortPrivates[atis->num_texture_ports]);

    for (i = 0; i < atis->num_texture_ports; i++)
        adapt->pPortPrivates[i].ptr = &pPortPriv[i];

    adapt->nAttributes = NUM_ATTRIBUTES;
    adapt->pAttributes = Attributes;
    adapt->pImages = Images;
    adapt->nImages = NUM_IMAGES;
    adapt->PutVideo = NULL;
    adapt->PutStill = NULL;
    adapt->GetVideo = NULL;
    adapt->GetStill = NULL;
    adapt->StopVideo = ATIStopVideo;
    adapt->SetPortAttribute = ATISetPortAttribute;
    adapt->GetPortAttribute = ATIGetPortAttribute;
    adapt->QueryBestSize = ATIQueryBestSize;
    adapt->PutImage = ATIPutImage;
    adapt->ReputImage = ATIReputImage;
    adapt->QueryImageAttributes = ATIQueryImageAttributes;

    /* gotta uninit this someplace */
    REGION_INIT(pScreen, &pPortPriv->clip, NullBox, 0);

    atis->pAdaptor = adapt;

    xvBrightness = MAKE_ATOM("XV_BRIGHTNESS");
    xvSaturation = MAKE_ATOM("XV_SATURATION");

    return adapt;
}


看看上面最重要的函数就是注册了    adapt->PutImage = ATIPutImage;
这个函数在最后就会被调用到。

大家可能会问这个adapt怎么会和上面的KdXVPutImage打上关系,这个自然是要注册的。
KdXVScreenInit->KdXVInitAdaptors

      adaptorPriv->flags = adaptorPtr->flags;
      adaptorPriv->PutVideo = adaptorPtr->PutVideo;
      adaptorPriv->PutStill = adaptorPtr->PutStill;
      adaptorPriv->GetVideo = adaptorPtr->GetVideo;
      adaptorPriv->GetStill = adaptorPtr->GetStill;
      adaptorPriv->StopVideo = adaptorPtr->StopVideo;
      adaptorPriv->SetPortAttribute = adaptorPtr->SetPortAttribute;
      adaptorPriv->GetPortAttribute = adaptorPtr->GetPortAttribute;
      adaptorPriv->QueryBestSize = adaptorPtr->QueryBestSize;
      adaptorPriv->QueryImageAttributes = adaptorPtr->QueryImageAttributes;
      adaptorPriv->PutImage = adaptorPtr->PutImage;
      adaptorPriv->ReputImage = adaptorPtr->ReputImage;


      portPriv->AdaptorRec = adaptorPriv;

这个函数指针都放在了AdaptorRec这个里面,而这个是在KdXVXXXX函数里面被调用的。
比如
KdXVPutImage
到输出的时候就会尝试调用这个注册的回调函数了。
  ret = (*portPriv->AdaptorRec->PutImage)(portPriv->screen, pDraw,
        src_x, src_y, WinBox.x1, WinBox.y1,
        src_w, src_h, drw_w, drw_h, format->id, data, width, height,
        sync, &ClipRegion, portPriv->DevPriv.ptr);


而这个回调函数就是我们之前ATISetupImageVideo里面注册的函数,通过一系列周转到这里来的。

回到正题,只看最重要的部分,如何硬件加速输出的。其实看代码grep一下,肯定是和寄存器相关,或者mmio相关的,因为老的框架都是使用mmio的。

ATIPutImage里面加入了Xrandr的处理,这个我们可以先不管,主要是屏幕旋转会造成问题,所以需要处理一下,不过我们如果不旋转可以不关注这个代码了。

最关键的代码如下我们解码出来的图片是在内存里面,但是实际上我们要显示出去,必须放到显存里面,这里我不想讲kaa的内存管理了,偏离标题了,
    if (pPortPriv->off_screen == NULL) {
        pPortPriv->off_screen = KdOffscreenAlloc(screen->pScreen,
            size * 2, 64, TRUE, ATIVideoSave, pPortPriv);
        if (pPortPriv->off_screen == NULL)
            return BadAlloc;
    }
反正就是先我们要在显存里面划拨一个区域来存放这个图片才行。
下面有一段pixmap的判断,这个先不说,
总之,我们必须保证这个东西是在显存里面,无论是不是offscreen的。
然后就是下面的代码了,
    switch(id) {
    case FOURCC_YV12:
    case FOURCC_I420:
        top &= ~1;
        nlines = ((((rot_y2 + 0xffff) >> 16) + 1) & ~1) - top;
        KdXVCopyPlanarData(screen, buf, pPortPriv->src_addr, randr,
            srcPitch, srcPitch2, dstPitch, rot_src_w, rot_src_h,
            height, top, left, nlines, npixels, id);
        break;
    case FOURCC_UYVY:
    case FOURCC_YUY2:
    default:
        nlines = ((rot_y2 + 0xffff) >> 16) - top;
        KdXVCopyPackedData(screen, buf, pPortPriv->src_addr, randr,
            srcPitch, dstPitch, rot_src_w, rot_src_h, top, left,
            nlines, npixels);
        break;
    }

这个代码实际上是用来进行数据格式的转换的,也就是说,其实显卡所支持的硬件加速的格式是有限的,如果这个格式不被硬件所支持的话,我们就需要进行软件的
格式转换,这里xserver处理的有点死板,xserver到最后的输出的时候只支持两种格式,所以所有的格式都会转换成那两种格式,如果没有记错的话
应该是YVYU,VYUY,也就是说xserver目前的框架,是将数据封装成这两个格式,然后给硬件的,当然,这个驱动是自己实现的,一般来说,硬件支持很多种格式,
还有我们看到上面的代码,从user application传递过来的之后四种格式,这个是因为我们的驱动只注册了四种格式,也就是说我们只支持这四种格式,
类似mplayer会通过函数来查询当前的驱动到底支持哪几种格式,xvinfo这个x自带的工具也可以看。
注册格式是在这个地方,如果硬件支持更多的比如NV12,NV21等可以加在这里,好像一般的嵌入式的处理器如果有2d处理的话,都会支持。
static KdImageRec Images[NUM_IMAGES] =
{
    XVIMAGE_YUY2,
    XVIMAGE_YV12,
    XVIMAGE_I420,
    XVIMAGE_UYVY
};

xserver默认只支持这几个fourcc,如果要更多,可以从别的驱动里面抄这些定义,或者抄mplayer或者pixman里面都有这些fourcc的定义。因为很多格式还没有被
标准化。


数据pack完成之后自然就是发送给硬件显示了。

这个就比较简单了,因为我们有一个缓冲区,这个缓冲区已经在显存里面了,我们要显示到屏幕的某个地方。
注意offscreen和online screen的区别,比如我们有一个显卡,显存500M,实际上只有很小一部分空间是对应在屏幕上面的。
一般来说,显卡操作显存,是比较快的,有各种不同的技术实现,各家也不一样。但是比内存是要快的,或者至少也要和内存的速度差不多,大块数据的搬运会比较快一些。
比如要从offscreen搬运到online screen,其实嵌入式来说,显存也是内存,划拨一块而已,但是这个操作一般不需要cpu参与了。

好,我们看看显示的过程,
一般驱动程序会单独写一个函数来处理这个过程。
这里其实只有两个需要注意的地方,一个就是clip的处理,一个就是composite的处理。
虽然说xvideo本身的设计是和composite背离的,不过xvideo也可以直接输出到一个texure或者是一个pixmap里面。因为无论是什么方式,无非是一块显存。
R128DisplayVideo
我们看看这个实现。
    BoxPtr pBox = REGION_RECTS(&pPortPriv->clip);
    int nBox = REGION_NUM_RECTS(&pPortPriv->clip);

这里是计算clip的,比如我们播放一个视频的时候,这个时候有菜单盖住屏幕,这样我们的输出区域就被剪切成多个区域了,2个或者说个等等。
我们就只能按小块输出了,每块单独输出,但是还是硬件加速的,我们看看循环就知道了。
    while (nBox--) {
        int srcX, srcY, dstX, dstY, srcw, srch, dstw, dsth;

        dstX = pBox->x1 + dstxoff;
        dstY = pBox->y1 + dstyoff;
        dstw = pBox->x2 - pBox->x1;
        dsth = pBox->y2 - pBox->y1;
        srcX = (pBox->x1 - pPortPriv->dst_x1) *
            pPortPriv->src_w / pPortPriv->dst_w;
        srcY = (pBox->y1 - pPortPriv->dst_y1) *
            pPortPriv->src_h / pPortPriv->dst_h;
        srcw = pPortPriv->src_w - srcX;
        srch = pPortPriv->src_h - srcY;

        BEGIN_DMA(6);
        OUT_RING(DMA_PACKET0(R128_REG_SCALE_SRC_HEIGHT_WIDTH, 2));
        OUT_RING_REG(R128_REG_SCALE_SRC_HEIGHT_WIDTH,
            (srch << 16) | srcw);
        OUT_RING_REG(R128_REG_SCALE_OFFSET_0, pPortPriv->src_offset +
            srcY * pPortPriv->src_pitch + srcX * 2);

        OUT_RING(DMA_PACKET0(R128_REG_SCALE_DST_X_Y, 2));
        OUT_RING_REG(R128_REG_SCALE_DST_X_Y, (dstX << 16) | dstY);
        OUT_RING_REG(R128_REG_SCALE_DST_HEIGHT_WIDTH,
            (dsth << 16) | dstw);
        END_DMA();
        pBox++;
    }

里面的OUT_RING我们就不看了,简单点理解,就是硬件输出。
各家硬件不一样,这里的目的是一样的,就是输出区域。nBox就是我们区域的个数了。
我们要把buffer输出到一个指定的区域,指定的偏移就可以了。

至于composite,我们无非是要指定输出的区域就行了,这个区域如果不在online screen,我们就输出到pixmap了。
然后composite会管理输出,可能是windowmanager做这个或者是xcompmgr类似这样的程序。
    DamageDamageRegion(pPortPriv->pDraw, &pPortPriv->clip);

这个是需要的。xv的框架输出,我们需要汇报一下,我们写了这个区域,如果有人也在使用这块区域,就需要知道这个事情。

所以实际上xv的框架涉及到的扩展比较多
Xv,Xcomposite,Xdamage,Xfixes,

大体上就是这样了,其他的函数都是一些辅助的函数,直接照抄就行了。

另外要说明的一点就是。各种不同的flag的作用。
VIDEO_CLIP_TO_VIEWPORT等等,这个是根据不同的显示模式来设置的。
overlay和texture是不一样的,overlay在现在好像用的很少了,不过因为是硬件的支持,可能比texture会快一些,毕竟一个单独的layer操作
起来加上硬件的alpha blending会快的多,而texture的输出,完全靠显卡,感觉现在的显卡虽然很强了,但是面对大量的数据的alpha blending
无论是从显存大小,还是数据传输还是计算能力,都还是不够的。pc如此,嵌入式更如此。
想想现在都是大分辨率1440x900这个是一般的小屏幕了,24bit depth,然后还要加上composite,需求加倍。内存占用就很多了,还有就是所有的窗口都要先
offscreen render一下,然后再说alpha blending,很慢,我也不知道composite有什么好,为了酷炫,就要付出更多的代价。
唉,用的是fcitx的全拼打字,还是有点慢的,上面的东西写了一个半小时,其他事情也还比较多
kaa不知道什么时候写了。
kdriver+kaa+kxv
xorg+exa+xv
两个框架其实是类似的,xorg扩展性更强一些,可以动态装载。
xorg的input 自动探测也比较简单,还像说说。evdev统一天下还需时日。
晕,输入居然多余的万字了,我太能了:)
看来我写小说还挺快,一个半小时能超过一万字呢

漏掉一点就是为什么要使用xvideo
这个之前的文章说过,就是我们从mpeg4或者h264等等文件解码出来的实际上是yuv的数据,比如可能是yuv422的
但是我们的屏幕实际上是rgb的,不同的系统不一样了,比如我们的pc一般rgb888的,对于嵌入式来说可能会选择rgb565这样的,所以就需要转换,
这个yuv转rgb是需要很多计算量的,另外就是硬件缩放,比如解码出来的图像是vga或者wvga的,我们要全屏显示的时候就需要缩放,这个时候也非常占用cpu。
没记错的话,应该都是浮点运算。虽然coretex-a8等等都有了vfp等等指令,但是实际上,这点性能还是远远不够。还有就是xvideo的设计就是为了快速的输出,
所以从框架上来说是很精简的,为了快速显示而设计的。

不过有一点要注意就是profile,我们一般会profile一下,看看这个xvideo的实现性能在什么地方,因为现在输出都是由硬件来做的,所以cpu占用很少,
后来用oprofile看的时候发现其实cpu占用最多的地方居然在数据封装的地方,也就是我们把用户程序传递过来的数据封装成硬件支持的格式。
这个地方还是可以用汇编优化一下的。改善很明显。毕竟是驱动里面最"热"的地方。

不过后来因为迁移到了xorg,所以kdrive这边的实现,包括kaa的驱动和xvideo的实现都没再继续用了
xorg这边有芯片提供商在开发xorg的dri+exa的driver,所以我就没再做这个工作了。