Обзор RenderScript

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

Чтобы начать работу с RenderScript, необходимо понять две основные концепции:

  • Сам язык является производным от C99 и предназначен для написания высокопроизводительного вычислительного кода. В статье Writing a RenderScript Kernel (Как написать ядро RenderScript) рассказывается, как использовать этот язык для написания вычислительных ядер.
  • Control API используется для управления временем жизни ресурсов RenderScript и выполнения ядра. Он доступен на трех языках: Java, C++ в Android NDK и на самом языке ядра, производном от C99. Первый и третий варианты описаны в статьях Использование RenderScript в коде Java и Single-Source RenderScript.

Написание ядра RenderScript

Ядро RenderScript обычно находится в файле .rs в каталоге <project_root>/src/rs. Каждый файл .rs называется скриптом. Каждый скрипт содержит собственный набор ядер, функций и переменных. Скрипт может содержать:

  • Объявление pragma (#pragma version(1)), в котором указывается версия языка ядра RenderScript, используемая в этом скрипте. В настоящее время единственное допустимое значение – 1.
  • Объявление pragma (#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 или и то, и другое. Среда выполнения RenderScript проверяет, имеют ли все входные и выходные объекты Allocation одинаковые размеры и соответствуют ли типы Element входных и выходных объектов Allocation прототипу ядра. Если любая из этих проверок не пройдена, 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, которую можно вызвать из любого ядра или вызываемой функции в скрипте, но которая не предоставляется Java API. Если к глобальной переменной или функции скрипта не требуется обращаться из кода Java, настоятельно рекомендуется объявить ее как static.

Настройка точности чисел с плавающей запятой

Вы можете управлять требуемым уровнем точности чисел с плавающей запятой в скрипте. Это полезно, если не требуется полное соответствие стандарту IEEE 754-2008 (используется по умолчанию). Следующие прагмы могут задавать разные уровни точности чисел с плавающей запятой:

  • #pragma rs_fp_full (по умолчанию, если ничего не указано) – для приложений, которым требуется точность с плавающей запятой, как описано в стандарте IEEE 754-2008.
  • #pragma rs_fp_relaxed – для приложений, которым не требуется строгое соответствие стандарту IEEE 754-2008 и которые могут работать с меньшей точностью. В этом режиме денормализованные числа округляются до нуля.
  • #pragma rs_fp_imprecise – для приложений, не требующих высокой точности. В этом режиме доступны все возможности rs_fp_relaxed, а также следующие:
    • Операции, результатом которых является -0.0, могут возвращать +0.0.
    • Операции с INF и NAN не определены.

Большинство приложений могут использовать rs_fp_relaxed без каких-либо побочных эффектов. Это может быть очень полезно для некоторых архитектур благодаря дополнительным оптимизациям, доступным только при пониженной точности (например, инструкциям SIMD CPU).

Доступ к 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 из библиотеки поддержки, выполните следующие действия:

  1. Убедитесь, что у вас установлена нужная версия Android SDK.
  2. Обновите настройки сборки Android, чтобы включить настройки RenderScript:
    • Откройте файл build.gradle в папке app модуля приложения.
    • Добавьте в файл следующие настройки RenderScript:

      Яркий

              android {
                  compileSdkVersion 36
      
                  defaultConfig {
                      minSdkVersion 9
                      targetSdkVersion 19
      
                      renderscriptTargetApi 18
                      renderscriptSupportModeEnabled true
                  }
              }
              

      Kotlin

              android {
                  compileSdkVersion(36)
      
                  defaultConfig {
                      minSdkVersion(9)
                      targetSdkVersion(19)
      
                      renderscriptTargetApi = 18
                      renderscriptSupportModeEnabled = true
                  }
              }
              

      Перечисленные выше настройки управляют определенным поведением в процессе сборки Android:

      • renderscriptTargetApi – указывает версию байт-кода, которую нужно сгенерировать. Рекомендуем указать в качестве значения самый низкий уровень API, который поддерживает все используемые вами функции, и задать для renderscriptSupportModeEnabled значение true. Допустимые значения для этого параметра – любое целое число от 11 до уровня API, выпущенного последним. Если в манифесте приложения указана другая минимальная версия SDK, она будет игнорироваться, а в качестве минимальной версии SDK будет использоваться целевое значение из файла сборки.
      • renderscriptSupportModeEnabled – указывает, что сгенерированный байт-код должен вернуться к совместимой версии, если устройство, на котором он выполняется, не поддерживает целевую версию.
  3. В классах приложения, использующих RenderScript, добавьте импорт для классов библиотеки поддержки:

    Kotlin

    import android.support.v8.renderscript.*

    Java

    import android.support.v8.renderscript.*;

Как использовать RenderScript в коде Java или Kotlin

Для использования RenderScript в коде Java или Kotlin требуются классы API, расположенные в пакете android.renderscript или android.support.v8.renderscript. Большинство приложений используют один и тот же базовый шаблон:

  1. Инициализируйте контекст RenderScript. Контекст RenderScript, созданный с помощью create(Context), обеспечивает возможность использования RenderScript и предоставляет объект для управления временем жизни всех последующих объектов RenderScript. Создание контекста следует рассматривать как потенциально продолжительную операцию, поскольку при этом могут создаваться ресурсы на разных аппаратных компонентах. По возможности не включайте его в критический путь приложения. Как правило, в приложении одновременно используется только один контекст RenderScript.
  2. Создайте хотя бы один Allocation, который будет передаваться скрипту. Allocation – это объект RenderScript, который обеспечивает хранение фиксированного объема данных. Ядра в скриптах принимают объекты Allocation в качестве входных и выходных данных, а доступ к объектам Allocation в ядрах можно получить с помощью rsGetElementAt_type() и rsSetElementAt_type(), если они связаны как глобальные переменные скрипта. Объекты Allocation позволяют передавать массивы из кода Java в код RenderScript и наоборот. Объекты Allocation обычно создаются с помощью createTyped() или createFromBitmap().
  3. Создайте нужные скрипты. При использовании RenderScript доступны два типа скриптов:
    • ScriptC – пользовательские скрипты, описанные в разделе Создание ядра RenderScript выше. Каждый скрипт имеет класс Java, отраженный компилятором RenderScript, чтобы упростить доступ к скрипту из кода Java. Этот класс имеет имя ScriptC_filename. Например, если бы ядро сопоставления, приведенное выше, находилось в invert.rs, а контекст RenderScript – в mRenderScript, то код Java или Kotlin для создания экземпляра скрипта выглядел бы так:

      Kotlin

      val invert = ScriptC_invert(renderScript)

      Java

      ScriptC_invert invert = new ScriptC_invert(renderScript);
    • ScriptIntrinsic. Это встроенные ядра RenderScript для распространенных операций, таких как размытие по Гауссу, свертка и смешивание изображений. Дополнительную информацию можно найти в подклассах ScriptIntrinsic.
  4. Заполните распределения данными. За исключением распределений, созданных с помощью createFromBitmap(), распределение заполняется пустыми данными при первом создании. Чтобы заполнить распределение, используйте один из методов копирования в Allocation. Методы копирования являются синхронными.
  5. Задайте необходимые глобальные переменные. Глобальные переменные можно задать с помощью методов в том же классе ScriptC_filename с именем set_globalname. Например, чтобы задать переменную int с названием threshold, используйте метод Java set_threshold(int), а чтобы задать переменную rs_allocation с названием lookup – метод Java set_lookup(Allocation). Методы set являются асинхронными.
  6. Запустите подходящие ядра и вызываемые функции.

    Методы запуска заданного ядра отражаются в том же классе ScriptC_filename с методами, названными forEach_mappingKernelName() или reduce_reductionKernelName(). Эти запуски асинхронны. В зависимости от аргументов ядра метод принимает одно или несколько распределений, все из которых должны иметь одинаковые размеры. По умолчанию ядро выполняется для каждой координаты в этих измерениях. Чтобы выполнить ядро для подмножества этих координат, передайте соответствующий Script.LaunchOptions в качестве последнего аргумента методу forEach или reduce.

    Запускайте вызываемые функции с помощью методов invoke_functionName, отраженных в том же классе ScriptC_filename. Эти запуски асинхронны.

  7. Извлечение данных из объектов Allocation и javaFutureType. Чтобы получить доступ к данным из Allocation в коде Java, необходимо скопировать эти данные обратно в Java, используя один из методов копирования в Allocation. Чтобы получить результат ядра редукции, необходимо использовать метод javaFutureType.get(). Методы copy и get() являются синхронными.
  8. Удалите контекст RenderScript. Удалить контекст RenderScript можно с помощью метода destroy() или разрешив сборку мусора для объекта контекста RenderScript. Это приводит к тому, что при любом дальнейшем использовании любого объекта, принадлежащего этому контексту, возникает исключение.

Асинхронная модель выполнения

Отраженные методы forEach, invoke, reduce и set являются асинхронными. Каждый из них может возвращать значение в Java до завершения запрошенного действия. Однако отдельные действия сериализуются в том порядке, в котором они запускаются.

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

Отраженные классы javaFutureType предоставляют метод get() для получения результата сокращения. get() – синхронная операция, которая сериализуется по отношению к редукции (асинхронной операции).

RenderScript с одним источником

В Android 7.0 (уровень API 24) появилась новая функция программирования – Single-Source RenderScript. Она позволяет запускать ядра из скрипта, в котором они определены, а не из Java. В настоящее время этот подход ограничен ядрами сопоставления, которые в этом разделе для краткости называются просто "ядрами". Эта функция также позволяет создавать распределения типа rs_allocation из скрипта. Теперь можно реализовать весь алгоритм исключительно в скрипте, даже если требуется несколько запусков ядра. Это дает два преимущества: код становится более читаемым, поскольку алгоритм реализуется на одном языке, и потенциально более быстрым, поскольку при нескольких запусках ядра происходит меньше переходов между Java и RenderScript.

В Single-Source RenderScript вы пишете ядра, как описано в разделе Writing a RenderScript Kernel. Затем вы пишете вызываемую функцию, которая вызывает 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) создает одномерное распределение из 16 384 элементов, каждый из которых имеет тип 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 следующим образом:

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, отделяя запуск ядер от их определения, что усложнило бы понимание всего алгоритма. Код RenderScript с одним источником не только легче читать, но и устраняет переход между Java и скриптом при запуске ядра. Некоторые итеративные алгоритмы могут запускать ядра сотни раз, что приводит к значительным накладным расходам на такой переход.

Глобальные переменные скрипта

Глобальная переменная скрипта – это обычная глобальная переменная, не относящаяся к типу static, в файле скрипта (.rs). Для глобальной переменной var, определенной в файле filename.rs, в классе ScriptC_filename будет отражен метод get_var. Если глобальный объект не равен const, также будет доступен метод set_var.

У глобальной переменной скрипта есть два отдельных значения: Java и script. Эти значения работают следующим образом:

  • Если у var есть статический инициализатор в скрипте, он указывает начальное значение var и в Java, и в скрипте. В противном случае начальное значение равно нулю.
  • Обращения к var в скрипте считывают и записывают его значение.
  • Метод 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 нет функции combiner, поэтому будет использоваться функция accumulator. Это правильное поведение, потому что если мы разделим набор значений на две части и сложим значения в этих двух частях по отдельности, сложение этих двух сумм будет таким же, как сложение всего набора.

Пример. В ядре findMinAndMax функция объединения проверяет, меньше ли минимальное значение, записанное в элементе данных аккумулятора "источник" *val, минимального значения, записанного в элементе данных аккумулятора "назначение" *accum, и соответствующим образом обновляет *accum. Аналогично работает и функция MAX. В результате *accum переходит в состояние, которое было бы достигнуто, если бы все входные значения были накоплены в *accum, а не частично в *accum и частично в *val.

После объединения всех элементов данных аккумулятора RenderScript определяет результат сокращения, который возвращается в Java. Для этого можно написать функцию outconverter. Если вы хотите, чтобы результатом редукции было итоговое значение объединенных элементов данных аккумулятора, писать функцию outconverter не нужно.

Пример. В ядре 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) (обязательный) – указывает, что определяется ядро редукции. Отраженный метод Java reduce_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–inN – один или несколько аргументов, которые автоматически заполняются на основе входных данных, переданных при запуске ядра (по одному аргументу на входные данные). Функция-аккумулятор может принимать любые специальные аргументы.

    Пример ядра с несколькими входными данными – dotProduct.

  • combiner(combinerName)

    (необязательно) – указывает название функции объединения для этого ядра редукции. После того как RenderScript вызовет функцию аккумулятора по одному разу для каждой координаты во входных данных, он вызовет эту функцию столько раз, сколько необходимо, чтобы объединить все элементы данных аккумулятора в один элемент данных аккумулятора. Функция должна быть определена следующим образом:

    static void combinerName(accumType *accum, const accumType *other) { … }

    accum – указатель на целевой элемент данных аккумулятора, который будет изменен этой функцией. other – указатель на "исходный" элемент данных аккумулятора, который функция должна объединить с *accum.

    Примечание. Возможно, что *accum, *other или оба значения были инициализированы, но не переданы в функцию-аккумулятор, то есть не обновлены в соответствии с входными данными. Например, в ядре findMinAndMax функция combiner fMMCombiner явно проверяет idx < 0, поскольку это указывает на элемент данных аккумулятора, значение которого равно INITVAL.

    Если вы не предоставите функцию объединения, RenderScript использует вместо нее функцию аккумулятора, которая ведет себя так, как если бы существовала функция объединения, которая выглядит следующим образом:

    static void combinerName(accumType *accum, const accumType *other) {
      accumulatorName(accum, *other);
    }

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

  • outconverter(outconverterName) (необязательно) – указывает название функции outconverter для этого ядра редукции. После того как RenderScript объединит все элементы данных аккумулятора, он вызовет эту функцию, чтобы определить результат редукции, который будет возвращен в Java. Функция должна быть определена следующим образом:

    static void outconverterName(resultType *result, const accumType *accum) { … }

    result – указатель на элемент данных результата (выделенный, но не инициализированный средой выполнения RenderScript), который эта функция должна инициализировать с результатом редукции. resultType – тип этого элемента данных, который не обязательно должен совпадать с accumType. accum – указатель на последний элемент данных аккумулятора, вычисленный функцией объединения.

    Если вы не предоставите функцию outconverter, RenderScript скопирует последний элемент данных аккумулятора в элемент данных результата, ведя себя так, как если бы существовала функция outconverter, которая выглядит следующим образом:

    static void outconverterName(accumType *result, const accumType *accum) {
      *result = *accum;
    }

    Если вы хотите получить результат, тип которого отличается от типа данных аккумулятора, то функция outconverter обязательна.

Обратите внимание, что у ядра есть типы входных данных, тип элемента данных аккумулятора и тип результата, ни один из которых не должен быть одинаковым. Например, в ядре findMinAndMax тип входных данных long, тип элемента данных аккумулятора MinAndMax и тип результата int2 различаются.

Что нельзя предположить?

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

Не следует полагаться на порядок, в котором RenderScript вызывает функции инициализатора, аккумулятора и объединителя. Некоторые из них могут вызываться параллельно. Нет гарантии, что два запуска одного и того же ядра с одними и теми же входными данными будут выполняться в одном и том же порядке. Гарантируется только то, что неинициализированный элемент данных аккумулятора будет виден только функции инициализатора. Пример:

  • Нет гарантии, что все элементы данных аккумулятора будут инициализированы до вызова функции аккумулятора, хотя она будет вызываться только для инициализированного элемента данных аккумулятора.
  • Порядок, в котором входные элементы передаются в функцию аккумулятора, не гарантируется.
  • Нет гарантии, что функция accumulator будет вызвана для всех входных элементов до вызова функции combiner.

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

Что вы должны гарантировать?

Поскольку система RenderScript может выполнять ядро разными способами, вы должны следовать определенным правилам, чтобы ядро работало так, как вы хотите. Если вы не будете следовать этим правилам, то можете получить неверные результаты, недетерминированное поведение или ошибки выполнения.

В правилах ниже часто говорится, что два элемента данных аккумулятора должны иметь одинаковое значение. Что это значит Это зависит от того, что должно делать ядро. Для математической редукции, такой как addint, обычно имеет смысл, чтобы "то же самое" означало математическое равенство. Для поиска "выбрать любой", например findMinAndMax ("найти местоположение минимального и максимального входных значений"), где может быть несколько вхождений идентичных входных значений, все местоположения заданного входного значения должны считаться "одинаковыми". Вы можете написать аналогичное ядро, чтобы найти местоположение крайних левых минимальных и максимальных входных значений, где, скажем, минимальное значение в местоположении 100 предпочтительнее идентичного минимального значения в местоположении 200. Для этого ядра "то же самое" будет означать идентичное местоположение, а не просто идентичное значение, а функции аккумулятора и объединителя должны отличаться от функций для findMinAndMax.

Функция инициализации должна создавать значение идентификатора. То есть если I и A – элементы данных аккумулятора, инициализированные функцией инициализатора, и I никогда не передавался функции аккумулятора (а A передавался), то:

Пример. В ядре addint элемент данных аккумулятора инициализируется нулем. Функция объединения для этого ядра выполняет сложение, а ноль – это значение идентификатора для сложения.

Пример. В ядре findMinAndMax элемент данных аккумулятора инициализируется как INITVAL.

  • fMMCombiner(&A, &I) не меняет A, потому что I – это INITVAL.
  • fMMCombiner(&I, &A) задает для I значение A, потому что I – это INITVAL.

Таким образом, INITVAL действительно является значением идентификатора.

Функция combiner должна быть коммутативной. То есть если A и B – элементы данных аккумулятора, инициализированные функцией инициализатора и, возможно, переданные функции аккумулятора ноль или более раз, то combinerName(&A, &B) должна задать для A то же значение, которое combinerName(&B, &A) задает для B.

Пример. В ядре addint функция объединения складывает значения двух элементов данных аккумулятора. Сложение коммутативно.

Пример. В ядре findMinAndMax fMMCombiner(&A, &B) – это то же самое, что и A = minmax(A, B), а minmax коммутативно, поэтому fMMCombiner также коммутативно.

Функция combiner должна быть ассоциативной. То есть если A, B и C – это элементы данных аккумулятора, инициализированные функцией инициализатора и, возможно, переданные функции аккумулятора ноль или более раз, то следующие две последовательности кода должны задать для A одно и то же значение:

  • combinerName(&A, &B);
    combinerName(&A, &C);
  • combinerName(&B, &C);
    combinerName(&A, &B);

Пример. В ядре addint функция combiner складывает значения двух элементов данных accumulator:

  • 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

Сложение ассоциативно, поэтому функция объединения также ассоциативна.

Пример. В ядре findMinAndMax

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 ассоциативна, поэтому fMMCombiner тоже.

Функции accumulator и combiner должны соответствовать базовому правилу свертки. То есть если A и B – элементы данных аккумулятора, A инициализирован функцией инициализатора и мог быть передан функции аккумулятора ноль или более раз, B не инициализирован, а args – список входных и специальных аргументов для определенного вызова функции аккумулятора, то следующие две последовательности кода должны задать для A одно и то же значение:

  • accumulatorName(&A, args);  // statement 1
  • initializerName(&B);        // statement 2
    accumulatorName(&B, args);  // statement 3
    combinerName(&A, &B);       // statement 4

Пример. В ядре addint для входного значения V:

  • Утверждение 1 совпадает с A += V
  • Утверждение 2 совпадает с B = 0
  • Утверждение 3 – это то же самое, что и B += V, которое, в свою очередь, означает то же самое, что и B = V.
  • Утверждение 4 – это то же самое, что и A += B, а это то же самое, что и A += V.

Инструкции 1 и 4 задают для A одно и то же значение, поэтому это ядро подчиняется основному правилу свертки.

Пример. В ядре findMinAndMax для входного значения V в координате X:

  • Утверждение 1 совпадает с A = minmax(A, IndexedVal(V, X))
  • Утверждение 2 совпадает с B = INITVAL
  • Утверждение 3 совпадает с
    B = minmax(B, IndexedVal(V, X))
    Поскольку B – это начальное значение, то формула будет выглядеть так:
    B = IndexedVal(V, X)
  • Утверждение 4 совпадает с
    A = minmax(A, B)
    , что равно
    A = minmax(A, IndexedVal(V, X))

Инструкции 1 и 4 задают для A одно и то же значение, поэтому это ядро подчиняется основному правилу свертки.

Вызов ядра редукции из кода Java

Для ядра редукции с именем kernelName, определенного в файле filename.rs, в классе ScriptC_filename есть три метода:

Kotlin

// 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);

Вот несколько примеров вызова ядра addint:

Kotlin

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();

Метод 1 имеет один входной аргумент Allocation для каждого входного аргумента в функции аккумулятора ядра. Среда выполнения RenderScript проверяет, чтобы все входные объекты Allocation имели одинаковые размеры и чтобы тип Element каждого из них соответствовал типу соответствующего входного аргумента прототипа функции аккумулятора. Если какая-либо из этих проверок не удалась, RenderScript выдает исключение. Ядро выполняется по всем координатам в этих измерениях.

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

Способ 3 аналогичен способу 1, но вместо входных данных Allocation используются входные данные массива Java. Это удобно, поскольку вам не нужно писать код для явного создания Allocation и копирования в него данных из массива Java. Однако использование способа 3 вместо способа 1 не повышает производительность кода. Для каждого входного массива метод 3 создает временный одномерный объект Allocation с соответствующим типом Element и включенным флагом setAutoPadding(boolean), а затем копирует массив в объект Allocation, как если бы использовался соответствующий метод copyFrom() класса Allocation. Затем вызывается метод 1, которому передаются эти временные распределения.

Примечание. Если ваше приложение будет выполнять несколько вызовов ядра с одним и тем же массивом или с разными массивами одинаковых размеров и типа Element, вы можете повысить производительность, явно создавая, заполняя и повторно используя объекты Allocation самостоятельно, а не с помощью метода 3.

javaFutureType, тип возвращаемого значения отраженных методов сокращения, – это отраженный статический вложенный класс в классе ScriptC_filename. Он представляет будущий результат выполнения ядра редукции. Чтобы получить фактический результат выполнения, вызовите метод get() этого класса, который возвращает значение типа javaResultType. get() – синхронный.

Kotlin

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 определяется на основе resultType функции outconverter. Если resultType не является беззнаковым типом (скаляр, вектор или массив), javaResultType – это непосредственно соответствующий тип Java. Если resultType – это беззнаковый тип и существует более крупный знаковый тип Java, то javaResultType – это более крупный знаковый тип Java. В противном случае это непосредственно соответствующий тип Java. Пример:

  • Если resultType – int, int2 или int[15], то javaResultType – int, Int2 или int[]. Все значения resultType могут быть представлены в виде javaResultType.
  • Если resultType – uint, uint2 или uint[15], то javaResultType – long, Long2 или long[]. Все значения resultType могут быть представлены в виде javaResultType.
  • Если resultType – это ulong, ulong2 или ulong[15], то javaResultType – это long, Long2 или long[]. Некоторые значения resultType не могут быть представлены в javaResultType.

javaFutureType – тип будущего результата, соответствующий resultType функции outconverter.

  • Если resultType не является типом массива, то javaFutureType – это result_resultType.
  • Если resultType – это массив длиной Count с элементами типа memberType, то javaFutureType – это resultArrayCount_memberType.

Пример:

Kotlin

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() { … }
  }
}

Если javaResultType – это тип объекта (включая тип массива), каждый вызов javaFutureType.get() для одного и того же экземпляра будет возвращать один и тот же объект.

Если javaResultType не может представлять все значения типа resultType, а ядро редукции создает непредставимое значение, то javaFutureType.get() выдает исключение.

Метод 3 и devecSiInXType

devecSiInXType – это тип Java, соответствующий inXType соответствующего аргумента функции аккумулятора. Если inXType не является беззнаковым или векторным типом, то devecSiInXType – это непосредственно соответствующий тип Java. Если inXType – беззнаковый скалярный тип, то devecSiInXType – тип Java, напрямую соответствующий знаковому скалярному типу того же размера. Если inXType – это тип вектора со знаком, то devecSiInXType – это тип Java, напрямую соответствующий типу компонента вектора. Если inXType – это тип вектора без знака, то devecSiInXType – это тип Java, который напрямую соответствует скалярному типу со знаком того же размера, что и тип компонента вектора. Пример:

  • Если inXType – int, то devecSiInXType – int.
  • Если inXType – int2, то devecSiInXType – int. Массив представляет собой сглаженное представление: в нем в два раза больше скалярных элементов, чем в Allocation – двухкомпонентных векторных элементов. Так же работают методы copyFrom() в Allocation.
  • Если inXType – uint, то deviceSiInXType – int. Значение со знаком в массиве Java интерпретируется как значение без знака с тем же битовым шаблоном в Allocation. Аналогичным образом работают методы copyFrom() в Allocation.
  • Если inXType – uint2, то deviceSiInXType – int. Это комбинация способов обработки int2 и uint: массив представляет собой плоское представление, а значения со знаком массива Java интерпретируются как значения беззнакового элемента RenderScript.

Обратите внимание, что в способе 3 типы входных данных обрабатываются иначе, чем типы результатов.

  • Векторные входные данные скрипта выравниваются на стороне Java, а векторные результаты – нет.
  • Беззнаковые входные данные скрипта представлены как знаковые входные данные того же размера на стороне Java, тогда как беззнаковый результат скрипта представлен как расширенный знаковый тип на стороне Java (за исключением случая с ulong).

Больше примеров ядер редукции

#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];
}

Дополнительные примеры кода

Примеры BasicRenderScript, RenderScriptIntrinsic и Hello Compute демонстрируют использование API, описанных на этой странице.