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 conjuntoAllocations
de entrada em umAllocation
de saída, umElement
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 argumentoin
é automaticamente preenchido com base noAllocation
de entrada passada para a inicialização do kernel. Os argumentosx
ey
são discutidos abaixo. O valor retornado do kernel é criado automaticamente no local apropriado na saídaAllocation
. Por padrão, esse kernel é executado em toda aAllocation
de entrada, com uma execução da função do kernel porElement
naAllocation
.Um kernel de mapeamento pode ter uma ou mais
Allocations
de entrada, uma únicaAllocation
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 deElement
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 globaisrs_allocation
e acessados a partir de um kernel ou de uma função invocável por meio dersGetElementAt_type()
oursSetElementAt_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 deAllocations
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 deaccumulator
addintAccum
, neste exemplo). Todas essas funções precisam serstatic
. Um kernel de redução sempre requer uma função deaccumulator
; 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 noAllocation
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 aAllocation
de entrada, com uma execução da função do acumulador porElement
naAllocation
. 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 tipoElement
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 nenhumaAllocations
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
ez
, que precisam ser do tipoint
ouuint32_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 argumentocontext
está disponível no Android 6.0 (API de nível 23) ou versões mais recentes.- Uma função
init()
opcional. A funçãoinit()
é 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 emrs_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 Android 3.0 (API de nível 11) ou 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:
- Verifique se você tem a versão necessária do SDK Android instalada.
- Atualize as configurações do processo de compilação do Android para incluir as configurações do RenderScript:
- Abra o arquivo
build.gradle
na pasta de apps do módulo do seu app. - 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 definarenderscriptSupportModeEnabled
comotrue
. 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.
- Abra o arquivo
- Nas classes do seu app que usam 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:
- Inicialize um contexto do RenderScript. O contexto
RenderScript
, criado comcreate(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. - Crie pelo menos um
Allocation
a ser passado para um script. UmAllocation
é um objeto do RenderScript que fornece armazenamento para uma quantidade fixa de dados. Os kernels nos scripts usam objetosAllocation
como entrada e saída, e os objetosAllocation
podem ser acessados nos kernels usandorsGetElementAt_type()
ersSetElementAt_type()
ao vincular como scripts globais. Os objetosAllocation
permitem que as matrizes sejam passadas de código Java para código RenderScript e vice versa. Os objetosAllocation
normalmente são criados comcreateTyped()
oucreateFromBitmap()
. - 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 eminvert.rs
e um contexto do RenderScript já estivesse localizado emmRenderScript
, 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
.
- 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
- 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 emAllocation
. Os métodos de cópia são síncronos. - Defina os scripts globais necessários. É possível definir globais usando métodos na mesma
classe
ScriptC_filename
chamadaset_globalname
. Por exemplo, para definir uma variávelint
chamadathreshold
, use o método Javaset_threshold(int)
. Para definir uma variávelrs_allocation
chamadalookup
, use o método Javaset_lookup(Allocation)
. Os métodosset
são assíncronos. - 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 chamadosforEach_mappingKernelName()
oureduce_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 umScript.LaunchOptions
apropriado como o último argumento para o métodoforEach
oureduce
.Inicie funções invocáveis usando os métodos
invoke_functionName
refletidos na mesma classeScriptC_filename
. Essas inicializações são assíncronas. - Recupere dados de objetos
Allocation
e objetos javaFutureType . Para acessar dados de umAllocation
no código Java, copie esses dados de volta para Java usando um dos métodos de cópia emAllocation
. Para ver o resultado de um kernel de redução, use o métodojavaFutureType.get()
. Os métodos de cópia eget()
são síncronos. - 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 refletidoreduce_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
ainN
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 combinadorfMMCombiner
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. 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 é, seI
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
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)
deixaA
o mesmo, porqueI
éINITVAL
.fMMCombiner(&I, &A)
defineI
comoA
, porqueI
éINITVAL
.
Portanto, INITVAL
é, de fato, 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)
é o mesmo de
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 aB = V
; - a instrução 4 é igual a
A += B
, que é igual aA += 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 aB = IndexedVal(V, X)
; - a instrução 4 é igual a
A = minmax(A, B)
, que é igual aA = 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. Exemplo:
- Se resultType for
int
,int2
ouint[15]
, javaResultType seráint
,Int2
ouint[]
. Todos os valores de resultType podem ser representados por javaResultType. - Se resultType for
uint
,uint2
ouuint[15]
, então javaResultType serálong
,Long2
oulong[]
. Todos os valores de resultType podem ser representados por javaResultType. - Se resultType for
ulong
,ulong2
ouulong[15]
, então javaResultType serálong
,Long2
oulong[]
. 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
.
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. 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étodoscopyFrom()
deAllocation
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étodoscopyFrom()
deAllocation
funcionam. - Se inXType for
uint2
, devecSiInXType seráint
. Essa é uma combinação da forma comoint2
euint
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.