Отладка приложений с использованием примитивов в XNA Framework 4.0. Часть 2

Отладка приложений с использованием примитивов в XNA Framework 4.0. Часть 2

Теперь пришло время рассмотреть те интересные методы, которые реализованы в данной библиотеке. Для этого открываем файл DebugShapeRenderer.cs.

В данном классе используются следующие принципы:

  • Условное выполнение методов
  • Динамическое расширение массивов
  • Кэширование
  • Пакетная обработка вершин

А теперь по порядку рассмотрим каждый из них:

Условное выполнение методов:

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

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

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

Способов таких несколько, один из мы сейчас и рассмотрим.

Пространство имен System.Diagnostics содержит аттрибут ConditionalAttribute, которым могут быть помечены методы какого-либо класса. Конструктор ConditionalAttribute принимает на вход строковый параметр, который обозначает условие, при выполнении которого код метода будет успешно выполнен. Если же условие не выполняется, то метод не вызовется.

Что же такое условие для ConditionalAttribute? Условием в данном случае будет выступать некоторая строковая константа, определяемая через препроцессорные директивы #define.

Например, метод Initialize как и все остальные методы класса DebugShapeRenderer зависит от константы DEBUG, которая автоматически создается при выборе опции Debug в конфигурации проекта:

[Conditional(«DEBUG»)]
public static void Initialize(GraphicsDevice graphicsDevice)
{
// If we already have a graphics device, we’ve already initialized once. We don’t allow that.
if (graphics != null)
throw new InvalidOperationException(«Initialize can only be called once.»);

// Save the graphics device
graphics = graphicsDevice;

// Create and initialize our effect
effect = new BasicEffect(graphicsDevice);
effect.VertexColorEnabled = true;
effect.TextureEnabled = false;
effect.DiffuseColor = Vector3.One;
effect.World = Matrix.Identity;

// Create our unit sphere vertices
InitializeSphere();
}

 

Легко убедиться в том, что если переставить конфигурацию с Debug на Release, то никакие фигуры не будут нарисованы.

Захотите ли вы использовать такой метод в своих проектах решать вам, но этот метод определенно заслуживает внимания.

Динамическое расширение массивов и пакетная обработка вершин.

Свегда существует проблема выбора коллекции данных. В данном случае рассмотрим выбор между массивами и списками.

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

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

Есть, конечно, компромиссные варианты, ArrayList, например, реализует «бесконечный» массив. То есть элементы массива распологаются в памяти последовательно, а когда память заканчивается, то такой массив автоматически расширяется в два раза. Правда такая операция занимает много времени.

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

Причем мы заранее не можем знать сколько именно вершин нам понадобится.

Давайте более подробно рассмотрим пример:

// We use a cache system to reuse our DebugShape instances to avoid creating garbage
private static readonly List<DebugShape> activeShapes = new List<DebugShape>();

// Allocate an array to hold our vertices; this will grow as needed by our renderer
private static VertexPositionColor[] verts = new VertexPositionColor[64];

 

В списке activeShapes хранятся все фигуры, которые нам нужно будет нарисовать.

Массиве verts будет создаваться прямо в методе Draw из линий, созтавляющих фигуры.

Теперь перейдем к методу Draw. Сейчас он может быть не слишком понятным, но постепенно мы рассмотрим все его особенности.

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

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

Так вот, быстрее всего будет сложить все вершины (на самом деле не все, но как можно больше) в один массив и нарисовать их одним вызовом метода DrawUserPrimitives (или аналогичного метода). По сути для видеокарты не составляет особых проблем нарисовать несколько десятков тысяч линий за раз, а вот рисовать тысячу раз по одной линии это же сложнее.

Вернемся к нашему методу Draw. Для начала посчитаем все вершины, которые нам нужно нарисовать:

int vertexCount = 0;
foreach (var shape in activeShapes)
vertexCount += shape.LineCount * 2;

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

Дальше нужно составить массив вершин. Но мы помним, что изначально мы выделяли память только под 64 вершины. Теперь нужно выделить правильное количество памяти.

Причем сейчас мы на самом деле знаем сколько точно элементов нам понадобится, мы ведь только что это посчитали.

Можно было бы выделять нужное количество памяти каждый раз, но, как вы понимаете, это значительно снизило бы производительность. Действительно зачем нам выделять память каждый раз, если мы ее уже выделяли в прошлый раз?

Проблема только в том, что того, что мы выделяли в прошлый раз может не хватить в этот раз. Более подробно рассмотрим процесс расширения массива:

Нужно выделить некоторое количество памяти. Вот тут перед нами опять встает вопрос: сколько памяти нужно выделить? Ровно столько, чтобы в массиве поместились все вершины или больше? А если больше, то на сколько? Сразу посмотрим, что в данном примере мы будем всегда в два раза больше памяти, чем нам реально нужно. Делается это из того предположения, что раз уж сейчас нам нужно расширять массив, то, вероятно, нам придется его расширять снова. Чтобы не делать этого слишком часто мы просто выделим в два раза больше памяти.

if (vertexCount > 0)
{
// Make sure our array is large enough
if (verts.Length < vertexCount)
{
// If we have to resize, we make our array twice as large as necessary so
// we hopefully won’t have to resize it for a while.
verts = new VertexPositionColor[vertexCount * 2];
}

// остальной код

}

Теперь остается заполнить массив вершинами из списка фигур и нарисовать:

int lineCount = 0;
int vertIndex = 0;
foreach (DebugShape shape in activeShapes)
{
lineCount += shape.LineCount;
int shapeVerts = shape.LineCount * 2;
for (int i = 0; i < shapeVerts; i++)
verts[vertIndex++] = shape.Vertices[i];
}

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

Более того, профиль Reach (тот, что используется на Windows Phone 7) просто не позволяет рисовать больше 65535 вершин за один вызов DrawUserPrimitives. Так что нам нужно разделить вешь наш массив на «пакеты» и каждый такой «пакет» рисовать по отдельности.

На сколько я понимаю offset в методе DrawUserPrimitives нужен как раз для этих целей.

Мы будем рисовать по 65535 (точнее минимум из 65535 и оставшимся количеством вершин) вершин за раз, а после этого сдвигать offset на 65535 (если еще остались вершины).

int vertexOffset = 0;
while (lineCount > 0)
{
// Figure out how many lines we’re going to draw
int linesToDraw = Math.Min(lineCount, 65535);

// Draw the lines
graphics.DrawUserPrimitives(PrimitiveType.LineList, verts, vertexOffset, linesToDraw);

// Move our vertex offset ahead based on the lines we drew
vertexOffset += linesToDraw * 2;

// Remove these lines from our total line count
lineCount -= linesToDraw;
}

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

[Conditional(«DEBUG»)]
public static void Draw(GameTime gameTime, Matrix view, Matrix projection)
{
// Update our effect with the matrices.
effect.View = view;
effect.Projection = projection;

// Calculate the total number of vertices we’re going to be rendering.
int vertexCount = 0;
foreach (var shape in activeShapes)
vertexCount += shape.LineCount * 2;

// If we have some vertices to draw
if (vertexCount > 0)
{
// Make sure our array is large enough
if (verts.Length < vertexCount)
{
// If we have to resize, we make our array twice as large as necessary so
// we hopefully won’t have to resize it for a while.
verts = new VertexPositionColor[vertexCount * 2];
}

// Now go through the shapes again to move the vertices to our array and
// add up the number of lines to draw.
int lineCount = 0;
int vertIndex = 0;
foreach (DebugShape shape in activeShapes)
{
lineCount += shape.LineCount;
int shapeVerts = shape.LineCount * 2;
for (int i = 0; i < shapeVerts; i++)
verts[vertIndex++] = shape.Vertices[i];
}

// Start our effect to begin rendering.
effect.CurrentTechnique.Passes[0].Apply();

// We draw in a loop because the Reach profile only supports 65,535 primitives. While it’s
// not incredibly likely, if a game tries to render more than 65,535 lines we don’t want to
// crash. We handle this by doing a loop and drawing as many lines as we can at a time, capped
// at our limit. We then move ahead in our vertex array and draw the next set of lines.
int vertexOffset = 0;
while (lineCount > 0)
{
// Figure out how many lines we’re going to draw
int linesToDraw = Math.Min(lineCount, 65535);

// Draw the lines
graphics.DrawUserPrimitives(PrimitiveType.LineList, verts, vertexOffset, linesToDraw);

// Move our vertex offset ahead based on the lines we drew
vertexOffset += linesToDraw * 2;

// Remove these lines from our total line count
lineCount -= linesToDraw;
}
}

// Go through our active shapes and retire any shapes that have expired to the
// cache list.
bool resort = false;
for (int i = activeShapes.Count — 1; i >= 0; i—)
{
DebugShape s = activeShapes[i];
s.Lifetime -= (float)gameTime.ElapsedGameTime.TotalSeconds;
if (s.Lifetime <= 0)
{
cachedShapes.Add(s);
activeShapes.RemoveAt(i);
resort = true;
}
}

// If we move any shapes around, we need to resort the cached list
// to ensure that the smallest shapes are first in the list.
if (resort)
cachedShapes.Sort(CachedShapesSort);
}

 

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

Запись опубликована в рубрике Компьютерная графика с метками , , , , . Добавьте в закладки постоянную ссылку.

3 отзыва на “Отладка приложений с использованием примитивов в XNA Framework 4.0. Часть 2

  1. Уведомление: Некоторые интересные ссылки (Март) | Александр Богатырев: сфера

  2. Уведомление: ??????????? ????????? ?? ????????? ? ???????? Microsoft ?? ??????? ????? – ?????? 2011 - MSDN Blogs

  3. Уведомление: Технические материалы по продуктам и решениям Microsoft на русском языке – апрель 2011 | Alexander Knyazev: блог

Оставьте комментарий