dev.nlited.com

>>

Direct2D in Direct3D

<<<< prev
next >>>>

2017-11-25 18:44:33 chip Page 2064 📢 PUBLIC

November 25 2017

I want to draw flat 2D text into the Direct3D frame.

TL;DR; Current D3Text code: D3Text Code

Using Direct2D in a Direct3D Program

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.

This Microsoft enthusiast put it well:

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?

A Microsoft enthusiast.

Using an amalgam of MSDN: Introducing Direct2D 1.1 and Katy's Code: Direct2D 1.1 Migration Guide I was able to stitch Direct2D/DirectWrite into a DirectX11/Direct3D program. It isn't pretty...

The 18-Step Process

  1. Required Direct3D objects:
  2. 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.
  3. Create the DWrite interface factory.
    The factory must be IDWriteFactory1.
    This only needs to be done once.
  4. Create a text formatter IDWriteTextFormat.
  5. 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.
  6. Extract the ID3D11DeviceContext1 interface from pContext.
  7. Extract the IDXGIDevice1 interface from pDXGI.
  8. Create a Direct2D device ID2D1Device from IDXGIDevice1.
  9. Create a Direct2D device context ID2D1DeviceContext from ID2D1Device.
  10. Get the current back buffer from pSwap to use as the Direct2D surface.
  11. Create a Direct2D Bitmap on the surface.
  12. Set the Bitmap as the ID2D1DeviceContext target.
  13. Set the ID2D1DeviceContext DPI to match the desktop.
  14. Create a ID2D1SolidColorBrush to draw the text.
  15. Start a Direct2D draw session.
  16. Submit all the 2D operations for the scene.
  17. End the Direct2D draw session.
  18. 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:

  1. Declare a D3Text object using Direct3D resources.
  2. 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.

The 3DText interface:


3DText Interface: #define D3TEXT_OBJ_SIZE 616 class D3Text { public: D3Text(void); D3Text(ID3D11Device *pDevice, ID3D11DeviceContext *pContext, IDXGISwapChain1 *pSwap); ~D3Text(void); HRESULT Create(ID3D11Device *pDevice, ID3D11DeviceContext *pContext, IDXGISwapChain1 *pSwap); void Begin(void); void End(void); void Reset(void); UINT Write(const WCHAR *Fmt,...); private: class nD3Text *pObj; BYTE Obj[D3TEXT_OBJ_SIZE]; };

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.

PIPC Interface and internal class: #include "pch.h" #include <new> #include <stdio.h> #include <windows.h> #include <d2d1_1.h> #include <d2d1_1helper.h> #include <dwrite_1.h> #include <wrl/client.h> #include "Game.h" #pragma message(__FILE__": Optimizer disabled.") #pragma optimize("",off) #pragma comment(lib,"d2d1.lib") #pragma comment(lib,"dwrite.lib") using namespace D2D1; using Microsoft::WRL::ComPtr; #define Zero(O) memset(&O,0,sizeof(O)) #define STRSIZE(S) (sizeof(S)/sizeof(S[0]) - 1) #define CHECK_OBJ_SIZE(name,size) \ template<int N> struct name##_CheckSizeT { short operator()() { return((N+0x7FFF)-size); } }; \ static void name##_CheckSize(void) { name##_CheckSizeT<sizeof(name)>()(); } #define SafeRelease(pObj) { if(pObj) pObj->Release(); pObj= 0; } /*************************************************************************/ /** Public interface **/ /*************************************************************************/ #define TEXT_MAX 200 class nD3Text { public: nD3Text(D3Text *pD3Text); ~nD3Text(void); HRESULT Create(void); HRESULT Create(HWND hWnd); HRESULT Create(ID3D11Device *pDevice, ID3D11DeviceContext *pContext, IDXGISwapChain1 *pSwap); void Begin(void); void End(void); void Reset(void); UINT Write(const WCHAR *Fmt, va_list ArgList); private: D3Text *pText; //Parent D3Text interface object. bool isCreated; //Was everything created successfully? WCHAR Text[TEXT_MAX]; //Final output text. UINT ChrCt; //Valid characters in Text[] WCHAR FontName[40]; //Font face DWRITE_FONT_WEIGHT FontWgt; //Font weight DWRITE_FONT_WEIGHT_REGULAR DWRITE_FONT_STYLE FontStyle; //Font style DWRITE_FONT_STYLE_NORMAL float FontHgt; //Font height (points) ID2D1RenderTarget *pRT; //Render target ID2D1SolidColorBrush *pbrText; //Text color IDWriteTextFormat *pFormat; //DWrite text formatter ID3D11Device *pDevice1; ID3D11DeviceContext *pContext1; IDXGIDevice *pDXGI; ID2D1Device *pd2dDevice; ID2D1DeviceContext *pd2dContext; IDXGISurface *pdxgiSurface; ID2D1Bitmap1 *pd2Bitmap; D2D1_SIZE_F szLayout; D2D1_RECT_F rLayout; }; CHECK_OBJ_SIZE(nD3Text,D3TEXT_OBJ_SIZE); D3Text::D3Text(void) { Zero(Obj); pObj= new(Obj) nD3Text(this); } D3Text::D3Text(ID3D11Device *pDevice, ID3D11DeviceContext *pContext, IDXGISwapChain1 *pSwap) { Zero(Obj); if(pObj= new(Obj) nD3Text(this)) pObj->Create(pDevice,pContext,pSwap); } D3Text::~D3Text(void) { if(pObj) { pObj->~nD3Text(); pObj= 0; } } HRESULT D3Text::Create(ID3D11Device *pDevice, ID3D11DeviceContext *pContext, IDXGISwapChain1 *pSwap) { return(pObj ? pObj->Create(pDevice,pContext,pSwap):ERROR_INVALID_HANDLE); } void D3Text::Reset(void) { if(pObj) pObj->Reset(); } void D3Text::Begin(void) { if(pObj) pObj->Begin(); } UINT D3Text::Write(const WCHAR *Fmt, ...) { va_list ArgList; va_start(ArgList,Fmt); UINT ChrCt= pObj ? pObj->Write(Fmt,ArgList):0; va_end(ArgList); return(ChrCt); }

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.

Constructors: /*************************************************************************/ /** nText **/ /*************************************************************************/ static ComPtr<ID2D1Factory1> gD2DFactory; static ComPtr<IDWriteFactory1> gDWriteFactory; nD3Text::nD3Text(D3Text *pD3Text) { pText= pD3Text; wcsncpy(FontName,L"Arial",STRSIZE(FontName)); FontWgt= DWRITE_FONT_WEIGHT_BOLD; FontStyle= DWRITE_FONT_STYLE_NORMAL; FontHgt= 18.0f; } nD3Text::~nD3Text(void) { isCreated= false; SafeRelease(pbrText); SafeRelease(pd2Bitmap); SafeRelease(pdxgiSurface); SafeRelease(pd2dContext); SafeRelease(pd2dDevice); SafeRelease(pDXGI); SafeRelease(pContext1); SafeRelease(pDevice1); SafeRelease(pFormat); SafeRelease(pRT); }

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.

Begin, End, Reset: void nD3Text::Begin(void) { ChrCt= 0; rLayout= { 0,0,szLayout.width,szLayout.height }; pd2dContext->BeginDraw(); } void nD3Text::End(void) { pd2dContext->EndDraw(); } void nD3Text::Reset(void) { //TODO: Release everything. }

Write() formats the text using vsprintf in its internal buffer and uses DrawTextW() to draw it.

Write(): UINT nD3Text::Write(const WCHAR *Fmt, va_list ArgList) { if(!isCreated) return(0); int ct= _vsnwprintf(Text+ChrCt,STRSIZE(Text)-ChrCt,Fmt,ArgList); if(ct<0) ct= STRSIZE(Text)-ChrCt; ChrCt+= ct; if(ChrCt) { if(pRT) { pRT->DrawTextW(Text,ChrCt,pFormat,rLayout,pbrText); } else if(pd2dContext) { pd2dContext->DrawTextW(Text,ChrCt,pFormat,rLayout,pbrText); } } return(ChrCt); }

Using D3Text

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().

Temporary Object: void Game::Render2(void) { D3Text text(m_d3dDevice,m_d3dContext,m_swapChain); text.Write(L"Frame %06llu",m_frameCt); }

A persistent 3DText object is declared as part of the global game class and the resources created later.

Persistent object: class Game { private: D3Text text; }; void Game::CreateResources2(void) { text.Create(m_d3dDevice,m_d3dContext,m_swapChain); } void Game::Render2(void) { text.Begin(); text.Write(L"Frame %06llu",m_frameCt); text.End(); }

Mistakes and Troubles

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.

Visual Studio Exceptions Visual Studio Exceptions

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:

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 43.720ms | 3.135.206.25