Direct3D12 的接口设计 bug
昨天被 D3D12 的一个 bug 坑了一晚上,这个问题很值得一写。
最初是发现 LUID ID3D12Device::GetAdapterLuid() 这个函数有问题。我用 mingw64 gcc 编译后的程序,只要调用了一个 api ,d3d12device 设备对象的虚函数表就被破坏掉了。下一次对这个设备的任何 api 调用都会 crash 掉。
由于这个函数的实现在 d3d12.dll 中,是没有源码的,所以只能用 gdb 调试了一下。发现了一个问题:这个 api 的返回值是 LUID ,它是一个结构体。C/C++ 函数返回结构体是没有统一的调用规范的,按道理 COM 的实现应该避免设计这种 API 。
按 COM 的规定,所有 API 必须返回 HRESULT ,只有少量例外可以返回 ULONG 整数。其实之前的 D3D 版本在设计类似 API 的时候都符合了这个约定,例如 D3D9 就有一个类似的 API :
HRESULT GetAdapterLUID( [in] UINT Adapter, [in] LUID *pLUID )
我猜想,微软的新同学在重新设计 D3D12 的时候已经忘记了 COM 规范,也不清楚这个世界上还有 VC 以外的 C++ 编译器的存在。实现 D3D12.DLL 的时候,按 VC 的调用规范,当 API 返回一个结构体的时候,是把调用方在栈上预留出的返回结构的地址作为一个参数传入的。
所以,这个 API 其实本质上是这样的:
void ID3D12Device::GetAdapterLuid(LUID *)
而 gcc 呢,它会根据结构体的实际大小来优化调用。如果结构体小于等于 64bit ,就通过寄存器返回结果,而不会传入堆栈地址。在 gcc 上,这个 API 就只有一个参数,就是 this 指针了。按 64 位系统的函数调用规范,第一个和第二个参数分别通过 rcx 和 rdx 传递。用 gcc 生成的调用代码,调用方认为没有第二个参数,rdx 是无意义的。这里恰巧 rdx 也保存了 this 。而被调用方,也就是 d3d12.dll 的实现,认为第二个参数是有意义的,是返回结构的地址,结果就把返回数据写入了 *this 中,这里恰好是这个对象的虚表之所在,程序就这么挂了。
那么是否可以指定 gcc 不要对小结构体返回优化呢?
gcc 提供了参数 -fpcc-struct-return 。不过,即使设置了这个参数,gcc 的 ABI 依旧和 vc 的不一致。因为对于 gcc 来说,返回值的地址是第一个参数,而 vc 似乎放在了第二个,它把第一个参数留给了 this 。
在微软更新 sdk 前,怎么绕过这个问题呢?我写了一个辅助函数:
static inline LUID D3D12DeviceGetAdapterLuid(ID3D12Device *device) { typedef void (STDMETHODCALLTYPE ID3D12Device::*GetAdapterLuid_f)(LUID *); LUID ret; (device->*(GetAdapterLuid_f)(&ID3D12Device::GetAdapterLuid))(&ret); return ret; }
同样存在问题的 API 还有 ID3D12DescriptorHeap 等,我还没有一一核查。
更糟糕的问题存在于 ID3D12Resource::GetDesc 这个 API ,它也返回了一个结构体 D3D12_RESOURCE_DESC
,这个 API 居然在 d3dsdk 的 d3dx12.h 中的 inline 函数 UpdateSubresources 里被调用了。
也就是说,如果你不修改 d3dsdk 的 .h 文件,几乎无法解决这个 bug :(
我一开始想到一个 trick ,实现一个 proxy 类,把这个 GetDesc 函数重定义一下:
static inline D3D12_RESOURCE_DESC ID3D12ResourceGetDesc(ID3D12Resource *res) { typedef void (STDMETHODCALLTYPE ID3D12Resource::*GetDesc_f)(D3D12_RESOURCE_DESC *); D3D12_RESOURCE_DESC ret; (res->*(GetDesc_f)(&ID3D12Resource::GetDesc))(&ret); return ret; } struct D3D12ResourceProxy : public ID3D12Resource { D3D12ResourceProxy(ID3D12Resource *p) : m_ptr(p) {} virtual HRESULT STDMETHODCALLTYPE Map(UINT Subresource, const D3D12_RANGE *pReadRange, void **ppData) { return m_ptr->Map(Subresource, pReadRange, ppData); } virtual void STDMETHODCALLTYPE Unmap(UINT Subresource, const D3D12_RANGE *pWrittenRange) { m_ptr->Unmap(Subresource, pWrittenRange); } virtual D3D12_RESOURCE_DESC STDMETHODCALLTYPE GetDesc(void) { // return struct 重新定义 return ID3D12ResourceGetDesc(m_ptr); } virtual D3D12_GPU_VIRTUAL_ADDRESS STDMETHODCALLTYPE GetGPUVirtualAddress( void) { return m_ptr->GetGPUVirtualAddress(); } virtual HRESULT STDMETHODCALLTYPE WriteToSubresource(UINT DstSubresource, const D3D12_BOX *pDstBox, const void *pSrcData, UINT SrcRowPitch, UINT SrcDepthPitch) { return m_ptr->WriteToSubresource(DstSubresource, pDstBox, pSrcData, SrcRowPitch, SrcDepthPitch); } virtual HRESULT STDMETHODCALLTYPE ReadFromSubresource(void *pDstData,UINT DstRowPitch,UINT DstDepthPitch, UINT SrcSubresource, const D3D12_BOX *pSrcBox) { return m_ptr->ReadFromSubresource(pDstData,DstRowPitch,DstDepthPitch,SrcSubresource,pSrcBox); } virtual HRESULT STDMETHODCALLTYPE GetHeapProperties(D3D12_HEAP_PROPERTIES *pHeapProperties,D3D12_HEAP_FLAGS *pHeapFlags) { return m_ptr->GetHeapProperties(pHeapProperties,pHeapFlags); } virtual HRESULT STDMETHODCALLTYPE GetDevice(REFIID riid, void **ppvDevice) { return m_ptr->GetDevice(riid, ppvDevice); } virtual HRESULT STDMETHODCALLTYPE GetPrivateData(REFGUID guid, UINT *pDataSize, void *pData) { return m_ptr->GetPrivateData(guid,pDataSize,pData); } virtual HRESULT STDMETHODCALLTYPE SetPrivateData(REFGUID guid, UINT DataSize, const void *pData) { return m_ptr->SetPrivateData(guid, DataSize, pData); } virtual HRESULT STDMETHODCALLTYPE SetPrivateDataInterface(REFGUID guid, const IUnknown *pData) { return m_ptr->SetPrivateDataInterface(guid, pData); } virtual HRESULT STDMETHODCALLTYPE SetName(LPCWSTR Name) { return m_ptr->SetName(Name); } virtual ULONG AddRef() { return m_ptr->AddRef(); } virtual HRESULT QueryInterface(REFIID riid, void **ppvObject) { return m_ptr->QueryInterface(riid, ppvObject); } virtual ULONG Release() { return m_ptr->Release(); } private: ID3D12Resource *m_ptr; };
然后,在调用 UpdateSubresources 的地方修改一下,先把 ID3D12Resource * 转换为 &D3D12ResourceProxy(p) 再传进去。后来发现这样不行,因为在 UpdateSubresources 内部又调用了 ID3D12GraphicsCommandList::CopyTextureRegion ,它会接收 ID3D12Resource * 。而 ID3D12GraphicsCommandList::CopyTextureRegion 已经实现在 d3d12.dll 中,这样又会回到调用协议不一致的问题。
把 sdk 实现放在公开的 .h 中真是个糟糕的设计。
Comments
你使用DX12,而不使用VC编译器,这本来就不是微软考虑的事情。你为什么不用VC呢?这玩意你就是用GCC编译了,你也不能跨平台
Posted by: FF | (15) November 2, 2018 04:53 PM
没有踩过类似的坑, 也不是很了解 D3D 的接口设计, 冒昧的来问一句... 会不会是 M$ 里头
__fastcall/__vectorcall 规范的问题, 还好 GCC 也有相应的关键字. ref: https://en.wikipedia.org/wiki/X86_calling_conventions#Microsoft_vectorcall
Posted by: anqur | (14) November 3, 2017 11:23 PM
传闻偶数版本的Direct3D都不能用,坐等Direct3D13
Posted by: 23333 | (13) October 17, 2017 05:06 PM
学习一下
Posted by: 互利符 | (12) October 10, 2017 08:05 PM
确实应该算是一个错误,可能是因为COM离开主流视野太久、以至于微软自己的开发者都不怎么注意了吧。
Posted by: glcolor | (11) October 9, 2017 09:05 PM
@爱斯基摩人 我觉得怪谭浩强发明 C/C++
Posted by: 我是扯淡的 | (10) September 30, 2017 01:37 PM
哈?楼上的~这坑也算到GCC的头上了?
Posted by: pig09 | (9) September 29, 2017 07:56 PM
这个应该算是GCC的坑吧,毕竟要判断目标环境的。
Posted by: 爱斯基摩人 | (8) September 28, 2017 09:56 PM
听说马云收购了你的公司,其实我早就知道你的公司会结束,因为在某个时间里,我曾向你的公司求职,叮当的态度是:“不要问那么多”,但是事实上我只是打算问问他为什么不用腾讯作为平台渠道而已……我没那么多精力去管那么多事情,做设计已经会让我很疲惫。
做游戏设计不难,但是需要一些小技巧,而普通人只知道怎么设计游戏,却不能理解这些巧妙……
Posted by: NEO | (7) September 27, 2017 04:28 PM
嗯
Posted by: hellogjq | (6) September 26, 2017 04:42 PM
gcc不管windows ABI的话, 那mingw也是有权利做修改来适配的.
D3D12只是违反COM规范, COM不代表整个windows, 所以并未违反VC的ABI.
Posted by: dwing | (5) September 21, 2017 04:27 PM
COM 也不是 C++ 。COM 是微软自己定的 ABI ,D3D12 的问题是微软违反了自己定的 ABI 。
Posted by: Cloud | (4) September 21, 2017 12:19 PM
mingw 并不是 gcc ,mingw 只是为 windows 提供 GNU 的最小支持。
怎么编译 C/C++ 代码是 gcc 的工作,不是 mingw 的。
另外,mingw 下还有别的选择,比如 clang 。
Posted by: Cloud | (3) September 21, 2017 12:15 PM
在windows平台VC就是C/C++ ABI的事实标准, mingw只能按VC的规则来, 以前为适配VC的双向支持也没少做各种妥协兼容.
Posted by: dwing | (2) September 21, 2017 10:09 AM
为 将 MinGW-w64 用在生产线的用户点赞。我一直用GCC/MinGW-w64(自编译版本),一般情况下 生成的代码都快于MSVC。无奈很多 代码都不支持。Python 3.x,Chromium, OpenJDK,MySQL,但是 OpenCV, Lua,SQLite 等还是很不错的,甚至直接全爆MSVC。
Posted by: DX | (1) September 20, 2017 10:31 PM