I want to write text onto a 3D surface. Microsoft pushed hard on
DirectWrite and Direct2D to replace the good old GDI for writing text.
Then broke basic text rendering in DirectX11, forcing a convoluted
process of breaking fonts down into primitives that are painted onto
3D surfaces. Then they brought DirectWrite/Direct2D interop with
DirectX11.1, but there is very little documentation available.
I created a nice little DirectWrite class to handle the DWrite
side of things. Now I need to figure out how to create a DirectX11(.1)
surface that can be turned into a RenderTarget.
Since the dawn of programming, text, at the very simplest level,
have been the center-point for communications in programs. There were
many ways to inform the user about this new universe you [the
programmer] were showing them. Long ago, application developers at
Microsoft knew this (and devs everywhere else) and created
implementations in every language: "printf", "print", "cout", and even
"echo". Then, on a shiny day, a new windows Graphics API is birthed, a
collection of API capable of communicating with most graphic devices
through an abstraction layer. It was widely successful.
Years later, the API(s) were upgraded to show and support the
latest in hardware capability: tessellation, latest shader model
support, to name a few.
But something was wrong.
Since Microsoft has taken out the D3DXFONT and recommends
DirectWrite in it's place, how it is it possible to draw font to a
DirectX 11 surface for rendering?
Create the Direct2D interface factory.
The factory must be ID2D1Factory1.
This only needs to be done once, the same factory instance can be
reused until the program ends.
Create the DWrite interface factory.
The factory must be IDWriteFactory1.
This only needs to be done once.
Create a text formatter IDWriteTextFormat.
Extract the ID3D111Device1 interface from pDevice.
DirectX 11.1 is required for D3/D2 interoperability. Older versions
of DirectX require a more complicated process relying on DX10.
Extract the ID3D11DeviceContext1 interface from pContext.
Extract the IDXGIDevice1 interface from pDXGI.
Create a Direct2D device ID2D1Device from IDXGIDevice1.
Create a Direct2D device context ID2D1DeviceContext from ID2D1Device.
Get the current back buffer from pSwap to use as the Direct2D surface.
Create a Direct2D Bitmap on the surface.
Set the Bitmap as the ID2D1DeviceContext target.
Set the ID2D1DeviceContext DPI to match the desktop.
Create a ID2D1SolidColorBrush to draw the text.
Start a Direct2D draw session.
Submit all the 2D operations for the scene.
End the Direct2D draw session.
Release all the Direct2D resources.
D3Text hides all these details behind a simple text-oriented
interface. Once I have debugged all the DirectX wiring I can then
implement fancy text formats by embedded control codes in the format
text. D3Text reduces the 18-step odyssey to a 2-step dance:
Declare a D3Text object using Direct3D resources.
Write text.
The Gory Details
I created a 3DText class to isolate the Direct2D code from the
Direct3D program, with the goal of making it a lightweight, disposable
object. It uses the PIPC style.
I started out reusing code from MyD2D, which creates a RenderTarget
from a window. I soon discovered that Microsoft is deprecating the
RenderTarget interface in favor of the DeviceContext. This makes it more
complicated to use Direct2D given just a window handle while making it
slightly easier to integrate with a Direct3D environment. 3DText assumes
the Direct3D environment has already been created.
The goal is to hide as much detail as possible from the interface.
The constructor for D3Text does very little since I want this to be
a lightweight, disposable object -- it should cost almost nothing to
create a 3DText object on the stack. I was tempted to haul in my IText
class to handle the sprintf duties but left that for later. For now,
3DText has a big internal text buffer and simply calls the standard
sprintf.
The Direct2D and DirectWrite factories are globals, they only need
to be created once and can be reused for all instances of 3DText.
The Create() interface is broken into two parts. Create(void)
creates all the Direct2D and DirectWrite resources that are not
dependent on Direct3D. The second Create() handles all the Direct3D
to Direct2D integration.
Create():
HRESULT nD3Text::Create(void) {
HRESULT Err= 0;
D2D1_FACTORY_OPTIONS d2dOptions;
Zero(d2dOptions);
d2dOptions.debugLevel= D2D1_DEBUG_LEVEL_INFORMATION;
auto uuid= __uuidof(IDWriteFactory);
auto pDWF= reinterpret_cast<IUnknown**>(gDWriteFactory.ReleaseAndGetAddressOf());
if(!gD2DFactory.Get() && FAILED(Err= D2D1CreateFactory(D2D1_FACTORY_TYPE_MULTI_THREADED,d2dOptions,gD2DFactory.ReleaseAndGetAddressOf()))) {
//Failed to create Direct2D factory.
} else if(!gDWriteFactory.Get() && FAILED(Err= DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED,uuid,pDWF))) {
//Failed to create DirectWrite factory.
} else if(FAILED(Err= gDWriteFactory->CreateTextFormat(FontName,0,FontWgt,FontStyle,DWRITE_FONT_STRETCH_NORMAL,FontHgt,L"en-us",&pFormat))) {
//Failed to create the font.
} else {
pFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_LEADING);
pFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_NEAR);
}
return(Err);
}
HRESULT nD3Text::Create(ID3D11Device *pDevice, ID3D11DeviceContext *pContext, IDXGISwapChain1 *pSwap) {
HRESULT Err= 0;
// I need to re-query the interface to make sure it is DirectX 11.1
if(FAILED(Create())) {
//Failed to create factories.
} else if(FAILED(Err= pDevice->QueryInterface(__uuidof(ID3D11Device1),(void**)&pDevice1))) {
//Device is invalid or not DX11.1
} else if(FAILED(Err= pContext->QueryInterface(__uuidof(ID3D11DeviceContext1),(void**)&pContext1))) {
//Context is invalid or not DX11.1
} else if(FAILED(Err= pDevice->QueryInterface(__uuidof(IDXGIDevice1),(void**)&pDXGI))) {
//Device does not support IDXGI
} else if(FAILED(Err= gD2DFactory->CreateDevice(pDXGI,&pd2dDevice))) {
//Failed to create D2D Device from DXGI.
//Err= Error(Err,L"D3Text:Create: Failed to create D2D device from DXGI device.");
} else if(FAILED(Err= pd2dDevice->CreateDeviceContext(D2D1_DEVICE_CONTEXT_OPTIONS_NONE,&pd2dContext))) {
//Failed to create D2D device context.
} else if(FAILED(Err= pSwap->GetBuffer(0,__uuidof(pdxgiSurface),(void**)&pdxgiSurface))) {
//Failed to create DXGI surface
} else {
auto pPixelFmt= PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM,D2D1_ALPHA_MODE_IGNORE);
auto pProp= BitmapProperties1(D2D1_BITMAP_OPTIONS_TARGET|D2D1_BITMAP_OPTIONS_CANNOT_DRAW,pPixelFmt);
if(FAILED(Err= pd2dContext->CreateBitmapFromDxgiSurface(pdxgiSurface,pProp,&pd2Bitmap))) {
//Failed to create D2D bitmap.
} else {
pd2dContext->SetTarget(pd2Bitmap);
FLOAT dpiX,dpiY;
szLayout= pd2Bitmap->GetSize();
gD2DFactory->GetDesktopDpi(&dpiX,&dpiY);
pd2dContext->SetDpi(dpiX,dpiY);
Begin();
pd2dContext->CreateSolidColorBrush(ColorF(ColorF::Black),&pbrText);
isCreated= true;
}
}
SafeRelease(pDevice1);
SafeRelease(pContext1);
SafeRelease(pDXGI);
SafeRelease(pd2dDevice);
SafeRelease(pdxgiSurface);
return(Err);
}
I need to have my own internal notion of draw sessions so I can
call Write() multiple times for each frame. Reset() provides a way
to handle losing and recreating the resources when the window is
resized or recreated.
D3Text can be used as either a long-term persistent object or a
short-term temporary object. The persistent object is much more
efficient and should be used if text is being rendered 60 times per
second. The temporary object is simpler and convenient for occasional
use.
A temporary 3DText object should be created and destroyed inside
the Direct3D render function. The constructor performs an implicit
Begin() and the destructor does an implicit End().
D2D1Factory::CreateDevice() failed. I was able to get the critical
information by enabling DEBUG_LEVEL_INFORMATION when creating the
factory object: D2D DEBUG WARNING - The Direct3D device was not created with D3D11_CREATE_DEVICE_BGRA_SUPPORT, and therefore is not compatible with Direct2D.
This is a great example of a code error message. I added D3D11_CREATE_DEVICE_BGRA_SUPPORT
to creationFlags in Game::CreateDevice() and was able to create the
Direct2D device.
All the Direct2D creation stuff succeeded, then the DrawText()
failed: D2D DEBUG ERROR - The parameter [defaultForegroundBrush] for ID2D1RenderTarget::DrawText is not optional. A NULL pointer was passed. This will cause Direct2D to crash.
I forgot to create the text brush. Easy fix.
The Direct2D RenderTarget seems to be deprecated in DirectX11,
replaced with the DeviceContext. (Which sounds a lot like something
from GDI. Everything old is new again.)
The DrawText() now fails with D2D DEBUG ERROR - An attempt to render a primitive outside of BeginDraw/EndDraw.
For expediency, I just wrapped the DrawText() with BeginDraw() and
EndDraw(). This worked, DrawText() succeeded, and I see text in
the window.
Each frame render is kicking out a warning: Exception thrown at 0x00007FFC3A1295FC in SimpleGrid.exe: Microsoft C++ exception: _com_error at memory location 0x000000E5EB24CC80.
The Visual Studio exception settings are found in Debug > Windows > Exceptions.
Enable "C++ > _com_error". This broke inside the nD3Text destructor while
releasing pbrText. This is because I am using the ComPtr wrapper.
During the destruction sequence some objects will implicitly call
Release() on dependent objects, resulting in an extra Release() when
the ComPtr is destroyed. I need to call ComPtr.Reset() explicitly,
which sort of defeats its whole purpose.
Exit Crash
Using ComPtr<> was not the magical fix for allocation
tracking -- D3Text triggers an exception during its destructor that
has me very confused. There seems to be some inter-dependent resource
freeing happening during the destruction that is causes Release() to
be called on objects that have already been released.
I tried calling Reset() on everything in the destructor, which
resulted in exceptions on ID2D1Device and ID2D1Context. Then I tried
keeping all the ComPtr objects for the lifetime of the D3Text object,
but then it was corrupting the stack. The implicit destructors in
~nD3Text:
ID2D1Bitmap
IDXGISurface: After destructor refct is 2.
ID2D1DeviceContext
ID2D1Device
IDXGIDevice: After destructor refct is 76.
ID3D11DeviceContext: After destructor refct is 7.
ID3D11Device: After destructor refct is 75.
IDWriteTextFormat
ID2D1SolidColorBrush
ID2D1RenderTarget
Doh! The stack corruption was because my PIPC implementation of
nD3Text had outgrown D3TEXT_OBJ_SIZE. I missed the warnings because
the default setting is to not treat warnings as errors. Fixing this
returned me to the twisty maze of exploding destructors.
It seems as though releasing pd2Bitmap implicitly releases
pd2dContext, which causes ~ComPtr<ID2D1DeviceContext> to call
Release() through an object that has already been freed. The refct
inside IUnknown is not public so I can't check it. This is a case of
too many accountants.
I ditched all the ComPtr business and am using pointers and
SafeRelease(). Now it is crashing on SafeRelease(pd2dContext). I have
spent six hours trying to write text to the screen. Making things more
complicated is not progress.
FIXED: I rearranged the code so that the
DXGISurface and D2D1Bitmap are created inside Begin() and released in
End(). This works, the destructor succeeds, and there are no live
objects on exit. Creating and releasing the surface and bitmap for
every frame sounds like a lot of work but it shouldn't take any time
since the bitmap is really just another reference to a block of GPU
memory that has already been allocated.
Unfortunately, I had forgotten to uncomment the DrawText(). It only
works if I never call DrawText(), so the bug is still there. Now I know
that DrawText() is the trigger, which would seem to rule out the order
in which the resources are created and destroyed.
Everything is OK if I call DrawLine() instead of DrawText(). p2dContext->DrawLine(Point2F(10.0f,10.0f),Point2F(90.0f,90.0f),pbrText,2.0f);
I tried switching between Direct2D v1/v2 and DirectWrite v1/v2 with
no change.
I am beginning to suspect this is a problem with the VMware graphics
driver.
WebV7 (C)2018 nlited | Rendered by tikope in 39.531ms | 3.145.47.193