вторник, 11 декабря 2012 г.

OpenGL 4.3. Рендеринг в текстуру, альтернативы FBO

Не так давно я привел перевод одной из статей по SSBO (shader_storage_buffer_object), сегодня я решил продолжить рассмотрение нововведений, появившихся в OpenGL 4.2-4.3, в частности  - продолжить знакомство с уже знакомым нам SSBO и clear_buffer_object, а так же рассмотреть появившееся в OpenGL 4.2 расширение shader_image_load_store.
Оба этих расширения предоставляют альтернативный вариант рендеринга в текстуру, но возникает закономерный вопрос о производительности этих подходов. В этой статье я разберу каждый из вариантов и сделаю оценку их производительности.

Для написания демки я набросал небольшой фреймвок, в частности вынес туда создание объекта буфера, создание шейдера и прочие мелочи. Так как работа с SSBO ничем не отличается от использования других объектов буфера (в том числе и VBO), то на этом я не буду останавливаться. Создание шейдера для OpenGL 4.3 тоже ничем не отличается от создания шейдера для OpenGL 1.4, потому эту часть я тоже пропущу и сосредоточусь на нововведениях.

В частности первое нововведение это возможность очистки объекта буфера, причем не просто очистки, но и заливки буфера произвольными данными. Эта возможность появилась в OpenGL 4.3 и описана в расширении clear_buffer_object. В частности нас интересует команда:
void ClearBufferData(enum target, enum internalformat,
     enum format, enum type, const void * data);
Ее формат достаточно простой - указываем какой буфер нужно очистить (буфер должен быть предварительно забинден  к соответствующему таргету), указываем внутренний формат данных, формат пикселя и тип данных (все аналогично как и для текстуры), и передаем ссылку на область памяти, содержащую данные для заливки в буфер.
Простой пример, если мы хотим очистить буфер SSBO, предназначенный для хранения цвета пикселя в формате RGBA8, то нам достаточно выполнить следующие действия:
const
  bRGBA: array[0..3] of byte = (255,255,255,255);
...
  glBindBuffer(GL_SHADER_STORAGE_BUFFER, BuffId);
  glClearBufferData(GL_SHADER_STORAGE_BUFFER, GL_RGBA8,
        GL_RGBA, GL_UNSIGNED_BYTE, @bRGBA[0]);
Для того, чтоб оценить производительность этой команды, я воспользовался прецизионным таймером (ARB_timer_query), сделав замер времени очистки вьюпорта (только буфер цвета) через стандартный "glClear" и очистку буфера, аналогичного формата и размера через "glClearBufferData". Результат получился таким:
glClear - 77us
glClearBufferData - 246us
Результат получился не в пользу "новинки", проиграв обычной очистке в 3.2 раза. Тем не менее, в дальнейшем нам придется использовать данный метод, если хотим использовать альтернативные методы рендеринга в текстуру.

Теперь собственно разберем что значит "альтернативные" методы и какие они есть. Углубляться в историю и рассматривать всякие "pBuffer" я не буду, как и команды glGetImage/glReadPixels. Сейчас нас будет интересовать только производительность относительно FBO.
С появлением в OpenGL 4.2 возможности не только читать из текстуры но и писать в нее, посредством расширения shader_image_load_store, мы получили первый альтернативный метод, так как теперь, вместо того чтоб выводить результат в стандартный буфер кадра через gl_FragColor, мы можем перенаправить вывод сразу в текстуру или несколько текстур. Соответствующий шейдер (в демке он идет под номером "Shader4") будет иметь вид:
#version 420

in vec4 gl_FragCoord;
in vec2 TexCoord;

layout(binding = 0, rgba8) writeonly uniform image2D Image;

void main()
{
    imageStore(Image, ivec2(gl_FragCoord.xy), vec4(TexCoord,0.0,1.0));
}

Что мы здесь делаем - ну во-первых мы не делаем запись в буфер кадра, вместо этого мы прикрепили к шейдеру текстуру "Image" и разрешили запись в нее, после чего просто сохраняем в нее текстурные координаты посредством функции "imageStore".

Так как меня интересовала не только производительность записи в буфер/текстуру, но и эффективность чтения из такой текстуры (а так же для контроля корректности записи), то я написал еще один шейдер(Shader5):

#version 420

in vec4 gl_FragCoord;
in vec2 TexCoord;

layout(location = 0) out vec4 FragColor;
layout(binding = 0, rgba8) readonly uniform image2D Image;

void main()
{
    FragColor = imageLoad(Image, ivec2(gl_FragCoord.xy));
}

Здесь я уже в качестве вывода использую стандартный буфер кадра, связанный с переменной "FragColor", в которую пишутся данные из текстуры "Image".

Вторым альтернативным способом рендеринга в текстуру является прямая запись в SSBO. Ниже приведен код соответствующего шейдера для записи в SSBO (Shader2):
#version 430

in vec2 TexCoord;
layout(binding = 2) buffer Col { uint Colors[];};

uniform float Width;

void main() 
{
  uint index = uint(gl_FragCoord.y*Width+gl_FragCoord.x);
  Colors[index] = packUnorm4x8(vec4(TexCoord.xy, 0.0, 1.0));
}

и шейдер на чтение из SSBO с выводом в буфер кадра(Shader3):
#version 430

layout(location = 0) out vec4 FragColor;
layout(binding = 2) buffer Col { uint Colors[];};

uniform float Width;

void main() 
{
  uint index = uint(gl_FragCoord.y*Width+gl_FragCoord.x);
  FragColor = unpackUnorm4x8(Colors[index]);
}

Что здесь происходит, ну во-первых, вместо использования текстуры мы уже используем буфер SSBO "Colors", для его объявления служит ключевое слово "buffer", а объявляется он по тем же правилам что и блок юниформ, с тем отличием, что мы можем использовать при объявлении буфера открытые массивы. Так как массив "Colors" у нас одномерный, то мне пришлось пересчитывать координаты фрагмента в смешение в буфере.
Второе замечание - массив объявлен как "uint", что как раз подходит для хранения формата RGBA8. Чтоб работать с таким форматом, в расширении shading_language_packing, были введены функции "packUnorm4x8" и "unpackUnorm4x8", позволяющие запаковать в uint вектор из 4-х флоатов.

Для рендеринга как в FBO так и в стандартный буфер кадра, используется такой шейдер (Shader1):
#version 330

in vec2 TexCoord;
layout(location = 0) out vec4 FragColor;

void main() 
{
  FragColor = vec4(TexCoord,0.0,1.0);
}

Для оценки так же осуществляется вывод текстурированного скринквада в стандартный буфер кадра (Shader6):

#version 330

in vec2 TexCoord;

layout(location = 0) out vec4 FragColor;

uniform sampler2D Image;

void main()
{
  FragColor = texture(Image, TexCoord.xy);
}

Во всех случаях использовался одинаковый вершинный шейдер скринквада (Shader.vert):
#version 420

layout(location = 0) in vec3 in_Position;
layout(location = 1) in vec2 in_TexCoord;

out vec2 TexCoord;

void main(void)
{ 
  gl_Position = vec4(in_Position, 1.0);
  TexCoord = in_TexCoord;
}

Ну и теперь переходим к тестированию. Как вы уже могли понять из приведенных выше шейдеров, выводится скринквад с градиентом, соответствующим распределению текстурных координат:


Результаты теста можно видеть на этом же скриншоте. Расшифровка результатов:
Первая строка: "Clearing Buffers" - результаты по времени очистки буферов через "glClear" (76.26мкс) и "glClearBufferData"(246.34мкс) соответственно.

Вторая строка:
FragColor = 127.9мкс - время вывода скринквада с прямой записью цвета в "FragColor" (Shader1);
SSBO Fill = 286.98мкс - время записи в буфер SSBO (Shader2);
SSBO Render = 363.58мкс - время вывода скринквада с чтением из SSBO (Shader3);

Третья строка:
ImageStore = 529.89мкс - время записи в текстуру через ImageStore (Shader4);
ImageLoad = 330.98мкс - время вывода скринква с текстурированием через ImageLoad (Shader5);
FBO = 169.50 мкс - время рендеринга в текстуру через FBO (Shader1)$

Четвертая строка:
SimpleTexturing = 236.74мкс - для сравнения был выведен обычный текстурированный скринквад.

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

С рендерингом в текстуру все сложнее. Возьмем за основу время рендеринга в FBO и скринквада с результатом: 170мкс+237мкс = 407мкс, это у нас будет как эталонное значение.

Используя рендеринг в SSBO суммарное время у нас получается 650мкс, что в полтора раза выше чем при обычном рендеринге в FBO.
Воспользовавшись расширением ImageLoadStore нас ждет наибольшее разочарование, так как суммарное время операции составляет целых 860мкс, что в два раза выше чем у FBO и на 30% выше чем у SSBO.  

Однако, нужно помнить что все это время измерено в микросекундах, в переводе на "FPS" вы получите 2500 фпс у FBO, 1500 фпс у SSBO и 1160 фпс у ImageLoadStore.
Отдельно стоит отметить результат "ImageLoad" и "SSBO Render". Не смотря на то, что запись в текстурный буфер два раза медленнее записи в SSBO, по чтению текстурный буфер выигрывает, так как там меньше операций (в SSBO нам пришлось вычислять смещение и распаковывать формат пикселя). Это тонко намекает что тест очень чувствителен к количеству операций и в реальных условиях разница будет не столь существенной.
Возможно чуть позже я добавлю еще одну демку, где существенно увеличу нагрузку как на вершинный так и фрагментный шейдер.

Пока можно с уверенностью сказать что альтернатива FBO есть :)

Демку с исходниками можно взять здесь.
Для запуска демки требуется драйвера с поддержкой OpenGL 4.3, на момент написания этой статьи существует только бета-версия драйверов и только у NVidia, кторые можно взять здесь:
https://developer.nvidia.com/opengl-driver
или ищите 310 версию в бета-драйверах NVidia:
http://www.nvidia.ru/Download/Find.aspx?lang=ru
На момент написания статьи последняя версия была 310.70.

Комментариев нет:

Отправить комментарий