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

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 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 в папке приложения вашего модуля приложения.
    • Добавьте в файл следующие настройки 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 from 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() последовательно применяет эти два ядра к входному изображению и создает выходное изображение. Распределения как для ввода, так и для вывода передаются как аргументы типа 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 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 для данного запуска ядра. Нет никакой гарантии, что два запуска одного и того же ядра с одинаковыми входными данными создадут одинаковое количество элементов данных аккумулятора.

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

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

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

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

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

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

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

Пример: в ядре AddInt элемент данных аккумулятора инициализируется до нуля. Функция Combiner для этого ядра выполняет дополнение; Ноль является значением идентификации для добавления.

Пример: в ядре 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 функция Combiner добавляет два значения элементов данных аккумулятора; Дополнение коммутативно.

Пример: в ядре 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 функция Combiner добавляет два значения элементов данных аккумулятора:

  • 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
    

Дополнение является ассоциативным, и поэтому функция Combiner также.

Пример: в ядре 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, ScriptC_ filename в filename .rs

Котлин

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

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

Метод 3 такой же, как и метод 1, за исключением того, что вместо того, чтобы принимать входы на распределение, он принимает входы массива Java. Это удобство, которое избавляет вас от необходимости записать код, чтобы явно создать данные и копировать его из массива Java. Однако использование метода 3 вместо метода 1 не увеличивает производительность кода . Для каждой входной массивы метод 3 создает временное 1-мерное распределение с подходящим типом Element и включенным setAutoPadding(boolean) и копирует массив с Allocation , как если бы с помощью соответствующего метода copyFrom() . Затем он вызывает метод 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 определяется из результата функции Outconverter . Если RESTORKTYPE не является беззнатным типом (скаляр, вектор или массив), JavaresultType является непосредственно соответствующим типом Java. Если ResultType является безрецептурным типом, и есть более крупный подписанный Java -тип, то JavaresultType - это то, что более крупный подписанный Java -тип; В противном случае это непосредственно соответствующий тип Java. Например:

  • Если ResoudType int , int2 или int[15] , то javaresulttype - это int , Int2 или int[] . Все значения ResultType могут быть представлены Javaresulttype .
  • Если ResultType uint , uint2 или uint[15] , то javaresulttype - это long , Long2 или long[] . Все значения ResultType могут быть представлены Javaresulttype .
  • Если ResoudType - ulong , ulong2 или ulong[15] , то javaresulttype - это long , Long2 или long[] . Существуют определенные значения результата , которые не могут быть представлены Javaresulttype .

Javafuturetype - это будущий тип результата, соответствующий результату функции Outconverter .

  • Если ResultType не является типом массива, то javafuturetype - result_ resultType .
  • Если ResultType является массивом количества длины с членами типа 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 не может представлять все значения типа Resultype , а ядро ​​снижения дает непреднамеренное значение, то javaFutureType .get() вызывает исключение.

Метод 3 и devecsiinxtype

devecsiinxtype - это тип Java, соответствующий инктипе соответствующего аргумента функции аккумулятора . Если inxtype не является бессобным типом или векторным типом, Devecsiinxtype является непосредственно соответствующим типом Java. Если inxtype является беззнатным скалярным типом, то Devecsiinxtype - это тип Java, непосредственно соответствующий подписанному скалярному типу того же размера. Если inxtype является подписанным векторным типом, то devecsiinxtype является типом Java, непосредственно соответствующим типу векторного компонента. Если inxtype является типом без знаки векторного типа, то devecsiinxtype - тип Java, непосредственно соответствующий подписанному скалярному типу того же размера, что и тип векторного компонента. Например:

  • Если inxtype in int , то devecsiinxtype in int .
  • Если inxtype - это int2 , то Devecsiinxtype - это int . Массив представляет собой сплющенное представление: у него в два раза больше скалярных элементов, чем ассигнование имеет 2-компонентные векторные элементы. Это так же, как методы copyFrom() работы Allocation .
  • Если inxtype uint , то Deficesiinxtype IS int . Подписанное значение в массиве Java интерпретируется как неподписанное значение одного и того же битпаттерна в распределении. Это так же, как методы copyFrom() работы Allocation .
  • Если inxtype - это uint2 , то Deficesiinxtype - это 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 Samples дополнительно демонстрируют использование API, охватываемых на этой странице.