First post, by CookiePLMonster
- Rank
- Newbie
A dgVoodoo2 bug I first noticed months ago on 2.74.1 when working on a commercial D3D9 game, and that is still happening on the latest version, 2.83.2 as of the time of writing this post - dgVoodoo2 appears to emulate the behaviour of IDirect3DResource9::SetPrivateData with the D3DSPD_IUNKNOWN flag incorrectly.
With this flag, the application is expected to pass a pointer to a COM object and a size of sizeof(IUnknown*), then the D3D9 runtime is expected to increase a reference count of this object. Similarly to any resource getters, GetPrivateData is then expected to increase the reference count of that object before returning it to the application, and FreePrivateData is expected to release the object.
When this is done in a game with dgVoodoo2 installed, the flag appears to be ignored when obtaining the pointer. SetPrivateData does increment the reference count of the passed object correctly, but GetPrivateData then returns garbage, and does not increment the reference count.
I created a small sample program verifying the documented behaviour of the private data. With dgVoodoo 2.83.2 (and earlier), the first test fails on the pointer comparison assertion.
#include <iostream>
#include <cassert>
#include <d3d9.h>
#pragma comment(lib, "d3d9.lib")
// {E3336238-D261-4C21-B79A-8E1DBE8B8858}
static const GUID TEST_DATA_GUID =
{ 0xe3336238, 0xd261, 0x4c21, { 0xb7, 0x9a, 0x8e, 0x1d, 0xbe, 0x8b, 0x88, 0x58 } };
static bool bDestructorCalled = false;
class TestObject : public IUnknown
{
public:
TestObject()
{
std::cout << "TestObject created" << std::endl;
}
~TestObject()
{
std::cout << "TestObject destroyed" << std::endl;
bDestructorCalled = true;
}
// Those are deliberately simplified
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject) override
{
AddRef();
*ppvObject = this;
return S_OK;
}
ULONG __stdcall AddRef() override
{
return ++ref;
}
ULONG __stdcall Release() override
{
ULONG newRef = --ref;
if (newRef == 0)
{
delete this;
}
return newRef;
}
private:
ULONG ref = 1;
};
int main()
{
// Error checks omitted for brevity, except when it's relevant for the test
WNDCLASSEXW wndClass { sizeof(wndClass) };
wndClass.lpfnWndProc = DefWindowProcW;
wndClass.hInstance = GetModuleHandleW(nullptr);
wndClass.lpszClassName = L"PrivateDataTest";
RegisterClassExW(&wndClass);
HWND window = CreateWindowExW(0, wndClass.lpszClassName, L"PrivateDataTest", WS_OVERLAPPEDWINDOW|WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT, 800, 600, nullptr, nullptr, wndClass.hInstance, nullptr);
IDirect3D9* d3d = Direct3DCreate9(D3D_SDK_VERSION);
D3DPRESENT_PARAMETERS presentParameters {};
presentParameters.SwapEffect = D3DSWAPEFFECT_DISCARD;
presentParameters.Windowed = TRUE;
IDirect3DDevice9* d3dDevice;
d3d->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, window, D3DCREATE_HARDWARE_VERTEXPROCESSING, &presentParameters, &d3dDevice);
IDirect3DSurface9* backBuffer;
d3dDevice->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &backBuffer);
// D3DSPD_IUNKNOWN docs state:
// The data at pData is a pointer to an IUnknown interface. SizeOfData must be set to the size of a pointer to IUnknown, that is, sizeof(IUnknown*).
// Direct3D automatically callsIUnknown through pData when the private data is destroyed. Private data will be destroyed by a subsequent call
// to IDirect3DResource9::SetPrivateData with the same GUID, a subsequent call to IDirect3DResource9::FreePrivateData, or when the IDirect3D9 object is released.
{
std::cout << "Test #1: Create a private data interface, get it, free it, check reference counters and the destructor call" << std::endl << std::endl;
bDestructorCalled = false;
TestObject* object = new TestObject;
HRESULT hr = backBuffer->SetPrivateData(TEST_DATA_GUID, object, sizeof(object), D3DSPD_IUNKNOWN);
assert(SUCCEEDED(hr));
object->AddRef();
ULONG ref1 = object->Release();
assert(ref1 == 2); // One from us, one from private data. dgVoodoo 2.83.2 does NOT fail here
IUnknown* storedObject;
DWORD cbSize = sizeof(storedObject);
hr = backBuffer->GetPrivateData(TEST_DATA_GUID, &storedObject, &cbSize);
assert(SUCCEEDED(hr));
assert(object == storedObject); // dgVoodoo 2.83.2 fails here
ref1 = storedObject->Release();
assert(ref1 == 2); // One from us, one from private data
object->Release();
hr = backBuffer->FreePrivateData(TEST_DATA_GUID);
assert(bDestructorCalled);
}
{
std::cout << "Test #2: Create a private data interface on a dummy surface, release that surface, check reference counters and the destructor call" << std::endl << std::endl;
bDestructorCalled = false;
TestObject* object = new TestObject;
HRESULT hr = backBuffer->SetPrivateData(TEST_DATA_GUID, object, sizeof(object), D3DSPD_IUNKNOWN);
assert(SUCCEEDED(hr));
char buffer[] = { 1, 2, 3, 4, 5, 6 };
hr = backBuffer->SetPrivateData(TEST_DATA_GUID, buffer, sizeof(buffer), 0);
assert(SUCCEEDED(hr));
object->Release();
assert(bDestructorCalled);
}
{
std::cout << "Test #3: Create a private data interface, overwrite it with plain data, check reference counters and the destructor call" << std::endl << std::endl;
bDestructorCalled = false;
IDirect3DSurface9* testSurface;
HRESULT hr = d3dDevice->CreateOffscreenPlainSurface(4, 4, D3DFMT_A8R8G8B8, D3DPOOL_SYSTEMMEM, &testSurface, nullptr);
assert(SUCCEEDED(hr));
TestObject* object = new TestObject;
hr = testSurface->SetPrivateData(TEST_DATA_GUID, object, sizeof(object), D3DSPD_IUNKNOWN);
assert(SUCCEEDED(hr));
ULONG ref1 = object->Release();
assert(ref1 == 1); // One from private data
testSurface->Release();
assert(bDestructorCalled);
}
return 0;
}
I don't know if any commercial games ever used this feature, or if I was to be the first (before stumbling upon this bug). Nonetheless, it's a supported usage scenario in D3D9.