Об RAII замолвите слово

 

Давненько я сюда не писал ничего технического, настало время заполнить сий пробел. Считаю важным зафиксировать здесь несколько заметок о технологии RAII (Resource Acquisition Is Initialization, получение ресурса есть инициализация). Еще информацию об этой технологии на просторах англоязычного интернета можно найти по названию Scoped-Based Resource Management (SBRM), по русски это будет звучать как-то так – управление ресурсами на основе области видимости.

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

Давайте проиллюстрируем важность и удобство техники на примере.

Например, у нас есть функция, которая получает мьютекс в начале и освобождает его в конце:

void foo( Mutex& mutex)
{
    mutex.acquire( );

    // выполняем какой-нибудь код

    mutex.release( );
}

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

void foo( Mutex& mutex )
{
    mutex.acquire( );

    // выполняем какой-нибудь код

    if ( shoudExit )
    {
        // неплохо бы сделать следующее
        mutex.release( );
        return;
    }

    // выполняем какой-нибудь код

    mutex.release( );
}

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

void foo( Mutex& mutex )
{
    mutex.acquire( );

    // очень много строк кода

    if ( newShouldExit )
    {
        // опаньки ...
        return;
    }

    // очень много строк кода

    mutex.release( );
}

Из примера видно, что такая техника управления ресурсами очень чувствительна к человеческой ошибке. Поэтому в таких случаях целесообразно применять RAII. Согласно стандарту С++ если мы определяем объект на стеке, его конструктор всегда будет выполнен в процессе инициализации, а деструктор в момент выхода из зоны видимости, то есть при return. Поэтому давайте использовать это соглашение в своих целях для управления ресурсами. Для начала напишем вспомогательный класс для автоматического получения и освобождения мьютекса:

class MutexLock
{
public:

    MutexLock( Mutex& mutex )
        :    _mutex( mutex )
    {
        _mutex.acquire( );
    }

    ~MutexLock( )
    {
        _mutex.release( );
    }

private:

    Mutex& _mutex;
}

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

void foo( Mutex& mutex )
{
    MutexLock lock( mutex );

    // очень много строк кода

    if ( newShouldExit )
    {
        // теперь нет необходимости освобождать мьютекс здесь
        return;
    }

    // очень много строк кода

    if ( shouldExit )
    {
        // и здесь тоже не надо
        return;
    }

    // очень много строка кода

    // и в конце также не надо
}

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

template < typename T >
class SmartPointer
{
public:

    SmartPointer( T* ptr )
        :    _ptr( ptr )
    {
    }

    ~SmartPointer( )
    {
        delete _ptr;
    }

    T& operator * ( )
    {
        return *_ptr;
    }

    T* operator -> ( )
    {
        return _ptr;
    }

private:

    T* _ptr;
}

Наша функция в свою очередь будет выглядеть так:

void foo( )
{
    // выделяем память
    SmartPointer ptr( new MyClass( ) );

    // очень много строк кода

    // это работает правильно, т.к. мы переопределили оператор ->
    ptr->DoSomething( );

    if ( shouldExit )
    {
        // память автоматически очищается в деструкторе
        return;
    }

    // очень много строк кода

    // память автоматически очищается здесь тоже
}

С памятью есть еще один интересный пример. Иногда требуется неким образом занять всю доступную память при запуске. Далее мы будем использовать эту память по своему усмотрению с помощью технологии placement new. Соответственно куски такой «сырой» превыделенной памяти мы назовем контекстами памяти. Соответственно мы имеем глобальный набор таких контекстов, после чего мы решаем, из какого контекста будет возвращен указатель на «выделенную» память.

Если дальше необходимо использовать какой-то контекст в рамках области видимости функции, то опять наблюдается уже знакомый паттерн:

class MemoryChunkUse
{
public:

    MemoryChunkUse( MemoryChunk& chunk )
        :    _chunk( chunk )
    {
        UseMemoryChunk( chunk );
    }

    ~MemoryChunkUse( )
    {
        ReleaseMemoryChunk( chunk );
    }

private:

    MemoryChunk& _chunk;
}

Функция будет выглядеть так:

void foo( )
{
    // используем контекст
    MemoryChunkUse useChunk( GRAPHICS_MEMORY_CHUNK );

    // очень много строк кода

    if ( shouldExit )
    {
        // здесь контекст памяти будет автоматически освобожден
        return;
    }

    // очень много строк кода

    // здесь контекст памяти также будет освобожден
}

С помощью технологии RAII вы заставляете компилятор освобождать ресурсы за вас, причем практически бесплатно. Круто, не так ли?

Выправляем буквы

 
 
 

После победного “да!” я решил нарисовать что-то посложнее. Например “пулѣ!” или “пПуУфФ” (который кстати показал всю ущербность расстановки букв). Однако не пулять, не пуф не дали правильного эффекта. Загрузив GIMP и сравнив то что получается в графическом редакторе стало ясно, что полный ахтунг. Увы, скринов не сделал поэтому не покажу. Но было не очень хорошо, или даже “очень не”.

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

Вооружившись листочком я начал искать корень зла (да, кстати он равен примерно 28). В итоге нашел. Текстурные координаты надо выбирать так, чтобы кроме глифа в нужный прямоугольник влезло еще немного пустого пространства (благо метрики шрифта это вполне позволяют), тогда не будет обрезок. Но самое главное, этот запас также надо проецировать и на вершинные координаты. Упражнения с математикой дали результат. Буквы нарисовались правильно, даже сравнение в GIMP’е это подтвердило. Но появилась новая проблема, хотя и понятная сразу – z-fighting. Ввиду дополнительного допуска, прямоугольники букв стали перекрываться. Но z-fighting в этом случае решается быстро – достаточно развести буквы по глубине и вуаля – z-fighting побежден. Кстати, для экранных координат оказалось достаточно значения 0.0001, для мировых – 0.001.

Ну и напоследок скрин. Белые буквы – то что получается в движке, красные (с 50% прозрачностью) наложены в GIMP’е – для сравнения. По моему – отлично!

puf

Да, да и еще раз да!

 
 
 

После того, как успешно получилось нарисовать строчку “да!” следующим шагом стало включение отрисовки шрифтов в конвейер. Причем задачу я поставил такую, чтобы отрисовка 2D (в экранных координатах) и 3D (в мировых) выполнялось как можно более идентичным кодом, а еще лучше – одним и тем же с парой настроек. Пришлось немного доработать конвейер и реализовать таки 2D камеру (до этого была только 3d, а 2d интерфейс рисовался с помощью пары хаков в рендере). Но в итоге результат превзошел все ожидания. Шейдеры (и пиксельный, и вершинный) – абсолютно одни и те же. Код объекта отрисовки (render entry) практически одинаковый, с одной лишь выборкой – брать матрицу проекции-вида из 2d или 3d камеры. Кроме того, благодаря такому объединению практически бесплатно получился еще и billboarding.

А теперь картинки, в нижнем углу маленькими буквами “да!” в экранных координатах. Справа – “да!” в мировых координатах, слева – “да!” в мировых координатах с применением технологии billboarding. Биллбоард однозначно видно по мере смещения и поворота камеры.

da1

da2

da3

P.S. К слову, полученные алгоритмы и шейдеры впоследствии пригодятся когда буду реализовывать системы частиц (Particle Systems)

Ну-ка буквы встаньте в ряд!

 
 
 

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

Но все проблемы были таки решены и сегодня показываю рендер строчек, пока рисуем только “да!”, отчасти от того, что конвейер движка еще не совсем готов рисовать буквы шейдерами, но я двигаюсь в этом направлении. Как только появится возможность рендерить строчки любые, нарисую что-нибудь посложнее и по длиннее. Пока же, только “да!”.

Кстати, выяснилось, что придется реализовывать traits для STL’евской basic_string чтобы она могла поддерживать правильно мой тип unichar (typedef unsigned long). Но об этом в другой раз.

И еще, то что буквы рисуются в “мировых” координатах – это я так захотел. При желании можно сделать так чтоб строки стали биллбордами, или же рисовать в экранных координатах. Все зависит только от матрицы проекции-вида. Здесь она для мира.

А теперь обещанные рендеры:

str_0

str_1

str_2

str_3

Продолжаем рисовать буковки, теперь и Юникод!

 
 
 

После того, как удалось нарисовать букву “g”, следующим логичным шагом было бы нарисовать буквы русские, т.е. Юникод. Однако возникает одна небольшая проблема. Имя ей – UTF-8, а точнее – отсутствие встроенных в С++ нормальных механизмов работы с unicode (wchar_t не в счет – это не совсем Юникод). Пока для целей тестирования были взяты определенные коды символов (буквы ё и ѣ), получены их utf-8 коды (0xd1 0×91 и 0xd1 0xa3 соответственно) и уже по этим кодам рассчитаны необходимые данные.

В дальнейшем же при визуализации текста стоит решить, какого подхода придерживаться. Можно использовать UTF-8 и const char, либо wchar_t. Первый вариант дает более компактное представление, но функции strcmp, strlen и т.д. будут работать неправильно. С точки же зрения wchar_t функции будут работать правильно, но в Windows sizeof(wchar_t) = 2, что не совсем Юникод, и кроме того, например, китайские символы – это три байта даже в UTF-8. Поэтому хочется const char и UTF-8, посмотрим что получится.

А теперь что получилось, буквы взяты специально экзотические

“ё”

bukva-Io

“ѣ” (ять, да-да тот самый ять :) )

bukva-Yat