SDL学习教程

本章目标
  • 理解SDL_Surface中pixels数据块格式。
  • SDL_PixelFormat结构的分配和释放过程。
  • 知道点对点块移、RLE块移优点和缺陷。
  • 哈希表、surface表、LRU链表协同管理图面。


SDL(Simple DirectMedia)是一个自由的跨平台多媒体开发包。 http://www.libsdl.org/intro.cn/toc.html上有SDL简介、基本编程帮助。


2.1 主图面

聊下学生画图画。学生拿张白纸,然后在纸上画图画,接下让把白纸、图画外延开来。对白纸,它说来也是图画,只不过是个纯白的矩形图画,但这个图画很特殊,它是其它图画容器,它以着自身图画和其它图画混叠同时发挥着管理作用,像限制整张图的尺寸。对图画,可以不用一笔笔画在白纸上,可以拿个印章,直接在白纸盖出个章的图画,可以这么说,要是现成的章够多,把各个“章”拼凑起来就能形成张好图画。

图画,在SDL中用图面(Surface)表示。就像以上说的白纸也是图画,SDL中也是用图面表示了白纸,但白纸这个图面毕竟特殊,像全系统只有一个,发挥着管理功能,我们把这个图面叫主图面。于是用SDL画图可以概述为以下这个过程。
  • 创建主图面。
  • 创建图面,把图面叠加到主图面。
  • 重复步骤2,直到绘画出整张画。

现在操作系统一般都基于桌面,相应地应用程序基于窗口,而且应用程序往往都有个主窗口,再结合主图面,会提出个疑问,它们之间是什么关系?——一可以这么认为,SDL主图面和程序看到的主窗口实现“同一个”功能。SDL是实现跨平台编程的基础,它要基于各平台抽出共性向上提供一个统一抽象,它的主图面(同时会有一个与之关联的SDL_Window变量)是各操作系统下主窗口的抽象。

接下让深入主图面,看主图面的创建过程。

2.1.1 创建窗口
主图面需要和一个窗口关联,对不同操作系统创建窗口往往是使用不用API,甚至是不同语言,像Mac OS X/iOS得用Object-C。为更清楚看到窗口创建过程,让进入源码级调试。

1、在kingdom工程中打开SDL工程中的SDL_windowswindow.c,在WIN_CreateWindow函数内设断点。
2、运行时选“Debug”——“Starting Debug”。会触发断点。

图2-2显示Windows操作系统下SDL如何创建窗口。CreateWindow返回的是HWND类型的窗口句柄,可这种类型其它操作系统是不认识的,SDL必须得有种方法实现跨平台,对此SDL引入SDL_Window结构,Watch中window就是此类型的变量。window中有个driverdata字段,名义上类型是void*,真实类型是struct SDL_WindowData*,但在各个操作系统下此结构中字段是不一样的,像Windows系统有表示窗口句柄(HWND)的hwnd,表示设备上下文(HDC)的hdc,iOS则没有那两个字段,却有表示UIWindow对象的uiwindow。

图2-2中代码显示了创建窗口三个步骤。
  • CreateWindow创建窗口,得到窗口句柄。
  • WIN_PumpEvents处理CreateWindow产生的消息,即常见的TranslateMessage、DispathMessage消息处理函数对。
    1. void WIN_PumpEvents(_THIS)
    2. {
    3.     MSG msg;
    4.     while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
    5.         TranslateMessage(&msg);
    6.         DispatchMessage(&msg);
    7.     }
    8. }
    复制代码
  • SetupWindowData填充SDL自定义的、用于封装窗口的SDL_window结构。

注:Windows在调用CreateWindow创建窗口前必须调用RegisterClass注册对应WNDCLASS,这个步骤是在SDL_VideoInit完成的,具体在SDL_RegisterApp函数。

对创建窗口还有个疑问,创建窗口时位置数据是怎么来的,即CreateWindow中的x、y、w、h参数。WIN_CreateWindow的父函数SDL_SetVidwoMode会传下希望宽度、高度,但SDL_VideoMode必须得有机制检验这个两个参数是否有效,像对于iPhone,它最大只支持480x320,一旦超过就要进行相应非法性处理(采取的是自动修正到有效尺寸)。SDL要能检验有效就得知道此设备能支持的系统分辨率,即显示能力。Watch中SDL_VideoDisplay类型的display变量存储了此时系统分率数据。各操作系统采用自个方法得到SDL_VideoDisplay,像Windows下是WIN_VideoInit(更具体是WIN_InitModes),iOS下则是UIKit_VideoInit。

2.1.2 得到像素格式
上面有提到主图面外延开来说属于图面,相应地,学生画图时用的白纸也算图画,只不过是白色矩形而已。现实中画纸是白色矩形,那在SDL中主图面是什么图画?这里可认为虚拟要超过现实,现实没法做出透明纸,程序实现的虚拟“纸”却可以,SDL中主图面一般两种情况:带alpha分量的透明矩形图画、不带alpha分量但保留alpha位置的黑色矩形图画。

美术绘画一般使用红色、黄色和蓝色这三种原色生成不同的颜色,这些颜色就定义了一个色彩空间。和美术绘画有不一样,程序表示色彩空间虽也用三种分量,但使用红色、绿色和蓝色,也就是常称的RGB三基色。除去三基色,程序表示颜色时还加一个透明度分量Alpha,在透明度值的定义上,各家图象格式在范围上一般都遵循0<=alpha<=255,但在具体值对应多少透明度却往往有自个定义,有的认为0是全透明,255是不透明,而有的却是反着来,相应的SDL处理透明度有它自个定义:0是全透明,255是不透明,中间值是半透明、不透明度递增。

透明度不是色彩基色,它用于两图像叠加,而且在叠加时是欲叠加的图面中alpha发生作用,主图面在叠加时只会充当叠加到图面角色,它存在alpha分量也就没啥意义。但由于增加一个透明度分量,程序为保证不失真,处理图像过程中往往都会“自动”保留出用于存储透明度分量位置,主图面为加速操作考虑也会多出这个位置。

虽然说A、R、G、B四分量表示一像素是常例,但不能要求所有时候都是一像素四分量,像传统的Window位图就没法存储Alpha分量,即使是四分量,也有次序上不同,像ARGB还是RGBA。接下的2.2.2会详细说像素格式问题,这里只要知道主图面作为一个图面,它也得有它的像素格式,它用这个格式来表示自个“原生”图像。主图面这个像素格式怎么来的,让进入调试。

1、在kingdom工程中打开SDL工程中的SDL_windowsframebuffer.c,WIN_CreateWindowFramebuffer函数内设断点。
2、运行时选“Debug”——“Starting Debug”。会触发断点。

图2-3显示Windows操作系统下SDL如何得到主图面像素格式。它可概述为采用从设备上下文(HDC)得到位图(HBITMAP),通过这个位图信息来判断当前是如何用A、R、G、B表示一像素。
  • CreateCompatibleBitmap创建兼容位图。
  • 调用两次GetDIBits得到“完整”位图信息(BITMAPINFO),位图信息中有每像素位数,R掩码、G掩码、B掩码。
  • 根据每像素位数及各分量掩码,调用SDL_MasksToPixelFormatEnum得到像素格式。

Watch中的bpp指示位图中每像素位数,它来自bmiHeader.biPlanes*bmiheader.biBitCount。(bmiHeader.biCompression == BI_BITFIELDS)它可归纳为两个提示:一、位图中像素是非压缩的R、G、B分量格式;二、GetDIBits时得到的BITMAPINFO中,紧跟bmiHeader字段后的数据指示三分量掩码(这部分内存传统上用于存储索引式位图的颜色表)。

注意下图2-3中传给SDL_MasksToPixelFormatEnum的Alpha分量掩码,强制传下0,也就是说它不使用Alpha分量,但由于bpp是32,像素格式中还是给保留一字节空位(方便叠加操作)。让在图2-3继续执行,会看到SDL_MasksToPixelFormatEnum计算出的format值是0x86161804,通过接下2.2.2内容,可分析出此格式意义:每像素占四字节,分量依次是XRGB,位数分配是8888,但实际有效位数是24。

2.1.3 创建图面
得到像素格式后,就可用它来创建图面。进入调试看创建是怎么个过程。

1、在kingdom工程中打开SDL工程中的SDL_surface.c,在SDL_CreateRGBSurfaceFrom函数内设断点。
2、运行时选“Debug”——“Starting Debug”。会触发断点。

图2-4显示如何创建主图面。它就是调用SDL_CreateRGBSurface,要是看过更多SDL代码会发现SDL创建非主图面的图面时用的也是这个SDL_CreateRGBSurface,也就是说创建主图面中图面的过程和创建其它图面没什么两样。

主图面中图面和其它图面有什么不一样?图2-4显示了一个特点:主图像中像素数据(pixels)是预分配的(参考WIN_CreateWindowFramebuffer,数据来自位图),相应地flags字段给添加了SDL_PREALLOC标志(有了该标志后,释放图面时不会释放pixels指向的内存)。

调用SDL_CreateRGBSurface时width、high传0是要通知SDL_CreateRGBSurface不要为pixels分配内存。

至此可说下主图面和主窗口关系。从“得到像素格式”可得出主图面像素格式是根据主窗口得出的,从“创建图面”可看出,主图面尺寸、像素数据来自主窗口,也就是说主图面基于主窗口创建的。程序中变量的隶属关系上,图2-4的Call Stack进入SDL_GetWindowSurface后则可看到这样一条语句。
window->surface = SDL_CreateWindowFramebuffer(window);
window就是主窗口,即变量隶属关系上主窗口有个指向主图面的指针变量。

联合图2-2、图2-3、图2-4,会看到“创建窗口”、“得到像素格式”、“创建图面”都是在在一个叫SDL_SetVideoMode的函数内,SDL_SetVideoMode是SDL向外提供的、用来实现“一次性”创建主图面的函数。

主图面小结
  • 创建主图面分三个步骤:创建窗口、得到像素格式、创建图面。
  • 应用程序调用SDL_SetVideoMode“一次性”创建主图面。
  • 主图面会关联一个窗口,它基于此窗口创建。
  • 主图面中图面的像素数据是预分配的。

2.2 渲染图像

2.2.1 图面(SDL_Surface)
视频是多媒体重要组成部分,处理视频是SDL基本任务。视频一般指动画,动画是由一系列帧组成。SDL中视频围绕处理图像帧。

SDL不处理把帧组织成动画,这个组织看后面本书“动画”。

图面(Surface)封装一图像。程序中它的类型的是struct SDL_Surface。SDL_Surface成员变量:
  • flags:标志。指示该图面是什么类型图面,像SDL_SRCALPHA指示该图面的像素数据块中有Alpha分量。
  • format:像素格式。参考1.2.2 像素格式(SDL_PixelFormat)
  • w, h:图面的宽度、高度。单位是像素。
  • pitch:宽距。一般情况下它的值等于w*每像素字节数,但内存对齐考虑,有时会补上N个字节。
  • pixels:像素数据块。
  • userdata:应用自定义数据。用它应用程序可以存放特定于该图面的数据块。
  • locked, lock_data:用于同步访问该图面。
  • clip_rect:裁剪矩形。一旦设了该矩形,该矩形之外的像素就不会被修改,类似于Photoshop中的选区。
  • map:快移时使用的一种结构。参考块移
  • refcount:使用计数。一旦创建,使用计数是1;当使用计数为0时,图面会释放它所占用资源,像像素格式块(format)、像素数据块(pixels)、快移时使用映射(map),同时包括自已这个SDL_Surface。


以改变图面颜色函数adjust_surface_color为例,具本说下SDL_Surface各字段意义。

  • flags:值SDL_SRCALPHA,指示该像素数据中有Alpha分量。
  • w,h:要调整的图像尺寸是72x72。
  • format,pitch:像素格式是0x86362004,它指示每一像素占四字节,四字节中分量依次是A、R、G、B。依据宽度和每像素字节数,计算出pitch值是288。

结合代码,看像素数据块字段:pixels。
  1. alpha = (*beg) >> 24;
  2. r = (*beg) >> 16;
  3. g = (*beg) >> 8;
  4. b = (*beg) >> 0;
  5. 拆出一像素的四个分量。

  6. r = std::max(0,std::min(255,int(r)+red));
  7. g = std::max(0,std::min(255,int(g)+green));
  8. b = std::max(0,std::min(255,int(b)+blue));
  9. 具体的调整颜色过程。可以看出每个分量最小值是0,最大值是255。

  10. *beg = (alpha << 24) + (r << 16) + (g << 8) + b;
  11. 把修改后的四分量合回一像素,并就地修改pixels数据块。
复制代码
像素数据块中格式是以左上角为(0, 0),然后从左到右x递增,像素0、像素1、像素2,像素w-1,一行结束后回到左侧继续下一行,像素w,像素w+1,一直这样直到最后一个像素w*h-1。而每一个像素内数据则由像素格式指定,像0x86362004,它指示每一像素占四字节,四字节中分量依次是A、R、G、B。

注:adjust_surface_color只处理一种像素格式,即每像素占四字节,且分量次序是ARGB。当然,由于存在各样格式图像,由它们直接生成的图像它的每像素字节数,分量排列次序都可以不同的,像adjust_surface_color是事先去除格式上差异,归于一种格式进行处理,把这种格式称为中性(neutral)像素格式。make_neutral_surface执行从“任一”像素格式图面生成中性格式图面。中性格式是《王国战争》自定义的,但对一个应用程序来说,一般总需要集中于一种像素格式进行处理。

2.2.2 像素格式(SDL_PixelFormat)
像素格式用于描述图像彩色空间。包括存在哪些空间分量,像RGB、YUV;空间中各分量次序,像RGB、BGR;分量占用位数,像888。分量中除RGB、YUV这些能表示真实颜色的还有个透明度分量:A(Alpha)。

像素格式有两种表现形式。1)uint32_t类型32位值;2)SDL_PixelFormat结构。在相互关系上,从前者解析出后者。

表现形式I:uint32_t类型32位值
BIT 默认值 描述
D31 1 何留值
D30-D28 0 固定值
D27-D24   像素类型
D23-D20   像素中会出现的各分量及次序(高位到低位)
D19-D16   各分量占用位数
D15-D8   每像素位数
D7-D0   每像素字节数(以它计算出的位数大于等于每像素位数)

常见的以uint32_t表示的像素格式
SDL_PIXELFORMAT_ARGB8888(0x86362004):存在A、R、G、B四个分量,排列次序是ARGB,每个分量占8位。
SDL_PIXELFORMAT_ABGR8888(0x86762004):存在A、R、G、B四个分量,排列次序是ABGR,每个分量占8位。
SDL_PIXELFORMAT_RGB888(0x86161804):存在RGB三个分量,排列次序是RGB,每个分量占8位,还保留了8位给Alpha分量。

表现形式I I:SDL_PixelFormat结构
SDL_PixelFormat是从uint32_t位值解析出的结构, 是SDL_Surface结构中一个字段。为叙述具体,以SDL_PIXELFORMAT_ARGB8888(0x86362004)为例子进行说明。
  • Uint32 format:依据它解析出的uint32_t类型32位值。例:0x86362004。
  • SDL_Palette* palette:调色板数据。例:0x00000000。
  • Uint8 BitsPerPixel:每像素位数。例:32。
  • Uint8 BytesPerPixel:每像素字节数。例:4。
  • Uint8 padding[2]:(对结构对齐而使用的填充字节)
  • Uint32 Rmask:32位值,R分量掩码。例:0x00ff0000。
  • Uint32 Gmask:32位值,G分量掩码。例:0x0000ff00。
  • Uint32 Bmask:32位值,B分量掩码。例:0x000000ff。
  • Uint32 Amask:32位值,A分量掩码。例:0xff000000。
  • Uint8 Rloss:8减去R分量占用位数。例:0。
  • Uint8 Gloss:8减去G分量占用位数。例:0。
  • Uint8 Bloss:8减去B分量占用位数。例:0。
  • Uint8 Aloss:8减去A分量占用位数。例:0。
  • Uint8 Rshift:Rmask的第一非0位位置。例:16。0x00ff0000的第一非0在16位。
  • Uint8 Gshift:Gmask的第一非0位位置。例:8。
  • Uint8 Bshift:Bmask的第一非0位位置。例:0。
  • Uint8 Ashift:Amask的第一非0位位置。例:24。
  • int refcount:引用计数。分配和释放时使用。
  • struct SDL_PixelFormat* next:下一个像素格式结构块。

分配/释放SDL_PixelFormat结构
以内存占用上说,单个SDL_PixelFormat结构占用60字节,单个来说不算大,但如果要处理数以万计图面(SDL_Surface),而每个图面都在这个么SDL_PixelFormat,这累加起来就大了。以实用观点说,虽然图像格式有好多种,但一次程序运行时会碰到可能也就十来种,也就是说只要十来个不一样的SDL_PixelFormat就可表示碰到的像素格式。基于这两个原因,没必要为每个图面都分配SDL_PixelFormat,SDL采用单独的链表来管理此次程序碰到的SDL_PixelFormat,SDL_Surface存储的只是指向此链表中某一结点的指针。

分配(SDL_AllocFormat)
1:从堆中分配;
2:分配出的堆块被组织成链表。formats指向链表头。
3:SDL_AllocFormat分配时,并不是每次都一定从堆中分配出一块。当它发现链表中已存在“相同”像素结构块,它只是把那块的使用计数增一。“相同”条件:SDL_PixelFormat中format值相等。

释放(SDL_FreeFormat)
1:SDL_FreeFormat释放时,并不是每次都一定从堆中释放出一块。当它发现要释放块使用计数减一后还大于零,只是简单减一就返回。
2:减一后等于零,则要搜索formats链表,删除该SDL_PixelFormat结点。

显示格式
之前有说过一个叫中性像素格式的概念,引入中性像素格式主要目的是为以一种统一格式处理(像缩放、裁剪、着色等)图像,因为统一的目的是为后续的处理操作,中性像素格式往往是最方便图像处理的像素格式,像ARGB8888。这里让考虑另一种使用场合,对图面来说它往往都要叠加到主图面、作为最终给用户看到的图像的一部分,当它叠加到主图面时,要是它此时的格式和图面格式一致,叠加最省CPU,把这种格式称为显示格式。

很显然,显示格式和主图面格式密切相关,像RGB分量次序要和主面图一致;分量占用字节数要和主图面一致。和主图面可能不一样的是,不管主图面是否存在Alpha分量,显示格式则总会存在Alpha。必须存在Alpha的原因是程序可能会基于显示格式表示的图面进行操作。如何由主图面格式得到显示格式,参考“SDL_DisplayFormatAlpha”。

小结三种像素格式。
文件格式:图像文件中像素格式。它由图像制作者决定。
显示格式:图像从文件中加载后,缓存在内存中格式。它决定于当前主图面格式。等待它的是两种操作:1)转换为中性格式参与生成新图面;2)转换为主图面格式叠加到主图面。
中性格式:程序为统一处理图像,主观定义的一种格式。在进行某种图像处理前,把参与处理的图面像素格式都转为中性格式。为后绪显示,处理生成的图面往往会被转换为显示格式放在内存中。

SDL_CreateRGBSurface
知道图面结构(SDL_Surface)、像素结构(SDL_PixelFromat)后,让理解如何以指定像素格式创建一个空的ARGB图面。SDL_CreateRGBSurface执行这个创建过程。
  1. SDL_Surface *SDLCALL SDL_CreateRGBSurface(Uint32 flags, int width, int height, int depth, Uint32 Rmask, Uint32 Gmask, Uint32 Bmask, Uint32 Amask);
复制代码
以指定像素格式创建一个空的ARGB图面。

执行流程
1:以SDL_calloc分配出SDL_Surface字节内存,栈内指针变量surface存储这个指针。
2:以参数中给的数据分配像素格式结构。surface->format指向这个结构。
3:把图面整个矩形赋给surface->clip_rect。
4:分配surface->h * surface->pitch字节,surface->pixels指向这块内块,zero这块内存。
5:分配map结构。surface->map指向这个结构。
6:surface->map->info.flags |= SDL_COPY_BLEND。(SDL_SetSurfaceBlendMode)
7:surface->flags |= SDL_SRCALPHA。(SDL_SetSurfaceBlendMode)。
8:设置surface使用计数为1。


1:SDL_CreateRGBSurface创建的是空图面。
2:SDL_CreateRBGSurface经常是那些要创建新图面函数的首个函数,那些函数在SDL_CreateRGBSurface图面上进行块移,像SDL_ConvertSurface。

Alpha混叠/叠加两图面
早期设备由于CPU性能、内存限制,对程序如何表示像素往往以如何“省”为目标,像采用尽量少的位表示一像素,采用调色板来表示像素,但现在,即使是相对较弱的手机,进行一般图像处理的话CPU、内存已不再是瓶井,转而是要求如何才能让显示的图像更逼真。要做到逼真,前提是图像像素格式中必须存在Alpha分量,可以这么说,在往后的图像处理中你可以不懂调色板,但你不能不知道什么是Alpha混叠。

Alpha混叠是把两图面混叠在一块生成一新图面,这看似是两个输入一个输出,实际程序实现中是把一图面混叠去另一图面,而生成结果“就地”写在另一图面。在这过程中把去混叠的图面叫源图面(Source、src),混叠到的、生成的图面称为目标图面(Destination、dst)。

Alpha混叠算法有多种,以下是一种较常见的混叠算法(SDL_COPY_BLEND),此种算法一个特点是和目标图面的透明度无关)。
  1. res = (s * alpha + d * (255-alpha)) / 255

  2. alpha:叠加标志中没有SDL_COPY_MODULATE_ALPHA时,alpha就是源图面中该像素透明度srcA;否则“alpha = (srcA*mudulateA) / 255”,mulateA是叠前预设的透明度调整值。但不论是否存在SDL_COPY_MODULATE_ALPHA,alpha都和目标图面的透明度无关。
  3. s、d:是源图面、目标图面中某一分量,res是混叠后的值。分量包括R、G、B。
  4. R、G、B、A取值范围是0到255。

复制代码
为减少乘法次数,让转换以上公式。
  1. res = (s * alpha + d * (255-alpha)) / 255
  2.      = (s * alpha + 255 * d – d * alpha) / 255
  3.      = (s * alpha – d * alpha) / 255 + d
  4.      = (s – d) * alpha / 255 + d
复制代码
SDL中的ALPHA_BLEND宏实现了以上公式。
  1. #define ALPHA_BLEND(sR, sG, sB, A, dR, dG, dB)        \
  2. do {                                                \
  3.         dR = ((((int)(sR-dR)*(int)A)/255)+dR);        \
  4.         dG = ((((int)(sG-dG)*(int)A)/255)+dG);        \
  5.         dB = ((((int)(sB-dB)*(int)A)/255)+dB);        \
  6. } while(0)
复制代码
和SDL_COPY_BLEND对应的还有两个值:SDL_COPY_ADD、SDL_COPY_MOD。它们是把两图面合成一图面的三种方式,不同的是SDL_COPY_BLEND采用Alpha混叠(要求源图面必须存在Alpha分量),后面两个不使用Alpha。以下代码可较明显看到它们三者区别。
  1. ......
  2. case SDL_COPY_BLEND:
  3.         dstR = srcR + ((255 - srcA) * dstR) / 255;
  4.         dstG = srcG + ((255 - srcA) * dstG) / 255;
  5.         dstB = srcB + ((255 - srcA) * dstB) / 255;
  6.         break;
  7. case SDL_COPY_ADD:
  8.         dstR = srcR + dstR; if (dstR > 255) dstR = 255;
  9.         dstG = srcG + dstG; if (dstG > 255) dstG = 255;
  10.         dstB = srcB + dstB; if (dstB > 255) dstB = 255;
  11.         break;
  12. case SDL_COPY_MOD:
  13.         dstR = (srcR * dstR) / 255;
  14.         dstG = (srcG * dstG) / 255;
  15.         dstB = (srcB * dstB) / 255;
  16.         break;
  17. ......
2.2.3 块移(Blit)
调整图面颜色(adjust_surface_color),缩放图面(scale_surface),这些是针对一个图面的操作。除它们外还有种操作是把两图面合成一个图面,像把一个小图面放到一个大图面的左上角,这个合成操作主要任务是处理像素数据块(SDL_Surface中的pixels)。像要把一个小图面放到一个大图面的左上角,直观上想到就是把小图面的pixels替换掉大图面pixels的左上角部分。对这种两图面像素数据“合成”操作,图像处理中存在个术语:块移(Blit)。SDL库中处理块移的操作就是SDL_BlitSurface。

在SDL库内,SDL_BlitSurface有另一个名称,SDL_UpperBlit,这两个函数就是同一个。
  1. #define SDL_BlitSurface SDL_UpperBlit
复制代码
SDL_UpperBlit函数参数及返回值
  1. int SDL_UpperBlit(SDL_Surface * src, const SDL_Rect * srcrect,SDL_Surface * dst, SDL_Rect * dstrect)
复制代码
  • src:[IN]。源图面。把一个小图面放到一个大图面的左上角操作中它就是那个小图面。不能是NULL。
  • srcrect:[IN]。源图面裁剪矩形。它基于的是源图面,因而它的尺寸不要超过源图面,在块移时,只块移裁剪矩形内的那部分。srcrect可以是NULL,NULL时表示块移动整个源图面。
  • dst:[IN OUT]。目标图面,同时也是块移动后图面。把一个小图面放到一个大图面的左上角操作中它是那个大图面,同时也是块移后图面。不能是NULL。
  • dstrect:[IN]。目标图面上的一个坐标点,它作为源图面块移到目标图面的(0, 0)点。dstrect类型虽是SDL_Rect类型,但它真正起作用的只有x、y,忽略w、h。

返回值:0成功块移;负数指示失败。

SDL_UpperBilt最后要调用SDL_LowerBlit。SDL_UpperBilt、SDL_LowerBlit中的Upper和Lower概念指的是任务的上半部和下半部,上半部主要任务是参数(主要是srcrect、dstrect)合法性检查、修正。下半部,也就是SDL_LowerBlit执行真正的块移。

块移,直观认为只要把小图面像数数据替换掉大图面相应位置的像素数据就行了,但实际上SDL_LowerBlit(包括它要调用到函数)是一个非常复杂函数,那么是什么原因造成的?
1、存在裁剪矩形。允许源图面存在裁剪矩形,这使得复制像素数据起始点、及每行起始点要有相应判断。
2、像素格式不一致。源图面和目标图面像素格式不一致。不一致分个两层次,一是每像数字节数不一致;二是字节数一致情况下,分量不致。
3、优化存在Alpha分量时块移。当Alpha分量是0时,表示该像素没有颜色,要是出现源图面中,该点就不需要块移,当源图面中存在大量Alpha=0像素时,可以用种更优方法来块移像素,它就是RLE块移。

在此把块移分为两种:以传统方法逐个像素拆包、修改、合包的称为点对点块移;以RLE方式的称为RLE块移。

2.2.4 点对点块移
为直观描述点对点块移,让进入代码级调试。
1、在kingdom工程中打开SDL工程中的SDL_blit.c。
2、在SDL_SoftBlit函数内设断点。
3、运行时选“Debug”——“Starting Debug”。
4、调出“Call Stack”窗口,选择SDL_LowerBlit函数。

这个块移动目的是把图标图像(/images/game-icon.png)生成的图面块移到一个空图面上。图标图像尺寸64x64。
源图面像素格式(0x86762004)和目标图面像素格式(0x86362004)不一致,不一致表现在第二层,在每像素都是4字节下,源图面分量排列次序是ABGR,目标图面是ARGB。

源图面像素格式不是中性格式,这里要把ABGR转为ARGB倒不是要转为中性格式,而是Windows位图(Bitmap)格式要求像素块是R、G、B这个存储次序。把调用栈设置到WIN_SetWindowIcon(_THIS, ...)可以察看如何从SDL_Surface生成一张Bitmp位图。

来看块移逻辑。块移时要用到SDL_Surface中的map字段,也可以这么说,map字段就是给块移用的。块移时涉及到两个图面,那么是源图面的map字段发挥作用?是目标图面map字段发挥作用?还是两个图面的map字段共同发挥作用?——源图面的map字段发挥作用。

map类型是struct SDL_BlitMap。它当中字段语义:
  • dst:目标图面。
  • identity:指示块移时两个图面是否有同样像素格式。当两图面相同像素格式时,块移具有最高效率。
  • blit:函数指针。指向不同方式(点对点/RLE)下块移函数,。
  • data:指针。在点对点块移和RLE块移有不同语义。
  • info:SDL_BlitInfo结构,块移信息。
  • palette_version:带调色板才需要。《王国战争》中所有图面都不带调色板,即这个字段总是0。


到调用SDL_SoftBlit时,map已被填充,那么是谁填充了map?——SDL_MapSurface。SDL_MapSurface负责填充此次块移时需要的map结构,更具体说SDL_MapSurface需要计算出blit、data字段。

blit
指向块移方式函数。点对点块移是SDL_SoftBlit,RLE块移是SDL_RLEBlit(不带Alpha分量)/SDL_RLEAlphaBlit(带Alpha分量)。

data
在点对点块移和RLE块移下有不同语义。
点对点块移时,它指向具体像素级块移函数,计算出是哪个函数的依据是源图面和目标图面的像素格式。像BlitNtoNCopyAlpha,它是源图面和目标图面具有相同每像素字节数,带Alpha分量时候。而当两图面具有相同像素格式时(map的identify=1),data指向就是SDL_BlitCopy。
RLE块时data语义参考“RLE块移”。

点对点块移下的map小结:块移过程中只使用源图面中map;map中所有字段会根据此次块移情况进行更新,也就是说map中原来值对后面没影响。块移整个过程中,目标图面map保持原来值。

点对点块移过程小结
1、SDL_MapSurface计算出此次块移时需要的map结构,map计算结果存放在源图面中。
2、调用map->blit,也就是SDL_SoftBlit。
3、SDL_SoftBlit在执行具体像素块移时调用map->data。


2.2.5 RLE块移

RLE块移:图面级概念。表示整个RLE块移过程。
RLE复制:像素数据级概念。特指RLE解码和复制到目标图面这个过程。

RLE块移是指SDL_BlitSurface时,源图面中数据先被RLE压缩,然后把这些RLE数据复制到目标图面的方式。和它同一概念是点对点块移,即SDL_SoftBlit。

看完本节后要能回答以下问题
1、什么指示此次块移是RLE方式?
(SDL_Surface.map.info.flags & SDL_COPY_RLE_DESIRED)为真时,指示以它为源图面的块移都将是RLE方式。
(SDL_Surface.flags & SDL_RLEACCEL)为真不能表示RLE方式,它是表示这个图面中的像素数据是RLE格式。

2、RLE块移前后,源、目标图面像素数据是什么格式
源图面:块移前可能是RLE也可能是未压缩格式,块移后肯定是RLE。
目标图面:块移前、后都只能是未压缩格式。是RLE还是点对点对它来说没区别。

3、RLE这样压缩、解压缩,效率上难道比点对点高吗?
当源图面存中在较多透明像素,并且对该原图面时行块移时连续的都是同一个目标图面,RLE块移效率要高于要比点对点高。此种方式下它的后N-1次过程有的只是解压缩,不须要压缩。

RLE块移
为直观描述RLE块移,让进入代码级调试。
1、在kingdom工程中打开SDL工程中的SDL_RLEaccel.c。
2、在SDL_RLEAlphaBlit函数内设断点。
3、运行时选“Debug”——“Starting Debug”。
4、调出“Call Stack”窗口,选择SDL_LowerBlit函数。

这个块移目的是把logo图像(/images/misc/logo.png)生成的图面块移到主图面上。

结合图2-1,注意点对点块移、RLE块移时源图面SDL_Surface中的几个字段。
  • flags:点对点时是0x00010000,RLE时是0x0001002。0x00000002值语义是SDL_RLEACCEL,它指示该图面存的是RLE压缩过的像素数据。
  • pixels:点对点时是像素块地址,RLE时是NULL!RLE图面把像素块数据块存放在map->data。
  • map->blit:块移方式函数。点对点时是SDL_SoftBlit,RLE时是SDL_RLEAlphaBlit。
  • map->data:点对点时是函数指针。RLE时是一个数据块指针,块中内容是struct RLEDestFormat加上RLE编码后的像素数据。


SDL_RLEAlphaBlit
1、计算出源图面像素数据起始地址(srcbuf)、目标图面像素起始地址(dstbuf)。源图面是RLE图面,map->data指示像素数据存放地址块,但这个块前头存的是RLE压缩辅助数据(RLEDestFormat)。计算srcbuf用的是srcbuf = (Uint8 *) src->map->data + sizeof(RLEDestFormat)。
2、源图面存在裁剪矩形,并且y>0时,调整srcbuf,让跳过这些行。跳行时必须同时解码相应行。
3、解码源图面,同时把像数数据混叠(do_blend)到目标图画。

小结:
1、RLE复制时,源图面已被RLE压缩。复制时执行的是RLE解码。
2、目标图面不能是RLE图面。
3、当存在源图面裁剪矩形时,如果矩形最底行的y小于源图面高度,接下那些行则不会被解码。其它行则一定会被解码,即使是小于裁剪矩形左上角y的行。

点对点块移时已写过,SDL_MapSurface负责填充此次块移时需要的map结构,更具体说SDL_MapSurface需要计算出blit、data字段,那针对blit字段,它是怎么计算出此个值是SDL_SoftBlit,还是SDL_RLEAlphaBlit/SDL_RLEBlit?

为直观描述这个判断,让进入代码级调试。
1、在kingdom工程中打开SDL工程中的SDL_blit.c。
2、在SDL_CalculateBlit函数内的“if (SDL_RLESurface(surface) == 0) {”处设断点。
3、运行时选“Debug”——“Starting Debug”。
4、调出“Call Stack”窗口。

在Watch中看surface,这个suface就是上图中的src。
SDL_MapSurface调用SDL_CalculateBlit进一计算map字段。对于map->blit,它起初赋值是SDL_SoftBlit,当if (map->info.flags & SDL_COPY_RLE_DESIRED)为真时,它要调用SDL_RLESurface,在该函数内map->blit会被赋值为SDL_RLEAlphaBlit/SDL_RLEBlit。也就是说, 判断此次块移是点对点还是RLE的依据就是源图面(map->info.flags & SDL_COPY_RLE_DESIRED)是否为真。

SDL_RLESurface
让思考两个问题:
1、RLE复制(SDL_RLEAlphaBlit/SDL_RLEBlit)执行的是RLE解码,那何时执行的编码?
2、RLE复制是使用SDL_RLEAlphaBlit还是SDL_RLEBlit,它的依据是什么?
这两个问题是在SDL_RLESurface中解决的,SDL_RLESurface主要任务是对图面进行RLE压缩。
让在SDL_RLESurface设断点。

此中surface就是上图中的surface,也就是块移中的源图面。

从以上可以看出,当像素数据中没有Alpha分量或map->info.flags没有SDL_COPY_BLEND标志,它调用RLEColorkeySurface进行压缩,解码时使用SDL_RLEBlit,否则调用RLEAlphaSurface进行压缩,解码时使用SDL_RLEAlphaBlit。

函数最后会把图面置上SDL_RLEACCEL标志,指示这是个RLE图面。

避免RLE复制前解码
对于RLE效率,到此我们会发现个问题,当一个源图面是RLE图面,它要进行RLE块移时必须先被解码(SDL_MapSurface首先会判断源图面是否是RLE图面,如果是则调用SDL_UnRLESurface进行解码),在判断出它此次要进行RLE块移,它又立即被RLE编码,RLE复制时它则被解码。依照这个过程,每一次RLE都要重复解码、编码、解码过程,效率太低。

为提高RLE块移效率,采取一种变通方法是连续N次块移到的都是同一个目标图面,后N-1次只须进行复制时解码。程序上方法就是修改map->dst使用机制,也就让前一次RLE块移时形成的map->dst对后面有影响,当此次dst就是上一次的dst时,略过SDL_MapSurface,直接调用RLE复制函数map->blit。(参考SDL_LowerBlit)。

此处又有个疑问,dst可以直接解了,何必一定要局限于dst,既然已经编码,难道不能“直接”解到其它图面?——答案是不能。解码时要用到编码时形成的RLEDestFormat(数据放在map->data),RLEDestFormat数据有效性依赖于某个目标图面。

RLE块移过程小结
1、当此次src->map->dst不是此次块移的目标图面,调用SDL_MapSurface计算出此次块移时需要的map结构,计算过程中如果源图面是RLE图面要进行RLE解码。
2、调用map->blit,即SDL_RLEAlphaBlit或SDL_RLEBlit。
3、SDL_RLEAlphaBlit/SDL_RLEBlit执行边解码边复制像素数据。

RLE压缩原理
术语

  • 透明像素(transparence):Alpha分量是0的像素。
  • 半透明像素(translucent):Alpha分量>0并且<255的像素。
  • 不透明像素(opaque):Alpha分量是255的像素。


1、RLE以行为一块进行压缩,块和块之间独立,各块之间互不影响。
2、行内压缩格式:(像素数都以两个字节表示)
  1. 第N行从左到右扫描,形成(透明和半透明像素数,不透明像素数)(不透明像素具体数据)
  2. 扫描线回到N行零点,从左到右扫描,形成(透明和不透明像素数,半透明像素数)(半透明像素具体数据)
复制代码

左侧是图像原始数据,格式ARGB,尺寸72x72,即一行72像素,00000000h到00000119h是第一行数据,右侧是RLE压编后数据。以解释第一行来看RLE是如何工作的。

第一次扫描目的是形成不透明像素。第一行没有不透明像素,(透明和半透明像素数,不透明像素数)(不透明像素具体数据)这部分内容了就占四个字节:48 00 00 00。“不透明像素具体数据”不占字节。

扫描线回到第一行点,第二次扫目的是形成半透明像素。在这一行中有数段半透明数据。
#1:(26, 5)。这一段前面有26个连续“透明和不透明像素”,然后是连续5个半透明像素。RLE编码后数据就是:1A 00 05 00。对于紧跟的“半透明像素具体数据,就是后面的20(4 * 5)个字节:18 FF 00 05 18 FF 00 24 18 FF 00 39 18 FF 00 22 18 FF 00 04。至此这一段形成00h到1bh字节。

#2:(10, 5)。这一段前面有10个连续“透明和不透明像素”,然后是连续5个半透明像素。RLE编码后数据就是:0A 00 05 00。对于紧跟的“半透明像素具体数据,就是后面的20(4 * 5)个字节。

#……

压缩就是这样一行一行持续下去。

RLE压缩是看到图像中存在透明像素,压缩就是去掉透明像素。也就是说,透明像素越多,压缩率越高,要是图像中没有透明像素,它压缩后形成的数据量比原图像还大。


2.2.6 块移小结

以上介绍了点对点块移、RLE块移,当程序中要用到大量图面时,为提高效率就要合理选择哪图面是用哪种块移方式。

以下情况下不推荐使用RLE。
  • 不会作为源图面的图面。此个是100%不推荐,因此主图面肯定不要RLE。
  • 很少透明像素的图面。没什么透明像素时,RLE压缩效率很低,这个低不仅表现在耗CPU还耗内存。
  • 块移到的目标图面是要经常变换的图面。RLE压缩过程是和某个目标图面联系在一起的,一旦下次块移换了个目标图面,上次RLE就必须被解压,然后才能根据此次的目标图面重新RLE。


程序中如何设置某图面是点对点还是RLE方式
  1. SDL_SetSurfaceRLE(SDL_Surface * surface, int flag)
复制代码
  • surface:要设置块移方式的图面。
  • flag:0时,图面设置为点对点方式;非0时设为RLE方式。
  1. SDL_SetAlpha(SDL_Surface * surface, Uint32 flag, Uint8 value)
复制代码
它是通过调用SDL_SetSurfaceRLE来设置块方试。调用具体语句:SDL_SetSurfaceRLE(surface, (flag & SDL_RLEACCEL)),也就是说flag置上了SDL_RLEACCEL标志时,该图面将进行RLE块移,否则点对点块移。

SDL_LockSurface/SDL_UnlockSurface
这两个函数用于加锁、解锁图面。当程序试图直接访问一个图面的像素数据时,最好调用下这对函数。

通过上面分析会发现,当一个图面是作为源图面被RLE块移后,和它对应的struct surface将保持着RLE格式。像pixels字段是NULL,map字段中的blit指向RLE块移解码函数(SDL_RLEBlit/SDL_RLEAlphaBlit),当然,flags字段中会有SDL_RLEACCEL标志指示该图面存储的是RLE格式。相应地,系统中也可能存在非RLE格式的struct surface,也就是说,这些图面的pixels指向未压缩的ARGB数据,flags字段中没有SDL_RLEACCEL标志。因此,编程时要面对的图面可能有两种格式:RLE和非RLE。

为实现要求的功能,程序不得不清楚将要处理的图面是什么格式,像要把一图面变得更透明,即把每像素的Alpha值都变小。它们是针对图面每像素进行操作,对这种操作,非RLE时,pixels字段存的就是未压缩的ARGB,直接编辑就可以,而RLE图面则先要进行RLE解压,为方便后续处理,编辑完像素后还要进行RLE压缩,恢复回原来的RLE状态。于是它们就形成了一种固定操作模式。
  • 判断要处理图面是RLE还是非RLE,是RLE的进行解压;
  • 执行要求的像素处理操作;
  • 此图面原来是RLE的进行RLE压缩,让恢复回原状态。

SDL_LockSurface/SDL_UnlockSurface就用于以上这个目的,SDL_LockSurface实现第一步,对图面加锁,SDL_UnlockSurface实现第三步,对图面解锁。很显然,要实现第三步解锁,它必须知道此个被加锁的图面原来是RLE还是非RLE,那么是哪个字段指示了这个标志?以下是两函数代码。
  1. int SDL_LockSurface(SDL_Surface * surface)
  2. {
  3.     if (!surface->locked) {
  4.         if (surface->flags & SDL_RLEACCEL) {
  5.             SDL_UnRLESurface(surface, 1);
  6.             surface->flags |= SDL_RLEACCEL;  /* save accel'd state */
  7.         }
  8.     }
  9.     ++surface->locked;
  10.     return (0);
  11. }

  12. void SDL_UnlockSurface(SDL_Surface * surface)
  13. {
  14.     if (!surface->locked || (--surface->locked > 0)) {
  15.         return;
  16.     }
  17.     if ((surface->flags & SDL_RLEACCEL) == SDL_RLEACCEL) {
  18.         surface->flags &= ~SDL_RLEACCEL;     /* stop lying */
  19.         SDL_RLESurface(surface);
  20.     }
  21. }
复制代码
SDL_LockSurface/SDL_UnlockSurface没有用额外变量来存储图面先前状态,而是采用继续在flags中置上SDL_RLEACCEL!这样操作会导致一个RLE图面经过SDL_LockSurface后是被解压了,但flags中仍旧有SDL_RLEACCEL标志,这不符合块移逻辑要求(块移逻辑一看到flags中有SDL_RLEACCEL,会率先执行RLE解压)。由于这个不符合,加锁后的图面不能用于块移,加锁/解锁的使用场合是上层想直接访问、修改图面中像素数据,就像以上说的修改图像透明度。而一旦访问像素结束后,必须调用SDL_UnlockSurface进行解锁,这不仅是本来RLE的要求恢复回RLE,更是要让图面的flags恢复到正常“真话”状态。

仔细看SDL_LockSurface/SDL_UnlockSurface会发现,当要处理图面是非RLE时,它的加锁、解锁只不过是修改locked字段,上层要是不想管locked,可说就没作用,即对非RLE图面的加、解锁可说是空操作。为配合是不是有必要必须执行加、解锁,SDL提供了一个宏,当要直接访问一图面的像素数据时,程序可调用该宏来知道是否要调用SDL_LockSurface/SDL_UnlockSurface进行加、解锁。
  1. #define SDL_MUSTLOCK(S)        (((S)->flags & SDL_RLEACCEL) != 0)
复制代码
虽然宏名叫SDL_MUSTLOCK,该宏的本质是判断要处理的图面是RLE还是非RLE。通过该宏,再结合SDL_LockSurface/SDL_UnlockSurface必须成对出现要求,程序可设计一个自动给待处理图面加、解锁的类,这类就构造、析构这两个函数。
  1. surface_lock::surface_lock(surface &surf) : surface_(surf), locked_(false)
  2. {
  3.         if (SDL_MUSTLOCK(surface_))
  4.                 locked_ = SDL_LockSurface(surface_) == 0;
  5. }

  6. surface_lock::~surface_lock()
  7. {
  8.         if (locked_)
  9.                 SDL_UnlockSurface(surface_);
  10. }
复制代码
为此当应用程序要直接访问图面surf像数据时,它只要在访问前以surf为参数创建surface_lock对象就行了。
  1. surface_lock lock(surf);
复制代码
一旦lock作用域失效后,像所在函数退出了,系统自动会调析构函数,从而实现自动解锁。

对SDL_LockSurface/SDL_UnlockSurface需要重申的是: 处于加锁中图面不要进行任何的块移操作

BUG?RLEAlphaSurface
RLEAlphaSurface用于执行RLE压缩,压缩时要使用surface中的map->dst字段。
  1. static int RLEAlphaSurface(SDL_Surface * surface)
  2. {       
  3.         SDL_Surface *dest;
  4.         SDL_PixelFormat *df;
  5.         ...
  6.         dest = surface->map->dst;
  7.         if (!dest)
  8.                 return -1;
  9.         df = dest->format;
  10.         ...
  11. }
复制代码
但是,surface->map-dst可能是无效的!
RLEAlphaSurface用于两种场合,一是块移(blit),二是SDL_UnlockSurface。在块移时,surface->map->dst总是有效的,但在SDL_UnlockSurface可能会变成无效。让看以下这样一个操作序列。
  • A是一个RLE图面。执行一次到图面C的块移。块移结束后,A的map->dst指向C。
  • C图面被释放。但没有更新A中的map->dst字段!(要C在释放同时更新块移到它图面的map->dst,这要求太过苛刻。)
  • 程序试图直接访问图面A的像素数据,于是调用SDL_LockSurface/SDL_UnlockSurface。当执行SDL_UnlockSurface时,dest虽然指针非NULL,但它指向图面其实已经无效。按SDL代码,执行后将倒致程序崩溃。

要如何解决这个问题。一种简单、彻底但存在争议的办法是让SDL_UnlockSurface不调用RLEAlphaSurface,也就是说RLEAlphaSurface被调用只剩块移场合,RLEAlphaSurface要操作的图面也就不会无效了。但是,这种方法带来的副作用是不管加锁前是不是RLE,一旦被使用过“锁”操作后,全都变成非RLE,如果程序中大量使用存在部分透明图面,这会大幅增加消耗内存。这个副作用使得这种方法看似简单但不实用。

为节约内存考虑,解锁SDL_UnlockSurface还是得调用RLEAlphaSurface。对这问题我建议使用两个方法。

方法一:严格RLEAlphaSurface对map->dst的有效性判断
官方给的RLEAlphaSurface判断map->dst只用了map->dst是否是NULL,这个条件太弱,一块内存是不是属于图面还有其它一些特征,“与”这些特征就可剔除掉更多的无效情况。
  1. if (!dest || !dest->format || !dest->refcount || !dest->w || !dest->h || dest->locked)
  2.         return -1;
  3. if ((dest->flags & SDL_RLEACCEL) || (dest->pitch < dest->w) || (dest->pitch & 3)) {
  4.         return -1;
  5. }
复制代码
(dest->flags & SDL_RLEACCEL)基于的事实是块移到的图面一般不会是RLE图面。当然,这里存在一种可能,该块移到图面被RLE后很快会被RLE解压缩,对于之前块移到它的图面来说它又成为一个有效的目标图面了。此处为简单不考虑这种可能性,对于RLEAlphaSurface来说,漏过一个无效图面会立即导致程序崩溃,把一个有效的判为无效大不了多占用该图面“额外”内存(本应RLE格式存在的以非RLE存在了),加之块移到图面被RLE又被解RLE是特例,一般程序很少出现。

方法二:找出程序可能导致RLEAlphaSurface无效的图面,换另种实现方法
RLEAlphaSurface要无效,它须要有个前提:它块移到的图面必须先它之前被释放。要破除这个前提,可以把图面“总是”块移到生命期是它父集的图面,像主图面,主图面生命期长于其它图面,那么RLEAlphaSurface也就“永远”不会无效。有些图面是无法做到块移动到图面是主图面,这时它可采用其它办法,像强制对该图面使用传统的点对点块移,使得加锁/解锁图面时不会出现和RLE相关的任何操作。要有效当然还有其它办法,在编写程序时要应地置宜,目的就是要RLEAlphaSurface不会遇到非法图面。

程序中可能存在数以万计图面,要分析这数以万计图面的来龙去脉然后找出哪个图面可能会导致RLEAlphaSurface非法是不现实的。但这个事情总得做,可采用办法是在RLEAlphaSurface判断map->dst返回无效的地方设断点(就像以上的两处return -1),然后每次运行都使用SDL的Debug版本,同时以调试模式运行exe,如此运行过程中只要程序遇到无效就会触发断点,跟据当时“Call Stack”就可找到哪图面会导致无效RLEAlphaSurface。

在解决问题时以上两种方法要一块使用,这样才能让程序更稳定。


2.2.7 管理图面

当程序中存在大量图面时,管理它们就变得必要了。
  • 缓存图面数据。从图像文件到生成SDL图面需要耗一定CPU,使用缓存、通过牺牲内存来降低CPU使用率。
  • 生成的图面要能灵活改变尺寸。像游戏中大地图格子,在1280x800大分辨率下,它是72x72尺寸,但在480x320小分辨率下是48x48。这个48x48是72x72经过缩小后得到,也就是说从文件得到48x48要经过两个步骤,当中就可使用缓存从而减少步骤。
  • 生成的图面要能灵活改变颜色。像一天有白天、黑夜,在不同时断下同样内容黑夜中颜色要比白天暗。如果白天图面是文件直接生成的图面,那黑夜图面是白天图面经过调整颜色后得到,也就是说从文件得到黑夜图面要经过两个步骤,当中可使用缓存从而减少步骤。

管理图面是通过数据结构来达到要求的,数据结构的优劣直接决定了管理效率。衡量管理效率可使用以下指标:
1、随机访问效率。
2、判断命中/未命中效率。
3、缓存不可能无休止膨胀,释放时算法。
4、管理结构占用的内存。

几个术语
哈希表(Hash table,也叫散列表):是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
开放寻址法:它是哈希表处理冲突方法。公式:Hi = (H(key) + di) MOD M, i=1,2,…, k(k<=M-1)。
关键码:哈希表中的关键码值。具体到此处是hash、hash1这两个整数值。
特征值:它指的是从image::locator中用于形成关键字的那些字段。包括图像文件名、类型、修饰字符串、坐标等。

流程描述
命中情况:一个*.png文件形成一个image::locator对象,根据locator中特征值形成关键码。把关键码值映射到哈希表中发现j记录和它匹配,命中!取出j记录中的index字段,以它为索引在surface表定位出index行,根据index行中的position字段定位到LRU中的index结点,把该结点提到LRU链表头。最后取出index行中surface给调用程序。

未命中情况:一个*.png文件形成一个image::locator对象,根据locator中特征值形成关键码。把关键码值映射到哈希表中发现没有记录和它匹配,未命中!调用IMG_Load由图像文件生成surface,从LRU中依释放策略得出index,由于index结点要成为最新访问结点,LRU中要把index结点提到链表首,相应的要把此次surface更新到surface表中index行。接下是更新哈希表,再次以关键码定位哈希表,找到第一个空行,把此次形成的更新该行。最后把surface返回给调用程序。

A:image::locator是什么类?特征值有哪些?
Q:image::locator是个封装图像文件的类,它不产生SDL图面,但它提供了在cache中定位出它所包含的图像对应的SDL图面的方法。一个image::locator对象对应着磁盘上一个图像文件。
struct image::value中的字段都是特征值。

filename_:图像文件名。它是特征值主要部分。它存的是已经去掉了修饰(如果参数给的文件名有修饰,修饰就会被放在modifications_)的文件名。

type_:一个locator(value)或是FILE类型,或是SUB_FILE类型。FILE、SUB_FILE区别在于要把要对从整个图像文件产生的图面进行后续处理。

SUB有一种字面意思是“子”,但SUB_FILE中SUB意义不是表示SUB_FILE得到图面是FILE得到图面的子图面,而是说此次得到的图面要在FILE图面上进行再处理。当然,如果这个处理是裁剪(~CROP),则恰好符合了SUB“子”这个概念。除裁剪,处理还包括移位(通过loc_)、缩放(~SCALE)、颜色势力化(~TC)、翻转(~FL)、灰度化(~GS)等。

再处理标志:modifications_不为空、loc_.valid()==true。当然modifications_为空并且loc_.valid()==false也可能是SUB_FILE,只不过这个再处理是空处理罢了。

value中还有两个字段:center_x_、center_y_,它们也只有在loc_.valid()==true才发生作用。

FILE得到的图面肯定是整个图像文件产生的图面。SUB_FILE和到的图面则要看后续处理操作,如果没有后续操作(modifications_为空并且loc_.valid()==false),它得到的图面就是FILE图面。

A:关键字为何有两个?如何计算hash、hash1?
Q:关键字中有两个值是为了让不同特征值却产生相同关键字的概率趋向零。
1、要在哈希表定位,必须使用整数作为关键字。
2、特征值中存在文件名,要把这个字符串转为整数,没法做到一对一。
3、对同一特征值用两个函数计算出哈希值,hash、hash1,认为(locator, )是一对一。
hash计算方法是通过boost库提供的hash算法。
hash1计算法是对文件名进行简单的RLEHash。

A:哈希映射函数和处理冲突方法?
Q:映射函数y= hash MOD M。M是哈希表长度,hash就是关键字中的hash,boost库计算出的哈希值有较好均匀特性。

处理突冲方法是当y = hash MOD M计算出的y有已被使用时(index=-1)执行y=y+1,如果这个y还是被使用,再执行y=y+1,直到找到空闲记录。
总和映射函数和处理冲突,它就是开放寻址法,Hi = (H(key) + di) MOD M, i=1,2,…, k(k<=M-1)。

在找空闲记录时,映射函数y= hash MOD M就够了,但要取属于自己的记录时,还要加上校验该记录上的hash1值,只有满足了该记录中的hash1等于自个计算出的hash1才能认为是属于自己的的记录。

A:LRU表作用?它在溢出策略中起什么作用?
Q:LRU全称Least Recently Used,即最近最少使用。引入它就是实现溢出策略。
LRU表使用规则:链表中结点从头到尾排序次序表示了该结点使用情况,越靠头结点它是最靠近现在使用过的,也就是说,最后一个结点是最不常用的,当要溢出时就释放该结点。

LRU链表总是保持一个固定长度N,这个长度就是surface表长度。因而当LRU表擦掉最尾结点时,只要它发现该结点指向了哈希表中一条记录,然后把那记录置为无效,意味着LRU表在处理surface表溢出时同时解决掉了哈希表的溢出问题。 LRU保证了哈希表中最多只存在N条有效记录。
  1. template
  2. int cache_type::add(const T &item, size_t hash, size_t hash1)
  3. {
  4.         // in locator_table, find a empty position.
  5.         size_t pos = hash % locator_table_size;
  6.         while (locator_table_[pos].index != -1) {
  7.                 if (++ pos == locator_table_size) {
  8.                         pos = 0;
  9.                 }
  10.         }

  11.         // calcuate index of content_
  12.         int index = lru_list_.back();

  13.         cache_item& elt = content_[index];
  14.         if (elt.pos_in_locator_table != -1) {
  15.                 locator_table_[elt.pos_in_locator_table].index = -1;
  16.         }
  17.         elt.item = item;
  18.         elt.pos_in_locator_table = pos;

  19.         lru_list_.erase(elt.position);
  20.         lru_list_.push_front(index);
  21.         elt.position = lru_list_.begin();

  22.         // fill (hash,hash1,index)
  23.         locator_table_[pos].hash = hash;
  24.         locator_table_[pos].hash1 = hash1;
  25.         locator_table_[pos].index = index;

  26.         return index;
  27. }
复制代码
add函数处理把item加入surface表。它主要分三个步骤:
1、在哈希表中找到一个空闲记录,pos指示了这条记录。
2、处理surface表和LRU。index直接取自LRU最后一个结点,如果该结点指向哈希表记录,就把该记录置为无效。item赋给surface表中记录。接下对LRU处理就是删除最后一个结点,然后再表头新加一个结点。
3、此次形成的更新到哈希表相应记录。

A:如何表示哈希表中记录是占用状态还是空闲状态?状态之间如何转变?
Q:哈希表记录中有个index字段,它-1表示空闲,非-1时表示占用。
初始时所有index是-1,整个表是空的。
空闲到占用:当有记录要被占用时,index值被置为一个大于等于零的值,这个值范围[0, N-1](N是surface表长度),它正是surface表的索引。
占用到空闲:LRU链表,也可认为surface表满时,当又来新记录,它就须要无效掉一条记录,surface表那条要被无效的记录通过内部一个变量(pos_in_locator_table)无效掉它在哈希表中的对应记录,也就是把那记录的index值置为-1。

A:如何处理冲突地址上意外无效?
Q:让看一种情况:图像P计算出的关键值是6,定位到6记录,结果发生冲突,依据冲突算法本应在位置6的被移动位置10,接下出现7图像被释放,当此后P图像再来访问,搜到7时,发现它是空的,于是就不在搜了,而认为P不在缓冲中,不命中!可P图像其实在缓存中。把这种情况叫冲突地址上意外的无效。

解决这办法是让1)检查到7时,虽然它是无效但还是继续搜。但它不可能无休止搜下去,必须约定一个次数,超过遇到这个数的-1才认为确实没在缓存中。2)加大哈希表长度,减少冲突机率。

哈希表度长度要定为多少?哈希表中最多只有N条有效记录,N是surfade表长度。哈希表最长,出现冲突机率越低。

遇到多少个-1才认为确实不存在?这个数设得越大,误判不命中次数越少,但设得越大,无疑会增大判断命中/不命中时间,导致算法效率变差。

以下是两组经验数据
surface表长度 1500 1500
哈希表长度 4500 6000
运行时间 60秒(滚动地图) 60秒(滚动地图)
最大冲突长度 21 13
忽略1个-1后从不命中变为命中次数 77286 108729
忽略2个-1后从不命中变为命中次数 10356 29822
忽略3个-1后从不命中变为命中次数 X 706

A:使用cache,如何减少改变过尺寸/颜色图面的生成步骤?
Q:有个地形图像P,在1280x800大分辨率下,它是72x72尺寸,但在480x320小分辨率下是48x48。这个48x48是72x72经过缩小后得到,也就是说从文件得到48x48要经过两个步骤,这时就可以为每个步骤使用一个cache。
images_:存的是直接从磁盘图像文件生成的72x72图面。
scaled_to_hex_images_:存储72x72图面经过缩小后图面,像48x48。
当主程序序分辨率发生变化,希望是56x56时,scaled_to_hex_images_将全部清空以存储新的56x56,56x56也要比72x72缩小而来,由于images_依旧有效,到此第一步骤时全部命中,等于减少一个步骤。

改变颜色基于的也是同一过程,多使用一个cache:tod_colored_images_。

A:cache中的clear_cookie_变量是干什么用的?
Q:clear_cookie_指示了在flush时是否要无效掉surface表中的所有记录。
当程序改变环境时,它会调用cache的flush操作,像下一回合,下一回合可能从黄昏变为黑夜,像改变了分辨率,要求格子尺寸从72x72变为48x48。而当碰到这些操作时,像黄昏变为黑夜,tod_colored_images_这个cache,它存的是之前改了颜色图像,即黄昏时图像,变为黑夜,颜色要更暗,但由于颜色这个参数不是image::locator特征值,即黄昏和黑夜下,同一图像它的hash、hash1值是一样的,要是不无效掉tod_colored_images_就会使得黑夜下取某个图像,它在tod_colored_images_命中,取出的却是黄昏时图像!

只有images_这个cache的clear_cookie_置为false,它可说是直接从磁盘上得到的图面,不经过缩放、不经过改色,一直有效,而为了让缓存不断发作用,它在flush中不无效surface表。

管理效率
1、随机访问效率。
由特征值计算出hash、hash1,hash模M得到哈希表中一个位置,接下是数个加法处理冲突,最坏的加法次数等于最大冲突长度。

2、判断命中/未命中效率。
由特征值计算出hash、hash1,hash模M得到哈希表中一个位置,由于会出“冲突地址上意外无效”,致使遇到一个-1时还要继续搜,要执行更多加法。最坏加法次数等于最大冲突长度*可认为确实不存在的-1个数。

3、缓存不可能无休止膨胀,释放时算法。
要做的操作是删除LRU链表尾结点,向LRU头加入一个结点。会保证释放的是最长时间未使用的图面。

4、管理结构占用的内存。
以surface表长度等于1500,哈希表长度等于6000为例。
哈希表占用内存:长度*一条记录字节数 = 6000*12 = 70.2125K。
surface表占用内存:长度*一条记录字节数 = 1500*cache_item对象长度。
LRU表占用内存:长度*一条记录字节数 = 1500*std::list一个节点长度。

小结
1、管理结构内存还有点的,再加上程序中还需要多个cache,内存占用成倍上升。为此cache能省要省,像确定不希望进行地图编辑的iOS系统就不要semi_brightened_images_。
2、“冲突地址上意外无效”带来的不仅仅是降低命中/未命中判断效率,更是会导致误判。虽然加大哈希表长度、增加认为确实不存在的-1个数可以减少误判,但解决不了根本问题。
3、(M>=N)哈希表长度M必须大于等于surface表长度N(否则判断是否命中时会进入死循环(locator::in_cache(...))。加上命中效率是以判断时能够多快遇到“-1”来衡量,出现更多“-1”则要求M比N越大越好,因而实际使用时M会数倍于N。


2.3 渲染字符串

2.3.1 渲染字符串原理
SDL_ttf提供了三各方式渲染字符串:Solid、Shaded和Blend。三种方式中Solid渲染速度最快,但是字体不太美观。Shaded有点慢,但字体美观,不过是空心字体。Blend渲染最慢,但字体最为美观。为达到最好显示效果,要选择Blend方式,虽然最耗cpu,但以现在cpu处理速度是可接受的,即使是较低性能的移动式平台。


SDL是如何以Blend方式渲染字符串,让进行入代码级调试:
1、在kingdom工程中打开SDL_ttf工程中的SDL_ttf.c。
2、在TTF_RenderUNICODE_Blended函数内的“alpha = *src++;”处设断点。
3、运行时选“Debug”——“Starting Debug”。
4、调出“Call Stack”窗口。

此次任务要渲染的是UTF8编码字符串,具体渲染过程:
1、kingdom.exe调用SDL_ttf.dll提供的库函数TTF_RenderUTF8_Blended。
2、TTF_RenderUTF8_Blended调用UTF8_to_UNICODE把UTF8字符串转为UNICODE编码。然后调用TTF_RenderUNICODE_Blended渲染这个UNICODE字符串。
3、TTF_RenderUNICODE_Blended渲染字符串过程:首先调用SDL_AllocSurface创建出一个ARGB像素格式的空图面;然后一个字符一个字符画在这个空图面上。

TTF_RenderUNICODE_Blended是如何把字符ch画在空图面上的?——它从font参数指定的字体文件中取出ch对应字模,依据字模信息,一像素接一像素,扫完一行后回到行首继续下一行,就这样一行接着一行在空图面的对应像素上进行着色。字模信息中每像素是介于[0,255]中一个值,这个着色是把这个值直接作为空图画上该像素的Alpha分量。

width:每一行都不一样。

小结
1、渲染字符串结果是形成一个SDL图面,字符串“画”在这个图面上。上层程序要记得释放这图面。
2、图面是32位的ARGB像素格式。为什么Blend方式比Solid、Shaded要费时,它采用32位而Solid、Shaded都采用8位像素格式是个重要原因。
3、为什么此种方式要叫Blend?它是用字模信息修改Alpha分量,Alpha是个混叠参数,Blend中文就译作混叠。由于它改的是Alpha分量,字模信息中各像素取值范围[0, 255],致使在一个字符内它画出的像素间也会存在明、暗差异(透明度不同造成)。。
4、渲染后得到是透明背景图面。
5、在字符编码上,SDL_ttf最终要处理成UNICODE编码,因而在上层编码选择时它是推荐用UNICODE。


2.4 渲染音频

2.4.1 音频格式
音频格式三要素:样本格式、通道数、采样率。
  • 样本格式:以什么样数值表示一个声音样本。样本格式包括三个参数:有符号还是无符号;一个样本占多少位,理论它可以任意,但使用中只有8和16;占用多个字节时,小端序(LSB)还是大端序(MSB),
  • 通道数:声音通道数目,像单声道,立体声(双声道)。各个通道中样本可以是不同格式,但同一通道内样本只一种格式。
  • 采样率:单位时间采样数。常用采样率有8K、16K、32K、44.1K、48K,当中的K不是1024而是1000。当存在多通道时,它是同时对各个通道采样,因而把采样率定义为单位时间采样样本数时,要注意这个样本不是指一个通道样本。


基于三要素计算一持读时间为T的声音占用字节数。
  1. 声音数据字节数 = 一样本字节数 X 通道数 X 采样率 X T。
复制代码
以上计算可以看出,声音数据字节数一定是“一样本字节数*通道数”整数倍,也就是说,如果出来不是“一样本字节数*通道数”字节长度的整数倍数据,则可以肯定这是段不完整声音,要正常播放需把多余字节给去掉。把一样本字节数*通道数称为声音粒度(Granularity),又因为一样本长度不是8就是16,以下是根据声音粒度去除多余字节公式:
  1. audio_len &= ~(Granularity - 1)。
  2. 注:audio_len是要调整声音数据字节数,同时存储调整后字节数。
复制代码
从音频文件中读出声音到声卡放出声音,要经过两种音频格式:音频文件中格式、渲染格式。
音频文件中格式:它是音频文件中用以记录声音的格式。这里注意区别下音频压缩格式,文件中音频格式和音频是采用什么压缩无关,压缩、解压缩不会改变音频格式。

渲染格式:音频数据写到声卡缓冲时的格式。声卡要能正常播放一段声音,它必须得知道播放缓冲存的是什么样格式数据。渲染可以是什么格式,这是由声卡驱动决定的,一般来说这个格式会很灵活,但一次程序运行时一旦选定一种渲染格式,一般就不大可能会改变。

很显然,音频文件中格式和某个音频文件相关,因而不同文件可以有不一样音频格式。但这段声音要能播放出来,这些格式必须统一转化为渲染格式,以着渲染格式写入声卡缓冲。为节省转换时间,需要的音频文件格式和渲染格式越相似越好。

Mix_LoadWAV_RW
Mix_LoadWAV_RW从一个声音文件读出数据,边读边解码(如果需要),最后把数据转换为渲染格式。
进入调试代码级调试。
1、在kingdom工程中打开SDL_mixer工程中的mixer.c。
2、在Mix_LoadWAV_RW函数内的“if ( SDL_ConvertAudio(&wavecvt) < 0 ) {”处设断点。
3、运行时选“Debug”——“Starting Debug”。
4、调出“Call Stack”窗口

wavespec表示音频文件中格式。图示中格式是:
  • format:样本格式。值0x8010(AUDIO_S16LSB),有符号16位数,小端序。
  • channels:通道数。值1,即单声道。
  • freq:采样率。值44100,它是44.1K采样率。


再看此时的mixer变量,它表示渲染格式。

  • format:样本格式。值0x8010(AUDIO_S16LSB),有符号16位数,小端序。
  • channels:通道数。值2,即双声道。
  • freq:采样率。值44100,它是44.1K采样率。


结合wavespec和mixer,它们有一样的样本格式、采样率,但不一样声道数,要能播放这段声音,需要把单声道转换为渲染格式要求的双声道。

SDL_BuildAudioCVT、SDL_ConvertAudio执行这个转化。转换分两步:SDL_BuildAudioCVT构造转换函数表,SDL_ConvertAudio调用转换表中函数进行转换。

让进入SDL_ConvertAudio,把断点设在cvt->filter_index = 0,执行到断点看cvt变量。

展开filters,它是一个函数指针数组,函数表中存在的SDL_ConvertStereo执行的就是把单声道转为双声道。此次转换只要这么一个函数就够了。
filter_index表示转换函数个数,但此处要执行时立即把它置0,那执行转换函数时怎么知道哪个是最后一个转换函数?
让进入SDL_ConverStereo,在它的函数尾会看到:

其它转换函数都有这样代码,它判断表中下个函数地址是否为NULL,如果是NUL它就是最后一个转换函数。因而虽然表中10个入口,但最多可存在9个转换函数。

存储声音数据的内存块
以文件中格式表示的声音数据
SDL_LoadWAV_RW/Mix_LoadOGG_RW边读声音文件边解码(如果需要),解码后数据放在chunk->abuf。
  • chunk->abuf:音频数据缓存,它用SDL_malloc在SDL_LoadWAV_RW/Mix_LoadOGG_RW分配,上层记得要用SDL_free进行释放。它是以音频文件中格式表示未压缩过(如果文件是压缩过则已经解压)音频数据。
  • chunk->alen:chunk->abuf的有效字节数,也就是音频数据字节数,它除上粒度,再除上采样率就是声音能持续播放时间。

以渲染格式表示的声音数据
文件中格式表示的声音数据进入SDL_ConvertAudio,这函数输出是以渲染格式表示音频数据,它把生成数据放在wavecvt.buf,wavecvt.len_cvt指示wavecvt.buf有效字节数。

从分析两种格式音频数据可以看出,Mix_LoadWAV_RW执行完后所有数据已经解码,并转化为渲染格式。到此看下它的返回值,即Mix_Chunk各字段意义。
  • allocated:1。
  • abuf:已经支持渲染格式的未压缩音频数据。
  • alen:abuf数据长度。
  • volume:MIX_MAX_VOLUME(128)。


Mix_LoadWAV_RW缺陷
Mix_LoadWAV_RW执行读文件、解码、转为渲染格式,它一个函数就干了这么多事,但这种做法存在很大问题,因为它“全”都做了,一旦声音数据量大,它界面反应慢是小事,更严重的是它要存放整块解码后数据,系统就没法提供它需要的如此大内存!让考虑下格式是16位、双声道、 44.1K,持续时间是4分钟的一段声音,它要占用近41M内存(4*44100*240),4分钟如此,更别说比它还大的文件。Mix_LoadWAV_RW适合处理小段声音,也就是后面要说的音效,而对于长时间声音不能用这函数,也就是后面说的音乐。音乐虽不用这函数,它的音频数据处理要经过阶段和音效差不多,都要经过读文件、解码、转换为渲染格式,转换时用的也是SDL_BuildAudioCVT、SDL_ConvertAudio,只不过它是一段一段地读、解码、转换、并播放。

2.4.2 播放声音
这里的播放声音特指把符合渲染格式音频数据放入声卡缓冲,声卡播放声音这个过程。
让进入调试状态
1、在kingdom工程中打开SDL_mixer工程中的mixer.c。
2、在mix_channels函数内设断点。
3、运行时选“Debug”——“Starting Debug”。
4、调出“Call Stack”窗口,设置到SDL_RunAudio。

SDL_RunAudio负责播放声音。
Watch窗口中显示currnt_audio这个SDL_AudioDriver实时信息,SDL_AudioDriver是对声卡驱动抽像,它实现播放声音是通过impl这个SDL_AudioDriverImpl对象。impl是个函数表,它提供了播放声音需要的一系列操作,像GetDeviceBuf负责把声卡缓冲地址返回给上层,WaitDevice阻塞住上层线程,直到声卡有有效缓冲。

impl对象是对声卡操作的抽像,正是它在细节上实现了SDL的跨平台渲染音频。示例中的“xaudio2”是使用DirectX实现渲染音频,MacOS X/iOS则使用“coreaudio”实现渲染音频。

通过查看SDL_RunAudio,可以小结下播放声音步骤,更直观说是如何调用impl中相关函数。
1、impl.GetDeviceBuf获得声卡缓冲地址。缓冲长度是个固定值,程序初始时设定,通常设为16384。
2、“(*fill)(udata, stream, stream_len)”向缓冲填入要被播放的音频数据。此处fill对应的是mix_channels。
3、impl.PlayDevice通知声卡驱动,缓冲区数据有效了,可以播放。声卡播放声音。
4、播放速度是要按采样率来的,有持续时间,那上层程序如何知道同步这个持续时间?它是调用impl.WaitDevice,这函数会阻塞调用线程,直到又一个声卡缓冲区有效。

fill一次填充的只是16384字节,声卡驱动不可能只有16384字节。以小字节填充一是为满足播放连贯性,A段在播放时B段在填充,A段一播放完B段已有效;二是为实现其它功能,像混合。因为这个小字节,impl.WaitDevice播放的一般不是刚填入的那段声音。

SDL_AudioDriver它是在“RunThread”这个线程上下文执行的,区别于像Mix_LoadWAV_RW的“Main Thread”线程。一旦开启SDL支持音频,它就会创建一个RunThread线程,它负责混合、播放声音。

以上播放步骤小结是device->convert.needed=0,注意下device->convert.needed=1时,它指示fill填入的音频数据不是渲染格式,要渲染须要转换。因而它fill进的不是impl.GetDeviceBuf得到的缓存,而是device->convert.buf这个内存地址。在把数据写入声卡缓存时要调用SDL_ConvertAudio把格式转换为渲染格式,再调用impl.GetDeviceBuf得到声卡缓存,把转换后数据写入缓存。

以上大概说了播放声音过程,接下让考虑一个问题:混合声音。开着电脑,一边玩游戏听背景音乐,一边开Winamp听mp3,这就是个混合声音例子。两个应用程序在产生声音,但听去它们被“无缝”接合了,好像声音是“并行”播放。

对声卡来说,它任意一时刻只能播放一种声音,或播放游戏背景音乐或播放mp3。我们听去“无缝”接合,那是声卡对声音进行“时分复用”,把多种声音“时分复用”到一个缓冲区上,由于每次播放一小段声音,像16384字节,人耳分辨不出如此细粒度声音,于是认为声音是“并行”播放。

同时开游戏背景音乐和mp3是复合两个应用程序声音,混合也发生在一个应用程序中。像开着背景音乐,然后按下一个按钮,为有好的用户界面,按下按钮有会出声音提示,这就是背景音乐和用户界面音效混和例子。或开着背景音乐,游戏中出现魔法攻击,魔法攻击时会放出魔法飞的声音,这是背景音乐和攻击音效混和例子。

回头看播放过程,它在向缓冲区填数据时用“(*fill)(udata, stream, stream_len)”,此处让考虑个想法,如果fill实现的不仅仅是把一种声音填入缓冲,而是把数种声音按策略进行混合,然后再填入缓冲,会不会就实现混合声音功能?——实际正是如此,fill这个函数指针一般情况下就是指向mix_channels这个以着通道方式将声音进行混合的函数。

2.4.3 混合声音
先说下SDL_mixer引入的两个术语:音乐和音效。
音乐(music):表现在有较长的持续时间。常见例子是歌曲,程序的背景音乐。
音效:表现在较短的持续时间,可能就是毫秒级。常见例子像按下按钮时提示音,特定事件发生的提示音,像新邮件到了,哪个好友上线了。

SDL_mixer提供的一大功能是混和声音,它混合声音的基础是把声音分成音乐和音效,至于具体到特定程序中哪段声音属音乐、哪段声音属音效,这是由程序员决定的。

为理解SDL_mixer如何混合声音,让进入调试状态。
1、在kingdom工程中打开SDL_mixer工程中的mixer.c。
2、在mix_channels函数的“while (mix_channel[i/].playing > 0 && index < len) {”上设断点。
3、运行时选“Debug”——“Starting Debug”。
4、等待程序直到进入主菜单界面,按下主菜上一个按钮,这时会要求声卡播放按钮按下提示音,倒至触发第2步上设置的断点。
5、代码窗口定位到mix_channels内以下窗口显示的位置。

结合2.4.2 播放声音,可以看到stream就是声卡缓存,len是缓存可用字节数。
由图示中代码可以发现,mix_channels混音策略是先处理音乐(mix_music)、然后处理音效(mix_channel)。

知道这个基本顺序,接下让问两个问题?
1、程序设定背景音乐是不断重复,是否意味着mix_music总会填满声卡缓存?如果填满,此次音效数据如何放入缓存?
程序设定背景音乐是不断重复,的确会造成mix_music总会填满声卡缓存。但是,虽然音乐填满了缓存,可如果查到此次有音效,音效还是会播放,因为音效数据填充方式是从缓存地址0处开始填!

这种覆盖式填法导致的实际情况是,虽然处理音乐早于音效,但音效播放优先级反高于音乐!

通过看整个mix_channels,会发现它向缓存填了三次数据。
1:memset(stream, mixer.silence, len)。从地址0开始,整缓存置为静音。
2:mix_music。从地址0开始,填充音乐。
3:mix_channel。从地址0开始,填充音效。

2、为什么音效要分散到数个通道?
原因主要出在音效多样性。举个例子,按下按钮同时来了新邮件,这时要出两个音效,作为处理原则,这两个音效数据不能交叉。当然,应用程序自可处理不交叉,只是SDL_mixer提供了一种辅助手段,上层只要使用两个通道,把按钮声放在A通道,新邮件声放在B通道,SDL_mixer就会保证了不交叉,并且引入优先级,通道号小的那种声音会被优先播放。

最后让说下_Mix_Channel中几个字段语义。
  • chunk:音效是通过Mix_LoadWAV_RW从文件“一步”生成,它返回的Mix_Chunk就被直接赋给这个chunk指针。
  • samples:要开始播放音频数据地址。它的初始值是chunk->alen,随着播放进行,它会不断向后挪。
  • playing:还需要播放音频字节数。它的初始值等于是chunk->alen。
  • tag:通道标识。这个是应用程序主观设置的值,用于区别各种通道。
  • looping:循环播放次数。0指示不循环播放。


2.5 处理事件

事件包括键盘输入、鼠标输入、缩放窗口、退出程序等,不同操作系统以不同机制触发事件,SDL作为实现跨平台编程的基础库,它需要向上层提供统一的处理事件抽像。

要如何处理事件?SDL是个库,它需要负责收集事件,但针对具体事件如何处理,那是上层应用程序的事,不过这同时要求SDL还有个任务是要能提供API,让上层从它那里检索出正发生的事件。由此可看出SDL需实现收集、检索功能,那这两个功能是如何融入整个系统,SDL实现了以下方案。
SDL收集事件。收集具体是把捕捉到事件表示为统一格式(SDL_Event)并放入事件队列。
应用程序“有空”时就查询事件队列,依次检索出各事件,针对具体事件实施具体处理。如何定义“有空”?一种是应用程序专门有个事件线程,线程不断从SDL检索事件;另一种是应用程序不专门开线程,只是作为主线程中一步骤,那里去检索事件。SDL建议是采用第二种。

接下让以鼠标弹开事件为例,分析SDL如何处理事件。

2.5.1 收集
收集过程不仅要实现捕捉事件,还要把事件放入事件队列。让进入源码级调试。

1、在kingdom工程中打开SDL工程中的SDL_mouse.c,SDL_SendMouseButton函数内设断点。
2、运行时选“Debug”——“Starting Debug”。加载界面时不要按任何鼠标键。
3、进入标题标幕后,按下鼠标左键,会触发断点。

图2-21显示SDL收集鼠标事件,并把事件放入事件队列(SDL_EventQ)。通过Call Stack,可描述出SDL是如何收集此个事件。
  • 在主线程(Main Thread),应用程序调用自个实现函数events::pump,后者调用SDL提供的API:SDL_PumpEvents。
  • SDL_PumpEvents是个跨平台收集事件函数,它要实现收集需调用各操作系下“真正”的收集函数,Windows系统就是WIN_PumpEvents。
  • 在“2.1.1 创建窗口”有提到WIN_PumpEvents,这里重复抄下该函数实现。
    1. void WIN_PumpEvents(_THIS)
    2. {
    3.         MSG msg;
    4.         while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
    5.                 TranslateMessage(&msg);
    6.                 DispatchMessage(&msg);
    7.         }
    8. }
    复制代码
    它从系统收集消息时调用PeekMessage,而不是阻塞式的GetMessage,这保证了要是没事件时主线程可以立即继续做其它事。
  • 捕捉到鼠标弹开事件,DispatchMessage接下把这事件发去窗口过程,即WIN_WindowProc,后者以个大case处理各个事件,鼠标弹开事件的消息码是WM_LBUTTONDOWN。
    1. LRESULT CALLBACK WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
    2. {
    3.         switch (msg) {
    4.         ......
    5.         case WM_LBUTTONDOWN:
    6.                 SDL_SendMouseButton(data->window, SDL_PRESSED, SDL_BUTTON_LEFT);
    7.                 break;
    8.         ......
    9. }
    复制代码
  • SDL_SendMouseButton根据事件状态值(state、button、x、y等)形成以SDL_Event结构表示的一个事件,并把这事件放入SDL事件队列SDL_EventQ。至此完成了捕捉事件、把事件放入事件队列。

结合Watch中的event变量,让说下SDL如何向上层提供统一的事件表示。各个操作系统以自个格式表示事件,SDL要跨平台实现处理事件则必须以一种统一的格式表示事件,这个统一格式就是SDL_Event。但事件种类繁多,光以“一种”struct是不可能表示它们的,为此SDL_Event采用C/C++语法中的union。
  1. typedef union SDL_Event
  2. {
  3.         Uint32 type;                    /**< Event type, shared with all events */
  4.         SDL_WindowEvent window;         /**< Window event data */
  5.         SDL_KeyboardEvent key;          /**< Keyboard event data */
  6.         SDL_TextEditingEvent edit;      /**< Text editing event data */
  7.         SDL_TextInputEvent text;        /**< Text input event data */
  8.         SDL_MouseMotionEvent motion;    /**< Mouse motion event data */
  9.         SDL_MouseButtonEvent button;    /**< Mouse button event data */
  10.         SDL_MouseWheelEvent wheel;      /**< Mouse wheel event data */
  11.         ......
  12. } SDL_Event;
复制代码
SDL_Event表示事件有以下几个特点。
  • 一个SDL_Event实例只能表示一个事件。
  • 采用union,使不同事件占用同一块内存。
  • 各事件按相似程度归类。WM_LBUTTONDOWN、WM_LBUTTONUP就归为SDL_MouseButtonEvent。
  • 各事件的特定结构中第一个字段必须是Unit32 type。

2.5.2 检索
收集过程已把事件放入SDL事件队列SDL_EventQ,以着一般逻辑去猜测,在图2-21中,应用程序实现的自个函数events::pump调用完收集函数SDL_PumpEvents后,接下应该是调用检索函数。事实的确如此,让在图2-21中的Call Stack进入events::pump()。
  1. void pump()
  2. {
  3.         SDL_PumpEvents();
  4.         ......
  5.         SDL_Event temp_event;
  6.         while (SDL_PoolEvent(&temp_event)) {
  7.                 ......
  8.         }
  9.         ......
  10. }
复制代码
SDL_PoolEvent就是SDL向外提供的一个检索API,它从事件队列SDL_EventQ中取出顶头事件,赋给temp_event。而且通过看SDL_PoolEvent代码会发现,SDL_PoolEvent实现的不仅有检索,它还有收集功能,即会调用SDL_PumpEvents!真正只是执行检索的函数是SDL_PeepEvents。

补充下2.5.1在进入调试时为什么要写“加载界面时不要按任何鼠标键”(为更直观了解接下要描述内容请重新Debug”——“Starting Debug”,并在加载界面时按下鼠标键以让触发第一步设置的断点)。加载界面时收集事件、检索事件是通过SDL_PoolEvent。SDL_PoolEvent以一个函数就实现了收集、检索,但在编写自个代码过程中往往并不是采用这种方式,而是把收集、检索分开处理,即进入标题屏幕后的那种处理方式。


你可能感兴趣的:(SDL)