☰
VB6 接入 DX9 实战:虚表调用、精灵渲染与设备丢失恢复
2026/10/31 14:14:08 网站建设 项目流程

你有没有过这种经历:手里攒了一堆 VB6 写的老代码,跑得挺好,就是画面卡得像幻灯片,想加点粒子、多层背景、几百个精灵同屏,GDI 那套BitBlt就开始喘。这时候你会发现,网上能搜到的 VB6 游戏资料,八成还停留在 DX7 的DirectDraw,剩下两成在教你怎么引用dx8vb.dll类型库。可你真正想用的是 DirectX9 的这套东西——IDirect3DDevice9、纹理、Present、设备丢失恢复,还有那个在国内 VB 圈子里流传挺广的 2D 引擎(下面按它的英文名缩写,统一叫 CW2D)。问题就来了:VB6 根本没有 DX9 的类型库,d3d9.dll是 C 的导出接口,一堆IDirect3D9*你连个变量类型都声明不出来。

这篇文章要解决的就是这件事。我把 VB6 摸到 DX9 接口的两条路、虚表调用的具体写法、设备创建的参数取舍、2D 精灵的渲染管线、设备丢失的完整恢复流程,还有 CW2D 这类引擎在 VB6 里怎么分层,全部拆开讲一遍,附带几个我踩过的、文档里绝对不写的坑。适合两类人看:手上还有 VB6 工程、想给它换套图形层的老手;以及想搞明白“一个没有现代类型系统的语言怎么调用 COM 风格接口”的爱好者。不需要你懂 C++,但至少要会声明 API、会用CopyMemory、知道Long是 32 位。

1. VB6 撞上 DX9:这套组合到底卡在哪

1.1 三条画图路线的真实取舍

VB6 想往屏幕上画东西,实际上就三条路,每条路都有明确的代价,选错了后面全是返工。第一条是 GDI 路线,PictureBox加BitBlt、StretchBlt,甚至用CreateDIBSection自己管位图。这条路门槛最低,代码全是现成 API,缺点是所有合成都在 CPU 上跑,没有 alpha 混合硬件加速,一帧要贴两百个带透明通道的精灵,主频就直接吃满了。而且 GDI 的绘制是有窗口消息驱动的,WM_PAINT一来你才画,帧率天然不稳,做动作游戏基本没法看。

第二条是dx8vb.dll路线,微软当年专门给 VB6 出了一套 DirectX8 类型库,引用之后可以Dim d3d As Direct3D8、Dim dev As Direct3DDevice8,甚至有个D3DXSprite的高层封装,代码写起来像 VB 该有的样子。这条路的问题有两个:一是它只到 DX8,DX8 和 DX9 的顶点声明、着色器模型差别不小,很多东西你在 DX9 教程里看到的方法直接搬不过来;二是dx8vb.dll依赖运行库,用户机器上没装就报“找不到工程或库”,分发的时候得单独处理,很烦。

第三条就是标题里说的 DX9 原生导出路线:直接Declare Function Direct3DCreate9 Lib "d3d9.dll",拿回一个 32 位指针,然后自己在 VB6 里模拟 COM 调用。这条路最硬,也最干净——目标机器只要有d3d9.dll(WinXP SP2 之后是系统自带的)就能跑,不依赖任何 VB 专用的 DX 运行库。代价是你得自己处理接口指针、虚表偏移、结构体对齐,一个字段填错就是0x8876086C这种不明所以的 HRESULT。

CW2D 这类引擎之所以值得研究,是因为它把这第三条路封装成了一套 VB 开发者能接受的写法:对外是普通的类模块和函数,对内是虚表调用。我个人的判断是,如果只是做个推箱子、扫雷这种量级的游戏,GDI 完全够,别折腾;如果目标里有同屏几百精灵、滚动地图、多层视差、即时战斗,那 DX9 这条路值得花两三天啃下来,因为啃完之后你拿到的是一套真正的硬件加速渲染层,而且这套知识在别的老语言上通用。

1.2 引擎封装的边界:哪些脏活该被挡掉

在动手写第一行代码之前,得先想清楚哪些东西该被引擎吃掉,哪些该留给业务代码。我见过不少 VB6 的“引擎”,实际上是给 API 套了层壳,游戏逻辑里到处是SetRenderState和DrawPrimitiveUP,改一个效果要翻十个文件。这种封装等于没封。合理的边界应该是这样的:设备和窗口的生命周期、设备丢失检测与恢复、纹理的加载与缓存、坐标系与缩放、精灵的排序与批次,这些属于引擎的事;具体某个角色跑到哪、放什么技能、掉多少血,属于业务逻辑,引擎一个字都不该管。

这里有个很实际的判断标准:如果你的业务代码里出现了 HRESULT、出现了VarPtr、出现了虚表索引数字,说明封装漏了。CW2D 在这方面做得比较克制,它暴露给上层的接口基本都是“加载一张图”、“创建一个精灵”、“播放动画第几帧”、“这一帧移动多少像素”这种动词,参数是像素和速度,不是字节和指针。这种设计在 VB6 里特别重要,因为 VB6 没有强类型检查帮你兜底,指针一旦漏到业务层,出错的时候堆栈根本看不懂,只会给你一个“应用程序发生异常”然后整个 IDE 崩掉,连断点都来不及打。

另一个值得提前决定的是2D 的坐标体系。DX9 的默认投影是左手系的 3D 空间,做 2D 有两条路:一条是用D3DFVF_XYZRHW,顶点直接给屏幕坐标加RHW = 1.0,完全绕过矩阵变换;另一条是设一个正交投影矩阵,顶点用D3DFVF_XYZ。前者的好处是快、简单、不用管矩阵,坏处是没法做整体的缩放和摄像机平移;后者灵活,但每次改视口都得重建矩阵,VB6 里手动拼 4x4 矩阵很容易算错。CW2D 用的是第一种,因为 2D 小游戏大部分只需要“贴图到指定像素位置”,需要摄像机的时候在业务层做坐标偏移就行,比维护矩阵省事得多。

2. 从函数导出到虚表调用:VB6 怎么摸到 D3D9 接口

2.1 Direct3DCreate9 的声明与返回指针的处理

d3d9.dll只导出一个创建函数,签名在 C 里是IDirect3D9* Direct3DCreate9(UINT SDKVersion),注意它用的是__stdcall,参数只有一个,返回一个指针。VB6 里声明起来非常直接:

Private Declare Function Direct3DCreate9 Lib "d3d9.dll" _ (ByVal SDKVersion As Long) As Long Private Const D3D_SDK_VERSION As Long = 32

SDKVersion这个参数它不是版本号,而是微软用来防止你拿老头文件编译出来的程序去调新运行库的一个校验值。DX9 一路走到 9.0c,这个值固定是 32。填错了Direct3DCreate9直接返回 0,不会有任何提示,这是新手最容易卡住的第一个坎——你以为 DLL 没加载,其实是你参数给错了。

返回的Long就是IDirect3D9接口指针。这里要建立一个概念:COM 接口指针指向的是一块内存,那块内存的头 4 个字节又是一个指针,指向虚函数表(vTable),vTable 里按顺序摆着一排函数地址。所以你拿到pD3D之后,第一步不是调它,而是把 vTable 的地址读出来:

Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _ (ByRef Destination As Any, ByRef Source As Any, ByVal Length As Long) Dim pD3D As Long Dim vTable As Long pD3D = Direct3DCreate9(D3D_SDK_VERSION) If pD3D = 0 Then MsgBox "Direct3DCreate9 返回空,检查 SDKVersion 是否为 32" Exit Sub End If CopyMemory vTable, ByVal pD3D, 4

注意CopyMemory第一个参数用了ByRef Destination As Any,第二个参数用ByVal pD3D——ByVal加在指针参数上,意思就是“把 pD3D 这个数值当作地址”,这才是取那块内存内容的正解。写成ByRef pD3D拿到的是变量自身的地址,读出来是垃圾值,后面跟着一串崩溃。

IDirect3D9的方法顺序我在下面这张表里列出来了,索引从 0 开始算,0 到 2 是IUnknown的三件套(QueryInterface、AddRef、Release),别漏掉,很多手抄的索引表就是从这里开始算错的:

索引方法用途
0-2IUnknown 三件套引用计数与接口查询
4GetAdapterCount取显卡适配器数量
8GetAdapterDisplayMode取当前桌面显示模式
14GetDeviceCaps取设备能力,判断硬件顶点
16CreateDevice创建设备,最关键的一个

2.2 DispCallFunc:一个能传任意参数的通用调用器

拿到函数地址只是第一步,怎么“调用”它才是难点。C 的__stdcall调用约定要求参数从右往左压栈、被调用方清理栈,VB6 自己生成不了这种调用序列。老一代 VB 玩家有两条路:一条是用机器码拼一个 thunk(在堆上分配可执行内存,写入push指令和call,再用CallWindowProc跳过去),性能好但写起来像在写汇编,调试基本靠猜;另一条是用oleaut32.dll里的DispCallFunc,它本来就是给自动化调用用的,支持任意数量参数和__stdcall,虽然有点慢(每次调用要走一次类型封送),但对于一帧几十次接口调用来说完全无所谓。

先声明:

Private Declare Function DispCallFunc Lib "oleaut32.dll" ( _ ByVal pvInstance As Long, ByVal oVft As Long, ByVal cc As Long, _ ByVal vtReturn As Integer, ByVal cActuals As Long, _ ByVal prgvt As Long, ByVal prgpvarg As Long, _ ByRef pvargResult As Variant) As Long Private Const CC_STDCALL As Long = 4 Private Const VT_I4 As Integer = 3 Private Const VT_VARIANT As Integer = 12 Private Const VT_BYREF As Integer = &H4000

pvInstance传对象指针本身,oVft传相对对象指针的字节偏移(不是相对 vTable 的),所以第 N 个方法就传N * 4。cc是调用约定,__stdcall对应 4。prgvt是一个Integer数组的地址,每项描述对应参数的类型;prgpvarg是一个Long数组的地址,每项是一个指向参数值的指针。最后一个pvargResult用来接返回值。

封装成一个通用函数的写法:

Public Function CallMethod(ByVal pObj As Long, ByVal Slot As Long, _ ByRef vArgs() As Variant, _ ByVal vtRet As Integer) As Long Dim cnt As Long, i As Long Dim vt() As Integer Dim pv() As Long Dim vRet As Variant cnt = UBound(vArgs) - LBound(vArgs) + 1 If cnt > 0 Then ReDim vt(0 To cnt - 1) ReDim pv(0 To cnt - 1) For i = 0 To cnt - 1 vt(i) = VT_VARIANT Or VT_BYREF pv(i) = VarPtr(vArgs(LBound(vArgs) + i)) Next i CallMethod = DispCallFunc(pObj, Slot * 4, CC_STDCALL, vtRet, cnt, _ VarPtr(vt(0)), VarPtr(pv(0)), vRet) Else CallMethod = DispCallFunc(pObj, Slot * 4, CC_STDCALL, vtRet, 0, _ 0, 0, vRet) End If End Function

有了这个,CreateDevice就变成了填参数的事:

Dim vArgs(0 To 5) As Variant Dim pDevice As Long Dim hr As Long pDevice = 0 vArgs(0) = 0 ' Adapter vArgs(1) = 1 ' D3DDEVTYPE_HAL vArgs(2) = Me.hWnd ' 焦点窗口 vArgs(3) = &H2 Or &H20 ' FPU_PRESERVE Or SW_VERTEX vArgs(4) = VarPtr(presentParams) ' D3DPRESENT_PARAMETERS 指针 vArgs(5) = VarPtr(pDevice) ' 输出设备指针 hr = CallMethod(pD3D, 16, vArgs, VT_I4)

这里vArgs(4)和vArgs(5)塞进去的是VarPtr转成的指针值,作为Long存进Variant,DispCallFunc按 4 字节压栈,正好对应 C 里的两个指针参数。这套写法看起来怪,但实测很稳,CW2D 里几乎所有 D3D 接口调用都是这么走的。

注意:DispCallFunc对参数个数有上限(实际受栈大小和类型数组长度约束,一般几十个没问题,但D3DXCreateTextureFromFileExW那种 14 个参数的函数,参数多到容易在类型数组上搞错位,建议分两步——先用D3DXGetImageInfoFromFileW拿尺寸,再调创建函数,把中间那几个可省略的参数固定下来)。

2.3 索引表怎么对齐,别靠手抄

我上面给的CreateDevice索引是 16,这个数字是对的,但我要强调一句:别把网上的索引表直接抄进代码。d3d9.h里的接口方法声明顺序决定了索引,而不同版本的头文件、不同的平台 SDK,中间某几个方法的顺序是有可能变化的(尤其是IDirect3DDevice9Ex那套扩展)。手抄表最典型的翻车场景是:前面几十个方法你抄对了,到BeginScene之后开始错位一位,结果EndScene调到了Clear,画面就变成一帧黑一帧花。

稳妥的做法是写个小脚本,从头文件里把指定接口的方法名按顺序抓出来,生成一份 VB 常量表:

Public Const M_PRESENT As Long = 17 Public Const M_RESET As Long = 16 Public Const M_BEGINSCENE As Long = 41 Public Const M_ENDSCENE As Long = 42 Public Const M_CLEAR As Long = 43 Public Const M_SETRENDERSTATE As Long = 57 Public Const M_SETTEXTURE As Long = 65 Public Const M_SETFVF As Long = 87 Public Const M_DRAWPRIMITIVEUP As Long = 81

这几个里,Present、Reset、BeginScene、EndScene是我在多个工程里反复验证过的,比较可靠;DrawPrimitiveUP这类靠后一点的,我建议你在自己的环境里用一个最小用例验证一遍——建个空窗口,只调一次,看画面有没有出现三角形,出来了就说明索引对,没出来就往上往下试一位,试一次不到五分钟,比事后查崩溃快得多。

3. 把窗口和设备立起来:初始化流程的每一步

3.1 D3DPRESENT_PARAMETERS 的字段顺序与对齐陷阱

这个结构体是初始化阶段最容易出事的地方。它在 C 里长这样:

typedef struct _D3DPRESENT_PARAMETERS_ { UINT BackBufferWidth; UINT BackBufferHeight; D3DFORMAT BackBufferFormat; UINT BackBufferCount; D3DMULTISAMPLE_TYPE MultiSampleType; DWORD MultiSampleQuality; D3DSWAPEFFECT SwapEffect; HWND hDeviceWindow; BOOL Windowed; BOOL EnableAutoDepthStencil; D3DFORMAT AutoDepthStencilFormat; DWORD Flags; UINT FullScreen_RefreshRateInHz; UINT PresentationInterval; } D3DPRESENT_PARAMETERS;

十四个字段,全是 4 字节,总共 56 字节。在 VB6 里对应写:

Private Type D3DPRESENT_PARAMETERS BackBufferWidth As Long BackBufferHeight As Long BackBufferFormat As Long BackBufferCount As Long MultiSampleType As Long MultiSampleQuality As Long SwapEffect As Long hDeviceWindow As Long Windowed As Long ' 必须用 Long,不能用 Boolean EnableAutoDepthStencil As Long ' 同上 AutoDepthStencilFormat As Long Flags As Long FullScreen_RefreshRate As Long PresentationInterval As Long End Type

Windowed和EnableAutoDepthStencil这两个坑我踩过。C 里它们是BOOL,很多人顺手就写成As Boolean,然后发现后面所有字段全部错位 2 字节,CreateDevice返回0x8876086C(无效参数)。原因是VB6 的Boolean占 2 字节,而 C 的BOOL占 4 字节,结构体对齐一乱,后面全废。同理,hDeviceWindow在 VB6 里Long存窗口句柄没问题,64 位系统下句柄理论上可能超 32 位,但在 VB6 进程里hWnd本身就是 32 位值,不用担心。

字段内容的选择上有几条经验值得说。BackBufferFormat在窗口模式下填D3DFMT_UNKNOWN(值为 0)就行,驱动会自动匹配桌面格式;全屏模式下要显式指定,一般用D3DFMT_X8R8G8B8(22)或者D3DFMT_A8R8G8B8(21),前者少了 alpha 通道,速度快一点,后者方便后续做离屏合成。SwapEffect用D3DSWAPEFFECT_DISCARD(1),这是最省事的一种——驱动可以随意丢弃后台缓冲内容,不需要你自己清理,性能最好,代价是你不能指望上一帧画的东西还留在后台缓冲里。

PresentationInterval控制垂直同步,D3DPRESENT_INTERVAL_IMMEDIATE(&H80000000)是不等,D3DPRESENT_INTERVAL_DEFAULT(0)和D3DPRESENT_INTERVAL_ONE(1)都是等一帧。做 2D 小游戏我建议默认开垂直同步,画面滚动明显顺滑;如果你的逻辑帧率是固定的(比如 60 帧逻辑),垂直同步会让Present阻塞,这时候计时器要用高精度计数器而不是Timer控件,否则帧间隔会抖。

3.2 创建失败时的排查顺序

CreateDevice返回非 0 的时候,别急着改代码,按这个顺序查,能省掉一大半时间:

第一步,看返回值。0x8876086C是D3DERR_INVALIDCALL,八成是结构体某个字段填错或者指针参数没给对;0x8876086A是D3DERR_NOTAVAILABLE,通常是设备类型不支持,比如在老集成显卡上强求D3DDEVTYPE_HAL加硬件顶点;0x88760872是D3DERR_OUTOFVIDEOMEMORY,纹理太大或者后台缓冲太大。

第二步,查GetDeviceCaps。在创建之前先调一次IDirect3D9的GetDeviceCaps(索引 14),参数是适配器号、设备类型、一个D3DCAPS9结构的指针。拿到之后看DevCaps字段有没有D3DDEVCAPS_HWTRANSFORMANDLIGHT(&H10000)这个标志位。有,就可以用D3DCREATE_HARDWARE_VERTEXPROCESSING(&H40);没有,老老实实用D3DCREATE_SOFTWARE_VERTEXPROCESSING(&H20)。VB6 里因为浮点精度的问题,我强烈建议无论硬件顶点是否支持,都加上D3DCREATE_FPU_PRESERVE(&H2),原因在下一节说。

第三步,检查窗口。hDeviceWindow必须是一个真实存在、已经创建完成、并且没有被销毁的窗口句柄。在Form_Load里创建设备是最常见的做法,但有些情况下窗体还没真正显示,句柄是有的,尺寸却是 0,这时候后台缓冲会创建一个 0x0 的表面,Present的时候什么都不显示。我现在习惯在Form_Load里先Me.Show,再进初始化。

3.3 FPU 精度这条暗线,以及消息循环怎么写

D3DCREATE_FPU_PRESERVE这个标志经常被忽略,但它在 VB6 里几乎是必须的。Direct3D 内部会把 FPU 的控制字改成单精度、关闭除零异常检查,用来提速,这对 C++ 程序没什么影响,因为编译期就按单精度算了。可 VB6 的Double是实打实的双精度,运算过程中依赖 FPU 的双精度模式,一旦被 D3D 改成单精度,你会发现游戏里的浮点运算结果开始飘——坐标偶尔差个零点几,速度累积几百帧之后角色跑偏了几十像素,这种 bug 极难查,因为单步调试的时候又正常了。

消息循环这块,VB6 的窗体本身有消息泵,你不能像 C 那样写while(GetMessage(...)),否则整个 IDE 会卡死。标准做法是用一个Timer控件或者自己开一个线程驱动帧循环。Timer的精度是 55 毫秒左右,对 60 帧来说完全不够。CW2D 用的是“主循环挂在Timer上,但内部用高精度计数器算实际耗时”的方案:

Private Declare Function QueryPerformanceCounter Lib "kernel32" _ (ByRef lpPerformanceCount As Currency) As Long Private Declare Function QueryPerformanceFrequency Lib "kernel32" _ (ByRef lpFrequency As Currency) As Long

这里用Currency接LARGE_INTEGER是个老 VB 技巧。Currency在内存里是 8 字节的定点数(小数点固定 4 位),把它当成一个 64 位整数容器用,只要不做乘除,读出来的原始位模式就是完整的 64 位计数值。用Double也能接,但Double只有 53 位有效位,计数器跑久了会丢低位,Currency是真正的 64 位存储,更保险。

每帧开头记一次,帧末尾再记一次,差值除以频率就是这一帧的秒数,拿去乘速度就是位移量。这个“按时间算位移”而不是“按帧算位移”的做法,是保证不同机器上跑得一样快的关键,也顺便解决了垂直同步带来的帧间隔抖动。

4. 2D 渲染的关键动作

4.1 纹理加载与颜色键的落地方式

DX9 里加载一张图,最省事的是D3DXCreateTextureFromFileExW,它在d3dx9_43.dll里导出,声明如下:

Private Declare Function D3DXCreateTextureFromFileExW Lib "d3dx9_43.dll" ( _ ByVal pDevice As Long, ByVal pSrcFile As Long, _ ByVal Width As Long, ByVal Height As Long, ByVal MipLevels As Long, _ ByVal Usage As Long, ByVal Format As Long, ByVal Pool As Long, _ ByVal Filter As Long, ByVal MipFilter As Long, ByVal ColorKey As Long, _ ByVal pSrcInfo As Long, ByVal pPalette As Long, _ ByRef ppTexture As Long) As Long

第一个要说的坑是DLL 名字。d3dx9_43.dll不是系统自带的,它属于 DirectX 最终用户运行时,装了才有。如果你的目标用户是干净的 Win7/Win10,很可能只有一个d3dx9_24.dll或者干脆没有。所以要么在安装包里带上这个 DLL(微软允许随程序分发),要么干脆不用 D3DX 的加载函数,改成自己读文件、用CreateTexture加LockRect手动填像素。后者代码量大一些,但依赖最少,CW2D 里两种都留了口子,发布版建议用后者。

第二个坑是字符串。VB6 的String内部就是 UTF-16,StrPtr(sPath)拿到的直接是宽字符指针,所以调...ExW这种宽字符版本完全没问题,不需要StrConv转来转去。很多人不知道这一点,绕了一圈用 ANSI 版本,结果中文路径全变问号。

颜色键的处理有两种思路。一种是给ColorKey参数传一个D3DCOLOR值,比如洋红&HFFFF00FF,D3DX 在加载时会把所有这个颜色的像素 alpha 置 0;另一种是从一开始就用带 alpha 通道的 PNG,让ColorKey传 0,alpha 直接来自图片本身。第二种更好,边缘不会出现锯齿硬边,但要注意 PNG 的预乘 alpha 问题——如果图片是“直通 alpha”(straight alpha),DX9 默认按“预乘 alpha”(premultiplied)处理,边缘会出现一圈黑边。解决办法是在导出图片时做预乘,或者在渲染时把混合模式从D3DBLEND_SRCALPHA换成D3DBLEND_ONE,同时把纹理阶段的颜色运算调成D3DTOP_MODULATE。这个细节我在一个像素风项目里卡了整整两天,最后发现是美工导出的 PNG 没预乘。

纹理池的选择上,2D 游戏基本都用D3DPOOL_MANAGED(1),交给 D3D 帮你管显存,设备丢失之后不用重新加载,引擎里省掉一大块恢复逻辑。代价是每次Lock会慢一点,但 2D 游戏很少每帧锁纹理,无所谓。如果你的图特别多、显存吃紧,可以分池:常用的放 managed,大背景放 default,配合自己写的恢复回调。

4.2 用 DrawPrimitiveUP 画精灵:顶点、FVF 和状态设置

很多人第一反应是用ID3DXSprite,它确实方便,Begin、Draw、End三步搞定。但它在 VB6 里有两个麻烦:一是ID3DXSprite的方法索引不像IDirect3DDevice9那么好查,d3dx9spr.h里的声明顺序得仔细对;二是它内部也是拼顶点调DrawPrimitiveUP,多一层封装在 VB6 这种调用开销大的环境里不划算。所以我更推荐直接自己拼顶点,代码不长,逻辑还全在手里。

顶点结构用带RHW的变换后格式,位置直接是屏幕像素坐标:

Private Type TLVERTEX X As Single Y As Single Z As Single RHW As Single Color As Long U As Single V As Single End Type

7 个字段,28 字节,Z一般填 0,RHW填 1,Color填&HFFFFFFFF(白色,不改变纹理颜色),U/V是纹理坐标,范围 0 到 1。FVF 标志就是D3DFVF_XYZRHW Or D3DFVF_DIFFUSE Or D3DFVF_TEX1,数值上等于&H4 Or &H40 Or &H100,也就是&H144。

画一个精灵就是四个顶点、两个三角形:

Dim v(0 To 3) As TLVERTEX Dim x As Single, y As Single Dim w As Single, h As Single ' 左上 v(0).X = x: v(0).Y = y: v(0).U = 0: v(0).V = 0 ' 右上 v(1).X = x + w: v(1).Y = y: v(1).U = 1: v(1).V = 0 ' 左下 v(2).X = x: v(2).Y = y + h: v(2).U = 0: v(2).V = 1 ' 右下 v(3).X = x + w: v(3).Y = y + h: v(3).U = 1: v(3).V = 1 For i = 0 To 3 v(i).Z = 0 v(i).RHW = 1 v(i).Color = &HFFFFFFFF Next i

顶点顺序要跟D3DPT_TRIANGLELIST的约定一致——每三个顶点一个三角形。上面的顺序是 0-1-2 和 1-3-2,两个三角形拼成矩形。顺序搞反了会触发背面剔除,画出来什么都没有,这时候把D3DRS_CULLMODE(索引 22)设成D3DCULL_NONE(1)就能验证是不是这个问题。

渲染状态这块,2D 精灵需要的最少配置是:

状态常量值设置
D3DRS_ALPHABLENDENABLE271 开启混合
D3DRS_SRCBLEND195(SRCALPHA)
D3DRS_DESTBLEND206(INVSRCALPHA)
D3DRS_LIGHTING1370 关闭光照
D3DRS_ZENABLE70 关闭深度
D3DRS_CULLMODE221 关闭剔除

这些状态每帧只需要设一次,在BeginScene之后、开始画之前统一设好,不要在每画一个精灵的时候重复调。SetRenderState走一次DispCallFunc的开销不算小,一帧贴五百个精灵就是五百次多余调用,白白丢掉几毫秒。同样道理,纹理切换要排好序——同一个纹理的精灵连着画,减少SetTexture次数,这就是最朴素的批次优化。

一次完整的绘制调用:

CallMethod pDevice, M_SETFVF, Arr(&H144), VT_I4 CallMethod pDevice, M_DRAWPRIMITIVEUP, Arr(4, 2, VarPtr(v(0)), 28), VT_I4

参数依次是图元类型(D3DPT_TRIANGLELIST= 4)、图元数量(2 个三角形)、顶点数组指针、每个顶点字节数。这一句画一个精灵。要画一批,就把 N 个精灵的顶点拼进一个大数组,然后一次调用传2 * N个三角形,性能能提升好几倍,因为省掉了 N-1 次接口调用和状态切换。CW2D 里就是这么做的:业务层调DrawSprite,引擎内部攒够一批或者遇到纹理切换才真正 flush 一次。

4.3 设备丢失:不是异常,是常态

这是 VB6 做 DX9 最容易翻车的地方,因为表现是“程序跑着跑着突然黑屏,然后就没反应了”,而 VB6 的 IDE 里你可能根本看不到异常。设备丢失在 DX9 里是正常现象:用户切了全屏、按了Ctrl+Alt+Del、笔记本合盖、驱动更新,都会让 D3D 设备进入D3DERR_DEVICELOST状态。这时候所有渲染相关的调用都会失败,但你的程序不会崩,它会安安静静地什么都不画,看起来就像卡死。

正确的处理方式是在每帧的Present之后检查返回值:

hr = CallMethod(pDevice, M_PRESENT, Arr(0, 0, 0, 0), VT_I4) Const D3

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询