提高 Direct2D 应用的性能

尽管 Direct2D 是硬件加速的,旨在实现高性能,但必须正确使用这些功能才能最大化吞吐量。 我们在此处展示的技术派生自研究常见方案,可能不适用于所有应用方案。 因此,仔细了解应用行为和性能目标有助于实现所需的结果。

资源使用情况

资源是视频或系统内存中的某种分配。 位图和画笔是资源的示例。

在 Direct2D 中,可以在软件和硬件中创建资源。 在硬件上创建和删除资源是昂贵的操作,因为它们需要大量开销来与视频卡通信。 让我们看看 Direct2D 如何将内容呈现到目标。

在 Direct2D 中,所有呈现命令都包含在对 BeginDraw 的调用和 对 EndDraw 的调用之间。 这些调用对呈现器目标进行。 在调用呈现操作 之前,必须调用 BeginDraw 方法。 调用 BeginDraw 后,上下文通常会生成一批呈现命令,但会延迟这些命令的处理,直到以下语句之一成立:

  • EndDraw 发生。 调用 EndDraw 时,它会导致任何批处理的绘图操作完成,并返回操作的状态。
  • 显式调用 FlushFlush 方法会导致处理批处理并发出所有挂起的命令。
  • 包含呈现命令的缓冲区已满。 如果在满足前两个条件之前此缓冲区已满,则会刷新呈现命令。

在刷新基元之前,Direct2D 会保留对相应资源(如位图和画笔)的内部引用。

重复使用资源

如前所述,资源创建和删除在硬件上成本高昂。 因此,尽可能重复使用资源。 以游戏开发中的位图创建为例。 通常,在游戏中构成场景的位图都是同时创建的,其中包含以后帧到帧渲染所需的所有不同变体。 在实际场景呈现和重新呈现时,将重复使用这些位图,而不是重新创建。

注意

不能将资源用于窗口调整大小操作。 调整窗口大小时,必须重新创建一些依赖于缩放的资源(例如兼容的呈现目标和可能的某些层资源),因为必须重绘窗口内容。 这一点对于保持渲染场景的整体质量非常重要。

 

限制刷新的使用

由于 Flush 方法会导致处理批处理的呈现命令,因此建议不要使用它。 对于最常见的方案,请将资源管理留给 Direct2D。

位图

如前所述,资源创建和删除是硬件中非常昂贵的操作。 位图是一种经常使用的资源。 在视频卡上创建位图的成本很高。 重用它们有助于加快应用程序的速度。

创建大型位图

视频卡通常具有最小内存分配大小。 如果请求的分配小于此大小,则会分配此最小大小的资源,并且将浪费多余的内存,并且无法用于其他内容。 如果需要许多小位图,更好的方法是分配一个大位图,并将所有小位图内容存储在此大位图中。 然后,可以在需要较小位图的位置读取较大位图的子区域。 通常,应在小位图之间) 填充 (透明黑色像素,以避免在操作期间较小的图像之间发生任何交互。 这也称为 atlas,它的优点是减少了位图创建开销和小位图分配的内存浪费。 建议将大多数位图保留为至少 64 KB,并限制小于 4 KB 的位图数。

创建位图图集

有一些常见的方案,位图图集可以很好地服务。 小位图可以存储在大位图中。 如果需要,可以通过指定目标矩形将这些小位图从较大的位图中提取出来。 例如,应用程序必须绘制多个图标。 与图标关联的所有位图都可以预先加载到大型位图中。 在呈现时,可以从大位图中检索它们。

注意

在视频内存中创建的 Direct2D 位图限制为存储该位图的适配器支持的最大位图大小。 创建大于的位图可能会导致错误。

 

注意

从 Windows 8 开始,Direct2D 包括一个 Atlas 效果,可使此过程更简单。

 

创建共享位图

创建共享位图使高级调用方能够创建由现有对象直接支持的 Direct2D 位图对象,这些对象已与呈现器目标兼容。 这可避免创建多个图面,并有助于减少性能开销。

注意

共享位图通常仅限于软件目标或与 DXGI 互操作的目标。 使用 CreateBitmapFromDxgiSurfaceCreateBitmapFromWicBitmapCreateSharedBitmap 方法创建共享位图。

 

复制位图

创建 DXGI 图面是一项成本高昂的操作,因此可以重复使用现有图面。 即使在软件中,如果位图主要采用所需形式(少数部分除外),最好更新该部分,而不是将整个位图扔掉并重新创建所有内容。 尽管可以使用 CreateCompatibleRenderTarget 实现相同的结果,但呈现通常比复制操作成本高得多。 这是因为,为了改进缓存位置,硬件实际上不会按照位图寻址的相同内存顺序存储位图。 位图可能会被重排。 重排隐藏在 CPU 中,驱动程序 (速度较慢,仅在) 低端部件上使用,或者由 GPU 上的内存管理器使用。 由于呈现时数据写入呈现器目标的方式受到约束,呈现目标通常不是重排,就是以低于最佳方式(如果知道永远不必渲染到图面)实现的方式重排。 因此, CopyFrom* 方法用于将矩形从源复制到 Direct2D 位图。

CopyFrom 可以采用其三种形式中的任何一种使用:

对虚线使用平铺位图

呈现虚线是一项非常昂贵的操作,因为基础算法的质量和准确性很高。 对于大多数不涉及直线几何图形的情况,使用平铺位图可以更快地生成相同的效果。

呈现复杂静态内容的一般准则

如果在帧上呈现相同的内容帧,则缓存内容,尤其是在场景很复杂时。

可以使用三种缓存技术:

  • 使用颜色位图进行全场景缓存。
  • 使用 A8 位图和 FillOpacityMask 方法按基元缓存。
  • 使用几何实现的每个基元缓存。

让我们更详细地了解其中的每一个。

使用颜色位图进行全场景缓存

呈现静态内容时,在动画等场景中,请创建另一个全彩色位图,而不是直接写入屏幕位图。 保存当前目标,将目标设置为中间位图,并呈现静态内容。 然后,切换回原始屏幕位图并在其中绘制中间位图。

下面是一个示例:

// Create a bitmap.
m_d2dContext->CreateBitmap(size, nullptr, 0,
    D2D1::BitmapProperties(
        D2D1_BITMAP_OPTIONS_TARGET,
        D2D1::PixelFormat(
            DXGI_FORMAT_B8G8R8A8_UNORM,
            D2D1_ALPHA_MODE_PREMULTIPLIED),
        dpiX, dpiY),
    &sceneBitmap);

// Preserve the pre-existing target.
ComPtr<ID2D1Image> oldTarget;
m_d2dContext->GetTarget(&oldTarget);

// Render static content to the sceneBitmap.
m_d2dContext->SetTarget(sceneBitmap.Get());
m_d2dContext->BeginDraw();
…
m_d2dContext->EndDraw();

// Render sceneBitmap to oldTarget.
m_d2dContext->SetTarget(oldTarget.Get());
m_d2dContext->DrawBitmap(sceneBitmap.Get());

此示例使用中间位图进行缓存,并切换设备上下文在呈现时指向的位图。 这样就无需为同一目的创建兼容的呈现目标。

使用 A8 位图和 FillOpacityMask 方法按基元缓存

当整个场景不是静态的,但由几何图形或静态文本等元素组成时,可以使用每个基元缓存技术。 此方法保留要缓存的基元的抗锯齿特征,并可用于更改画笔类型。 它使用 A8 位图,其中 A8 是一种表示 8 位的 alpha 通道的像素格式。 A8 位图可用于将几何图形/文本绘制为掩码。 当必须操作静态内容 的不透明度时,可以平移、旋转、倾斜或缩放掩码的不透明度,而不是操作内容本身。

下面是一个示例:

// Create an opacity bitmap.
m_d2dContext->CreateBitmap(size, nullptr, 0,
    D2D1::BitmapProperties(
        D2D1_BITMAP_OPTIONS_TARGET,
        D2D1::PixelFormat(
            DXGI_FORMAT_A8_UNORM,
            D2D1_ALPHA_MODE_PREMULTIPLIED),
        dpiX, dpiY),
    &opacityBitmap);

// Preserve the pre-existing target.
ComPtr<ID2D1Image> oldTarget;
m_d2dContext->GetTarget(&oldTarget);

// Render to the opacityBitmap.
m_d2dContext->SetTarget(opacityBitmap.Get());
m_d2dContext->BeginDraw();
…
m_d2dContext->EndDraw();

// Call the FillOpacityMask method
// Note: for this call to work correctly the anti alias mode must be D2D1_ANTIALIAS_MODE_ALIASED. 
m_d2dContext->SetTarget(oldTarget.Get());
m_d2dContext->FillOpacityMask(
    opacityBitmap.Get(),
    m_contentBrush().Get(),
    D2D1_OPACITY_MASK_CONTENT_GRAPHICS);

使用几何实现的每个基元缓存

另一种称为几何实现的按基元缓存技术在处理几何图形时提供了更大的灵活性。 如果要重复绘制别名或抗锯齿几何图形,将其转换为几何实现并重复绘制实现比重复绘制几何图形本身更快。 几何实现通常也比不透明掩码消耗更少的内存, (尤其是大型几何) ,并且它们对比例变化的敏感度较低。 有关详细信息,请参阅 Geometry 实现概述

下面是一个示例:

    // Compute a flattening tolerance based on the scales at which the realization will be used.
    float flatteningTolerance = D2D1::ComputeFlatteningTolerance(...);

    ComPtr<ID2D1GeometryRealization> geometryRealization;

    // Create realization of the filled interior of the geometry.
    m_d2dDeviceContext1->CreateFilledGeometryRealization(
        geometry.Get(),
        flatteningTolerance,
        &geometryRealization
        );

    // In your app's rendering code, draw the geometry realization with a brush.
    m_d2dDeviceContext1->BeginDraw();
    m_d2dDeviceContext1->DrawGeometryRealization(
        geometryRealization.Get(),
        m_brush.Get()
        );
    m_d2dDeviceContext1->EndDraw();

几何图形呈现

对绘图几何图形使用特定的绘图基元

对泛型 DrawGeometry 调用使用更具体的绘图基元调用,如 DrawRectangle。 这是因为使用 DrawRectangle 时,几何图形已知,因此渲染速度更快。

呈现静态几何图形

如果几何图形是静态的,请使用上面介绍的每个基元缓存技术。 不透明度掩码和几何实现可以大大提高包含静态几何图形的场景的呈现速度。

使用多线程设备上下文

预期呈现大量复杂几何内容的应用程序应考虑在创建 Direct2D 设备上下文时指定D2D1_DEVICE_CONTEXT_OPTIONS_ENABLE_MULTI_THREADED_OPTIMIZATIONS标志。 指定此标志后,Direct2D 将跨系统上存在的所有逻辑核心分布渲染,这可以显著减少总体呈现时间。

注意:

  • 截至Windows 8.1,此标志仅影响路径几何图形呈现。 它不会影响仅包含其他基元类型 (场景,例如文本、位图或几何图形实现) 。
  • 当在软件 ((即使用 WARP Direct3D 设备) 进行渲染时)中呈现时,此标志也没有任何影响。 若要控制软件多线程,调用方在创建 WARP Direct3D 设备时应使用 D3D11_CREATE_DEVICE_PREVENT_INTERNAL_THREADING_OPTIMIZATIONS 标志。
  • 指定此标志可能会增加呈现过程中的峰值工作集,还可能会增加已利用多线程处理的应用程序中的线程争用。

使用 Direct2D 绘制文本

Direct2D 文本呈现功能分为两个部分。 第一部分公开为 ID2D1RenderTarget::D rawTextID2D1RenderTarget::D rawTextLayout 方法,使调用方能够传递字符串和格式参数或 DWrite 文本布局对象以使用多种格式。 这应该适合大多数调用方。 呈现文本的第二种方法(以 ID2D1RenderTarget::D rawGlyphRun 方法公开)为已经知道要呈现的字形位置的客户提供光栅化。 在 Direct2D 中绘制时,以下两个常规规则可帮助提高文本性能。

DrawTextLayout 与 DrawText

使用 DrawTextDrawTextLayout,应用程序可以轻松呈现由DirectWrite API 设置格式的文本。 DrawTextLayout 将现有 DWriteTextLayout 对象绘制到 RenderTargetDrawText 根据传入的参数为调用方构造DirectWrite布局。 如果必须多次呈现相同的文本,请使用 DrawTextLayout 而不是 DrawText,因为 每次调用 DrawText 都会创建布局。

选择正确的文本呈现模式

将文本抗锯齿模式设置为显式 D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE 。 呈现灰度文本的质量与 ClearType 相当,但速度要快得多。

缓存

使用完整场景或按基元位图缓存,就像绘制其他基元一样。

剪裁任意形状

此处的图显示了将剪辑应用于图像的结果。

一个图像,该图像显示剪辑前后的图像示例。

可以通过使用带几何图形掩码的层或带有不透明度画笔的 FillGeometry 方法来获取此结果。

下面是使用层的示例:

// Call PushLayer() and pass in the clipping geometry.
m_d2dContext->PushLayer(
    D2D1::LayerParameters(
        boundsRect,
        geometricMask));

下面是使用 FillGeometry 方法的示例:

// Create an opacity bitmap and render content.
m_d2dContext->CreateBitmap(size, nullptr, 0,
    D2D1::BitmapProperties(
        D2D1_BITMAP_OPTIONS_TARGET,
        D2D1::PixelFormat(
            DXGI_FORMAT_A8_UNORM,
            D2D1_ALPHA_MODE_PREMULTIPLIED),
        dpiX, dpiY),
    &opacityBitmap);

m_d2dContext->SetTarget(opacityBitmap.Get());
m_d2dContext->BeginDraw();
…
m_d2dContext->EndDraw();

// Create an opacity brush from the opacity bitmap.
m_d2dContext->CreateBitmapBrush(opacityBitmap.Get(),
    D2D1::BitmapBrushProperties(),
    D2D1::BrushProperties(),
    &bitmapBrush);

// Call the FillGeometry method and pass in the clip geometry and the opacity brush
m_d2dContext->FillGeometry( 
    clipGeometry.Get(),
    brush.Get(),
    opacityBrush.Get()); 

在此代码示例中,调用 PushLayer 方法时,不会传入应用创建的层。 Direct2D 会为你创建一个层。 Direct2D 能够管理此资源的分配和销毁,而无需应用进行任何参与。 这允许 Direct2D 在内部重复使用层并应用资源管理优化。

在Windows 8对层的使用进行了许多优化,建议尽可能尝试使用层 API 而不是 FillGeometry

Windows 8 中的 PushLayer

ID2D1DeviceContext 接口派生自 ID2D1RenderTarget 接口,是Windows 8中显示 Direct2D 内容的关键,有关此接口的详细信息,请参阅设备和设备上下文。 使用设备上下文接口,可以跳过调用 CreateLayer 方法,然后将 NULL 传递给 ID2D1DeviceContext::P ushLayer 方法。 Direct2D 自动管理层资源,并且可以在层和效果图之间共享资源。

轴对齐剪辑

如果要剪裁的区域与绘图图面的轴对齐,而不是任意对齐。 这种情况适用于使用剪辑矩形而不是层。 性能提升更适用于别名几何图形,而不是抗锯齿几何图形。 有关轴对齐剪辑的详细信息,请参阅 PushAxisAlignedClip 主题。

DXGI 互操作性:避免频繁切换

Direct2D 可与 Direct3D 表面无缝互操作。 这对于创建呈现 2D 和 3D 内容混合的应用程序非常有用。 但是,每次在绘制 Direct2D 和 Direct3D 内容之间切换都会影响性能。

呈现到 DXGI 图面时,Direct2D 会在呈现时保存 Direct3D 设备的状态,并在渲染完成后将其还原。 每次完成一批 Direct2D 呈现时,都会支付此保存和还原的成本以及刷新所有 2D 操作的成本,但不会刷新 Direct3D 设备。 因此,若要提高性能,请限制 Direct2D 和 Direct3D 之间的呈现开关数。

了解像素格式

创建呈现器目标时,可以使用 D2D1_PIXEL_FORMAT 结构指定呈现器目标使用的像素格式和 alpha 模式。 alpha 通道是指定覆盖率值或不透明度信息的像素格式的一部分。 如果呈现器目标不使用 alpha 通道,则应使用 D2D1_ALPHA_MODE_IGNORE alpha 模式创建它。 这可以节省在呈现不需要的 alpha 通道上花费的时间。

有关像素格式和 alpha 模式的详细信息,请参阅 支持的像素格式和 Alpha 模式

场景复杂性

在分析将要呈现的场景中的性能热点时,了解场景是填充速率绑定还是顶点绑定可以提供有用的信息。

  • 填充速率:填充速率是指图形卡每秒可以呈现和写入视频内存的像素数。
  • 顶点绑定:当场景包含大量复杂几何图形时,它是顶点绑定的。

了解场景复杂性

可以通过更改呈现目标的大小来分析场景复杂性。 如果呈现器目标大小成比例减小而可见性能提升,则应用程序受填充速率限制。 否则,场景复杂性是性能瓶颈。

当场景受填充速率限制时,减小呈现目标的大小可以提高性能。 这是因为要呈现的像素数将与呈现目标的大小成比例减少。

当场景处于顶点绑定状态时,降低几何图形的复杂性。 但请记住,这是以牺牲图像质量为代价的。 因此,应在所需的质量和所需的性能之间做出谨慎的权衡决定。

提高 Direct2D 打印应用的性能

Direct2D 提供与打印的兼容性。 如果不知道要绘制到哪些设备,或者绘图如何转换为打印,则可以将相同的 Direct2D 绘图命令 (Direct2D 命令列表) 发送到 Direct2D 打印控件进行打印。

可以进一步微调他们对 Direct2D 打印控件和 Direct2D 绘图命令的使用情况,以提供性能更好的打印结果。

Direct2D 打印控件在看到导致打印质量或性能降低 (的 Direct2D 代码模式时输出调试消息,例如本主题后面列出的代码模式) 提醒你在哪里可以避免性能问题。 若要查看这些调试消息,需要在代码中启用 Direct2D 调试层 。 有关启用 调试消息 输出的说明,请参阅调试消息。

创建 D2D 打印控件时设置正确的属性值

创建 Direct2D 打印控件时,可以设置三个属性。 其中两个属性会影响 Direct2D 打印控件处理某些 Direct2D 命令的方式,进而影响整体性能。

  • 字体子集模式: Direct2D 打印控件在发送要打印的页面之前,将每个页面使用的字体资源子集。 此模式可减小打印所需的页面资源的大小。 根据页面上的字体使用情况,可以选择不同的字体子集模式来获得最佳性能。
    • D2D1_PRINT_FONT_SUBSET_MODE_DEFAULT 在大多数情况下提供最佳打印性能。 设置为此模式时, Direct2D 打印控件使用启发式策略来决定何时对字体进行子集。
    • 对于包含 1 或 2 页的简短打印作业,建议 D2D1_PRINT_FONT_SUBSET_MODE_EACHPAGE ,其中 Direct2D 打印控制子集并在每页中嵌入字体资源,然后在页面打印后放弃该字体子集。 此选项可确保每页在生成后立即打印,但略微增加打印 (所需的页面资源大小,) 通常字体子集较大。
    • 对于具有多页文本和小字号 (如 100 页使用单个字体) 的文本的打印作业,建议 D2D1_PRINT_FONT_SUBSET_MODE_NONE,其中 Direct2D 打印控件根本不包含字体资源;相反,它会发送原始字体资源以及第一个使用该字体的页面,并为以后的页面重新使用字体资源,而不重新发送它们。
  • 光栅化 DPI:当 Direct2D 打印控件需要在 Direct2D-XPS 转换期间对 Direct2D 命令进行光栅化时,它将使用此 DPI 进行光栅化。 换句话说,如果页面没有任何光栅化内容,则设置任何 DPI 都不会更改性能和质量。 根据页面上的光栅化使用情况,可以选择不同的光栅化 DAPI,以便在保真度和性能之间实现最佳平衡。
    • 如果在创建 Direct2D 打印控件时未指定值,则 150 是默认值,在大多数情况下,这是打印质量和打印性能的最佳平衡。
    • 较高的 DPI 值通常会导致更好的打印质量 (因为保留) 但性能较低,因为它生成的位图较大。 我们不建议任何大于 300 的 DPI 值,因为这不会引入人眼在视觉上可感知的额外信息。
    • 较低的 DPI 可能意味着更好的性能,但也可能产生较低的质量。

避免使用某些 Direct2D 绘图模式

Direct2D 可以直观表示的内容与打印子系统可以在整个打印管道中维护和传输的内容之间存在差异。 Direct2D 打印控件通过近似或光栅化打印子系统本身不支持的 Direct2D 基元来弥补这些间隙。 这种近似值通常会导致打印保真度降低、打印性能降低或两者兼而有之。 因此,即使客户可以使用相同的绘图模式进行屏幕和打印呈现,也并非在所有情况下都是理想的。 最好不要为打印路径尽可能多地使用此类 Direct2D 基元和模式,或者自行进行光栅化,因为可以完全控制光栅化图像的质量和大小。

下面是打印性能和质量不理想的情况列表,可能需要考虑改变代码路径以获得最佳打印性能。

以直接和简单的方式绘制文本

Direct2D 在呈现要显示的文本时进行了多项优化,以提高性能和/或更好的视觉质量。 但并非所有优化都会提高打印性能和质量,因为纸上打印的 DPI 通常要高得多,并且打印不需要适应动画等场景。 因此,建议在创建用于打印的命令列表时,直接绘制原始文本或字形,并避免以下任何优化。

  • 避免使用 FillOpacityMask 方法绘制文本。
  • 避免在别名模式下绘制文本。

尽可能绘制原始位图

如果目标位图是 JPEG、PNG、TIFF 或 JPEG-XR,则可以从磁盘文件或内存中流创建 WIC 位图,然后使用 ID2D1DeviceContext::CreateBitmapFromWicBitmap 从该 WIC 位图创建 Direct2D 位图,最后直接将该 Direct2D 位图传递到 Direct2D 打印控件,而无需进一步操作。 通过这样做,Direct2D 打印控件能够重用位图流,这通常通过跳过冗余位图编码和解码) 来提高打印性能 (,并在位图中保留元数据(如颜色配置文件)时 (更好的打印质量) 。

绘制原始位图可为应用程序提供以下优势。

  • 通常, Direct2D 打印会保留原始信息 (且不会丢失或产生噪音) ,直到管道后期,尤其是当应用不知道 (或不想知道打印管道的详细信息) (,例如打印到哪个打印机、DPI 是目标打印机等) 。
  • 在许多情况下,延迟位图光栅化意味着更好的性能 (例如将 96dpi 照片打印到 600dpi 打印机) 。
  • 在某些情况下,传递原始图像是实现高保真度 ((如嵌入颜色配置文件) )的唯一方法。

但是,由于以下原因,无法选择进行此类优化:

  • 通过查询打印机信息和早期光栅化,可以完全控制纸张上的最终外观,自行对内容进行光栅化。
  • 在某些情况下,早期光栅化实际上可以提高应用的端到端性能, (例如) 打印电子钱包大小的照片。
  • 在某些情况下,传递原始位图需要对现有代码体系结构进行重大更改 (例如图像延迟加载和资源更新路径,这些路径在某些应用程序中) 。

结论

尽管 Direct2D 是硬件加速的,旨在实现高性能,但必须正确使用这些功能才能最大程度地提高吞吐量。 我们在此处查看的技术派生自研究常见方案,可能不适用于所有应用程序方案。