Статьи

Написать 3D Soft Engine с нуля: Бонус Партия

В предыдущем уроке мы узнали, как заполнять наши треугольники. Поскольку мы работаем на базе процессора с нашим программным 3D-движком, это действительно начинает стоить много процессорного времени. Хорошей новостью является то, что современные процессоры являются многоядерными . Тогда мы могли бы представить себе использование параллелизма для повышения производительности . Мы собираемся сделать это только в C #, и я объясню, почему мы не будем делать это в HTML5. Мы также увидим несколько простых советов, которые могут повысить производительность в таком коде цикла рендеринга. В самом деле, мы собираемся перейти с 5 FPS на 50 FPS , что увеличит производительность в 10 раз!

Вычислить FPS

Первым шагом является вычисление FPS, чтобы можно было проверить, собираемся ли мы повысить производительность, изменив наш алгоритм. Вы можете сделать это в C # или TypeScript / JavaScript, конечно.

Нам нужно знать время дельты между двумя кадрами . Затем нам просто нужно захватить текущее время, нарисовать новый кадр ( requestAnimationFrame в HTML5 или CompositionTarget.Rendering в XAML), снова захватить текущее время и сравнить его с предыдущим сохраненным временем. Вы получите результат в миллисекундах. Чтобы получить FPS, просто разделите 1000 на этот результат. Например, если это 16,66 мс, оптимальное время дельты , у вас будет 60 FPS.

Вы можете сделать это после каждого рендеринга кадра, чтобы иметь очень точный FPS или вычислить средний FPS, например, для 60 выборок. Мы с Дэвидом уже работали над этой темой в этой серии: Сравнительный анализ игры HTML5: HTML5 Potatoes Gaming Bench

В заключение, в C # добавьте новый элемент управления TextBlock XAML с именем « fps » и используйте этот код для вычисления FPS:

  DateTime previousDate; 
  void CompositionTarget_Rendering ( отправитель объекта , объект e) 
     // Fps 
     var now = DateTime .Now; 
     var currentFps = 1000.0 / (сейчас - previousDate) .TotalMilliseconds; 
     previousDate = сейчас; 
      fps.Text = string .Format ( "{0: 0.00} fps" , currentFps); 
      // цикл рендеринга 
     device.Clear (0, 0, 0, 255); 
      foreach ( сетка в ячейках) 
     { 
         mesh.Rotation = new Vector3 (mesh.Rotation.X, mesh.Rotation.Y + 0.01f, mesh.Rotation.Z); 
         device.Render (мера, сетка); 
     } 
      device.Present (); 
 

Используя этот код, используя собственное разрешение моего Lenovo Carbon X1 Touch (1600 × 900), я использую в среднем 5 FPS с решением C #, описанным в предыдущей статье. Мой Lenovo встраивает Intel Core i7-3667U с графическим процессором HD4000. Это гиперпоточный двухъядерный процессор. Затем он показывает 4 логических процессора .

Стратегии оптимизации и распараллеливания

Приложения WinRT используют .NET Framework 4.5, которая по умолчанию включает библиотеку параллельных задач (TPL). Если вы обращаете внимание на то, как вы пишете свой алгоритм, и если ваш алгоритм может быть распараллелен, сделать его параллельным становится очень легко благодаря TPL. Если вы еще не знаете эту концепцию, обратите внимание на Параллельное программирование в .NET Framework 4: Начало работы

Избегайте прикосновений к элементам управления пользовательского интерфейса

Первое правило для многопоточности / многозадачности состоит в том, чтобы код 0 касался интерфейса в порождении потоков. Только поток пользовательского интерфейса может касаться / манипулировать графическими элементами управления. В нашем случае у нас был фрагмент кода, обращающийся к bmp.PixelWidth или bmp.PixelHeight, где bmp имеет тип WriteableBitmap . WriteableBitmap рассматривается как элемент пользовательского интерфейса и не является потокобезопасным. Вот почему нам нужно сначала изменить эти блоки кода, чтобы сделать их «параллелизуемыми». В предыдущем уроке мы начали с этого. Вам просто нужно сохранить эти значения в начале. Мы сделали это в renderWidth и renderHeight . Используйте эти значения в своем коде вместо доступа к bmp . Измените все ссылки на bmp.PixelWidth на renderWidth и на bmp.PixelHeight на renderHeight.

Кстати, это правило важно не только для распараллеливания. Это также для оптимизации производительности в целом. Таким образом, просто удаляя доступ к свойствам WriteableBitmap в моем коде, я переключаюсь со средней скорости 5 FPS на более чем 45 FPS на одной машине!

Это же правило очень важно (даже, может быть, даже больше) в HTML5. Вы должны избегать прямого тестирования свойств элементов DOM . Операции DOM очень медленные. Так что на самом деле не стоит заходить к ним каждые 16 мсек, если в этом нет необходимости. Всегда кэшируйте значения, которые необходимо проверить позже. Мы уже делали это в предыдущих уроках для HTML5-версии 3D-движка.

Быть самодостаточным

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

Возможно, вы видели, что у нас есть несколько областей, где мы можем переключить наш классический цикл For с помощью цикла Parallel.For .

Первый случай в методе DrawTriangle . Затем мы собираемся нарисовать несколько линий на треугольнике параллельно. Затем вы можете легко преобразовать цикл 2 normal For в цикл 2 Parallel.For :

  если (dP1P2> dP1P3) 
     Параллельно .For (( int ) p1.Y, ( int ) p3.Y + 1, y => 
         { 
             если (у <p2.Y) 
             { 
                 ProcessScanLine (y, p1, p3, p1, p2, color); 
             } 
             еще 
             { 
                 ProcessScanLine (y, p1, p3, p2, p3, color); 
             } 
         }); 
  еще 
  { 
     Параллельно .For (( int ) p1.Y, ( int ) p3.Y + 1, y => 
         { 
             если (у <p2.Y) 
             { 
                 ProcessScanLine (y, p1, p2, p1, p3, color); 
             } 
             еще 
             { 
                 ProcessScanLine (y, p2, p3, p1, p3, color); 
             } 
         }); 
 

Но в моем случае результат немного удивителен. Я понижаю производительность, переключаясь с 45 FPS до 40 FPS ! Так что может быть причиной этого недостатка производительности?

Что ж, в этом случае параллельное рисование нескольких линий недостаточно для питания ядер. Затем мы тратим больше времени на переключение контекста и переход от одного ядра к другому, чем на реальную обработку. Это можно проверить с помощью встроенных инструментов профилирования Visual Studio 2012: Concurrency Visualizer для Visual Studio 2012

Вот карта использования ядер с этим первым подходом распараллеливания :

image

Различные цвета связаны с рабочими потоками. Это действительно неэффективно. Посмотрите на разницу с непараллельной версией :

image

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

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

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

Проблема с использованием этого подхода заключается в методе PutPixel . Теперь, когда мы хотим нарисовать несколько граней параллельно, мы можем попасть в случаи, когда 2 ядра / нити будут пытаться получить доступ к одному и тому же пикселю в параллельном режиме. Затем нам нужно защитить доступ к пикселю, прежде чем работать с ним. Нам также необходимо найти эффективный способ блокировки доступа к пиксельным буферам. Действительно, если мы тратим больше времени на защиту данных, чем на работу с ними, распараллеливание снова станет бесполезным.

Решение состоит в том, чтобы использовать массив, содержащий поддельные объекты, которые мы заблокируем.

  закрытый объект [] lockBuffer; 
  Публичное устройство ( WriteableBitmap BMP) 
     это .bmp = bmp; 
     renderWidth = bmp.PixelWidth; 
     renderHeight = bmp.PixelHeight; 
      // размер заднего буфера равен количеству пикселей для рисования 
     // на экране (ширина * высота) * 4 (значения R, G, B и Alpha).  
     backBuffer = новый байт [renderWidth * renderHeight * 4]; 
     deepBuffer = new float [renderWidth * renderHeight]; 
     lockBuffer = новый объект [renderWidth * renderHeight]; 
     for ( var i = 0; i <lockBuffer.Length; i ++) 
     { 
         lockBuffer [i] = новый объект (); 
     } 
  // Вызывается поставить пиксель на экран с определенными координатами X, Y 
  public void PutPixel ( int x, int y, float z, Color4 color) 
     // Поскольку у нас есть 1-D массив для нашего заднего буфера 
     // нам нужно знать эквивалентную ячейку в 1-D 
     // в 2D координатах на экране 
     var index = (x + y * renderWidth); 
     var index4 = index * 4; 
      // Защита нашего буфера от параллелизма потоков 
     блокировка (lockBuffer [индекс]) 
     { 
         if (deepBuffer [index] <z) 
         { 
             возврат ;  // Сброс 
         } 
          deepBuffer [index] = z; 
          backBuffer [index4] = ( byte ) (color.Blue * 255); 
         backBuffer [index4 + 1] = ( байт ) (color.Green * 255); 
         backBuffer [index4 + 2] = ( байт ) (color.Red * 255); 
         backBuffer [index4 + 3] = ( байт ) (color.Alpha * 255); 
     } 
 

Используя этот второй подход, я двигаюсь в среднем с 45 FPS до 53 FPS. Вы можете подумать, что прирост производительности не так впечатляет. Но в следующем уроке метод drawTriangle будет намного сложнее обрабатывать тени и освещение. Например, используя этот подход с затенением Гуро, распараллеливание почти удвоит производительность .

Мы также можем проанализировать новое представление Cores с помощью этого второго подхода распараллеливания:

image

Сравните его с предыдущим представлением Cores, и вы поймете, почему это намного эффективнее.

Вы можете скачать решение C #, содержащее эту оптимизированную версию, здесь:

C # : SoftEngineCSharpPart4Bonus.zip

Итак, что не так с HTML5 / JavaScript в этом случае?

HTML5 предлагает новый API для разработчиков JavaScript для обработки аналогичных подходов. Он называется Web Workers и может решать проблему использования многоядерных процессоров в определенных сценариях.

Дэвид Катухе и я уже освещали эту тему несколько раз в этих 3 статьях:

Введение в веб-работников HTML5: многопоточный подход JavaScript : сначала прочитайте эту статью, если вы еще не знаете веб-работников

Использование веб-работников для повышения производительности манипулирования изображениями : очень интересная статья, в которой мы работаем над работой с пикселями.

Серия учебных пособий : использование WinJS и WinRT для создания забавного приложения камеры HTML5 для Windows 8 (4/4) : четвертая часть серии учебных пособий, в которой я использую веб-работников для применения некоторых фильтров к изображениям, снятым с помощью веб- камеры .

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

Но это не наша главная проблема, чтобы попытаться ускорить наш 3D-движок в HTML5. В случае копирования данные отправляются с помощью операции memcpy (), которая выполняется очень быстро. Реальная проблема заключается в том, когда рабочие закончат свою работу по обработке. Вам необходимо отправить результаты обратно в основной поток, и этот поток пользовательского интерфейса должен перебирать все массивы, отправленные обратно, чтобы перестроить массивы основных пикселей. Эта операция, к сожалению, просто убьет любой прирост производительности, который мы могли бы получить у рабочих.

В заключение, я не нашел способа реализовать подход параллелизма в HTML5 с нашим программным механизмом 3D. Но я, возможно, что-то пропустил. Если вам удастся обойти текущие ограничения веб-работников для получения значительного прироста производительности, я открыт для предложений! 🙂

В нашем следующем уроке мы вернемся к регулярным урокам, чтобы поговорить о плоской штриховке и штриховке Гуро. Наши объекты начнут действительно сиять! 🙂

Первоначально опубликовано: http://blogs.msdn.com/b/davrous/archive/2013/06/25/tutorial-part-4-bonus-learning-how-to-write-a-3d-software-engine-in -c-ts-or-js-optimizing-amp-parallelism.aspx . Перепечатано здесь с разрешения автора.