四、DirectDraw高级特性
1、直接内存访问DMA
有些显示设备能够在系统内存表面支持位块传输操作(或其它操作),这些操作就是直接内存访问DMA(Direct Memory Access)。 你可以利用DMA的支持来加速一定的操作。例如,在这种设备上你可以从系统内存位块传输到视频内存上,同时处理器还准备处理下一帧的画面。
在使用DMA操作之前,必须检测设备是否支持DMA及支持的程度。步骤是先用 IDirectDraw2::GetCaps方法来获取设备的能力,然后再查看 DDCAPS结构中的dwCaps成员是否含有DDCAPS_CANBLTSYSMEM标志,若是则表明设备支持DMA。
如果设备支持DMA,还需要知道驱动程序是如何支持的。这就需要查看有关的结构成员,这些成员包含了有关系统内存——视频内存、视频内存——系统内存和系统内存——系统内存之间位块传输操作的信息。DDCAPS结构的几个成员提供了这些信息,如下表所示:
System-to-video
Video-to-system
System-to-system
dwSVBCaps
dwVSBCaps
dwSSBCaps
dwSVBCKeyCaps
dwVSBCKeyCaps
dwSSBCKeyCaps
dwSVBFXCaps
dwVSBFXCaps
dwSSBFXCaps
dwSVBRops
dwVSBRops
dwSSBRops
例如,系统内存到视频内存的位块传输的性能标志由dwSVBCaps、dwSVBCKeyCaps、dwSVBFXCaps和dwSVBRops提供。视频内存到系统内存的位块传输性能标志包含在以“dwVSB”开头的成员中,系统内存到系统内存之间的位块传输性能标志包含在以“dwSSB”开头的成员中。
这些成员最关键的特性是对异步DMA位块传输操作的支持。如果驱动程序支持表面间的异步DMA位块传输操作,DDCAPS_BLTQUEUE 标志将会设在dwSVBCaps、dwVSBCaps、和dwSSBCaps成员中。若该标志不存在,就表明驱动程序不支持异步DMA位块传输操作。
系统内存到视频内存之间的SRCCOPY传输是硬件支持的最常见的位块传输操作。因此,对这种操作最典型的应用是从系统表面内存将许多纹理移动到视频内存。系统内存到视频内存的DMA传输大约同处理器控制的传输速度一样快,但具有更大的优点,因为这些操作可以同主机处理器并行运行。
硬件传输使用物理内存地址而不是虚拟内存地址,某些设备驱动程序要求提供物理内存地址。该机制是通过对IDirectDrawSurface3::PageLock方法的调用完成的。如果设备驱动程序不要求页锁定,在用IDirectDraw2::GetCaps方法获取硬件的能力时,DDCAPS2_NOPAGELOCKREQUIRED标志将被 设定。
页锁定一个表面可以防止系统将表面的物理内存再做她用,并且保持该表面的物理内存不变,直到调用IDirectDrawSurface3::PageUnlock方法为止。如果设备驱动程序请求页锁定,DirectDraw只允许DMA操作在应用程序已经页锁定了的系统内存表面上。如果在这种情况下没有调用IDirectDrawSurface3::PageLock,DirectDraw将用软件仿真执行传输工作。需要注意的是,锁定的表面多了之后将会大大降低Windows运行的效率。因此,最好只在全屏独占模式下处理较多数量的系统内存,并且当应用程序最小化时能够对这些表面进行解锁。当然,应用程序恢复后,应该重新对系统内存表面页锁定。
2、在窗口模式下使用DirectDraw调色板
显示在全屏独占方式下时,IDirectDrawPalette接口方法是直接写向硬件的。显示在窗口模式下时,IDirectDrawPalette接口方法将调用GDI调色板处理函数使得能与其它的Windows应用程序和谐地工作在一起。
2.1、窗口模式下调色板实体的类型
同全屏独占式的应用程序不一样,窗口模式的应用程序必须同其它的应用程序共享桌面调色板。用于同 DirectDrawPalette对象和GDI一同工作的PALETTEENTRY 结构包含了一个peFlags成员,该成员含有系统应该怎样解释PALETTEENTRY结构的信息。peFlags成员描述了三种类型的调色板实体。
2.1.1、窗口静态实体
在标准模式下,Windows保留调色板实体0~9和实体246~255用于系统颜色、显示菜单条、菜单文本和窗口边界等等。为了使应用程序保持一致的外观,避免破坏其它应用程序的外观表现,你需要保护这些设给主表面的调色板的实体。通常,程序员可以调用Win32函数GetSystemPaletteEntries来获取系统调色板,然后显式地在自定义调色板中设定相同的调色板,使之在分配给主表面前同系统调色板匹配。尽管系统调色板的实体复制到自定义调色板中在最开始也可以工作,但在用户改变了桌面颜色的配色时,该自定义调色板就会变的无效。
为了避免用户改变颜色配色后调色板的效果很差,你可提供一个系统调色板的索引来替代指定的颜色值,从而保护合适的实体。用这种方法,不管系统对所给的实体使用了什么颜色,调色板都会始终匹配,不需要做任何更新工作。用于 peFlags成员的PC_EXPLICIT标志使你能够直接索引到一个系统调色板实体。使用了这一标志后,系统就不再认为结构的其它成员包含了颜色信息。你可以将peRed成员的值设置为需要的系统调色板索引而将其它的颜色都设为0。例如,若想保证你的调色板中的适当的实体总是同系统调色板颜色配色匹配,可以使
用以下代码:
// Set the first and last 10 entries to match the system palette.
PALETTEENTRY pe[256];
ZeroMemory(pe, sizeof(pe));
for(int i=0;i<10;i++){
pe[i].peFlags = pe[i+246].peFlags = PC_EXPLICIT;
pe[i].peRed = i;
pe[i+246].peRed = i+246;
}
你可以调用Win32函数SetSystemPaletteUse强制Windows只使用第一个和最后一个调色板实体0和255。在这种情况下,应该在PALETTEENTRY结构中只将实体0和255设置为PC_EXPLICIT。
2.1.2、运动实体(Animated entries)
你可在PALETTEENTRY结构中使用PC_RESERVED标志来指定将用于运动是调色板实体。Windows不允许其它的应用程序将它自己的逻辑调色板实体映射到你指定的物理调色板实体,从而在你的应用程序运动调色板时,避免其它的应用程序改变它们的颜色。
2.1.3、非运动实体(Nonanimated entries)
你可在PALETTEENTRY结构中使用PC_NOCOLLAPSE标志来指定非运动调色板实体。PC_NOCOLLAPSE标志通知Windows不用已经分配了的调色板实体替代该非运动实体。
2.2、在窗口模式下创建调色板
下面是在非独占窗口模式下创建DirectDraw调色板的例子。为了使调色板能够正常的工作,最好在提交给 IDirectDraw2::CreatePalette方法的PALETTEENTRY结构中将256个实体都设置好。
LPDIRECTDRAW lpDD; // Assumed to be initialized previously
PALETTEENTRY pPaletteEntry[256];
int index;
HRESULT ddrval;
LPDIRECTDRAWPALETTE lpDDPal;
// First set up the Windows static entries.
for (index = 0; index < 10 ; index++)
{
// The first 10 static entries:
pPaletteEntry[index].peFlags = PC_EXPLICIT;
pPaletteEntry[index].peRed = index;
pPaletteEntry[index].peGreen = 0;
pPaletteEntry[index].peBlue = 0;
// The last 10 static entries:
pPaletteEntry[index+246].peFlags = PC_EXPLICIT;
pPaletteEntry[index+246].peRed = index+246;
pPaletteEntry[index+246].peGreen = 0;
pPaletteEntry[index+246].peBlue = 0;
}
// Now set up private entries. In this example, the first 16
// available entries are animated.
for (index = 10; index < 26; index ++)
{
pPaletteEntry[index].peFlags = PC_NOCOLLAPSE|PC_RESERVED;
pPaletteEntry[index].peRed = 255;
pPaletteEntry[index].peGreen = 64;
pPaletteEntry[index].peBlue = 32;
}
// Now set up the rest, the nonanimated entries.
for (; index < 246; index ++) // Index is set up by previous for loop
{
pPaletteEntry[index].peFlags = PC_NOCOLLAPSE;
pPaletteEntry[index].peRed = 25;
pPaletteEntry[index].peGreen = 6;
pPaletteEntry[index].peBlue = 63;
}
// All 256 entries are filled. Create the palette.
ddrval = lpDD->CreatePalette(DDPCAPS_8BIT, pPaletteEntry,
&lpDDPal,NULL);
2.3、窗口模式下设置调色板实体
用于IDirectDraw2::CreatePalette方法的PALETTEENTRY中的规则同样适用于IDirectDrawPalette::SetEntries方法。因此,你可以维持PALETTEENTRY结构组而不需要重建它。在需要的时候,更改结构组并调用IDirectDrawPalette::SetEntries方法。在大多数的环境中,处于窗口模式时,不要试图来设置任何窗口静态实体,否则将会出现无法预料的结果。只有在对实体复位时才允许设置窗口静态实体。
对应调色板动画,你只需要改变PALETTEENTRY结构组中的很少一部分实体,将这些实体提交给IDirectDrawPalette::SetEntries方法。
如果想对这些实体进行进行复位操作,只能复位那些用PC_NOCOLLAPSE标志和PC_RESERVED标志标记过的实体。试图运动其它的实体会带来不可预知的错误。下面的是非独占模式下的调色板动画的例子:
LPDIRECTDRAW lpDD; // Already initialized
PALETTEENTRY pPaletteEntry[256]; // Already initialized
LPDIRECTDRAWPALETTE lpDDPal; // Already initialized
int index;
HRESULT ddrval;
PALETTEENTRY temp;
// Animate some entries. Cycle the first 16 available entries.
// They were already animated.
temp = pPaletteEntry[10];
for (index = 10; index < 25; index ++)
{
pPaletteEntry[index] = pPaletteEntry[index+1];
}
pPaletteEntry[25] = temp;
// Set the values. Do not pass a pointer to the entire palette entry
//structrue, but only to the changed entries.
ddrval = lpDDPal->SetEntries(
0, // Flags must be zero
10, // First entry
16, // Number of entries
& (pPaletteEntry[10])); // Where to get the data
3、多显示器系统
Windows 98(Memphis)和Windows NT支持单一系统上的多个显示设备和显示器。多显示器系统(通常称为 “MultiMon”)使得操作系统能够使用两个或两个以上的显示设备和显示器来创建单一的逻辑桌面。例如,在有两台显示器的MultiMon系统中,用户既能够在其中一个显示器上显示应用程序,也能够将窗口从一台显示器拖放到另一台显示器上。而DirectDraw支持这一体系结构。一个DirectDraw应用程序能够列出硬件设备,选择一种设备,然后利用该设备的GUID为该设备创建一个DirectDraw对象。这一技术可以保证不管是在MultiMon
系统还是在单一显示器系统都能获得最佳的性能表现。
当前的活动显示设备被认为是“缺省设备”,或者是“空设备”(Null Device,因为当前活动的显示设备在列出来时是以NULL作为它的GUID的)。已有的许多应用程序都可以为“空设备”创建DirectDraw对象,假定该设备是硬件加速的。但在MultiMon系统中,“空设备”并不总是硬件加速的,它依赖于当时采用的协作等级。
在全屏独占模式下,“空设备”是硬件加速的,但系统并不知道其它已安装的设备。这就意味着,全屏独占模式的应用程序运行在MultiMon系统上的速度同运行在其它系统上的速度一样快,但不能使用内嵌的对跨越显示设备的图象操作的支持。需要使用多个设备的全屏独占模式的应用程序能为每一个要使用的设备创建一个DirectDraw对象。注意,为指定的设备创建DirectDraw对象,你必须提供该对象的GUID。调用DirectDrawEnumerate方法时可以列出每一个设备的GUID。
当设定了标准协作等级后,“空设备”就不具有硬件加速能力,实际上,它只是将两个物理设备的资源结合在一起来仿真一个逻辑设备。
另一方面,“空设备”自动具有跨越显示器的图形操作能力。所以,当第二个显示器的逻辑位置处于主显示器的左边时,负的坐标值就是合法的。
如果应用程序在设定了标准协作等级后要求实现硬件加速功能,它就必须使用指定设备的GUID创建一个单一的DirectDraw对象。如果不使用“空设备”,就不能获得跨越显示设备的图象操作能力。也就是说,跨越主表面边界的位块传输操作将会被剪切(若使用了剪切板)或失败(返回DDERR_INVALIDRECT)。
不管在哪种系统下,都应该在获取对象的能力和查询其它的接口之前创建DirectDraw对象之后立即设置协作等级,还要避免在MultiMon系统中多次设置协作等级。如果需要从全屏模式切换到标准模式,最好创建新的DirectDraw对象,而不要继续使用老的DirectDraw对象。
4、视频端口(Video Ports)
DirectDraw视频端口扩展是一个低级编程接口,它的目的并不是用于目前的系统多媒体编程。视频端口接口的对象是视频软件公司,如开发DirectShow之类的公司。想要在自己的软件中使用视频技术的程序员也可以使用视频端口扩展。不过,在很多软件中提供的高级编程接口就足以满足需要了。
DirectDrawVideoPort对象表征了某些计算机系统中安装了的视频端口硬件。一般来说,视频端口对象控制了视频端口硬件怎样将从视频解码器接受到的视频信号直接应用到帧缓冲区。根据需要,可以创建多个DirectDrawVideoPort对象来控制多个视频通道。因为每个通道都可以分别列出和配置,所以,视频硬件不需要对每个通道做完全相同的处理。
视频端口硬件能够直接在帧缓冲区中对表面进行存取,而绕过CPU和PCI总线。直接对帧缓冲区的存取使得能有效地播放活动视频和录制视频而不必装载入CPU中。一旦存在于某个表面中,图象就能在屏幕上如覆盖一样的显示,它可用于 Direct3D纹理、CPU的截获存取和其它的处理。
在配备了视频端口设备的计算机中,视频流中的数据能够从视频源通过视频解码器和视频端口直接流向帧缓冲区。这些部件通常和显示适配器连接在一起,也可以作为单独的硬件互相连接在一起。下图给出了数据流的处理过程:
视频源是什么呢?在视频端口技术范围内,严格来说,视频源就是一个硬件视频输入(Video Input)设备,如Zoom视频端口、MPEG编码器等。这些硬件源将信号以不同的格式(包括NTSC、PAL和SECAM)通过连到视频解码器的物理连接发布出去。视频解码器(Viceo Decoder)是另一个硬件部件。它的工作是翻译视频源提供的信息并将其用遵循的连接格式发送到视频端口。解码器保持同视频端口的物理连接,它还负责将视频数据、时针信息和同步信息送到视频端口。
视频端口(Video-Port)也是一组硬件。它存在于显示适配器的VGA芯片上,具有直接访问帧缓冲区的能力。它处理来自于解码器的信息并放置于帧缓冲区中以备显示。在对数据的处理过程中,视频端口能操作图象数据以提供比例变化、拉伸压缩、颜色控制和剪切等服务。
帧缓冲区(Frame Buffer)接受视频端口提供的视频数据,然后应用程序就可用编程的方法处理这些图象数据,将其位块传输到其它的位置或使用覆盖将其显示在屏幕上。
DirectDraw视频端口扩展的实质是包含了DirectDrawVideoPort对象,可通过IDDVideoPortContainer和IDirectDrawVideoPort接口利用视频端口技术提供视频服务。DirectDrawVideoPort对象并不能控制视频解码器,只能提供自己的服务。DirectDraw也不能控制视频源,因为视频源已经超出了视频端口的范围。DirectDrawVideoPort对象只是表征视频端口本身。它用接口方法设定的参数将输入的信号和图象数据送到帧缓冲区执行翻转或其它的处理。IDDVideoPortContainer接口(可通过IDirectDraw2::QueryInterface获取)提供了查询硬件创建视频端口对象的能力。你可以用IDDVideoPortContainer::CreateVideoPort方法创建一个视频端口对象,该对象通过IDirectDrawVideoPort接口显示其功能。
利用这些接口,你能够检查视频端口的能力,分配覆盖表面以接收图象数据、开始和停止视频播放、设置硬件参数处理图象数据使产生剪切、颜色转换、比例缩放和拉伸压缩效果。DirectDraw视频端口扩展支持同一台计算机上的多个视频端口,允许创建多个视频端口对象。
5、获取翻转和位块传输状态
我们已经知道,调用IDirectDrawSurface3::Flip方法之后,主表面和后台缓冲区就会互相交换。但交换的动作并不是立即就会发生。例如,若前面的一个翻转任务还未完成时,该方法就返回DDERR_WASSTILLDRAWING,DirectDraw继续对IDirectDrawSurface3::Flip进行调用直到返回DD_OK为止。如果应用程序在调用IDirectDrawSurface3::Flip方法返回DD_OK之前一直处于等待状态,那么该程序的效率就会非常低。取而代之的是,你可以创建一个函数在后台缓冲区调用
IDirectDrawSurface3::GetFlipStatus方法检测前面的翻转工作是否完成。如果未完成,返回的还是DDERR_WASSTILLDRAWING,应用程序就可以在下一次检查该状态之间的一段时间内执行其它的任务。下面是这一思想的例子:
while(lpDDSBack->GetFlipStatus(DDGFS_ISFLIPDONE) == DDERR_WASSTILLDRAWING);
// Waiting for the previous flip to finish. The application can
// perform another task here.
ddrval = lpDDSPrimary->Flip(NULL, 0);
你也可以用同样的方法调用IDirectDrawSurface3::GetBltStatus来检测一次位块传输是否完成。因为IDirectDrawSurface3::GetFlipStatus和IDirectDrawSurface3::GetBltStatus能够立即返回应用程序目前所处的状态,所以可以在程序中周期地使用它们而不至于太影响程序的运行速度。
执行颜色填充的工作需要调用IDirectDrawSurface3::Blt方法。例如,若程序中最常显示的颜色是兰色,你就可以以兰色调用 IDirectDrawSurface3::Blt方法,利用DDBLT_COLORFILL标志首先填充表面,然后再将其它的物体写到上面。该方法允许你非常快速地填充最常用的颜色,然后只需要向表面写很少数量的颜色。下面是执行颜色填充的例子:
DDBLTFX ddbltfx;
ddbltfx.dwSize = sizeof(ddbltfx);
ddbltfx.dwFillColor = 0;
ddrval = lpDDSPrimary->Blt(
NULL, // Destination
NULL, NULL, // Source rectangle
DDBLT_COLORFILL, &ddbltfx);
switch(ddrval)
{
case DDERR_WASSTILLDRAWING:
.
.
.
case DDERR_SURFACELOST:
.
.
.
case DD_OK:
.
.
.
default:
}
6、检测显示硬件的能力
DirectDraw使用软件仿真来执行那些不被用户的硬件支持的DirectDraw功能。为了提高DirectDraw应用程序的执行速度,应该在创建DirectDraw对象后马上检测显示硬件的能力,然后再尽可能地利用硬件提供的特性构件程序。你可以利用IDirectDraw2::GetCaps方法检测硬件的性能。并不是所有的硬件特性都可以用软件来仿真。如果你想使用一种仅仅被一些硬件支持的特性,最好还是作好系统可能不支持这种硬件的准备,也就是要提供如果硬件不支持这种硬件的解决方法。
7、在显示内存中存储位图
通常,从显示内存到显示内存之间的位块传输比从系统内存到显示内存之间的位块传输效率要高。因此,最好在显示内存中尽可能多地存储将要用到的精灵位图。大多数的显示适配器都包含了足够的额外内存以存储不止一个主表面和后台缓冲区。你可以利用 DDCAPS结构中的dwVidMemTotal和dwVidMemFree成员来检测显示内存中还有多少内存可用来存储位图。包含在DirectX SDK中的DirectX Viewer范例程序给出了这一过程。
8、三缓冲(Triple Buffering)
在某些情况下,显示适配器可能有比较多的显示内存,这就有可能使用三缓冲来提高显示速度。三缓冲使用一个主表面和两个后台缓冲区。下面是初始化一个三缓冲的例子:
// The lpDDSPrimary, lpDDSMiddle, and lpDDSBack are globally
// declared, uninitialized LPDIRECTDRAWSURFACE variables.
DDSURFACEDESC ddsd;
ZeroMemory (&ddsd, sizeof(ddsd));
// Create the primary surface with two back buffers.
ddsd.dwSize = sizeof(ddsd);
ddsd.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT;
ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE |
DDSCAPS_FLIP | DDSCAPS_COMPLEX;
ddsd.dwBackBufferCount = 2;
ddrval = lpDD->CreateSurface(&ddsd, &lpDDSPrimary, NULL);
// If we successfully created the flipping chain,
// retrieve pointers to the surfaces we need for
// flipping and blitting.
if(ddrval == DD_OK)
{
// Get the surface directly attached to the primary (the back buffer).
ddsd.ddsCaps.dwCaps = DDSCAPS_BACKBUFFER;
ddrval = lpDDSPrimary->GetAttachedSurface(&ddsd.ddsCaps,
&lpDDSMiddle);
if(ddrval != DD_OK) ;
// Display an error message here.
}
你不需要保留三缓冲翻转链中所有表面的轨迹,只需要保留主表面和后台缓冲表面的指针。主表面指针用来翻转处于翻转链中的主表面,后台缓冲的指针则是用于位块传输。三缓冲允许应用程序继续位块传输到后台缓冲区,只要后台缓冲的上一次位块传输已经完成,即使翻转未完成也没有关系。执行翻转并不是一个同步事件,一次翻转可能比另一次翻转的时间还要长。因此,如果只使用了一个后台缓冲区,当
IDirectDrawSurface3::Flip方法返回DD_OK之前,程序可能会耗用不少时间处于等待状态。
9、DirectDraw应用程序和窗口风格
如果应用程序在窗口模式下使用DirectDraw,就可以使用任何的风格建立窗口。全屏独占模式的出现不能用 WS_EX_TOOLWINDOW风格建立窗口,它应该用WS_EX_TOPMOST扩展窗口风格和WS_VISIBLE窗口风格确保图象正确显示。这两种风格可将应用程序保持在窗口Z方向的最前端,避免GDI向主表面画图。下面的例子显示了如何使独占全屏模式的应用程序安全运行:
// Register the window class, display the window, and init all DirectX and graphic objects.
BOOL WINAPI InitApp(INT nWinMode)
{
WNDCLASSEX wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.hInstance = g_hinst;
wcex.lpszClassName = g_szWinName;
wcex.lpfnWndProc = WndProc;
wcex.style = CS_VREDRAW|CS_HREDRAW|CS_DBLCLKS;
wcex.hIcon = LoadIcon (NULL, IDI_APPLICATION);
wcex.hIconSm = LoadIcon (NULL, IDI_WINLOGO);
wcex.hCursor = LoadCursor (NULL, IDC_ARROW);
wcex.lpszMenuName = MAKEINTRESOURCE(IDR_APPMENU);
wcex.cbClsExtra = 0 ;
wcex.cbWndExtra = 0 ;
wcex.hbrBackground = GetStockObject (NULL_BRUSH);
RegisterClassEx(&wcex);
g_hwndMain = CreateWindowEx(
WS_EX_TOPMOST,
g_szWinName,
g_szWinCaption,
WS_VISIBLE|WS_POPUP,
0,0,CX_SCREEN,CY_SCREEN,
NULL,
NULL,
g_hinst,
NULL);
if(!g_hwndMain)
return(FALSE);
SetFocus(g_hwndMain);
ShowWindow(g_hwndMain, nWinMode);
UpdateWindow(g_hwndMain);
return TRUE;
}