RenderTarget->GetSize 不工作

RenderTarget->GetSize not working

为了自学 Direct2D,我正在关注 this example 来自 MSDN。

但是我有一个问题。调用 D2D1_SIZE_F rtSize = m_pRenderTarget->GetSize(); 始终 return 的大小为 0,0,并且在调试器中导致 DrawLine 调用出现异常。如果我省略 GetSize() 调用并使用有效值填充 D2D1_SIZE_F 结构,它就可以工作。

初始化渲染目标的相关代码为:

    RECT rc;
    GetClientRect(m_hwnd, &rc);

    D2D1_SIZE_U size = D2D1::SizeU(
        rc.right - rc.left,
        rc.bottom - rc.top
        );

    // Create a Direct2D render target.
    hr = m_pDirect2dFactory->CreateHwndRenderTarget(
        D2D1::RenderTargetProperties(),
        D2D1::HwndRenderTargetProperties(m_hwnd, size),
        &m_pRenderTarget
        );

我已经用调试器验证过有效值的大小。

调用GetSize的绘图代码部分:

    m_pRenderTarget->BeginDraw();

    m_pRenderTarget->SetTransform(D2D1::Matrix3x2F::Identity());

    m_pRenderTarget->Clear(D2D1::ColorF(D2D1::ColorF::White));
    D2D1_SIZE_F rtSize = m_pRenderTarget->GetSize();
    // Draw a grid background.
    int width = static_cast<int>(rtSize.width);
    int height = static_cast<int>(rtSize.height);

    for (int x = 0; x < width; x += 10)
    {
        m_pRenderTarget->DrawLine(
            D2D1::Point2F(static_cast<FLOAT>(x), 0.0f),
            D2D1::Point2F(static_cast<FLOAT>(x), rtSize.height),
            m_pLightSlateGrayBrush,
            0.5f
            );
    }

所以我的问题是为什么 GetSize() return 0,0 之后会导致 AV?

顺便说一句:我正在使用: Windows 7 终极 64 位 Code::Blocks IDE TDM-GCC-64 gcc编译器v4.8.1 我在 Unicode 模式下编译 #define UNICODE 无论我编译为 32 位还是 64 位,都会出现问题(是的,我对 64 位模式做了一些小调整,以确保我在 WndProc 中有一个指向应用程序对象的有效指针)

Why does GetSize() return 0,0 and causes an AV later on?

因为 GCC/MinGW-W64 生成的对 GetSize 的调用与 d2d1.dll 中实现的调用约定不匹配。 GetSize 的 return 类型 D2D_SIZE_F 是一个结构。根据 Microsoft Docs 有两种方法从函数 return 结构:

User-defined types can be returned by value from global functions and static member functions. To return a user-defined type by value in RAX, it must have a length of 1, 2, 4, 8, 16, 32, or 64 bits. It must also have no user-defined constructor, destructor, or copy assignment operator. It can have no private or protected non-static data members, and no non-static data members of reference type. It can't have base classes or virtual functions. And, it can only have data members that also meet these requirements. (This definition is essentially the same as a C++03 POD type. Because the definition has changed in the C++11 standard, we don't recommend using std::is_pod for this test.) Otherwise, the caller must allocate memory for the return value and pass a pointer to it as the first argument.

当 GCC/MinGW-W64 编译文章中的示例代码时,调用者只为调用 GetSize 设置一个参数(在 rcx 中),并期望值为 return编辑于rax

# AT&T syntax (destination operand last)
mov 0x10(%rbx),%rcx    # rcx <- pointer to IRenderContext
mov (%rcx),%rax        # rax <- pointer to virtual function table
callq *0x1a8(%rax)     # virtual function call (expect result in rax)

在 Visual Studio 生成的代码中,调用者在调用 GetSize 之前将 rdx 设置为指向堆栈上的某个位置:

# Intel syntax (destination operand first)
mov rax,qword ptr [rsp+168h]     # rax <- pointer to IRenderContext
mov rax,qword ptr [rax]          # rax <- pointer to virtual function table
lea rdx,[rsp+68h]                # rdx <- address of return value (hidden argument)
mov rcx,qword ptr [rsp+168h]     # rcx <- this pointer (hidden argument)
call qword ptr [rax+1A8h]        # virtual function call (expect result at [rdx])

在 GCC/MinGW-W64 上,rdx 中的值不是有效地址,因此当 GetSize 的实现尝试将 return 值存储在内存中时,会发生访问冲突。

D2D_SIZE_F 是一个 64 位 POD 结构(只是两个浮点数的结构),所以在我看来 GCC 在 rax 寄存器中 return 它是正确的.我不知道是什么让 Visual Studio 使用 return-by-pointer,恐怕也不知道如何让 GCC 为兼容性做同样的事情。

我认为这实际上与调用约定的 nine-year-old bug in gcc 和不清楚或不正确的 MS 文档有关。

根据该错误报告,如果 return 结构无法装入寄存器,则其指针将在 RDX(第二个参数)中,被调用对象将在 RCX(第一个参数)中。 gcc 以相反的方式进行,在 RCX 中使用 return 指针(第一个参数)并在 RDX 中调用对象(第二个参数)。

根据文档,并不是 100% 清楚哪种方式是正确的:Return Values documentation for C++ documentation says to make the return value pointer the first argument. Separately, the Calling Conventions documentation for Debuggingthis 指针作为隐式第一个参数传递。

显然 gcc 和 MSVC 不同意这两个规则的应用顺序。在我的有限测试中,Clang 似乎同意 MSVC,但我还不能完全遵循逻辑。 Clang 似乎确实将这种情况视为 'thiscall' 和 in that case excludes the RCX register for hidden return object pointers。我还没有弄清楚它实际上是如何将 'this' 指针放入 RCX 中的,它可能不是非常重要。

回到这个问题,它不是return按值构建结构。对于较小的 Compiler Explorer test,MSVC 在 RAX 中唯一一次使用隐藏的 return 值而不是 return-by-value 是在成员调用时, 这是一个对象。 Clang 同意,你可以在 Clang IR 中非常清楚地看到它把对象指针放在第一位,然后是 hidden-return-struct-pointer:

call void @"?GetPixelSize@ID2D1RenderTarget@@QEBA?AUD2D_SIZE_U@@XZ"(%class.ID2D1RenderTarget* %4, %struct.D2D_SIZE_U* sret %2), !dbg !31

我怀疑这与 gcc 错误有关的原因是,我猜测潜在的问题是处理将“return 值指针”和“this 指针”移动到参数列表。

gcc(我猜?)首先处理被调用的对象,并将其作为新的第一个参数推送。然后它独立地查看 return 对象,或者 returns-by-value,或者将其作为新的第一个参数推送,最终将被调用的对象留在第二个。

Clang 正在以相反的方式处理。它首先处理 return 对象,但已经知道这是一个 this-call,这就是它知道如何避免上面的 ECX。如果它已经处理了被调用的对象指针,那么 ECX 就已经被分配了。然而,在决定 return 是按值还是按隐藏对象指针时,它显然已经知道它正在处理一个 this 指针,因为这有所不同。

并且知道这一点,并从上面看到的 CCIfSRet 向后搜索,我发现 Clang specifically marks that for non-POD return values, or Instance methods, the return value is indirect and not by-value. This code is not hit if the return value is not a structure,这就是为什么(如在 Compiler Explorer 中看到的那样)uint64_t 没有在这里变成了间接 return。

这也是我唯一看到explicitly sets that the 'return structure pointer' comes after the 'called-object' pointer的地方。我想所有其他 ABI 都将它们放在与 gcc 相同的顺序中。

(我无法在 Compiler Explorer 上检查 gcc,因为似乎没有提供支持 Win32 ABI 的版本,例如 mingw64 或 tdm 构建)

这是确保两个隐藏参数的正确顺序的同一个地方,即避免了让我开始寻找这个猎物的 gcc 错误。

现在我知道代码在哪里git blame告诉我这是a known thing about the x64 ABI in llvm 3.5 in 2014 although a bunch of other cases were fixed in llvm 9 in 2019


当然,Clang 不是 MSVC。它大概是在模拟观察到的 MSVC 行为,但 MSVC 结果可能只是处理顺序的巧合,而且恰好与 gcc 相反。


因此,虽然 gcc 通过严格阅读 ABI 文档是正确的,但与 MSVC(ABI 所有者)和 Clang 相比,它在处理具有聚合 return 值的实例方法的隐藏参数方面有两个不匹配之处。一个被窃听了,这个问题正在重现另一个。


workaround in mingw-w64's headers 通过使隐藏结构-return-指针成为显式指针参数来发挥作用。这既确保 gcc 不会尝试将其传递到寄存器中,又将其放在 之后 隐藏的被调用对象参数。

您可以看到 implementation side of the same fix in Wine,它已经在使用显式的被调用对象指针,因此为了获得正确的顺序,也需要使用显式的 return-结构指针参数.


旁注:我没有调查 32 位故障。

我快速浏览了一下 Clang(我 不知道 在这里是正确的,因为 Compiler Explorer 似乎不提供 32 位 MSVC)并且它似乎对 __stdcall__thiscall 产生相同的调用,只是 __stdcall 版本保存了 ECX,但 __thiscall 版本没有。我想这只是允许函数执行的内容以及完成后必须恢复的内容的区别。

基于 commit description in Clang's history 我怀疑同一个 9 年前的错误也在影响 32 位 gcc。


更新:查看 Return values documentation 个月后,我注意到此限制 记录在案:

User-defined types can be returned by value from global functions and static member functions.

因此成员函数不支持 return-by-value-in-register 方法,静态成员函数除外,并且在这种情况下 gcc 根据 ABI 文档不正确。