Устав от монотонности ежедневных задач, я решил бросить взгляд на обыденную рутину серьёзных парней, и, возможно, приложить свою руку. На ум сразу пришел профильный тред с местного форума, 10 страниц которого посвящены войне идеологий и публичной демонстрации широты собственных познаний. Но было в нем и кое-что стоящее, а именно — задача по наложению медианного фильтра. Именно она и была выбрана в качестве challenge`а. Почему-то хотелось экзотики, а упоминание «GLSL» окончательно сформировало возможное решение. Фрагментный шейдер обращается к исходному изображению в 2d текстуре, и реализует логику медианного фильтра. Результирующий фрагмент сохраняется как пиксел в буфер кадра. 100%-я обработка каждого пиксела исходного изображения достигается за счет ортографической проекции прямоугольника с исходной текстурой в буфер кадра. Размеры буфера кадра равны размерам исходного изображения. Таким образом все тексели исходного изображения будут обработаны фильтром. Задача и идея решения были просты и понятны. Оставалось лишь столкнуться и превозмочь. Говорить о эффективных вычислениях с помощью GLSL можно только обладая графическим ускорителем. И он есть: GeForce 8400M GS (NV86). Свежий драйвер от nVidia для Vista 32bit порадовал поддержкой OpenGL 3.2 и GLSL 1.50 на моём, не самом новом графическом процессоре. Тут же решил писать для forward compatibility device context и не оглядываться на все deprecated вызовы API, существовавшие до версии 3.0. FreeGLUT и GLEW сэкономили мне несколько часов реального времени, которое должно было быть потрачено на проверку расширений, загрузку entry-points функций API, создание окна и OGL context. Будете проходить мимо их сайтов на SourceForge.net — обязательно сделайте donation ;) GLSL Современные GPU, это уже не какая-то статическая считалка с отлитыми в кремнии алгоритмами Transformation & Lighting, это вполне себе независимые вычислительные устройства со своими АЛУ, инструкциями, кешами и программами. И вот «эти ваши шейдеры» на GLSL, суть — просто куски С-подобного кода, компилируемого драйвером на этапе выполнения в байткод и передаваемые на GPU для выполнения. Важный момент: GLSL допускает использование процедур и понимает циклы (особено в развернутом виде), но отрицает рекурсию (видимо сказывается отсутствие стека). Итак, будем рисовать текстурированный прямоугольник на весь экран. Массивы вершин, текстурных координат и индексов для сборки геометрии создаются в буферах и отправляются в память GPU навсегда. Проецирование прямоугольника производится вершинным шейдером, выполняющим ортографическую проекцию. Текстурные координаты проходят дальше в фрагментный шейдер.
// vertex shader source const GLchar* ortho_vs="#version 140\n" "const mat4 projection = mat4( 2.0, 0.0, 0.0, 0.0," " 0.0, 2.0, 0.0, 0.0," " 0.0, 0.0,-2.0, 0.0," " -1.0,-1.0,-1.0, 1.0);" "in vec3 position;" "in vec2 texture0;" "centroid out vec2 coord0;" "void main()" "{" " coord0 = texture0;" " gl_Position = projection * vec4(position, 1.0);" "}"; |
_Winnie C++ Colorizer |
А вот собственно и сам фрагментный шейдер, реализующий median3×3. Был добыт на просторах сети и немного поправлен:
// fragment shader source const GLchar* median3x3_fs= "#version 140\n" "#define s2(a, b) temp = a; a = min(a, b); b = max(temp, b);\n" "#define mn3(a, b, c) s2(a, b); s2(a, c);\n" "#define mx3(a, b, c) s2(b, c); s2(a, c);\n" "#define mnmx3(a, b, c) mx3(a, b, c); s2(a, b);\n" "#define mnmx4(a, b, c, d) s2(a, b); s2(c, d); s2(a, c); s2(b, d);\n" "#define mnmx5(a, b, c, d, e) s2(a, b); s2(c, d); mn3(a, c, e); mx3(b, d, e);\n" "#define mnmx6(a, b, c, d, e, f) s2(a,d); s2(b,e); s2(c,f); mn3(a,b,c); mx3(d,e,f);\n" "" "uniform sampler2DRect image;" "out vec4 fragment;" "centroid in vec2 coord0;" "" "void main()" "{" " vec4 v[6];" " v[0] = textureOffset(image, coord0, ivec2(-1, -1));" " v[1] = textureOffset(image, coord0, ivec2( 0, -1));" " v[2] = textureOffset(image, coord0, ivec2( 1, -1));" " v[3] = textureOffset(image, coord0, ivec2(-1, 0));" " v[4] = texture(image, coord0);" // centroid " v[5] = textureOffset(image, coord0, ivec2( 1, 0));" " vec4 temp;" " mnmx6(v[0], v[1], v[2], v[3], v[4], v[5]);" " v[5] = textureOffset(image, coord0, ivec2(-1, 1));" " mnmx5(v[1], v[2], v[3], v[4], v[5]);" " v[5] = textureOffset(image, coord0, ivec2(0, 1));" " mnmx4(v[2], v[3], v[4], v[5]);" " v[5] = textureOffset(image, coord0, ivec2(1, 1));" " mnmx3(v[3], v[4], v[5]);" " fragment = v[4];" "}"; |
_Winnie C++ Colorizer |
Пара слов о характерных моментах:
- доступ к исходному изображению через переменную sampler2DRect image (почему не sampler2D? — об этом ниже);
- результурующее значение будет записано в переменную fragment (модификатор доступа out говорит сам за себя);
- in vec2 coord0 содержит интерполированные текстурные координаты для данного фрагмента;
- использование векторных переменных (четыре float в каждом из vec4);
- отсутствие циклов.
Собственно алгоритм прост — поиск 5-й порядковой статистики в множестве из 9 значений. Метод поиска которых подробно описывается например здесь. В нашем случае, это конечное число перестановок макросом s2. Просто, развернуто, молодежно. Data streaming Дело осталось за малым — организавать in/out данных и мерять производительность. Эталоном нагрузки был взят 1 мегапиксел (1MP) 32 битного изображения. Поскольку размеры изображения не обязаны быть равны степени двойки (FullHD video), текстура имеет тип texture_rectangle и обращение к ней происходит через sampler2DRect. Это потенциально позволяет экономить память. Почему-то, сразу же не захотелось рисовать фильтруемое изображение в окно и был организован off-screen rendering в RenderBuffer, равный по размерам исходному изображению. Все готово для первого замера. Итак, BGRA текстура 1024×1024 и медианный фильтр 3×3: ~11.5 мс. Та же текстура, но с модифицированным фрагментным шейдером, оперирующим только red компонентами текселей: ~3.7мс. Неплохо. Но эти цифры не включают главного — передачи данных на GPU и чтения результата в системную память. ОК. Добавляем glTexSubImage2D и glReadPixels в функцию рисования кадра и… огорчаемся. Обновление BGRA 1024×1024 изображения в текстуре, отображение шейдером в RenderBuffer и считывание в системную память — 38мс. Печаль. Конечно, подобные результаты никого не устраивают и для таких ситуаций есть официальный workaround — data streaming для GPU. OpenGL API предоставлет для этих целей разнообразные Pixel Buffer Object’ы — управляемые драйвером области памяти. Можно указать предполагаемый способ использования pbo и драйвер самостоятельно определит тип памяти для размещения данных. Поддерживается резервирование памяти и memory-map-unmap. Вызвав glTexSubImage2D или glReadPixels в контексте PBO, можно указать драйверу на необходимость асинхронной операции ввода/вывода. При этом операции копирования будут отложены до более удобного времени, а вызовы glTexSubImage2D и glReadPixels завершаются немедленно. Создав очереди из PBO для ввода и вывода можно надеяться на довольно честный streaming. Попробовал реализовать изображенную на картинке схему: BGR vs BGRA В начале планировалось использовать не 32 битный (BGRA), а 24 битный (BGR) файл изображения. И шокирующие результаты не заставили себя ждать. Многократные замеры последовательности data streaming в конвеере операций: BGR 1024×1024 -> write-only PBO0 write-onlyPBOn (пишем в первый свободный, текстуру обновляем из первого записанного) -> 2D Rect Texture -> BGR RenderBuffer -> read-onlyPBO0… read-onlyPBOn -> median3×3 BGR 1024×1024 показали ~45мс т. е. хуже чем без использования PBO. Те же синхронные операции копирования + бесполезные переключения буферов. Тушите свет. Странные результаты. Более точечные замеры показали, что порядка 30мс теряется исключительно в glReadPixels. Чтение форумов и эксперименты дали вполне логичный ответ. проблема в переупорядочивании байт для записи в выходной буфер. И с этим драйвер ничего поделать не мог. Действительно, все вычисления на карточке выполняются над четырьмя компонентами. даже если не сохранять один из них — будут подставлены значения по умолчанию. И тут требуется упаковать весь RenderBuffer в массив не с четырьмя, а с тремя компонентами. И без «дырок». Естественно выполняются перемещения и переупорядочивания компонент цвета. Надо сказать что и порядок компонентов цвета (RGBA или BGRA), совершенно безразличный для алгоритма, то же сказывается на скорости ввода/вывода, пусть и не так катастрофически. Результат Осознав и исправив столь важную деталь как, количество байт в формате упаковки данных для считывания, получил те самые преимущества асинхронного ввода/вывода c использованием PBO: ~21мс на один кадр BGRA 1024×1024. Увеличение длинны очередей с двух до, скажем, пяти буферов значительного повышения скорости ввода/вывода не принесло. А вот память под них выделялась. Остановился на трех. Думаю что в моём случае «горлышком» являются заниженные частоты и меньшее количество АЛУ на самом GPU. Потратив немного времени на перестановку команд OpenGL по вводу-отображению-выводу, добился некоторой одновременности их выполнения элементами GPU — стабильные ~17мс на 1024×1024 BGRA кадр. думаю что не так уж и много для мобильного ускорителя. Уверен, что и это не предел, для восьмибитного серого изображения можно выплонить 1 кадр менее чем за 4мс. (c учетом расходов на ввод/вывод). Причем, только переписыванием шейдера и изменением порядка передачи данных. Резюмируя, скажу, что GLSL вполне себе справился с поставленной задачей. Не уверен, что подобные медоты вычислений можно вписать в уже существующие решения и системы, не потеряв буквально все на операциях ввода/вывода. И его применимость в задачах данного класса допустима, но не более. Пробовать использовать API спроектированные для аппаратного ускорения 3d графики в качестве платформы для вычислений, то же, что объяснять людоеду племени «ням-ням» (на его же языке) как прекрасна летняя тундра и как восхитительны переливы северного сияния в середине полярной ночи. Можно, конечно, но получается крайне топорно и не понятно. Проблема в том, что его язык предназначен что бы говорить про другое. Что бы эффективно доносить суть, нужно использовать понятный язык и APIs, не замутненные абстракцией иной предметной области. И такие инструменты, конечно же, существуют. Например, nVidia CUDA. UPDATE: Source code
Релоцировались? Теперь вы можете комментировать без верификации аккаунта.