Visão geral do RenderScript

O RenderScript é um framework para executar tarefas intensivas em termos de computação com alto desempenho no Android. O RenderScript é orientado principalmente para uso com processamento paralelo de dados, embora cargas de trabalho seriais também possam se beneficiar. O tempo de execução do RenderScript trabalha carregando em paralelo todos os processadores disponíveis em um dispositivo, como CPUs e GPUs com vários núcleos. Isso permite que você se concentre em expressar algoritmos em vez de programar trabalho. O RenderScript é especialmente útil para aplicativos que usam processamento de imagens, fotografia computacional ou visão computacional.

Para começar a usar o RenderScript, você precisa entender dois conceitos principais:

  • A linguagem em si, é derivada de C99 para criar código de computação de alto desempenho. A seção Como criar um kernel do RenderScript descreve como usá-lo para criar kernels de computação.
  • A API de controle é usada para gerenciar o ciclo de vida de recursos do RenderScript e controlar a execução do kernel. Ela está disponível em três linguagens diferentes: Java, C++ no Android NDK e a própria linguagem de kernel derivada de C99. As seções Como usar o RenderScript no código Java e RenderScript de fonte única descrevem a primeira e a terceira opções, respectivamente.

Como criar um kernel do RenderScript

Um kernel do RenderScript normalmente reside em um arquivo .rs no diretório <project_root>/src/rs; cada arquivo .rs é chamado de script. Cada script contém o próprio conjunto de kernels, funções e variáveis. Um script pode conter:

  • Uma declaração pragma (#pragma version(1)) que declara a versão da linguagem de kernel do RenderScript usada no script. Atualmente, 1 é o único valor válido.
  • Uma declaração pragma (#pragma rs java_package_name(com.example.app)) que declara o nome do pacote das classes Java refletidas desse script. Observe que o arquivo .rs precisa fazer parte do pacote do seu aplicativo e não de um projeto de biblioteca.
  • Zero ou mais funções invocáveis. Uma função invocável é uma função do RenderScript de linha de execução única que você pode chamar do seu código Java com argumentos arbitrários. Elas geralmente são úteis para a configuração inicial ou para computação em série em um pipeline de processamento maior.
  • Zero ou mais scripts globais. Um script global é semelhante a uma variável global em C. É possível acessar scripts globais a partir do código Java, e eles são frequentemente usados para transmissão de parâmetro para kernels do RenderScript. Os scripts globais são explicados em mais detalhes aqui.

  • Zero ou mais kernels de computação. Um kernel de computação é uma função ou um conjunto de funções que podem direcionar o tempo de execução do RenderScript para execução paralela em uma coleção de dados. Existem dois tipos de kernel de computação: kernels de mapeamento (também chamados de foreach) e kernels de redução.

    Um kernel de mapeamento é uma função paralela que opera em um conjunto de Allocations das mesmas dimensões. Por padrão, ele é executado uma vez para cada coordenada nessas dimensões. Ele é normalmente, mas não exclusivamente, usado para transformar um conjunto Allocations de entrada em um Allocation de saída, um Element por vez.

    • Veja um exemplo de um kernel de mapeamento simples:

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

      Na maioria dos aspectos, isso é idêntico a uma função C padrão. A propriedade RS_KERNEL aplicada ao protótipo da função especifica que a função é um kernel de mapeamento do RenderScript em vez de uma função invocável. O argumento in é automaticamente preenchido com base no Allocation de entrada passada para a inicialização do kernel. Os argumentos x e y são discutidos abaixo. O valor retornado do kernel é criado automaticamente no local apropriado na saída Allocation. Por padrão, esse kernel é executado em toda a Allocation de entrada, com uma execução da função do kernel por Element na Allocation.

      Um kernel de mapeamento pode ter uma ou mais Allocations de entrada, uma única Allocation de saída ou ambos. O tempo de execução do RenderScript é verificado para garantir que todas as alocações de entrada e saída tenham as mesmas dimensões e que os tipos de Element das alocações de entrada e saída correspondam ao protótipo do kernel. Se uma dessas verificações falhar, o RenderScript lançará uma exceção.

      OBSERVAÇÃO: antes do Android 6.0 (API de nível 23), um kernel de mapeamento não podia ter mais de uma Allocation de entrada.

      Se você precisar de mais Allocations de entrada ou saída que o kernel tem, esses objetos precisarão ser vinculados a scripts globais rs_allocation e acessados a partir de um kernel ou de uma função invocável por meio de rsGetElementAt_type() ou rsSetElementAt_type().

      OBSERVAÇÃO: RS_KERNEL é uma macro definida automaticamente pelo RenderScript para sua conveniência:

      #define RS_KERNEL __attribute__((kernel))
      

    Um kernel de redução é uma família de funções que opera em um conjunto de Allocations de entrada das mesmas dimensões. Por padrão, a função de acumulador é executada uma vez para cada coordenada nessas dimensões. Ela normalmente, mas não exclusivamente, é usada para "reduzir" um conjunto de Allocations de entrada para um único valor.

    • Veja um exemplo de um kernel de redução simples que adiciona os Elements de entrada:

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

      Um kernel de redução consiste em uma ou mais funções criadas pelo usuário. #pragma rs reduce é usado para definir o kernel especificando seu nome (addint, neste exemplo) e os nomes e papéis das funções que compõem o kernel (uma função de accumulator addintAccum, neste exemplo). Todas essas funções precisam ser static. Um kernel de redução sempre requer uma função de accumulator; ele também pode ter outras funções, dependendo do que você quer que o kernel faça.

      Uma função de acumulador do kernel de redução precisa retornar void e ter pelo menos dois argumentos. O primeiro argumento (accum, neste exemplo) é um indicador para um item de dados do acumulador, e o segundo (val, neste exemplo) é preenchido automaticamente com base no Allocation de entrada passado para a inicialização do kernel. O item de dados do acumulador é criado pelo tempo de execução do RenderScript; por padrão, ele é inicializado como zero. Por padrão, esse kernel é executado em toda a Allocation de entrada, com uma execução da função do acumulador por Element na Allocation. Por padrão, o valor final do item de dados do acumulador é tratado como o resultado da redução, e é retornado para Java. O tempo de execução do RenderScript é verificado para garantir que o tipo Element da alocação de entrada corresponda ao protótipo da função do acumulador. Se não corresponder, o RenderScript lançará uma exceção.

      Um kernel de redução tem uma ou mais Allocations de entrada, mas nenhuma Allocations de saída.

      Os kernels de redução são explicados em mais detalhes aqui.

      Os kernels de redução são compatíveis com o Android 7.0 (API de nível 24) e versões mais recentes.

    Uma função do kernel de mapeamento ou uma função de acumulador do kernel de redução pode acessar as coordenadas da execução atual usando os argumentos especiais x, y e z, que precisam ser do tipo int ou uint32_t. Esses argumentos são opcionais.

    Uma função do kernel de mapeamento ou uma função de acumulador do kernel de redução também pode usar o argumento especial context do tipo rs_kernel_context. Ela é necessária para uma família de APIs de tempo de execução que são usadas para consultar determinadas propriedades da execução atual, por exemplo, rsGetDimX. O argumento context está disponível no Android 6.0 (API de nível 23) ou versões mais recentes.

  • Uma função init() opcional. A função init() é um tipo especial de função invocável que o RenderScript executa quando o script é instanciado pela primeira vez. Isso permite que alguns processamentos ocorram automaticamente na criação do script.
  • Zero ou mais scripts estáticos globais e funções. Um script estático global equivale a um script global, mas não pode ser acessado a partir do código Java. Uma função estática é uma função C padrão que pode ser chamada a partir de qualquer kernel ou função invocável no script, mas não é exposta à API Java. Se um script global ou uma função não precisar ser acessado a partir do código Java, é altamente recomendável que ele seja declarado static.

Como definir a precisão de ponto flutuante

Você pode controlar o nível exigido de precisão de ponto flutuante em um script. Isso será útil se o padrão IEEE 754-2008 completo (usado por padrão) não for obrigatório. Os pragmas a seguir podem definir um nível diferente de precisão de ponto flutuante:

  • #pragma rs_fp_full (padrão, se nada for especificado): para apps que exigem precisão de ponto flutuante, conforme descrito pelo padrão IEEE 754-2008.
  • #pragma rs_fp_relaxed: para apps que não exigem conformidade rigorosa com IEEE 754-2008 e toleram menos precisão. Esse modo permite a liberação de zero para desnormalização e arredondamento em direção a zero.
  • #pragma rs_fp_imprecise: para apps que não têm requisitos de precisão rigorosos. Esse modo ativa tudo em rs_fp_relaxed, além do seguinte:
    • As operações que resultarem em -0,0 poderão retornar +0,0.
    • Operações em INF e NAN são indefinidas.

A maioria dos apps pode usar rs_fp_relaxed sem efeitos colaterais. Isso pode ser muito benéfico em algumas arquiteturas devido a otimizações adicionais disponíveis apenas com precisão relaxada (como instruções de CPU SIMD).

Como acessar APIs do RenderScript em Java

Ao desenvolver um aplicativo para Android que usa o RenderScript, é possível acessar a API em Java de duas maneiras:

  • android.renderscript: as APIs desse pacote de classes estão disponíveis em dispositivos com o Android 3.0 (API de nível 11) e versões mais recentes.
  • android.support.v8.renderscript: as APIs desse pacote estão disponíveis em uma Biblioteca de Suporte, permitindo que elas sejam usadas em dispositivos com Android 2.3 (API de nível 9) ou versões mais recentes.

Veja as compensações:

  • Se você usar as APIs da Biblioteca de Suporte, a parte RenderScript do seu app será compatível com dispositivos que executam o Android 2.3 (API de nível 9) ou versões mais recentes, independentemente dos recursos do RenderScript usados. Isso permite que seu app funcione em mais dispositivos do que se você usar as APIs nativas (android.renderscript).
  • Alguns recursos do RenderScript não estão disponíveis nas APIs da Biblioteca de Suporte.
  • Se você usar as APIs da Biblioteca de Suporte, terá APKs maiores (talvez significativamente) do que se usar as APIs nativas (android.renderscript).

Como usar as APIs da Biblioteca de Suporte do RenderScript

Para usar as APIs da Biblioteca de Suporte do RenderScript, é preciso configurar seu ambiente de desenvolvimento para poder acessá-las. As seguintes ferramentas do SDK Android são necessárias para usar essas APIs:

  • Ferramentas do SDK Android revisão 22.2 ou versões mais recentes
  • Ferramentas de compilação do Android SDK revisão 18.1.0 ou versões mais recentes

Observe que, a partir das Ferramentas de compilação do SDK Android versão 24.0.0, o Android 2.2 (API de nível 8) não é mais compatível.

Você pode verificar e atualizar a versão instalada dessas ferramentas no Android SDK Manager.

Para usar as APIs da Biblioteca de Suporte do RenderScript:

  1. Verifique se você tem a versão necessária do SDK Android instalada.
  2. Atualize as configurações do processo de build do Android para incluir as configurações do RenderScript:
    • Abra o arquivo build.gradle na pasta do app do módulo do aplicativo.
    • Adicione as seguintes configurações do RenderScript ao arquivo:

      Groovy

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

      Kotlin

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

      As configurações listadas acima controlam o comportamento específico no processo de compilação do Android:

      • renderscriptTargetApi: - especifica a versão do bytecode a ser gerado. Recomendamos que você defina esse valor como o menor nível de API capaz de fornecer toda a funcionalidade que estiver usando e defina renderscriptSupportModeEnabled como true. Os valores válidos para essa configuração são valores inteiros de 11 ao nível da API lançado mais recentemente. Se a versão mínima do SDK especificada no manifesto do seu app estiver definida como um valor diferente, esse valor será ignorado, e o valor de destino no arquivo de versão será usado para definir a versão mínima do SDK.
      • renderscriptSupportModeEnabled: - especifica que o bytecode gerado precisará regredir a uma versão compatível se o dispositivo em que ele está sendo executado não for compatível com a versão de destino.
  3. Nas classes do app que usam o RenderScript, adicione uma importação para as classes da Biblioteca de Suporte:

    Kotlin

    import android.support.v8.renderscript.*
    

    Java

    import android.support.v8.renderscript.*;
    

Como usar o RenderScript a partir do código Java ou Kotlin

O uso do RenderScript a partir do código Java ou Kotlin depende das classes de API localizadas no pacote android.renderscript ou android.support.v8.renderscript. A maioria dos apps segue o mesmo padrão de uso básico:

  1. Inicialize um contexto do RenderScript. O contexto RenderScript, criado com create(Context), garante que o RenderScript possa ser usado e fornece um objeto para controlar o ciclo de vida de todos os objetos do RenderScript subsequentes. Considere a criação de contexto como uma operação possivelmente longa, já que pode criar recursos em diferentes partes de hardware; ela não deve estar no caminho crítico de um app, se possível. Normalmente, um app terá apenas um contexto do RenderScript por vez.
  2. Crie pelo menos um Allocation a ser passado para um script. Um Allocation é um objeto do RenderScript que fornece armazenamento para uma quantidade fixa de dados. Os kernels nos scripts usam objetos Allocation como entrada e saída, e os objetos Allocation podem ser acessados nos kernels usando rsGetElementAt_type() e rsSetElementAt_type() ao vincular como scripts globais. Os objetos Allocation permitem que as matrizes sejam passadas de código Java para código RenderScript e vice versa. Os objetos Allocation normalmente são criados com createTyped() ou createFromBitmap().
  3. Crie scripts que sejam necessários. Há dois tipos de script disponíveis ao usar o RenderScript:
    • ScriptC: são os scripts definidos pelo usuário, conforme descrito em Como criar um kernel do RenderScript acima. Cada script tem uma classe Java refletida pelo compilador do RenderScript para facilitar o acesso ao script a partir do código Java. Essa classe tem o nome ScriptC_filename. Por exemplo, se o kernel de mapeamento acima estivesse localizado em invert.rs e um contexto do RenderScript já estivesse localizado em mRenderScript, o código Java ou Kotlin para instanciar o script seria:

      Kotlin

      val invert = ScriptC_invert(renderScript)
      

      Java

      ScriptC_invert invert = new ScriptC_invert(renderScript);
      
    • ScriptIntrinsic: são kernels do RenderScript incorporados para operações comuns, como desfoque gaussiano, convolução e combinação de imagens. Para saber mais, consulte as subclasses de ScriptIntrinsic.
  4. Preencha alocações com dados. Com exceção das alocações criadas com createFromBitmap(), uma alocação é preenchida com dados vazios quando é criada pela primeira vez. Para preencher uma alocação, use um dos métodos de cópia em Allocation. Os métodos de cópia são síncronos.
  5. Defina os scripts globais necessários. É possível definir globais usando métodos na mesma classe ScriptC_filename chamada set_globalname. Por exemplo, para definir uma variável int chamada threshold, use o método Java set_threshold(int). Para definir uma variável rs_allocation chamada lookup, use o método Java set_lookup(Allocation). Os métodos set são assíncronos.
  6. Inicie os kernels apropriados e as funções invocáveis.

    Os métodos para iniciar determinado kernel são refletidos na mesma classe ScriptC_filename com métodos chamados forEach_mappingKernelName() ou reduce_reductionKernelName(). Essas inicializações são assíncronas. Dependendo dos argumentos para o kernel, o método usa uma ou mais alocações, todas com as mesmas dimensões. Por padrão, um kernel é executado sobre cada coordenada nessas dimensões. Para executar um kernel sobre um subconjunto dessas coordenadas, passe um Script.LaunchOptions apropriado como o último argumento para o método forEach ou reduce.

    Inicie funções invocáveis usando os métodos invoke_functionName refletidos na mesma classe ScriptC_filename. Essas inicializações são assíncronas.

  7. Recupere dados de objetos Allocation e objetos javaFutureType . Para acessar dados de um Allocation no código Java, copie esses dados de volta para Java usando um dos métodos de cópia em Allocation. Para ver o resultado de um kernel de redução, use o método javaFutureType.get(). Os métodos de cópia e get() são síncronos.
  8. Destrua o contexto do RenderScript. Você pode destruir o contexto do RenderScript com destroy() ou permitindo que o objeto de contexto do RenderScript seja coletado como lixo. Isso faz com que qualquer uso posterior de qualquer objeto pertencente a esse contexto gere uma exceção.

Modelo de execução assíncrona

Os métodos refletidos forEach, invoke, reduce e set são assíncronos. Cada um pode retornar a Java antes de concluir a ação solicitada. No entanto, as ações individuais são serializadas na ordem em que são iniciadas.

A classe Allocation fornece métodos de cópia para copiar dados de e para alocações. O método de cópia é síncrono e é serializado em relação a qualquer uma das ações assíncronas acima que tocam na mesma alocação.

As classes javaFutureType refletidas fornecem um método get() para ver o resultado de uma redução. get() é síncrono e é serializado em relação à redução (que é assíncrona).

RenderScript de fonte única

O Android 7.0 (API de nível 24) introduz um novo recurso de programação chamado RenderScript de fonte única, em que os kernels são iniciados a partir do script em que são definidos, e não a partir de Java. No momento, essa abordagem está limitada aos kernels de mapeamento, que são simplesmente chamados de “kernels” nesta seção para fins de concisão. Esse novo recurso também permite a criação de alocações do tipo rs_allocation de dentro do script. Agora é possível implementar um algoritmo inteiro somente dentro de um script, mesmo se várias inicializações do kernel forem necessárias. O benefício é duplo: código mais legível, porque mantém a implementação de um algoritmo em uma linguagem; e um código possivelmente mais rápido, devido ao menor número de transições entre Java e RenderScript em várias inicializações de kernel.

Em RenderScript de fonte única, você cria os kernels como descrito em Como criar um kernel do RenderScript. Em seguida, crie uma função invocável que chama rsForEach() para iniciá-la. Essa API usa uma função do kernel como primeiro parâmetro, seguida por alocações de entrada e saída. Uma API semelhante rsForEachWithOptions() usa um argumento extra do tipo rs_script_call_t, que especifica um subconjunto dos elementos das alocações de entrada e saída para o processamento da função do kernel.

Para iniciar o processamento do RenderScript, chame a função invocável do Java. Siga as etapas em Como usar o RenderScript a partir do código Java. Na etapa inicie os kernels apropriados, chame a função invocável usando invoke_function_name(), que iniciará todo o cálculo, incluindo a inicialização dos kernels.

Muitas vezes, as alocações são necessárias para salvar e passar resultados intermediários de uma inicialização do kernel para outra. Você pode criá-las usando rsCreateAllocation(). Uma forma fácil de usar essa API é rsCreateAllocation_<T><W>(…), em que T é o tipo de dados de um elemento e W é a largura do vetor do elemento. A API considera os tamanhos nas dimensões X, Y e Z como argumentos. Para alocações 1D ou 2D, o tamanho da dimensão Y ou Z pode ser omitido. Por exemplo, rsCreateAllocation_uchar4(16384) cria uma alocação 1D de 16384 elementos, cada uma é do tipo uchar4.

As alocações são gerenciadas pelo sistema automaticamente. Você não precisa liberá-las explicitamente. No entanto, é possível chamar rsClearObject(rs_allocation* alloc) para indicar que não precisa mais processar alloc na alocação, fazendo que o sistema possa liberar recursos o mais cedo possível.

A seção Como criar um kernel do RenderScript contém um kernel de exemplo que inverte uma imagem. O exemplo abaixo se expande para aplicar mais de um efeito a uma imagem, usando RenderScript de fonte única. Ele inclui outro kernel, greyscale, que transforma uma imagem colorida em preto e branco. Em seguida, uma função invocável process() aplica esses dois kernels consecutivamente a uma imagem de entrada e produz uma imagem de saída. As alocações de entrada e saída são transmitidas como argumentos do 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);
}

Você pode chamar a função process() de Java ou Kotlin da seguinte maneira:

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

Este exemplo mostra como um algoritmo que envolve duas inicializações do kernel pode ser implementado completamente na própria linguagem do RenderScript. Sem o RenderScript de fonte única, era necessário iniciar os dois kernels do código Java, separando as inicializações do kernel das definições do kernel e dificultando a compreensão de todo o algoritmo. O código do RenderScript de fonte única não é apenas mais fácil de ler, mas também elimina a transição entre Java e o script nas inicializações do kernel. Alguns algoritmos iterativos podem iniciar kernels centenas de vezes, tornando a sobrecarga dessa transição considerável.

Scripts globais

Um script global é uma variável não global comum static em um arquivo de script (.rs). Para um script global chamado var definido no arquivo filename.rs, haverá um método get_var refletido na classe ScriptC_filename. A menos que o script global seja const, também haverá um método set_var.

Um determinado script global tem dois valores separados: um valor Java e um valor script. Esses valores se comportam da seguinte maneira:

  • Se var tiver um inicializador estático no script, ele especificará o valor inicial de var em Java e no script. Caso contrário, esse valor inicial será zero.
  • Acessos a var no script leem e criam o valor de script.
  • O método get_var lê o valor de Java.
  • O método set_var (se existir) cria o valor de Java imediatamente e cria o valor de script de forma assíncrona.

OBSERVAÇÃO: isso significa que, exceto para qualquer inicializador estático no script, os valores criados em um script global a partir de um script não são visíveis para Java.

Kernels de redução em detalhes

Redução é o processo de combinar uma coleção de dados em um único valor. Esse é um primitivo útil em programação paralela, com aplicações como as seguintes:

  • calcular a soma ou o produto em todos os dados;
  • operações lógicas de cálculo (and, or, xor) em todos os dados;
  • encontrar o valor mínimo ou máximo nos dados;
  • pesquisar um valor específico ou a coordenada de um valor específico nos dados.

No Android 7.0 (API de nível 24) ou versões mais recentes, o RenderScript é compatível comkernels de redução para permitir algoritmos de redução eficientes criados pelo usuário. Você pode iniciar os kernels de redução em entradas com 1, 2 ou 3 dimensões.

Um exemplo acima mostra um kernel de redução addint simples. Veja um kernel de redução findMinAndMax mais complicado que encontra os locais dos valores long mínimo e máximo em uma 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;
}

OBSERVAÇÃO: há mais exemplos de kernels de redução aqui.

Para executar um kernel de redução, o ambiente de execução do RenderScript cria uma ou mais variáveis chamadas itens de dados do acumulador para reter o estado do processo de redução. O tempo de execução do RenderScript escolhe o número de itens de dados do acumulador de modo a maximizar o desempenho. O tipo de item de dados do acumulador (accumType) é determinado pela função de acumulador do kernel. O primeiro argumento dessa função é um ponteiro para um item de dados do acumulador. Por padrão, todos os itens de dados do acumulador são inicializados do zero (como se fossem memset). No entanto, você pode criar uma função de inicializador para fazer algo diferente.

Exemplo: no kernel addint, os itens de dados do acumulador (do tipo int) são usados para somar valores de entrada. Como não há uma função de inicializador, cada item de dados do acumulador é inicializado do zero.

Exemplo: no kernel findMinAndMax, os itens de dados do acumulador (do tipo MinAndMax) são usados para rastrear os valores mínimos e máximos encontrados até o momento. Há uma função de inicializador para defini-los como LONG_MAX e LONG_MIN, respectivamente, e para definir os locais desses valores como -1, indicando que os valores não estão realmente presentes na parte (vazia) da entrada que foi processada.

O RenderScript chama sua função de acumulador uma vez para cada coordenada nas entradas. Normalmente, a função precisa atualizar o item de dados do acumulador de alguma forma de acordo com a entrada.

Exemplo: no kernel addint, a função do acumulador adiciona o valor de um elemento de entrada ao item de dados do acumulador.

Exemplo: no kernel findMinAndMax, a função do acumulador verifica se o valor de um elemento de entrada é menor ou igual ao valor mínimo registrado no item de dados do acumulador e/ou maior ou igual ao valor máximo registrado no item de dados do acumulador e atualiza esse item adequadamente.

Depois que a função do accumulator for chamada uma vez para cada coordenada nas entradas, o RenderScript precisará combinar os itens de dados do acumulador em um único item de dados do acumulador. É possível criar uma função do combinador para fazer isso. Se a função do acumulador tiver uma única entrada e nenhum argumento especial, você não precisará criar uma função do combinador. O RenderScript usará a função de acumulador para combinar os itens de dados do acumulador. É possível ainda criar uma função do combinador caso esse comportamento padrão não seja o desejado.

Exemplo: no kernel addint, não há função do combinador, então a função do acumulador será usada. Esse é o comportamento correto, porque se dividirmos uma coleção de valores em duas partes e adicionarmos os valores nessas duas partes separadamente, somar essas duas partes será o mesmo que somar toda a coleção.

Exemplo: no kernel findMinAndMax, a função do combinador verifica se o valor mínimo registrado no item de dados do acumulador de origem *val é menor que o valor mínimo registrado no item de dados do acumulador de destino *accum e atualiza *accum adequadamente. Ele faz um trabalho semelhante para o valor máximo. Isso atualiza *accum para o estado que teria se todos os valores de entrada tivessem sido acumulados em *accum em vez de alguns em *accum e outros em *val.

Depois que todos os itens de dados do acumulador foram combinados, o RenderScript determina o resultado da redução para retornar ao Java. É possível criar uma função de saída para fazer isso. Não é necessário criar uma função de saída caso queira que o valor final dos itens de dados do acumulador combinados seja o resultado da redução.

Exemplo: no kernel addint, não há nenhuma função de saída. O valor final dos itens de dados combinados é a soma de todos os elementos da entrada, que é o valor que queremos retornar.

Exemplo: no kernel findMinAndMax, a função de saída inicializa um valor de resultado int2 para manter os locais dos valores mínimo e máximo resultantes da combinação de todos os itens de dados do acumulador.

Como criar um kernel de redução

#pragma rs reduce define um kernel de redução especificando seu nome e os nomes e papéis das funções que compõem o kernel. Todas essas funções precisam ser static. Um kernel de redução sempre requer uma função accumulator. Você pode omitir algumas ou todas as outras funções, dependendo do que quer que o kernel faça.

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

O significado dos itens no #pragma é o seguinte:

  • reduce(kernelName) (obrigatório): especifica que um kernel de redução está sendo definido. Um método Java refletido reduce_kernelName iniciará o kernel.
  • initializer(initializerName) (opcional): especifica o nome da função inicializadora desse kernel de redução. Quando o kernel é iniciado, o RenderScript chama essa função uma vez para cada item de dados do acumulador. A função precisa ser definida da seguinte forma:

    static void initializerName(accumType *accum) { … }

    accum é um indicador para um item de dados do acumulador para que essa função seja inicializada.

    Se você não fornecer uma função de inicializador, o RenderScript inicializará todos os itens de dados do acumulador do zero (como se tivesse memset), comportando-se como se houvesse uma função inicializadora assim:

    static void initializerName(accumType *accum) {
      memset(accum, 0, sizeof(*accum));
    }
  • accumulator(accumulatorName) (obrigatório): especifica o nome da função do acumulador para esse kernel de redução. Quando você inicia o kernel, o RenderScript chama essa função uma vez para cada coordenada nas entradas para atualizar um item de dados do acumulador de acordo com as entradas. A função precisa ser definida desse jeito:

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

    accum é um indicador para um item de dados do acumulador para que essa função seja modificada. in1 a inN são um ou mais argumentos que são preenchidos automaticamente com base nas entradas passadas para a inicialização do kernel, um argumento por entrada. A função de acumulador pode pegar qualquer um dos argumentos especiais.

    Um exemplo de kernel com várias entradas é dotProduct.

  • combiner(combinerName)

    (opcional): especifica o nome da função do combinador para esse kernel de redução. Depois que o RenderScript chama a função do acumulador uma vez para cada coordenada nas entradas, ele chama essa função quantas vezes forem necessárias para combinar todos os itens de dados do acumulador em um único item. A função precisa ser definida assim:

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

    accum é um indicador para um item de dados do acumulador de destino para que essa função seja modificada. other é um indicador para um item de dados do acumulador de origem para que essa função seja combinada com *accum.

    OBSERVAÇÃO: é possível que *accum, *other ou ambos tenham sido inicializados, mas nunca tenham passado para a função de acumulador; ou seja, um ou ambos nunca foram atualizados de acordo com dados de entrada. Por exemplo, no kernel findMinAndMax, a função do combinador fMMCombiner verifica explicitamente se há idx < 0, porque isso indica um item de dados do acumulador, cujo valor é INITVAL.

    Se você não fornecer uma função do combinador, o RenderScript usará a função do acumulador no lugar, comportando-se como se houvesse uma função do combinador com a seguinte aparência:

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

    Uma função do combinador será obrigatória se o kernel tiver mais de uma entrada, se o tipo de dados de entrada não for o mesmo tipo de dados do acumulador ou se a função do acumulador usar um ou mais argumentos especiais.

  • outconverter(outconverterName) (opcional): especifica o nome da função de saída do kernel de redução. Depois que o RenderScript combina todos os itens de dados do acumulador, ele chama essa função para determinar o resultado da redução a ser retornada para Java. A função precisa ser definida assim:

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

    result é um indicador para um item de dados de resultado (alocado, mas não inicializado pelo tempo de execução do RenderScript) para que essa função seja inicializada com o resultado da redução. resultType é o tipo desse item de dados, que não precisa ser igual a accumType. accum é um indicador para o item de dados acumulados final computado pela função do combinador.

    Se você não fornecer uma função de saída, o RenderScript copiará o item de dados do acumulador final para o item de dados do resultado, comportando-se como se houvesse uma função de saída semelhante a seguinte:

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

    Se você quiser um tipo de resultado diferente do tipo de dados do acumulador, a função de saída será obrigatória.

Observe que um kernel tem tipos de entrada, um tipo de item de dados do acumulador e um tipo de resultado, e nenhum deles precisa ser o mesmo. Por exemplo, no kernel findMinAndMax, o tipo de entrada long, o tipo de item de dados do acumulador MinAndMax e o tipo de resultado int2 são todos diferentes.

O que não se pode presumir?

Não dependa do número de itens de dados do acumulador criados pelo RenderScript para uma determinada inicialização do kernel. Não há garantia de que duas inicializações do mesmo kernel com a mesma entrada criarão o mesmo número de itens de dados do acumulador.

Não confie na ordem em que o RenderScript chama as funções de inicializador, acumulador e combinador; ele pode até chamar algumas em paralelo. Não há garantia de que duas inicializações do mesmo kernel com a mesma entrada seguirão a mesma ordem. A única garantia é que somente a função de inicialização verá um item de dados de acumulador não inicializado. Por exemplo:

  • Não há garantia de que todos os itens de dados do acumulador serão inicializados antes que a função de acumulador seja chamada, embora ela só seja chamada em um item de dados de acumulador inicializado.
  • Não há garantia sobre a ordem em que os elementos de entrada são passados para a função de acumulador.
  • Não há garantia de que a função do acumulador tenha sido chamada para todos os elementos de entrada antes que a função do combinador seja chamada.

Uma consequência disso é que o kernel findMinAndMax não é determinístico: se a entrada contém mais de uma ocorrência do mesmo valor mínimo ou máximo, você não tem como saber qual ocorrência o kernel encontrará.

O que você precisa garantir?

Como o sistema do RenderScript pode optar por executar um kernel de muitas maneiras diferentes, é preciso seguir certas regras para garantir que seu kernel se comporte como você quer. Se você não seguir essas regras, poderá receber resultados incorretos, comportamento não determinístico ou erros de tempo de execução.

As regras abaixo costumam dizer que dois itens de dados do acumulador precisam ter "o mesmo valor". O que isso significa? Isso depende do que você quer que o kernel faça. Para uma redução matemática, como addint, geralmente faz sentido que "o mesmo" signifique igualdade matemática. Para uma pesquisa "escolher qualquer" como findMinAndMax (encontre o local dos valores mínimo e máximo de entrada), onde pode haver mais de uma ocorrência de valores de entrada idênticos, todos os locais de um determinado valor de entrada precisam ser considerados "os mesmos". Você poderia criar um kernel parecido para "encontrar a localização dos últimos valores mínimos e máximos de entrada", onde (digamos) é preferível um valor mínimo na posição 100 em relação a um valor mínimo idêntico na posição 200; para esse kernel, "o mesmo" significaria local idêntico, não apenas valor idêntico, e as funções de acumulador e combinador teriam que ser diferentes das de findMinAndMax.

A função inicializadora precisa criar um valor de identidade. Isto é, se I e A são itens de dados do acumulador inicializados pela função de inicializador, e I nunca foi passado para a função de acumulador (mas A pode ter sido), então
  • combinerName(&A, &I) precisa deixar A o mesmo
  • combinerName(&I, &A) precisa deixar I o mesmo de A

Exemplo: no kernel addint, um item de dados do acumulador é inicializado como zero. A função do combinador para esse kernel realiza a adição; zero é o valor de identidade da adição.

Exemplo:no kernel findMinAndMax, um item de dados acumulador é inicializado como INITVAL.

  • fMMCombiner(&A, &I) deixa A o mesmo, porque I é INITVAL.
  • fMMCombiner(&I, &A) define I como A, porque I é INITVAL.

Portanto, INITVAL é realmente um valor de identidade.

A função do combinador precisa ser comutativa. Isto é, se A e B são itens de dados do acumulador inicializados pela função de inicialização e podem ter sido transferidos para a função de acumulador zero ou mais vezes, então combinerName(&A, &B) precisa definir A como o mesmo valor que combinerName(&B, &A) define B.

Exemplo: no kernel addint, a função do combinador adiciona os dois valores do item de dados do acumulador; a adição é comutativa.

Exemplo:no kernel findMinAndMax, fMMCombiner(&A, &B) é igual a A = minmax(A, B), e minmax é comutativo, de modo que fMMCombiner também é.

A função do combinador precisa ser associativa. Isto é, se A, B e C forem itens de dados do acumulador inicializados pela função inicializadora e que podem ter sido transmitidos à função do acumulador zero ou mais vezes, as duas sequências de código a seguir precisarão definir A como o mesmo valor:

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

Exemplo:no kernel addint, a função do combinador adiciona os dois valores do item de dados do 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
    

A adição é associativa e, portanto, a função do combinador também é.

Exemplo: no kernel findMinAndMax,

fMMCombiner(&A, &B)
é igual a
A = minmax(A, B)
. Portanto, as duas sequências são:

  • 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 é associativo e, portanto, fMMCombiner também é.

A função do acumulador e a função do combinador juntas precisam obedecer à regra básica de dobra. Isto é, se A e B são itens de dados do acumulador, A foi inicializado pela função de inicialização e pode ter sido passado para a função de acumulador zero ou mais vezes, B não foi inicializado e args é a lista de argumentos de entrada e argumentos especiais para uma chamada específica para a função do acumulador, então as duas sequências de código a seguir precisam definir A como o mesmo valor:

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

Exemplo: no kernel addint, para um valor de entrada V:

  • a instrução 1 é igual a A += V;
  • a instrução 2 é igual a B = 0;
  • a instrução 3 é igual a B += V, que é igual a B = V;
  • a instrução 4 é igual a A += B, que é igual a A += V.

As instruções 1 e 4 definem A como o mesmo valor e, desse modo, esse kernel obedece à regra básica de dobra.

Exemplo:no kernel findMinAndMax, para um valor de entrada V na coordenada X:

  • a instrução 1 é igual a A = minmax(A, IndexedVal(V, X));
  • a instrução 2 é igual a B = INITVAL;
  • a instrução 3 é igual a
    B = minmax(B, IndexedVal(V, X))
    
    , que, como B é o valor inicial, é igual a
    B = IndexedVal(V, X)
    
    ;
  • a instrução 4 é igual a
    A = minmax(A, B)
    
    , que é igual a
    A = minmax(A, IndexedVal(V, X))
    
    .

As instruções 1 e 4 definem A como o mesmo valor e, desse modo, esse kernel obedece à regra básica de dobra.

Como chamar um kernel de redução a partir do código Java

Para um kernel de redução chamado kernelName definido no arquivo filename.rs, há três métodos refletidos na classe 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);

Veja alguns exemplos de como chamar o 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();

O método 1 tem um argumento Allocation de entrada para cada argumento de entrada na função de acumulador do kernel. O tempo de execução do RenderScript verifica se todas as alocações de entrada têm as mesmas dimensões e se o tipo Element de cada uma das alocações de entrada corresponde ao argumento de entrada correspondente do protótipo da função do acumulador. Se alguma dessas verificações falhar, o RenderScript lançará uma exceção. O kernel é executado sobre cada coordenada nessas dimensões.

O método 2 é igual ao método 1, mas usa um argumento adicional sc que pode ser usado para limitar a execução do kernel a um subconjunto das coordenadas.

O método 3 é igual ao método 1, exceto que, em vez de usar entradas de alocação, ele usa as entradas da matriz Java. Essa é uma conveniência que evita que você precise criar código para criar explicitamente uma alocação e copiar dados para ela a partir de uma matriz Java. No entanto, usar o método 3 em vez do método 1 não aumenta o desempenho do código. Para cada matriz de entrada, o método 3 cria uma alocação unidimensional temporária com o tipo Element apropriado e setAutoPadding(boolean) ativado, e copia a matriz para a alocação como se fosse o método copyFrom() apropriado de Allocation. Em seguida, ele chama o método 1, passando essas alocações temporárias.

OBSERVAÇÃO: se seu app criar várias chamadas do kernel com a mesma matriz ou com diferentes matrizes do mesmo tipo de elemento e as mesmas dimensões, você poderá melhorar o desempenho criando, preenchendo e reutilizando alocações explicitamente por conta própria em vez de usar o método 3.

javaFutureType, o tipo de retorno dos métodos de redução refletida, é uma classe aninhada estática refletida dentro da classe ScriptC_filename. Ele representa o resultado futuro de uma execução de kernel de redução. Para ver o resultado real da execução, chame o método get() dessa classe, que retorna um valor do tipo javaResultType. get() é 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 é determinado a partir do resultType da função de saída. A menos que resultType seja um tipo não sinalizado (escalar, vetorial ou de matriz), javaResultType é o tipo Java diretamente correspondente. Se resultType for um tipo não sinalizado e houver um tipo sinalizado maior em Java, então javaResultType será aquele tipo maior sinalizado por Java; caso contrário, será o tipo de Java diretamente correspondente. Por exemplo:

  • Se resultType for int, int2 ou int[15], javaResultType será int, Int2 ou int[]. Todos os valores de resultType podem ser representados por javaResultType.
  • Se resultType for uint, uint2 ou uint[15], então javaResultType será long, Long2 ou long[]. Todos os valores de resultType podem ser representados por javaResultType.
  • Se resultType for ulong, ulong2 ou ulong[15], então javaResultType será long, Long2 ou long[]. Certos valores de resultType não podem ser representados por javaResultType.

javaFutureType é o tipo de resultado futuro correspondente ao resultType da função de saída.

  • Se resultType não for um tipo de matriz, javaFutureType será result_resultType.
  • Se resultType for uma matriz de tamanho Count com membros do tipo memberType, então javaFutureType será resultArrayCount_memberType.

Por exemplo:

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

Se javaResultType for um tipo de objeto (incluindo um tipo de matriz), cada chamada para javaFutureType.get() na mesma instância retornará o mesmo objeto.

Se javaResultType não puder representar todos os valores do tipo resultType e um kernel de redução produzir um valor não representável, javaFutureType.get() lançará uma exceção.

Método 3 e devecSiInXType

devecSiInXType é o tipo Java correspondente ao inXType do argumento correspondente da função do acumulator. A menos que inXType seja um tipo não sinalizado ou vetorial, devecSiInXType é o tipo Java diretamente correspondente. Se inXType for um tipo escalar não sinalizado, devecSiInXType será o tipo Java que corresponde diretamente ao tipo escalar sinalizado de mesmo tamanho. Se inXType for um tipo vetorial sinalizado, devecSiInXType será o tipo Java que corresponde diretamente ao tipo de componente de vetor. Se inXType for um tipo vetorial não sinalizado, devecSiInXType será o tipo Java correspondente diretamente ao tipo escalar sinalizado do mesmo tamanho do tipo de componente de vetor. Por exemplo:

  • Se inXType for int, devecSiInXType será int.
  • Se inXType for int2, devecSiInXType será int. A matriz é uma representação nivelada: tem duas vezes mais elementos escalares, porque a alocação tem elementos vetoriais de dois componentes. É dessa mesma forma que os métodos copyFrom() de Allocation funcionam.
  • Se inXType for uint, devecSiInXType será int. Um valor sinalizado na matriz Java é interpretado como um valor não sinalizado do mesmo padrão de bits na alocação. É dessa mesma forma que os métodos copyFrom() de Allocation funcionam.
  • Se inXType for uint2, devecSiInXType será int. Essa é uma combinação da forma como int2 e uint são manipulados: a matriz é uma representação nivelada, e valores sinalizados da matriz Java são interpretados como valores de elemento não sinalizados do RenderScript.

Observe que, para o Método 3, os tipos de entrada são tratados de maneira diferente dos tipos de resultado:

  • A entrada do vetor de um script é nivelada no lado do Java, enquanto o resultado de vetor de um script não é.
  • A entrada não sinalizada de um script é representada como uma entrada sinalizada do mesmo tamanho no lado do Java, enquanto o resultado não sinalizado de um script é representado como um tipo sinalizado ampliado no lado do Java (exceto no caso de ulong).

Mais exemplos de kernels de redução

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

Outros exemplos de código

Os exemplos BasicRenderScript, RenderScriptIntrinsic e Hello Compute (links em inglês), demonstram ainda mais o uso das APIs abordadas nesta página.