Рендерскрипт, Часть Вторая.

Renderscript — это новая функция, представленная в Honeycomb. Также известно, что Renderscript ранее использовался разработчиками Android (например, на нем были написаны встроенные живые обои в версии 2.1 (Eclair).

Так или иначе, полный доступ к API был доступен только в Honeycomb. В первой вступительной статье из блога разработчиков ( оригинальный | перевод ) обещали, что скоро будет второй, с более подробным описанием архитектуры Renderscript и примером ее использования.

Собственно, под катом есть и то, и другое.

Здесь и далее от имени инженера из команды разработчиков Android Р.

Джейсона Сэмса.

В статье введение в рендерскрипт Я дал краткий обзор этой технологии.

В этом посте я сосредоточусь на «вычислениях».

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



Цели дизайна рендерскрипта

У Renderscript есть три основные цели, давайте рассмотрим их в порядке от наиболее важных к наименее важным: Портативность: Код приложения должен работать на всех устройствах, даже если их оборудование радикально отличается друг от друга.

ARM теперь выпускается в нескольких вариантах — с поддержкой VFP и без нее, с поддержкой NEON и без нее, а также с разными счетчиками регистров.

Помимо ARM, существуют другие архитектуры ЦП, похожие на x86, а также другие графические процессоры и еще больше различных DSP (процессоров цифровых сигналов).

Производительность: Вторая цель — достичь максимально высокого уровня производительности, но с ограничениями переносимости.

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

Удобство использования: Третьей целью было максимально упростить разработку.

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

Эти три цели ограничивают дизайн и приводят к некоторым компромиссам.

Это компромиссы, которые отличают Renderscript от существующих решений, таких как Dalvik или NDK. Их следует рассматривать как разные инструменты, служащие разным целям.



Основные решения на этапе проектирования

Первое решение, которое нам пришлось принять, — это язык разработки.

Когда дело доходит до выбора языка, выбор вариантов практически неограничен.

C и C++ рассматривались как языки программирования шейдеров.

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

Отсутствие указателей и рекурсии считались ограничениями удобства использования.

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

Расширенные возможности C++ сложно запустить на оборудовании, отличном от процессора.

В конце концов, мы решили основывать Renderscript на стандарте C99, поскольку он обеспечивает такую же производительность, как и другие решения, хорошо понятен разработчикам и не создает проблем при запуске на широком спектре различного оборудования.

Второй компромисс — это сам рабочий процесс Renderscript. Мы особенно сосредоточились на том, как скомпилировать исходный код в машинный код. Изучив различные варианты в ходе разработки, мы реализовали два решения.

Старая версия (между Eclair и Gingerbread) полностью компилировала исходный код C в машинный код. Хотя это и предоставило приложениям такие возможности, как генерация кода «на лету», это оказалось проблемой с удобством использования.

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

Также слабые процессоры были лишены возможности статического анализа и некоторых оптимизаций, которые можно было провести.

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

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

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

Это компромисс между производительностью и портативностью.

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

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

Хотя тестирование и настройка под определенный набор устройств — это неплохо, никакой объем работы не позволит настроить приложение под еще не выпущенное и недоступное разработчику оборудование.

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

Учитывая, что нашей целью номер один была мобильность, мы выбрали это решение.

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

Например, одно компьютерное оборудование поддерживает указатели и рекурсию, а другое — нет. Мы могли бы запретить эти вещи и предоставить разработчикам какой-то API с наименьшим общим знаменателем, но вместо этого мы выбрали анализ времени выполнения.

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

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

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

Удобство использования было нашей главной целью при проектировании.

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

Такой код подвержен ошибкам и его сложно писать.

Статический анализ, выполняемый Renderscript на хосте, помогает решить эту проблему.

Каждый пользователь скрипта создает свой «склеивающий» класс Dalvik. Имена связующего класса и его методов доступа извлекаются из сценария.

Это упрощает использование скрипта от Dalvik.

Пример: прикладной уровень

Учитывая все компромиссы, описанные выше, как должен выглядеть простой пример вычислительного приложения? В этом базовом примере мы возьмем обычный объект android.graphics.Bitmap и запустим скрипт, который скопирует его в другой растровый объект и преобразует его в монохромный.

Давайте посмотрим на код приложения, вызывающего сценарий, прежде чем рассматривать сам сценарий; пример из HelloCompute SDK:

  
   

private Bitmap mBitmapIn; private Bitmap mBitmapOut; private RenderScript mRS; private Allocation mInAllocation; private Allocation mOutAllocation; private ScriptC_mono mScript; private void createScript() { mRS = RenderScript.create(this); mInAllocation = Allocation.createFromBitmap(mRS, mBitmapIn, Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT); mOutAllocation = Allocation.createTyped(mRS, mInAllocation.getType()); mScript = new ScriptC_mono(mRS, getResources(), R.raw.mono); mScript.set_gIn(mInAllocation); mScript.set_gOut(mOutAllocation); mScript.set_gScript(mScript); mScript.invoke_filter(); mOutAllocation.copyTo(mBitmapOut);

Этот метод предполагает, что два растровых изображения уже созданы и имеют одинаковый размер и формат. Первое, что нужно приложениям Renderscript, — это объект контекста.

Это центральный объект, который используется для создания других объектов Renderscript и управления ими.

Первая строка кода создает объект mRS, этот объект должен оставаться активным до тех пор, пока приложение намерено использовать его или любые объекты, созданные с его помощью.

Следующие два метода вызывают создание выделений (выделяют) из растровых изображений для расчета.

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

При создании распределителя его потенциальные возможности использования должны быть пронумерованы, чтобы система могла определить правильный тип памяти для намеченных задач.

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

Распределения — это основные единицы памяти, которые использует Renderscript. Второй макет, созданный с помощью метода createTyped(), генерирует макет, идентичный первому.

Более того, определение этой структуры возвращается запросом getType() из первой.

Типы Renderscript определяют структуру макета.

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

Следующая строка загружает скрипт под названием «mono.rs», используя для его получения R.raw.mono. Сам скрипт хранится как необработанный ресурс в пакете приложения (APK).

Обратите внимание на имя созданного «связующего» класса — ScriptC_mono. Следующие строки устанавливают свойства скрипта, используя сгенерированные методы класса «клей».

Теперь мы закончили, хотя метод ignore_filter() на самом деле сделал часть работы за нас.

Суть в том, чтобы вызвать метод filter() самого скрипта «mono.rs».

Если у метода есть параметры, то их следует передать сюда.

Возврат значений не разрешен, поскольку вызовы асинхронны.

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



Пример: сценарий

Вот сам скрипт «mono.rs», который вызывается приведенным выше кодом:

#pragma version(1) #pragma rs java_package_name(com.android.example.hellocompute) rs_allocation gIn; rs_allocation gOut; rs_script gScript; const static float3 gMonoMult = {0.299f, 0.587f, 0.114f}; void root(const uchar4 *v_in, uchar4 *v_out, const void *usrData, uint32_t x, uint32_t y) { float4 f4 = rsUnpackColor8888(*v_in); float3 mono = dot(f4.rgb, gMonoMult); *v_out = rsPackColorTo8888(mono); } void filter() { rsForEach(gScript, gIn, gOut, 0); }

Первая строка скрипта просто сообщает компилятору, для какой версии Renderscript он был написан.

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

Далее идут три глобальные переменные, соответствующие созданным в управляющем коде.

Четвертая глобальная переменная не является отражающей, поскольку она статична.

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

Метод root() является особенным для Renderscript. Концептуально он похож на метод main() в C. Когда скрипт вызывается во время выполнения, эта функция является точкой входа.

В данном случае параметрами являются пиксели из наших мест размещения.

Общий указатель пользователя также предоставляется с адресом внутри самого места размещения, которое обрабатывается этим вызовом.

В этом примере эти параметры игнорируются.

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

Обратите внимание, что точка возвращает обычное число с плавающей запятой, которое можно присвоить типу float3, который просто копирует значение в каждый компонент x, y и z. Наконец, мы снова используем встроенную возможность для упаковки значений в обычный 32-битный пиксель.

Это также пример перегрузки метода, поскольку существуют разные версии rsPackColorTo8888, которые принимают данные в форме RGB(float3) и RGBA(float4).

Метод filter() вызывается из управляющего кода для выполнения преобразования.

Он просто выполняет расчет для каждого элемента макета.

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

Второй и третий параметры — это расположение входных и выходных данных.

Последний параметр предназначен для передачи некоторых пользовательских данных в метод root().

forEach будет работать на нескольких процессорах, если они есть на устройстве.

В будущем forEach сможет предоставлять точки перехода, где управление может передаваться от одного процессора к другому.

В этом примере было бы разумно предположить, что в будущем filter() будет выполняться на CPU, а root() — на GPU или DSP. Я надеюсь, что это дало вам возможность поближе познакомиться с дизайном Renderscript и показать простой пример того, как он работает. От переводчика: Буду рад услышать любые комментарии от разработчиков, которые так или иначе начали работать с этой технологией.

Теги: #Android #Android #RenderScript #вычисления #графика #Разработка для Android

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.