Поделиться через


Точное профилирование вызовов API Direct3D (Direct3D 9)

После того как у вас есть функциональное приложение Microsoft Direct3D и вы хотите повысить производительность, вы обычно используете средство профилирования вне полки или некоторые пользовательские методы измерения для измерения времени, необходимого для выполнения одного или нескольких вызовов интерфейса программирования приложений (API). Если вы сделали это, но получают результаты времени, которые зависят от одной последовательности от отрисовки до следующей, или вы делаете гипотезы, которые не содержат фактических результатов эксперимента, следующие сведения могут помочь вам понять, почему.

Приведенные здесь сведения основаны на предположении, что у вас есть знания и опыт работы со следующими сведениями:

  • Программирование на C/C++
  • Программирование API Direct3D
  • Измерение времени API
  • Видео карта и его драйвер программного обеспечения
  • Возможные необъяснимые результаты из предыдущего опыта профилирования

Точное профилирование Direct3D сложно

Профилировщик сообщает о времени, затраченном на каждый вызов API. Это делается для повышения производительности путем поиска и настройки горячих точек. Существуют различные виды профилировщиков и методов профилирования.

  • Профилировщик выборки бездействует большую часть времени, просыпаясь через определенные интервалы для выборки (или записи) выполняемых функций. Он возвращает процент времени, затраченного на каждый вызов. Как правило, профилировщик выборки не является очень инвазивным для приложения и имеет минимальное влияние на издержки для приложения.
  • Профилировщик инструментирования измеряет фактическое время, которое требуется для возврата вызова. Для этого требуется компиляция разделителей запуска и остановки в приложение. Профилировщик инструментирования является сравнительно более инвазивным для приложения, чем профилировщик выборки.
  • Также можно использовать настраиваемый метод профилирования с высокопроизводительной таймером. Это дает результаты очень похоже на профилировщик инструментирования.

Используемый тип профилировщика или профилирования является лишь частью задачи создания точных измерений.

Профилирование дает ответы, которые помогут вам повысить производительность бюджета. Например, предположим, что вызов API в среднем составляет тысячу циклов часов для выполнения. Вы можете подтвердить некоторые выводы о производительности, например следующее:

  • 2 ГГц ЦП (который тратит 50 процентов отрисовки времени) ограничен вызовом этого API 1 млн раз в секунду.
  • Чтобы достичь 30 кадров в секунду, этот API не может вызывать более 33 000 раз на кадр.
  • Вы можете отрисовки только 3,3K объектов на кадр (при условии, что 10 из этих вызовов API для последовательности отрисовки каждого объекта).

Другими словами, если у вас было достаточно времени на вызов API, можно ответить на вопрос бюджетирования, например количество примитивов, которые можно отобразить в интерактивном режиме. Но необработанные числа, возвращаемые профилировщиком инструментирования, не будут точно отвечать на вопросы бюджетирования. Это связано с тем, что графический конвейер имеет сложные проблемы проектирования, такие как количество компонентов, необходимых для работы, количество процессоров, которые управляют потоками работы между компонентами и стратегиями оптимизации, реализованными в среде выполнения и в драйвере, который предназначен для повышения эффективности конвейера.

Каждый вызов API проходит через несколько компонентов

Каждый вызов обрабатывается несколькими компонентами по пути от приложения к видео карта. Например, рассмотрим следующую последовательность отрисовки, содержащую два вызова для рисования одного треугольника:

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

На следующей концептуальной схеме показаны различные компоненты, через которые должны проходить вызовы.

diagram of graphics components that api calls go through

Приложение вызывает Direct3D, который управляет сценой, обрабатывает взаимодействие пользователей и определяет, как выполняется отрисовка. Все эти действия указываются в последовательности отрисовки, которая отправляется в среду выполнения с помощью вызовов API Direct3D. Последовательность отрисовки является практически аппаратной независимой (то есть вызовы API являются аппаратными независимыми, но приложение имеет знания о том, какие функции поддерживает видео карта).

Среда выполнения преобразует эти вызовы в независимый от устройства формат. Среда выполнения обрабатывает все взаимодействие между приложением и драйвером, чтобы приложение выполнялось на нескольких совместимых аппаратных компонентах (в зависимости от необходимых функций). При измерении вызова функции профилировщик инструментирования измеряет время, затраченное на функцию, а также время возврата функции. Одним из ограничений профилировщика инструментирования является то, что оно может не включать время, которое требуется драйверу для отправки полученной работы в видео карта, а также времени для видео карта для обработки работы. Другими словами, профилировщик инструментирования вне полки не может атрибутировать всю работу, связанную с каждым вызовом функции.

Драйвер программного обеспечения использует аппаратные знания о видео карта для преобразования независимых от устройств команд в последовательность команд видео карта. Драйверы также могут оптимизировать последовательность команд, отправляемых в видео карта, чтобы эффективно выполнять отрисовку видео карта. Эти оптимизации могут привести к проблемам профилирования, так как объем выполненных работ не является тем, что он, как представляется, (возможно, потребуется понять оптимизации для их учета). Драйвер обычно возвращает управление средой выполнения, прежде чем видео карта завершит обработку всех команд.

Видео карта выполняет большую часть отрисовки путем объединения данных из буферов вершин и индексов, текстур, сведений о состоянии отрисовки и графических команд. Когда видео карта завершит отрисовку, работа, созданная из последовательности отрисовки, завершена.

Каждый вызов API Direct3D должен обрабатываться каждым компонентом (средой выполнения, драйвером и видео карта), чтобы отобразить все.

Существует несколько процессоров, управляющих компонентами

Связь между этими компонентами еще более сложна, так как приложение, среда выполнения и драйвер управляются одним процессором, а видео карта управляется отдельным процессором. На следующей схеме показаны два типа процессоров: центральный модуль обработки (ЦП) и графический модуль обработки (GPU).

diagram of a cpu and a gpu and their components

Компьютерные системы имеют по крайней мере один ЦП и один GPU, но могут иметь несколько из одного или обоих. Процессоры находятся на материнской плате, и GPU находятся либо на материнской плате, либо на видео карта. Скорость ЦП определяется микросхемой часов на материнской плате, а скорость GPU определяется отдельной микросхемой часов. Часы ЦП управляют скоростью работы, выполняемой приложением, средой выполнения и драйвером. Приложение отправляет работу на GPU через среду выполнения и драйвер.

ЦП и GPU обычно выполняются на разных скоростях, независимо друг от друга. GPU может реагировать на работу, как только работа доступна (если GPU завершил обработку предыдущей работы). Работа с GPU выполняется параллельно с работой ЦП, как выделено кривой линией на рисунке выше. Профилировщик обычно измеряет производительность ЦП, а не GPU. Это затрудняет профилирование, так как измерения, сделанные профилировщиком инструментирования, включают время ЦП, но не может включать время GPU.

Цель GPU — отключить загрузку от ЦП к процессору, специально предназначенному для работы с графикой. На современных карта видео GPU заменяет большую часть преобразования и освещения в конвейере с ЦП на GPU. Это значительно сокращает рабочую нагрузку ЦП, оставляя больше циклов ЦП доступными для другой обработки. Чтобы настроить графическое приложение для пиковой производительности, необходимо измерить производительность ЦП и GPU, а также сбалансировать работу между двумя типами процессоров.

В этом документе не рассматриваются разделы, связанные с измерением производительности GPU или балансировкой работы между ЦП и GPU. Если вы хотите лучше понять производительность GPU (или определенного видео карта), посетите веб-сайт поставщика, чтобы найти дополнительные сведения о производительности GPU. Вместо этого в этом документе основное внимание уделяется работе, выполняемой средой выполнения и драйвером, сокращая работу GPU до незначительной суммы. Это, в частности, на основе опыта, что приложения, испытывающие проблемы с производительностью, обычно ограничены ЦП.

Оптимизация среды выполнения и драйвера могут маскирование измерений API

Среда выполнения имеет встроенную оптимизацию производительности, которая может перегружать измерение отдельного вызова. Ниже приведен пример сценария, демонстрирующего эту проблему. Рассмотрим следующую последовательность отрисовки:

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

Пример 1. Простая последовательность отрисовки

Глядя на результаты двух вызовов в последовательности отрисовки, профилировщик инструментирования может возвращать результаты, аналогичные следующим:

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

Профилировщик возвращает количество циклов ЦП, необходимых для обработки работы, связанной с каждым вызовом (помните, что GPU еще не включен в эти числа, так как GPU еще не работал над этими командами). Так как IDirect3Device9::D rawPrimitive требуется почти миллион циклов для обработки, вы можете заключить, что это не очень эффективно. Однако вскоре вы увидите, почему это заключение неправильно и как можно создавать результаты, которые можно использовать для бюджетирования.

Для измерения изменений состояния требуются тщательные последовательности отрисовки

Все вызовы, отличные от IDirect3Device9::D rawPrimitive, DrawIndexedPrimitive или Clear (например, SetTexture, SetVertexDeclaration и SetRenderState), создают изменение состояния. Каждое изменение состояния задает состояние конвейера, которое управляет выполнением отрисовки.

Оптимизации во время выполнения и (или) драйвер предназначены для ускорения отрисовки путем уменьшения объема необходимой работы. Ниже приведены несколько оптимизаций изменений состояния, которые могут загрязнять средние показатели профиля:

  • Драйвер (или среда выполнения) может сохранить изменение состояния в качестве локального состояния. Так как драйвер может работать в алгоритме "ленивого" (откладывания работы до тех пор, пока это не обязательно), работа, связанная с некоторыми изменениями состояния, может быть отложена.
  • Среда выполнения (или драйвер) может удалять изменения состояния путем оптимизации. Примером этого может быть удаление избыточного изменения состояния, которое отключает освещение, так как освещение ранее было отключено.

Нет безупреходного способа просмотреть последовательность отрисовки и заключить, какие изменения состояния будут задавать грязное бит и отложить работу, или просто будет удалена оптимизацией. Даже если вы можете определить оптимизированные изменения состояния в текущей среде выполнения или драйвере, завтрашняя среда выполнения или драйвер, скорее всего, будет обновлена. Вы также не легко знаете, что было предыдущее состояние, поэтому трудно определить избыточные изменения состояния. Единственным способом проверки стоимости изменения состояния является измерение последовательности отрисовки, включающей изменения состояния.

Как вы видите, осложнения, вызванные несколькими процессорами, командами, обрабатываемыми несколькими компонентами, и оптимизациями, встроенными в компоненты, затрудняет прогнозирование профилирования. В следующем разделе будут рассмотрены все эти проблемы профилирования. Будут показаны примеры последовательностей отрисовки Direct3D с сопутствующими методами измерения. С помощью этих знаний вы сможете создавать точные повторяемые измерения при отдельных вызовах.

Как точно профилировать последовательность отрисовки Direct3D

Теперь, когда были выделены некоторые из проблем профилирования, в этом разделе показаны методы, которые помогут вам создать измерения профиля, которые можно использовать для бюджетирования. Точные повторяющиеся измерения профилирования возможны, если вы понимаете связь между компонентами, контролируемыми ЦП, и как избежать оптимизации производительности, реализованной средой выполнения и драйвером.

Чтобы начать, необходимо точно измерять время выполнения одного вызова API.

Выбор средства точного измерения, например QueryPerformanceCounter

Операционная система Microsoft Windows включает таймер высокого разрешения, который можно использовать для измерения времени с высоким разрешением. Текущее значение одного такого таймера можно вернуть с помощью QueryPerformanceCounter. После вызова QueryPerformanceCounter для возврата значений начала и остановки разница между двумя значениями может быть преобразована в фактическое истекшее время (в секундах) с помощью QueryPerformanceCounter.

Преимущества использования QueryPerformanceCounter заключается в том, что он доступен в Windows и легко использовать. Просто окружите вызовы вызовами QueryPerformanceCounter и сохраните значения начала и остановки. Поэтому в этом документе показано, как использовать 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(&start) вызывается непосредственно перед вызовом SetTexture и QueryPerformanceCounter(&stop) сразу после метода DrawPrimitive. После получения значения остановки вызывается запрос QueryPerformanceFrequency для возврата freq, который является частотой таймера высокого разрешения. В этом гипотетическом примере предположим, что вы получите следующие результаты для запуска, остановки и 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

Другими словами, для обработки SetTexture и DrawPrimitive на этом компьютере 2 ГГц требуется около 4568 часов. Эти значения можно преобразовать в фактическое время выполнения всех вызовов следующим образом:

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

При использовании QueryPerformanceCounter необходимо добавить начальные и стоп-измерения в последовательность отрисовки и использовать QueryPerformanceFrequency для преобразования разницы (число тиков) в число циклов ЦП или в фактическое время. Определение метода измерения является хорошим началом для разработки пользовательской реализации профилирования. Но прежде чем переходить и начать делать измерения, необходимо знать, как справиться с видео карта.

Фокус на измерениях ЦП

Как упоминалось ранее, ЦП и GPU работают параллельно, чтобы обрабатывать работу, созданную вызовами API. Для реального приложения требуется профилирование обоих типов процессоров, чтобы узнать, ограничено ли приложение ЦП или GPU. Так как производительность GPU зависит от поставщика, было бы очень сложно создать результаты в этом документе, который охватывает множество доступных видео карта.

Вместо этого в этом документе основное внимание будет сосредоточено только на профилировании работы, выполняемой ЦП, с помощью пользовательского метода измерения работы среды выполнения и драйвера. Работа GPU будет сокращена до незначительного объема, чтобы результаты ЦП были более видимыми. Одним из преимуществ этого подхода является то, что этот метод дает результаты в приложении, которое вы должны иметь возможность сопоставить с вашими измерениями. Чтобы уменьшить работу, необходимую видео карта на незначительный уровень, просто уменьшите работу отрисовки до минимального объема. Это можно сделать путем ограничения вызовов рисования для отрисовки одного треугольника и может быть дополнительно ограничено, чтобы каждый треугольник содержал только один пиксель.

Единица измерения, используемая в этом документе для измерения работы ЦП, будет число циклов часов ЦП, а не фактического времени. Циклы часов ЦП имеют преимущество, что оно является более переносимым (для приложений, ограниченных ЦП), чем фактическое истекшее время на компьютерах с разными скоростями ЦП. Это можно легко преобразовать в фактическое время при необходимости.

В этом документе не рассматриваются разделы, связанные с балансировкой рабочей нагрузки между ЦП и GPU. Помните, что цель этого документа заключается не в том, чтобы оценить общую производительность приложения, но показать, как точно измерять время, затраченное средой выполнения и драйвером для обработки вызовов API. С помощью этих точных измерений можно взять на себя задачу бюджетирования ЦП, чтобы понять определенные сценарии производительности.

Управление оптимизацией среды выполнения и драйвера

Определяя метод измерения и стратегию сокращения работы GPU, следует понять оптимизацию среды выполнения и драйвера, которые находятся в пути при профилировании.

Работа ЦП может быть разделена на три сегмента: работа приложения, работа среды выполнения и работа драйвера. Игнорируйте работу приложения, так как это находится под контролем программиста. С точки зрения приложения среда выполнения и драйвер похожи на черные ящики, так как приложение не имеет контроля над тем, что реализовано в них. Ключ заключается в том, чтобы понять методы оптимизации, которые могут быть реализованы в среде выполнения и драйвере. Если вы не понимаете эти оптимизации, очень легко перейти к неправильному выводу о объеме работы ЦП выполняется на основе измерений профиля. В частности, существует два раздела, связанные с чем-то, что называется буфером команд, и что это может сделать для маскирования профилирования. Ниже приведены следующие разделы:

  • Оптимизация среды выполнения с помощью буфера команд. Буфер команд — это оптимизация среды выполнения, которая снижает влияние перехода в режиме. Чтобы управлять временем перехода в режиме, см. раздел "Управление буфером команд".
  • Отрицание эффектов времени буфера команд. Истекшее время перехода режима может оказать большое влияние на измерения профилирования. Стратегия заключается в том, чтобы сделать последовательность отрисовки большой по сравнению с переходом в режиме.

Управление буфером команд

Когда приложение вызывает API, среда выполнения преобразует вызов API в независимый от устройства формат (который будет вызывать команду) и сохраняет его в буфере команд. Буфер команд добавляется на следующую схему.

diagram of cpu components, including a command buffer

Каждый раз, когда приложение вызывает другой вызов API, среда выполнения повторяет эту последовательность и добавляет другую команду в буфер команд. В какой-то момент среда выполнения очищает буфер (отправляя команды драйверу). В Windows XP пустая буфер команды приводит к переходу режима, так как операционная система переключается из среды выполнения (в пользовательском режиме) на драйвер (работает в режиме ядра), как показано на следующей схеме.

  • режим пользователя — не привилегированный режим процессора, который выполняет код приложения. Приложения в режиме пользователя не могут получить доступ к системным данным, кроме системных служб.
  • режим ядра — режим привилегированного процессора, в котором выполняется исполнительный код на основе Windows. Драйвер или поток, работающий в режиме ядра, имеет доступ ко всем системным памяти, прямой доступ к оборудованию и инструкциям ЦП для выполнения операций ввода-вывода с оборудованием.

diagram of transitions between user mode and kernel mode

Переход происходит каждый раз, когда ЦП переключается с пользователя на режим ядра (и наоборот), а количество циклов, необходимых для него, большое по сравнению с отдельным вызовом API. Если среда выполнения отправила каждому вызову API драйверу при его вызове, каждый вызов API будет нести стоимость перехода в режиме.

Вместо этого буфер команд — это оптимизация среды выполнения, предназначенная для снижения эффективной стоимости перехода в режиме. Буфер команд помещает в очередь множество команд драйвера в подготовке к переходу в один режим. Когда среда выполнения добавляет команду в буфер команд, элемент управления возвращается приложению. Профилировщик не знает, что команды драйвера, вероятно, еще не были отправлены водителю. В результате числа, возвращаемые профилировщиком инструментирования вне полки, вводят в заблуждение, так как он измеряет работу среды выполнения, но не связанную работу драйвера.

Результаты профиля без перехода в режиме

С помощью последовательности отрисовки из примера 2 ниже приведены некоторые типичные измерения времени, иллюстрирующие величину перехода в режиме. Если вызовы SetTexture и DrawPrimitive не вызывают переход в режиме, профилировщик инструментирования вне полки может возвращать результаты, аналогичные следующим:

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

Каждое из этих чисел — это время, необходимое для среды выполнения, чтобы добавить эти вызовы в буфер команд. Так как нет перехода в режиме, драйвер еще не работал. Результаты профилировщика точны, но они не измеряют всю работу, которую последовательность отрисовки в конечном итоге приведет к выполнению ЦП.

Результаты профиля с переходом в режиме

Теперь ознакомьтесь с тем же примером, когда происходит переход в режиме. На этот раз предположим, что SetTexture и DrawPrimitive вызывают переход в режиме. Еще раз профилировщик инструментирования вне полки может возвращать результаты, аналогичные следующим:

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 циклов), измерение для DrawPrimitive с переходом режима (947 950 циклов) является точным, но бесполезным с точки зрения бюджетирования работы ЦП. Результат содержит правильную работу среды выполнения, драйвер работает для SetTexture, драйвер работает для любых команд, предшествующих SetTexture, и двух режимов перехода. Однако измерение отсутствует в работе драйвера DrawPrimitive .

Переход в режиме может произойти в ответ на любой вызов. Он зависит от того, что было ранее в буфере команд. Чтобы понять, сколько работы ЦП (среды выполнения и драйвера) связано с каждым вызовом, необходимо управлять переходом в режиме. Для этого требуется механизм управления буфером команд и временем перехода в режиме.

Механизм запроса

Механизм запросов в 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. Первый вызов очищает буфер команд, так как вызов GetData с D3DGETDATA_FLUSH заставляет буфер команд очищаться. Каждый последующий вызов проверка GPU, чтобы увидеть, когда он завершит обработку всей работы буфера команд. Этот цикл не возвращает S_OK до простоя GPU.
  4. Пример времени начала.
  5. Вызов профилируемых вызовов API.
  6. Добавьте второй маркер события запроса в буфер команд. Этот маркер будет использоваться для отслеживания завершения вызовов.
  7. Первый вызов очищает буфер команд, так как вызов GetData с D3DGETDATA_FLUSH заставляет буфер команд очищаться. Когда GPU завершит обработку всех работ в буфере команд, GetData возвращает S_OK, а цикл завершается, так как GPU неактивен.
  8. Пример времени остановки.

Ниже приведены результаты, измеряемые с помощью QueryPerformanceCounter и QueryPerformanceFrequency:

Локальная переменная Количество галок
start 1792998845060
stop 1792998845090
Частота 3579545

 

Преобразование галок в циклы снова (на компьютере с частотой 2 ГГц):

# 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. Первый вызов очищает буфер команд путем вызова GetData с D3DGETDATA_FLUSH. Когда 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 вернет S_OK на основе вероятности завершения GPU.

     

  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, измеряет всю работу ЦП. Это делается с помощью сочетания маркеров запросов и сравнения состояния запроса. Маркеры запуска и остановки запросов, добавленные в буфер команд, используются для управления объемом работы в буфере. Ожидая, пока не будет возвращен правильный код возврата, начальное измерение выполняется непосредственно перед началом чистой последовательности отрисовки, и измерение остановки выполняется сразу после завершения работы, связанной с содержимым буфера команды. Это эффективно фиксирует работу ЦП, выполняемую средой выполнения, а также драйвером.

Теперь, когда вы знаете о буфере команд и эффекте, который он может иметь при профилировании, следует знать, что существует несколько других условий, которые могут привести к тому, что среда выполнения пуста буфера команд. Необходимо следить за этими в последовательностях отрисовки. Некоторые из этих условий отвечают на вызовы API, другие находятся в ответ на изменения ресурсов в среде выполнения. Любое из следующих условий приведет к переходу в режиме:

  • Если один из методов блокировки (Lock) вызывается в буфере вершин, буфере индекса или текстуре (при определенных условиях с определенными флагами).
  • При создании буфера или вершины, буфера индекса или текстуры.
  • Когда устройство или буфер вершин, буфер индекса или текстура уничтожаются последним выпуском.
  • При вызове ValidateDevice.
  • При вызове Present.
  • Когда буфер команды заполняется.
  • При вызове GetData с D3DGETDATA_FLUSH.

Внимательно следите за этими условиями в последовательностях отрисовки. Каждый раз при добавлении режима в измерения профилирования будут добавлены 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 измеряет 2840 галок сейчас. Преобразование галок в циклы совпадает с тем, что мы уже показали:

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

Другими словами, для обработки вызовов 1500 вызовов в цикле отрисовки требуется около 6,9 млн циклов на этом компьютере с 2 ГГц. Из 6,9 миллиона циклов время переходов в режиме составляет около 10k, поэтому теперь результаты профиля почти полностью измеряются с SetTexture и DrawPrimitive.

Обратите внимание, что для примера кода требуется массив двух текстур. Чтобы избежать оптимизации среды выполнения, которая будет удалять SetTexture , если она устанавливает один и тот же указатель текстур при каждом вызове, просто используйте массив двух текстур. Таким образом, каждый раз, когда цикл, указатель текстуры изменяется, и выполняется полная работа, связанная с SetTexture . Убедитесь, что оба текстуры имеют одинаковый размер и формат, чтобы при выполнении текстуры не изменялось другое состояние.

Теперь у вас есть методика профилирования Direct3D. Он использует счетчик высокой производительности (QueryPerformanceCounter) для записи количества тиков, которые требуется ЦП для обработки работы. Работа тщательно контролируется, чтобы среда выполнения и драйвер работали с вызовами API с помощью механизма запроса. Запрос предоставляет два средства управления: сначала очистить буфер команды перед запуском последовательности отрисовки, а во-вторых, чтобы вернуться после завершения работы GPU.

До сих пор в этом документе показано, как профилировать последовательность отрисовки. Каждая последовательность отрисовки была довольно простой, содержащей один вызов DrawPrimitive и вызов SetTexture. Это было сделано, чтобы сосредоточиться на буфере команд и использовании механизма запроса для управления им. Ниже приведена краткая сводка по профилю произвольной последовательности отрисовки:

  • Используйте счетчик высокой производительности, например QueryPerformanceCounter, чтобы оценить время, необходимое для обработки каждого вызова API. Используйте QueryPerformanceFrequency и частоту часов ЦП для преобразования этого числа циклов ЦП на вызов API.
  • Свести к минимуму объем работы 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 изменения состояния

Обратите внимание, что цикл содержит два вызова, SetTexture и DrawPrimitive. Цикл последовательности отрисовки 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-теста значительно изменяет объем выполненных работ (ЦП, а также 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-testing, а второй Параметр SetRenderState включает z-testing. За каждым параметром 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-func задает уровень сравнения при записи в буфер z (между z-значением текущего пикселя с z-значением пикселя в буфере глубины). D3DCMP_NEVER отключает сравнение z-тестирования, а D3DCMP_ALWAYS устанавливает сравнение при каждом выполнении z-тестирования.

Профилирование одного из этих изменений состояния в последовательности отрисовки с помощью DrawPrimitive создает результаты, аналогичные следующим:

Изменение одного состояния Среднее число циклов
только D3DRS_ZENABLE 2000

 

or

Изменение одного состояния Среднее число циклов
только 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 треугольников, задание текстуры и рисование каждого треугольника. Этот цикл отрисовки занимает около 2750 циклов для SetTexture и 1100 циклов для DrawPrimitive, как показано в предыдущих разделах. Вы можете интуитивно ожидать, что перемещение 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, измерение отдельных изменений состояния зависит от механизма запроса для управления буфером команд и размещения изменений состояния в цикле, чтобы свести к минимуму влияние переходов в режиме. Этот метод измеряет стоимость переключения состояния, так как профилировщик возвращает среднее значение включения и отключения состояния.

С помощью этой возможности можно начать создавать произвольные последовательности отрисовки и точно измерять связанную среду выполнения и работу драйвера. Затем номера можно использовать для ответа на вопросы о бюджете, такие как "сколько больше этих вызовов" можно сделать в последовательности отрисовки, сохраняя разумный частоту кадров, предполагая, что сценарии с ограниченными ЦП.

Итоги

В этом документе показано, как управлять буфером команд, чтобы отдельные вызовы могли быть точно профилированы. Числа профилирования можно создать в галках, циклах или абсолютном времени. Они представляют объем работы среды выполнения и драйвера, связанного с каждым вызовом 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 и зависят от драйвера.

Методы, описанные в этом документе, предназначены для измерения работы среды выполнения и драйвера. Как правило, нецелесообразно предоставлять результаты, соответствующие производительности ЦП и GPU в каждом приложении, так как для этого потребуется исчерпывающий массив последовательностей отрисовки. Кроме того, особенно трудно провести тест производительности GPU, так как он сильно зависит от настройки состояния в конвейере до последовательности отрисовки. Например, включение альфа-смешивания не влияет на объем необходимой работы ЦП, но может оказать большое влияние на объем работы, выполняемой GPU. Таким образом, методы, описанные в этом документе, ограничивают работу GPU минимальным количеством возможных путем ограничения объема данных, которые должны быть отрисованы. Это означает, что числа в таблице будут наиболее тесно соответствовать результатам, полученным из приложений, которые ограничены ЦП (в отличие от приложения, ограниченного GPU).

Вам рекомендуется использовать методы, представленные для покрытия сценариев и конфигураций, наиболее важных для вас. Значения в таблице можно использовать для сравнения с числами, которые вы создаете. Так как каждый драйвер зависит, единственным способом создания фактических чисел, которые вы увидите, является создание результатов профилирования с помощью сценариев.

Вызов API Среднее число циклов
SetVertexDeclaration 6500 - 11250
SetFVF 6400 - 11200
SetVertexShader 3000 - 12100
SetPixelShader 6300 - 7000
SPECULARENABLE 1900 - 11200
SetRenderTarget 6000 - 6250
SetPixelShaderConstant (1 Константа) 1500 - 9000
НОРМАЛИЗАЦИЯНОРМАЛЬНЫХ 2200 - 8100
LightEnable 1300 - 9000
SetStreamSource 3700 - 5800
ОСВЕЩЕНИЯ 1700 - 7500
ДИФФУМЕНТНЫЙ ИСТОЧНИК 900 - 8300
A МБ IENTSOURCE 900 - 8200
COLORVERTEX 800 - 7800
SetLight 2200 - 5100
SetTransform 3200 - 3750
SetIndices 900 - 5600
A МБ IENT 1150 - 4800
SetTexture 2500 - 3100
SPECULARMATERIALSOURCE 900 - 4600
EMISSIVESOURCE 900 - 4500
SetMaterial 1000 - 3700
ZENABLE 700 - 3900
ОБОЛОЧКА 1600 - 2700
MINFILTER 1700 - 2500
MAGFILTER 1700 - 2400
SetVertexShaderConstant (1 константа) 1000 - 2700
COLOROP 1500 - 2100
COLORARG2 1300 - 2000
COLORARG1 1300 - 1980
CULLMODE 500 - 2570
C пакет интерфейса пользователя PING 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
DES ТБ LEND 475 - 510
COLORWRITEENABLE 465 - 515
CCW_STENCILFAIL 340 - 560
CCW_STENCILPASS 340 - 545
CCW_STENCILZFAIL 330 - 495
SCISSORTESTENABLE 375 - 440
CCW_STENCILFUNC 250 - 480
SetScisorRect 150 - 340

 

Дополнительные разделы