使用 Direct2D 进行Server-Side渲染

Direct2D 非常适合要求在 Windows Server 上进行服务器端呈现的图形应用程序。 本概述介绍使用 Direct2D 进行服务器端呈现的基础知识。 它包含以下部分:

Server-Side渲染的要求

下面是图表服务器的典型方案:图表和图形在服务器上呈现,并作为位图传送以响应 Web 请求。 服务器可能配备低端图形卡或根本没有图形卡。

此方案显示了三个应用程序要求。 首先,应用程序必须有效地处理多个并发请求,尤其是在多核服务器上。 其次,在具有低端图形卡或没有图形卡的服务器上运行时,应用程序必须使用软件呈现。 最后,应用程序必须在会话 0 中作为服务运行,以便它不需要用户登录。 有关会话 0 的详细信息,请参阅 会话 0 隔离对 Windows 中的服务和驱动程序的影响

可用 API 的选项

服务器端呈现有三个选项:GDI、GDI+ 和 Direct2D。 与 GDI 和 GDI+ 一样,Direct2D 是一种本机 2D 呈现 API,它使应用程序能够更好地控制图形设备的使用。 此外,Direct2D 唯一支持单线程工厂和多线程工厂。 以下部分比较每个 API 的绘制质量与多线程服务器端呈现。

GDI

与 Direct2D 和 GDI+ 不同,GDI 不支持高质量的绘图功能。 例如,GDI 不支持用于创建平滑线的抗锯齿,并且仅对透明度的有限支持。 根据 Windows 7 和 Windows Server 2008 R2 上的图形性能测试结果,尽管 GDI 中重新设计了锁,但 Direct2D 的缩放效率高于 GDI。 有关这些测试结果的详细信息,请参阅 工程 Windows 7 图形性能

此外,使用 GDI 的应用程序限制为每个进程 10240 GDI 句柄,每个会话 65536 个 GDI 句柄。 原因是 Windows 在内部使用 16 位 WORD 来存储每个会话的句柄索引。

GDI+

虽然 GDI+ 支持对高质量绘图进行抗锯齿和 alpha 混合,但对于服务器方案,GDI+ main问题是它不支持在会话 0 中运行。 由于会话 0 仅支持非交互式功能,因此与显示设备直接或间接交互的函数将收到错误。 函数的特定示例不仅包括处理显示设备的功能,还包括那些间接处理设备驱动程序的功能。

与 GDI 类似,GDI+ 受其锁定机制的限制。 GDI+ 中的锁定机制在 Windows 7 和 Windows Server 2008 R2 中与以前版本中相同。

Direct2D

Direct2D 是一种硬件加速的即时模式二维图形 API,可提供高性能和高质量的渲染。 它提供单线程和多线程工厂,以及粗粒度软件呈现的线性缩放。

为此,Direct2D 定义了根工厂接口。 通常,在工厂上创建的对象只能与从同一工厂创建的其他对象一起使用。 调用方可以在创建时请求单线程工厂或多线程工厂。 如果请求了单线程工厂,则不执行线程锁定。 如果调用方请求多线程工厂,则每当调用 Direct2D 时,将获取工厂范围的线程锁。

此外,Direct2D 中的线程锁定比在 GDI 和 GDI+ 中锁定更精细,因此线程数的增加对性能的影响最小。

如何使用 Direct2D 进行Server-Side渲染

以下部分介绍如何使用软件呈现、如何以最佳方式使用单线程和多线程工厂,以及如何绘制复杂绘图并将其保存到文件中。

软件渲染

服务器端应用程序通过创建 IWICBitmap 呈现目标来使用软件呈现,呈现目标类型设置为 D2D1_RENDER_TARGET_TYPE_SOFTWARE 或 D2D1_RENDER_TARGET_TYPE_DEFAULT。 有关 IWICBitmap 呈现目标的详细信息,请参阅 ID2D1Factory::CreateWicBitmapRenderTarget 方法;有关呈现器目标类型的详细信息,请参阅 D2D1_RENDER_TARGET_TYPE

多线程处理

了解如何跨线程创建和共享工厂和呈现目标可能会显著影响应用程序的性能。 以下三个图显示了三种不同的方法。 最佳方法如图 3 所示。

具有单个呈现目标的 direct2d 多线程关系图。

在图 1 中,不同的线程共享相同的工厂和相同的呈现目标。 当多个线程同时更改共享呈现目标的状态(例如同时设置转换矩阵)时,此方法可能会导致不可预知的结果。 由于 Direct2D 中的内部锁定不会同步共享资源(如呈现器目标),此方法可能会导致 BeginDraw 调用在线程 1 中失败,因为在线程 2 中, BeginDraw 调用已在使用共享呈现器目标。

具有多个呈现目标的 direct2d 多线程关系图。

为了避免在图 1 中遇到不可预知的结果,图 2 显示了一个多线程工厂,每个线程都有自己的呈现目标。 此方法虽然有效,但它实际上充当单线程应用程序。 原因是工厂范围的锁仅适用于绘图操作级别,因此同一工厂中的所有绘图调用都会被序列化。 因此,尝试进入绘图调用时线程 1 被阻止,而线程 2 正在执行另一个绘图调用。

具有多个工厂和多个呈现目标的 direct2d 多线程关系图。

图 3 显示了使用单线程工厂和单线程呈现目标的最佳方法。 由于在使用单线程工厂时不执行锁定,因此每个线程中的绘图操作可以并发运行以实现最佳性能。

生成位图文件

若要使用软件呈现生成位图文件,请使用 IWICBitmap 呈现目标。 使用 IWICStream 将位图写入文件。 使用 IWICBitmapFrameEncode 将位图编码为指定的图像格式。 下面的代码示例演示如何绘制下图并将其保存到文件中。

示例输出图像。

此代码示例首先创建 IWICBitmapIWICBitmap 呈现目标。 然后,它将一个包含一些文本的绘图、一个表示小时玻璃的路径几何图形和一个转换后的小时玻璃呈现为 WIC 位图。 然后,它使用 IWICStream::InitializeFromFilename 将位图保存到文件。 如果应用程序需要将位图保存在内存中,请改用 IWICStream::InitializeFromMemory 。 最后,它使用 IWICBitmapFrameEncode 对位图进行编码。

// Create an IWICBitmap and RT
static const UINT sc_bitmapWidth = 640;
static const UINT sc_bitmapHeight = 480;
if (SUCCEEDED(hr))
{
    hr = pWICFactory->CreateBitmap(
        sc_bitmapWidth,
        sc_bitmapHeight,
        GUID_WICPixelFormat32bppBGR,
        WICBitmapCacheOnLoad,
        &pWICBitmap
        );
}

// Set the render target type to D2D1_RENDER_TARGET_TYPE_DEFAULT to use software rendering.
if (SUCCEEDED(hr))
{
    hr = pD2DFactory->CreateWicBitmapRenderTarget(
        pWICBitmap,
        D2D1::RenderTargetProperties(),
        &pRT
        );
}

// Create text format and a path geometry representing an hour glass. 
if (SUCCEEDED(hr))
{
    static const WCHAR sc_fontName[] = L"Calibri";
    static const FLOAT sc_fontSize = 50;

    hr = pDWriteFactory->CreateTextFormat(
        sc_fontName,
        NULL,
        DWRITE_FONT_WEIGHT_NORMAL,
        DWRITE_FONT_STYLE_NORMAL,
        DWRITE_FONT_STRETCH_NORMAL,
        sc_fontSize,
        L"", //locale
        &pTextFormat
        );
}
if (SUCCEEDED(hr))
{
    pTextFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER);
    pTextFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER);
    hr = pD2DFactory->CreatePathGeometry(&pPathGeometry);
}
if (SUCCEEDED(hr))
{
    hr = pPathGeometry->Open(&pSink);
}
if (SUCCEEDED(hr))
{
    pSink->SetFillMode(D2D1_FILL_MODE_ALTERNATE);

    pSink->BeginFigure(
        D2D1::Point2F(0, 0),
        D2D1_FIGURE_BEGIN_FILLED
        );

    pSink->AddLine(D2D1::Point2F(200, 0));

    pSink->AddBezier(
        D2D1::BezierSegment(
            D2D1::Point2F(150, 50),
            D2D1::Point2F(150, 150),
            D2D1::Point2F(200, 200))
        );

    pSink->AddLine(D2D1::Point2F(0, 200));

    pSink->AddBezier(
        D2D1::BezierSegment(
            D2D1::Point2F(50, 150),
            D2D1::Point2F(50, 50),
            D2D1::Point2F(0, 0))
        );

    pSink->EndFigure(D2D1_FIGURE_END_CLOSED);

    hr = pSink->Close();
}
if (SUCCEEDED(hr))
{
    static const D2D1_GRADIENT_STOP stops[] =
    {
        {   0.f,  { 0.f, 1.f, 1.f, 1.f }  },
        {   1.f,  { 0.f, 0.f, 1.f, 1.f }  },
    };

    hr = pRT->CreateGradientStopCollection(
        stops,
        ARRAYSIZE(stops),
        &pGradientStops
        );
}
if (SUCCEEDED(hr))
{
    hr = pRT->CreateLinearGradientBrush(
        D2D1::LinearGradientBrushProperties(
            D2D1::Point2F(100, 0),
            D2D1::Point2F(100, 200)),
        D2D1::BrushProperties(),
        pGradientStops,
        &pLGBrush
        );
}
if (SUCCEEDED(hr))
{
    hr = pRT->CreateSolidColorBrush(
        D2D1::ColorF(D2D1::ColorF::Black),
        &pBlackBrush
        );
}
if (SUCCEEDED(hr))
{
    // Render into the bitmap.
    pRT->BeginDraw();
    pRT->Clear(D2D1::ColorF(D2D1::ColorF::White));
    D2D1_SIZE_F rtSize = pRT->GetSize();

    // Set the world transform to a 45 degree rotation at the center of the render target
    // and write "Hello, World".
    pRT->SetTransform(
        D2D1::Matrix3x2F::Rotation(
            45,
            D2D1::Point2F(
                rtSize.width / 2,
                rtSize.height / 2))
            );

    static const WCHAR sc_helloWorld[] = L"Hello, World!";
    pRT->DrawText(
        sc_helloWorld,
        ARRAYSIZE(sc_helloWorld) - 1,
        pTextFormat,
        D2D1::RectF(0, 0, rtSize.width, rtSize.height),
        pBlackBrush);

    // Reset back to the identity transform.
    pRT->SetTransform(D2D1::Matrix3x2F::Translation(0, rtSize.height - 200));
    pRT->FillGeometry(pPathGeometry, pLGBrush);
    pRT->SetTransform(D2D1::Matrix3x2F::Translation(rtSize.width - 200, 0));
    pRT->FillGeometry(pPathGeometry, pLGBrush);
    hr = pRT->EndDraw();
}

if (SUCCEEDED(hr))
{
    // Save the image to a file.
    hr = pWICFactory->CreateStream(&pStream);
}

WICPixelFormatGUID format = GUID_WICPixelFormatDontCare;

// Use InitializeFromFilename to write to a file. If there is need to write inside the memory, use InitializeFromMemory. 
if (SUCCEEDED(hr))
{
    static const WCHAR filename[] = L"output.png";
    hr = pStream->InitializeFromFilename(filename, GENERIC_WRITE);
}
if (SUCCEEDED(hr))
{
    hr = pWICFactory->CreateEncoder(GUID_ContainerFormatPng, NULL, &pEncoder);
}
if (SUCCEEDED(hr))
{
    hr = pEncoder->Initialize(pStream, WICBitmapEncoderNoCache);
}
if (SUCCEEDED(hr))
{
    hr = pEncoder->CreateNewFrame(&pFrameEncode, NULL);
}
// Use IWICBitmapFrameEncode to encode the bitmap into the picture format you want.
if (SUCCEEDED(hr))
{
    hr = pFrameEncode->Initialize(NULL);
}
if (SUCCEEDED(hr))
{
    hr = pFrameEncode->SetSize(sc_bitmapWidth, sc_bitmapHeight);
}
if (SUCCEEDED(hr))
{
    hr = pFrameEncode->SetPixelFormat(&format);
}
if (SUCCEEDED(hr))
{
    hr = pFrameEncode->WriteSource(pWICBitmap, NULL);
}
if (SUCCEEDED(hr))
{
    hr = pFrameEncode->Commit();
}
if (SUCCEEDED(hr))
{
    hr = pEncoder->Commit();
}

结论

如上所示,使用 Direct2D 进行服务器端呈现简单明了。 此外,它还提供高质量且高度可并行化呈现,可在服务器的低特权环境中运行。

Direct2D 参考