Descripción general de RenderScript

RenderScript es un marco de trabajo para ejecutar tareas con mucha carga de procesamiento y alto rendimiento en Android. RenderScript está orientado principalmente al uso con procesamiento paralelo de datos, aunque las cargas de trabajo en serie también pueden beneficiarse. El entorno de ejecución de RenderScript permite trabajar en paralelo con todos los procesadores disponibles en un dispositivo, como las GPU y CPU de varios núcleos. Eso te permite concentrarte en la expresión de algoritmos en lugar de en la programación del trabajo. RenderScript es especialmente útil para aplicaciones que ejecutan procesamiento de imágenes, fotografía computacional o visión artificial.

Para comenzar a usar RenderScript, hay dos conceptos principales que debes comprender:

  • El lenguaje en sí es un derivado de C99 para escribir código de computación de alto rendimiento. En Cómo escribir un kernel de RenderScript, se describe cómo usarlo para escribir kernels de procesamiento.
  • La API de control se usa para administrar el ciclo de vida de los recursos de RenderScript y controlar la ejecución del kernel. Está disponible en tres lenguajes diferentes: Java, C++ en NDK de Android y el lenguaje kernel derivado de C99. En Cómo usar RenderScript desde código Java y RenderScript de una fuente única se describen la primera y la tercera opción, respectivamente.

Cómo escribir un kernel de RenderScript

Un kernel de RenderScript generalmente reside en un archivo .rs en el directorio <project_root>/src/; cada archivo se denomina secuencia de comandos. Cada secuencia de comandos contiene su propio conjunto de kernels, funciones y variables. Una secuencia de comandos puede incluir lo siguiente:

  • Una declaración pragma (#pragma version(1)) que declara la versión del lenguaje kernel de RenderScript que se usa en la secuencia de comandos. En la actualidad, 1 es el único valor válido.
  • Una declaración pragma (#pragma rs java_package_name(com.example.app)) que declara el nombre del paquete de las clases Java reflejadas en la secuencia de comandos. Ten en cuenta que tu archivo .rs debe formar parte del paquete de aplicación y no de un proyecto de biblioteca.
  • Cero o más funciones invocables. Una función invocable es una función RenderScript de subproceso único que puedes llamar desde el código Java con argumentos arbitrarios. A menudo son útiles para la configuración inicial o los procesamientos en serie dentro de una canalización de procesamiento más grande.
  • Cero o más globales de secuencias de comandos. Un global de secuencia de comandos es similar a una variable global en C. Puedes acceder a los globales de secuencias de comandos desde el código Java, y estos se usan a menudo para pasar parámetros a los kernels de RenderScript. Los globales de secuencias de comandos se explican con más detalle aquí.

  • Cero o más kernels de procesamiento. Un kernel de procesamiento es una función o colección de funciones que puedes indicar que el entorno de ejecución de RenderScript ejecute en paralelo en una colección de datos. Existen dos tipos de kernels de procesamiento: kernels de asignación (también llamados kernels foreach) y kernels de reducción.

    Un kernel de asignación es una función paralela que opera en una colección de Allocations de las mismas dimensiones. De forma predeterminada, se ejecuta una vez por cada coordenada en esas dimensiones. Se suele usar (aunque no exclusivamente) para transformar una colección de Allocations de entrada en Allocation de salida, un Element a la vez.

    • A continuación, se incluye un ejemplo de un kernel de asignación simple:

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

      En términos generales, es idéntico a una función C estándar. La propiedad RS_KERNEL aplicada al prototipo de la función especifica que la función es un kernel de asignación RenderScript en lugar de una función invocable. El argumento in se completa automáticamente según la Allocation de entrada que se pasa al lanzamiento del kernel. Los argumentos x y y se analizan a continuación. El valor que muestra el kernel se escribe automáticamente en la ubicación adecuada en la Allocation de salida. De manera predeterminada, este kernel se ejecuta en toda su Allocation de entrada, con una ejecución de la función del kernel por Element en la Allocation.

      Un kernel de asignación puede tener una o más Allocations de entrada, una sola Allocation de salida, o ambas. El entorno de ejecución de RenderScript comprueba que todas las asignaciones de entrada y salida tengan las mismas dimensiones y que los tipos Element de las asignaciones de entrada y salida coincidan con el prototipo del kernel. Si falla alguna de esas comprobaciones, RenderScript arroja una excepción.

      NOTA: Antes de Android 6.0 (API nivel 23), un kernel de asignación no podía tener más de una Allocation de entrada.

      Si necesitas más Allocations de entrada o salida de las que tiene el kernel, esos objetos deben estar vinculados a globales de secuencias de comandos de rs_allocation y se debe acceder a ellos desde un kernel o función invocable por medio de rsGetElementAt_type() o rsSetElementAt_type().

      NOTA: RS_KERNEL es una macro definida automáticamente por RenderScript para tu conveniencia:

          #define RS_KERNEL __attribute__((kernel))
          

    Un kernel de reducción es una familia de funciones que opera en una colección de Allocations de entrada de las mismas dimensiones. De forma predeterminada, su función acumuladora se ejecuta una vez por cada coordenada en esas dimensiones. Se suele usar (aunque no exclusivamente) para "reducir" una colección de Allocations de entrada a un solo valor.

    • A continuación, se incluye un ejemplo de un kernel de reducción simple que suma el Elements de su entrada:

          #pragma rs reduce(addint) accumulator(addintAccum)
      
          static void addintAccum(int *accum, int val) {
            *accum += val;
          }

      Un kernel de reducción incluye una o más funciones escritas por el usuario. #pragma rs reduce se utiliza para definir el kernel especificando su nombre (en este ejemplo,addint) y los nombres y los roles de las funciones que conforman el kernel (en este ejemplo, un addintAccum de una función accumulator) Todas estas funciones deben ser static. Un kernel de reducción siempre requiere una función accumulator. También puede tener otras funciones según lo que quieras que haga.

      Una función de acumulador del kernel de reducción debe mostrar void y debe tener al menos dos argumentos. El primer argumento (en este ejemplo, accum) es un puntero a un elemento de datos del acumulador y el segundo (en este ejemplo, val) se completa automáticamente según la Allocation de entrada que se pasa al lanzamiento del kernel. El elemento de datos del acumulador se crea mediante el entorno de ejecución de RenderScript. De manera predeterminada, se inicializa en cero. De manera predeterminada, este kernel se ejecuta en toda su Allocation de entrada, con una ejecución de la función de acumulador por Element en la Allocation. De manera predeterminada, el valor final del elemento de datos del acumulador se trata como resultado de la reducción y se devuelve a Java. El entorno de ejecución de RenderScript comprueba que el tipo Element de la asignación de entrada coincida con el prototipo de la función del acumulador. Si no coincide, RenderScript arroja una excepción.

      Un kernel de reducción tiene una o más Allocations de entrada, pero ninguna de salida.

      Los kernels de reducción se explican con más detalle aquí.

      Los kernels de reducción son compatibles con Android 7.0 (API nivel 24) y versiones posteriores.

    Una función de kernel de asignación o una función de kernel de reducción de acumulador pueden acceder a las coordenadas de la ejecución actual con los argumentos especiales x, y y z, que deben ser de tipo int o uint32_t. Estos argumentos son opcionales.

    Una función de kernel de asignación o una función de kernel de reducción de acumulador también pueden admitir el argumento especial opcional context de tipo rs_kernel_context. Es necesario para una familia de API de entorno de ejecución que se utilizan para consultar ciertas propiedades de la ejecución actual, por ejemplo, rsGetDimX. (El argumento context está disponible en Android 6.0 [API nivel 23] y versiones posteriores).

  • Una función init() opcional. La función init() es un tipo especial de función invocable que ejecuta RenderScript cuando se crea una instancia de la secuencia de comandos por primera vez. Eso permite que algunos procesamientos ocurran automáticamente en la creación de la secuencia de comandos.
  • Cero o más globales y funciones de secuencias de comandos estáticas . Un global de secuencia de comandos estática es equivalente a un global de secuencia de comandos. La única diferencia es que no se puede acceder desde el código Java. Una función estática es una función C estándar que se puede llamar desde cualquier kernel o función invocable en la secuencia de comandos, pero no está expuesta a la API de Java. Si no es necesario acceder a un global o una función de secuencia de comandos desde el código Java, se recomienda que se declare static.

Cómo configurar la precisión de punto flotante

Puedes controlar el nivel requerido de precisión de punto flotante en una secuencia de comandos. Esto es útil si no se requiere el estándar IEEE 754-2008 completo (usado de forma predeterminada). Con los siguientes pragmas, se puede establecer un nivel diferente de precisión de punto flotante:

  • #pragma rs_fp_full (opción predeterminada si no se especifica otra), para apps que requieren precisión de punto flotante según lo establecido en el estándar IEEE 754-2008.
  • #pragma rs_fp_relaxed, para apps que no requieren un cumplimiento estricto de IEEE 754-2008 y pueden tolerar una menor precisión. Este modo habilita el vaciado a cero para denormales y el redondeo a cero.
  • #pragma rs_fp_imprecise, para apps que no tienen requisitos de precisión estrictos. Este modo habilita todo en rs_fp_relaxed junto con lo siguiente:
    • Las operaciones que dan como resultado -0.0 pueden mostrar +0.0 en su lugar.
    • Las operaciones en INF y NAN no están definidas.

La mayoría de las aplicaciones pueden usar rs_fp_relaxed sin efectos secundarios. Esto puede ser muy beneficioso en algunas arquitecturas debido a optimizaciones adicionales que solo están disponibles con una precisión relajada (como las instrucciones SIMD de CPU).

Cómo acceder a las API de RenderScript desde Java

Cuando desarrollas una aplicación para Android que utiliza RenderScript, puedes acceder a su API desde Java de las siguientes maneras:

A continuación, se detallan las ventajas:

  • Si usas las API de la biblioteca de compatibilidad, la parte de RenderScript de la aplicación será compatible con dispositivos que ejecuten Android 2.3 (API nivel 9) y versiones posteriores, independientemente de las funciones de RenderScript que uses. Esto permite que la aplicación funcione en más dispositivos que si usas las API nativas (android.renderscript).
  • Algunas funciones de RenderScript no están disponibles por medio de las API de la biblioteca de compatibilidad.
  • Si usas las API de la biblioteca de compatibilidad, obtendrás APK (quizás mucho) más grandes que si usas las API nativas (android.renderscript).

Cómo usar las API de la biblioteca de compatibilidad de RenderScript

Para usar las API de RenderScript de la biblioteca de compatibilidad, debes configurar tu entorno de desarrollo para que pueda acceder a ellas. Se requieren las siguientes herramientas del SDK de Android para usar estas API:

  • Revisión 22.2 o posterior de las herramientas del SDK de Android
  • Revisión 18.1.0 o posterior de las herramientas de compilación del SDK de Android

Ten en cuenta que, a partir de las herramientas de compilación del SDK de Android 24.0.0, ya no se admite Android 2.2 (API nivel 8).

Puedes consultar y actualizar la versión instalada de estas herramientas en el SDK Manager de Android.

Para usar las API de RenderScript de la biblioteca de compatibilidad, debes verificar lo siguiente:

  1. Asegúrate de tener instalada la versión del SDK de Android requerida.
  2. Actualiza la configuración del proceso de compilación de Android para incluir la configuración de RenderScript:
    • Abre el archivo build.gradle en la carpeta de la app del módulo de aplicación.
    • Agrega la siguiente configuración de RenderScript al archivo:
          android {
              compileSdkVersion 28
      
              defaultConfig {
                  minSdkVersion 9
                  targetSdkVersion 19
          
                  renderscriptTargetApi 18
                  renderscriptSupportModeEnabled true
          
              }
          }
          

      La configuración mencionada anteriormente controla el comportamiento específico en el proceso de compilación de Android:

      • renderscriptTargetApi: Especifica la versión de bytecode que se generará. Recomendamos establecer este valor en la API de nivel más bajo capaz de proporcionar toda la funcionalidad que usas y establecer renderscriptSupportModeEnabled en true. Los valores válidos para esta configuración son cualquier valor de número entero entre 11 y el nivel de API publicado más recientemente. Si la versión mínima de SDK especificada en el manifiesto de tu aplicación se establece en un valor diferente, se ignora ese valor y el valor objetivo en el archivo de compilación se usa para establecer la versión mínima del SDK.
      • renderscriptSupportModeEnabled: Especifica que el bytecode generado debe volver a una versión compatible si el dispositivo en el que se ejecuta no es compatible con la versión objetivo.
  3. En tus clases de aplicación que usan RenderScript, agrega una importación para las clases de la biblioteca de compatibilidad:

    Kotlin

        import android.support.v8.renderscript.*
        

    Java

        import android.support.v8.renderscript.*;
        

Cómo usar RenderScript desde código Kotlin o Java

El uso de RenderScript desde código Kotlin o Java depende de las clases de API ubicadas en android.renderscript o el paquete android.support.v8.renderscript. La mayoría de las aplicaciones sigue el mismo patrón de uso básico:

  1. Inicializa un contexto de RenderScript. El contexto RenderScript, creado con create(Context), garantiza que se pueda utilizar RenderScript y proporciona un objeto para controlar la vida útil de todos los objetos posteriores de RenderScript. Debes considerar la creación de contexto como una operación que puede ser de larga duración, ya que puede crear recursos en diferentes piezas de hardware. Por eso, si es posible, no debe estar en la ruta crítica de una aplicación. Normalmente, una aplicación tendrá un solo contexto RenderScript por vez.
  2. Crea al menos una Allocation para pasar a una secuencia de comandos. Una Allocation es un objeto de RenderScript que proporciona almacenamiento para una cantidad fija de datos. Los kernels en las secuencias de comandos toman objetos Allocation como su entrada y salida, y se puede acceder a los objetos Allocation en los kernels mediante rsGetElementAt_type() y rsSetElementAt_type() cuando se vinculan como secuencias de comandos globales. Los objetos Allocation permiten que los arreglos pasen del código Java al código RenderScript, y viceversa. En general, los objetos Allocation se crean usando createTyped() o createFromBitmap().
  3. Crea las secuencias de comandos que sean necesarias. Existen dos tipos de secuencias de comandos disponibles cuando se usa RenderScript:
    • ScriptC: Estas son las secuencias de comandos definidas por el usuario como se describe arriba, en Cómo escribir un kernel de RenderScript. Cada secuencia de comandos tiene una clase de Java que refleja el compilador de RenderScript para facilitar el acceso a la secuencia de comandos desde el código Java; esta clase tiene el nombre ScriptC_filename. Por ejemplo, si el kernel de asignación anterior se encuentra en invert.rs y hay un contexto RenderScript en mRenderScript, el código Java o Kotlin para crear una instancia de la secuencia de comandos será:

      Kotlin

          val invert = ScriptC_invert(renderScript)
          

      Java

          ScriptC_invert invert = new ScriptC_invert(renderScript);
          
    • ScriptIntrinsic: Son kernels de RenderScript incorporados para operaciones comunes, como el desenfoque gaussiano, la convolución y la fusión de imágenes. Para obtener más información, consulta las subclases de ScriptIntrinsic.
  4. Propaga asignaciones con datos. A excepción de las asignaciones creadas con createFromBitmap(), una asignación se propaga con datos vacíos cuando se crea por primera vez. Para propagar una asignación, usa uno de los métodos de "copia" en Allocation. Los métodos de "copia" son síncronos.
  5. Establece los globales de secuencias de comandos que sean necesarios. Para establecer globales, puedes usar métodos en la misma clase ScriptC_filename, denominados set_globalname. Por ejemplo, para establecer una variable int denominada threshold, usa el método de Java set_threshold(int). Para establecer una variable rs_allocation denominada lookup, usa el método de Java set_lookup(Allocation). Los métodos set son asíncronos.
  6. Inicia las funciones invocables y los kernels apropiados.

    Los métodos para iniciar un kernel determinado se reflejan en la misma clase ScriptC_filename con los métodos denominados forEach_mappingKernelName() o reduce_reductionKernelName(). Estos lanzamientos son asíncronos. Según los argumentos del kernel, el método toma una o más asignaciones, y todas deben tener las mismas dimensiones. De manera predeterminada, un kernel se ejecuta en cada una de las coordenadas de esas dimensiones; para ejecutar un kernel sobre un subconjunto de esas coordenadas, pasa una Script.LaunchOptions apropiada como último argumento al método forEach o reduce.

    Ejecuta funciones invocables con los métodos invoke_functionName reflejados en la misma clase ScriptC_filename. Estos lanzamientos son asíncronos.

  7. Recupera datos de objetos Allocation y objetos javaFutureType. Para acceder a los datos de una Allocation desde código Java, debes volver a copiar los datos a Java usando uno de los métodos de "copia" en Allocation. Si deseas obtener el resultado de un kernel de reducción, debes usar el método javaFutureType.get(). Los métodos get() y de "copia" son síncronos.
  8. Anula el contexto RenderScript. Puedes destruir el contexto RenderScript con destroy() o permitiendo que se recolecte el objeto de contexto RenderScript. Como consecuencia, cualquier uso posterior de cualquier objeto que pertenezca a ese contexto genera una excepción.

Modelo de ejecución asíncrono

Los métodos reflejados forEach, invoke, reduce y set son asíncronos; cada uno puede regresar a Java antes de completar la acción solicitada. Sin embargo, las acciones individuales se serializan en el orden en que se lanzan.

La clase Allocation proporciona métodos de "copia" para copiar datos desde asignaciones y hacia ellas. Un método de "copia" es síncrono y se serializa con respecto a cualquiera de las acciones asíncronas anteriores que tocan la misma asignación.

Las clases javaFutureType reflejadas proporcionan un método get() para obtener el resultado de una reducción. get() es síncrono y se serializa con respecto a la reducción (que es asíncrona).

RenderScript de fuente única

Android 7.0 (API nivel 24) incluye una función de programación nueva, llamada RenderScript de fuente única, en la que se inician los kernels desde la secuencia de comandos en la que están definidos, en lugar de desde Java. En la actualidad, este enfoque se limita a los kernels de asignación. Para mayor concisión, en esta sección se denominan simplemente "kernels". Esta función nueva también admite la creación de asignaciones de tipo rs_allocation desde la secuencia de comandos. Ahora es posible implementar un algoritmo completo solo dentro de una secuencia de comandos, incluso si se requieren varios lanzamientos de kernel. El beneficio es doble: código más legible, ya que mantiene la implementación de un algoritmo en un idioma, y código potencialmente más rápido debido a menos transiciones entre Java y RenderScript en múltiples lanzamientos de kernel.

Para escribir kernels en un RenderScript de fuente única, consulta Cómo escribir un kernel de RenderScript. Luego, escribes una función invocable que llama a rsForEach() para iniciarlos. Esa API toma una función de kernel como el primer parámetro, seguido de asignaciones de entrada y salida. Una API rsForEachWithOptions() similar toma un argumento adicional de tipo rs_script_call_t, que especifica un subconjunto de los elementos de las asignaciones de entrada y salida para la función del kernel que se va a procesar.

Para iniciar el procesamiento de RenderScript, llama a la función invocable desde Java. Sigue los pasos en Cómo usar RenderScript desde el código Java. En el paso para iniciar los kernels apropiados, llama a la función invocable con invoke_function_name(), que iniciará todo el procesamiento, incluido el lanzamiento de kernels.

A menudo, se necesitan asignaciones para guardar y pasar resultados intermedios de un lanzamiento de kernel a otro. Puedes crearlas con rsCreateAllocation(). Una forma fácil de usar de esa API es rsCreateAllocation_<T><W>(…), donde T es el tipo de datos de un elemento y W es el ancho del vector del elemento. La API toma los tamaños en las dimensiones X, Y y Z como argumentos. Para asignaciones 1D o 2D, se puede omitir el tamaño de la dimensión Y o Z. Por ejemplo, rsCreateAllocation_uchar4(16384) crea una asignación 1D de 16,384 elementos, cada uno de los cuales es del tipo uchar4.

El sistema administra las asignaciones automáticamente. No tienes que liberarlas de manera explícita. Sin embargo, puedes llamar a rsClearObject(rs_allocation* alloc) para indicar que ya no necesitas el controlador alloc de la asignación subyacente, de modo que el sistema pueda liberar recursos lo antes posible.

En la sección Cómo escribir un kernel de RenderScript, se incluye un kernel de ejemplo que invierte una imagen. En el ejemplo incluido a continuación, se amplía ese caso para aplicar más de un efecto a una imagen mediante el RenderScript de fuente única. Se incluye otro kernel, greyscale, que convierte una imagen en color en una en blanco y negro. Luego, una función invocable process() aplica esos dos kernels consecutivamente a una imagen de entrada y produce una imagen de salida. Las asignaciones para la entrada y la salida se pasan como argumentos de tipo 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);
    }
    

Puedes llamar a la función process() desde Java o Kotlin de la siguiente manera:

Kotlin

    val RS: RenderScript = RenderScript.create(context)
    val script = ScriptC_singlesource(RS)
    val inputAllocation: Allocation = Allocation.createFromBitmapResource(
            RS,
            resources,
            R.drawable.image
    )
    val outputAllocation: Allocation = Allocation.createTyped(
            RS,
            inputAllocation.type,
            Allocation.USAGE_SCRIPT or Allocation.USAGE_IO_OUTPUT
    )
    script.invoke_process(inputAllocation, outputAllocation)
    

Java

    // File SingleSource.java

    RenderScript RS = RenderScript.create(context);
    ScriptC_singlesource script = new ScriptC_singlesource(RS);
    Allocation inputAllocation = Allocation.createFromBitmapResource(
        RS, getResources(), R.drawable.image);
    Allocation outputAllocation = Allocation.createTyped(
        RS, inputAllocation.getType(),
        Allocation.USAGE_SCRIPT | Allocation.USAGE_IO_OUTPUT);
    script.invoke_process(inputAllocation, outputAllocation);
    

En este ejemplo, se muestra cómo un algoritmo que incluye dos lanzamientos de kernel puede implementarse por completo en el lenguaje RenderScript. Sin el RenderScript de fuente única, tendrías que iniciar ambos kernels desde el código Java y separar los inicios del kernel de las definiciones del kernel, lo que dificultaría la comprensión de todo el algoritmo. El código RenderScript de fuente única no solo es más fácil de leer, sino que también quita la transición entre Java y la secuencia de comandos en los lanzamientos de kernel. Es posible que algunos algoritmos iterativos inicien kernels cientos de veces, lo que hace que la sobrecarga de dicha transición sea significativa.

Globales de secuencia de comandos

Un global de secuencia de comandos es una variable global no static ordinaria en un archivo de secuencia de comandos (.rs). Para un global de secuencia de comandos llamado var definido en el archivo filename.rs, habrá un método get_var reflejado en la clase ScriptC_filename. A menos que el global sea const, también habrá un método set_var.

Un global de secuencia de comandos determinado tiene dos valores separados: un valor de Java y un valor de secuencia de comandos. Esos valores se comportan de la siguiente manera:

  • Si var tiene un inicializador estático en la secuencia de comandos, especifica el valor inicial de var tanto en Java como en la secuencia de comandos. De lo contrario, ese valor inicial es cero.
  • Los accesos a var en la secuencia de comandos leen y escriben su valor de secuencia de comandos.
  • El método get_var lee el valor de Java.
  • El método set_var (si existe) escribe el valor de Java de inmediato y escribe el valor de la secuencia de comandos asíncronamente.

NOTA: Eso significa que, a excepción de cualquier inicializador estático en la secuencia de comandos, los valores escritos en un global desde una secuencia de comandos no son visibles para Java.

Kernels de reducción en profundidad

La reducción es el proceso de combinar una colección de datos en un solo valor. Esta es una primitiva útil en programación paralela, con aplicaciones como las siguientes:

  • calcular la suma o el producto de todos los datos
  • procesar operaciones lógicas (and, or, xor) de todos los datos
  • encontrar el valor mínimo o máximo dentro de los datos
  • buscar un valor específico o la coordenada de un valor específico dentro de los datos

En Android 7.0 (API nivel 24) y versiones posteriores, RenderScript admite kernels de reducción para permitir algoritmos de reducción eficientes escritos por el usuario. Puedes iniciar kernels de reducción en entradas con 1, 2 o 3 dimensiones.

Un ejemplo anterior muestra un kernel de reducción addint simple. A continuación, se incluye un kernel de reducción findMinAndMax más complicado que busca las ubicaciones de los valores long mínimos y máximos en una Allocation unidimensional:

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

NOTA: Hay más ejemplos de kernels de reducción aquí.

Para ejecutar un kernel de reducción, el entorno de ejecución de RenderScript crea una o más variables llamadas elementos de datos del acumulador para mantener el estado del proceso de reducción. El entorno de ejecución de RenderScript selecciona el número de elementos de datos del acumulador de manera que se maximice el rendimiento. El tipo de elementos de datos del acumulador (accumType) se determina mediante la función de acumulador del kernel. El primer argumento de esa función es un puntero a un elemento de datos del acumulador. De manera predeterminada, cada elemento de datos del acumulador se inicializa en cero (como si lo hiciera memset). Sin embargo, puedes escribir una función de inicializador para hacer algo diferente.

Ejemplo: En el kernel addint, los elementos de datos del acumulador (de tipo int) se usan para sumar valores de entrada. Como no hay una función de inicializador, todos los elementos de datos del acumulador se inicializan en cero.

Ejemplo: En el kernel findMinAndMax, los elementos de datos del acumulador (de tipo MinAndMax) se utilizan para realizar un seguimiento de los valores mínimos y máximos encontrados hasta el momento. Hay una función de inicializador para establecerlos en LONG_MAX y LONG_MIN, respectivamente, y para establecer las ubicaciones de estos valores en -1, lo que indica que en realidad los valores no están presentes en la parte (vacía) de la entrada que se procesó.

RenderScript llama a tu función de acumulador una vez por cada coordenada en la(s) entrada(s). En general, tu función debe actualizar el elemento de datos del acumulador de alguna manera de acuerdo con la entrada.

Ejemplo: En el kernel addint, la función de acumulador agrega el valor de un elemento de entrada al elemento de datos del acumulador.

Ejemplo: En el kernel findMinAndMax, la función del acumulador verifica si el valor de un elemento de entrada es menor o igual que el valor mínimo registrado en el elemento de datos del acumulador y/o mayor o igual que el valor máximo registrado en el elemento de datos del acumulador, y actualiza el elemento de datos del acumulador según corresponda.

Después de llamar a la función de acumulador una vez para cada coordenada en la(s) entrada(s), RenderScript debe combinar los elementos de datos del acumulador en un solo elemento de datos del acumulador. Para hacerlo, puedes escribir una función de combinador. Si la función de acumulador tiene una sola entrada y no tiene argumentos especiales, no necesitas escribir una función de combinador; RenderScript utilizará la función de acumulador para combinar los elementos de datos del acumulador. (Igualmente, puedes escribir una función de combinador si no deseas usar ese comportamiento predeterminado).

Ejemplo: En el kernel addint, no existe una función de combinador, por lo que se utilizará la función de acumulador. Este es el comportamiento correcto, ya que, si dividimos un grupo de valores en dos y sumamos los valores en esas dos partes por separado, hacer esas dos sumas equivale a sumar todo el grupo.

Ejemplo: En el kernel findMinAndMax, la función de combinador verifica si el valor mínimo registrado en el elemento de datos del acumulador "fuente" *val es menor que el valor mínimo registrado en el elemento de datos del acumulador de "destino" *accum, y actualiza *accum en consecuencia. Hace algo similar para el valor máximo. De esa manera, se actualiza *accum al estado que hubiera tenido si todos los valores de entrada se hubieran acumulado en *accum en lugar de algunos en *accum y algunos en *val.

Después de combinar todos los elementos de datos del acumulador, RenderScript determina el resultado de la reducción que mostrará a Java. Para ello, puedes escribir una función de convertidor externo. No es necesario que escribas una función de convertidor externo si quieres que el valor final de los elementos combinados de datos del acumulador sea el resultado de la reducción.

Ejemplo: En el kernel addint, no hay ninguna función de convertidor externo. El valor final de los elementos de datos combinados es la suma de todos los elementos de la entrada, que es el valor que queremos mostrar.

Ejemplo: En el kernel findMinAndMax, la función del convertidor externo inicializa un valor de resultado int2 para mantener las ubicaciones de los valores mínimo y máximo que resultan de la combinación de todos elementos de datos del acumulador.

Cómo escribir un kernel de reducción

#pragma rs reduce define un kernel de reducción especificando su nombre y los nombres y roles de las funciones que conforman el kernel. Todas esas funciones deben ser static. Un kernel de reducción siempre requiere una función accumulator; puedes omitir algunas de las demás funciones o todas ellas en función de lo que desees que haga el kernel.

#pragma rs reduce(kernelName) \
      initializer(initializerName) \
      accumulator(accumulatorName) \
      combiner(combinerName) \
      outconverter(outconverterName)
    

El significado de los elementos en #pragma es el siguiente:

  • reduce(kernelName) (obligatorio): Especifica que se está definiendo un kernel de reducción. Un método de Java reflejado reduce_kernelName iniciará el kernel.
  • initializer(initializerName) (opcional): Especifica el nombre de la función de inicialización para este kernel de reducción. Cuando inicias el kernel, RenderScript llama a esta función una vez por cada elemento de datos del acumulador. La función debe definirse de la siguiente manera:

    static void initializerName(accumType *accum) { … }

    accum es un puntero a un elemento de datos del acumulador que debe inicializar esta función.

    Si no proporcionas una función de inicializador, RenderScript inicializa cada elemento de datos del acumulador en cero (como si lo hiciera memset) y se comporta como si hubiera una función de inicializador como la siguiente:

    static void initializerName(accumType *accum) {
          memset(accum, 0, sizeof(*accum));
        }
  • accumulator(accumulatorName) (obligatorio): Especifica el nombre de la función de acumulador para este kernel de reducción. Cuando inicias el kernel, RenderScript llama a esta función una vez por cada coordenada en la(s) entrada(s) para realizar alguna actualización en un elemento de datos del acumulador de acuerdo con la(s) entrada(s). La función debe definirse de la siguiente manera:

        static void accumulatorName(accumType *accum,
                                    in1Type in1, …, inNType inN
                                    [, specialArguments]) { … }
        

    accum es un puntero a un elemento de datos del acumulador que esta función debe modificar. De in1 a inN son uno o más argumentos que se completan automáticamente en función de las entradas pasadas al lanzamiento del kernel, un argumento por entrada. De manera alternativa, la función de acumulador puede tomar cualquiera de los argumentos especiales.

    Un kernel de ejemplo con múltiples entradas es dotProduct.

  • combiner(combinerName)

    (opcional): Especifica el nombre de la función de combinador de este kernel de reducción. Después de que RenderScript llama a la función de acumulador una vez por cada coordenada en la(s) entrada(s), llama a esta función tantas veces como sea necesario para combinar todos los elementos de datos del acumulador en un solo elemento de datos del acumulador. La función debe definirse de la siguiente manera:

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

    accum es un puntero a un elemento de datos del acumulador de "destino" que esta función debe modificar. other es un puntero a un elemento de datos del acumulador "fuente" que esta función debe "combinar" en *accum.

    NOTA: Es posible que se hayan inicializado *accum, *other o ambos, pero nunca se hayan pasado a la función de acumulador; es decir, uno o ambos nunca se actualizaron de acuerdo con los datos de entrada. Por ejemplo, en el kernel findMinAndMax, la función de combinador fMMCombiner verifica de manera explícita idx < 0 porque eso indica ese elemento de datos del acumulador, cuyo valor es INITVAL.

    Si no proporcionas una función de combinador, RenderScript utiliza la función de acumulador en su lugar y se comporta como si hubiera una función de combinador similar a esta:

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

    Una función de combinador es obligatoria si el kernel tiene más de una entrada, si el tipo de datos de entrada no coincide con el tipo de datos del acumulador o si la función del acumulador toma uno o más argumentos especiales.

  • outconverter(outconverterName) (opcional): Especifica el nombre de la función de convertidor externo para este kernel de reducción. Después de que RenderScript combina todos los elementos de datos del acumulador, llama a esta función para determinar el resultado de la reducción para mostrar a Java. La función debe definirse de la siguiente manera:

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

    result es un puntero a un elemento de datos de resultado (asignado pero no inicializado por el entorno de ejecución de RenderScript) para que esta función se inicialice con el resultado de la reducción. resultType es el tipo de ese elemento de datos, que no tiene que ser igual a accumType. accum es un puntero al elemento final de datos del acumulador que procesa la función de combinador.

    Si no proporcionas una función de convertidor externo, RenderScript copia el elemento final de datos del acumulador en el elemento de datos de resultado y se comporta como si hubiera una función de convertidor externo similar a lo siguiente:

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

    Si deseas un tipo de resultado diferente al del tipo de datos del acumulador, la función de convertidor externo es obligatoria.

Ten en cuenta que un kernel tiene tipos de entrada, un tipo de elemento de datos del acumulador y un tipo de resultado, que no deben ser iguales. Por ejemplo, en el kernel findMinAndMax, el tipo de entrada long, el tipo de elemento de datos del acumulador MinAndMax y el tipo de resultado int2 son todos diferentes.

¿Qué es lo que no puedes suponer?

No debes confiar en la cantidad de elementos de datos del acumulador que crea RenderScript para un lanzamiento de kernel determinado. No hay ninguna garantía de que dos lanzamientos del mismo kernel con las mismas entradas creen la misma cantidad de elementos de datos del acumulador.

No debes confiar en el orden en que RenderScript llama a las funciones de inicializador, acumulador y combinador; hasta es posible que llame a algunas en paralelo. No hay ninguna garantía de que dos lanzamientos del mismo kernel con la misma entrada sigan el mismo orden. La única garantía es que solo la función de inicializador verá un elemento de datos del acumulador no inicializado. Por ejemplo:

  • No hay ninguna garantía de que todos los elementos de datos del acumulador se inicialicen antes de que se invoque la función de acumulador, aunque solo se llamará en un elemento de datos del acumulador inicializado.
  • No hay ninguna garantía sobre el orden en que los elementos de entrada se pasan a la función de acumulador.
  • No hay ninguna garantía de que se haya llamado a la función de acumulador para todos los elementos de entrada antes de que se llame a la función de combinador.

Una consecuencia de esto es que el kernel findMinAndMax no es determinista: si la entrada contiene más de una repetición del mismo valor mínimo o máximo, no hay forma de saber qué repetición encontrará el kernel.

¿Qué es lo que debes garantizar?

Debido a que el sistema RenderScript puede elegir ejecutar un kernel de muchas maneras diferentes, debes seguir determinadas reglas para asegurarte de que tu kernel se comporte de la manera deseada. Si no sigues esas reglas, es posible que obtengas resultados incorrectos, comportamientos no deterministas o errores de entorno de ejecución.

En las reglas detalladas a continuación, a menudo se indica que dos elementos de datos del acumulador deben tener "valores iguales". ¿Qué significa esto? Eso depende de lo que quieras que haga el kernel. Para una reducción matemática como addint, generalmente, tiene sentido que "igual" signifique igualdad matemática. Para una búsqueda del tipo "elegir cualquiera", como findMinAndMax ("buscar la ubicación de valores de entrada mínimos y máximos") donde puede haber más de una ocurrencia de valores de entrada idénticos, todas las ubicaciones de un valor de entrada dado deben considerarse "iguales". Podrías escribir un kernel similar para "encontrar la ubicación de los primeros valores mínimo y máximo de entrada a la izquierda" donde (por ejemplo) se prefiera un valor mínimo en la ubicación 100 a un valor mínimo idéntico en la ubicación 200; para este kernel, "igual" significaría ubicación idéntica, no solo un valor idéntico, y las funciones de acumulador y de combinador tendrían que ser diferentes de las de findMinAndMax.

La función de inicializador debe crear un valor de identidad. Es decir, si I y A son elementos de datos del acumulador que inicializa la función de inicializador, y I nunca se pasó a la función de acumulador (pero es posible que A sí se haya pasado), entonces
  • combinerName(&A, &I) debe dejar A igual
  • combinerName(&I, &A) debe dejar I igual que A

Ejemplo: En el kernel addint, un elemento de datos del acumulador se inicializa en cero. La función de combinador para este kernel realiza la suma; cero es el valor de identidad para la suma.

Ejemplo: En el kernel findMinAndMax, un elemento de datos del acumulador se inicializa en INITVAL.

  • fMMCombiner(&A, &I) deja A igual porque I es INITVAL.
  • fMMCombiner(&I, &A) establece I en A porque I es INITVAL.

Por lo tanto, INITVAL es un valor de identidad.

La función de combinador debe ser conmutativa. Es decir, si A y B son elementos de datos del acumulador inicializados por la función de inicializador y que se pasaron a la función de acumulador cero o más veces, entonces combinerName(&A, &B) debe configurar A en el mismo valor en que combinerName(&B, &A) establece B.

Ejemplo: En el kernel addint, la función de combinador suma los dos valores del elemento de datos del acumulador; la suma es conmutativa.

Ejemplo: En el kernel findMinAndMax, fMMCombiner(&A, &B) es igual que A = minmax(A, B), y minmax es conmutativo, por lo que fMMCombiner también lo es.

La función de combinador debe ser asociativa. Es decir, si A, B y C son elementos de datos del acumulador inicializados por la función de inicializador y que pueden haberse pasado a la función de acumulador cero o más veces, entonces las siguientes dos secuencias de código deben establecer A en el mismo valor:

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

Ejemplo: En el kernel addint, la función de combinador suma los dos valores del elemento de datos del acumulador:

  •     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
        

La suma es asociativa, por lo que la función de combinador también lo es.

Ejemplo: En el kernel findMinAndMax,

    fMMCombiner(&A, &B)
    
es lo mismo que
    A = minmax(A, B)
    
Por lo tanto, las dos secuencias son
  •     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 es asociativo, por lo que fMMCombiner también lo es.

La función de acumulador y la función de combinador deben obedecer a la regla básica de plegado. Es decir, si A y B son elementos de datos del acumulador, A se inicializó con la función de inicializador y puede haberse pasado a la función de acumulador cero o más veces, B no se inicializó y args es la lista de argumentos de entrada y argumentos especiales para una llamada particular a la función de acumulador, entonces las siguientes dos secuencias de código deben establecer A en el mismo valor:

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

Ejemplo: En el kernel addint, para un valor de entrada V:

  • El enunciado 1 es igual a A += V
  • El enunciado 2 es igual a B = 0
  • El enunciado 3 es igual a B += V, que es igual a B = V
  • El enunciado 4 es igual a A += B, que es igual a A += V

Los enunciados 1 y 4 establecen A en el mismo valor, por lo que este kernel obedece la regla básica de plegado.

Ejemplo: En el kernel findMinAndMax, para un valor de entrada V en la coordenada X:

  • El enunciado 1 es igual a A = minmax(A, IndexedVal(V, X))
  • El enunciado 2 es igual a B = INITVAL
  • El enunciado 3 es igual a
        B = minmax(B, IndexedVal(V, X))
        
    que, debido a que B es el valor inicial, es igual a
        B = IndexedVal(V, X)
        
  • El enunciado 4 es igual a
        A = minmax(A, B)
        
    que es igual a
        A = minmax(A, IndexedVal(V, X))
        

Los enunciados 1 y 4 establecen A en el mismo valor, por lo que este kernel obedece la regla básica de plegado.

Cómo llamar a un kernel de reducción desde el código Java

Para un kernel de reducción llamado kernelName definido en el archivo filename.rs, hay tres métodos reflejados en la clase ScriptC_filename:

Kotlin

    // Function 1
    fun reduce_kernelName(ain1: Allocation, …,
                                   ainN: Allocation): javaFutureType

    // Function 2
    fun reduce_kernelName(ain1: Allocation, …,
                                   ainN: Allocation,
                                   sc: Script.LaunchOptions): javaFutureType

    // Function 3
    fun reduce_kernelName(in1: Array<devecSiIn1Type>, …,
                                   inN: Array<devecSiInNType>): javaFutureType
    

Java

    // Method 1
    public javaFutureType reduce_kernelName(Allocation ain1, …,
                                            Allocation ainN);

    // Method 2
    public javaFutureType reduce_kernelName(Allocation ain1, …,
                                            Allocation ainN,
                                            Script.LaunchOptions sc);

    // Method 3
    public javaFutureType reduce_kernelName(devecSiIn1Type[] in1, …,
                                            devecSiInNType[] inN);
    

A continuación, se incluyen algunos ejemplos de cómo llamar al kernel addint:

Kotlin

    val script = ScriptC_example(renderScript)

    // 1D array
    //   and obtain answer immediately
    val input1 = intArrayOf()
    val sum1: Int = script.reduce_addint(input1).get()  // Method 3

    // 2D allocation
    //   and do some additional work before obtaining answer
    val typeBuilder = Type.Builder(RS, Element.I32(RS)).apply {
        setX()
        setY()
    }
    val input2: Allocation = Allocation.createTyped(RS, typeBuilder.create()).also {
        populateSomehow(it) // fill in input Allocation with data
    }
    val result2: ScriptC_example.result_int = script.reduce_addint(input2)  // Method 1
    doSomeAdditionalWork() // might run at same time as reduction
    val sum2: Int = result2.get()
    

Java

    ScriptC_example script = new ScriptC_example(renderScript);

    // 1D array
    //   and obtain answer immediately
    int input1[] = ;
    int sum1 = script.reduce_addint(input1).get();  // Method 3

    // 2D allocation
    //   and do some additional work before obtaining answer
    Type.Builder typeBuilder =
      new Type.Builder(RS, Element.I32(RS));
    typeBuilder.setX();
    typeBuilder.setY();
    Allocation input2 = createTyped(RS, typeBuilder.create());
    populateSomehow(input2);  // fill in input Allocation with data
    ScriptC_example.result_int result2 = script.reduce_addint(input2);  // Method 1
    doSomeAdditionalWork(); // might run at same time as reduction
    int sum2 = result2.get();
    

El método 1 tiene un argumento de entrada Allocation por cada argumento de entrada en la función de acumulador del kernel. El entorno de ejecución de RenderScript comprueba que todas las asignaciones de entrada tengan las mismas dimensiones y que el tipo Element de cada una de las asignaciones de entrada coincida con el argumento de entrada correspondiente del prototipo de la función de acumulador. Si falla alguna de estas comprobaciones, RenderScript genera una excepción. El kernel se ejecuta sobre cada coordenada en esas dimensiones.

El método 2 es igual que el método 1, excepto en que el método 2 toma un argumento adicional sc que puede usarse para limitar la ejecución del kernel a un subconjunto de coordenadas.

El método 3 es igual que el método 1, excepto en que, en lugar de tomar entradas de asignación, toma entradas de arreglo de Java. Esta ventaja te ahorra tener que escribir código para crear una asignación y copiar datos desde un arreglo de Java de manera explícita. Sin embargo, usar el método 3 en lugar del método 1 no aumenta el rendimiento del código. Para cada arreglo de entrada, el método 3 crea una asignación temporal unidimensional con el tipo de Element adecuado y setAutoPadding(boolean) habilitados, y copia el arreglo en la asignación como si fuera el método copyFrom() adecuado de Allocation. Luego, llama al método 1 y pasa esas asignaciones temporales.

NOTA: Si tu aplicación va a realizar múltiples llamadas al kernel con el mismo arreglo o con arreglos diferentes que tengan las mismas dimensiones y el mismo tipo de elemento, puedes mejorar el rendimiento. Para ello, crea, propaga y reutiliza de manera explícita las asignaciones tú mismo en lugar de usar del método 3.

javaFutureType, el tipo de resultado de los métodos de reducción reflejada, es una clase anidada, estática y reflejada que está dentro de la clase ScriptC_filename. Representa el resultado futuro de una ejecución de kernel de reducción. Para obtener el resultado real de la ejecución, llama al método get() de esa clase, que muestra un valor de tipo javaResultType. get() es síncrono.

Kotlin

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

Java

    public class ScriptC_filename extends ScriptC {
      public static class javaFutureType {
        public javaResultType get() { … }
      }
    }
    

javaResultType se determina a partir del resultType de la función de convertidor externo. A menos que resultType sea un tipo sin firma (escalar, vector o arreglo), javaResultType es el tipo de Java directamente correspondiente. Si resultType es un tipo sin firma y hay un tipo de Java más grande con firma, javaResultType es ese tipo de Java más grande con firma; de lo contrario, es el tipo de Java directamente correspondiente. Por ejemplo:

  • Si resultType es int, int2 o int[15], javaResultType es int, Int2 o int[]. Todos los valores de resultType se pueden representar con javaResultType.
  • Si resultType es uint, uint2 o uint[15], javaResultType es long, Long2 o long[]. Todos los valores de resultType se pueden representar con javaResultType.
  • Si resultType es ulong, ulong2 o ulong[15], javaResultType es long, Long2 o long[]. Hay determinados valores de resultType que no se pueden representar con javaResultType.

javaFutureType es el tipo de resultado futuro correspondiente al resultType de la función de convertidor.

  • Si resultType no es un tipo de arreglo, javaFutureType es result_resultType.
  • Si resultType es un arreglo de longitud Count con miembros del tipo memberType, javaFutureType es resultArrayCount_memberType.

Por ejemplo:

Kotlin

    class ScriptC_filename(rs: RenderScript) : ScriptC(…) {

        // for kernels with int result
        object result_int {
            fun get(): Int = …
        }

        // for kernels with int[10] result
        object resultArray10_int {
            fun get(): IntArray = …
        }

        // for kernels with int2 result
        //   note that the Kotlin type name "Int2" is not the same as the script type name "int2"
        object result_int2 {
            fun get(): Int2 = …
        }

        // for kernels with int2[10] result
        //   note that the Kotlin type name "Int2" is not the same as the script type name "int2"
        object resultArray10_int2 {
            fun get(): Array<Int2> = …
        }

        // for kernels with uint result
        //   note that the Kotlin type "long" is a wider signed type than the unsigned script type "uint"
        object result_uint {
            fun get(): Long = …
        }

        // for kernels with uint[10] result
        //   note that the Kotlin type "long" is a wider signed type than the unsigned script type "uint"
        object resultArray10_uint {
            fun get(): LongArray = …
        }

        // for kernels with uint2 result
        //   note that the Kotlin type "Long2" is a wider signed type than the unsigned script type "uint2"
        object result_uint2 {
            fun get(): Long2 = …
        }

        // for kernels with uint2[10] result
        //   note that the Kotlin type "Long2" is a wider signed type than the unsigned script type "uint2"
        object resultArray10_uint2 {
            fun get(): Array<Long2> = …
        }
    }
    

Java

    public class ScriptC_filename extends ScriptC {
      // for kernels with int result
      public static class result_int {
        public int get() { … }
      }

      // for kernels with int[10] result
      public static class resultArray10_int {
        public int[] get() { … }
      }

      // for kernels with int2 result
      //   note that the Java type name "Int2" is not the same as the script type name "int2"
      public static class result_int2 {
        public Int2 get() { … }
      }

      // for kernels with int2[10] result
      //   note that the Java type name "Int2" is not the same as the script type name "int2"
      public static class resultArray10_int2 {
        public Int2[] get() { … }
      }

      // for kernels with uint result
      //   note that the Java type "long" is a wider signed type than the unsigned script type "uint"
      public static class result_uint {
        public long get() { … }
      }

      // for kernels with uint[10] result
      //   note that the Java type "long" is a wider signed type than the unsigned script type "uint"
      public static class resultArray10_uint {
        public long[] get() { … }
      }

      // for kernels with uint2 result
      //   note that the Java type "Long2" is a wider signed type than the unsigned script type "uint2"
      public static class result_uint2 {
        public Long2 get() { … }
      }

      // for kernels with uint2[10] result
      //   note that the Java type "Long2" is a wider signed type than the unsigned script type "uint2"
      public static class resultArray10_uint2 {
        public Long2[] get() { … }
      }
    }
    

Si javaResultType es un tipo de objeto (incluido un tipo de arreglo), cada llamada a javaFutureType.get() en la misma instancia mostrará el mismo objeto.

Si javaResultType no puede representar todos los valores de tipo resultType, y un kernel de reducción produce un valor irrepresentable, javaFutureType.get() arroja una excepción.

El método 3 y devecSiInXType

devecSiInXType es el tipo de Java correspondiente al inXType del argumento correspondiente de la función de acumulador. A menos que inXType sea un tipo sin firma o un tipo de vector, devecSiInXType es el tipo de Java directamente correspondiente. Si inXType es un tipo escalar sin firma, devecSiInXType es el tipo de Java directamente correspondiente al tipo escalar con firma del mismo tamaño. Si inXType es un tipo de vector con firma, devecSiInXType es el tipo de Java directamente correspondiente al tipo de componente de vector. Si inXType es un tipo de vector sin firma, devecSiInXType es el tipo de Java directamente correspondiente al tipo escalar con firma del mismo tamaño que el tipo de componente de vector. Por ejemplo:

  • Si inXType es int, devecSiInXType es int.
  • Si inXType es int2, devecSiInXType es int. El arreglo es una representación plana: tiene el doble de elementos escalares que la asignación y tiene elementos de vectores de 2 componentes. Los métodos copyFrom() de Allocation funcionan de la misma manera.
  • Si inXType es uint, deviceSiInXType es int. Un valor con firma en el arreglo de Java se interpreta como un valor sin firma del mismo patrón de bits en la asignación. Los métodos copyFrom() de Allocation funcionan de la misma manera.
  • Si inXType es uint2, deviceSiInXType es int. Esta es una combinación de la forma en que se manejan int2 y uint: el arreglo es una representación plana y los valores con firma del arreglo de Java se interpretan como valores de elemento sin firma de RenderScript.

Ten en cuenta que, para el método 3, los tipos de entrada se manejan de manera diferente que los tipos de resultados:

  • La entrada del vector de una secuencia de comandos se aplana en el lado de Java, mientras que el resultado del vector de una secuencia de comandos no.
  • La entrada sin firma de una secuencia de comandos se representa como una entrada con firma del mismo tamaño en el lado de Java, mientras que el resultado sin firma de una secuencia de comandos se representa como un tipo ampliado con firma en el lado de Java (excepto en el caso de ulong).

Más ejemplos de kernels de reducción

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

Códigos de ejemplo adicionales

Las muestras de BasicRenderScript, RenderScriptIntrinsic y Hello Compute también muestran el uso de las API incluidas en esta página.