Обзор рендерскрипта

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 или и то, и другое. Среда выполнения 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 входного распределения прототипу функции аккумулятора; если оно не соответствует, 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 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).

Доступ к 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 версии 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 в папке приложения вашего модуля приложения.
    • Добавьте в файл следующие настройки RenderScript:

      классный

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

      Котлин

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

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

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

    Котлин

    import android.support.v8.renderscript.*

    Ява

    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 для создания экземпляра сценария будет таким:

      Котлин

      val invert = ScriptC_invert(renderScript)

      Ява

      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 предоставляет методы копирования для копирования данных в распределения и из него. Метод копирования является синхронным и сериализуется по отношению к любому из асинхронных действий, описанных выше, которые затрагивают одно и то же распределение.

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

RenderScript из одного исходного кода

В 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 в качестве аргументов. Для 1D или 2D распределений размер измерения Y или Z можно опустить. Например, rsCreateAllocation_uchar4(16384) создает одномерное выделение из 16384 элементов, каждый из которых имеет тип uchar4 .

Распределения управляются системой автоматически. Вам не нужно явно освобождать их. Однако вы можете вызвать rsClearObject(rs_allocation* alloc) чтобы указать, что вам больше не нужен alloc выделения базового выделения, чтобы система могла освободить ресурсы как можно раньше.

Раздел «Написание ядра RenderScript» содержит пример ядра, инвертирующего изображение. В приведенном ниже примере показано, как применить к изображению более одного эффекта с помощью RenderScript с одним исходным кодом. Он включает в себя еще одно ядро, greyscale , которое превращает цветное изображение в черно-белое. Затем вызываемая функцияprocess 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)

Ява

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

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

Глобальная переменная сценария — это обычная static глобальная переменная в файле сценария ( .rs ). Для глобального скрипта с именем var, определенного в файле filename .rs , будет метод get_ var отраженный в классе ScriptC_ filename . Если глобальная переменная не является const , также будет метод set_ var .

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

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

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

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

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

Пример. В ядре 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 — это указатель на элемент данных аккумулятора, который эта функция может изменить. in1in 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 — указатель на конечный элемент данных аккумулятора, вычисленный функцией объединения .

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

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

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

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

Чего ты не можешь предположить?

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

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

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

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

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

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

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

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

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

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

  • fMMCombiner(& A , & I ) оставляет A прежним, потому что I равно INITVAL .
  • fMMCombiner(& I , & A ) устанавливает I в A , потому что I INITVAL .

Следовательно, INITVAL действительно является идентификационным значением.

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

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

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

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

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

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

  • 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 тоже.

Функция аккумулятора и функция суммирования вместе должны подчиняться основному правилу свертывания . То есть, если 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 :

Котлин

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

Ява

// 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 :

Котлин

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

Ява

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 проверяет, чтобы все входные распределения имели одинаковые размеры и что тип Element каждого из входных распределений соответствовал типу соответствующего входного аргумента прототипа аккумуляторной функции. Если какая-либо из этих проверок завершается неудачно, RenderScript выдает исключение. Ядро выполняется по каждой координате в этих измерениях.

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

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

ПРИМЕЧАНИЕ. Если ваше приложение будет выполнять несколько вызовов ядра с одним и тем же массивом или с разными массивами одного и того же размера и типа элемента, вы можете повысить производительность, явно создавая, заполняя и повторно используя выделения самостоятельно, вместо использования метода 3.

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

Котлин

class ScriptC_filename(rs: RenderScript) : ScriptC(…) {
    object javaFutureType {
        fun get(): javaResultType {}
    }
}

Ява

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 — это 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> =     }
}

Ява

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, непосредственно соответствующий знаковому скалярному типу того же размера, что и тип векторного компонента. Например:

  • Если inXTypeint , то devecSiInXTypeint .
  • Если inXTypeint2 , то devecSiInXTypeint . Массив представляет собой плоское представление: он имеет в два раза больше скалярных элементов, чем распределение имеет двухкомпонентные векторные элементы. Точно так же работают методы copyFrom() функции Allocation .
  • Если inXTypeuint , то deviceSiInXTypeint . Знаковое значение в массиве Java интерпретируется как беззнаковое значение того же битового шаблона в распределении. Точно так же работают методы copyFrom() функции Allocation .
  • Если inXTypeuint2 , то deviceSiInXTypeint . Это комбинация способов обработки 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, описанных на этой странице.