?

Log in

No account? Create an account

Записки о геймдеве

C++ reflection - возвращение
joker_ru

Как заметили на форуме gamedev.ru, прошлая версия DynamicCast проигрывала стандартной версии при использовании на Intel процессорах, с длинными именами классов (>20 символов) , на AMD же все по прежнему было хорошо, это меня удивило и я стал разбираться.

Профайлер подсказал, что все дело в использовании стандартного strcmp. После использования самописного аналога - Intel и AMD снова работают как и ожидалось. Как приятный бонус скорость DynamicCast подросла почти на 50% и на моем AMD 3000+ в 4.5 раза быстрее чем стандартный cast.

Так же в качестве бонуса, по подсказке my.name,  сделал автоматическое определение базового класса (без указания его в макросе объявляющим метаданные) и перенес определение рефлекшн аттрибутов в хидер.

В общем вот: новая версия reflectionLibrary_v2


Данный пост размещен в моем личном блоге SergeyMakeev.com.

Математика / Геометрия
joker_ru

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


Данный пост размещен в моем личном блоге SergeyMakeev.com.

Unreal Development Kit - лайтмапы
joker_ru

Забавно, что в недавно вышедшем UDK текстуры лайтмапов пожаты как я писал в этом посте : Alpha-test высокого качества, только вместо альфы используют текстуры L8. Реально можно сильно сэкономить на разрешении лайтмапов, жалко у нас их нет в движке.


Данный пост размещен в моем личном блоге SergeyMakeev.com.

C++ reflection с блэк-джеком и шлюхами
joker_ru

Отсутствие Reflection в C++ порождает множество проблем, лично у меня, и возможно у многих людей, которые будут читать данный текст.

Первая проблема в том, что все функции оперирующие или выдающие как результат множества объектов должны работать с базовым типом, каким нибудь IEntity. Для примера: выбрали мы из мира с помощю AABB набор игровых объектов и хотим нанести урон, всем кто поддерживает интерфейс IDamageable, для этого обычно используют dynamic_cast или какой нибудь getType. На консолях, где RTTI в релизном билде обычно запрещен - приходится изобретать свои методы (см. библиотеку от Insomniac).

Вторая проблема, отсутсвие в С++ Property и средств для работы с ними. Движки по типу Unreal Engine3 и CryEngine приучают творческих людей работать с набором параметров у любого игрового объекта - будь то объект миссии, текстура или спецэффект, что совершенно логично и удобно.

Но вот незадача, в C++ отсутсвует возможноть получить какие либо метаданные класса - что бы узнать его поля / свойства и использовать это для управления объектом в реальном времени. Т.е. написание удобного и визуального средства редактирования C++ классов становится проблемой.

Существует множество вариантов решения, кто-то пишет уникальные редакторы для уникальных объектов, кто-то использует кодогенерацию мета-информации и классов, кто-то просто игнорирует проблему или пишет редактируемые объекты на скриптовых языках с поддержкой Reflection.

Третья проблема, система автоматического save / load любых объектов. Делают её обычно банально через virtual ::save(file * f) и virtual ::load (file *f) для всех объектов которым нужно уметь сохраняться, и с разной степенью успеха борются с проблемой при загрузке - как, имея имя класса в const char * создать экземпляр класса.

Чтобы решить эти проблемы один раз и навсегда, я написал маленькую библиотеку С++ reflection (скачать тут) которая позволяет удобно добавлять произвольную метаинформацию к любым классам, не требует специального препроцессора или кодогенерации и намного быстрее существующих средств в языке (dynamic_cast) или библиотек.

Вот пример объявления метаданных внутри cpp файла.

BEGIN_REFLECTION_METADATA(foo_class, base_class)
    ATTRIBUTE_FLOAT("myFloatProperty", &foo_class::setFloatFunc, &foo_class::getFloatFunc);
    ATTRIBUTE_INT("myIntProperty", &foo_class::setIntFunc, &foo_class::getIntFunc);
END_REFLECTION_METADATA(foo)
_Winnie C++ Colorizer

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

Пример использования dynamic_cast

base * base_ptr = new foo();
foo * foo_ptr = base_ptr->DynamicCast<foo>();
_Winnie C++ Colorizer

Плюсы и минусы библиотеки:
+ можно создавать классы по текстовому имени класса
+ получать список всех классов с метаинформацией и создавать их по метаинформации
+ получать список аттрибутов (property) для любого класса
+ быстрый dynamic_cast
+ получать метаинформацию из созданного экземпляра класса
+ аттрибуты поддерживают наследование
+ вся информация строится в compile time

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

Вот несколько примеров скорости работы:

"Xenon X360 (3.2 GHz)"
-------------------
dynamic_cast 9.52M casts per second
reflection::cast 18.94M casts per second

прирост быстродействия x 1.99

"AMD 64X2 3800+ (2.0GHz)"
-------------------
dynamic_cast 12.02M casts per second
reflection::cast 35.50M casts per second

прирост быстродействия x 2.95

"Core2Duo E6750 (2.66GHz)"
-------------------
dynamic_cast 26.49M casts per second
reflection::cast 45.70M casts per second

прирост быстродействия x 1.73
_Winnie C++ Colorizer

Буду рад мнениям и результатам тестов - скомпилированный тест производительности лежит внутри архива. Повторяю ссылку, что бы не искать её выше по тексту. (Библиотека reflection)

UPDATE: Обновил архив, ускорил получение метаданных из указателя на объект.


Данный пост размещен в моем личном блоге SergeyMakeev.com.

Миллион батчей в секунду на Xbox360
joker_ru

Во время очередного витка оптимизации на Xbox360, захотелось очень дешевых по производительности батчей - т.к. батчей в кадре было достаточно много, а CPU времени они кушали еще больше, при тысяче батчей в кадре тратилось 20-25 ms на установку констант и вызов DP. CPU лимит в 40-50 FPS явно не устраивал.

Как потом выяснилось часть оверхеда была от использования D3D Effects в качестве шейдерной системы. Большое количество Load Hit Store и двойное копирование всех констант, были одной из проблем.

Итак, задача: 30 тысяч строк написанных и отлаженых шейдеров которые нельзя менять и желание иметь в runtime подобие PS3 libGCM и полностью контролировать генерацию command buffer.
Я начал рыть в сторону PrecompiledCommandBuffer т.к. там явно была возможность записать CommandBuffer и потом отправить его в GPU. К сожалению, изменять параметры у записанного CB можно очень ограниченно, поэтому данный метод не подошел.

В результате экспериментов я получил дамп памяти GPU буфера и стал его разбирать ручками. К счастью, Xenos это развитие чипа ATI R500, а на R500 документация открыта (спасибо AMD/ATI). Очень быстро был готов парсер CB, который выдавал полную информацию, о том что лежит в буфере.

Выяснилось, что часть операций которые можно делать из D3D совсем по другому мапяться в железо, да и Runtime D3D сохраняет зеркальные стейты GPU, что тоже не бесплатно. Также паралельно выяснилось, что размер CB на каждом батче слишком большой, т.к. частично шейдеры эмбедились в CB и устанавливалось очень много констант (они всегда эмбедятся в буфер).

Как результат всех эксперементов я написал компилятор на входе берущий D3D Effect файл и генерирующий из него кусочки CB (кусочки что бы можно было не сабмитить уже установленные в GPU данные), так же runtime ремапинг адресов, сабмишн данных в GPU и всякий дополнительный код.

Теперь рендер батча стал просто заполнением буфера памяти, поборовшись Load-Hit-Store на заполнении памяти и сделав кеширование CB в системной памяти (общая память CPU и GPU на X360 и на PC в режиме WriteCombined, что отключает все кеши и ведет к разным пенальти из за непоследовательной записи) я получил хороший прирост производительности.

После всех основных оптимизаций, батчи стали стоить до 4ms на кадр (вместо 25ms), профайлинг показывал узкое место на доступе к памяти (констант было от 200 до 400 на батч). У ATI GPU есть очень приятная фича, на вход микрокода вершинного шейдера приходит номер индекса, и сборка вершины происходит в шейдере с возможностью random read из потока вершин. Я переделал большинство констант на vertex stream (данные vertex stream заполнялись в другом cpu потоке) и избавился от 90% констант при отрисовке, результат: 1-2ms из секунды это расходы на отрисовку 1000 батчей, на этом и остановился.

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

Итого:

Было: D3D Effects + D3D runtime
0.02ms batch * 1000 = 20ms на кадр (50000 batches per second)

Стало: Своя библиотека работы с GPU:
0.001ms batch * 1000 = 1ms на кадр (1000000 batches per second)


Данный пост размещен в моем личном блоге SergeyMakeev.com.

Много бесплатных текстур
joker_ru

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


Данный пост размещен в моем личном блоге SergeyMakeev.com.

Alpha-test высокого качества
joker_ru

Valve уже давно публиковали свой доклад SIGGRAPH 2007 “Improved Alpha-Tested Magnification for Vector Textures and Special Effects” о том, как очень получить сверх-качественные текстуры с альфа каналом, очень низкого разрешения.

Техника очень проста: берем альфа канал текстуры большого разрешения, к примеру 4096×4096 и уменьшаем до размера 64×64 или меньше со специальным фильтром.

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

В результате: получаетсся практически векторное качество альфа-маски текстуры и работает на любом GPU поддерживающем LINEAR фильтрацию и альфа-тест, что снова актуально учитывая всякие NintendoDS и iPhone.

Я накидал по данному докладу тестовый фильтр (скачать тут), результаты более чем хороши, попробуйте сами.

Пользоваться так:
DistanceToAlphaFilter исходная_текстура текстура_результат.tga resolution_x resolution_y

Для загрузки файлов используется D3DXCreateTextureFromFile, поэтому нужен файл D3DX9_39.dll (в комплекте). Форматы исходной текстуры могут быть: bmp, dds, dib, hdr, jpg, pfm, png, ppm, tga

Для отрисовки получившейся маски нужно установить следующие стейты:
ALPHABLEND = false;
ALPHATEST = true;
COLORARG1=texture;
ALPHAARG1=texture;


Данный пост размещен в моем личном блоге SergeyMakeev.com.

GDC09 free content
joker_ru

Тут лежит free content с GDC07, GDC08, GDC09 - в принципе не очень много, но есть всякие интересные доклады.

Вот тут, еще немножко есть http://msinilo.pl/blog/?p=345

И вот тут, еще про God of War 3


Данный пост размещен в моем личном блоге SergeyMakeev.com.

Полезный ключ cl.exe /EP
joker_ru

Раньше, когда я занимался в свободное время J2ME играми, очень напрягало отсутствие препроцессора в java, т.к. сложно было делать и поддерживать много микроизменений в коде требуемых издателями/особенностями телефонов и т.д. Тогда я сделал свой препроцессор для java с синтаксисом как у  C++,  недавно выяснилось - снова изобретал велосипед, но т.к. на момент написания я информации не нашел, напишу об этом тут:

У cl.exe есть отличный ключик /EP о котором естественно написано в MSDN. Так вот этот ключик обрабатывает препроцессром заданый файл и результат выдает в stdout. Обрабатывается файл полноценно, гораздо лучше чем мой велосипед, на который я еще и время зря тратил.

Файл: test.java

//test.java
//------------------------------------------------------------------
import java.io.*;
import javax.microedition.io.StreamConnection;
import javax.microedition.lcdui.*;
import javax.microedition.rms.*;

#ifdef NOKIA
import com.nokia.mid.ui.FullCanvas;
#endif

public class Game
#ifdef NOKIA
extends FullCanvas
#else
extends Canvas
#endif
implements Runnable
{
    public Game()
    {
#ifdef SOUND_SUPPORT
      #include "sound_check.java"
#endif
    }
}

//------------------------------------------------------------------
_Winnie C++ Colorizer

Файл: sound_check.java

//sound_check.java
//------------------------------------------------------------------

        //check if sound is supported
        Class c = null;
        try
        {
          c = Class.forName("javax.microedition.media.Player");
        } catch (Exception ex) { }

        if (c != null)
        {
            System.out.println("Sound supported");
            return true;
        }

        System.out.println("Sound NOT supported");

        System.out.println(__DATE__);

//------------------------------------------------------------------
_Winnie C++ Colorizer

И запустим на них: cl /C /I”c:\commonfiles\” /DNOKIA=1 /DSOUND_SUPPORT=1 /EP /Tctest.java > output.java в результате получим output.java который уже можно скармливать java компилятору.

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


Данный пост размещен в моем личном блоге SergeyMakeev.com.

Нормалмапы, как они есть
joker_ru

Все давно знают и используют у себя в движках нормал мапы. Художники делают хайрез, “снимают” карту нормалей и все при этом счастливы. Нормалмапы естественно в tangent space, что бы все как у людей было и про мэпинг артистам не думать. Но иногда, на модельках с нормалмапом появляются “швы” их как-то обычно забарывают, но особо про это никто не думает и даже на ушедших в печать играх часто можно увидеть швы на нормалмапе.

Так вот, если подумать, почему же появляются ошибки ? Для этого представим как все работает: запекалка нормал-мапа с хайреза, трассирует лучи и строит карту нормалей (из карты разницы высот или другим способом - неважно) но полученная карта нормалей получается в object space. Затем, запекалка нормал-мапа сама СЧИТАЕТ tangent space и используя посчитанные tangent матрицы, конвертирует полученные нормали в tangent пространство.
В этом и кроется подвох: если расчет tangent матриц у вас в игре и в запекалке карт нормалей отличается (а он 99% будет отличаться, т.к. в каких случаях разрывать мэпинг а в каких нет, даже в разных 3D пакетах реализованно по разному) в местах несовпадения tanget векторов будет шов. Обычно для устранения эффекта стараются не делать зеркальный мэпинг для моделек с нормал-мапом, это минимизирует ошибку, но не полностью её исключает.

Возникает логичный вопрос, что же делать ? Можно конечно пытаться “угадать”, как считается tangent пространство в модуле запекающем нормали в 3D пакете, но это не самый лучший выход на мой взгляд. К счастью недавно я обнаружил вот эту програмку, она умеет считать нормал-мапы используя CUDA и прочие радости и САМОЕ ГЛАВНОЕ позволяет написать свой генератор tangent матриц, такой же как у вас в игре. После этого про швы можно больше не думать. Надеюсь кому-нибудь это поможет уменьшить количество проблем в проекте. К сожалению alien интерфейс делает не очень казуальной использование софтинки, но привыкнуть можно.


Данный пост размещен в моем личном блоге SergeyMakeev.com.