Буфера в OpenGL, Расширение ARB_vertex_buffer_object.

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

Если с текстурами все достаточно просто - они один раз загружаются в память GPU и больше не изменяются, то с геометрическими данными (координаты вершин, нормали, текстурные координаты) дело обстоит значительно хуже.

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

Можно заметно повысить эффективность передачи данных при помощи использования так называемых вершинных массивов (vertex arrays). При этом данные хранятся в памяти CPU и ускорителю передается только указатель на них и размер.

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

Однако этот способ требует постоянной передачи массивов данных от CPU к GPU - каждый вызов, в котором используется указатель на массив приводит к передаче его графическому ускорителю и именно эта постоянная необходимость передачи большого объема данных через шину PCI Express ограничивает эффективность приложения.

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

Поэтому было бы гораздо эффективнее сразу загрузить такие данные в память GPU, после чего использование этих данных графическим ускорителем не будет требовать их передачи от CPU.

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

Для этого блоки данных помещаются в так называемые вершинные буфера (vertex buffer objects, VBO), при этом каждый такой буфер представляет из себя просто массив байт.

Пользователю предоставляется два пути для работы с вершинным буфером. При использовании первого пути данные через команды OpenGL (glBufferDataARB, glBufferSubDataARB, glGetBufferSubDataARB) передаются сразу большими блоками в/из GPU.

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

Вершинный буфер может быть использован в любой команде OpenGL, принимающей указатель на данные. Использование вершинных буфером в этих командах позволяет GPU более эффективно работать с памятью, повышая быстродействие приложения.

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

	isExtensionSupported ( "GL_ARB_vertex_buffer_object" )

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

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

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

    void glGenBuffersARB ( GLsizei n, GLuint * buffers );

Этот запрос выделяет n свободных идентификаторов и записывает их в массив buffers.

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

    void glBindBufferARB ( GLenum target, GLuint buffer );

Параметр target задает тип данных, которые будет содержать буфер. Изначально поддерживались только два типа - информацию о вершинах (GL_ARRAY_BUFFER_ARB) или индексы в другие массивы (GL_ELEMENTS_ARRAY_BUFFER_ARB). Сейчас добавлено много новых типов - см. расширения EXT_pixel_buffer_object, EXT_texture_buffer_object и EXT_bindable_uniform.

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

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

    void glDeleteBuffersARB ( GLsizei n, const GLuint * buffers );

Данная команда уничтожает сразу n буферов, идентификаторы которых содержатся в массиве buffers.

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

    void glBufferDataARB ( GLenum target, GLsizeiptrARB size, const void * data, GLenum usage );

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

Обратите внимание на использование типа GLsizeiptrARB (GLintptrARB) вместо обычных целочисленных типов. Это связано с поддержкой 64-битовых значений.

Последний параметр, usage, несет в себе информацию о предполагаемом использовании буфера. Эта информация является лишь "намеком" и служит для того, чтобы система могла более эффективно управлять выделением памяти для вершинного буфера. Этот параметр никак не ограничивает пользователя в использовании буфера и предназначен лишь для целей оптимизации. Он может принимать следующие значения - GL_STREAM_DRAW_ARB, GL_DYNAMIC_DRAW_ARB, GL_STATIC_DRAW_ARB, GL_STREAM_READ_ARB, GL_DYNAMIC_READ_ARB, GL_STATIC_READ_ARB, GL_STREAM_COPY_ARB, GL_DYNAMIC_COPY_ARB и GL_STATIC_COPY_ARB.

Каждая из этих констант несет в себе информацию как о том, для чего будет использоваться буфер (draw, read, copy), так и о частоте изменения его содержимого (stream, dynamic, static). Следющие две таблицы разъясняют смысл использованных констант.

Таблица 1. Цель использования буфера.

НазначениеКомментарий
DRAWБуфер будет использоваться для передачи данных GPU, например для вывода объектов
READБуфер будет использоваться пользователем для чтения из него. Важна скорость для чтения данных CPU
COPYБудет использоваться как для чтения данных из GPU так и для вывода объектов

Таблица 2. Частота изменения содержимого буфера.

Частота обновления/использованияКомментарий
STREAMПредполагается что на каждый вывод данных будет их изменение
DYNAMICПредполагается, что будет частое как использование, так и изменение содержимого буфера
STATICПредполагается, что данные будут один заданы и потом много раз использованы

Для изменения данных в уже существующем буфере служит команда

    void glBufferSubDataARB ( GLenum target, GLintptrARB offs,
                              GLsizeiptrARB size, const void * data );

Параметр target имеет тот же смысл, что и ранее. Параметры offs и size задают начало и размер изменяемого блока данных внутри вершинного буфера, а параметр data указывает на область памяти, содержащую новые значения.

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

    void glGetBufferSubDataARB ( GLenum target, GLintptrARB offs,
                                 GLsizeiptrARB size, void * data );

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

Вторым способом доступа к содержимому вершинного буфера является его отображение (mapping) в адресное пространство приложения.

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

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

Для отображения текущего вершинного буфера в системную память служит следующая команда.

    void * glMapBufferARB ( GLenum target, GLenum access );

Параметр target имеет то же смысл, что и ранее. Параметр access определяет какие операции планируется проводить с данными через полученный указатель. Допустимыми значениями являются GL_READ_ONLY_ARB (только чтение), GL_WRITE_ONLY_ARB (только запись) и GL_READ_WRITE_ARB (и чтение и запись). Использование этого параметра позволяет системе оптимизировать способ отображения.

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

Для закрытия отображения используется команда

    GLboolean glUnmapBufferARB ( GLenum target );

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

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

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

Для проверки того, является ли беззнаковое число идентификатором какого-либо вершинного буфера служит команда

    GLboolean glIsBufferARB ( GLuint buffer );

Вершинный буфер может использоваться для задания данных во всех командах OpenGL, где передается указатель на блок памяти с данными (например, glVectorPointer или glDrawElements).

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

    glBindBufferARB ( GL_ARRAY_BUFFER_ARB, bufId );
    glVertexPointer ( 3, GL_FLOAT, 0, NULL );

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

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

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

Обратите внимание, что можно использовать разные вершинные массивы в качестве источников разных данных - например часто изменяемые координаты вершин хранить в одном вершинном массиве, а цвета и текстурные координаты - в другом. Это позволит уменьшить объем передаваемых данных, поскольку будут передаваться только данные из часто изменяемого буфера, а остальные данные могут все время находиться в памяти GPU.

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

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

VBO можно легко "завернуть" в класс на С++, предостатвив простой и удобный способ создания/уничтожения и работы с ними. Ниже приводится заголовочный файл предлагаемой реализации, используенмой в ряде примеров на этом сайте.

#ifndef __VERTEX_BUFFER_CLASS__
#define __VERTEX_BUFFER_CLASS__

#include    "libExt.h"

class   VertexBuffer
{
    GLuint  id;
    GLenum  target;
    bool    ok;
    
public:
    VertexBuffer  ();
    ~VertexBuffer ();

    GLenum  getId () const
    {
        return id;
    }
    
    void    bind   ( GLenum theTarget );
    void    unbind ();
    
    void    setData    ( unsigned size, const void * ptr, GLenum usage );
    void    setSubData ( unsigned offs, unsigned size, const void * ptr );
    void    getSubData ( unsigned offs, unsigned size, void * ptr );
    void  * map        ( GLenum access );
    bool    unmap      ();

    void    clear ()
    {
        glBufferDataARB ( target, 0, NULL, 0 /*usage*/ );
    }
    
    static  bool    isSupported ();
};

#endif

На следующем листинге приводится реализация данного класса.

#include    "VertexBuffer.h"

VertexBuffer :: VertexBuffer ()
{
    glGenBuffersARB ( 1, &id );
    
    ok     = ( glIsBufferARB ( id ) == GL_TRUE );
    target = 0;
}

VertexBuffer :: ~VertexBuffer ()
{
    glDeleteBuffersARB ( 1, &id );
}

void    VertexBuffer :: bind ( GLenum theTarget )
{
    glBindBufferARB ( target = theTarget, id );
}

void    VertexBuffer :: unbind ()
{
    glBindBufferARB ( target, 0 );
}

void    VertexBuffer :: setData ( unsigned size, const void * ptr, GLenum usage )
{
    glBufferDataARB ( target, size, ptr, usage );
}

void    VertexBuffer :: setSubData ( unsigned offs, unsigned size, const void * ptr )
{
    glBufferSubDataARB ( target, offs, size, ptr );
}

void    VertexBuffer :: getSubData ( unsigned offs, unsigned size, void * ptr )
{
    glGetBufferSubDataARB ( target, offs, size, ptr );
}

void  * VertexBuffer :: map ( GLenum access )
{
    return glMapBufferARB ( target, access );
}

bool    VertexBuffer :: unmap ()
{
    return glUnmapBufferARB ( target ) == GL_TRUE;
}

bool    VertexBuffer :: isSupported ()
{
    return isExtensionSupported ( "GL_ARB_vertex_buffer_object" );
}

Дополнительную информацию по данному расширению можно найти на OpenGL Extension Registry Более полную информацию о работе с данным расширениям (как и со многими другими), можно найти в книге "Расширения OpenGL".

Valid HTML 4.01 Transitional

Напиши мне