Panoramica di RenderScript

RenderScript è un framework per l'esecuzione di attività ad alta intensità di calcolo con prestazioni elevate su Android. RenderScript è principalmente orientato all'utilizzo con il calcolo dati parallelo, sebbene anche i carichi di lavoro seriali possano trarne vantaggio. Il runtime di RenderScript carica in contemporanea il lavoro tra i processori disponibili su un dispositivo, ad esempio CPU multi-core e GPU. In questo modo puoi concentrarti sull'espressione degli algoritmi anziché sulla pianificazione del lavoro. RenderScript è particolarmente utile per le applicazioni che eseguono elaborazione di immagini, fotografia computazionale o visione artificiale.

Per iniziare a utilizzare RenderScript, ci sono due concetti principali che dovresti comprendere:

  • Il language stesso è un linguaggio derivato da C99 per la scrittura di codice di computing ad alte prestazioni. Scrivere un kernel RenderScript descrive come utilizzarlo per scrivere kernel computing.
  • L'API di controllo viene utilizzata per gestire la durata delle risorse RenderScript e controllare l'esecuzione del kernel. È disponibile in tre linguaggi diversi: Java, C++ in Android NDK e lo stesso linguaggio del kernel derivato da C99. Utilizzando RenderScript da codice Java e Single-Source RenderScript, vengono descritti rispettivamente la prima e la terza opzione.

Scrittura di un kernel RenderScript

Un kernel RenderScript in genere risiede in un file .rs nella directory <project_root>/src/rs. Ogni file .rs è chiamato script. Ogni script contiene il proprio set di kernel, funzioni e variabili. Uno script può contenere:

  • Una dichiarazione pragma (#pragma version(1)) che dichiara la versione del linguaggio kernel RenderScript utilizzato in questo script. Attualmente, 1 è l'unico valore valido.
  • 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 essere parte del pacchetto dell'applicazione e non in un progetto di libreria.
  • Zero o più funzioni richiamabili. Una funzione richiamabile è una funzione RenderScript a thread singolo che puoi richiamare dal 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ù ampia.
  • Zero o più script globali. Uno script globale è simile a una variabile globale in C. Puoi accedere agli script globali dal codice Java, che vengono spesso utilizzati per il passaggio di parametri ai kernel RenderScript. I valori globali degli script sono descritti più dettagliatamente qui.

  • Zero o più kernel computing. Un kernel di computing è una funzione o una raccolta di funzioni che il runtime di RenderScript può eseguire in parallelo su una raccolta di dati. Esistono due tipi di kernel di computing: kernel di mappatura (chiamati anche kernel foreach) e kernel di riduzione.

    Un kernel di mappatura è 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. In genere (ma non esclusivamente) viene utilizzato per trasformare una raccolta di input Allocations in una output Allocation uno Element alla volta.

    • Ecco un esempio di un semplice kernel di mappatura:

      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 la maggior parte degli aspetti, questa è identica a una funzione C standard. La proprietà RS_KERNEL applicata al prototipo della funzione specifica che la funzione è un kernel di mappatura RenderScript anziché una funzione richiamabile. L'argomento in viene compilato automaticamente in base all'input Allocation passato al lancio del kernel. Gli argomenti x e y sono discussi di seguito. Il valore restituito dal kernel viene scritto automaticamente nella posizione appropriata nell'output Allocation. Per impostazione predefinita, questo kernel viene eseguito su tutto l'input Allocation, con un'esecuzione della funzione kernel per Element nel campo Allocation.

      Un kernel di mappatura può avere uno o più Allocations di input, un singolo output Allocation o entrambi. Il runtime di RenderScript verifica che tutte le allocazioni di input e output abbiano le stesse dimensioni e che i tipi Element delle allocazioni di input e output corrispondano al prototipo del kernel. Se uno di questi controlli ha esito negativo, RenderScript genera un'eccezione.

      NOTA:prima di Android 6.0 (livello API 23), un kernel di mappatura non può avere più di un Allocation di input.

      Se hai bisogno di un valore Allocations di input o output maggiore rispetto al kernel, questi oggetti devono essere associati a script globali rs_allocation e l'accesso deve essere eseguito da un kernel o da una funzione invokabile tramite rsGetElementAt_type() o rsSetElementAt_type().

      NOTA:RS_KERNEL è una macro definita automaticamente da RenderScript per 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 sua funzione di accumulo viene eseguita una volta per ogni coordinata in quelle dimensioni. Viene utilizzato in genere (ma non esclusivamente) per "ridurre" una raccolta di input Allocations a un singolo valore.

    • Ecco un esempio di un semplice kernel di riduzione che somma la 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 funzione accumulator addintAccum, in questo esempio). Tutte queste funzioni devono essere static. Un kernel di riduzione richiede sempre una funzione accumulator; 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 avere almeno due argomenti. Il primo argomento (accum, in questo esempio) è un puntatore a un elemento dati accumulatore e il secondo (val, in questo esempio) viene compilato automaticamente in base all'input Allocation passato all'avvio del kernel. L'elemento dati accumulatore viene creato dal runtime RenderScript; per impostazione predefinita, è inizializzato su zero. Per impostazione predefinita, questo kernel viene eseguito su tutto l'input Allocation, con un'esecuzione della funzione di accumulatore per Element in Allocation. Per impostazione predefinita, il valore finale dell'elemento dati accumulatore viene trattato come risultato della riduzione e viene restituito a Java. Il runtime di RenderScript verifica che il tipo Element dell'allocazione di input corrisponda al prototipo della funzione accumulatore. Se non corrisponde, RenderScript genera un'eccezione.

      Un kernel di riduzione ha uno o più input Allocations ma nessun output Allocations.

      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 attuale utilizzando gli argomenti speciali x, y e z, che devono essere di tipo int o uint32_t. Questi argomenti sono facoltativi.

    Una funzione kernel di mappatura o una funzione accumulatore del kernel di riduzione può anche prendere l'argomento speciale facoltativo context di tipo rs_kernel_context. È necessario da una famiglia di API runtime utilizzate per eseguire query su determinate proprietà dell'esecuzione attuale, ad esempio rsGetDimX. L'argomento context è disponibile in Android 6.0 (livello API 23) e versioni successive.

  • Una funzione init() facoltativa. La funzione init() è un tipo speciale di funzione richiamabile che RenderScript esegue quando viene creata un'istanza dello script per la prima volta. Ciò consente di effettuare automaticamente alcuni calcoli al momento della creazione dello script.
  • Zero o più funzioni e globali degli script statici. Uno script statico globale equivale a uno script globale, con la differenza che non è possibile accedervi dal codice Java. Una funzione statica è una funzione C standard che può essere chiamata da qualsiasi kernel o funzione invokable nello script, ma non è esposta all'API Java. Se non è necessario accedere a uno script globale o a una funzione dal codice Java, ti consigliamo vivamente di dichiarare static.

Impostazione della precisione con rappresentazione in virgola mobile

Puoi controllare il livello richiesto di precisione con rappresentazione in virgola mobile in uno script. Ciò è utile se non è necessario lo standard completo IEEE 754-2008 (utilizzato per impostazione predefinita). I seguenti pragmi possono impostare un diverso livello di precisione con rappresentazione in virgola mobile:

  • #pragma rs_fp_full (valore predefinito se non viene specificato nulla): per le app che richiedono la precisione in virgola mobile come descritto dallo standard IEEE 754-2008.
  • #pragma rs_fp_relaxed: per le app che non richiedono la rigorosa conformità allo standard IEEE 754-2008 e possono tollerare una precisione minore. Questa modalità abilita il flush-to-zero per i denorm e l'arrotondamento verso lo zero.
  • #pragma rs_fp_imprecise: per le app che non hanno requisiti di precisione rigorosi. Questa modalità attiva tutti gli elementi in rs_fp_relaxed, insieme a quanto segue:
    • Le operazioni che restituiscono -0,0 possono invece 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ò potrebbe essere molto vantaggioso su alcune architetture a causa di ottimizzazioni aggiuntive disponibili solo con una precisione più semplice (come le istruzioni per la CPU SIMD).

Accesso alle API RenderScript da Java

Quando sviluppi un'applicazione per Android che utilizza RenderScript, puoi accedere alla relativa API da Java in uno dei due seguenti modi:

  • android.renderscript: le API in questo pacchetto della classe 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 consente di utilizzarle sui 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 che eseguono Android 2.3 (livello API 9) e versioni successive, indipendentemente dalle funzionalità di RenderScript che utilizzi. In questo modo l'applicazione può funzionare su più dispositivi rispetto alle API native (android.renderscript).
  • Alcune funzionalità di RenderScript non sono disponibili tramite le API Support Library.
  • Se usi le API Support Library, otterrai APK (probabilmente notevolmente) più grandi rispetto alle API native (android.renderscript).

Utilizzo delle API della libreria di supporto RenderScript

Per utilizzare le API Support Library RenderScript, devi configurare l'ambiente di sviluppo in modo da potervi accedere. Per utilizzare queste API sono necessari i seguenti strumenti SDK Android:

  • Android SDK Tools versione 22.2 o successive
  • SDK Build-tools per Android versione 18.1.0 o successiva

Tieni presente che a partire dalla versione 24.0.0 dell'SDK per Android, 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 Support Library RenderScript:

  1. Assicurati di avere installato la versione richiesta dell'SDK Android.
  2. Aggiorna le impostazioni per il processo di compilazione 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 di RenderScript al file:

      Alla moda

              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 bytecode da generare. Ti consigliamo di impostare questo valore sul livello API più basso in grado di fornire tutte le funzionalità che stai utilizzando e di impostare renderscriptSupportModeEnabled su true. I valori validi per questa impostazione sono qualsiasi valore intero compreso tra 11 e il livello API rilasciato più di recente. Se la versione minima dell'SDK specificata nel file 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 tornare a una versione compatibile se il dispositivo su cui è in esecuzione non supporta la versione di destinazione.
  3. 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 da codice Java o Kotlin

L'utilizzo di RenderScript dal codice Java o Kotlin si basa sulle classi API situate nel pacchetto android.renderscript o android.support.v8.renderscript. La maggior parte delle applicazioni segue lo stesso pattern di utilizzo di base:

  1. Inizializza un contesto RenderScript. Il contesto RenderScript, creato con create(Context), garantisce l'utilizzo di RenderScript e fornisce un oggetto per controllare la durata di tutti gli oggetti RenderScript successivi. Dovresti considerare la creazione di contesto come un'operazione potenzialmente a lunga esecuzione, dal momento che può creare risorse su diversi componenti hardware; se possibile, non dovrebbe trovarsi nel percorso critico di un'applicazione. In genere, un'applicazione ha un solo contesto RenderScript alla volta.
  2. Crea almeno un Allocation da passare a uno script. Un Allocation è un oggetto RenderScript che fornisce archiviazione per una quantità fissa di dati. I kernel negli script prendono gli oggetti Allocation come input e output e gli oggetti Allocation possono essere accessibili nei kernel utilizzando rsGetElementAt_type() e rsSetElementAt_type() se sono associati come globali di script. Gli oggetti Allocation consentono il passaggio di array dal codice Java al codice RenderScript e viceversa. Gli oggetti Allocation vengono in genere creati utilizzando createTyped() o createFromBitmap().
  3. Crea gli script necessari. Quando utilizzi RenderScript, sono disponibili due tipi di script:
    • ScriptC: si tratta degli script definiti dall'utente, come descritto nella sezione precedente Scrittura di un kernel RenderScript. Ogni script ha una classe Java rifletta dal compilatore RenderScript per semplificare l'accesso allo script dal codice Java; questa classe ha il nome ScriptC_filename. Ad esempio, se il kernel di mappatura sopra riportato si trovava in invert.rs e un contesto RenderScript era già presente in mRenderScript, 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: sono kernel RenderScript integrati per le operazioni comuni, come la sfocatura gaussiana, la convoluzione e la combinazione delle immagini. Per ulteriori informazioni, vedi le sottoclassi di ScriptIntrinsic.
  4. Compilare le allocazioni con i dati. Ad eccezione delle allocazioni create con createFromBitmap(), un'allocazione viene compilata con dati vuoti quando viene creata per la prima volta. Per compilare un'allocazione, usa uno dei metodi "copy" in Allocation. I metodi di tipo "copy" sono sincroni.
  5. Imposta gli script globali necessari. Puoi impostare i valori globali utilizzando i metodi nella stessa classe ScriptC_filename denominata set_globalname. Ad esempio, per impostare una variabile int denominata threshold, utilizza il metodo Java set_threshold(int); per impostare una variabile rs_allocation denominata lookup, utilizza il metodo Java set_lookup(Allocation). I metodi set sono asincroni.
  6. Avvia i kernel appropriati e le funzioni richiamabili.

    I metodi per avviare un determinato kernel si riflettono nella stessa classe ScriptC_filename con i metodi denominati forEach_mappingKernelName() o reduce_reductionKernelName(). Questi lanci sono asincroni. A seconda degli argomenti del kernel, il metodo prevede una o più allocazioni, che devono tutte avere le stesse dimensioni. Per impostazione predefinita, un kernel viene eseguito su ogni coordinata in quelle dimensioni; per eseguire un kernel su un sottoinsieme di queste coordinate, passa un Script.LaunchOptions appropriato come ultimo argomento al metodo forEach o reduce.

    Avvia le funzioni richiamabili utilizzando i metodi invoke_functionName riflette nella stessa classe ScriptC_filename. Questi lanci sono asincroni.

  7. Recupera i dati dagli oggetti Allocation e dagli oggetti javaFutureType. Per accedere ai dati da un Allocation dal codice Java, devi copiare i dati in Java utilizzando uno dei metodi "copy" in Allocation. Per ottenere il risultato di un kernel di riduzione, devi usare il metodo javaFutureType.get(). I metodi "copy" e get() sono sincroni.
  8. Riduci il contesto di RenderScript. Puoi eliminare il contesto RenderScript con destroy() o consentendo la garbage collection dell'oggetto di contesto RenderScript. In questo modo, qualsiasi ulteriore utilizzo di qualsiasi oggetto appartenente a quel contesto genera un'eccezione.

Modello di esecuzione asincrono

I metodi forEach, invoke, reduce e set riportati sono asincroni: ciascuno può tornare a Java prima di completare l'azione richiesta. Tuttavia, le singole azioni vengono serializzate nell'ordine in cui vengono lanciate.

La classe Allocation fornisce metodi di "copia" per copiare i dati da e dalle allocazioni. Il metodo di "copy" è sincrono ed è serializzato rispetto a qualsiasi azione asincrona superiore corrispondente alla stessa allocazione.

Le classi javaFutureType riflesse forniscono un metodo get() per ottenere il risultato di una riduzione. get() è sincrono ed è 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 chiamati semplicemente "kernel" per concisione. 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 richiesti più avvii del kernel. Il vantaggio è duplice: un codice più leggibile, perché mantiene l'implementazione di un algoritmo in un solo linguaggio, e un codice potenzialmente più veloce, a causa del minor numero di transizioni tra Java e RenderScript in più lanci del kernel.

In RenderScript a origine singola, scrivi i kernel come descritto in Scrittura di un kernel RenderScript. Scrivi quindi una funzione richiamabile che chiama rsForEach() per avviarla. L'API prende una funzione kernel come primo parametro, seguita dalle allocazioni di input e output. Un'API simile rsForEachWithOptions() utilizza un argomento aggiuntivo di tipo rs_script_call_t, che specifica un sottoinsieme di elementi dalle allocazioni di input e output che la funzione kernel deve elaborare.

Per avviare il calcolo di RenderScript, richiama la funzione invokable da Java. Segui i passaggi descritti in Utilizzare RenderScript dal codice Java. Nel passaggio per avviare i kernel appropriati, chiama la funzione invokable utilizzando invoke_function_name(), che avvierà l'intero calcolo, incluso l'avvio dei kernel.

Spesso sono necessarie allocazioni per salvare e passare i risultati intermedi da un lancio di kernel all'altro. Puoi crearle utilizzando rsCreateAllocation(). Una forma facile da usare di questa API è rsCreateAllocation_<T><W>(…), dove T è il tipo di dati per un elemento e W è la larghezza del vettore dell'elemento. L'API prende le dimensioni nelle dimensioni X, Y e Z come argomenti. Per le allocazioni 1D o 2D, è possibile omettere la dimensione della dimensione Y o Z. 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 risorse il prima possibile.

La sezione Scrittura di un kernel RenderScript contiene un kernel di esempio che inverte un'immagine. L'esempio riportato di seguito espande questa impostazione per applicare più di un effetto a un'immagine, utilizzando il RenderScript di origine singola. Include un altro kernel, greyscale, che trasforma un'immagine a colori in bianco e nero. Una funzione richiamabile process() applica quindi questi due kernel consecutivamente a un'immagine di input e produce un'immagine di output. Le allocazioni sia dell'input che dell'output vengono passate 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 coinvolge due lanci del kernel può essere implementato completamente nel linguaggio RenderScript stesso. Senza RenderScript a singola origine, dovresti avviare entrambi i kernel dal codice Java, separando gli avvii dalle definizioni del kernel e rendendo più difficile la comprensione dell'intero algoritmo. Non solo è più facile leggere il codice RenderScript a sorgente singola, ma elimina anche la transizione tra Java e lo script tra i lanci del kernel. Alcuni algoritmi iterativi possono avviare i kernel centinaia di volte, rendendo considerevole l'overhead.

Globali script

Uno script globale è una variabile globale normale nonstatic in un file di script (.rs). Per uno script globale denominato var definito nel file filename.rs, ci sarà un metodo get_var riportato nella classe ScriptC_filename. A meno che il valore globale non sia const, ci sarà 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 sia nello script. In caso contrario, il valore iniziale è zero.
  • Accede a var all'interno dello script legge e scrive il relativo valore dello script.
  • Il metodo get_var legge il valore Java.
  • Il metodo set_var (se esistente) scrive immediatamente il valore Java e scrive il valore dello script in modo asincrono.

NOTA: questo significa che, ad eccezione di qualsiasi inizializzatore statico nello script, i valori scritti in un elemento globale dall'interno di uno script non sono visibili in Java.

Riduzione dei kernel in profondità

La riduzione è il processo di combinazione di una raccolta di dati in un singolo valore. Questa è una primitiva utile nella programmazione parallela, con applicazioni come le seguenti:

  • calcolando la somma o il prodotto su tutti i dati
  • di operazioni logiche (and, or, xor) su tutti i dati
  • trovando il valore minimo o massimo all'interno dei dati
  • Cercare 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 efficienti scritti dall'utente. Puoi avviare i kernel di riduzione su input con 1, 2 o 3 dimensioni.

Un esempio riportato sopra mostra un semplice kernel di riduzione addint. Ecco un kernel di riduzione findMinAndMax più complicato che trova le località dei valori long minimo e massimo 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 puoi trovare altri kernel di riduzione di esempio.

Per eseguire un kernel di riduzione, il runtime RenderScript crea una o più variabili chiamate elementi di dati accumulatore per conservare lo stato del processo di riduzione. Il runtime di RenderScript sceglie il numero di elementi dati di accumulatori in modo da massimizzare le prestazioni. Il tipo degli elementi di dati dell'accumulatore (accumType) è determinato dalla funzione accumulatore del kernel: il primo argomento per quella funzione è un puntatore a un elemento di dati accumulatore. Per impostazione predefinita, ogni elemento dati accumulatore viene inizializzato su zero (come se se fosse 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, pertanto ogni elemento di dati dell'accumulatore è 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 rilevati fino a quel momento. È disponibile una funzione inizializzatore per impostarli rispettivamente su LONG_MAX e LONG_MIN e per impostare le posizioni di questi valori su -1, a indicare che i valori non sono effettivamente presenti nella parte (vuota) dell'input che è stata elaborata.

RenderScript chiama la funzione di accumulatore una volta per ogni coordinata negli input. Di solito, la funzione dovrebbe aggiornare l'elemento dati 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 è inferiore o uguale al valore minimo registrato nell'elemento dati accumulatore e/o maggiore o uguale al valore massimo registrato nell'elemento di dati accumulatore, quindi aggiorna la voce di dati accumulatore di conseguenza.

Dopo che la funzione accumulatore è stata richiamata una volta per ogni coordinata negli input, RenderScript deve combinare gli elementi di dati accumulatore in un unico elemento di dati accumulatore. Per farlo, puoi scrivere una funzione combiner. Se la funzione accumulatore ha un singolo input e non sono argomenti speciali, non è necessario scrivere una funzione di combinazione; RenderScript utilizzerà la funzione accumulatore per combinare gli elementi dati accumulatori. Puoi comunque scrivere una funzione combinatore se questo comportamento predefinito non è quello desiderato.

Esempio: nel kernel addint non esiste una funzione combinatore, quindi verrà utilizzata la funzione di accumulatore. Questo è il comportamento corretto, perché se dividiamo una raccolta di valori in due parti e i valori in queste due parti vengono sommati separatamente, la somma di queste due somme equivale ad aggiungere l'intera raccolta.

Esempio: nel kernel findMinAndMax, la funzione combinatore controlla se il valore minimo registrato nell'elemento dati dell'accumulatore di origine *val è inferiore al valore minimo registrato nell'elemento dati dell'accumulatore "destination" *accum e aggiorna di conseguenza *accum. Funziona in modo simile per il valore massimo. Questo aggiorna *accum 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 per tornare in Java. puoi scrivere una funzione di convertitore esterno per farlo. Non è necessario scrivere una funzione di outconverter se desideri che il valore finale degli elementi di dati accumulatori combinati sia il risultato della riduzione.

Esempio: nel kernel addint non è presente una funzione di 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 località dei valori minimo e massimo derivanti 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 suo nome, nonché 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 corso la definizione di un kernel di riduzione. Un metodo Java reduce_kernelName riflesso avvierà il kernel.
  • initializer(initializerName) (facoltativo): specifica il nome della funzione di inizializzazione per questo kernel di riduzione. All'avvio del kernel, RenderScript chiama questa funzione una volta per ogni elemento dati accumulatore. La funzione deve essere definita come segue:

    static void initializerName(accumType *accum) { … }

    accum è un puntatore a un elemento dati accumulatore per l'inizializzazione di questa funzione.

    Se non fornisci una funzione di inizializzazione, RenderScript inizializza ogni elemento di dati dell'accumulatore su zero (come se fosse memset), comportandosi come se ci fosse una funzione di inizializzazione simile alla seguente:

    static void initializerName(accumType *accum) {
      memset(accum, 0, sizeof(*accum));
    }
  • accumulator(accumulatorName) (obbligatorio): specifica il nome della funzione di accumulatore per questo kernel di riduzione. Quando avvii il kernel, RenderScript chiama questa funzione una volta per ogni coordinata negli input, per aggiornare in qualche modo un dato accumulatore in base agli input. La funzione deve essere definita come segue:

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

    accum è un puntatore a un elemento dati accumulatore da modificare con questa funzione. Da in1 a inN sono uno o più argomenti che vengono compilati automaticamente in base agli input passati al lancio del kernel, un argomento per input. La funzione di accumulatore può facoltativamente accettare uno qualsiasi degli argomenti speciali.

    Un kernel di esempio con più input è dotProduct.

  • combiner(combinerName)

    (facoltativo): specifica il nome della funzione combinatore per questo kernel di riduzione. Dopo che RenderScript chiama la funzione accumulatore una volta per ogni coordinata negli input, la chiama tutte le volte necessarie per combinare tutti gli elementi di dati accumulatori in un unico dato di dati accumulatore. La funzione deve essere definita come segue:

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

    accum è un puntatore a un elemento dati accumulatore "destinazione" che questa funzione può modificare. other è un puntatore a un elemento dati dell'accumulatore "origine" per questa funzione in modo da "combinare" in *accum.

    NOTA: è possibile che *accum, *other o entrambi siano stati inizializzati, ma non siano mai stati passati alla funzione di accumulatore; ovvero uno o entrambi non sono mai stati aggiornati in base ai dati di input. Ad esempio, nel kernel findMinAndMax, la funzione combinatore fMMCombiner controlla esplicitamente idx < 0 perché questo indica un dato accumulatore il cui valore è INITVAL.

    Se non fornisci una funzione combinatore, RenderScript utilizza la funzione di accumulatore al suo posto, comportandosi come se ci fosse una funzione combinatore simile alla seguente:

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

    Una funzione combinatore è obbligatoria se il kernel ha più di un input, se il tipo di dati di input non è identico al tipo di dati accumulatore o se la funzione accumulatore accetta uno o più argomenti speciali.

  • outconverter(outconverterName) (facoltativo): specifica il nome della funzione di outconverter per questo kernel di riduzione. Dopo aver combinato tutti gli elementi di dati dell'accumulatore, RenderScript chiama questa funzione per determinare il risultato della riduzione per tornare in Java. La funzione deve essere definita come segue:

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

    result è un puntatore a un elemento dati dei risultati (allocato ma non inizializzato dal runtime di RenderScript) affinché questa funzione venga inizializzata con il risultato della riduzione. resultType è il tipo di elemento dati, che non deve essere necessariamente uguale a accumType. accum è un puntatore all'elemento dati dell'accumulatore finale calcolato dalla funzione combinata.

    Se non fornisci una funzione di outconverter, RenderScript copia l'elemento di dati dell'accumulatore finale nell'elemento di dati dei risultati, comportandosi come se esistesse una funzione di outconverter simile a questa:

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

    Se vuoi un tipo di risultato diverso dal tipo di dati accumulatore, la funzione di outconverter è obbligatoria.

Tieni presente che un kernel ha tipi di input, un tipo di elemento 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 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 dati di accumulatori creati da RenderScript per un lancio del kernel specifico. Non vi è alcuna garanzia che due avvii dello stesso kernel con gli stessi input creino lo stesso numero di elementi di dati accumulatori.

Non devi fare affidamento sull'ordine in cui RenderScript chiama le funzioni inizializzatore, accumulatore e combinatore; potrebbe persino chiamarle alcune in parallelo. Non c'è alcuna garanzia che due avvii dello stesso kernel con lo stesso input seguiranno lo stesso ordine. L'unica garanzia è che solo la funzione di inizializzazione vedrà mai un dato accumulatore non inizializzato. Ecco alcuni esempi:

  • Non vi è alcuna garanzia che tutti gli elementi di dati accumulatori vengano inizializzati prima che venga richiamata la funzione accumulatore, anche se verrà richiamata solo su un elemento dati accumulatore inizializzato.
  • Non esiste alcuna garanzia dell'ordine in cui gli elementi di input vengono passati alla funzione accumulatore.
  • Non vi è alcuna garanzia che la funzione accumulatore sia stata chiamata per tutti gli elementi di input prima che venga chiamata la funzione combinatore.

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 hai modo di sapere quale occorrenza troverà il kernel.

Cosa devi garantire?

Poiché il sistema RenderScript può scegliere di eseguire un kernel in molti modi diversi, devi seguire alcune regole per assicurarti che il kernel si comporti nel modo desiderato. Se non segui queste regole, potresti ricevere risultati errati, comportamenti non deterministici o errori di runtime.

Le regole riportate di seguito spesso prevedono che due elementi dati accumulatori debbano avere "lo stesso valore". Che cosa significa? Dipende da ciò che vuoi che faccia il kernel. Per una riduzione matematica come addint, di solito ha senso che "lo stesso" indichi l'uguaglianza matematica. Per una ricerca "scegli qualsiasi" come findMinAndMax ("trova la località dei valori di input minimo e massimo") in cui potrebbero esserci più occorrenze di valori di input identici, tutte le località di un determinato valore di input devono essere considerate "uguali". Potresti scrivere un kernel simile per trovare la località dei valori di input minimo e massimo a sinistra dove (ad esempio) un valore minimo nella posizione 100 è preferito rispetto a un valore minimo identico nella località 200; per questo kernel, "lo stesso" significherebbe una località identica, non semplicemente valore identico, e le funzioni di accumulatore e combinatore dovrebbero essere diverse da quelle di MinAndMax.

La funzione di inizializzazione deve creare un valore di identità. Vale a dire, se I e A sono elementi di dati accumulatori inizializzati dalla funzione di inizializzazione e I non è mai stato passato alla funzione accumulatore (ma A potrebbe averlo fatto),
  • combinerName(&A, &I) deve lasciare A lo stesso
  • combinerName(&I, &A) deve lasciare I lo stesso di A

Esempio: nel kernel addint, un elemento di dati accumulatore viene inizializzato su zero. La funzione combinatore per questo kernel esegue l'aggiunta; zero è il valore dell'identità da aggiungere.

Esempio: nel kernel findMinAndMax, un elemento di dati accumulatore viene inizializzato in INITVAL.

  • fMMCombiner(&A, &I) lascia lo stesso A, perché I è INITVAL.
  • fMMCombiner(&I, &A) imposta I su A perché I è INITVAL.

Pertanto, INITVAL è effettivamente un valore dell'identità.

La funzione combinatore deve essere commutativa. In altre parole, se A e B sono elementi dati accumulatori inizializzati dalla funzione di inizializzazione e che possono essere stati passati alla funzione di accumulatore zero o più volte, combinerName(&A, &B) deve impostare A sullo stesso valore che combinerName(&B, &A) imposta B.

Esempio: nel kernel addint, la funzione combinatore aggiunge i valori degli elementi di dati dei due accumulatori; l'addizione è commutativa.

Esempio: nel kernel findMinAndMax, fMMCombiner(&A, &B) è uguale a A = minmax(A, B), mentre minmax è commutativo, quindi anche fMMCombiner lo è.

La funzione combinatore deve essere associativa. In altre parole, se A, B e C sono elementi di dati accumulatori inizializzati dalla funzione di inizializzazione e che possono 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 combinatore aggiunge i due valori degli elementi dati di accumulatori:

  • 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 lo è anche la funzione combinatore.

Esempio: nel kernel findMinAndMax,

fMMCombiner(&A, &B)
è uguale a
A = minmax(A, B)
Quindi le due sequenze vengono

  • 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, così come lo è anche fMMCombiner.

La funzione di accumulatore e la funzione combinatore devono rispettare la regola pieghevole di base. In altre parole, se A e B sono elementi di dati accumulatori, A è stato inizializzato dalla funzione inizializzatore e può essere stato passato alla funzione di accumulatore zero o più volte, B non è stato inizializzato e args è l'elenco di argomenti di input e argomenti speciali per una determinata chiamata alla funzione accumulatore, le seguenti due sequenze di codici 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 dell'addint, per un valore di input V:

  • L'istruzione 1 è uguale a A += V
  • L'istruzione 2 è uguale a B = 0
  • L'istruzione 3 è uguale a B += V, che equivale a B = V
  • L'istruzione 4 è uguale a A += B, che equivale a A += V

Le istruzioni 1 e 4 impostano A sullo stesso valore, quindi questo kernel obbedisce alla regola di piegatura di base.

Esempio:nel kernel findMinAndMax, per un valore di input V nella coordinata X:

  • L'istruzione 1 è uguale a A = minmax(A, IndexedVal(V, X))
  • L'istruzione 2 è uguale a B = INITVAL
  • L'istruzione 3 è uguale a
    B = minmax(B, IndexedVal(V, X))
    
    che, poiché B è il valore iniziale, è uguale a
    B = IndexedVal(V, X)
    
  • L'istruzione 4 è uguale a
    A = minmax(A, B)
    
    che equivale a
    A = minmax(A, IndexedVal(V, X))
    

Le istruzioni 1 e 4 impostano A sullo stesso valore, quindi questo kernel obbedisce alla regola di piegatura di base.

Chiamata a un kernel di riduzione dal codice Java

Per un kernel di riduzione denominato kernelName definito nel file filename.rs, ci sono 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 Allocation di input per ogni argomento di input nella funzione accumulatore del kernel. Il runtime di RenderScript verifica che tutte le allocazioni di input abbiano le stesse dimensioni e che il tipo Element di ciascuna allocazioni di input corrisponda a quello dell'argomento di input corrispondente del prototipo della funzione accumulatore. Se uno di questi controlli non riesce, RenderScript genera un'eccezione. Il kernel viene eseguito su ogni coordinata in queste dimensioni.

Il metodo 2 è uguale al metodo 1, ad eccezione del fatto che il metodo 2 utilizza un argomento aggiuntivo sc che può essere utilizzato per limitare l'esecuzione del kernel a un sottoinsieme di coordinate.

Il metodo 3 è uguale al metodo 1, tranne per il fatto che, anziché prendere input di allocazione, richiede input di array Java. Questa è una comodità che ti consente di evitare di scrivere codice per creare esplicitamente un'allocazione e copiare dati al suo interno da un array Java. Tuttavia, l'utilizzo del metodo 3 anziché del metodo 1 non aumenta le prestazioni del codice. Per ogni array di input, il metodo 3 crea un'allocazione monodimensionale temporanea con il tipo Element appropriato e setAutoPadding(boolean) abilitato e copia l'array nell'allocazione come se fosse utilizzato il metodo copyFrom() appropriato di Allocation. Quindi chiama il metodo 1, passando le allocazioni temporanee.

NOTA: se l'applicazione effettua più chiamate kernel con lo stesso array o con array diversi delle stesse dimensioni e dello stesso tipo di elemento, puoi migliorare le prestazioni creando, completando e riutilizzando le allocazioni in modo esplicito, anziché utilizzare il metodo 3.

javaFutureType, il tipo restituito dei metodi di riduzione riflessa, è una classe nidificata statica 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() è sincrona.

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 è determinato da resultType della funzione outconverter. A meno che resultType non sia un tipo non firmato (scalare, vettoriale o array), javaResultType è il tipo Java direttamente corrispondente. Se resultType è un tipo non firmato ed esiste un tipo con firma Java più grande, javaResultType è il tipo con firma Java più grande, altrimenti è il tipo Java direttamente corrispondente. Ecco alcuni esempi:

  • Se resultType è int, int2 o int[15], allora javaResultType è int, Int2 o int[]. Tutti i valori di resultType possono essere rappresentati da javaResultType.
  • Se resultType è uint, uint2 o uint[15], allora javaResultType è long, Long2 o long[]. Tutti i valori di resultType possono essere rappresentati da javaResultType.
  • Se resultType è ulong, ulong2 o ulong[15], allora javaResultType è long, Long2 o long[]. Alcuni valori di resultType non possono essere rappresentati da javaResultType.

javaFutureType è il tipo di risultato futuro corrispondente al resultType della funzione outconverter.

  • Se resultType non è un tipo di array, javaFutureType è result_resultType.
  • Se resultType è un array di lunghezza Count con membri di tipo memberType, javaFutureType è resultArrayCount_memberType.

Ecco alcuni esempi:

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() nella 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 accumulatore. A meno che inXType non sia un tipo non firmato o un tipo vettoriale, devecSiInXType è il tipo Java direttamente corrispondente. Se inXType è un tipo scalare non firmato, devecSiInXType è il tipo Java direttamente corrispondente al tipo scalare firmato della stessa dimensione. Se inXType è un tipo vettoriale firmato, devecSiInXType è il tipo Java direttamente corrispondente al tipo di componente vettoriale. Se inXType è un tipo vettoriale non firmato, devecSiInXType è il tipo Java direttamente corrispondente al tipo scalare firmato delle stesse dimensioni del tipo di componente vettoriale. Ecco alcuni esempi:

  • Se inXType è int, devecSiInXType è int.
  • Se inXType è int2, devecSiInXType è int. L'array è una rappresentazione lineare semplificata: ha il doppio di elementi scalari rispetto all'allocazione ha elementi vettori a due componenti. È come funzionano i metodi copyFrom() di Allocation.
  • Se inXType è uint, deviceSiInXType è int. Un valore con segno nell'array Java viene interpretato come un valore non firmato dello stesso bitpattern nell'allocazione. È lo stesso funzionamento dei metodi copyFrom() di Allocation.
  • Se inXType è uint2, deviceSiInXType è int. Questa è una combinazione della modalità di gestione di int2 e uint: l'array è una rappresentazione bidimensionale e i valori con segno di array Java sono interpretati come valori di elemento non firmato 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 è bidimensionale sul lato Java, mentre il risultato vettoriale di uno script non lo è.
  • L'input non firmato di uno script è rappresentato come input con segno della stessa dimensione sul lato Java, mentre il risultato non firmato di uno script è rappresentato come un tipo con segno ampliato sul lato Java (tranne nel caso di ulong).

Altri kernel di riduzione di esempio

#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.