« 日程表服务 | 返回首页 | BGFX 的一个 lua 封装库 »

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编译了,你也不能跨平台
没有踩过类似的坑, 也不是很了解 D3D 的接口设计, 冒昧的来问一句... 会不会是 M$ 里头 __fastcall/__vectorcall 规范的问题, 还好 GCC 也有相应的关键字. ref: https://en.wikipedia.org/wiki/X86_calling_conventions#Microsoft_vectorcall
传闻偶数版本的Direct3D都不能用,坐等Direct3D13
学习一下
确实应该算是一个错误,可能是因为COM离开主流视野太久、以至于微软自己的开发者都不怎么注意了吧。
@爱斯基摩人 我觉得怪谭浩强发明 C/C++
哈?楼上的~这坑也算到GCC的头上了?
这个应该算是GCC的坑吧,毕竟要判断目标环境的。
听说马云收购了你的公司,其实我早就知道你的公司会结束,因为在某个时间里,我曾向你的公司求职,叮当的态度是:“不要问那么多”,但是事实上我只是打算问问他为什么不用腾讯作为平台渠道而已……我没那么多精力去管那么多事情,做设计已经会让我很疲惫。 做游戏设计不难,但是需要一些小技巧,而普通人只知道怎么设计游戏,却不能理解这些巧妙……
gcc不管windows ABI的话, 那mingw也是有权利做修改来适配的. D3D12只是违反COM规范, COM不代表整个windows, 所以并未违反VC的ABI.
COM 也不是 C++ 。COM 是微软自己定的 ABI ,D3D12 的问题是微软违反了自己定的 ABI 。
mingw 并不是 gcc ,mingw 只是为 windows 提供 GNU 的最小支持。 怎么编译 C/C++ 代码是 gcc 的工作,不是 mingw 的。 另外,mingw 下还有别的选择,比如 clang 。
在windows平台VC就是C/C++ ABI的事实标准, mingw只能按VC的规则来, 以前为适配VC的双向支持也没少做各种妥协兼容.
为 将 MinGW-w64 用在生产线的用户点赞。我一直用GCC/MinGW-w64(自编译版本),一般情况下 生成的代码都快于MSVC。无奈很多 代码都不支持。Python 3.x,Chromium, OpenJDK,MySQL,但是 OpenCV, Lua,SQLite 等还是很不错的,甚至直接全爆MSVC。

Post a comment

非这个主题相关的留言请到:留言本