Технология многопоточности для Ваших XNA игр Часть 3

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

На первый взгляд всё выглядит хорошо. Обмен буферами происходит в тот момент, когда активен только 1 поток. Поток обновления и прорисовки никогда не будут обращаться к одному и тому же буферу. Тяжело в это поверить, но так и есть. Из-за того, что буферы никогда не работают с одним и тем же буфером, некоторые проблемы могут появиться из-за кэширования. Когда поток обновления завершает свою работу, данные не всегда попадают в главную память. Процессор кэширует данные и задерживает их запись в главную память для того чтобы улучшить производительность. Но это значит, что некоторые данные могут не успеть записаться в память, из-за чего поток прорисовки может получить старые данные при чтении. Та же ситуация случается с потоком обновления. Процессор кэширует данные и содержимое буфера может не совпадать с содержимым памяти из-за кэширования. Так мы можем получить старые данные. Решением этой проблемы будет форсирование записи данных прямо в оперативную память. Это необходимая операция, для того чтобы убедиться в том, что в буферах всегда будет актуальные данные. Для этой цели мы будем использовать функцию Thread.MemoryBarrier(). В теории она должна располагаться в потоке обновления после произведения вычислений для обсчета следующего кадра, а также в потоке прорисовке перед тем, как получить содержимое буфера. Для перестраховки добавим вызов этой функции до и после всех вычислений. Для полноты картины следует упомянуть о том, что создание блокировки решило бы эту проблемы за нас, но использовать блокировку для данных к которым никогда не обращаются из двух потоков одновременно кажется неправильным. Таким образом, годится Thread.MemoryBarrier(). Последние 4 функции после сделанных изменений будут выглядеть так:

public
void StartUpdateProcessing(out ChangeBuffer updateBuffer, out GameTime gameTime)

{


//ждём начального сигнала


updateFrameStart.WaitOne();


//убеждаемся что берем данные не из кэша


Thread.MemoryBarrier();


//получаем буфер обновления


updateBuffer = buffers[currentUpdateBuffer];


//получаем игровое время


gameTime = this.gameTime;

}

public
void StartRenderProcessing(out ChangeBuffer renderBuffer, out GameTime gameTime)

{


//ждём начального сигнала


renderFrameStart.WaitOne();


// убеждаемся что берем данные не из кэша


Thread.MemoryBarrier();


//получаем буфер рендеринга


renderBuffer = buffers[currentRenderBuffer];


//возвращаем игровое время


gameTime = this.gameTime;

}

public
void SubmitUpdate()

{


// убеждаемся что берем данные не из кэша


Thread.MemoryBarrier();


//обновление готово


updateFrameEnd.Set();

}

public
void SubmitRender()

{


// убеждаемся что берем данные не из кэша


Thread.MemoryBarrier();


//прорисовка готова


renderFrameEnd.Set();

}

Я надеюсь, это поможет Вам понять, что на самом деле происходит с примитивами синхронизации.

Внимательный читатель наверняка заметил, что система использует 3 потока. Один поток для прорисовки, один для обновления, и один который вызывает функции GlobalStartFrame() и GlobalSynchronize() для синхронизации всего. Я так все организовал, потому что так проще для понимания. В примере далее по тексту статьи поток синхронизации будет объединён с потоком рендеринга, в то время как обновление будет производиться в другом потоке. Код, который мы будем на самом деле использовать в классе Game будет выглядеть примерно так:

Вызов GlobalStartFrame()

запуск кода прорисовки

вызов GlobalSynchronize()

Таким образом, AutoResetEvents на самом деле не необходимы, но я воспользовался ими, чтобы было понятней. Вы же можете обходиться с ними, как хотите.

3.3.3 Смена буферов и сообщения

Другой интересный аспект, особенно если Вы программируете для Xbox. Как Вы, возможно, знаете, при использовании XNA Game Studio полезно сводить количество мусора к минимуму. Но в нашей архитектуре необходимо генерировать ChangeMessage на каждом кадре. Часто в течение кадра будет создано много таких сообщений, в зависимости от сцены. Поэтому, очевидно, что эти сообщения должны быть не объектами а структурами, потому что структуры не пишутся в heap, и у нас не будет с мусором от них.

Как бы там ни было, в некоторых ситуациях работать с сообщениями как с объектами удобнее чем со структурами, и в таких случаях, если у нас есть возможность не обращать внимание на мусор, то вариант с объектами предпочтительней. В противном случае нам придётся иметь дело со структурами.

Первым вариантом видится создание структуры, содержащей все возможные варианты изменения, которые мы передаем от потока обновления потоку прорисовки. Вы наверняка понимаете, что это плохой вариант, потому как такая структура может быть достаточно большой. На решение, которое мы будем использовать, меня вдохновила презентация Frank Savage на Gamefest 2008, про производительность в XNA Game Studio. В своей презентации он показывает как в C# осуществить union’ы. Я понимаю, это звучит дико, но на самом деле это возможно. Некоторые из вас знают что такое union. Union — это структура данных, которая хранит один из нескольких типов данных в одном месте памяти. Например, когда мы объявляем union для хранения int и float, оба эти поля будут храниться в одном месте памяти. Не смотря на то что размер int — 4 байта и размер float — 4 байта, размер union, содержащего обоих будет тоже 4 байта, а не 8 как структура. Таким образом, в момент присвоения значения union’у int или float оба значения будут записаны в одно и то же местоположение. На первый взгляд это может показаться бесполезным, но на самом деле это пригодится в нашем случае. В то время как структура сообщения остается той же, мы можем интерпретировать данные в ней в любом нужном типе. Надеюсь что Вам все понятно, в противном же случае ситуация прояснится когда появится пример кода.

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

  • UpdateCameraView, который мы будем использовать чтобы задать потоку прорисовки новую матрицу View для использования с камерой
  • UpdateWorldMatrix, который мы будем использовать для обновления матрицы мира у объекта
  • UpdateHighlightColor, с помощью которого мы будем обновлять цвет подсветки у объекта
  • CreateNewRenderData, который мы будем использовать как сигнал к созданию нового объекта для прорисовки. Сам объект передаем параметром
  • DeleteRenderData, которым мы будем сигнализировать о том, что некий объект был уничтожен и его не надо больше прорисовывать

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

public
enum ChangeMessageType

{


UpdateCameraView,


UpdateWorldMatrix,


UpdateHighlightColor,


CreateNewRenderData,


DeleteRenderData,

}

Далее мы определяем структуру сообщения. Для того чтобы убедиться в том, что структура ведет себя как union, надо проделать несколько шагов. Во-первых, надо добавить атрибут [StructLayout(LayoutKind.Explicit)] к определению структуры. Это позволяет нам задавать сдвиг в памяти куда писать каждое поле и откуда читать его значение. В нашей структуре это будет ChangeMessageType, который будет показывать как должна будет интерпретироваться инстанция этой структуры. Это поле будет иметь сдвиг 0, потому как это первое поле в структуре. Мы должны убедиться в том, что больше никакое поле не использует этот адрес. Размер типа ChangeMessageType равен 4 байтам, поэтому все последующие поля должны иметь сдвиг больший 4х. Теперь мы продолжим добавлять поля для этой структуры, базируясь на вышеопределённых типах сообщений.

  • Сообщению типа UpdateCameraView необходимо передать матрицу для прорисовки, поэтому добавляем поле Matrix со сдвигом 4.

Заметим, что все последующие типы сообщений требуют в качестве параметра объекта. Как мы сказали ранее, мы идентифицируем эти объекты с помощью идентификатора типа int. Поэтому добавляем поле типа int со сдвигом 4. Индивидуальное поле каждого последующего типа будет начинаться со сдвига 8.

  • для сообщения UpdateWorldMatrix необходима матрица. Поэтому добавляем поле типа Matrix со сдвигом 8
  • Для сообщения типа UpdateHighlightColor требуется Vector4, содержащий новый цвет. Добавляем поле типа Vector4 со сдвигом 8
  • CreateNewRenderData посылает позицию и цвет, которые будут использованы для создания нового объекта RenderData в потоке прорисовке. Мы добавляем поле позиции со сдвигом 8 и поле цвета со сдвигом 20 (Vector3 занимает 12 байт)
  • Наконец DeleteRenderData не требует никаких дополнительных полей кроме поля с ID объекта.

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

[StructLayout(LayoutKind.Explicit)]

public
struct ChangeMessage

{


//это поле есть во всех сообщениях


//identifies обозначает тип сообщения


[FieldOffset(0)]


public ChangeMessageType MessageType;


//поле нужно, когда используется тип UpdateCameraView


[FieldOffset(4)]


public Matrix CameraViewMatrix;


//поле используется со всеми типами сообщений


[FieldOffset(4)]


public
int ID;


//поле, необходимое для UpdateWorldMatrix


[FieldOffset(8)]


public Matrix WorldMatrix;


//поле, необходимое для UpdateHighlightColor


[FieldOffset(8)]


public Vector4 HighlightColor;


//, необходимое для CreateNewRenderData


[FieldOffset(8)]


public Vector3 Position;


[FieldOffset(20)]


public Vector3 Color;


//для DeleteRenderData никаких полей не нужно

Ниже Вы можете увидеть как эти типы будут расположены в памяти.


Как вы можете видеть, полный объём структуры всего лишь 72 байта, но может быть использован для 5 разных типов сообщений. Например, представим что поток обновления создает 2 следующих сообщения:

//создаем сообщения для обновления матрицы камеры

ChangeMessage updateCamera = new ChangeMessage();

updateCamera.MessageType = ChangeMessageType.UpdateCameraView;

updateCamera.CameraViewMatrix = Matrix.CreateLookAt(…);

//создаем сообщения для обновления мировых координат объекта с индексом 5

ChangeMessage updateWorld = new ChangeMessage();

updateWorld.MessageType = ChangeMessageType.UpdateWorldMatrix;

updateWorld.ID = 5;

updateWorld.UpdatedWorldMatrix = Matrix.CreateTranslation(…);

Как Вы видите, структура используется в одном случае как сообщение типа UpdateCameraView, а в другом как сообщение UpdateWorldMatrix. Когда мы используем её как UpdateCameraView, нам необходимо заполнить только нужные поля. Теперь положим что эти сообщения кладутся в буфер и затем поток прорисовки берет каждое сообщение из буфера и анализирует его. Код будет выглядеть примерно так:

switch (msg.MessageType)

{


case ChangeMessageType.UpdateWorldMatrix:


camera.View = msg.CameraViewMatrix;


break;


case ChangeMessageType.UpdateCameraView:


renderObjects[msg.ID].World = msg.UpdatedWorldMatrix;


break;


[…]

}

Итак, основываясь на msg.MessageType мы можем интерпретировать сообщение нужным образом и использовать только необходимые поля.

Определив структуру ChangeMessage, буфер будет просто содержать список таких сообщений.

public
class ChangeBuffer

{


public List<ChangeMessage> Messages { get; set; }


public ChangeBuffer()


{


Messages = new List<ChangeMessage>();


}


public
void Add(ChangeMessage msg)


{


Messages.Add(msg);


}


public
void Clear()


{


Messages.Clear();


}

}

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

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

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s