RenderTarget->GetSize не работает
Чтобы узнать себя Direct2D я следую этому примеру из MSDN.
У меня есть, однако, одна проблема. Вызов D2D1_SIZE_F rtSize = m_pRenderTarget->GetSize();
всегда возвращает размер 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() возвращает 0,0 и вызывает AV позже?
Кстати: я использую:
Windows 7 Ultimate, 64-битный код::Blocks IDE, компилятор gcc TDM-GCC-64 v4.8.1, я компилирую в режиме Unicode #define UNICODE
Проблема возникает по-другому, если я компилирую в 32-битные или 64-битные (да, я сделал несколько незначительных корректировок для 64-битного режима, чтобы убедиться, что у меня есть действительный указатель на объект приложения в WndProc)
1 ответ
Почему GetSize() возвращает 0,0 и вызывает AV позже?
Поскольку вызов GetSize, сгенерированный GCC/MinGW-W64, не соответствует соглашению о вызовах реализации в d2d1.dll
, Тип возврата D2D_SIZE_F
GetSize это структура. Согласно MSDN, есть два способа вернуть структуру из функции:
Пользовательские типы могут быть возвращены по значению из глобальных функций и статических функций-членов. Чтобы быть возвращенными по значению в RAX, пользовательские типы должны иметь длину 1, 2, 4, 8, 16, 32 или 64 бита; нет пользовательского конструктора, деструктора или оператора копирования; нет частных или защищенных нестатических членов данных; нет нестатических данных-членов ссылочного типа; нет базовых классов; нет виртуальных функций; и нет членов данных, которые также не отвечают этим требованиям. (По сути, это определение типа POD C++03. Поскольку определение изменилось в стандарте C++11, мы не рекомендуем использовать std::is_pod для этого теста.) В противном случае вызывающая сторона берет на себя ответственность выделение памяти и передача указателя на возвращаемое значение в качестве первого аргумента.
Когда GCC/MinGW-W64 компилирует пример кода из статьи, вызывающая сторона устанавливает только один аргумент (в rcx
) для вызова GetSize и ожидает, что значение будет возвращено в 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, вызывающий абонент устанавливает rdx
чтобы указать местоположение в стеке перед вызовом GetSize:
# 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 пытается сохранить возвращаемое значение в памяти, происходит нарушение доступа.
D2D_SIZE_F
является 64-битной структурой POD (просто структурой из двух чисел с плавающей запятой), поэтому мне кажется, что GCC правильно, чтобы вернуть его в rax
регистр. Я не знаю, что заставляет Visual Studio использовать возврат по указателю, и, боюсь, как заставить GCC делать то же самое для совместимости.
Я думаю, что это на самом деле связано с ошибкой девятилетней давности в gcc и нечеткой или неправильной документацией MS о соглашении о вызовах.
Согласно этому отчету об ошибке, если возвращаемая структура не может быть помещена в регистр, ее указатель будет в RDX (2-й аргумент), а вызываемый объект будет в RCX (1-й аргумент). gcc делает это наоборот, с указателем возврата в RCX (1-й аргумент) и вызываемым объектом в RDX (2-й аргумент).
В документации не совсем ясно, какой путь правильный: в документации по возвращаемым значениям для документации C++ говорится, что указатель возвращаемого значения должен быть первым аргументом. Отдельно в документации по соглашениям о вызовах для отладки говорится, чтоthis
pointer is passed as an implicit first parameter.
Clearly gcc and MSVC disagree about the order of those two rules being applied. It appears in my limited tested that Clang agrees with MSVC, but I haven't been able to follow the logic fully yet. Clang does seem to treat this situation as a 'thiscall', and in that case excludes the RCX register for hidden return object pointers. I haven't worked out how it actually puts the 'this' pointer into RCX though, it's probably not super-important.
Back to this problem, where it's not returning the structure by value. With a small Compiler Explorer test, the only time that MSVC uses a hidden return value instead of return-by-value in RAX is when it's a member call, and it's an object. Clang agrees, and you can see really clearly in the Clang IR that it put the object pointer first, and the hidden-return-struct-pointer second:
call void @"?GetPixelSize@ID2D1RenderTarget@@QEBA?AUD2D_SIZE_U@@XZ"(%class.ID2D1RenderTarget* %4, %struct.D2D_SIZE_U* sret %2), !dbg !31
The reason I suspect this is related to the gcc bug, is that I am guessing that the underlying issue is the ordering of dealing with moving the "return value pointer" and "this pointer" into the argument list.
gcc (I guess?) processes the called object first, and pushes it as the new first argument. Then it independently looks at the return object, and either returns-by-value, or pushes it as the new-new first argument, leaving the called object ultimately second.
Clang is processing the other way 'round. It processes the return object first, but already knows it's a this-call, which is how it knows to avoid ECX above. If it had already processed the called object pointer, ECX would have been allocated already. However, when deciding if the return is by-value or by-hidden-object-pointer, it clearly already knows that it is dealing with a this-pointer, because that makes the difference.
And knowing that, and hunting backwards from the CCIfSRet
seen above, I found that 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, which is why (as seen in Compiler Explorer) a uint64_t doesn't get turned into an indirect return here.
This is also the only place I saw that explicitly sets that the 'return structure pointer' comes after the 'called-object' pointer. I guess every other ABI puts them in the same order as gcc.
(I couldn't check gcc on Compiler Explorer, as there doesn't seem to be a version offered which supports the Win32 ABI, e.g., mingw64 or tdm builds)
This is the same place that ensures the correct order of the two hidden parameters, i.e. avoids the gcc bug that started me on this hunt in the first place.
And now that I know where the code is, git blame
shows me that this was 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.
Of course, Clang is not MSVC. It is presumably emulating an observed behaviour of MSVC, but it's possible that the MSVC outcome is just a coincidence of order-of-processing, and it happens to be the opposite of gcc.
So while gcc is correct by a strict reading of the ABI docs, it has two mismatches compared to both MSVC (the ABI owner) and Clang around handling hidden arguments for instance methods with aggregate return values. One has been bugged, and this problem is reproducing the other.
The workaround in mingw-w64's headers functions by making the hidden structure-return-pointer an explicit pointer parameter. This both ensures gcc doesn't try to pass it in a register, and also puts it after the hidden called-object parameter.
You can see the implementation side of the same fix in Wine, which was already using an explicit called-object pointer, and so to get the ordering correct, needs to use an explicit return-structure-pointer parameter too.
Примечание: я не рассматривал 32-битный сбой.
Я быстро просмотрел Clang (который, я не знаю, здесь правильный, так как Compiler Explorer, похоже, не предлагает 32-разрядный MSVC), и, похоже, он вызывает один и тот же вызов для обоих__stdcall
а также __thiscall
, за исключением того, что __stdcall
версия сохраняет ECX, но __thiscall
версии нет. Я думаю, это просто разница в том, что функции разрешено топтать, и что она должна восстанавливать, когда это будет сделано.
Основываясь на описании коммита в истории Clang, я подозреваю, что та же ошибка девятилетней давности влияет и на 32-битный gcc.