Introducing Direct2D
Kenny Kerr
This column is based on a prerelease version of Windows 7. Details are subject to change.
Contents
With the introduction of Windows Vista a few years ago, it was clear that the era of the Windows Graphics Device Interface (GDI) was coming to an end. GDI, even with the help of GDI+, had been showing its age especially when compared to the superior graphics capabilities of Windows Presentation Foundation (WPF). As if that weren't enough, GDI lost its hardware acceleration while WPF took advantage of Direct3D's abundance of hardware acceleration.
However, if you want to develop high-performance and high-quality commercial applications, you'll still look to C++ and native code to deliver that power. That's why few things excite me more about Windows 7 than the introduction of Direct2D and DirectWrite. Direct2D is a brand new 2D graphics API designed to support the most demanding and visually rich desktop applications with the best possible performance. DirectWrite is also a brand-new API that complements Direct2D and provides hardware-accelerated text, when used with Direct2D, and high-quality text layout and rendering with advanced support for OpenType typography and ClearType text rendering.
In this article, I will explore these new technologies and give you an idea of why it matters and how you can start using them today.
Architecture And Principals
On Windows XP, GDI and Direct3D had equal standing as far as the operating system was concerned. The user-mode portions of GDI and Direct3D interfaced directly with their kernel-mode counterparts. As a result, both could be directly hardware accelerated, given suitable display drivers. With Windows Vista, Microsoft delegated the responsibility of controlling video hardware exclusively to Direct3D. GDI suddenly became the legacy graphics API that was supported predominantly through software-based rendering on top of Direct3D.
So GDI is dead and Direct3D is the future, but where does this leave us and how does this all relate to Direct2D and WPF? Well, the first thing to note is that Direct3D is effectively the Windows graphics processing unit (GPU) API, whether you want to render graphics or hotwire your GPU for added computing power. Ignoring OpenGL (which is still supported), Direct3D is the lowest-level graphics pipeline for directly controlling a graphics display adapter from a user-mode application.
Direct3D is what is known as an immediate mode graphics API. This simply means that the API provides a thin layer over any graphics hardware, providing access to the hardware's features while filling in some of the blanks should the hardware be limited in some way. Every time you want to render or update the display, you effectively need to tell Direct3D that you are about to render, supply the Direct3D pipeline with everything it needs to render a frame, and then tell it that you're finished, and it will cause the display to be updated. Although Direct3D provides many advanced 3D functions, it is up to you to directly control all of them and it has relatively few 3D primitives. Needless to say, this is not a trivial task.
Prior to Windows Vista, Direct3D also included a higher-level retained-mode graphics API that was built on top of the immediate-mode API. This API provided direct support for manipulating 3D objects, called scenes—a hierarchy of frames with objects and lighting. It was called retained mode because the API retains a copy of the entire scene graph, so to speak. Applications simply update the scene, and the API automatically takes care of rendering and updating the display. The trouble, of course, is that all of this comes at a cost; if the cost is too high or the abstractions don't quite meet your requirements, you have to discard this technique entirely, rely on the immediate mode API directly, and provide your own geometry algorithms.
If you're at all familiar with WPF, then the previous description of a retained-mode graphics API should sound quite familiar. Although Direct3D discontinued its retained-mode API, WPF includes its own internal retained-mode API, known as the Media Integration Layer (MIL). MIL sits between WPF and Direct3D and retains a copy of the scene graph defined by WPF, allowing managed code to interact with visual objects while remaining in the background, coordinating with MIL to ensure that changes are reflected in the scene graph that it retains. This allows WPF to provide a very rich and interactive experience for developers, but it comes with a price.
Having said all that, it should be fairly obvious where Direct2D comes in. Direct2D provides rich rendering support for simple and complex geometries, bitmaps, and text built directly on top of Direct3D's immediate-mode graphics API for unparalleled performance. The result is a high-performance and low-overhead approach for producing high-quality graphics content for your applications. It provides a dramatically simpler API for producing 2D content compared to using Direct3D directly while adding little overhead in most cases. In fact, with the exception of a few specific operations such as per-primitive anti-aliasing, you'd be hard pressed to outperform Direct2D with Direct3D. Direct2D should also easily outperform the likes of MIL and Quartz2DE.
As if that weren't enough, it goes even further and provides many additional features, such as remote rendering over Remote Desktop Protocol (RDP), software fallback for server-side rendering or to compensate for a lack of hardware, support for rendering ClearType text, and unparalleled interoperability with both GDI and Direct3D. Let's just say there's none of this WPF "airspace" nonsense.
Now let's get down to writing some code! To keep the examples focused, I'm going to use the Active Template Library (ATL) and the Windows Template Library (WTL) to handle all of the boilerplate windowing code for me. Remember, Direct2D is about rendering. If you want it to render to a window, you still need to manage that window. If you want mouse input, you still need to respond to window messages as usual. You get the idea. Figure 1 contains a skeleton of a window that I'll fill out during the rest of this article.
Figure 1 Window Skeleton
#define HR(_hr_expr) { hr = _hr_expr; if (FAILED(hr)) return hr; } class Window : public CWindowImpl<Window, CWindow, CWinTraits<WS_OVERLAPPEDWINDOW>> { public: BEGIN_MSG_MAP(Window) MSG_WM_DESTROY(OnDestroy) END_MSG_MAP() HRESULT Create() { VERIFY(__super::Create(0)); // top-level VERIFY(SetWindowText(L"Direct2D Sample")); VERIFY(SetWindowPos(0, // z-order 100, // x 100, // y 600, // pixel width, 400, // pixel height, SWP_NOZORDER | SWP_SHOWWINDOW)); VERIFY(UpdateWindow()); return S_OK; } private: void OnDestroy() { ::PostQuitMessage(1); } };
Factories And Resources
As with various other APIs like Direct3D and XmlLite, Direct2D uses a lightweight version of the COM specification to manage object lifetime through interfaces derived from IUnknown. There's no need to initialize the COM run time and worry about apartments or proxies. It's just a convention to simplify resource management and allow APIs and applications to expose and consume objects in a well-defined way. Keep in mind that just because Direct2D uses COM interfaces does not mean that you can provide your own implementations of those interfaces. Unless otherwise noted, Direct2D will work only with its own implementations. I would suggest that you use ATL's CComPtr smart pointer class for managing the interface pointers, as I do in the examples in this article.
Every Direct2D application begins by creating a factory object. The D2D1CreateFactory function returns an implementation of the ID2D1Factory interface:
CComPtr<ID2D1Factory> factory;
HR(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &factory));
Of course, before you can use any of these types, you need to include d2d1.h, the Direct2D header file, which declares the various Direct2D interfaces, structures, and constants as well as the D2D1CreateFactory function. The d2d1.lib file is provided to import the D2D1CreateFactory function. The d2d1helper.h header file also provides a great many helpful functions and classes to simply the use of Direct2D from C++. I normally include Direct2D in a project by adding the following directives in the project's precompiled header:
#include <d2d1.h>
#include <d2d1helper.h>
#pragma comment(lib, "d2d1.lib")
As I mentioned, Direct2D does not rely on COM, but the first parameter to D2D1CreateFactory might lead you to believe otherwise. The D2D1_FACTORY_TYPE enum defines both D2D1_FACTORY_TYPE_SINGLE_THREADED and D2D1_FACTORY_TYPE_MULTI_THREADED constants, but these have nothing to do with COM apartments and don't assume any thread affinity. The D2D1_FACTORY_TYPE_SINGLE_THREADED constant merely specifies that the factory object, as well as any and all objects rooted in that factory, may be accessed by only a single thread at a time. Objects created in this way will provide the best performance for single-threaded access, avoiding any unnecessary serialization into and out of the rendering calls. The D2D1_FACTORY_TYPE_MULTI_THREADED constant, on the other hand, specifies that the factory object, as well as any and all objects rooted in that factory, may be accessed by multiple threads concurrently. Direct2D provides the necessary synchronization with interlocked reference counting. This is useful for rendering to different targets concurrently while sharing certain resources. Keep in mind that this parallelism is specific to the CPU, and instructions sent to the GPU may still be serialized and ultimately parallelized independently.
OK, so what is the factory object used for? Well, it is responsible for creating all device-independent resources, predominantly geometries, as well as creating render targets representing devices. The render target then has the responsibility of creating device-dependent resources. The distinction between device-dependent and device-independent resources is critical for using Direct2D correctly. One of the benefits of a retained-mode graphics API such as WPF is that the intermediate layer between the hardware and the programming model generally takes care of scenarios where the display device may be lost. This may happen for a variety of reasons. The resolution may change in the background, a display adapter may be removed such as when a user undocks a laptop, a remote desktop connection session may be lost, and so on. An immediate-mode graphics API like Direct2D needs to take into account these events. Of course, the benefit is that resources used to render to a particular device may be physically present on the device and thus provide far better performance.
For these reasons, it is good practice to clearly separate the creation of device-independent resources from device-dependent resources. To accommodate this, in Figure 2 I add these two methods to the Window class from Figure 1.
Figure 2 Create Device-Independent and Device-Dependent Resources Separately
HRESULT
CreateDeviceIndependentResources()
{
HRESULT hr;
HR(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED,
&m_factory));
// TODO: Create device-independent
resources here
return S_OK;
}
HRESULT CreateDeviceResources()
{
HRESULT hr;
// TODO: Create device resources
here
return S_OK;
}
I'll flesh them out in the next few sections, but for the time being, you can call CreateDeviceIndependentResources from the Create method of the Window class because device-independent resources need be created only once. CreateDeviceResources will, however, be called on demand to create device resources as needed. You'll also want to add the m_factory member variable as follows:
CComPtr<ID2D1Factory> m_factory;
Render Targets
A render target is used to represent a device and is itself dependent on the underlying device. With a render target, you can create various resources, such as brushes, and perform the actual drawing operations. Direct2D supports a number of different types of render targets. If you are building a Direct2D application from scratch, you might create a render target to render content to a window (HWND). You can also create a render target to render to a GDI device context (DC) or to a DirectX Graphics Infrastructure (DXGI) surface for use in a Direct3D application. You can even create a render target for various types of bitmap resources for off-screen rendering.
Various factory methods are provided for creating the different types of render targets. The methods themselves are pretty obvious. For example, the CreateHwndRenderTarget method creates a window render target, and the CreateDxgiSurfaceRenderTarget method creates a DXGI surface render target. To render to the window, you're going to need to update the CreateDeviceResources method to create the window render target. At the very minimum, it might look something like Figure 3. Keep in mind that this sample is not DPI-aware, but that is a topic for another article.
Figure 3 Create the Window Render Target
HRESULT CreateDeviceResources()
{
HRESULT hr;
if (0 == m_target)
{
CRect rect;
VERIFY(GetClientRect(&rect));
D2D1_SIZE_U size =
D2D1::SizeU(rect.Width(), rect.Height());
HR(m_factory->CreateHwndRenderTarget(D2D1::RenderTargetProperties(),
D2D1::HwndRenderTargetProperties(m_hWnd, size), &m_target));
}
return S_OK;
}
You'll also want to add the m_target member variable:
CComPtr<ID2D1HwndRenderTarget> m_target;
The CreateDeviceResources method creates the device resources only if the render target has not yet been created or, as you will see shortly, if the device is lost and the render target needs to be re-created.
The CreateHwndRenderTarget method's first parameter is actually of type D2D1_RENDER_TARGET_PROPERTIES. This parameter is common to all the render target creation functions. The RenderTargetProperties function is actually a helper function defined in the d2d1helper.h header file. It represents a common pattern for initializing many of the data structures used by Direct2D and helps to simplify your code dramatically. The RenderTargetProperties helper function has parameters with default values to account for the most common initialization of the resulting structure. You can override these values to adjust the pixel format and DPI information, as well as additional characteristics of the render target, such as whether to force hardware- or software-based rendering and whether to support GDI interop.
Likewise, CreateHwndRenderTarget's second parameter is actually a D2D1_HWND_RENDER_TARGET_PROPERTIES structure and provides information specific to the window.
Having created the render target, I can now do some actual rendering. To do this, add a Render method to the Window class with the basic outline shown in Figure 4.
Figure 4 Rendering
void Render()
{
if
(SUCCEEDED(CreateDeviceResources()))
{
if (0 ==
(D2D1_WINDOW_STATE_OCCLUDED & m_target->CheckWindowState()))
{
m_target->BeginDraw();
m_target->SetTransform(D2D1::Matrix3x2F::Identity());
m_target->Clear(D2D1::ColorF(D2D1::ColorF::Red));
// TODO: add drawing code
here
if (D2DERR_RECREATE_TARGET ==
m_target->EndDraw())
{
DiscardDeviceResources();
}
}
}
}
The logic here is quite important because it enables very efficient updating, thanks to the reuse of device resources and because it must anticipate device loss. The Render method starts by calling the CreateDeviceResources method that I've just described. Of course it won't have to do anything if the device resources are still available. As an optimization, I check the window state to ensure that it is not occluded, which is just a fancy term that indicates the window is obstructed from view and any painting would just be a waste of precious CPU and GPU resources. Although this rarely happens when the Desktop Window Manager (DWM) is handling desktop composition, it doesn't hurt and is a nice optimization when desktop composition is disabled.
Actual drawing must be sandwiched between calls to the render target's BeginDraw and EndDraw methods. Most of the render target's methods have a void return type. Since drawing operations are batched, any failures will be detected only when the drawing operations are flushed and rendered to the device. The EndDraw method thus has an HRESULT return type. It uses the D2DERR_RECREATE_TARGET constant to indicate that the render target has been invalidated and must be recreated. It then calls DiscardDeviceResources, which must release all the device resources.
void DiscardDeviceResources()
{
m_target.Release();
}
I should also just mention the calls to the render target's SetTransform and Clear methods. SetTransform in this case specifies that any subsequent drawing operations should be transformed using an identity matrix. Of course, an identity matrix is really no translation at all, so this just assures that drawing operations use the literal coordinate space in device-independent pixels. You can, however, call SetTransform repeatedly during rendering to change the transform on the fly. The Clear method simply clears the drawing area for subsequent drawing operations and in this case makes it red.
Of course, if you were to create the window at this point, you would notice that the window remains white. Something still needs to call the Render method to update the display. For this, you can add message handlers for the WM_PAINT and WM_DISPLAYCHANGE window messages. You can do that by adding the following to the message map:
MSG_WM_PAINT(OnPaint)
MSG_WM_DISPLAYCHANGE(OnDisplayChange)
Responding to WM_PAINT is obvious, but handling WM_DISPLAYCHANGE is equally important to ensure that the window is properly repainted should the display resolution or color depth change (see Figure 5).
Figure 5 Repainting
void OnPaint(CDCHandle /*dc*/)
{
PAINTSTRUCT paint;
VERIFY(BeginPaint(&paint));
Render();
EndPaint(&paint);
}
void OnDisplayChange(UINT /*bpp*/, CSize /*resolution*/)
{
Render();
}
Notice that the DC returned by BeginPaint is not used at all. Creating the window will now result in a window with a red background, as expected. Resizing the window will however, reveal some flickering. What's happening is that the window background is still automatically being cleared using the window class background brush because of the default handling of the WM_ERASEBKGND message. To avoid this, simply handle this message and return TRUE in the OnEraseBackground method to indicate that you will take care of clearing the window background. You can now resize the window while avoiding any flickering.
While you're at it, you will probably want to handle window resizing. To do so, add a message handler for the WM_SIZE window message, as follows:
MSG_WM_SIZE(OnSize)
The OnSize handle then needs to simply inform the render target that the size has changed:
void OnSize(UINT /*type*/, CSize
size)
{
if (0 != m_target)
{
if
(FAILED(m_target->Resize(D2D1::SizeU(size.cx, size.cy))))
{
DiscardDeviceResources();
VERIFY(Invalidate(FALSE));
}
}
}
If it fails to resize, you simply discard the device resources, and they will automatically be recreated the next time the window is rendered. Notice how the Resize method accepts the new size in device pixels since the WM_SIZE message communicates the size of the client area in pixels. Even though Direct2D uses a device-independent coordinate system, the window render target understands that it ultimately needs to map to device pixels.
Brushes And Drawing Commands
To do any meaningful drawing, you'll want to create some brushes. You will need brushes to paint various geometric shapes as well as text. Brushes fall in the category of device-dependent resources, so their creation should go in the CreateDeviceResources method. As you might expect, Direct2D provides solid and bitmap brushes as well as linear and radial gradient brushes. To create a solid color brush, you can add the following method call right after creating the render target. That way, it is re-created only when the render target is recreated.
HR(m_target->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Black), &m_brush));
Remember to also release the brush in the DiscardDeviceResources method as follows:
m_brush.Release();
You'll also want to add the m_brush member variable as follows:
CComPtr<ID2D1SolidColorBrush> m_brush;
Figure 7 Basic Drawing
You can now add some basic drawing commands to the Render method between the Clear and EndDraw methods. The code in Figure 6 produces a circle with a diagonal line through it. Although I'll leave a complete discussion of brushes for a future article, it is worth mentioning that Direct2D allows you to efficiently change the color of a brush while rendering, obviating the need to create multiple brushes for different colors.
Figure 6 Circle with Diagonal Line
m_target->DrawLine(D2D1::Point2(10.0f, 10.0f), //
start
D2D1::Point2(200.0f,
200.0f), // end
m_brush,
10.0f); // stroke
width
const D2D1_POINT_2F center = D2D1::Point2(105.0f, 105.0f);
const D2D1_ELLIPSE ellipse =
D2D1::Ellipse(center,
95.0f, // radius X
95.0f); // radius Y
m_target->DrawEllipse(&ellipse,
m_brush,
5.0f); // stroke
width
As I've mentioned, Direct2D uses a device-independent coordinate system so the coordinates expressed don't necessarily map to display pixels but rather will honor the DPI setting for the target device. Figure 7 shows the result of the drawing commands.
Although I've used only an opaque brush, you can also specify an alpha value when creating the brush and alpha blend to your heart's content.
Unfortunately, I've completely run out of space, but you should now have a good idea of the potential that Direct2D and DirectWrite will have on native applications going forward. There are many more reasons to love these new technologies that I hope to dig into in an upcoming column, including rich geometries and operations as well as all the capabilities of DirectWrite. In the meantime, start experimenting with Direct2D. I'm sure you'll find it impressive.
Insights: Direct2D Rendering
Many people will be tempted to contrast the performance of Direct2D and Direct3D applications. This is something of an apples to oranges comparison in that Direct2D provides a set of primitives much more comparable to other 2D rendering APIs such as GDI/GDI+ or WPF. It also is much simpler to use. However, it might be surprising to some customers that Direct2D can actually out-perform a simple, or naïve, Direct3D application in a number of different scenarios. For example, Direct2D can outperform a naïve Direct3D app when drawing large numbers of lines with different colors or when blending repeatedly from a bitmap.
The key to how Direct2D does this is to realize that Direct2D does not maintain a 1:1 relationship between a drawing request issued to it and the primitives that it issues to Direct3D. In fact, Direct2D tries to aggregate its requests to Direct3D in a number of different ways.
Reduced Vertex Buffer Mapping
Consider a naïve Direct3D application. It creates a vertex buffer, maps it, writes its geometry in the buffer, un-maps it and draws it. The problem with this approach is that if the vertex buffer is still in use by the GPU when the next map request comes in, the CPU will have to stall until the GPU is done with it, and then it can only be mapped back into the application memory. This can dramatically decrease the application's performance.
Direct2D solves this problem by leaving the vertex buffer mapped into memory and accumulating many successive primitives' geometry into it. Only when the vertex buffer is full, or the application calls Flush or EndDraw, does Direct2D then send the draw calls down into Direct3D. This minimizes the number of GPU stalls because the number of map and un-map calls is greatly reduced.
Coalescing Draw Calls
Even after geometries are allowed to accumulate in the vertex buffer, a naïve Direct3D application would have to issue a Draw call to Direct3D for most Direct2D Draw calls. Consider two different rectangles rendered with different colors, each color might require writing different shader constant data to represent it. Each time the shader constant data is updated, a different Draw call needs to be issued.
So long as certain other constants do not change. For example, the same type of brush is used consecutively. Or, if a bitmap brush, the same input bitmap is used consistently. Direct2D can use its vertex shaders to continue to accumulate shader constant data and then issue one Direct3D Draw call for a large number of Direct2D Draw calls.
Out-of-Order Text Rendering
The Direct2D text rendering pipeline is executed entirely in hardware, through three stages. The first stage writes the glyphs into a texture, the next stage down-samples these glyphs and the final stage performs a clear-type filtering operation to transfer the text to the render target. A naïve application would render each stage sequentially, changing the render target three times and issuing a Draw call to move the data between each texture. Direct2D will render the text primitives out of order with other primitives. It will first accumulate a large number of glyphs. It then down-samples all of them and blends the final set of glyphs in order with the other geometric primitives issued to Direct2D. Coalescing the text rendering in this way reduces the number of times the render target changes as well as the number of general Direct3D device state changes in order to render the text, even if other primitives are interleaved with the Direct2D text rendering calls.
By combining vertex buffer batching, draw call coalescing, and out-of-order rendering of text, Direct2D is capable of achieving rendering performance that would require extensive amount of development effort to match if the application were instead directly targeting Direct3D.
--Mark Lawrence, Senior Software Development Engineer, Microsoft.
源文档 <http://msdn.microsoft.com/en-gb/magazine/dd861344.aspx>