Главная Статьи Ссылки Скачать Скриншоты Юмор Почитать Tools Проекты Обо мне Гостевая |
Одним из самых "узких" мест в работе с современным 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".