准确分析 Direct3D API 调用 (Direct3D 9)

拥有正常运行的 Microsoft Direct3D 应用程序并想要提高其性能后,通常会使用现成的分析工具或某种自定义度量技术来测量执行一个或多个应用程序编程接口 (API) 调用所需的时间。 如果已执行此操作,但获取的计时结果因呈现序列而异,或者您所做的假设不符合实际试验结果,则以下信息可能有助于了解原因。

此处提供的信息基于以下假设:你具备以下方面的知识和经验:

  • C/C++ 编程
  • Direct3D API 编程
  • 测量 API 计时
  • 视频卡及其软件驱动程序
  • 以前的分析体验中可能无法解释的结果

难以准确分析 Direct3D

探查器报告每次 API 调用所用的时间。 这样做是为了通过查找和优化热点来提高性能。 有不同类型的探查器和分析技术。

  • 采样探查器大部分时间处于空闲状态,在特定时间间隔唤醒以采样 (或记录正在执行的函数) 。 它返回每次调用花费的时间百分比。 通常,采样探查器对应用程序不是非常侵入性的,并且对应用程序的开销影响最小。
  • 检测探查器测量调用返回的实际时间。 它需要将启动分隔符和停止分隔符编译到应用程序中。 与采样探查器相比,检测探查器对应用程序的侵入性相对较高。
  • 还可以将自定义分析技术与高性能计时器配合使用。 这会产生非常类似于检测探查器的结果。

使用的探查器类型或分析技术只是生成准确度量的一部分挑战。

分析提供了有助于预算性能的答案。 例如,假设你知道 API 调用平均执行一千个时钟周期。 可以断言有关性能的一些结论,如下所示:

  • 2 GHz CPU (将 50% 的时间用于呈现) ,限制为每秒调用此 API 100 万次。
  • 若要实现每秒 30 帧,则每个帧调用此 API 的次数不能超过 33,000 次。
  • 假设每个对象的呈现序列 () 有 10 个 API 调用,则每个帧只能呈现 3.3K 个对象。

换句话说,如果每次 API 调用有足够的时间,则可以回答预算问题,例如可以交互呈现的基元数。 但检测探查器返回的原始数字无法准确回答预算问题。 这是因为图形管道存在复杂的设计问题,例如需要执行工作的组件数、控制组件之间工作流的处理器数,以及在运行时和旨在提高管道效率的驱动程序中实现的优化策略。

每个 API 调用都经过多个组件

在从应用程序到视频卡时,每个调用都由多个组件处理。 例如,请考虑以下呈现序列,其中包含用于绘制单个三角形的两个调用:

SetTexture(...);
DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1);

以下概念图显示了调用必须通过的不同组件。

API 调用经过的图形组件的示意图

应用程序调用 Direct3D,该 Direct3D 控制场景、处理用户交互并确定呈现方式。 所有这些工作都在呈现序列中指定,该序列使用 Direct3D API 调用发送到运行时。 呈现序列几乎与硬件无关 (也就是说,API 调用独立于硬件,但应用程序知道视频卡支持哪些功能) 。

运行时将这些调用转换为与设备无关的格式。 运行时处理应用程序与驱动程序之间的所有通信,以便应用程序将在多个兼容的硬件 (上运行,具体取决于) 所需的功能。 测量函数调用时,检测探查器测量它在函数中花费的时间以及函数返回的时间。 检测探查器的一个限制是,它可能不包括驱动程序将结果工作发送到视频卡所花费的时间,也不包括视频卡处理工作所需的时间。 换句话说,现成的检测探查器无法将与每个函数调用关联的所有工作属性。

软件驱动程序使用有关视频卡的硬件特定知识将独立于设备的命令转换为一系列视频卡命令。 驱动程序还可以优化发送到视频卡的命令序列,以便有效地对视频卡进行渲染。 这些优化可能会导致分析问题,因为完成的工作量似乎不是 (可能需要了解优化来解释它们) 。 驱动程序通常在视频卡处理完所有命令之前将控制权返回到运行时。

视频卡通过组合来自顶点和索引缓冲区、纹理、呈现状态信息和图形命令的数据来执行大部分渲染。 当视频卡完成呈现时,从呈现序列创建的工作将完成。

每个 Direct3D API 调用必须由每个组件处理, (运行时、驱动程序和视频卡) 呈现任何内容。

有多个处理器控制组件

这些组件之间的关系更加复杂,因为应用程序、运行时和驱动程序由一个处理器控制,视频卡由单独的处理器控制。 下图显示了两种类型的处理器: (CPU) 的中央处理单元和 (GPU) 的图形处理单元。

CPU 和 GPU 及其组件的示意图

电脑系统至少有一个 CPU 和一个 GPU,但可以有多个或两者。 CPU 位于主板上,GPU 位于主板上或视频卡。 CPU 的速度由主板上的时钟芯片决定,GPU 的速度由单独的时钟芯片决定。 CPU 时钟控制应用程序、运行时和驱动程序完成的工作速度。 应用程序通过运行时和驱动程序将工作发送到 GPU。

CPU 和 GPU 通常以不同的速度运行,彼此独立。 一旦工作可用,GPU 可能会立即响应工作, (假设 GPU 已完成) 处理以前的工作。 GPU 工作与上图中曲线突出显示的 CPU 工作并行完成。 探查器通常衡量 CPU 的性能,而不是 GPU 的性能。 这使得分析具有挑战性,因为检测探查器进行的测量包括 CPU 时间,但可能不包括 GPU 时间。

GPU 的用途是将处理从 CPU 卸载到专为图形工作设计的处理器。 在现代视频卡上,GPU 取代了从 CPU 到 GPU 的管道中的大部分转换和照明工作。 这大大减少了 CPU 工作负荷,使更多的 CPU 周期可供其他处理使用。 若要优化图形应用程序以获得峰值性能,需要测量 CPU 和 GPU 的性能,并平衡两种类型的处理器之间的工作。

本文档不涵盖与测量 GPU 性能或平衡 CPU 与 GPU 之间的工作相关的主题。 如果想要更好地了解 GPU (或特定视频卡) 的性能,请访问供应商的网站以查找有关 GPU 性能的详细信息。 相反,本文档重点介绍运行时和驱动程序完成的工作,方法是将 GPU 工作减少到可忽略不计的数量。 这在一定程度上基于遇到性能问题的应用程序通常受 CPU 限制的经验。

运行时和驱动程序优化可以屏蔽 API 度量

运行时内置了性能优化,可能会使单个调用的度量不堪重负。 下面是演示此问题的示例方案。 请考虑以下呈现顺序:

  BeginScene();
    ...
    SetTexture(...);
    DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1);
    ...
  EndScene();
  Present();

示例 1:简单呈现序列

查看呈现序列中两个调用的结果,检测探查器可能会返回类似于以下结果:

Number of cycles for SetTexture       : 100
Number of cycles for DrawPrimitive    : 950,500

探查器返回处理与每个调用关联的工作的 CPU 周期数, (请记住,GPU 未包含在这些数字中,因为 GPU 尚未开始处理这些命令,但尚未) 。 由于 IDirect3DDevice9::D rawPrimitive 需要近 100 万个周期来处理,因此可以得出结论,它的效率并不高。 但是,你很快就会了解为什么这个结论不正确,以及如何生成可用于预算的结果。

测量状态更改需要仔细的呈现序列

除 IDirect3DDevice9::D rawPrimitiveDrawIndexedPrimitiveClear ((如 SetTextureSetVertexDeclarationSetRenderState)以外的所有调用) 都会生成状态更改。 每个状态更改都设置管道状态,用于控制呈现方式。

运行时和/或驱动程序中的优化旨在通过减少所需的工作量来加快呈现速度。 下面是几个可能会污染配置文件平均值的状态更改优化:

  • 驱动程序 (或运行时) 可以将状态更改保存为本地状态。 由于驱动程序可以在“惰性”算法中运行, (推迟工作,直到绝对有必要) ,因此与某些状态更改相关的工作可能会延迟。
  • 运行时 (或驱动程序) 可以通过优化删除状态更改。 其中一个示例可能是删除冗余状态更改,该更改会禁用照明,因为以前已禁用照明。

无法万无一失地查看呈现序列并得出结论,哪些状态更改将设置脏位并延迟工作,或者只是通过优化删除。 即使可以识别当前运行时或驱动程序中的优化状态更改,明天的运行时或驱动程序也可能会更新。 你也不容易知道以前的状态是什么,因此很难识别冗余状态更改。 验证状态更改成本的唯一方法是测量包含状态更改的呈现序列。

如你所看到的,由于具有多个处理器、由多个组件处理命令以及组件中内置的优化而导致的复杂性使得分析难以预测。 在下一部分中,将解决其中每个分析难题。 将显示示例 Direct3D 呈现序列以及随附的测量技术。 掌握这些知识后,你将能够在单个调用上生成准确、可重复的度量值。

如何准确分析 Direct3D 呈现序列

现在,已突出显示一些分析难题,本部分将介绍有助于生成可用于预算的配置文件度量的技术。 如果了解 CPU 控制的组件之间的关系,以及如何避免运行时和驱动程序实现的性能优化,则可以进行准确、可重复的分析测量。

首先,需要能够准确地测量单个 API 调用的执行时间。

选择准确的度量工具,如 QueryPerformanceCounter

Microsoft Windows 操作系统包含一个高分辨率计时器,可用于测量高分辨率运行时间。 可以使用 QueryPerformanceCounter 返回一个此类计时器的当前值。 调用 QueryPerformanceCounter 返回开始和停止值后,可以使用 QueryPerformanceCounter 将两个值之间的差转换为实际运行时间 (秒) 。

使用 QueryPerformanceCounter 的优点是它在 Windows 中可用且易于使用。 只需使用 QueryPerformanceCounter 调用将调用括起来,然后保存 start 和 stop 值。 因此,本文将演示如何使用 QueryPerformanceCounter 来分析执行时间,类似于检测探查器测量时间的方式。 以下示例演示如何在源代码中嵌入 QueryPerformanceCounter

  BeginScene();
    ...
    // Start profiling
    LARGE_INTEGER start, stop, freq;
    QueryPerformanceCounter(&start);

    SetTexture(...);
    DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1); 

    QueryPerformanceCounter(&stop);
    stop.QuadPart -= start.QuadPart;
    QueryPerformanceFrequency(&freq);
    // Stop profiling
    ...
  EndScene();
  Present();

示例 2:使用 QPC 实现自定义分析

start 和 stop 是两个大整数,用于保存高性能计时器返回的开始值和停止值。 请注意,QueryPerformanceCounter (&启动) 在 SetTexture 和 QueryPerformanceCounter (&停止) 在 DrawPrimitive 之后调用之前调用。 获取停止值后,将调用 QueryPerformanceFrequency 以返回 freq,这是高分辨率计时器的频率。 在此假设示例中,假设你获得以下 start、stop 和 freq 结果:

局部变量 刻度数
start 1792998845094
stop 1792998845102
频率 3579545

 

可以将这些值转换为执行 API 调用所花费的周期数,如下所示:

# ticks = (stop - start) = 1792998845102 - 1792998845094 = 8 ticks

# cycles = CPU speed * number of ticks / QPF
# 4568   = 2 GHz      * 8              / 3,579,545

换句话说,在这 2 GHz 计算机上处理 SetTextureDrawPrimitive 需要大约 4568 个时钟周期。 可以将这些值转换为执行所有调用的实际时间,如下所示:

(stop - start)/ freq = elapsed time
8 ticks / 3,579,545 = 2.2E-6 seconds or between 2 and 3 microseconds.

使用 QueryPerformanceCounter 需要向呈现序列添加开始和停止度量值,并使用 QueryPerformanceFrequency 将) 刻度数 (刻度数转换为 CPU 周期数或实际时间。 确定度量技术是开发自定义分析实现的良好开端。 但在开始测量之前,你需要知道如何处理视频卡。

专注于 CPU 度量

如前所述,CPU 和 GPU 并行工作,以处理 API 调用生成的工作。 实际应用程序需要分析这两种类型的处理器,以确定应用程序是 CPU 受限还是 GPU 受限。 由于 GPU 性能特定于供应商,因此在本文中生成涵盖各种可用视频卡的结果将非常具有挑战性。

相反,本文将重点介绍通过使用自定义技术测量运行时和驱动程序工作来分析 CPU 执行的工作。 GPU 工作将减少到微不足道的数量,以便 CPU 结果更加明显。 此方法的一个好处是,此方法会在附录中生成与度量值关联的结果。 若要将视频卡所需的工作减少到微不足道的水平,只需尽可能减少渲染工作。 这可以通过限制绘制调用来呈现单个三角形来实现,并且可以进一步约束,以便每个三角形仅包含一个像素。

本文中用于测量 CPU 工作的度量单位将是 CPU 时钟周期数,而不是实际时间。 CPU 时钟周期的优点是,与具有不同 CPU 速度的计算机之间的实际运行时间相比,CPU 限制应用程序) 的可移植性 (更高。 如果需要,可以轻松将其转换为实际时间。

本文档不涵盖与均衡 CPU 和 GPU 之间的工作负荷相关的主题。 请记住,本文的目标不是测量应用程序的整体性能,而是展示如何准确测量运行时和驱动程序处理 API 调用所需的时间。 通过这些准确的度量值,可以承担预算 CPU 的任务,以了解某些性能方案。

控制运行时和驱动程序优化

确定度量技术和减少 GPU 工作的策略后,下一步是了解分析时妨碍的运行时和驱动程序优化。

CPU 工作可分为三个存储桶:应用程序工作、运行时工作和驱动程序工作。 忽略应用程序工作,因为这由程序员控制。 从应用程序的角度来看,运行时和驱动程序就像黑盒一样,因为应用程序无法控制其中实现的内容。 关键是要了解可能在运行时和驱动程序中实现的优化技术。 如果不了解这些优化,很容易根据配置文件度量得出有关 CPU 正在执行的工作量的错误结论。 具体而言,有两个主题与命令缓冲区以及它可以对分析进行模糊处理的功能相关。 这些主题是:

  • 使用命令缓冲区优化运行时。 命令缓冲区是一种运行时优化,可降低模式转换的影响。 若要控制模式转换的时间,请参阅 控制命令缓冲区
  • 取消命令缓冲区的计时效果。 模式转换的运行时间可能会对分析度量产生很大影响。 其策略是 使呈现序列与模式转换相比较大

控制命令缓冲区

当应用程序发出 API 调用时,运行时会将 API 调用转换为与设备无关的格式 (我们将) 调用命令,并将其存储在命令缓冲区中。 命令缓冲区将添加到下图中。

CPU 组件示意图,包括命令缓冲区

每次应用程序进行另一个 API 调用时,运行时都会重复此序列,并将另一个命令添加到命令缓冲区。 在某些时候,运行时会清空缓冲区 (将命令发送到驱动程序) 。 在 Windows XP 中,清空命令缓冲区会导致模式转换,因为操作系统从以用户模式) 运行的运行时 (切换到在内核模式下运行的驱动程序 () ,如下图所示。

  • 用户模式 - 执行应用程序代码的非特权处理器模式。 用户模式应用程序无法访问系统数据,除非通过系统服务。
  • 内核模式 - 基于 Windows 的执行代码运行的特权处理器模式。 在内核模式下运行的驱动程序或线程可以访问所有系统内存、直接访问硬件以及 CPU 指令以使用硬件执行 I/O。

用户模式与内核模式之间的转换关系图

每次 CPU 从用户模式切换到内核模式时都会发生转换 (反之亦然) ,并且与单个 API 调用相比,它所需的周期数很大。 如果运行时在调用驱动程序时向驱动程序发送了每个 API 调用,则每个 API 调用都会产生模式转换的成本。

相反,命令缓冲区是一种运行时优化,旨在降低模式转换的有效成本。 命令缓冲区将许多驱动程序命令排队,为单一模式转换做准备。 当运行时将命令添加到命令缓冲区时,控制权将返回到应用程序。 探查器无法知道驱动程序命令可能尚未发送到驱动程序。 因此,现成的检测探查器返回的数字具有误导性,因为它测量运行时工作,而不是衡量关联的驱动程序工作。

不带模式转换的探查结果

使用示例 2 中的呈现序列,下面是一些典型的计时度量值,用于说明模式转换的大小。 假设 SetTextureDrawPrimitive 调用不会导致模式转换,现成的检测探查器可能会返回如下所示的结果:

Number of cycles for SetTexture           : 100
Number of cycles for DrawPrimitive        : 900

其中每个数字都是运行时将这些调用添加到命令缓冲区所需的时间。 由于没有模式转换,因此驱动程序尚未完成任何工作。 探查器结果是准确的,但它们不会测量呈现序列最终将导致 CPU 执行的所有工作。

使用模式转换分析结果

现在,看看当模式转换发生时,同一示例会发生什么情况。 这一次,假设 SetTextureDrawPrimitive 导致模式转换。 同样,现成的检测探查器可能会返回类似于以下结果:

Number of cycles for SetTexture           : 98 
Number of cycles for DrawPrimitive        : 946,900

为 SetTexture 测量的时间大致相同,但是,DrawPrimitive 中花费的时间量急剧增加是由于模式转换。 下面是所发生的情况:

  1. 假设命令缓冲区在呈现序列开始之前有一个命令的空间。
  2. SetTexture 将转换为与设备无关的格式,并添加到命令缓冲区。 在此方案中,此调用将填充命令缓冲区。
  3. 运行时尝试将 DrawPrimitive 添加到命令缓冲区,但不能,因为它已满。 相反,运行时会清空命令缓冲区。 这会导致内核模式转换。 假设转换需要大约 5000 个周期。 此时间会导致 DrawPrimitive 花费的时间。
  4. 然后,驱动程序处理与从命令缓冲区清空的所有命令关联的工作。 假设驱动程序处理几乎已填充命令缓冲区的命令的时间约为 935,000 个周期。 假设与 SetTexture 关联的驱动程序工作周期约为 2750 个周期。 此时间会导致 DrawPrimitive 花费的时间。
  5. 当驱动程序完成其工作时,用户模式转换会将控制权返回到运行时。 命令缓冲区现在为空。 假设转换需要大约 5000 个周期。
  6. 呈现序列通过转换 DrawPrimitive 并将其添加到命令缓冲区完成。 假设这需要大约 900 个周期。 此时间会导致 DrawPrimitive 花费的时间。

汇总结果后,会看到:

DrawPrimitive = kernel-transition + driver work    + user-transition + runtime work
DrawPrimitive = 5000              + 935,000 + 2750 + 5000            + 900
DrawPrimitive = 947,950  

就像没有模式转换的 DrawPrimitive 的度量值 (900 个周期) 一样,使用模式转换 (947,950 个周期的 DrawPrimitive 的度量) 是准确的,但在预算 CPU 工作方面却无用。 结果包含正确的运行时工作、 SetTexture 的驱动程序工作、针对 SetTexture 之前的任何命令的驱动程序工作以及两个模式转换。 但是,度量值缺少 DrawPrimitive 驱动程序工作。

在响应任何调用时,可能会发生模式转换。 这取决于之前命令缓冲区中的内容。 需要控制模式转换,以了解 (运行时和驱动程序) 与每次调用关联的 CPU 工作量。 为此,需要一种用于控制命令缓冲区和模式转换计时的机制。

查询机制

Microsoft Direct3D 9 中的查询机制旨在允许运行时查询 GPU 的进度,并从 GPU 返回某些数据。 分析时,如果 GPU 工作最小化,因此对性能的影响微乎其微,则可以从 GPU 返回状态以帮助测量驱动程序工作。 毕竟,当 GPU 看到驱动程序命令时,驱动程序工作就完成了。 此外,查询机制可以共同控制两个命令缓冲区特征,这两个特征对分析很重要:命令缓冲区何时清空,以及缓冲区中的工作量。

下面是使用查询机制的相同呈现序列:

// 1. Create an event query from the current device
IDirect3DQuery9* pEvent;
m_pD3DDevice->CreateQuery(D3DQUERYTYPE_EVENT, &pEvent);

// 2. Add an end marker to the command buffer queue.
pEvent->Issue(D3DISSUE_END);

// 3. Empty the command buffer and wait until the GPU is idle.
while(S_FALSE == pEvent->GetData( NULL, 0, D3DGETDATA_FLUSH ))
    ;

// 4. Start profiling
LARGE_INTEGER start, stop;
QueryPerformanceCounter(&start);

// 5. Invoke the API calls to be profiled.
SetTexture(...);
DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1);

// 6. Add an end marker to the command buffer queue.
pEvent->Issue(D3DISSUE_END);

// 7. Force the driver to execute the commands from the command buffer.
// Empty the command buffer and wait until the GPU is idle.
while(S_FALSE == pEvent->GetData( NULL, 0, D3DGETDATA_FLUSH ))
    ;
    
// 8. End profiling
QueryPerformanceCounter(&stop);

示例 3:使用查询控制命令缓冲区

下面是以下每一行代码的更详细说明:

  1. 通过创建具有D3DQUERYTYPE_EVENT的查询对象来创建事件查询。
  2. 通过调用 Issue (D3DISSUE_END ) 将查询事件标记添加到命令缓冲区。 此标记告知驱动程序跟踪 GPU 何时完成执行标记之前的任何命令。
  3. 第一个调用会清空命令缓冲区,因为使用 D3DGETDATA_FLUSH 调用 GetData 会强制清空命令缓冲区。 每个后续调用都会检查 GPU,以查看它何时完成所有命令缓冲区工作。 在 GPU 处于空闲状态之前,此循环不会返回S_OK。
  4. 采样开始时间。
  5. 调用要分析的 API 调用。
  6. 将第二个查询事件标记添加到命令缓冲区。 此标记将用于跟踪调用的完成情况。
  7. 第一个调用会清空命令缓冲区,因为使用 D3DGETDATA_FLUSH 调用 GetData 会强制清空命令缓冲区。 当 GPU 处理完所有命令缓冲区工作后, GetData 将返回S_OK,并且循环退出,因为 GPU 处于空闲状态。
  8. 采样停止时间。

下面是使用 QueryPerformanceCounter 和 QueryPerformanceFrequency 测量的结果:

局部变量 时钟周期数
start 1792998845060
stop 1792998845090
频率 3579545

 

在 2 GHz 计算机上 (再次将时钟周期转换为周期) :

# ticks  = (stop - start) = 1792998845090 - 1792998845060 = 30 ticks
# cycles = CPU speed * number of ticks / QPF
# 16,450 = 2 GHz      * 30             / 3,579,545

下面是每个调用的周期数的细目:

Number of cycles for SetTexture           : 100
Number of cycles for DrawPrimitive        : 900
Number of cycles for Issue                : 200
Number of cycles for GetData              : 16,450

查询机制允许我们控制运行时和正在测量的驱动程序工作。 若要了解其中每个数字,下面是针对每个 API 调用的响应,以及估计的计时:

  1. 第一个调用通过使用 D3DGETDATA_FLUSH 调用 GetData 来清空命令缓冲区。 当 GPU 处理完所有命令缓冲区工作后, GetData 将返回S_OK,并且循环退出,因为 GPU 处于空闲状态。

  2. 呈现序列首先将 SetTexture 转换为与设备无关的格式,并将其添加到命令缓冲区。 假设这需要大约 100 个周期。

  3. DrawPrimitive 将转换并添加到命令缓冲区。 假设这需要大约 900 个周期。

  4. 问题 将查询标记添加到命令缓冲区。 假设这需要大约 200 个周期。

  5. GetData 会导致命令缓冲区被清空,从而强制进行内核模式转换。 假设这需要大约 5000 个周期。

  6. 然后,驱动程序处理与所有四个调用关联的工作。 假设处理 SetTexture 的驱动程序时间约为 2964 个周期, DrawPrimitive 约为 3600 个周期, 问题 大约为 200 个周期。 因此,所有四个命令的总驱动程序时间约为 6450 个周期。

    注意

    驱动程序还需要一点时间来查看 GPU 的状态。 由于 GPU 工作是微不足道的,因此 GPU 应该已经完成。 GetData 将根据 GPU 完成的可能性返回S_OK。

     

  7. 当驱动程序完成其工作时,用户模式转换会将控制权返回到运行时。 命令缓冲区现在为空。 假设这需要大约 5000 个周期。

GetData 的数字包括:

GetData = kernel-transition + driver work + user-transition
GetData = 5000              + 6450        + 5000           
GetData = 16,450  

driver work = SetTexture + DrawPrimitive + Issue = 
driver work = 2964       + 3600          + 200   = 6450 cycles 

与 QueryPerformanceCounter 结合使用的查询机制可测量所有 CPU 工作。 这是通过查询标记和查询状态比较的组合完成的。 添加到命令缓冲区的启动和停止查询标记用于控制缓冲区中的工作量。 通过等待,直到返回正确的返回代码,开始度量是在干净呈现序列开始之前进行的,停止测量是在驱动程序完成与命令缓冲区内容关联的工作之后进行的。 这可以有效地捕获运行时和驱动程序完成的 CPU 工作。

现在,你已了解命令缓冲区及其对分析的影响,你应该知道还有其他一些条件可能导致运行时清空命令缓冲区。 需要在呈现序列中为这些项watch。 其中一些条件是响应 API 调用,其他条件是响应运行时中的资源更改。 以下任一情况都将导致模式转换:

  • 当锁定方法之一 (锁定) 在特定条件下调用某些标志) 的顶点缓冲区、索引缓冲区或纹理 (。
  • 创建设备或顶点缓冲区、索引缓冲区或纹理时。
  • 当设备或顶点缓冲区、索引缓冲区或纹理被上一个版本销毁时。
  • 调用 ValidateDevice 时。
  • 调用 Present 时。
  • 当命令缓冲区填满时。
  • 使用 D3DGETDATA_FLUSH 调用 GetData 时。

在呈现序列中小心为这些条件watch。 每次添加模式转换时,都会向分析度量添加 10,000 个周期的驱动程序工作。 此外,命令缓冲区不会以静态方式调整大小。 运行时可能会更改缓冲区的大小,以响应应用程序正在生成的工作量。 这是另一个依赖于呈现序列的优化。

因此,在分析期间要小心控制模式转换。 查询机制提供了一种用于清空命令缓冲区的可靠方法,以便你可以控制模式转换的时间以及缓冲区包含的工作量。 但是,即使这种技术也可以通过减少模式转换时间来改进,使其与测量结果无关紧要。

使呈现序列与模式转换相比较大

在前面的示例中,内核模式开关和用户模式开关消耗大约 10,000 个与运行时和驱动程序工作无关的周期。 由于模式转换内置于操作系统中,因此无法将其缩减为零。 若要使模式转换变得微不足道,需要调整呈现序列,使驱动程序和运行时工作比模式切换大一个数量级。 可以尝试执行减法来删除转换,但将成本摊销到更大的渲染序列成本会更可靠。

在模式转换变得微不足道之前减少模式转换的策略是向呈现序列添加循环。 例如,如果添加了将重复呈现序列 1500 次的循环,让我们看看分析结果:

// Initialize the array with two textures, same size, same format
IDirect3DTexture* texArray[2];

CreateQuery(D3DQUERYTYPE_EVENT, pEvent);
pEvent->Issue(D3DISSUE_END);
while(S_FALSE == pEvent->GetData( NULL, 0, D3DGETDATA_FLUSH ))
    ;

LARGE_INTEGER start, stop;
// Now start counting because the video card is ready
QueryPerformanceCounter(&start);

// Add a loop to the render sequence 
for(int i = 0; i < 1500; i++)
{
  SetTexture(taxArray[i%2]);
  DrawPrimitive(D3DPT_TRIANGLELIST, i*3, 1);
}

pEvent->Issue(D3DISSUE_END);

while(S_FALSE == pEvent->GetData( NULL, 0, D3DGETDATA_FLUSH ))
    ;
QueryPerformanceCounter(&stop);

示例 4:向呈现序列添加循环

下面是使用 QueryPerformanceCounter 和 QueryPerformanceFrequency 测量的结果:

局部变量 Tics 数
start 1792998845000
stop 1792998847084
频率 3579545

 

使用 QueryPerformanceCounter 现在测量 2,840 个时钟周期。 将计时周期转换为周期与已显示相同:

# ticks  = (stop - start) = 1792998847084 - 1792998845000 = 2840 ticks
# cycles    = machine speed * number of ticks / QPF
# 6,900,000 = 2 GHz          * 2840           / 3,579,545

换句话说,在这台 2 GHz 计算机上需要大约 690 万个周期来处理呈现循环中的 1500 个调用。 在 690 万个周期中,模式转换的时间大约为 10,000,因此现在配置文件结果几乎完全测量与 SetTextureDrawPrimitive 相关的工作。

请注意,代码示例需要两个纹理数组。 若要避免运行时优化在每次调用时设置相同的纹理指针时删除 SetTexture ,只需使用两个纹理数组即可。 这样,每次通过循环时,纹理指针都会更改,并且执行与 SetTexture 关联的完整工作。 确保两个纹理的大小和格式相同,以便在纹理相同时不会更改其他状态。

现在,你有一种用于分析 Direct3D 的技术。 它依赖于高性能计数器 (QueryPerformanceCounter) 来记录 CPU 处理工作所花费的时钟周期数。 使用查询机制与 API 调用关联的运行时和驱动程序工作经过精心控制。 查询提供两种控制方式:首先在呈现序列开始前清空命令缓冲区,第二种是在 GPU 工作完成时返回。

到目前为止,本文已演示如何分析呈现序列。 每个呈现序列都非常简单,包含一个 DrawPrimitive 调用和 一个 SetTexture 调用。 这样做是为了专注于命令缓冲区和使用查询机制来控制它。 下面简要概述了如何分析任意呈现序列:

  • 使用高性能计数器(如 QueryPerformanceCounter)测量处理每个 API 调用所需的时间。 使用 QueryPerformanceFrequency 和 CPU 时钟速率将其转换为每个 API 调用的 CPU 周期数。
  • 通过呈现三角形列表来最大程度地减少 GPU 工作量,其中每个三角形都包含一个像素。
  • 使用查询机制在呈现序列之前清空命令缓冲区。 这可以保证分析将捕获与呈现序列关联的正确数量的运行时和驱动程序工作。
  • 使用查询事件标记控制添加到命令缓冲区的工作量。 此同一查询会检测 GPU 何时完成其工作。 由于 GPU 工作是琐碎的,这几乎相当于测量驱动程序工作完成的时间。

所有这些技术都用于分析状态更改。 假设已阅读并了解如何控制命令缓冲区,并成功完成 DrawPrimitive 上的基线测量,则可以向呈现序列添加状态更改。 向呈现序列添加状态更改时,还存在一些额外的分析挑战。 如果打算向呈现序列添加状态更改,请务必继续下一部分。

分析 Direct3D 状态更改

Direct3D 使用许多呈现状态来控制管道的几乎所有方面。 导致状态更改的 API 包括除 Draw*Primitive 调用以外的任何函数或方法。

状态更改很棘手,因为如果不进行呈现,可能无法查看状态更改的成本。 这是延迟算法的结果,驱动程序和 GPU 使用延迟工作,直到它绝对必须完成。 通常,应按照以下步骤来测量单个状态更改:

  1. 首先分析 DrawPrimitive
  2. 向呈现序列添加一个状态更改,并分析新序列。
  3. 减去两个序列之间的差以获取状态更改的成本。

当然,你学到的有关使用查询机制并将呈现序列置于循环中以抵消模式转换成本的所有内容仍然适用。

分析简单状态更改

从包含 DrawPrimitive 的呈现序列开始,下面是用于测量添加 SetTexture 的成本的代码序列:

// Get the start counter value as shown in Example 4 

// Initialize a texture array as shown in Example 4
IDirect3DTexture* texArray[2];

// Render sequence loop 
for(int i = 0; i < 1500; i++)
{
  SetTexture(0, texArray[i%2];
  
  // Force the state change to propagate to the GPU
  DrawPrimitive(D3DPT_TRIANGLELIST, i*3, 1);
}

// Get the stop counter value as shown in Example 4 

示例 5:测量一个状态更改 API 调用

请注意,循环包含两个调用: SetTextureDrawPrimitive。 呈现序列循环 1500 次,并生成类似于以下结果:

局部变量 Tics 数
start 1792998860000
stop 1792998870260
频率 3579545

 

再次将计时周期转换为周期会生成:

# ticks  = (stop - start) = 1792998870260 - 1792998860000 = 10,260 ticks
# cycles    = machine speed * number of ticks / QPF
5,775,000   = 2 GHz          * 10,260         / 3,579,545

除以循环中的迭代次数会生成:

5,775,000 cycles / 1500 iterations = 3850 cycles for one iteration

循环的每个迭代都包含状态更改和绘图调用。 减去 DrawPrimitive 呈现序列的结果会留下:

3850 - 1100 = 2750 cycles for SetTexture

这是将 SetTexture 添加到此呈现序列的平均周期数。 这种相同的技术可以应用于其他状态更改。

为什么 SetTexture 称为简单状态更改? 因为所设置的状态受到约束,所以每次状态更改时管道都会执行相同的工作量。 将两个纹理限制为相同的大小和格式可确保每次 SetTexture 调用的工作量相同。

分析需要切换的状态更改

还有其他状态更改会导致图形管道执行的工作量在呈现循环的每次迭代中发生变化。 例如,如果启用了 z 测试,则只有在针对现有像素的 z 值测试新像素的 z 值后,每个像素颜色才会更新呈现目标。 如果禁用 z 测试,则不会完成此每像素测试,并且输出的写入速度要快得多。 启用或禁用 z 测试状态会显著改变呈现期间 CPU 和 GPU) (完成的工作量。

SetRenderState 需要特定的呈现状态和状态值才能启用或禁用 z 测试。 特定状态值在运行时进行评估,以确定所需的工作量。 很难在呈现循环中测量此状态更改,并且仍然使管道状态处于前置状态,以便其切换。 唯一的解决方案是在呈现序列期间切换状态更改。

例如,分析技术需要重复两次,如下所示:

  1. 首先分析 DrawPrimitive 呈现序列。 将此称为基线。
  2. 分析切换状态更改的第二个呈现序列。 呈现序列循环包含:
    • 将状态设置为“false”条件的状态更改。
    • DrawPrimitive 就像原始序列一样。
    • 将状态设置为“true”条件的状态更改。
    • 第二个 DrawPrimitive 强制实现第二个状态更改。
  3. 查找两个呈现序列之间的差异。 这通过以下方式实现:
    • 将基线 DrawPrimitive 序列乘以 2,因为新序列中有两个 DrawPrimitive 调用。
    • 从原始序列中减去新序列的结果。
    • 将结果除以 2 可获取“false”和“true”状态更改的平均成本。

使用呈现序列中使用的循环技术,需要通过将呈现序列中的每次迭代的状态从“true”切换到“false”条件来衡量更改管道状态的成本。 这里的“true”和“false”的含义不是字面意思,这只是意味着状态需要设置为对立的条件。 这会导致在分析期间测量这两种状态更改。 当然,你学到的有关使用查询机制和将呈现序列置于循环中以抵消模式转换成本的所有内容仍然适用。

例如,下面是用于测量打开或关闭 z 测试的成本的代码序列:

// Get the start counter value as shown in Example 4 

// Add a loop to the render sequence 
for(int i = 0; i < 1500; i++)
{
  // Precondition the pipeline state to the "false" condition
  SetRenderState(D3DRS_ZENABLE, FALSE);
  
  // Force the state change to propagate to the GPU
  DrawPrimitive(D3DPT_TRIANGLELIST, (2*i + 0)*3, 1);

  // Set the pipeline state to the "true" condition
  SetRenderState(D3DRS_ZENABLE, TRUE);

  // Force the state change to propagate to the GPU
  DrawPrimitive(D3DPT_TRIANGLELIST, (2*i + 1)*3, 1); 
}

// Get the stop counter value as shown in Example 4 

示例 5:测量切换状态更改

循环通过执行两个 SetRenderState 调用来切换状态。 第一个 SetRenderState 调用禁用 z 测试,第二个 SetRenderState 调用将启用 z 测试。 每个 SetRenderState 后跟 DrawPrimitive,以便与状态更改关联的工作由驱动程序处理,而不是仅在驱动程序中设置脏位。

对于此呈现序列,这些数字是合理的:

局部变量 时钟周期数
start 1792998845000
stop 1792998861740
频率 3579545

 

再次将时钟周期转换为周期将产生:

# ticks  = (stop - start) = 1792998861740 - 1792998845000 = 15,120 ticks
# cycles    = machine speed * number of ticks / QPF
 9,300,000  = 2 GHz          * 16,740         / 3,579,545

除以循环中的迭代次数产生:

9,300,000 cycles / 1500 iterations = 6200 cycles for one iteration

循环的每个迭代都包含两个状态更改和两个绘制调用。 假设 1100 个周期) 离开, (减去绘制调用:

6200 - 1100 - 1100 = 4000 cycles for both state changes

这是两个状态更改的平均周期数,因此每次状态更改的平均时间是:

4000 / 2  = 2000 cycles for each state change

因此,启用或禁用 z 测试的平均周期数为 2000 个周期。 值得注意的是,QueryPerformanceCounter 正在测量一半时间的 z-enable 和一半时间的 z-disable。 此方法实际上测量这两种状态更改的平均值。 换句话说,你正在测量切换状态的时间。 使用此方法,你无法知道启用和禁用时间是否等效,因为你已经测量了两者的平均值。 不过,在将切换状态作为导致此状态更改的应用程序进行预算时,只能通过切换此状态来执行此操作,这是一个合理的数字。

现在,你可以应用这些技术并分析所需的所有状态更改,对吧? 并不全面。 你仍然需要小心优化,这些优化旨在减少需要完成的工作量。 设计呈现序列时,应注意两种类型的优化。

注意状态更改优化

上一部分介绍了如何分析这两种状态更改:受约束的简单状态更改,每次迭代生成相同的工作量,以及可显著更改已完成的工作量的切换状态更改。 如果采用上一个呈现序列并向其添加另一个状态更改,会发生什么情况? 例如,此示例采用 z-enable> 呈现序列,并为其添加 z-func 比较:

// Add a loop to the render sequence 
for(int i = 0; i < 1500; i++)
{
  // Precondition the pipeline state to the opposite condition
  SetRenderState(D3DRS_ZFUNC, D3DCMP_NEVER);

  // Precondition the pipeline state to the opposite condition
  SetRenderState(D3DRS_ZENABLE, FALSE);
  
  // Force the state change to propagate to the GPU
  DrawPrimitive(D3DPT_TRIANGLELIST, (2*i + 0)*3, 1);

  // Now set the state change you want to measure
  SetRenderState(D3DRS_ZFUNC, D3DCMP_ALWAYS);

  // Now set the state change you want to measure
  SetRenderState(D3DRS_ZENABLE, TRUE);

  // Force the state change to propagate to the GPU
  DrawPrimitive(D3DPT_TRIANGLELIST, (2*i + 1)*3, 1); 
}

当写入 z 缓冲区时,z-func 状态设置当前像素的 z 值与深度缓冲区) 中像素的 z 值之间的 z 缓冲区 (比较级别。 D3DCMP_NEVER关闭 z 测试比较,同时D3DCMP_ALWAYS将比较设置为每次完成 z 测试时都会发生。

使用 DrawPrimitive 在呈现序列中分析以下任一状态更改将生成类似于以下结果:

单状态更改 平均周期数
仅D3DRS_ZENABLE 2000

 

单状态更改 平均周期数
仅D3DRS_ZFUNC 600

 

但是,如果在同一呈现序列中分析D3DRS_ZENABLE和D3DRS_ZFUNC,可能会看到如下所示的结果:

两种状态更改 平均周期数
D3DRS_ZENABLE + D3DRS_ZFUNC 2000

 

结果预期为 2000 和 600 个 (或 2600 个) 周期的总和,因为驱动程序正在执行与设置这两种呈现状态相关的所有工作。 相反,平均值为 2000 个周期。

此结果反映了在运行时、驱动程序或 GPU 中实现的状态更改优化。 在这种情况下,驱动程序可以看到第一个 SetRenderState 并设置脏状态,这会推迟工作到以后。 当驱动程序看到第二个 SetRenderState 时,可以冗余设置相同的脏状态,并且相同的工作将再次推迟。 调用 DrawPrimitive 时,最终处理与脏状态关联的工作。 驱动程序执行一次工作,这意味着前两个状态更改由驱动程序有效合并。 同样,当调用第二个 DrawPrimitive 时,驱动程序会将第三个和第四个状态更改有效地合并为单个状态更改。 最终结果是驱动程序和 GPU 会为每个绘图调用处理单个状态更改。

这是依赖于序列的驱动程序优化的一个很好的示例。 驱动程序通过设置脏状态推迟了两次工作,然后执行一次工作以清除脏状态。 这是一个很好的示例,说明当工作推迟到绝对必要时,这种效率改进可能会发生。

如何知道哪些状态更改在内部设置了脏状态,从而将工作推迟到以后? 仅通过测试呈现序列 (或与驱动程序编写器) 对话。 驱动程序会定期更新和改进,因此优化列表不是静态的。 只有一种方法可以绝对了解给定呈现序列中状态更改在一组特定硬件上的成本:这就是度量它。

注意 DrawPrimitive 优化

除了状态更改优化之外,运行时还将尝试优化驱动程序必须处理的绘制调用数。 例如,请考虑以下回向绘制调用:

DrawPrimitive(D3DPT_TRIANGLELIST, 0, 3); // Draw 3 primitives, vertices 0 - 8
DrawPrimitive(D3DPT_TRIANGLELIST, 9, 4); // Draw 4 primitives, vertices 9 - 20

示例 5a:两个绘图调用

此序列包含两个绘制调用,运行时将合并为等效于的单个调用:

DrawPrimitive(D3DPT_TRIANGLELIST, 0, 7); // Draw 7 primitives, vertices 0 - 20

示例 5b:单个串联绘图调用

运行时会将这两个特定的绘图调用连接成单个调用,这会使驱动程序的工作减少 50%,因为驱动程序现在只需要处理一个绘制调用。

通常,运行时会在以下情况下连接两个或更多个背靠背 DrawPrimitive 调用:

  1. 基元类型是三角形列表 (D3DPT_TRIANGLELIST) 。
  2. 每个连续 DrawPrimitive 调用都必须引用顶点缓冲区内的连续顶点。

同样,连接两个或更多个背靠背 DrawIndexedPrimitive 调用的正确条件是:

  1. 基元类型是三角形列表 (D3DPT_TRIANGLELIST) 。
  2. 每个连续 的 DrawIndexedPrimitive 调用必须在索引缓冲区内按顺序引用连续索引。
  3. 每次连续 的 DrawIndexedPrimitive 调用都必须对 BaseVertexIndex 使用相同的值。

若要防止在分析过程中出现串联,请修改呈现序列,使基元类型不是三角形列表,或修改呈现序列,以便不存在使用连续顶点 (或索引) 的背靠背绘制调用。 更具体地说,运行时还将连接满足以下两个条件的绘制调用:

  • 当上一次调用为 DrawPrimitive 时,如果下一个绘图调用:
    • 使用三角形列表,AND
    • 指定 StartVertex = previous StartVertex + previous PrimitiveCount * 3
  • 使用 DrawIndexedPrimitive 时,如果下一个绘图调用:
    • 使用三角形列表,AND
    • 指定 StartIndex = previous StartIndex + previous PrimitiveCount * 3,AND
    • 指定 BaseVertexIndex = 上一个 BaseVertexIndex

下面是一个更微妙的绘图调用串联示例,在分析时很容易忽略它。 假设呈现序列如下所示:

  for(int i = 0; i < 1500; i++)
  {
    SetTexture(...);
    DrawPrimitive(D3DPT_TRIANGLELIST, i*3, 1);
  }

示例 5c:一个状态更改和一个绘图调用

循环循环访问 1500 个三角形,设置纹理并绘制每个三角形。 对于 SetTexture ,此呈现循环大约需要 2750 个周期,对于 DrawPrimitive 需要大约 1100 个周期,如前面的部分所示。 你可能会直观地预期,在呈现循环外部移动 SetTexture 应将驱动程序完成的工作量减少 1500 * 2750 个周期,即与调用 SetTexture 1500 次相关的工作量。 代码片段如下所示:

  SetTexture(...); // Set the state outside the loop
  for(int i = 0; i < 1500; i++)
  {
//    SetTexture(...);
    DrawPrimitive(D3DPT_TRIANGLELIST, i*3, 1);
  }

示例 5d:循环外部状态更改的示例 5c

SetTexture 移到呈现循环外部确实可以减少与 SetTexture 关联的工作量,因为它被调用一次,而不是 1500 次。 一个不太明显的次要影响是 ,DrawPrimitive 的工作也从 1500 次调用减少到 1 个调用,因为满足用于串联绘图调用的所有条件。 处理呈现序列时,运行时会将 1500 个调用处理到单个驱动程序调用中。 通过移动这一行代码,驱动程序工作量已大大减少:

total work done = runtime + driver work

Example 5c: with SetTexture in the loop:
runtime work = 1500 SetTextures + 1500 DrawPrimitives 
driver  work = 1500 SetTextures + 1500 DrawPrimitives 

Example 5d: with SetTexture outside of the loop:
runtime work = 1 SetTexture + 1 DrawPrimitive + 1499 Concatenated DrawPrimitives 
driver  work = 1 SetTexture + 1 DrawPrimitive 

这些结果完全正确,但在原始问题的上下文中非常具有误导性。 绘制调用优化导致驱动程序工作量大幅减少。 这是执行自定义分析时的常见问题。 消除呈现序列的调用时,请小心避免绘制调用串联。 事实上,此方案是一个强大的示例,说明此运行时优化可以改进驱动程序性能。

现在,你已了解如何度量状态更改。 首先分析 DrawPrimitive。 然后,将每个额外的状态更改添加到序列 (在某些情况下添加一个调用,而在其他情况下,添加两个调用) 并测量两个序列之间的差异。 可以将结果转换为时钟周期、周期或时间。 与使用 QueryPerformanceCounter 测量呈现序列一样,测量各个状态更改依赖于查询机制来控制命令缓冲区,并将状态更改置于循环中以最大程度地减少模式转换的影响。 此方法测量切换状态的成本,因为探查器返回启用和禁用状态的平均值。

借助此功能,可以开始生成任意呈现序列并准确测量关联的运行时和驱动程序工作。 然后,这些数字可用于回答预算问题,例如在呈现序列中可以进行“多少个这些调用”,同时在 CPU 受限的情况下仍保持合理的帧速率。

总结

本文演示如何控制命令缓冲区,以便可以准确分析单个调用。 分析数字可以以计时周期、周期或绝对时间生成。 它们表示与每个 API 调用关联的运行时和驱动程序工作量。

首先在呈现序列中分析 Draw*Primitive 调用。 请牢记以下事项:

  1. 使用 QueryPerformanceCounter 测量每个 API 调用的时钟周期数。 如果需要,请使用 QueryPerformanceFrequency 将结果转换为周期或时间。
  2. 在启动之前,使用查询机制清空命令缓冲区。
  3. 将呈现序列包含在循环中,以最大程度地减少模式转换的影响。
  4. 使用查询机制测量 GPU 何时完成其工作。
  5. 注意对完成的工作量有重大影响的运行时串联。

这为你提供了 可用于从中生成的 DrawPrimitive 的基线性能。 若要分析一个状态更改,请遵循以下其他提示:

  1. 将状态更改添加到新序列的已知呈现序列配置文件。 由于测试是在循环中完成的,因此需要将状态设置为两次相反的值, (例如启用和禁用) 。
  2. 比较两个序列之间的周期时间差异。
  3. 对于显著更改管道 ((如 SetTexture) )的状态更改,请减去两个序列之间的差异以获取状态更改的时间。
  4. 对于显著更改管道 (因此需要切换状态(如 SetRenderState) )的状态更改,请减去呈现序列与除以 2 之间的差异。 这将为每个状态更改生成平均周期数。

但是,请注意在分析时导致意外结果的优化。 状态更改优化可能会设置脏状态,从而导致工作延迟。 这可能会导致配置文件结果不如预期的那样直观。 串联的绘图调用将大大减少驱动程序工作,从而导致误导性的结论。 精心计划的呈现序列用于防止发生状态更改和绘制调用串联。 诀窍是防止在分析期间进行优化,以便生成的数字是合理的预算数字。

注意

在没有查询机制的情况下在应用程序中复制此分析策略更为困难。 在 Direct3D 9 之前,清空命令缓冲区的唯一可预测方法是锁定活动图面 ((例如呈现目标),) 等待 GPU 空闲。 这是因为,除了等待 GPU 完成之外,锁定图面会强制运行时清空命令缓冲区,以防缓冲区中有任何呈现命令命令应该在图面被锁定之前对其进行更新。 尽管使用 Direct3D 9 中引入的查询机制更为突出,但此方法具有功能性。

 

附录

此表中的数字是与其中每个状态更改关联的运行时和驱动程序工作量的近似值范围。 近似值基于使用本文所示技术对驱动程序进行的实际测量。 这些数字是使用 Direct3D 9 运行时生成的,并且依赖于驱动程序。

本文中的技术旨在测量运行时和驱动程序工作。 通常,提供与每个应用程序中 CPU 和 GPU 的性能匹配的结果是不切实际的,因为这需要详尽的呈现序列数组。 此外,特别难以对 GPU 的性能进行基准测试,因为它在呈现序列之前高度依赖于管道中的状态设置。 例如,启用 alpha 混合几乎不会影响所需的 CPU 工作量,但会对 GPU 完成的工作量产生重大影响。 因此,本文中的技术通过限制需要呈现的数据量,将 GPU 工作限制在尽可能小的数量。 这意味着表中的数字将与 CPU 受限 (应用程序(而不是受 GPU) 限制的应用程序)获得的结果最接近。

建议你使用提供的技术来介绍对你最重要的方案和配置。 表中的值可用于与生成的数字进行比较。 由于每个驱动程序各不相同,因此生成实际数字的唯一方法是使用方案生成分析结果。

API 调用 平均周期数
SetVertexDeclaration 6500 - 11250
SetFVF 6400 - 11200
SetVertexShader 3000 - 12100
SetPixelShader 6300 - 7000
SPECULARENABLE 1900 - 11200
SetRenderTarget 6000 - 6250
SetPixelShaderConstant (1 常量) 1500 - 9000
NORMALIZENORMALS 2200 - 8100
LightEnable 1300 - 9000
SetStreamSource 3700 - 5800
照明 1700 - 7500
DIFFUSEMATERIALSOURCE 900 - 8300
AMBIENTMATERIALSOURCE 900 - 8200
COLORVERTEX 800 - 7800
SetLight 2200 - 5100
SetTransform 3200 - 3750
SetIndices 900 - 5600
环境 1150 - 4800
SetTexture 2500 - 3100
SPECULARMATERIALSOURCE 900 - 4600
EMISSIVEMATERIALSOURCE 900 - 4500
SetMaterial 1000 - 3700
ZENABLE 700 - 3900
WRAP0 1600 - 2700
MINFILTER 1700 - 2500
放大镜 1700 - 2400
SetVertexShaderConstant (1 常量) 1000 - 2700
COLOROP 1500 - 2100
COLORARG2 1300 - 2000
COLORARG1 1300 - 1980
CULLMODE 500 - 2570
裁剪 500 - 2550
DrawIndexedPrimitive 1200 - 1400
ADDRESSV 1090 - 1500
ADDRESSU 1070 - 1500
DrawPrimitive 1050 - 1150
SRGBTEXTURE 150 - 1500
STENCILMASK 570 - 700
STENCILZFAIL 500 - 800
STENCILREF 550 - 700
ALPHABLENDENABLE 550 - 700
STENCILFUNC 560 - 680
STENCILWRITEMASK 520 - 700
STENCILFAIL 500 - 750
ZFUNC 510 - 700
ZWRITEENABLE 520 - 680
STENCILENABLE 540 - 650
STENCILPASS 560 - 630
SRCBLEND 500 - 685
Two_Sided_StencilMODE 450 - 590
ALPHATESTENABLE 470 - 525
ALPHAREF 460 - 530
ALPHAFUNC 450 - 540
DESTBLEND 475 - 510
COLORWRITEENABLE 465 - 515
CCW_STENCILFAIL 340 - 560
CCW_STENCILPASS 340 - 545
CCW_STENCILZFAIL 330 - 495
SCISSORTESTENABLE 375 - 440
CCW_STENCILFUNC 250 - 480
SetScissorRect 150 - 340

 

高级主题