RenderScript — это фреймворк для высокопроизводительного выполнения ресурсоемких вычислительных задач на Android. RenderScript в первую очередь ориентирован на использование с параллельными вычислениями, хотя последовательные рабочие нагрузки также могут получить от него пользу. Среда выполнения RenderScript распараллеливает работу между процессорами, доступными на устройстве, такими как многоядерные ЦП и графические процессоры. Это позволяет сосредоточиться на описании алгоритмов, а не на планировании работы. RenderScript особенно полезен для приложений, занимающихся обработкой изображений, вычислительной фотографией или компьютерным зрением.
Для начала работы с RenderScript вам следует понять две основные концепции:
- Сам язык является производным от C99 и предназначен для написания высокопроизводительного вычислительного кода. В статье «Написание ядра RenderScript» описывается, как использовать его для написания вычислительных ядер.
- API управления используется для управления временем жизни ресурсов RenderScript и контроля выполнения ядра. Он доступен на трех разных языках: Java, C++ в Android NDK и самом языке ядра, производном от C99. В статьях «Использование RenderScript из кода Java» и «RenderScript из одного источника» описаны первый и третий варианты соответственно.
Написание ядра RenderScript
Ядро RenderScript обычно находится в файле .rs в каталоге <project_root>/src/rs ; каждый файл .rs называется скриптом . Каждый скрипт содержит свой собственный набор ядер, функций и переменных. Скрипт может содержать:
- Объявление прагмы (
#pragma version(1)), указывающее версию языка ядра RenderScript, используемого в этом скрипте. В настоящее время допустимым значением является 1. - Объявление прагмы (
#pragma rs java_package_name(com.example.app)), которая объявляет имя пакета Java-классов, отраженных в этом скрипте. Обратите внимание, что ваш файл.rsдолжен быть частью пакета вашего приложения, а не находиться в библиотечном проекте. - Ноль или более вызываемых функций . Вызываемая функция — это однопоточная функция RenderScript, которую можно вызвать из Java-кода с произвольными аргументами. Они часто полезны для первоначальной настройки или последовательных вычислений в рамках более крупного конвейера обработки.
Ноль или более глобальных переменных скрипта . Глобальная переменная скрипта аналогична глобальной переменной в C. Вы можете получить доступ к глобальным переменным скрипта из кода Java, и они часто используются для передачи параметров ядрам RenderScript. Более подробное описание глобальных переменных скрипта можно найти здесь .
Нулевое или более вычислительных ядер . Вычислительное ядро — это функция или набор функций, которые можно поручить среде выполнения RenderScript для параллельного выполнения над набором данных. Существует два типа вычислительных ядер: ядра отображения (также называемые ядрами foreach ) и ядра редукции .
Ядро отображения — это параллельная функция, которая работает с набором
Allocationsодинаковой размерности. По умолчанию она выполняется один раз для каждой координаты в этой размерности. Обычно (но не исключительно) она используется для преобразования набора входныхAllocationsв выходноеAllocationпо одномуElementза раз.Вот пример простого ядра отображения :
uchar4 RS_KERNEL invert(uchar4 in, uint32_t x, uint32_t y) { uchar4 out = in; out.r = 255 - in.r; out.g = 255 - in.g; out.b = 255 - in.b; return out; }
В большинстве аспектов это идентично стандартной функции C. Свойство
RS_KERNEL, применяемое к прототипу функции, указывает, что функция является ядром отображения RenderScript, а не вызываемой функцией. Аргументinавтоматически заполняется на основе входногоAllocation, передаваемого при запуске ядра. Аргументыxиyобсуждаются ниже . Значение, возвращаемое ядром, автоматически записывается в соответствующее место в выходномAllocation. По умолчанию это ядро выполняется по всему своему входномуAllocation, при этом для каждогоElementвAllocationвыполняется одно выполнение функции ядра.Ядро отображения может иметь одно или несколько входных
Allocationsпамяти (Allocation), одно выходноеAllocation) или и то, и другое. Среда выполнения RenderScript проверяет, чтобы все входные и выходные выделения памяти имели одинаковые размеры, а также чтобы типыElementвходных и выходных выделений памяти соответствовали прототипу ядра; если какая-либо из этих проверок не пройдена, RenderScript генерирует исключение.ПРИМЕЧАНИЕ: До Android 6.0 (уровень API 23) ядро отображения не может иметь более одного
Allocationвходных данных.Если вам требуется больше
Allocationsввода или вывода, чем имеется в ядре, эти объекты следует привязать к глобальным переменным скриптаrs_allocationи получить к ним доступ из ядра или вызываемой функции черезrsGetElementAt_ type ()илиrsSetElementAt_ type ().ПРИМЕЧАНИЕ:
RS_KERNEL— это макрос, автоматически определяемый RenderScript для вашего удобства:#define RS_KERNEL __attribute__((kernel))
Ядро редукции — это семейство функций, работающих с набором входных
Allocationsодинаковой размерности. По умолчанию его функция-аккумулятор выполняется один раз для каждой координаты в этой размерности. Обычно (но не исключительно) оно используется для «редукции» набора входныхAllocationsк одному значению.Вот пример простого ядра редукции , которое суммирует
Elementsвходных данных:#pragma rs reduce(addint) accumulator(addintAccum) static void addintAccum(int *accum, int val) { *accum += val; }
Ядро редукции состоит из одной или нескольких пользовательских функций. Для определения ядра используется
#pragma rs reduceкоторая указывает его имя (в этом примере — `addint), а также имена и роли функций, составляющих ядро (в этом примере — функция-accumulator`addintAccum). Все такие функции должны бытьstatic. Ядро редукции всегда требует наличия функции-accumulator; оно также может содержать другие функции, в зависимости от того, что вы хотите, чтобы ядро делало.Функция-аккумулятор ядра редукции должна возвращать
voidи иметь как минимум два аргумента. Первый аргумент (в этом примере —accum) — это указатель на элемент данных аккумулятора , а второй (в этом примереval) автоматически заполняется на основе входногоAllocation, переданного при запуске ядра. Элемент данных аккумулятора создается средой выполнения RenderScript; по умолчанию он инициализируется нулем. По умолчанию это ядро выполняется для всего своего входногоAllocation, при этом для каждогоElementвAllocationвыполняется одно выполнение функции-аккумулятора. По умолчанию конечное значение элемента данных аккумулятора рассматривается как результат редукции и возвращается в Java. Среда выполнения RenderScript проверяет, соответствует ли типElementвходного Allocation прототипу функции-аккумулятора; если он не совпадает, RenderScript генерирует исключение.Ядро редукции имеет одно или несколько входных
Allocations, но не имеет выходныхAllocations.Более подробное описание ядер редукции приведено здесь .
Ядра для редукции поддерживаются в Android 7.0 (уровень API 24) и более поздних версиях.
Функция ядра отображения или функция аккумулятора ядра редукции могут получать доступ к координатам текущего выполнения, используя специальные аргументы
x,yиz, которые должны быть типаintилиuint32_t. Эти аргументы являются необязательными.Функция ядра отображения или функция аккумулятора ядра редукции также может принимать необязательный специальный аргумент
contextтипа rs_kernel_context . Он необходим для семейства API среды выполнения, которые используются для запроса определенных свойств текущего выполнения — например, rsGetDimX . (Аргументcontextдоступен в Android 6.0 (уровень API 23) и более поздних версиях.)- Необязательная функция
init(). Функцияinit()— это особый тип вызываемой функции, которую RenderScript запускает при первом создании экземпляра скрипта. Это позволяет автоматически выполнять некоторые вычисления при создании скрипта. - Ноль или более статических глобальных переменных и функций скрипта . Статическая глобальная переменная скрипта эквивалентна глобальной переменной скрипта, за исключением того, что к ней нельзя получить доступ из кода Java. Статическая функция — это стандартная функция C, которую можно вызвать из любой функции ядра или вызываемой функции в скрипте, но она не доступна через API Java. Если глобальная переменная скрипта или функция не требует доступа из кода Java, настоятельно рекомендуется объявить её
static.
Настройка точности вычислений с плавающей запятой
В скрипте можно задать требуемый уровень точности вычислений с плавающей запятой. Это полезно, если не требуется полный стандарт IEEE 754-2008 (используемый по умолчанию). Следующие директивы pragma позволяют установить другой уровень точности вычислений с плавающей запятой:
-
#pragma rs_fp_full(по умолчанию, если ничего не указано): Для приложений, требующих точности вычислений с плавающей запятой в соответствии со стандартом IEEE 754-2008. -
#pragma rs_fp_relaxed: Для приложений, не требующих строгого соответствия стандарту IEEE 754-2008 и допускающих меньшую точность. Этот режим включает сброс значений в ноль для денормалей и округление в сторону нуля. -
#pragma rs_fp_imprecise: Для приложений, не предъявляющих строгих требований к точности. Этот режим включает все параметры изrs_fp_relaxedа также следующие:- В результате выполнения операций может быть возвращено значение +0,0.
- Действия в сетях INF и NAN не определены.
В большинстве приложений можно использовать rs_fp_relaxed без каких-либо побочных эффектов. Это может быть очень полезно на некоторых архитектурах благодаря дополнительным оптимизациям, доступным только при использовании ослабленной точности (например, в инструкциях SIMD ЦП).
Доступ к API RenderScript из Java
При разработке Android-приложения, использующего RenderScript, доступ к его API из Java можно получить двумя способами:
-
android.renderscript— API-интерфейсы из этого пакета классов доступны на устройствах под управлением Android 3.0 (уровень API 11) и выше. -
android.support.v8.renderscript— API-интерфейсы из этого пакета доступны через библиотеку поддержки , что позволяет использовать их на устройствах под управлением Android 2.3 (уровень API 9) и выше.
Вот компромиссы, на которые приходится идти:
- Если вы используете API библиотеки поддержки, часть вашего приложения, использующая RenderScript, будет совместима с устройствами под управлением Android 2.3 (уровень API 9) и выше, независимо от того, какие функции RenderScript вы используете. Это позволяет вашему приложению работать на большем количестве устройств, чем при использовании нативных API (
android.renderscript). - Некоторые функции RenderScript недоступны через API библиотеки поддержки.
- Если вы используете API библиотеки поддержки, вы получите (возможно, значительно) большие APK-файлы, чем если бы вы использовали собственные API (
android.renderscript).
Использование API библиотеки поддержки RenderScript
Для использования API RenderScript из библиотеки поддержки необходимо настроить среду разработки таким образом, чтобы она имела к ним доступ. Для использования этих API требуются следующие инструменты Android SDK:
- Android SDK Tools версии 22.2 или выше.
- Android SDK Build-tools версии 18.1.0 или выше
Обратите внимание, что начиная с Android SDK Build-tools 24.0.0, поддержка Android 2.2 (уровень API 8) прекращена.
Проверить и обновить установленные версии этих инструментов можно в Android SDK Manager .
Для использования API RenderScript из библиотеки поддержки:
- Убедитесь, что у вас установлена необходимая версия Android SDK.
- Обновите настройки процесса сборки Android, чтобы включить параметры RenderScript:
- Откройте файл
build.gradleв папке app вашего модуля приложения. - Добавьте в файл следующие параметры RenderScript:
Классный
android { compileSdkVersion 36 defaultConfig { minSdkVersion 9 targetSdkVersion 19 renderscriptTargetApi 18 renderscriptSupportModeEnabled true } }
Котлин
android { compileSdkVersion(36) defaultConfig { minSdkVersion(9) targetSdkVersion(19) renderscriptTargetApi = 18 renderscriptSupportModeEnabled = true } }
Указанные выше параметры управляют определенным поведением в процессе сборки Android:
-
renderscriptTargetApi— указывает версию байт-кода, которая будет сгенерирована. Рекомендуется установить это значение на самый низкий уровень API, обеспечивающий всю используемую вами функциональность, и установитьrenderscriptSupportModeEnabledвtrue. Допустимые значения для этого параметра — любое целое число от 11 до самого последнего выпущенного уровня API. Если минимальная версия SDK, указанная в манифесте вашего приложения, установлена на другое значение, это значение игнорируется, и для установки минимальной версии SDK используется целевое значение в файле сборки. -
renderscriptSupportModeEnabled— указывает, что сгенерированный байт-код должен использовать совместимую версию, если устройство, на котором он выполняется, не поддерживает целевую версию.
-
- Откройте файл
- В классах вашего приложения, использующих RenderScript, добавьте импорт классов из библиотеки поддержки:
Котлин
import android.support.v8.renderscript.*
Java
import android.support.v8.renderscript.*;
Использование RenderScript из кода Java или Kotlin
Использование RenderScript из кода Java или Kotlin основано на классах API, расположенных в пакете android.renderscript или android.support.v8.renderscript . Большинство приложений используют один и тот же базовый шаблон:
- Инициализируйте контекст RenderScript. Контекст
RenderScript, созданный с помощьюcreate(Context), гарантирует возможность использования RenderScript и предоставляет объект для управления временем жизни всех последующих объектов RenderScript. Создание контекста следует рассматривать как потенциально длительную операцию, поскольку она может создавать ресурсы на разных аппаратных компонентах; по возможности, она не должна находиться в критическом пути приложения. Как правило, приложение будет иметь только один контекст RenderScript одновременно. - Создайте как минимум один
Allocationдля передачи в скрипт.Allocation— это объект RenderScript, предоставляющий хранилище для фиксированного объема данных. Ядра в скриптах принимают объектыAllocationв качестве входных и выходных данных, а доступ к объектамAllocationв ядрах можно получить с помощьюrsGetElementAt_ type ()иrsSetElementAt_ type ()если они связаны как глобальные переменные скрипта. ОбъектыAllocationпозволяют передавать массивы из кода Java в код RenderScript и наоборот. ОбъектыAllocationобычно создаются с помощьюcreateTyped()илиcreateFromBitmap(). - Создавайте все необходимые скрипты. При использовании RenderScript вам доступны два типа скриптов:
- ScriptC : Это определяемые пользователем скрипты, как описано в разделе «Написание ядра RenderScript» выше. Каждый скрипт имеет Java-класс, который компилятор RenderScript обрабатывает для упрощения доступа к скрипту из Java-кода; этот класс имеет имя
ScriptC_ filename. Например, если ядро сопоставления, описанное выше, находится вinvert.rs, а контекст RenderScript уже находится вmRenderScript, то код Java или Kotlin для создания экземпляра скрипта будет выглядеть следующим образом:Котлин
val invert = ScriptC_invert(renderScript)
Java
ScriptC_invert invert = new ScriptC_invert(renderScript);
- ScriptIntrinsic : Это встроенные ядра RenderScript для распространенных операций, таких как размытие по Гауссу, свертка и смешивание изображений. Для получения дополнительной информации см. подклассы
ScriptIntrinsic.
- ScriptC : Это определяемые пользователем скрипты, как описано в разделе «Написание ядра RenderScript» выше. Каждый скрипт имеет Java-класс, который компилятор RenderScript обрабатывает для упрощения доступа к скрипту из Java-кода; этот класс имеет имя
- Заполнение выделенных областей данными. За исключением выделенных областей, созданных с помощью
createFromBitmap(), при первом создании выделенная область заполняется пустыми данными. Для заполнения выделенной области используйте один из методов копирования вAllocation. Методы копирования являются синхронными . - Установите все необходимые глобальные переменные скрипта . Вы можете устанавливать глобальные переменные, используя методы в том же классе
ScriptC_ filenameкоторые называютсяset_ globalname. Например, чтобы установитьintпеременную с именемthreshold, используйте метод Javaset_threshold(int); а чтобы установить переменнуюrs_allocationс именемlookup, используйте метод Javaset_lookup(Allocation). Методыsetявляются асинхронными . - Запустите соответствующие ядра и вызываемые функции.
Методы для запуска заданного ядра отражены в том же классе
ScriptC_ filenameс методами, названнымиforEach_ mappingKernelName ()илиreduce_ reductionKernelName (). Эти запуски являются асинхронными . В зависимости от аргументов ядра, метод принимает одну или несколько областей выделения памяти (Allocation), все из которых должны иметь одинаковые размеры. По умолчанию ядро выполняется для каждой координаты в этих измерениях; чтобы выполнить ядро для подмножества этих координат, передайте соответствующийScript.LaunchOptionsв качестве последнего аргумента методуforEachилиreduce.Запускайте вызываемые функции, используя методы
invoke_ functionName, указанные в том же классеScriptC_ filename. Эти запуски являются асинхронными . - Получение данных из объектов
Allocationи объектов javaFutureType . Для доступа к данным из объектаAllocationв коде Java необходимо скопировать эти данные обратно в Java, используя один из методов `copy` вAllocation. Для получения результата ядра редукции необходимо использовать метод `javaFutureType .get(). Методы `copy` иget()являются синхронными . - Удалите контекст RenderScript. Вы можете уничтожить контекст RenderScript с помощью
destroy()или разрешив сборщику мусора удалить объект контекста RenderScript. Это приведет к тому, что любое дальнейшее использование любого объекта, принадлежащего этому контексту, вызовет исключение.
Асинхронная модель выполнения
Методы forEach , invoke , reduce и set являются асинхронными — каждый из них может вернуться в Java до завершения запрошенного действия. Однако отдельные действия сериализуются в порядке их запуска.
Класс Allocation предоставляет методы «копирования» для копирования данных в объекты Allocation и из них. Метод «копирования» является синхронным и сериализуется относительно любых асинхронных действий, описанных выше, которые затрагивают тот же объект Allocation.
Классы javaFutureType, использующие отражение, предоставляют метод get() для получения результата редукции. get() является синхронным и сериализуется относительно самой редукции (которая является асинхронной).
Рендерскрипт с одним исходным кодом
В Android 7.0 (уровень API 24) представлена новая функция программирования под названием Single-Source RenderScript , в которой ядра запускаются из скрипта, где они определены, а не из Java. В настоящее время этот подход ограничен отображением ядер, которые в этом разделе для краткости просто называются «ядрами». Эта новая функция также поддерживает создание выделений памяти типа rs_allocation внутри скрипта. Теперь можно реализовать весь алгоритм исключительно в скрипте, даже если требуется несколько запусков ядер. Преимущество двоякое: более читаемый код, поскольку реализация алгоритма остается на одном языке; и потенциально более быстрый код, благодаря меньшему количеству переходов между Java и RenderScript при нескольких запусках ядер.
В RenderScript с одним исходным кодом ядра пишутся, как описано в разделе «Написание ядра RenderScript» . Затем пишется вызываемая функция, которая вызывает rsForEach() для их запуска. Этот API принимает функцию ядра в качестве первого параметра, за которой следуют выделения памяти для ввода и вывода. Аналогичный API rsForEachWithOptions() принимает дополнительный аргумент типа rs_script_call_t , который указывает подмножество элементов из выделенной памяти для ввода и вывода, которые должна обработать функция ядра.
Для запуска вычислений RenderScript необходимо вызвать вызываемую функцию из Java. Следуйте инструкциям в разделе «Использование RenderScript из Java-кода» . На шаге «Запуск соответствующих ядер» вызовите вызываемую функцию с помощью invoke_ function_name () , что запустит все вычисления, включая запуск ядер.
Часто требуется выделение памяти для сохранения и передачи промежуточных результатов от одного запуска ядра к другому. Создать его можно с помощью функции rsCreateAllocation() . Простой в использовании вариант этого API — rsCreateAllocation_<T><W>(…) , где T — тип данных элемента, а W — ширина вектора для элемента. API принимает в качестве аргументов размеры в измерениях X, Y и Z. Для одномерных или двумерных выделений размер в измерении Y или Z можно опустить. Например, rsCreateAllocation_uchar4(16384) создает одномерное выделение памяти на 16384 элемента, каждый из которых имеет тип uchar4 .
Выделение ресурсов осуществляется системой автоматически. Вам не нужно явно освобождать или освобождать их. Однако вы можете вызвать rsClearObject(rs_allocation* alloc) чтобы указать, что вам больше не нужен дескриптор alloc для базового выделения ресурсов, чтобы система могла освободить ресурсы как можно раньше.
В разделе «Написание ядра RenderScript» приведен пример ядра, которое инвертирует изображение. Приведенный ниже пример расширяет его возможности, позволяя применять к изображению более одного эффекта с использованием Single-Source RenderScript. Он включает в себя еще одно ядро, greyscale , которое преобразует цветное изображение в черно-белое. Затем вызываемая функция process() последовательно применяет эти два ядра к входному изображению и создает выходное изображение. Параметры выделения памяти как для входного, так и для выходного изображения передаются в качестве аргументов типа rs_allocation .
// File: singlesource.rs #pragma version(1) #pragma rs java_package_name(com.android.rssample) static const float4 weight = {0.299f, 0.587f, 0.114f, 0.0f}; uchar4 RS_KERNEL invert(uchar4 in, uint32_t x, uint32_t y) { uchar4 out = in; out.r = 255 - in.r; out.g = 255 - in.g; out.b = 255 - in.b; return out; } uchar4 RS_KERNEL greyscale(uchar4 in) { const float4 inF = rsUnpackColor8888(in); const float4 outF = (float4){ dot(inF, weight) }; return rsPackColorTo8888(outF); } void process(rs_allocation inputImage, rs_allocation outputImage) { const uint32_t imageWidth = rsAllocationGetDimX(inputImage); const uint32_t imageHeight = rsAllocationGetDimY(inputImage); rs_allocation tmp = rsCreateAllocation_uchar4(imageWidth, imageHeight); rsForEach(invert, inputImage, tmp); rsForEach(greyscale, tmp, outputImage); }
Вызвать функцию process() из Java или Kotlin можно следующим образом:
Котлин
val RS: RenderScript = RenderScript.create(context) val script = ScriptC_singlesource(RS) val inputAllocation: Allocation = Allocation.createFromBitmapResource( RS, resources, R.drawable.image ) val outputAllocation: Allocation = Allocation.createTyped( RS, inputAllocation.type, Allocation.USAGE_SCRIPT or Allocation.USAGE_IO_OUTPUT ) script.invoke_process(inputAllocation, outputAllocation)
Java
// File SingleSource.java RenderScript RS = RenderScript.create(context); ScriptC_singlesource script = new ScriptC_singlesource(RS); Allocation inputAllocation = Allocation.createFromBitmapResource( RS, getResources(), R.drawable.image); Allocation outputAllocation = Allocation.createTyped( RS, inputAllocation.getType(), Allocation.USAGE_SCRIPT | Allocation.USAGE_IO_OUTPUT); script.invoke_process(inputAllocation, outputAllocation);
Этот пример демонстрирует, как алгоритм, включающий два запуска ядра, может быть полностью реализован на языке RenderScript. Без Single-Source RenderScript вам пришлось бы запускать оба ядра из кода Java, разделяя запуск ядер от их определений и затрудняя понимание всего алгоритма. Код Single-Source RenderScript не только проще для чтения, но и исключает переходы между Java и скриптом при каждом запуске ядра. Некоторые итеративные алгоритмы могут запускать ядра сотни раз, что делает накладные расходы на такие переходы значительными.
Глобальные переменные скрипта
Глобальная переменная скрипта — это обычная static глобальная переменная в файле скрипта ( .rs ). Для глобальной переменной скрипта с именем var, определенной в файле filename .rs , будет существовать метод get_ var отраженный в классе ScriptC_ filename . Если глобальная переменная не является const , будет также существовать метод set_ var .
Данная глобальная переменная скрипта имеет два отдельных значения — значение Java и значение скрипта . Эти значения ведут себя следующим образом:
- Если переменная `var` имеет статический инициализатор в скрипте, он задаёт начальное значение переменной `var` как в Java, так и в скрипте. В противном случае это начальное значение равно нулю.
- Доступ к переменной внутри скрипта позволяет читать и записывать её значение.
- Метод
get_ varсчитывает значение из Java-кода. - Метод
set_ var(если он существует) немедленно записывает значение Java, а значение скрипта — асинхронно .
ПРИМЕЧАНИЕ: Это означает, что, за исключением статических инициализаторов в скрипте, значения, записанные в глобальную переменную изнутри скрипта, не видны Java.
Углубленное изучение ядер редукции
Сокращение — это процесс объединения набора данных в одно значение. Это полезный примитив в параллельном программировании, находящийся в таких областях применения, как:
- вычисление суммы или произведения по всем данным
- Вычисление логических операций (
and,or,xor) над всеми данными. - нахождение минимального или максимального значения в данных
- поиск определенного значения или координат определенного значения в данных.
В Android 7.0 (уровень API 24) и более поздних версиях RenderScript поддерживает ядра редукции , позволяющие создавать эффективные алгоритмы редукции, написанные пользователем. Вы можете запускать ядра редукции для входных данных с 1, 2 или 3 измерениями.
Приведенный выше пример демонстрирует простое ядро редукции addint . Вот более сложное ядро редукции findMinAndMax , которое находит местоположения минимального и максимального значений типа long в одномерном Allocation :
#define LONG_MAX (long)((1UL << 63) - 1) #define LONG_MIN (long)(1UL << 63) #pragma rs reduce(findMinAndMax) \ initializer(fMMInit) accumulator(fMMAccumulator) \ combiner(fMMCombiner) outconverter(fMMOutConverter) // Either a value and the location where it was found, or INITVAL. typedef struct { long val; int idx; // -1 indicates INITVAL } IndexedVal; typedef struct { IndexedVal min, max; } MinAndMax; // In discussion below, this initial value { { LONG_MAX, -1 }, { LONG_MIN, -1 } } // is called INITVAL. static void fMMInit(MinAndMax *accum) { accum->min.val = LONG_MAX; accum->min.idx = -1; accum->max.val = LONG_MIN; accum->max.idx = -1; } //---------------------------------------------------------------------- // In describing the behavior of the accumulator and combiner functions, // it is helpful to describe hypothetical functions // IndexedVal min(IndexedVal a, IndexedVal b) // IndexedVal max(IndexedVal a, IndexedVal b) // MinAndMax minmax(MinAndMax a, MinAndMax b) // MinAndMax minmax(MinAndMax accum, IndexedVal val) // // The effect of // IndexedVal min(IndexedVal a, IndexedVal b) // is to return the IndexedVal from among the two arguments // whose val is lesser, except that when an IndexedVal // has a negative index, that IndexedVal is never less than // any other IndexedVal; therefore, if exactly one of the // two arguments has a negative index, the min is the other // argument. Like ordinary arithmetic min and max, this function // is commutative and associative; that is, // // min(A, B) == min(B, A) // commutative // min(A, min(B, C)) == min((A, B), C) // associative // // The effect of // IndexedVal max(IndexedVal a, IndexedVal b) // is analogous (greater . . . never greater than). // // Then there is // // MinAndMax minmax(MinAndMax a, MinAndMax b) { // return MinAndMax(min(a.min, b.min), max(a.max, b.max)); // } // // Like ordinary arithmetic min and max, the above function // is commutative and associative; that is: // // minmax(A, B) == minmax(B, A) // commutative // minmax(A, minmax(B, C)) == minmax((A, B), C) // associative // // Finally define // // MinAndMax minmax(MinAndMax accum, IndexedVal val) { // return minmax(accum, MinAndMax(val, val)); // } //---------------------------------------------------------------------- // This function can be explained as doing: // *accum = minmax(*accum, IndexedVal(in, x)) // // This function simply computes minimum and maximum values as if // INITVAL.min were greater than any other minimum value and // INITVAL.max were less than any other maximum value. Note that if // *accum is INITVAL, then this function sets // *accum = IndexedVal(in, x) // // After this function is called, both accum->min.idx and accum->max.idx // will have nonnegative values: // - x is always nonnegative, so if this function ever sets one of the // idx fields, it will set it to a nonnegative value // - if one of the idx fields is negative, then the corresponding // val field must be LONG_MAX or LONG_MIN, so the function will always // set both the val and idx fields static void fMMAccumulator(MinAndMax *accum, long in, int x) { IndexedVal me; me.val = in; me.idx = x; if (me.val <= accum->min.val) accum->min = me; if (me.val >= accum->max.val) accum->max = me; } // This function can be explained as doing: // *accum = minmax(*accum, *val) // // This function simply computes minimum and maximum values as if // INITVAL.min were greater than any other minimum value and // INITVAL.max were less than any other maximum value. Note that if // one of the two accumulator data items is INITVAL, then this // function sets *accum to the other one. static void fMMCombiner(MinAndMax *accum, const MinAndMax *val) { if ((accum->min.idx < 0) || (val->min.val < accum->min.val)) accum->min = val->min; if ((accum->max.idx < 0) || (val->max.val > accum->max.val)) accum->max = val->max; } static void fMMOutConverter(int2 *result, const MinAndMax *val) { result->x = val->min.idx; result->y = val->max.idx; }
Примечание: Дополнительные примеры ядер редукции можно найти здесь .
Для запуска ядра редукции среда выполнения RenderScript создает одну или несколько переменных, называемых элементами данных-аккумуляторами, для хранения состояния процесса редукции. Среда выполнения RenderScript выбирает количество элементов данных-аккумуляторов таким образом, чтобы максимизировать производительность. Тип элементов данных-аккумуляторов ( accumType ) определяется функцией аккумулятора ядра — первым аргументом этой функции является указатель на элемент данных-аккумулятор. По умолчанию каждый элемент данных-аккумулятор инициализируется нулем (как если бы он был инициализирован с помощью memset ); однако вы можете написать функцию инициализации , чтобы сделать что-то другое.
Пример: В ядре addint элементы данных аккумулятора (типа int ) используются для суммирования входных значений. Функция инициализации отсутствует, поэтому каждый элемент данных аккумулятора инициализируется нулем.
Пример: В ядре findMinAndMax элементы данных аккумулятора (типа MinAndMax ) используются для отслеживания найденных на данный момент минимального и максимального значений. Существует функция инициализации, которая устанавливает их значениями LONG_MAX и LONG_MIN соответственно; а также устанавливает координаты этих значений в -1, указывая на то, что значения фактически отсутствуют в (пустой) части обработанных входных данных.
RenderScript вызывает вашу функцию-аккумулятор один раз для каждой координаты во входных данных. Как правило, ваша функция должна каким-либо образом обновлять элемент данных аккумулятора в соответствии с входными данными.
Пример: В ядре addint функция аккумулятора добавляет значение входного элемента к элементу данных аккумулятора.
Пример: В ядре функции findMinAndMax функция аккумулятора проверяет, меньше ли значение входного элемента минимального значения, записанного в элементе данных аккумулятора, и/или больше ли оно максимального значения, записанного в элементе данных аккумулятора, и соответствующим образом обновляет элемент данных аккумулятора.
После того, как функция аккумулятора будет вызвана один раз для каждой координаты во входных данных, RenderScript должен объединить элементы данных аккумулятора в один элемент данных аккумулятора. Для этого можно написать функцию-комбинатор . Если функция аккумулятора имеет один вход и не имеет специальных аргументов , то вам не нужно писать функцию-комбинатор; RenderScript будет использовать функцию аккумулятора для объединения элементов данных аккумулятора. (Вы все равно можете написать функцию-комбинатор, если такое поведение по умолчанию вас не устраивает.)
Пример: В ядре addint отсутствует функция объединения, поэтому будет использоваться функция аккумулятора. Это правильное поведение, поскольку если мы разделим набор значений на две части и сложим значения в этих двух частях по отдельности, то суммирование этих двух сумм будет равносильно суммированию всего набора значений.
Пример: В ядре функции findMinAndMax функция combiner проверяет, меньше ли минимальное значение, записанное в элементе данных аккумулятора "источника" *val , минимального значения, записанного в элементе данных аккумулятора "назначения" *accum , и соответствующим образом обновляет *accum . Аналогичная работа выполняется и для максимального значения. Это обновляет *accum до состояния, которое он имел бы, если бы все входные значения были накоплены в *accum а не часть в *accum , а часть в *val .
После объединения всех элементов данных аккумулятора RenderScript определяет результат редукции, который необходимо вернуть в Java. Для этого можно написать функцию-конвертер . Однако, если вы хотите, чтобы итоговое значение объединенных элементов данных аккумулятора было результатом редукции, писать функцию-конвертер не обязательно.
Пример: В ядре addint отсутствует функция outconverter. Итоговое значение объединенных элементов данных — это сумма всех элементов входных данных, то есть значение, которое мы хотим вернуть.
Пример: В ядре функции findMinAndMax функция outconverter инициализирует значение результата int2 для хранения адресов минимального и максимального значений, полученных в результате объединения всех элементов данных аккумулятора.
Написание ядра редукции
#pragma rs reduce определяет ядро редукции, указывая его имя, а также имена и роли функций, составляющих ядро. Все такие функции должны быть static . Ядро редукции всегда требует наличия функции- accumulator ; вы можете опустить некоторые или все остальные функции в зависимости от того, что вы хотите, чтобы ядро делало.
#pragma rs reduce(kernelName) \ initializer(initializerName) \ accumulator(accumulatorName) \ combiner(combinerName) \ outconverter(outconverterName)
Значение элементов в директиве #pragma следующее:
-
reduce( kernelName )(обязательно): Указывает, что определяется ядро редукции. Отраженный метод Javareduce_ kernelNameзапустит ядро. initializer( initializerName )(необязательно): Указывает имя функции инициализации для этого ядра редукции. При запуске ядра RenderScript вызывает эту функцию один раз для каждого элемента данных аккумулятора . Функция должна быть определена следующим образом:static void initializerName(accumType *accum) { … }
accum— это указатель на элемент данных аккумулятора, который необходимо инициализировать этой функцией.Если вы не укажете функцию инициализации, RenderScript инициализирует каждый элемент данных аккумулятора нулем (как если бы это было сделано с помощью
memset), ведя себя так, как если бы существовала функция инициализации, которая выглядит следующим образом:static void initializerName(accumType *accum) { memset(accum, 0, sizeof(*accum)); }
accumulator( accumulatorName )(обязательно): Указывает имя функции аккумулятора для этого ядра редукции. При запуске ядра RenderScript вызывает эту функцию один раз для каждой координаты во входных данных, чтобы обновить элемент данных аккумулятора в соответствии с входными данными. Функция должна быть определена следующим образом:static void accumulatorName(accumType *accum, in1Type in1, …, inNType inN [, specialArguments]) { … }
accum— это указатель на элемент данных аккумулятора, который эта функция должна изменить.in1—in N— это один или несколько аргументов, которые автоматически заполняются на основе входных данных, переданных при запуске ядра, по одному аргументу на каждый входной параметр. Функция аккумулятора может дополнительно принимать любой из специальных аргументов .Примером ядра с несколькими входными данными является
dotProduct.-
combiner( combinerName )(необязательно): Указывает имя функции объединения для этого ядра редукции. После того, как RenderScript вызовет функцию аккумулятора один раз для каждой координаты во входных данных, он вызовет эту функцию столько раз, сколько необходимо для объединения всех элементов данных аккумулятора в один элемент данных аккумулятора. Функция должна быть определена следующим образом:
static void combinerName(accumType *accum, const accumType *other) { … }
accum— это указатель на целевой элемент данных аккумулятора, который эта функция должна изменить.other— это указатель на исходный элемент данных аккумулятора, который эта функция должна объединить в*accum.ПРИМЕЧАНИЕ: Возможно, что
*accum,*otherили оба параметра были инициализированы, но никогда не передавались в функцию аккумулятора; то есть один или оба параметра никогда не обновлялись в соответствии с входными данными. Например, в ядре findMinAndMax функция объединенияfMMCombinerявно проверяетidx < 0поскольку это указывает на такой элемент данных аккумулятора, значение которого равно INITVAL .Если вы не укажете функцию-комбинатор, RenderScript будет использовать вместо неё функцию-аккумулятор, ведя себя так, как если бы существовала функция-комбинатор, которая выглядит следующим образом:
static void combinerName(accumType *accum, const accumType *other) { accumulatorName(accum, *other); }
Функция объединения является обязательной, если ядро имеет более одного входа, если тип входных данных не совпадает с типом данных аккумулятора или если функция аккумулятора принимает один или несколько специальных аргументов .
outconverter( outconverterName )(необязательно): Указывает имя функции outconverter для этого ядра редукции. После того, как RenderScript объединит все элементы данных аккумулятора, он вызывает эту функцию, чтобы определить результат редукции, который необходимо вернуть в Java. Функция должна быть определена следующим образом:static void outconverterName(resultType *result, const accumType *accum) { … }
result— это указатель на элемент данных результата (выделенный, но не инициализированный средой выполнения RenderScript), который эта функция должна инициализировать результатом редукции. resultType — это тип этого элемента данных, который не обязательно должен совпадать с accumType .accum— это указатель на окончательный элемент данных аккумулятора, вычисленный функцией combiner .Если вы не укажете функцию преобразования выходных данных, RenderScript скопирует итоговый элемент данных аккумулятора в результирующий элемент данных, ведя себя так, как если бы функция преобразования выходных данных существовала и выглядела следующим образом:
static void outconverterName(accumType *result, const accumType *accum) { *result = *accum; }
Если вам нужен результат другого типа, отличного от типа данных аккумулятора, то функция outconverter является обязательной.
Обратите внимание, что ядро имеет входные типы, тип элемента данных аккумулятора и тип результата, и ни один из них не обязательно должен совпадать. Например, в ядре findMinAndMax входной тип long , тип элемента данных аккумулятора MinAndMax и тип результата int2 — все разные.
Что нельзя предполагать?
You must not rely on the number of accumulator data items created by RenderScript for a given kernel launch. There is no guarantee that two launches of the same kernel with the same input(s) will create the same number of accumulator data items.
You must not rely on the order in which RenderScript calls the initializer, accumulator, and combiner functions; it may even call some of them in parallel. There is no guarantee that two launches of the same kernel with the same input will follow the same order. The only guarantee is that only the initializer function will ever see an uninitialized accumulator data item. For example:
- There is no guarantee that all accumulator data items will be initialized before the accumulator function is called, although it will only be called on an initialized accumulator data item.
- There is no guarantee on the order in which input Elements are passed to the accumulator function.
- There is no guarantee that the accumulator function has been called for all input Elements before the combiner function is called.
One consequence of this is that the findMinAndMax kernel is not deterministic: If the input contains more than one occurrence of the same minimum or maximum value, you have no way of knowing which occurrence the kernel will find.
What must you guarantee?
Because the RenderScript system can choose to execute a kernel in many different ways , you must follow certain rules to ensure that your kernel behaves the way you want. If you do not follow these rules, you may get incorrect results, nondeterministic behavior, or runtime errors.
The rules below often say that two accumulator data items must have " the same value" . What does this mean? That depends on what you want the kernel to do. For a mathematical reduction such as addint , it usually makes sense for "the same" to mean mathematical equality. For a "pick any" search such as findMinAndMax ("find the location of minimum and maximum input values") where there might be more than one occurrence of identical input values, all locations of a given input value must be considered "the same". You could write a similar kernel to "find the location of leftmost minimum and maximum input values" where (say) a minimum value at location 100 is preferred over an identical minimum value at location 200; for this kernel, "the same" would mean identical location , not merely identical value , and the accumulator and combiner functions would have to be different than those for findMinAndMax .
The initializer function must create an identity value . That is, ifI and A are accumulator data items initialized by the initializer function, and I has never been passed to the accumulator function (but A may have been), then-
combinerName (& A , & I )must leaveAthe same -
combinerName (& I , & A )must leaveIthe same asA
Example: In the addint kernel, an accumulator data item is initialized to zero. The combiner function for this kernel performs addition; zero is the identity value for addition.
Example: In the findMinAndMax kernel, an accumulator data item is initialized to INITVAL .
-
fMMCombiner(& A , & I )leavesAthe same, becauseIisINITVAL. -
fMMCombiner(& I , & A )setsItoA, becauseIisINITVAL.
Therefore, INITVAL is indeed an identity value.
The combiner function must be commutative . That is, if A and B are accumulator data items initialized by the initializer function, and that may have been passed to the accumulator function zero or more times, then combinerName (& A , & B ) must set A to the same value that combinerName (& B , & A ) sets B .
Example: In the addint kernel, the combiner function adds the two accumulator data item values; addition is commutative.
Example: In the findMinAndMax kernel, fMMCombiner(& A , & B ) is the same as A = minmax( A , B ) , and minmax is commutative, so fMMCombiner is also.
The combiner function must be associative . That is, if A , B , and C are accumulator data items initialized by the initializer function, and that may have been passed to the accumulator function zero or more times, then the following two code sequences must set A to the same value :
combinerName(&A, &B); combinerName(&A, &C);
combinerName(&B, &C); combinerName(&A, &B);
Example: In the addint kernel, the combiner function adds the two accumulator data item values:
A = A + B A = A + C // Same as // A = (A + B) + C
B = B + C A = A + B // Same as // A = A + (B + C) // B = B + C
Addition is associative, and so the combiner function is also.
Example: In the findMinAndMax kernel,
fMMCombiner(&A, &B)
A = minmax(A, B)
A = minmax(A, B) A = minmax(A, C) // Same as // A = minmax(minmax(A, B), C)
B = minmax(B, C) A = minmax(A, B) // Same as // A = minmax(A, minmax(B, C)) // B = minmax(B, C)
minmax is associative, and so fMMCombiner is also.
The accumulator function and combiner function together must obey the basic folding rule . That is, if A and B are accumulator data items, A has been initialized by the initializer function and may have been passed to the accumulator function zero or more times, B has not been initialized, and args is the list of input arguments and special arguments for a particular call to the accumulator function, then the following two code sequences must set A to the same value :
accumulatorName(&A, args); // statement 1
initializerName(&B); // statement 2 accumulatorName(&B, args); // statement 3 combinerName(&A, &B); // statement 4
Example: In the addint kernel, for an input value V :
- Statement 1 is the same as
A += V - Statement 2 is the same as
B = 0 - Statement 3 is the same as
B += V, which is the same asB = V - Statement 4 is the same as
A += B, which is the same asA += V
Statements 1 and 4 set A to the same value, and so this kernel obeys the basic folding rule.
Example: In the findMinAndMax kernel, for an input value V at coordinate X :
- Statement 1 is the same as
A = minmax(A, IndexedVal( V , X )) - Statement 2 is the same as
B = INITVAL - Statement 3 is the same as
which, because B is the initial value, is the same asB = minmax(B, IndexedVal(V, X))
B = IndexedVal(V, X)
- Statement 4 is the same as
which is the same asA = minmax(A, B)
A = minmax(A, IndexedVal(V, X))
Statements 1 and 4 set A to the same value, and so this kernel obeys the basic folding rule.
Calling a reduction kernel from Java code
For a reduction kernel named kernelName defined in the file filename .rs , there are three methods reflected in the class ScriptC_ filename :
Котлин
// Function 1 fun reduce_kernelName(ain1: Allocation, …, ainN: Allocation): javaFutureType // Function 2 fun reduce_kernelName(ain1: Allocation, …, ainN: Allocation, sc: Script.LaunchOptions): javaFutureType // Function 3 fun reduce_kernelName(in1: Array<devecSiIn1Type>, …, inN: Array<devecSiInNType>): javaFutureType
Java
// Method 1 public javaFutureType reduce_kernelName(Allocation ain1, …, Allocation ainN); // Method 2 public javaFutureType reduce_kernelName(Allocation ain1, …, Allocation ainN, Script.LaunchOptions sc); // Method 3 public javaFutureType reduce_kernelName(devecSiIn1Type[] in1, …, devecSiInNType[] inN);
Here are some examples of calling the addint kernel:
Котлин
val script = ScriptC_example(renderScript) // 1D array // and obtain answer immediately val input1 = intArrayOf(…) val sum1: Int = script.reduce_addint(input1).get() // Method 3 // 2D allocation // and do some additional work before obtaining answer val typeBuilder = Type.Builder(RS, Element.I32(RS)).apply { setX(…) setY(…) } val input2: Allocation = Allocation.createTyped(RS, typeBuilder.create()).also { populateSomehow(it) // fill in input Allocation with data } val result2: ScriptC_example.result_int = script.reduce_addint(input2) // Method 1 doSomeAdditionalWork() // might run at same time as reduction val sum2: Int = result2.get()
Java
ScriptC_example script = new ScriptC_example(renderScript); // 1D array // and obtain answer immediately int input1[] = …; int sum1 = script.reduce_addint(input1).get(); // Method 3 // 2D allocation // and do some additional work before obtaining answer Type.Builder typeBuilder = new Type.Builder(RS, Element.I32(RS)); typeBuilder.setX(…); typeBuilder.setY(…); Allocation input2 = createTyped(RS, typeBuilder.create()); populateSomehow(input2); // fill in input Allocation with data ScriptC_example.result_int result2 = script.reduce_addint(input2); // Method 1 doSomeAdditionalWork(); // might run at same time as reduction int sum2 = result2.get();
Method 1 has one input Allocation argument for every input argument in the kernel's accumulator function . The RenderScript runtime checks to ensure that all of the input Allocations have the same dimensions and that the Element type of each of the input Allocations matches that of the corresponding input argument of the accumulator function's prototype. If any of these checks fail, RenderScript throws an exception. The kernel executes over every coordinate in those dimensions.
Method 2 is the same as Method 1 except that Method 2 takes an additional argument sc that can be used to limit the kernel execution to a subset of the coordinates.
Method 3 is the same as Method 1 except that instead of taking Allocation inputs it takes Java array inputs. This is a convenience that saves you from having to write code to explicitly create an Allocation and copy data to it from a Java array. However, using Method 3 instead of Method 1 does not increase the performance of the code . For each input array, Method 3 creates a temporary 1-dimensional Allocation with the appropriate Element type and setAutoPadding(boolean) enabled, and copies the array to the Allocation as if by the appropriate copyFrom() method of Allocation . It then calls Method 1, passing those temporary Allocations.
NOTE: If your application will make multiple kernel calls with the same array, or with different arrays of the same dimensions and Element type, you may improve performance by explicitly creating, populating, and reusing Allocations yourself, instead of by using Method 3.
javaFutureType , the return type of the reflected reduction methods, is a reflected static nested class within the ScriptC_ filename class. It represents the future result of a reduction kernel run. To obtain the actual result of the run, call the get() method of that class, which returns a value of type javaResultType . get() is synchronous .
Котлин
class ScriptC_filename(rs: RenderScript) : ScriptC(…) { object javaFutureType { fun get(): javaResultType { … } } }
Java
public class ScriptC_filename extends ScriptC { public static class javaFutureType { public javaResultType get() { … } } }
javaResultType is determined from the resultType of the outconverter function . Unless resultType is an unsigned type (scalar, vector, or array), javaResultType is the directly corresponding Java type. If resultType is an unsigned type and there is a larger Java signed type, then javaResultType is that larger Java signed type; otherwise, it is the directly corresponding Java type. For example:
- If resultType is
int,int2, orint[15], then javaResultType isint,Int2, orint[]. All values of resultType can be represented by javaResultType . - If resultType is
uint,uint2, oruint[15], then javaResultType islong,Long2, orlong[]. All values of resultType can be represented by javaResultType . - If resultType is
ulong,ulong2, orulong[15], then javaResultType islong,Long2, orlong[]. There are certain values of resultType that cannot be represented by javaResultType .
javaFutureType is the future result type corresponding to the resultType of the outconverter function .
- If resultType is not an array type, then javaFutureType is
result_ resultType. - If resultType is an array of length Count with members of type memberType , then javaFutureType is
resultArray Count _ memberType.
Например:
Котлин
class ScriptC_filename(rs: RenderScript) : ScriptC(…) { // for kernels with int result object result_int { fun get(): Int = … } // for kernels with int[10] result object resultArray10_int { fun get(): IntArray = … } // for kernels with int2 result // note that the Kotlin type name "Int2" is not the same as the script type name "int2" object result_int2 { fun get(): Int2 = … } // for kernels with int2[10] result // note that the Kotlin type name "Int2" is not the same as the script type name "int2" object resultArray10_int2 { fun get(): Array<Int2> = … } // for kernels with uint result // note that the Kotlin type "long" is a wider signed type than the unsigned script type "uint" object result_uint { fun get(): Long = … } // for kernels with uint[10] result // note that the Kotlin type "long" is a wider signed type than the unsigned script type "uint" object resultArray10_uint { fun get(): LongArray = … } // for kernels with uint2 result // note that the Kotlin type "Long2" is a wider signed type than the unsigned script type "uint2" object result_uint2 { fun get(): Long2 = … } // for kernels with uint2[10] result // note that the Kotlin type "Long2" is a wider signed type than the unsigned script type "uint2" object resultArray10_uint2 { fun get(): Array<Long2> = … } }
Java
public class ScriptC_filename extends ScriptC { // for kernels with int result public static class result_int { public int get() { … } } // for kernels with int[10] result public static class resultArray10_int { public int[] get() { … } } // for kernels with int2 result // note that the Java type name "Int2" is not the same as the script type name "int2" public static class result_int2 { public Int2 get() { … } } // for kernels with int2[10] result // note that the Java type name "Int2" is not the same as the script type name "int2" public static class resultArray10_int2 { public Int2[] get() { … } } // for kernels with uint result // note that the Java type "long" is a wider signed type than the unsigned script type "uint" public static class result_uint { public long get() { … } } // for kernels with uint[10] result // note that the Java type "long" is a wider signed type than the unsigned script type "uint" public static class resultArray10_uint { public long[] get() { … } } // for kernels with uint2 result // note that the Java type "Long2" is a wider signed type than the unsigned script type "uint2" public static class result_uint2 { public Long2 get() { … } } // for kernels with uint2[10] result // note that the Java type "Long2" is a wider signed type than the unsigned script type "uint2" public static class resultArray10_uint2 { public Long2[] get() { … } } }
If javaResultType is an object type (including an array type), each call to javaFutureType .get() on the same instance will return the same object.
If javaResultType cannot represent all values of type resultType , and a reduction kernel produces an unrepresentible value, then javaFutureType .get() throws an exception.
Method 3 and devecSiInXType
devecSiInXType is the Java type corresponding to the inXType of the corresponding argument of the accumulator function . Unless inXType is an unsigned type or a vector type, devecSiInXType is the directly corresponding Java type. If inXType is an unsigned scalar type, then devecSiInXType is the Java type directly corresponding to the signed scalar type of the same size. If inXType is a signed vector type, then devecSiInXType is the Java type directly corresponding to the vector component type. If inXType is an unsigned vector type, then devecSiInXType is the Java type directly corresponding to the signed scalar type of the same size as the vector component type. For example:
- If inXType is
int, then devecSiInXType isint. - If inXType is
int2, then devecSiInXType isint. The array is a flattened representation: It has twice as many scalar Elements as the Allocation has 2-component vector Elements. This is the same way that thecopyFrom()methods ofAllocationwork. - If inXType is
uint, then deviceSiInXType isint. A signed value in the Java array is interpreted as an unsigned value of the same bitpattern in the Allocation. This is the same way that thecopyFrom()methods ofAllocationwork. - If inXType is
uint2, then deviceSiInXType isint. This is a combination of the wayint2anduintare handled: The array is a flattened representation, and Java array signed values are interpreted as RenderScript unsigned Element values.
Note that for Method 3 , input types are handled differently than result types:
- A script's vector input is flattened on the Java side, whereas a script's vector result is not.
- A script's unsigned input is represented as a signed input of the same size on the Java side, whereas a script's unsigned result is represented as a widened signed type on the Java side (except in the case of
ulong).
More example reduction kernels
#pragma rs reduce(dotProduct) \ accumulator(dotProductAccum) combiner(dotProductSum) // Note: No initializer function -- therefore, // each accumulator data item is implicitly initialized to 0.0f. static void dotProductAccum(float *accum, float in1, float in2) { *accum += in1*in2; } // combiner function static void dotProductSum(float *accum, const float *val) { *accum += *val; }
// Find a zero Element in a 2D allocation; return (-1, -1) if none #pragma rs reduce(fz2) \ initializer(fz2Init) \ accumulator(fz2Accum) combiner(fz2Combine) static void fz2Init(int2 *accum) { accum->x = accum->y = -1; } static void fz2Accum(int2 *accum, int inVal, int x /* special arg */, int y /* special arg */) { if (inVal==0) { accum->x = x; accum->y = y; } } static void fz2Combine(int2 *accum, const int2 *accum2) { if (accum2->x >= 0) *accum = *accum2; }
// Note that this kernel returns an array to Java #pragma rs reduce(histogram) \ accumulator(hsgAccum) combiner(hsgCombine) #define BUCKETS 256 typedef uint32_t Histogram[BUCKETS]; // Note: No initializer function -- // therefore, each bucket is implicitly initialized to 0. static void hsgAccum(Histogram *h, uchar in) { ++(*h)[in]; } static void hsgCombine(Histogram *accum, const Histogram *addend) { for (int i = 0; i < BUCKETS; ++i) (*accum)[i] += (*addend)[i]; } // Determines the mode (most frequently occurring value), and returns // the value and the frequency. // // If multiple values have the same highest frequency, returns the lowest // of those values. // // Shares functions with the histogram reduction kernel. #pragma rs reduce(mode) \ accumulator(hsgAccum) combiner(hsgCombine) \ outconverter(modeOutConvert) static void modeOutConvert(int2 *result, const Histogram *h) { uint32_t mode = 0; for (int i = 1; i < BUCKETS; ++i) if ((*h)[i] > (*h)[mode]) mode = i; result->x = mode; result->y = (*h)[mode]; }
Additional code samples
The BasicRenderScript , RenderScriptIntrinsic , and Hello Compute samples further demonstrate the use of the APIs covered on this page.