8 февр. 2014 г.

Git-p4 работа с Perforce через Git

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

   Но, лично для меня привыкшего к использованию Git оказалось довольно трудным обходится без тех возможностей которые он дает. Удобная работа с ветками (branch), их слияние (merge), быстрое переключение между ними, откат изменений (revert) и правка последних комитов это, и многое другое, действительно упрощает процесс разработки делая его более удобным, быстрым и гибким.

   Конечно, всегда можно взять необходимую часть проекта и просто положить под контроль Git’а. И все будет идти хорошо ровно до того момента, когда нужно будет как-то взаимодействовать с другими разработчиками которые пользуются только Perforce. Это достаточно неудобно...

   К счастью, есть написанный на питоне скрипт "git-p4" который дает возможность непосредственно взаимодействовать с сервером Perforce используя git-p4 команды. Этот скрипт достаточно сохранить (можно без расширения *.py) в папку "C:\Program Files (x86)\Git\bin" (она должна быть включена в переменной среде PATH). И в этой же папке, необходимо создать пакетный файл (batch file)
"git-p4.bat" со следующим содержимым:
@python "%~d0%~p0git-p4" %*
Это позволит обойти отсутствие поддержки Python в последних версиях "Git for Windows".
Также, в системе должен быть установлен сам Python и Perforce command line tools.

Если все правильно, то набрав в консоли git-p4 увидим:
C:\Users\LVI>git-p4
usage: C:\Program Files (x86)\Git\bin\git-p4 <command> [options]
valid commands: clone, rollback, debug, commit, rebase, branches, sync, submit
Try C:\Program Files (x86)\Git\bin\git-p4 <command> --help for command specific help.
Несколько сопоставлений с Git командами:
  • git push --- git-p4 submit
  • git fetch --- git-p4 sync
  • git pull --- git-p4 rebase
Перед инициализацией Git репозитория, сперва для него нужно создать отдельный Workspace в Perforce. Это можно сделать несколькими способами: через P4V Visual Client, объявив переменную среды P4CLIENT или создав файл p4config.txt (в корне Workspace директории):
P4CLIENT=GitP4client
P4USER=username
P4PORT=perforce:1666
Теперь, рабочий процесс будет сводится к следующему:

1) Чтобы клонировать необходимую часть проекта, пишем в bat файле или в консоли:
p4 set P4CONFIG=p4config.txt
p4 client
git-p4 clone --use-client-spec --detect-branches //depot/some/path/xxx_project
В первой строчке задаем текущий конфиг (поиск файла p4config.txt будет вестись начиная с Workspace "Root" директории и продолжится до корня диска), вторая откроет client spec в котором можно будет задать workspace mapping.

2) Комитим изменения локально в Git (обычная работа с Git так, как привыкли)
git commit some_file
Единственное замечание, вместо "git merge" лучше использовать "git rebase".

3) Забираем последние изменения из Perforce depot’а:
git-p4 rebase
4) Сабмитим локальные изменения в Perforce depot:
git-p4 submit
Перед каждым сабмитом в Perforce, обязательно делаем "git-p4 rebase".

Links:
How to use Git-P4: http://answers.perforce.com/articles/KB_Article/Git-P4#working_in_git
Git-p4 Docs: http://git-scm.com/docs/git-p4
Git-p4 Wiki: https://git.wiki.kernel.org/index.php/Git-p4_Usage
Git for Windows: http://git-scm.com/download

21 авг. 2012 г.

Система рендеринга текста OpenGL 3

Работая над собственной системой GUI у меня в svn выделилась целая ветвь, связанная с работой со шрифтами и рендерингом текста. В итоге это перерасло в отдельную библиотеку (FSGL3), которую я планирую использовать в своих небольших проектах для отображения различной текстовой информации.
Целевыми платформами являются Windows и OS X последних версий.
Библиотека основана на современном GAPI OpenGL 3.2 и в ней нет ни строчки из STL.
В процессе выбора лучшего решения ее дизайна и архитектуры у меня собралось немного материала, который я решил выложить здесь.

Немного теории

Рендеринг текста

Наиболее распространенный подход рендеринга текста основан на растеризации векторного представления символов в растр (карту битов 'bitmap') и, в дальнейшем, отображение каждого символа в виде прямоугольника ('quad'), состоящего из двух треугольников.
Этот метод хорош по производительности и дает множество путей для его реализации, и возможностей по оптимизации.
Для отображения 2D текста особенно выгодно использовать однородные координаты (clip space), которые находятся в пределах [-1, 1]. Это позволяет свести к минимуму вычисления, не используя матричные преобразования.

Для перевода вершин прямоугольника quad’а из абсолютных экранных координат (screen space) в однородные координаты (clip space) мы можем просто воспользоваться уравнениями:
где sX и sY - экранные координаты с началом (0,0) в верхнем левом углу.

Выбор способа растеризации шрифтов

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

Выбор подхода для кэширования данных

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

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

Но если нам необходимо отобразить текстовую информацию на заранее известном языке или языках, которая меняется не часто (статический текст), то выполнять поиск для каждого символа по кэшу - это ненужные накладные расходы. Гораздо лучше будет, если речь идет об Unicode, используя код символа, выполнять проверку его принадлежности только к конкретному диапазону (ASCII, CYRILIC и т.д.) т.е. да, сохранять данные сразу для нескольких заведомо используемых диапазонов символов. Так как ASCII символы нам нужны всегда, то это будет ASCII(0-127) + доп. диапазон (например: Cyrillic(1024-1279)) + ... .
При значительном объеме отображаемого текста процент использования символов будет очень большим. Кроме того, если текст вообще всегда статичен, то имеет смысл его отрендерить в текстуру и отображать за один раз.

Детали реализации

Поскольку в библиотеке используется функционал OpenGL 3.2, то нам не обойтись без шейдерных программ.
Vertex Shader :
#version 150 core

precision highp float;
// поскольку Mac OS, пока, потдержывает только OpenGL 3.2 то
// приходится явно включать расширение для работы с лэйаутами(layout)
#extension GL_ARB_explicit_attrib_location : enable

#define ATTRIB_POSITION  0
layout(location = ATTRIB_POSITION) in vec4 g_Vertex;

uniform vec2 u_Scale;
uniform vec4 u_Color;

out vec2 v_TexCoord0;
out vec4 v_Color;

void main( void )
{
    // переводим координаты из "screen-space" в "clip-space"
    gl_Position = vec4(-1 + (g_Vertex.x * u_Scale.x), 1 - (g_Vertex.y * u_Scale.y), 0, 1);
    v_Color = (u_Color / 255);
    // uv = zw
    v_TexCoord0 = g_Vertex.zw;
}
Здесь, поскольку у нас только 2D текст, для оптимизации, мы передаем позицию вершин и текстурные координаты в одном атрибуте (g_Vertex) и далее разделяем их. Коэффициент масштабирования текста передается через uniform переменную u_Scale, значение которой рассчитывается в методе SetScreenSize:
void CFontSystem::SetScreenSize(int sWidth, int sHeight)
{
    m_fontShader.Begin_Use();

    m_scaleX = 2.0 / (float)sWidth;
    m_scaleY = 2.0 / (float)sHeight;

    m_fontShader.Set_Float2(m_scaleX, m_scaleY, "u_Scale");
}
Этот метод должен вызываться каждый раз при изменении размеров окна для корректного отображения символов.
Также стоит отметить, что по возможности, большинство вычислений стоит производить именно в вершинном шейдере.

Лучший способ отобразить простой 2D текст - это использовать для него текстуру, которая содержит только значение альфа-канала (alpha = 1 (непрозрачность) - рисуется цвет текста, alpha = 0 (прозрачность) - рисуется цвет фона, когда значение alpha лежит в пределах 0-1, цвет текста смешивается с цветом фона).
Fragment Shader :
#version 150 core

uniform sampler2D textureUnit0;

// текстурные координаты и данные цвета из вершинного шейдера
in vec2 v_TexCoord0;
in vec4 v_Color;
// результирующий цвет пикселя
out vec4 FragColor;

void main( void )
{
    FragColor = vec4(1, 1, 1, texture(textureUnit0, v_TexCoord0).r ) * v_Color;
}
В этот шейдер на вход поступает "grayscale" текстура шрифта, содержащая все глифы (glyph) символов, у которой берется значение альфа-канала. Далее устанавливается цвет (rgb), полученный из вершинного шейдера.


Сборка атрибутов вершин для текста

Общий метод "сборки" атрибутов вершин для динамического и статического текста выглядит следующим образом:
inline void PointerInc(float *& pData, const float &pX, const float &pY, const float &u, const float &v)
{
    float *p = pData;
    *p++ = pX;
    *p++ = pY;
    *p++ = u;
    *p   = v;

    pData += 4;
}

template < typename T >
void CFontSystem::BuildTextVertices(const T* text)
{
    // получаем шрифт по ID
    CFont &font = *g_pFontManager.GetFontByID(m_hFont);

    // получаем высоту строки шрифта
    const int height = font.Height();

    int x = 0, y = 0;
    // текущие координаты для отображения
    int posX = m_DrawTextPos[0];
    int posY = m_DrawTextPos[1];

    m_VertexPerFrame += m_VertexCount;
    m_VertexCount = 0;
    
    // обрабатываем текстовую информацию
    for (int i = 0; i < m_TextLenght; ++i)
    {
        const T &Ch = text[i];
        
        // выставляем кэш для текущего символа
        if ( !font.AssignCacheForChar( Ch ) )
            continue;
        
        // получаем метрики символа
        const GlyphDesc_t &g = *font.GetGlyphDesc();
         
        // корректируем позицию на экране с учетом метрик символа
        x = posX + g.bitmapLeft;
        y = (posY + height) - g.bitmapTop;
        
        // переводим курсор на следующий символ
        posX += g.advanceX;
        posY += g.advanceY;
        
        // если встретили неотображаемый символ
        if (iswspace(Ch))
        {   
            // проверяем не является ли он символом новой строки
            if (Ch == '\n') {
                posX = m_DrawTextPos[0];
                posY += height;
            }
            continue;
        }

        const int w = g.bitmapWidth;
        const int h = g.bitmapHeight;

        // получаем текстурные координаты для символа из кэша
        const float *pTex = font.GetTexCoords();

        // записываем данные вершин начиная с указателя pBaseVertex
        PointerInc(pBaseVertex, x,     y,     pTex[ 0], pTex[1]);
        PointerInc(pBaseVertex, x,     y + h, pTex[ 2], pTex[3]);
        PointerInc(pBaseVertex, x + w, y,     pTex[ 4], pTex[5]);
        PointerInc(pBaseVertex, x + w, y,     pTex[ 6], pTex[7]);
        PointerInc(pBaseVertex, x,     y + h, pTex[ 8], pTex[9]);
        PointerInc(pBaseVertex, x + w, y + h, pTex[10], pTex[11]);

        m_VertexCount += 6;
    }
}
В этом методе для всех символов переданного текста мы получаем данные атрибутов вершин из кэша шрифта и записываем их по указателю pBaseVertex. Указатель pBaseVertex указывает (тавтология :)) на начало той области памяти, куда именно мы хотим записать наши данные вершин.


Вершинный буфер для статического текста

Все данные вершин статического текста хранятся в одном Vertex Buffer Object(VBO) формата (x,y,u,v .. x,y,u,v). Для возможности разделять начало данных вершин очередного текстового блока используется следующая структура:
//-----------------------------------------------------------------------------
// Информацию о статическом тексте
//-----------------------------------------------------------------------------
struct StaticTextInfo_t
{
    // первая вершина для блока текста в VBO
    unsigned short firstVertex;
    // общее количество вершин для текста
    unsigned short countVertex;
    // цвет текста
    float color[4];
};
Метод работы с вершинным буфером для статического текста:
template < typename T >
int CFontSystem::BuildStaticText(const T* text)
{
    // проверяем достаточно ли у нас памяти для переданного текста
    // 6 vertices * float4(x,y,s,t) * sizeof(float) = 96
    if ( !text || (m_StaticFreeVMem < (m_TextLenght * 96)))
        return -1;
        
    glBindBuffer(GL_ARRAY_BUFFER, m_VertexBufferId[0]);

    // получаем указатель на начало буфера
    void* pVM = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);

    if ( !pVM )
    {
        fprintf(stderr, "OpenGL Error glMapBuffer");
        return -1;
    }
    
    // получаем указатель на свободную часть буфера
    pBaseVertex = (float*)pVM + (m_CurrStaticVertex * 4);

    // получаем данные вершин для текста и записываем их по указателю
    BuildTextVertices < T > (text);

    glUnmapBuffer(GL_ARRAY_BUFFER);

    glBindBuffer(GL_ARRAY_BUFFER, 0);

    StaticTextInfo_t ti;
    
    ti.countVertex = m_VertexCount;
    ti.firstVertex = m_CurrStaticVertex;
    ti.color[0] = m_DrawTextColor[0];
    ti.color[1] = m_DrawTextColor[1];
    ti.color[2] = m_DrawTextColor[2];
    ti.color[3] = m_DrawTextColor[3];

    m_CurrStaticVertex += m_VertexCount;

    // m_VertexCount *(float4(x,y,s,t) * sizeof(float))
    m_StaticFreeVMem -= m_VertexCount * 16;

    m_StaticTextInfo.Append(ti);

    return  m_StaticTextInfo.Num();
}
Метод отображения статического текста:
void CFontSystem::PrintStaticText(const int idText)
{

    /* ... */

    // индекс в массиве
    const int index = idText - 1;

    if((numString - 1) < index)
        return;

    StaticTextInfo_t &dti = m_StaticTextInfo[index];
    
    // передаем данные о цвете
    m_fontShader.Set_Float4v(dti.color, m_UnifColor);

    glDrawArrays(GL_TRIANGLES, dti.firstVertex, dti.countVertex);

    /* ... */
}

Получение данных символов в кэше

Для проверки принадлежности символа к конкретному диапазону таблицы Unicode определим структуру, содержащую информацию о нижней и верхней границах диапазона символов и массив из этих диапазонов:
struct  UCharacterRange
{
    int lowRange;   // нижняя граница диапазона в Unicode таблице
    int upperRange; // верхняя граница диапазона в Unicode таблице
    int chOffset;   // количество символов до начала следующего диапазона
};
std::vector<UCharacterRange> m_UChRanges;
Теперь поиск для каждого символа будет сводиться только к определению - к какому из диапазонов он принадлежит:
int CFont::FindCharInCache(wchar_t wch) const
{
    for (int i = 0; i < m_iNumRanges; ++i)
    {
        // если символ попадает в диапазон шрифта
        if ((wch >= m_UChRanges[i].lowRange) && (wch <= m_UChRanges[i].upperRange))
        {
            // возвращаем его индекс в кэше
            return m_UChRanges[i].chOffset + (wch - m_UChRanges[i].lowRange);
        }
    }
    return 0;
}
Как правило, таких диапазонов достаточно не более десятка, так что прямой перебор в цикле вполне подойдет.

Использование FSGL3

1. Инициализация и задание параметров


Вызываем метод инициализации библиотеки:
FontSystem().Initialize();
Задаем масштабирование для шрифта, передавая размеры области отображения:
FontSystem().SetScreenSize( SCREEN_WIDTH, SCREEN_HEIGHT );
Загружаем шрифт и получаем его дескриптор, по которому, в дальнейшем, можно будет определять каким шрифтом отображать текст:
HFont hFreeSans_14 = FontSystem().Create_Font("YourPath/FreeSans.ttf", 14);
assert(hFreeSans_14 != INVALID_FONT);
Добавляем желаемые диапазоны символов для шрифтов, используя полученные дескрипторы:
FontSystem().AddGlyphSetToFont( hFreeSans_14, BASIC_LATIN );
FontSystem().AddGlyphSetToFont( hFreeSans_14, CYRILLIC );
// или
FontSystem().AddGlyphSetToFont( hFreeSans_14, 31, 127 );
FontSystem().AddGlyphSetToFont( hFreeSans_14, 1024, 1104 );
Порядок добавления не важен, главное чтобы присутствовал ASCII диапазон, а то получите интересный результат :)  После добавления всех интересующих диапазонов, необходимо просто вызвать:
FontSystem().BuildAllFonts();
В этом методе происходит получение метрики для всех глифов, определение высоты текстуры шрифта, создается атлас шрифта, в него копируется bitmap каждого глифа, расчитуются текстурные координаты, строится кэш.

При необходимости можно задавать ширину текстурного атласа шрифта (по умолчанию 1024):
SetFontTextureWidth( 512 );
Полученная текстура для данных диапазонов:
Для того, чтобы иметь возможность обходиться без функционала FreeType, можно сохранять кэш отдельного шрифта или шрифтов (используя их дескрипторы) на диск (сохраняются только метрики глифов + текстура):
FontSystem().DumpFontCache( hFreeSans_14, "./YourPath/" );
Для загрузки:
hFreeSans_14 = FontSystem().LoadFontCache( "./YourPath/FreeSans_14.cfnt" );
assert(hFreeSans_14 != INVALID_FONT);
После загрузки всех файлов вызываем функцию построения кэша шрифтов:
FontSystem().BuildCache();

2. Отображение текста

// Строка unicode текста
const wchar_t WText[] = L"Hello World\nили Здравствуй мир! :)";

// Выставляем дескриптор (Handle) нужного шрифта
FontSystem().BindFont( hFreeSans_14 );

// Задаем позицию для текста
FontSystem().SetTextPos(100, 25);
    
// Указываем цвет
FontSystem().SetTextColor(255, 0, 200);

// Передаем строку текста и получаем для нее ID
int id_SText = FontSystem().SetStaticWText(WText, wcslen(WText));

while( running )
{
    glClearColor(0.7, 0.7, 0.7, 1);
    glClear(GL_COLOR_BUFFER_BIT);
        
    // Биндим шейдерную программу, сбрасываем все пред. состояния и т.д.
    FontSystem().EnableStateDraw();
        
    // Отображаем статический текст
    FontSystem().PrintStaticText( id_SText );
        
    // Выставляем Handle следующего шрифта
    FontSystem().BindFont( hVerdanaB_11 );

    FontSystem().SetTextColor(0, 0, 255);
    FontSystem().SetTextPos(100, 10);
        
    std::string time;
    NowTime(time);
        
    // Отображаем динамическую строку текста
    FontSystem().PrintText(time.c_str(), time.length());
        
    FontSystem().DisableStateDraw();

    /* ... */
}

Результат:


Для любого текста можно получить его BBox. Для этого, к примеру, загружаем текст из файла:
std::wstring wstr = ReadWholeFileIntoString( "SomeFile.txt" );
int idText = FontSystem.SetStaticWText( wstr.c_str(), wstr.lenght() );
Получаем ограничивающий прямоугольник (bounding box) для этого текста:
BBox_t bbox;
FontSystem().GetWTextBBox( wstr.c_str(), wstr.lenght(), bbox );
Далее отображаем:
FontSystem().DrawOutLinedRect( bbox );
FontSystem().PrintStaticText( idText );

TODO list:
Добавление эффектов blur, outline, scanline, dropshadow, свободное масштабирование текста с применением технологии distance fields, поддержка кернинга.
По оптимизации попробую добавить instancing или geometry shader для генерации геометрии квада.

Технические детали
Поскольку библиотека не использует C runtime library, то не имеет значения, какая именно версия проекта собирается (single-threaded, multi-threaded или multi-threaded DLL). Это позволяет собирать все проекты, использующие библиотеку с такими же версиями C runtime.

При сборке для Mac OS, FreeType упорно не хотела линковаться статически, в итоге  нормально собралось со следующими опциями:
./configure --prefix=/ft2 CFLAGS="-Os -arch x86_64" --disable-shared --without-bzip2

Links:

29 мар. 2012 г.

Простая система логирования на C++

    Система логирования - нужный и полезный инструмент для протоколирования работы любой программы. Она значительно экономит время, проведенное под отладкой и вылавливанием различных багов.
Существует много готовых библиотек (log4cplus, Apache log4cxx и тд.), однако в ряде случаев гораздо удобней и полезней иметь свою реализацию, которую можно в дальнейшем модифицировать под конкретную задачу.
И так, сначала определимся с требованиями к ней. Это удобство использования, эффективность применения, возможность расширения и дальнейших модификаций, типобезопасность и, по возможности, кросс платформеннсть. Также она будет поддерживать несколько уровней детализации сообщений (info, error, warning, debug ...). 
Для вывода форматированного текста мы воспользуемся функционалом  std::ostringstream, в нашем классе лога это переменная “m_oss”, которая будет накапливать в себе все сообщения. Функция член класса GetMsg() будет обеспечивать доступ к этой переменной.
Допустим мы хотим, чтобы сообщения могли выводиться как в консоль, так и в файл лога. Для этого заведем два класса: один будет отвечать за сбор сообщений, а второй, куда и как именно они будут выводиться.
Листинг шаблонного класса лога:
// уровни протоколирования
enum LogLevel_t { logError, logWarning, logInfo, logDebug,
                  logDebug1, logDebug2, logDebug3 };

template < typename T >
class Logger {
public:
    Logger( void );
    virtual ~Logger( void );

    std::ostringstream& GetMsg(LogLevel_t level = logInfo);
    // устанавливает уровень детализации
    static LogLevel_t& ReportingLevel( void );
 
    static const char* LevelToString(LogLevel_t level);
    static LogLevel_t LevelFromString(const char* ch_level);

protected:
    std::ostringstream m_oss;
private:
    Logger(const Logger&);
    Logger& operator = (const Logger&);
};

16 янв. 2012 г.

Оптимизация мэша, вершинный кэш

При экспорте 3d моделей из редактора трехмерной графики или при чтении из таких форматов как 3ds, collada, fbx, ..., зачастую их данные содержат дублирующиеся вершины.
    Поэтому лучше при экспорте выполнять проверку на наличие таких вершин и сохранять только уникальные данные пропуская вертексы, у которых все вершинные атрибуты совпадают.
Если вершины мэша содержат только координаты, то оптимизация выглядела бы так:
Но как правило, каждая вершина, имеет несколько вершинных атрибутов (нормаль, тангенс/битангенс, текстурные координаты, цвет), поэтому она будет считаться уникальной только если совпадают все атрибуты.
    Алгоритм удаления дублирующихся вершин достаточно прост: каждую вершину в сетке модели необходимо проверить на уникальность, сравнивая все ее атрибуты с атрибутами других вершин, если они совпадают, то сравниваемая вершина пропускается, но при этом добавляется индекс уже имеющейся в новом наборе.