RenderScript è un framework per l'esecuzione di attività ad alta intensità di calcolo con prestazioni elevate su Android. RenderScript è orientato principalmente all'uso con il calcolo parallelo dei dati, anche se possono trarne vantaggio anche i carichi di lavoro seriali. Il runtime RenderScript parallelizza il lavoro tra i processori disponibili su un dispositivo, come CPU e GPU multi-core. In questo modo puoi concentrarti sull'espressione degli algoritmi anziché sulla pianificazione del lavoro. RenderScript è particolarmente utile per le applicazioni che eseguono l'elaborazione delle immagini, la fotografia computazionale o la computer vision.
Per iniziare a utilizzare RenderScript, devi comprendere due concetti principali:
- Il linguaggio stesso è un linguaggio derivato da C99 per scrivere codice di calcolo ad alte prestazioni. Scrittura di un kernel RenderScript descrive come utilizzarlo per scrivere kernel di calcolo.
- L'API Control viene utilizzata per gestire il ciclo di vita delle risorse RenderScript e controllare l'esecuzione del kernel. È disponibile in tre lingue diverse: Java, C++ in Android NDK e il linguaggio del kernel derivato da C99. Utilizzo di RenderScript dal codice Java e RenderScript a origine singola descrivono rispettivamente la prima e la terza opzione.
Scrivere un kernel RenderScript
Un kernel RenderScript si trova in genere in un file .rs
nella
directory <project_root>/src/rs
; ogni file .rs
è chiamato
script. Ogni script contiene il proprio insieme di kernel, funzioni e variabili. Uno script può
contenere:
- Una dichiarazione pragma (
#pragma version(1)
) che dichiara la versione del linguaggio del kernel RenderScript utilizzato in questo script. Al momento, l'unico valore valido è 1. - Una dichiarazione pragma (
#pragma rs java_package_name(com.example.app)
) che dichiara il nome del pacchetto delle classi Java riflesse da questo script. Tieni presente che il file.rs
deve far parte del pacchetto dell'applicazione e non di un progetto di libreria. - Zero o più funzioni richiamabili. Una funzione richiamabile è una funzione RenderScript a thread singolo che puoi chiamare dal tuo codice Java con argomenti arbitrari. Questi sono spesso utili per la configurazione iniziale o i calcoli seriali all'interno di una pipeline di elaborazione più grande.
Zero o più variabili globali dello script. Una variabile globale di script è simile a una variabile globale in C. Puoi accedere alle variabili globali dello script dal codice Java, che vengono spesso utilizzate per il passaggio di parametri ai kernel RenderScript. Le variabili globali dello script sono spiegate più in dettaglio qui.
Zero o più kernel di calcolo. Un kernel di calcolo è una funzione o una raccolta di funzioni che puoi indirizzare al runtime RenderScript per l'esecuzione in parallelo su una raccolta di dati. Esistono due tipi di kernel di calcolo: i kernel di mapping (chiamati anche kernel foreach) e i kernel di riduzione.
Un kernel di mapping è una funzione parallela che opera su una raccolta di
Allocations
delle stesse dimensioni. Per impostazione predefinita, viene eseguito una volta per ogni coordinata in queste dimensioni. Viene in genere (ma non esclusivamente) utilizzato per trasformare una raccolta di inputAllocations
in un outputAllocation
Element
alla volta.Ecco un esempio di un semplice kernel di mapping:
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; }
Per molti aspetti, questa funzione è identica a una funzione C standard. La proprietà
RS_KERNEL
applicata al prototipo di funzione specifica che la funzione è un kernel di mappatura RenderScript anziché una funzione richiamabile. L'argomentoin
viene compilato automaticamente in base all'Allocation
di input passato all'avvio del kernel. Gli argomentix
ey
sono descritti di seguito. Il valore restituito dal kernel viene scritto automaticamente nella posizione appropriata nell'outputAllocation
. Per impostazione predefinita, questo kernel viene eseguito sull'intero inputAllocation
, con un'esecuzione della funzione kernel per ogniElement
inAllocation
.Un kernel di mappatura può avere uno o più
Allocations
di input, un singoloAllocation
di output o entrambi. Il runtime di RenderScript verifica che tutte le allocazioni di input e output abbiano le stesse dimensioni e che i tipiElement
delle allocazioni di input e output corrispondano al prototipo del kernel. Se uno di questi controlli non va a buon fine, RenderScript genera un'eccezione.NOTA:prima di Android 6.0 (livello API 23), un kernel di mappatura potrebbe non avere più di un
Allocation
di input.Se hai bisogno di più input o output
Allocations
di quelli del kernel, questi oggetti devono essere associati alle variabili globali dello scriptrs_allocation
e accessibili da una funzione del kernel o richiamabile tramitersGetElementAt_type()
orsSetElementAt_type()
.NOTA:
RS_KERNEL
è una macro definita automaticamente da RenderScript per tua comodità:#define RS_KERNEL __attribute__((kernel))
Un kernel di riduzione è una famiglia di funzioni che opera su una raccolta di input
Allocations
delle stesse dimensioni. Per impostazione predefinita, la relativa funzione di accumulo viene eseguita una volta per ogni coordinata in queste dimensioni. Viene in genere (ma non esclusivamente) utilizzato per "ridurre" una raccolta diAllocations
di input a un singolo valore.Ecco un esempio di un semplice kernel di riduzione che somma
Elements
del suo input:#pragma rs reduce(addint) accumulator(addintAccum) static void addintAccum(int *accum, int val) { *accum += val; }
Un kernel di riduzione è costituito da una o più funzioni scritte dall'utente.
#pragma rs reduce
viene utilizzato per definire il kernel specificandone il nome (addint
, in questo esempio) e i nomi e i ruoli delle funzioni che lo compongono (una funzioneaccumulator
addintAccum
, in questo esempio). Tutte queste funzioni devono esserestatic
. Un kernel di riduzione richiede sempre una funzioneaccumulator
; può anche avere altre funzioni, a seconda di ciò che vuoi che faccia il kernel.Una funzione di accumulatore del kernel di riduzione deve restituire
void
e deve avere almeno due argomenti. Il primo argomento (accum
, in questo esempio) è un puntatore a un elemento di dati dell'accumulatore e il secondo (val
, in questo esempio) viene compilato automaticamente in base all'inputAllocation
passato all'avvio del kernel. L'elemento di dati dell'accumulatore viene creato dal runtime RenderScript; per impostazione predefinita, viene inizializzato su zero. Per impostazione predefinita, questo kernel viene eseguito sull'intero inputAllocation
, con un'esecuzione della funzione di accumulatore perElement
inAllocation
. Per impostazione predefinita, il valore finale dell'elemento di dati dell'accumulatore viene trattato come risultato della riduzione e viene restituito a Java. Il runtime RenderScript verifica che il tipoElement
dell'allocazione di input corrisponda al prototipo della funzione di accumulatore. In caso contrario, RenderScript genera un'eccezione.Un kernel di riduzione ha uno o più
Allocations
di input, ma nessunAllocations
di output.I kernel di riduzione sono spiegati in modo più dettagliato qui.
I kernel di riduzione sono supportati in Android 7.0 (livello API 24) e versioni successive.
Una funzione kernel di mappatura o una funzione accumulatore del kernel di riduzione può accedere alle coordinate dell'esecuzione corrente utilizzando gli argomenti speciali
x
,y
ez
, che devono essere di tipoint
ouint32_t
. Questi argomenti sono facoltativi.Una funzione kernel di mappatura o una funzione accumulatore del kernel di riduzione può anche accettare l'argomento speciale facoltativo
context
di tipo rs_kernel_context. È necessaria per una famiglia di API di runtime utilizzate per eseguire query su determinate proprietà dell'esecuzione corrente, ad esempio rsGetDimX. L'argomentocontext
è disponibile in Android 6.0 (livello API 23) e versioni successive.- Una funzione
init()
facoltativa. La funzioneinit()
è un tipo speciale di funzione richiamabile che RenderScript esegue quando lo script viene istanziato per la prima volta. Ciò consente di eseguire automaticamente alcuni calcoli durante la creazione dello script. - Zero o più funzioni e variabili globali statiche dello script. Una variabile globale di script statica è equivalente a una
variabile globale di script, tranne per il fatto che non è accessibile dal codice Java. Una funzione statica è una funzione C
standard che può essere chiamata da qualsiasi kernel o funzione richiamabile nello script, ma non è esposta
all'API Java. Se non è necessario accedere a una funzione o a una variabile globale di uno script dal codice Java, è
consigliabile dichiararla
static
.
Impostazione della precisione in virgola mobile
Puoi controllare il livello richiesto di precisione in virgola mobile in uno script. Questa opzione è utile se non è necessario lo standard IEEE 754-2008 completo (utilizzato per impostazione predefinita). I seguenti pragma possono impostare un diverso livello di precisione in virgola mobile:
#pragma rs_fp_full
(valore predefinito se non viene specificato nulla): per le app che richiedono precisione in virgola mobile come descritto nello standard IEEE 754-2008.#pragma rs_fp_relaxed
: Per le app che non richiedono una rigorosa conformità allo standard IEEE 754-2008 e possono tollerare una precisione inferiore. Questa modalità attiva lo svuotamento a zero per i numeri denormalizzati e l'arrotondamento verso zero.#pragma rs_fp_imprecise
: Per le app che non hanno requisiti di precisione rigorosi. Questa modalità abilita tutte le funzionalità dirs_fp_relaxed
, oltre a quanto segue:- Le operazioni che danno come risultato -0.0 possono restituire +0.0.
- Le operazioni su INF e NAN non sono definite.
La maggior parte delle applicazioni può utilizzare rs_fp_relaxed
senza effetti collaterali. Ciò può essere molto
vantaggioso su alcune architetture grazie a ottimizzazioni aggiuntive disponibili solo con una precisione
rilassata (ad esempio le istruzioni della CPU SIMD).
Accesso alle API RenderScript da Java
Quando sviluppi un'applicazione Android che utilizza RenderScript, puoi accedere alla relativa API da Java in uno dei due modi seguenti:
android.renderscript
: le API in questo pacchetto di classi sono disponibili sui dispositivi con Android 3.0 (livello API 11) e versioni successive.android.support.v8.renderscript
: le API in questo pacchetto sono disponibili tramite una libreria di supporto, che ti consente di utilizzarle su dispositivi con Android 2.3 (livello API 9) e versioni successive.
Ecco i compromessi:
- Se utilizzi le API Support Library, la parte RenderScript della tua applicazione sarà
compatibile con i dispositivi con Android 2.3 (livello API 9) e versioni successive, indipendentemente dalle funzionalità
RenderScript che utilizzi. In questo modo, la tua applicazione può funzionare su più dispositivi rispetto a quando utilizzi le API native (
android.renderscript
). - Alcune funzionalità di RenderScript non sono disponibili tramite le API della libreria di supporto.
- Se utilizzi le API Support Library, otterrai APK (forse in modo significativo) più grandi rispetto a quelle che otterresti se utilizzassi le API native (
android.renderscript
).
Utilizzo delle API della libreria di supporto RenderScript
Per utilizzare le API RenderScript della libreria di supporto, devi configurare il tuo ambiente di sviluppo per poterle utilizzare. Per utilizzare queste API sono necessari i seguenti strumenti SDK Android:
- Strumenti SDK Android revisione 22.2 o versioni successive
- Revisione 18.1.0 o successive di Android SDK Build-tools
Tieni presente che a partire da Android SDK Build-tools 24.0.0, Android 2.2 (livello API 8) non è più supportato.
Puoi controllare e aggiornare la versione installata di questi strumenti in Android SDK Manager.
Per utilizzare le API RenderScript della libreria di supporto:
- Assicurati di aver installato la versione richiesta dell'SDK Android.
- Aggiorna le impostazioni per il processo di build di Android in modo da includere le impostazioni di RenderScript:
- Apri il file
build.gradle
nella cartella dell'app del modulo dell'applicazione. - Aggiungi le seguenti impostazioni RenderScript al file:
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 } }
Le impostazioni elencate sopra controllano un comportamento specifico nel processo di compilazione di Android:
renderscriptTargetApi
: specifica la versione del bytecode da generare. Ti consigliamo di impostare questo valore sul livello API più basso in grado di fornire tutte le funzionalità che utilizzi e di impostarerenderscriptSupportModeEnabled
sutrue
. I valori validi per questa impostazione sono tutti i numeri interi da 11 al livello API rilasciato più di recente. Se la versione minima dell'SDK specificata nel manifest dell'applicazione è impostata su un valore diverso, questo valore viene ignorato e il valore target nel file di build viene utilizzato per impostare la versione minima dell'SDK.renderscriptSupportModeEnabled
: specifica che il bytecode generato deve eseguire il fallback a una versione compatibile se il dispositivo su cui viene eseguito non supporta la versione di destinazione.
- Apri il file
- Nelle classi dell'applicazione che utilizzano RenderScript, aggiungi un'importazione per le classi della libreria di supporto:
Kotlin
import android.support.v8.renderscript.*
Java
import android.support.v8.renderscript.*;
Utilizzo di RenderScript dal codice Java o Kotlin
L'utilizzo di RenderScript dal codice Java o Kotlin si basa sulle classi API che si trovano nel pacchetto android.renderscript
o android.support.v8.renderscript
. La maggior parte
delle applicazioni segue lo stesso pattern di utilizzo di base:
- Inizializza un contesto RenderScript. Il contesto
RenderScript
, creato concreate(Context)
, garantisce che RenderScript possa essere utilizzato e fornisce un oggetto per controllare la durata di tutti gli oggetti RenderScript successivi. Dovresti considerare la creazione del contesto come un'operazione potenzialmente di lunga durata, poiché potrebbe creare risorse su diversi hardware; se possibile, non dovrebbe trovarsi nel percorso critico di un'applicazione. In genere, un'applicazione ha un solo contesto RenderScript alla volta. - Crea almeno un
Allocation
da passare a uno script. UnAllocation
è un oggetto RenderScript che fornisce spazio di archiviazione per una quantità fissa di dati. I kernel negli script prendono come input e output oggettiAllocation
e gli oggettiAllocation
possono essere accessibili nei kernel utilizzandorsGetElementAt_type()
ersSetElementAt_type()
se associati come variabili globali dello script. Gli oggettiAllocation
consentono di passare array dal codice Java al codice RenderScript e viceversa. Gli oggettiAllocation
vengono in genere creati utilizzandocreateTyped()
ocreateFromBitmap()
. - Crea gli script necessari. Quando utilizzi RenderScript, sono disponibili due tipi di script:
- ScriptC: si tratta degli script definiti dall'utente, come descritto in Scrittura di un kernel RenderScript sopra. Ogni script ha una classe Java
riflessa dal compilatore RenderScript per facilitare l'accesso allo script dal codice Java;
questa classe ha il nome
ScriptC_filename
. Ad esempio, se il kernel di mappatura sopra si trova ininvert.rs
e un contesto RenderScript si trova già inmRenderScript
, il codice Java o Kotlin per creare un'istanza dello script sarebbe:Kotlin
val invert = ScriptC_invert(renderScript)
Java
ScriptC_invert invert = new ScriptC_invert(renderScript);
- ScriptIntrinsic: questi sono kernel RenderScript integrati per operazioni comuni,
come sfocatura gaussiana, convoluzione e fusione di immagini. Per ulteriori informazioni, consulta le sottoclassi di
ScriptIntrinsic
.
- ScriptC: si tratta degli script definiti dall'utente, come descritto in Scrittura di un kernel RenderScript sopra. Ogni script ha una classe Java
riflessa dal compilatore RenderScript per facilitare l'accesso allo script dal codice Java;
questa classe ha il nome
- Compila le allocazioni con i dati. Ad eccezione delle allocazioni create con
createFromBitmap()
, un'allocazione viene compilata con dati vuoti al momento della creazione. Per compilare un'allocazione, utilizza uno dei metodi di "copia" inAllocation
. I metodi "copy" sono sincroni. - Imposta le variabili globali dello script necessarie. Puoi impostare le variabili globali utilizzando i metodi della
stessa classe
ScriptC_filename
denominataset_globalname
. Ad esempio, per impostare una variabileint
denominatathreshold
, utilizza il metodo Javaset_threshold(int)
; per impostare una variabilers_allocation
denominatalookup
, utilizza il metodo Javaset_lookup(Allocation)
. I metodiset
sono asincroni. - Avvia i kernel e le funzioni richiamabili appropriati.
I metodi per avviare un determinato kernel sono riflessi nella stessa classe
ScriptC_filename
con metodi denominatiforEach_mappingKernelName()
oreduce_reductionKernelName()
. Questi avvii sono asincroni. A seconda degli argomenti del kernel, il metodo accetta una o più allocazioni, che devono avere tutte le stesse dimensioni. Per impostazione predefinita, un kernel viene eseguito su ogni coordinata di queste dimensioni; per eseguire un kernel su un sottoinsieme di queste coordinate, passa unScript.LaunchOptions
appropriato come ultimo argomento al metodoforEach
oreduce
.Avvia le funzioni richiamabili utilizzando i metodi
invoke_functionName
riflessi nella stessa classeScriptC_filename
. Questi avvii sono asincroni. - Recupera i dati dagli oggetti
Allocation
e dagli oggetti javaFutureType. Per accedere ai dati di unAllocation
dal codice Java, devi copiarli di nuovo in Java utilizzando uno dei metodi "copy" inAllocation
. Per ottenere il risultato di un kernel di riduzione, devi utilizzare il metodojavaFutureType.get()
. I metodi "copy" eget()
sono sincroni. - Elimina il contesto RenderScript. Puoi eliminare il contesto RenderScript
con
destroy()
o consentendo la garbage collection dell'oggetto contesto RenderScript. In questo modo, qualsiasi ulteriore utilizzo di un oggetto appartenente a quel contesto genera un'eccezione.
Modello di esecuzione asincrono
I metodi forEach
, invoke
, reduce
e set
riflessi sono asincroni: ognuno può tornare a Java prima di completare l'azione richiesta. Tuttavia, le singole azioni vengono serializzate nell'ordine in cui vengono avviate.
La classe Allocation
fornisce metodi di "copia" per copiare i dati in e da Allocations. Un metodo "copy" è sincrono e viene serializzato rispetto a qualsiasi
delle azioni asincrone precedenti che interessano la stessa allocazione.
Le classi javaFutureType riflesse forniscono
un metodo get()
per ottenere il risultato di una riduzione. get()
è
sincrono e viene serializzato rispetto alla riduzione (che è asincrona).
RenderScript a origine singola
Android 7.0 (livello API 24) introduce una nuova funzionalità di programmazione chiamata Single-Source
RenderScript, in cui i kernel vengono avviati dallo script in cui sono definiti, anziché
da Java. Questo approccio è attualmente limitato ai kernel di mappatura, che in questa sezione vengono semplicemente chiamati "kernel" per brevità. Questa nuova funzionalità supporta anche la creazione di allocazioni di tipo
rs_allocation
dall'interno dello script. Ora è possibile
implementare un intero algoritmo esclusivamente all'interno di uno script, anche se sono necessari più avvii del kernel.
Il vantaggio è duplice: un codice più leggibile, perché mantiene l'implementazione di un algoritmo in un unico linguaggio, e un codice potenzialmente più veloce, grazie a un minor numero di transizioni tra Java e RenderScript in più avvii del kernel.
In Single-Source RenderScript, scrivi i kernel come descritto in
Scrivere un kernel RenderScript. Poi scrivi una funzione richiamabile che chiama
rsForEach()
per avviarle. Questa API accetta una funzione kernel come primo
parametro, seguita dalle allocazioni di input e output. Un'API simile
rsForEachWithOptions()
accetta un argomento aggiuntivo di tipo
rs_script_call_t
, che specifica un sottoinsieme degli elementi delle allocazioni di input e
output da elaborare per la funzione del kernel.
Per avviare il calcolo di RenderScript, chiama la funzione richiamabile da Java.
Segui i passaggi descritti in Utilizzo di RenderScript dal codice Java.
Nel passaggio Avvia i kernel appropriati, chiama
la funzione richiamabile utilizzando invoke_function_name()
, che avvierà
l'intero calcolo, incluso l'avvio dei kernel.
Le allocazioni sono spesso necessarie per salvare e passare
i risultati intermedi da un avvio del kernel all'altro. Puoi crearli utilizzando
rsCreateAllocation(). Una forma di questa API facile da usare è
rsCreateAllocation_<T><W>(…)
, dove T è il tipo di dati per un
elemento e W è la larghezza del vettore per l'elemento. L'API accetta le dimensioni
X, Y e Z come argomenti. Per le allocazioni 1D o 2D, le dimensioni per la dimensione Y o Z possono
essere omesse. Ad esempio, rsCreateAllocation_uchar4(16384)
crea un'allocazione 1D di
16384 elementi, ognuno dei quali è di tipo uchar4
.
Le allocazioni vengono gestite automaticamente dal sistema. Non
devi rilasciarli o liberarli esplicitamente. Tuttavia, puoi chiamare
rsClearObject(rs_allocation* alloc)
per indicare che non hai più bisogno dell'handle
alloc
per l'allocazione sottostante,
in modo che il sistema possa liberare le risorse il prima possibile.
La sezione Scrittura di un kernel RenderScript contiene un esempio
di kernel che inverte un'immagine. L'esempio seguente espande questo concetto per applicare più di un effetto a un'immagine,
utilizzando Single-Source RenderScript. Include un altro kernel, greyscale
, che trasforma un'immagine a colori in bianco e nero. Una funzione richiamabile process()
applica quindi i due kernel
consecutivamente a un'immagine di input e produce un'immagine di output. Le allocazioni per l'input e
l'output vengono trasmesse come argomenti di 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); }
Puoi chiamare la funzione process()
da Java o Kotlin nel seguente modo:
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);
Questo esempio mostra come un algoritmo che prevede due lanci del kernel può essere implementato completamente nel linguaggio RenderScript stesso. Senza Single-Source RenderScript, dovresti avviare entrambi i kernel dal codice Java, separando gli avvii dei kernel dalle definizioni dei kernel e rendendo più difficile la comprensione dell'intero algoritmo. Il codice RenderScript a origine singola non solo è più facile da leggere, ma elimina anche la transizione tra Java e lo script durante l'avvio del kernel. Alcuni algoritmi iterativi possono avviare kernel centinaia di volte, rendendo considerevole l'overhead di questa transizione.
Variabili globali dello script
Una variabile globale dello script è una normale variabile non globale static
in un file di script (.rs
). Per una variabile
globale dello script denominata var definita nel
file filename.rs
, sarà presente un
metodo get_var
riflesso nella
classe ScriptC_filename
. A meno che il valore globale
non sia const
, sarà presente anche un
metodo set_var
.
Un determinato script globale ha due valori separati: un valore Java e un valore script. Questi valori si comportano come segue:
- Se var ha un inizializzatore statico nello script, specifica il valore iniziale di var sia in Java che nello script. In caso contrario, il valore iniziale è zero.
- Gli accessi a var all'interno dello script leggono e scrivono il relativo valore.
- Il metodo
get_var
legge il valore Java. - Il metodo
set_var
(se esiste) scrive il valore Java immediatamente e scrive il valore dello script in modo asincrono.
NOTA: ciò significa che, ad eccezione di qualsiasi inizializzatore statico nello script, i valori scritti in una variabile globale all'interno di uno script non sono visibili a Java.
Riduzione dei kernel in dettaglio
La riduzione è il processo di combinazione di una raccolta di dati in un unico valore. Si tratta di una primitiva utile nella programmazione parallela, con applicazioni come le seguenti:
- calcolando la somma o il prodotto di tutti i dati
- calcolo di operazioni logiche (
and
,or
,xor
) su tutti i dati - trovare il valore minimo o massimo all'interno dei dati
- cercando un valore specifico o la coordinata di un valore specifico all'interno dei dati
In Android 7.0 (livello API 24) e versioni successive, RenderScript supporta i kernel di riduzione per consentire algoritmi di riduzione scritti dall'utente efficienti. Puoi avviare i kernel di riduzione sugli input con 1, 2 o 3 dimensioni.
Un esempio precedente mostra un semplice kernel di riduzione addint.
Ecco un kernel di riduzione findMinAndMax più complesso
che trova le posizioni dei valori minimo e massimo di long
in un
Allocation
unidimensionale:
#define LONG_MAX (long)((1UL << 63) - 1) #define LONG_MIN (long)(1UL << 63) #pragma rs reduce(findMinAndMax) \ initializer(fMMInit) accumulator(fMMAccumulator) \ combiner(fMMCombiner) outconverter(fMMOutConverter) // Either a value and the location where it was found, or INITVAL. typedef struct { long val; int idx; // -1 indicates INITVAL } IndexedVal; typedef struct { IndexedVal min, max; } MinAndMax; // In discussion below, this initial value { { LONG_MAX, -1 }, { LONG_MIN, -1 } } // is called INITVAL. static void fMMInit(MinAndMax *accum) { accum->min.val = LONG_MAX; accum->min.idx = -1; accum->max.val = LONG_MIN; accum->max.idx = -1; } //---------------------------------------------------------------------- // In describing the behavior of the accumulator and combiner functions, // it is helpful to describe hypothetical functions // IndexedVal min(IndexedVal a, IndexedVal b) // IndexedVal max(IndexedVal a, IndexedVal b) // MinAndMax minmax(MinAndMax a, MinAndMax b) // MinAndMax minmax(MinAndMax accum, IndexedVal val) // // The effect of // IndexedVal min(IndexedVal a, IndexedVal b) // is to return the IndexedVal from among the two arguments // whose val is lesser, except that when an IndexedVal // has a negative index, that IndexedVal is never less than // any other IndexedVal; therefore, if exactly one of the // two arguments has a negative index, the min is the other // argument. Like ordinary arithmetic min and max, this function // is commutative and associative; that is, // // min(A, B) == min(B, A) // commutative // min(A, min(B, C)) == min((A, B), C) // associative // // The effect of // IndexedVal max(IndexedVal a, IndexedVal b) // is analogous (greater . . . never greater than). // // Then there is // // MinAndMax minmax(MinAndMax a, MinAndMax b) { // return MinAndMax(min(a.min, b.min), max(a.max, b.max)); // } // // Like ordinary arithmetic min and max, the above function // is commutative and associative; that is: // // minmax(A, B) == minmax(B, A) // commutative // minmax(A, minmax(B, C)) == minmax((A, B), C) // associative // // Finally define // // MinAndMax minmax(MinAndMax accum, IndexedVal val) { // return minmax(accum, MinAndMax(val, val)); // } //---------------------------------------------------------------------- // This function can be explained as doing: // *accum = minmax(*accum, IndexedVal(in, x)) // // This function simply computes minimum and maximum values as if // INITVAL.min were greater than any other minimum value and // INITVAL.max were less than any other maximum value. Note that if // *accum is INITVAL, then this function sets // *accum = IndexedVal(in, x) // // After this function is called, both accum->min.idx and accum->max.idx // will have nonnegative values: // - x is always nonnegative, so if this function ever sets one of the // idx fields, it will set it to a nonnegative value // - if one of the idx fields is negative, then the corresponding // val field must be LONG_MAX or LONG_MIN, so the function will always // set both the val and idx fields static void fMMAccumulator(MinAndMax *accum, long in, int x) { IndexedVal me; me.val = in; me.idx = x; if (me.val <= accum->min.val) accum->min = me; if (me.val >= accum->max.val) accum->max = me; } // This function can be explained as doing: // *accum = minmax(*accum, *val) // // This function simply computes minimum and maximum values as if // INITVAL.min were greater than any other minimum value and // INITVAL.max were less than any other maximum value. Note that if // one of the two accumulator data items is INITVAL, then this // function sets *accum to the other one. static void fMMCombiner(MinAndMax *accum, const MinAndMax *val) { if ((accum->min.idx < 0) || (val->min.val < accum->min.val)) accum->min = val->min; if ((accum->max.idx < 0) || (val->max.val > accum->max.val)) accum->max = val->max; } static void fMMOutConverter(int2 *result, const MinAndMax *val) { result->x = val->min.idx; result->y = val->max.idx; }
NOTA:qui sono disponibili altri kernel di riduzione degli esempi.
Per eseguire un kernel di riduzione, il runtime RenderScript crea una o più
variabili chiamate elementi di dati dell'accumulatore per contenere lo stato del processo di riduzione. Il runtime RenderScript
sceglie il numero di elementi di dati dell'accumulatore in modo da massimizzare le prestazioni. Il tipo
di elementi di dati dell'accumulatore (accumType) è determinato dalla funzione
accumulatore del kernel. Il primo argomento di questa funzione è un puntatore a un elemento di dati
dell'accumulatore. Per impostazione predefinita, ogni elemento dati dell'accumulatore viene inizializzato a zero (come se
tramite memset
); tuttavia, puoi scrivere una funzione di inizializzazione per fare qualcosa
di diverso.
Esempio: nel kernel addint, gli elementi di dati dell'accumulatore (di tipo int
) vengono utilizzati per sommare i valori di input. Non esiste una funzione di inizializzazione, quindi ogni elemento dati dell'accumulatore viene inizializzato su
zero.
Esempio: nel kernel findMinAndMax, gli elementi di dati dell'accumulatore (di tipo MinAndMax
) vengono utilizzati per tenere traccia dei valori minimo e massimo trovati finora. Esiste una funzione di inizializzazione per impostare questi valori rispettivamente su LONG_MAX
e LONG_MIN
e per impostare le posizioni di questi valori su -1, indicando che i valori non sono effettivamente presenti nella parte (vuota) dell'input che è stata elaborata.
RenderScript chiama la funzione di accumulo una volta per ogni coordinata dell'input o degli input. In genere, la funzione deve aggiornare l'elemento di dati dell'accumulatore in qualche modo in base all'input.
Esempio:nel kernel addint, la funzione accumulatore aggiunge il valore di un elemento di input all'elemento di dati accumulatore.
Esempio: nel kernel findMinAndMax, la funzione accumulatore verifica se il valore di un elemento di input è minore o uguale al valore minimo registrato nell'elemento di dati accumulatore e/o maggiore o uguale al valore massimo registrato nell'elemento di dati accumulatore e aggiorna l'elemento di dati accumulatore di conseguenza.
Dopo che la funzione di accumulatore è stata chiamata una volta per ogni coordinata negli input, RenderScript deve combinare gli elementi di dati dell'accumulatore in un unico elemento di dati dell'accumulatore. A questo scopo, puoi scrivere una funzione combiner. Se la funzione di accumulatore ha un singolo input e nessun argomento speciale, non è necessario scrivere una funzione combinatore; RenderScript utilizzerà la funzione di accumulatore per combinare gli elementi di dati dell'accumulatore. Puoi comunque scrivere una funzione combiner se questo comportamento predefinito non è quello che vuoi.
Esempio:nel kernel addint non esiste una funzione combinatore, quindi verrà utilizzata la funzione accumulatore. Questo è il comportamento corretto, perché se dividiamo una raccolta di valori in due parti e sommiamo i valori in queste due parti separatamente, la somma di queste due somme è uguale alla somma dell'intera raccolta.
Esempio: nel kernel findMinAndMax, la funzione combiner verifica se il valore minimo registrato nell'elemento di dati dell'accumulatore "source" *val
è inferiore al valore minimo registrato nell'elemento di dati dell'accumulatore "destination" *accum
e aggiorna *accum
di conseguenza. Esegue un lavoro simile per il valore massimo. In questo modo, *accum
viene aggiornato allo stato che avrebbe avuto se tutti i valori di input fossero stati accumulati in
*accum
anziché alcuni in *accum
e altri in
*val
.
Dopo aver combinato tutti gli elementi di dati dell'accumulatore, RenderScript determina il risultato della riduzione da restituire a Java. A questo scopo, puoi scrivere una funzione outconverter. Non è necessario scrivere una funzione outconverter se vuoi che il valore finale degli elementi di dati dell'accumulatore combinato sia il risultato della riduzione.
Esempio: nel kernel addint, non esiste una funzione outconverter. Il valore finale degli elementi di dati combinati è la somma di tutti gli elementi dell'input, ovvero il valore che vogliamo restituire.
Esempio: nel kernel findMinAndMax, la funzione outconverter inizializza un valore di risultato int2
per contenere le posizioni dei valori minimo e massimo risultanti dalla combinazione di tutti gli elementi di dati dell'accumulatore.
Scrittura di un kernel di riduzione
#pragma rs reduce
definisce un kernel di riduzione specificando il nome e i nomi e i ruoli delle funzioni che lo compongono. Tutte queste funzioni devono essere
static
. Un kernel di riduzione richiede sempre una funzione accumulator
; puoi omettere alcune o tutte le altre funzioni, a seconda di ciò che vuoi che faccia il kernel.
#pragma rs reduce(kernelName) \ initializer(initializerName) \ accumulator(accumulatorName) \ combiner(combinerName) \ outconverter(outconverterName)
Il significato degli elementi in #pragma
è il seguente:
reduce(kernelName)
(obbligatorio): specifica che è in fase di definizione un kernel di riduzione. Un metodo Java riflessoreduce_kernelName
avvierà il kernel.initializer(initializerName)
(facoltativo): specifica il nome della funzione di inizializzazione per questo kernel di riduzione. Quando avvii il kernel, RenderScript chiama questa funzione una volta per ogni elemento di dati dell'accumulatore. La funzione deve essere definita nel seguente modo:static void initializerName(accumType *accum) { … }
accum
è un puntatore a un elemento di dati dell'accumulatore da inizializzare per questa funzione.Se non fornisci una funzione di inizializzazione, RenderScript inizializza ogni elemento dati dell'accumulatore a zero (come se fosse
memset
), comportandosi come se esistesse una funzione di inizializzazione di questo tipo:static void initializerName(accumType *accum) { memset(accum, 0, sizeof(*accum)); }
accumulator(accumulatorName)
(obbligatorio): specifica il nome della funzione di accumulo per questo kernel di riduzione. Quando avvii il kernel, RenderScript chiama questa funzione una volta per ogni coordinata negli input, per aggiornare un elemento dati accumulatore in qualche modo in base agli input. La funzione deve essere definita nel seguente modo:static void accumulatorName(accumType *accum, in1Type in1, …, inNType inN [, specialArguments]) { … }
accum
è un puntatore a un elemento di dati dell'accumulatore che questa funzione deve modificare.in1
fino ainN
sono uno o più argomenti che vengono compilati automaticamente in base agli input passati all'avvio del kernel, un argomento per input. La funzione di accumulo può facoltativamente accettare uno qualsiasi degli argomenti speciali.Un esempio di kernel con più input è
dotProduct
.combiner(combinerName)
(facoltativo): specifica il nome della funzione di combinazione per questo kernel di riduzione. Dopo che RenderScript chiama la funzione di accumulatore una volta per ogni coordinata negli input, chiama questa funzione tutte le volte necessarie per combinare tutti gli elementi di dati dell'accumulatore in un unico elemento di dati dell'accumulatore. La funzione deve essere definita nel seguente modo:
static void combinerName(accumType *accum, const accumType *other) { … }
accum
è un puntatore a un elemento di dati dell'accumulatore "destinazione" che questa funzione deve modificare.other
è un puntatore a un elemento di dati dell'accumulatore "origine" che questa funzione deve "combinare" in*accum
.NOTA:è possibile che
*accum
,*other
o entrambi siano stati inizializzati, ma non siano mai stati passati alla funzione di accumulo, ovvero che uno o entrambi non siano mai stati aggiornati in base ai dati di input. Ad esempio, nel kernel findMinAndMax, la funzione di combinazionefMMCombiner
verifica esplicitamente la presenza diidx < 0
perché indica un elemento di dati dell'accumulatore, il cui valore è INITVAL.Se non fornisci una funzione di combinazione, RenderScript utilizza la funzione di accumulatore al suo posto, comportandosi come se esistesse una funzione di combinazione simile a questa:
static void combinerName(accumType *accum, const accumType *other) { accumulatorName(accum, *other); }
Una funzione Combiner è obbligatoria se il kernel ha più di un input, se il tipo di dati di input non è uguale al tipo di dati dell'accumulatore o se la funzione di accumulo accetta uno o più argomenti speciali.
outconverter(outconverterName)
(facoltativo): specifica il nome della funzione outconverter per questo kernel di riduzione. Dopo che RenderScript combina tutti gli elementi di dati dell'accumulatore, chiama questa funzione per determinare il risultato della riduzione da restituire a Java. La funzione deve essere definita come segue:static void outconverterName(resultType *result, const accumType *accum) { … }
result
è un puntatore a un elemento di dati dei risultati (allocato ma non inizializzato dal runtime di RenderScript) che questa funzione deve inizializzare con il risultato della riduzione. resultType è il tipo di questo elemento di dati, che non deve necessariamente essere lo stesso di accumType.accum
è un puntatore all'elemento di dati dell'accumulatore finale calcolato dalla funzione combiner.Se non fornisci una funzione outconverter, RenderScript copia l'elemento di dati dell'accumulatore finale nell'elemento di dati del risultato, comportandosi come se esistesse una funzione outconverter che si presenta in questo modo:
static void outconverterName(accumType *result, const accumType *accum) { *result = *accum; }
Se vuoi un tipo di risultato diverso dal tipo di dati dell'accumulatore, la funzione outconverter è obbligatoria.
Tieni presente che un kernel ha tipi di input, un tipo di elemento di dati accumulatore e un tipo di risultato,
nessuno dei quali deve essere uguale. Ad esempio, nel
kernel findMinAndMax, il tipo
di input long
, il tipo di elemento di dati dell'accumulatore MinAndMax
e il tipo
di risultato int2
sono tutti diversi.
Cosa non puoi dare per scontato?
Non devi fare affidamento sul numero di elementi di dati dell'accumulatore creati da RenderScript per un determinato avvio del kernel. Non è garantito che due lanci dello stesso kernel con gli stessi input creino lo stesso numero di elementi di dati dell'accumulatore.
Non devi fare affidamento sull'ordine in cui RenderScript chiama le funzioni di inizializzazione, accumulatore e combinatore; potrebbe persino chiamarne alcune in parallelo. Non è garantito che due lanci dello stesso kernel con lo stesso input seguano lo stesso ordine. L'unica garanzia è che solo la funzione di inizializzazione vedrà un elemento di dati dell'accumulatore non inizializzato. Ad esempio:
- Non è garantito che tutti gli elementi di dati dell'accumulatore vengano inizializzati prima che venga chiamata la funzione dell'accumulatore, anche se verrà chiamata solo su un elemento di dati dell'accumulatore inizializzato.
- Non è garantito l'ordine in cui gli elementi di input vengono passati alla funzione di accumulatore.
- Non è garantito che la funzione di accumulo sia stata chiamata per tutti gli elementi di input prima che venga chiamata la funzione di combinazione.
Una conseguenza di ciò è che il kernel findMinAndMax non è deterministico: se l'input contiene più di un'occorrenza dello stesso valore minimo o massimo, non puoi sapere quale occorrenza troverà il kernel.
Che cosa devi garantire?
Poiché il sistema RenderScript può scegliere di eseguire un kernel in molti modi diversi, devi seguire determinate regole per assicurarti che il kernel si comporti nel modo desiderato. Se non segui queste regole, potresti ottenere risultati errati, un comportamento non deterministico o errori di runtime.
Le regole riportate di seguito spesso indicano che due elementi di dati dell'accumulatore devono avere "lo stesso valore". Che cosa significa? Dipende da cosa vuoi che faccia il kernel. Per una riduzione matematica come addint, di solito ha senso che "lo stesso" significhi uguaglianza matematica. Per una ricerca "scegli uno" come findMinAndMax ("trova la posizione dei valori di input minimi e massimi") in cui potrebbero esserci più occorrenze di valori di input identici, tutte le posizioni di un determinato valore di input devono essere considerate "uguali". Potresti scrivere un kernel simile a "trova la posizione dei valori di input minimo e massimo più a sinistra" in cui (ad esempio) un valore minimo nella posizione 100 è preferibile a un valore minimo identico nella posizione 200; per questo kernel, "lo stesso" significherebbe una posizione identica, non semplicemente un valore identico, e le funzioni di accumulatore e combinatore dovrebbero essere diverse da quelle di findMinAndMax.
La funzione di inizializzazione deve creare un valore di identità. ovvero, seI
e A
sono elementi di dati dell'accumulatore inizializzati dalla funzione di inizializzazione e I
non è mai stato passato alla funzione di accumulatore (ma A
potrebbe esserlo), allora
Esempio: nel kernel addint, un elemento di dati accumulatore viene inizializzato a zero. La funzione di combinazione per questo kernel esegue l'addizione; zero è il valore di identità per l'addizione.
Esempio:nel kernel findMinAndMax, un elemento di dati dell'accumulatore viene inizializzato
su INITVAL
.
fMMCombiner(&A, &I)
lasciaA
invariato, perchéI
èINITVAL
.fMMCombiner(&I, &A)
impostaI
suA
, perchéI
èINITVAL
.
Pertanto, INITVAL
è effettivamente un valore di identità.
La funzione di combinazione deve essere commutativa. ovvero,
se A
e B
sono elementi di dati dell'accumulatore inizializzati
dalla funzione di inizializzazione e che potrebbero essere stati passati alla funzione di accumulatore zero
o più volte, allora combinerName(&A, &B)
deve
impostare A
sullo stesso valore
che combinerName(&B, &A)
imposta B
.
Esempio:nel kernel addint, la funzione combiner aggiunge i due valori dell'elemento di dati dell'accumulatore; l'addizione è commutativa.
Esempio: nel kernel findMinAndMax,
fMMCombiner(&A, &B)
è uguale a
A = minmax(A, B)
e minmax
è commutativo, quindi
anche fMMCombiner
lo è.
La funzione di combinazione deve essere associativa. ovvero,
se A
, B
e C
sono
elementi di dati dell'accumulatore inizializzati dalla funzione di inizializzazione e che potrebbero essere stati passati
alla funzione di accumulatore zero o più volte, le due sequenze di codice seguenti devono
impostare A
sullo stesso valore:
combinerName(&A, &B); combinerName(&A, &C);
combinerName(&B, &C); combinerName(&A, &B);
Esempio:nel kernel addint, la funzione combiner aggiunge i due valori dell'elemento di dati dell'accumulatore:
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
L'addizione è associativa, quindi anche la funzione di combinazione.
Esempio: nel kernel findMinAndMax,
fMMCombiner(&A, &B)
A = minmax(A, B)
A = minmax(A, B) A = minmax(A, C) // Same as // A = minmax(minmax(A, B), C)
B = minmax(B, C) A = minmax(A, B) // Same as // A = minmax(A, minmax(B, C)) // B = minmax(B, C)
minmax
è associativa, quindi lo è anche fMMCombiner
.
La funzione di accumulo e la funzione di combinazione devono rispettare la regola di
piegatura di base. ovvero, se A
e B
sono elementi di dati dell'accumulatore, A
è stato
inizializzato dalla funzione di inizializzazione e potrebbe essere stato passato alla funzione di accumulatore
zero o più volte, B
non è stato inizializzato e args è
l'elenco degli argomenti di input e degli argomenti speciali per una determinata chiamata alla funzione di accumulatore,
allora le due sequenze di codice seguenti devono impostare A
sullo stesso valore:
accumulatorName(&A, args); // statement 1
initializerName(&B); // statement 2 accumulatorName(&B, args); // statement 3 combinerName(&A, &B); // statement 4
Esempio: nel kernel addint, per un valore di input V:
- L'affermazione 1 è uguale a
A += V
- L'affermazione 2 è uguale a
B = 0
- L'affermazione 3 è uguale a
B += V
, che è uguale aB = V
- L'affermazione 4 è uguale a
A += B
, che è uguale aA += V
Le istruzioni 1 e 4 impostano A
sullo stesso valore, quindi questo kernel rispetta la
regola di piegatura di base.
Esempio: nel kernel findMinAndMax, per un valore di input V alla coordinata X:
- L'affermazione 1 è uguale a
A = minmax(A, IndexedVal(V, X))
- L'affermazione 2 è uguale a
B = INITVAL
- L'affermazione 3 è uguale a
che, poiché B è il valore iniziale, è uguale aB = minmax(B, IndexedVal(V, X))
B = IndexedVal(V, X)
- L'affermazione 4 è uguale a
che è uguale aA = minmax(A, B)
A = minmax(A, IndexedVal(V, X))
Le istruzioni 1 e 4 impostano A
sullo stesso valore, quindi questo kernel rispetta la
regola di piegatura di base.
Chiamare un kernel di riduzione dal codice Java
Per un kernel di riduzione denominato kernelName definito nel file filename.rs
, esistono tre metodi riflessi nella 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);
Ecco alcuni esempi di chiamata al kernel addint:
Kotlin
val script = ScriptC_example(renderScript) // 1D array // and obtain answer immediately val input1 = intArrayOf(…) val sum1: Int = script.reduce_addint(input1).get() // Method 3 // 2D allocation // and do some additional work before obtaining answer val typeBuilder = Type.Builder(RS, Element.I32(RS)).apply { setX(…) setY(…) } val input2: Allocation = Allocation.createTyped(RS, typeBuilder.create()).also { populateSomehow(it) // fill in input Allocation with data } val result2: ScriptC_example.result_int = script.reduce_addint(input2) // Method 1 doSomeAdditionalWork() // might run at same time as reduction val sum2: Int = result2.get()
Java
ScriptC_example script = new ScriptC_example(renderScript); // 1D array // and obtain answer immediately int input1[] = …; int sum1 = script.reduce_addint(input1).get(); // Method 3 // 2D allocation // and do some additional work before obtaining answer Type.Builder typeBuilder = new Type.Builder(RS, Element.I32(RS)); typeBuilder.setX(…); typeBuilder.setY(…); Allocation input2 = createTyped(RS, typeBuilder.create()); populateSomehow(input2); // fill in input Allocation with data ScriptC_example.result_int result2 = script.reduce_addint(input2); // Method 1 doSomeAdditionalWork(); // might run at same time as reduction int sum2 = result2.get();
Il metodo 1 ha un argomento di input Allocation
per ogni argomento di input nella funzione di accumulatore del kernel. Il runtime RenderScript verifica che tutte le allocazioni di input
abbiano le stesse dimensioni e che il tipo Element
di ciascuna
delle allocazioni di input corrisponda a quello dell'argomento di input corrispondente del prototipo della funzione
dell'accumulatore. Se uno di questi controlli ha esito negativo, RenderScript genera un'eccezione. Il
kernel viene eseguito su ogni coordinata di queste dimensioni.
Il metodo 2 è uguale al metodo 1, tranne per il fatto che il metodo 2 accetta un argomento aggiuntivo sc
che può essere utilizzato per limitare l'esecuzione del kernel a un sottoinsieme delle coordinate.
Il metodo 3 è uguale al metodo 1, tranne per il fatto che
anziché accettare input di allocazione, accetta input di array Java. Si tratta di una comodità che
ti evita di dover scrivere codice per creare esplicitamente un'allocazione e copiarvi i dati
da un array Java. Tuttavia, l'utilizzo del metodo 3 anziché del metodo 1 non aumenta il
rendimento del codice. Per ogni array di input, il metodo 3 crea un'allocazione temporanea unidimensionale con il tipo Element
appropriato e setAutoPadding(boolean)
abilitato e copia l'array nell'allocazione come se fosse stato copiato con il metodo copyFrom()
appropriato di Allocation
. Quindi chiama il metodo 1, passando queste allocazioni temporanee.
NOTA:se la tua applicazione effettuerà più chiamate al kernel con lo stesso array o con array diversi delle stesse dimensioni e dello stesso tipo di elemento, potresti migliorare le prestazioni creando, compilando e riutilizzando esplicitamente le allocazioni, anziché utilizzando il metodo 3.
javaFutureType, il tipo restituito dei metodi di riduzione riflessi, è una classe statica nidificata riflessa all'interno della classe ScriptC_filename
. Rappresenta il risultato futuro di un'esecuzione
del kernel di riduzione. Per ottenere il risultato effettivo dell'esecuzione, chiama
il metodo get()
di quella classe, che restituisce un valore
di tipo javaResultType. get()
è sincrono.
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 viene determinato da resultType della funzione outconverter. A meno che resultType non sia un tipo senza segno (scalare, vettore o array), javaResultType è il tipo Java corrispondente. Se resultType è un tipo senza segno ed esiste un tipo Java con segno più grande, allora javaResultType è quel tipo Java con segno più grande; altrimenti, è il tipo Java corrispondente. Ad esempio:
- Se resultType è
int
,int2
oint[15]
, allora javaResultType èint
,Int2
oint[]
. Tutti i valori di resultType possono essere rappresentati da javaResultType. - Se resultType è
uint
,uint2
ouint[15]
, allora javaResultType èlong
,Long2
olong[]
. Tutti i valori di resultType possono essere rappresentati da javaResultType. - Se resultType è
ulong
,ulong2
, oulong[15]
, allora javaResultType èlong
,Long2
olong[]
. Esistono determinati valori di resultType che non possono essere rappresentati da javaResultType.
javaFutureType è il tipo di risultato futuro corrispondente a resultType della funzione outconverter.
- Se resultType non è un tipo di array, allora javaFutureType
è
result_resultType
. - Se resultType è un array di lunghezza Count con membri di tipo memberType,
allora javaFutureType è
resultArrayCount_memberType
.
Ad esempio:
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 è un tipo di oggetto (incluso un tipo di array), ogni chiamata
a javaFutureType.get()
sulla stessa istanza restituirà lo stesso
oggetto.
Se javaResultType non può rappresentare tutti i valori di tipo resultType e un
kernel di riduzione produce un valore non rappresentabile,
javaFutureType.get()
genera un'eccezione.
Metodo 3 e devecSiInXType
devecSiInXType è il tipo Java corrispondente a inXType dell'argomento corrispondente della funzione di accumulo. A meno che inXType non sia un tipo senza segno o un tipo di vettore, devecSiInXType è il tipo Java corrispondente. Se inXType è un tipo scalare senza segno, devecSiInXType è il tipo Java corrispondente direttamente al tipo scalare con segno delle stesse dimensioni. Se inXType è un tipo di vettore con segno, devecSiInXType è il tipo Java corrispondente direttamente al tipo di componente del vettore. Se inXType è un tipo di vettore senza segno, devecSiInXType è il tipo Java corrispondente direttamente al tipo scalare con segno della stessa dimensione del tipo di componente del vettore. Ad esempio:
- Se inXType è
int
, allora devecSiInXType èint
. - Se inXType è
int2
, allora devecSiInXType èint
. L'array è una rappresentazione appiattita: ha il doppio degli elementi scalari rispetto agli elementi vettoriali a due componenti dell'allocazione. È lo stesso modo in cui funzionano i metodicopyFrom()
diAllocation
. - Se inXType è
uint
, allora deviceSiInXType èint
. Un valore firmato nell'array Java viene interpretato come un valore non firmato con lo stesso pattern di bit nell'allocazione. È lo stesso modo in cui funzionano i metodicopyFrom()
diAllocation
. - Se inXType è
uint2
, allora deviceSiInXType èint
. Si tratta di una combinazione del modo in cui vengono gestitiint2
euint
: l'array è una rappresentazione piatta e i valori con segno dell'array Java vengono interpretati come valori Element senza segno di RenderScript.
Tieni presente che per il Metodo 3, i tipi di input vengono gestiti in modo diverso rispetto ai tipi di risultati:
- L'input vettoriale di uno script viene compresso sul lato Java, mentre il risultato vettoriale di uno script non lo è.
- L'input senza segno di uno script è rappresentato come un input con segno delle stesse dimensioni sul lato Java, mentre il risultato senza segno di uno script è rappresentato come un tipo con segno ampliato sul lato Java (tranne nel caso di
ulong
).
Altri kernel di riduzione degli esempi
#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]; }
Altri esempi di codice
Gli esempi BasicRenderScript, RenderScriptIntrinsic e Hello Compute dimostrano ulteriormente l'utilizzo delle API trattate in questa pagina.