Panoramica di RenderScript

RenderScript è un framework per l'esecuzione di attività ad alta intensità di calcolo ad alte prestazioni su Android. RenderScript è principalmente orientato all'utilizzo con il calcolo parallelo dei dati, anche se anche i carichi di lavoro seriali possono trarre vantaggio. Il runtime RenderScript esegue in parallelo il lavoro su più processori disponibili su un dispositivo, ad esempio 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 visione artificiale.

Per iniziare a utilizzare RenderScript, devi conoscere due concetti principali:

  • Il linguaggio stesso è un linguaggio derivato da C99 per la scrittura di codice di calcolo ad alte prestazioni. L'articolo Scrivere un kernel RenderScript descrive come utilizzarlo per scrivere kernel di calcolo.
  • L'API di controllo 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 stesso derivato da C99. Utilizzare RenderScript dal codice Java e RenderScript a sorgente singola descrivono rispettivamente la prima e la terza opzione.

Scrivere un kernel RenderScript

Un kernel RenderScript in genere si trova 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, 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 far parte del pacchetto dell'applicazione e non di un progetto della libreria.
  • Zero o più funzioni invocabili. Una funzione invocabile è una funzione RenderScript a thread singolo che puoi chiamare dal codice Java con argomenti arbitrari. Spesso sono utili per la configurazione iniziale o per i calcoli seriali all'interno di una pipeline di elaborazione più grande.
  • Zero o più variabili globali dello script. Una variabile globale dello script è simile a una variabile globale in C. Puoi accedere alle variabili globali dello script dal codice Java e queste vengono spesso utilizzate per il passaggio dei parametri ai kernel di RenderScript. Le variabili globali dello script sono spiegate più dettagliatamente qui.

  • Zero o più kernel di calcolo. Un kernel di calcolo è una funzione o una raccolta di funzioni che puoi indicare al runtime RenderScript di eseguire in parallelo su una raccolta di dati. Esistono due tipi di kernel di calcolo: 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 utilizzata per trasformare una raccolta di input Allocations in un output Allocation uno Element alla volta.

    • Ecco un esempio di kernel di mappatura semplice:

      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, è 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 invocabile. 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 dell'output Allocation. Per impostazione predefinita, questo kernel viene eseguito sull'intero input Allocation, con un'esecuzione della funzione del kernel per Element in Allocation.

      Un kernel di mappatura può avere uno o più input Allocations, un singolo output Allocation o entrambi. Il codice di 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 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 input Allocation.

      Se hai bisogno di più Allocations di input o output rispetto a quelli del kernel, questi oggetti devono essere associati alle variabili globali dello script rs_allocation e accessibili da un kernel o da una funzione invocabile 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 inputAllocations delle stesse dimensioni. Per impostazione predefinita, la sua funzione di accumulo viene eseguita una volta per ogni coordinata in queste dimensioni. In genere (ma non esclusivamente) viene utilizzato per "ridurre" una raccolta di input Allocations a un singolo valore.

    • Ecco un esempio di un semplice kernel di riduzione che somma il 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 costituiscono (una funzione accumulator addintAccum, in questo esempio). Tutte queste funzioni devono essere static. Un kernel di riduzione richiede sempre una funzione accumulator; può avere anche altre funzioni, a seconda di cosa 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'input Allocation passato al lancio del kernel. L'elemento dati dell'accumulatore viene creato dal runtime RenderScript. Per impostazione predefinita, viene inizializzato a zero. Per impostazione predefinita, questo kernel viene eseguito sull'intero input Allocation, con un'esecuzione della funzione di accumulatore per Element in Allocation. Per impostazione predefinita, il valore finale dell'elemento dati dell'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 di accumulo. 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 descritti 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 di accumulo del kernel di riduzione può accedere alle coordinate dell'esecuzione corrente utilizzando gli argomenti speciali x, y e z, che devono essere di tipo int o uint32_t. Questi argomenti sono facoltativi.

    Una funzione del kernel di mappatura o una funzione di accumulo del kernel di riduzione può anche accettare l'argomento speciale facoltativo context di tipo rs_kernel_context. È necessario per una famiglia di API di runtime utilizzate per eseguire query su determinate proprietà dell'esecuzione corrente, 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 invocabile che viene eseguita da RenderScript al primo caricamento dello script. In questo modo, alcuni calcoli vengono eseguiti automaticamente durante la creazione dello script.
  • Zero o più funzioni e variabili globali di script statiche. Un parametro globale dello script statico è equivalente a un parametro globale dello script, tranne per il fatto che non è possibile accedervi dal codice Java. Una funzione statica è una funzione C standard che può essere chiamata da qualsiasi kernel o funzione invocabile nello script, ma non è esposta all'API Java. Se non è necessario accedere a una funzione o a una variabile globale dello script dal codice Java, è vivamente consigliato 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 è richiesto 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 la precisione in virgola mobile descritta dallo standard IEEE 754-2008.
  • #pragma rs_fp_relaxed: per le app che non richiedono la conformità rigorosa allo standard IEEE 754-2008 e possono tollerare una precisione inferiore. Questa modalità attiva l'azzeramento per le denorme e la arrotondamento verso zero.
  • #pragma rs_fp_imprecise: per le app che non hanno requisiti di precisione rigorosi. Questa modalità attiva tutto ciò che è disponibile in rs_fp_relaxed, oltre a quanto segue:
    • Le operazioni che hanno 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 utile su alcune architetture a causa di ottimizzazioni aggiuntive disponibili solo con una precisione ridotta (ad esempio le istruzioni SIMD della CPU).

Accesso alle API RenderScript da Java

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

  • 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 di 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 della libreria di supporto, la parte di RenderScript della tua applicazione sarà compatibile con i dispositivi con Android 2.3 (livello API 9) e versioni successive, indipendentemente dalle funzionalità di RenderScript che utilizzi. In questo modo, la tua applicazione può funzionare su più dispositivi rispetto all'utilizzo delle 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 (eventualmente notevolmente) più grandi rispetto a quelli 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 potervi accedere. Per utilizzare queste API sono necessari i seguenti strumenti Android SDK:

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

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:

  1. Assicurati di aver installato la versione dell'SDK per Android richiesta.
  2. Aggiorna le impostazioni per la procedura 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 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 impostare renderscriptSupportModeEnabled su true. I valori validi per questa impostazione sono qualsiasi valore intero da 11 al 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 compilazione 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 è in esecuzione non supporta la versione di destinazione.
  3. Nei 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.*;
    

Utilizzare RenderScript dal codice Java o Kotlin

L'utilizzo di RenderScript dal codice Java o Kotlin si basa sulle classi API nel android.renderscript o nel pacchetto 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 che RenderScript possa essere utilizzato e fornisce un oggetto per controllare il ciclo di vita di tutti gli oggetti RenderScript successivi. La creazione del contesto deve essere considerata un'operazione potenzialmente di lunga durata, poiché potrebbe creare risorse su diversi componenti hardware. Se possibile, non deve essere nel percorso critico di un'applicazione. In genere, un'applicazione avrà un solo contesto RenderScript alla volta.
  2. Crea almeno un Allocation da passare a un script. Un Allocation è un oggetto RenderScript che fornisce spazio di archiviazione per una quantità fissa di dati. I kernel negli script accettano oggetti Allocation come input e output e agli oggetti Allocation si può accedere nei kernel utilizzando rsGetElementAt_type() e rsSetElementAt_type() quando sono associati come variabili globali dello script. Gli oggetti Allocation consentono di passare gli array dal codice Java al codice RenderScript e viceversa. In genere, gli oggetti Allocation vengono creati utilizzando createTyped() o createFromBitmap().
  3. Crea tutti gli script necessari. Quando utilizzi RenderScript, hai a disposizione due tipi di script:
    • ScriptC: si tratta di script definiti dall'utente come descritto in Scrivere 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 di cui sopra si trovasse in invert.rs e un contesto RenderScript fosse già in mRenderScript, il codice Java o Kotlin per l'inizializzazione dello script sarebbe:

      Kotlin

      val invert = ScriptC_invert(renderScript)
      

      Java

      ScriptC_invert invert = new ScriptC_invert(renderScript);
      
    • ScriptIntrinsic: si tratta di kernel RenderScript integrati per operazioni comuni, come sfocatura Gaussiana, convezione e sfumatura delle immagini. Per ulteriori informazioni, consulta le sottoclassi di ScriptIntrinsic.
  4. Compila le allocazioni con i dati. Ad eccezione delle allocazioni create con createFromBitmap(), un'allocazione viene compilata con dati vuoti al primo creazione. Per compilare un'allocazione, utilizza uno dei metodi "copy" in Allocation. I metodi "copy" sono sincroni.
  5. Imposta eventuali variabili globali dello script necessarie. Puoi impostare le variabili globali utilizzando i metodi della stessa classe ScriptC_filename denominata set_globalname. Ad esempio, per impostare una variabile int denominata threshold, utilizza il metodo Java set_threshold(int) e per impostare una variabile rs_allocation denominata lookup, utilizza il metodo Java set_lookup(Allocation). I metodi set sono asincroni.
  6. Lancia i kernel e le funzioni invocabili appropriati.

    I metodi per avviare un determinato kernel sono riportati nella stessa classe ScriptC_filename con metodi denominati forEach_mappingKernelName() o reduce_reductionKernelName(). Questi lanci 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 in queste 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 invocabili utilizzando i metodi invoke_functionName riflettuti nella stessa classe ScriptC_filename. Questi lanci sono asincroni.

  7. Recupero dei dati dagli oggetti Allocation e javaFutureType. Per accedere ai dati di un Allocation dal codice Java, devi copiarli nuovamente in Java utilizzando uno dei metodi "copy" in Allocation. Per ottenere il risultato di un kernel di riduzione, devi utilizzare il metodo javaFutureType.get(). I metodi "copy" e get() sono sincroni.
  8. Abbatti il contesto RenderScript. Puoi distruggere il contesto RenderScript con destroy() o consentendo la raccolta dei rifiuti dell'oggetto del contesto RenderScript. Ciò provoca un'eccezione per qualsiasi ulteriore utilizzo di un oggetto appartenente a quel contesto.

Modello di esecuzione asincrona

I metodi riflessi forEach, invoke, reduce e set sono asincroni: ognuno 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 "copy" 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).

Single-Source RenderScript

Android 7.0 (livello API 24) introduce una nuova funzionalità di programmazione chiamata RenderScript con codice singolo, 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 indicati come "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 solo all'interno di uno script, anche se sono necessari più lanci del kernel. Il vantaggio è duplice: codice più leggibile, perché mantiene l'implementazione di un algoritmo in un unico linguaggio, e codice potenzialmente più veloce, a causa di un minor numero di transizioni tra Java e RenderScript in più lanci del kernel.

In RenderScript a sorgente singola, scrivi i kernel come descritto in Scrivere un kernel RenderScript. Poi scrivi una funzione invocabile che chiama rsForEach() per avviarli. Questa API accetta una funzione di 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 di elementi delle allocazioni di input e output da elaborare per la funzione del kernel.

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

Spesso sono necessarie allocazioni per salvare e passare risultati intermedi da un lancio del kernel all'altro. Puoi crearli utilizzando rsCreateAllocation(). Una forma facile da usare di questa API è rsCreateAllocation_<T><W>(…), dove T è il tipo di dato di un elemento e W è la larghezza del vettore per l'elemento. L'API accetta come argomenti le dimensioni nelle dimensioni X, Y e Z. Per le allocazioni 1D o 2D, la dimensione per la dimensione Y o Z può essere omessa. Ad esempio, rsCreateAllocation_uchar4(16384) crea un'allocazione 1D di 16384 elementi, ciascuno di tipo uchar4.

Le allocazioni vengono gestite automaticamente dal sistema. Non devi rilasciarle o liberarle 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 Scrivere un kernel RenderScript contiene un kernel di esempio che inverte un'immagine. L'esempio seguente espande questa funzionalità per applicare più di un effetto a un'immagine utilizzando RenderScript a sorgente singola. Include un altro kernel, greyscale, che trasforma un'immagine a colori in bianco e nero. Una funzione invocabile process() applica poi questi due kernel consecutivamente a un'immagine di input e produce un'immagine di output. Le allocazioni sia per l'input sia per l'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 come segue:

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 di kernel possa essere implementato completamente nel linguaggio RenderScript stesso. Senza Single-Source RenderScript, dovresti lanciare entrambi i kernel dal codice Java, separando i lanci dei kernel dalle definizioni dei kernel e rendendo più difficile la comprensione dell'intero algoritmo. Il codice RenderScript a sorgente singolo non è solo più facile da leggere, ma elimina anche la transizione tra Java e lo script durante i lanci del kernel. Alcuni algoritmi iterativi possono avviare i kernel cento volte, rendendo considerevole il sovraccarico di questa transizione.

Variabili globali dello script

Un parametro globale dello script è una normale variabile globale non static in un file di script (.rs). Per un script globale denominato var definito nel file filename.rs, esisterà 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 distinti: un valore Java e un valore script. Questi valori si comportano come segue:

  • Se var ha un inizializzante statico nello script, specifica il valore iniziale di var sia in Java sia nello script. In caso contrario, il valore iniziale è zero.
  • Gli accessi a var all'interno dello script leggono e scrivono il 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: ciò significa che, ad eccezione di eventuali inizializzatori statici nello script, i valori scritti in un ambito globale all'interno di uno script non sono visibili a Java.

Informazioni dettagliate sui kernel di riduzione

La riduzione è il processo di combinazione di una raccolta di dati in un singolo valore. Si tratta di un'operazione di base utile nella programmazione parallela, con applicazioni come quelle riportate di seguito:

  • Calcolo della somma o del prodotto su tutti i dati
  • Esegue operazioni logiche (and, or, xor) su tutti i dati
  • Trovare 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 scritti dall'utente efficienti. Puoi avviare i kernel di riduzione sugli 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 posizioni dei valori long minimo e massimo in un Allocation 1D:

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

Per eseguire un kernel di riduzione, il runtime di RenderScript crea una o più variabili chiamate elementi di dati dell'accumulatore per memorizzare lo stato del processo di riduzione. Il runtime RenderScript scegliendo il numero di elementi di dati dell'accumulatore in modo da massimizzare le prestazioni. Il tipo degli elementi di dati dell'accumulatore (accumType) è determinato dalla funzione dell'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 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 dati dell'accumulatore viene inizializzato a 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 impostarli 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 negli input. In genere, la funzione deve aggiornare l'elemento dati dell'accumulatore in qualche modo in base all'input.

Esempio: nel kernel addint, la funzione di accumulatore aggiunge il valore di un elemento di input all'elemento di dati dell'accumulatore.

Esempio: nel kernel findMinAndMax, la funzione di accumulatore controlla se il valore di un elemento di input è minore o uguale al valore minimo registrato nell'elemento dati dell'accumulatore e/o maggiore o uguale al valore massimo registrato nell'elemento dati dell'accumulatore e aggiorna l'elemento dati dell'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 di combinazione. Se la funzione di accumulo ha un singolo input e nessun argomento speciale, non è necessario scrivere una funzione combinatore. RenderScript utilizzerà la funzione di accumulo per combinare gli elementi di dati dell'accumulatore. Puoi comunque scrivere una funzione di combinazione se questo comportamento predefinito non è quello che vuoi.

Esempio: nel kernel addint non è presente una funzione di combinazione, pertanto verrà utilizzata la funzione di accumulo. 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 equivale alla somma dell'intera raccolta.

Esempio: nel kernel findMinAndMax, la funzione di combinatore controlla se il valore minimo registrato nell'elemento di dati dell'accumulatore "origine" *val è inferiore al valore minimo registrato nell'elemento di dati dell'accumulatore "destinazione" *accum e aggiorna *accum di conseguenza. Funziona in modo 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 per tornare a Java. A questo scopo, puoi scrivere una funzione outconverter. Non è necessario scrivere una funzione di conversione in uscita se vuoi che il valore finale degli elementi di dati dell'accumulatore combinato sia il risultato della riduzione.

Esempio: nel kernel addint non è presente la 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 specificandone il nome, 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 cosa vuoi che il kernel faccia.

#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 riflesso reduce_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 dati dell'accumulatore. La funzione deve essere definita come segue:

    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 simile alla seguente:

    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 in qualche modo un elemento dati dell'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 di dati dell'accumulatore da modificare da parte di questa funzione. in1 fino a inN 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 degli argomenti speciali.

    Un kernel di esempio con più input è dotProduct.

  • combiner(combinerName)

    (Facoltativo) Specifica il nome della funzione di combinatore per questo kernel di riduzione. Dopo che RenderScript chiama la funzione di accumulatore una volta per ogni coordinata negli input, la chiama il numero di volte necessario per combinare tutti gli elementi dati dell'accumulatore in un unico elemento dati dell'accumulatore. La funzione deve essere definita come segue:

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

    accum è un puntatore a un elemento di dati dell'accumulatore "destinazione" da modificare con questa funzione. other è un puntatore a un elemento di dati dell'accumulatore "source" per questa funzione 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 di combinatore fMMCombiner controlla esplicitamente la presenza di idx < 0 perché indica un elemento di dati dell'accumulatore di questo tipo, il cui valore è INITVAL.

    Se non fornisci una funzione di combinatore, RenderScript la sostituisce con la funzione di accumulatore, comportandosi come se esistesse una funzione di combinatore simile alla seguente:

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

    Una funzione di combinazione è 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 di conversione in uscita per questo kernel di riduzione. Dopo che RenderScript ha combinato 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 dati del risultato (allocato ma non inizializzato dal runtime di RenderScript) per l'inizializzazione di questa funzione con il risultato della riduzione. resultType è il tipo di questo elemento dati, che non deve essere necessariamente uguale a accumType. accum è un puntatore all'elemento di dati dell'accumulatore finale calcolato dalla funzione combinatore.

    Se non fornisci una funzione di conversione in uscita, RenderScript copia l'elemento di dati dell'accumulatore finale nell'elemento di dati del risultato, comportandosi come se esistesse una funzione di conversione in uscita simile alla seguente:

    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 dati dell'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 assumere?

Non devi fare affidamento sul numero di elementi di dati dell'accumulatore creati da RenderScript per un determinato lancio 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, accumulo e combinazione; potrebbe persino chiamarne alcune in parallelo. Non vi è alcuna garanzia che due lanci dello stesso kernel con lo stesso input seguiranno lo stesso ordine. L'unica garanzia è che solo la funzione di inizializzazione vedrà un elemento di dato dell'accumulatore non inizializzato. Ad esempio:

  • Non è garantito che tutti gli elementi dati dell'accumulatore vengano inizializzati prima della chiamata della funzione dell'accumulatore, anche se verrà chiamata solo su un elemento dato dell'accumulatore inizializzato.
  • Non è garantito l'ordine in cui gli elementi di input vengono passati alla funzione di accumulo.
  • Non è garantito che la funzione di accumulo sia stata chiamata per tutti gli elementi di input prima della chiamata della 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 hai modo di sapere quale occorrenza verrà trovata dal 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 come preferisci. 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 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, in genere ha senso che "lo stesso" significhi uguaglianza matematica. Per una ricerca "scegli qualsiasi" come findMinAndMax ("trova la posizione dei valori di input minimo e massimo") in cui potrebbe esserci più di un'occorrenza di valori di input identici, tutte le posizioni di un determinato valore di input devono essere considerate "uguali". Potresti scrivere un kernel simile per "trovare la posizione dei valori di input minimo e massimo più a sinistra" dove, ad esempio, è preferibile un valore minimo nella posizione 100 rispetto a un valore minimo identico nella posizione 200; per questo kernel, "lo stesso" significherebbe posizione identica, non solo valore identico, e le funzioni di accumulatore e combinatore dovrebbero essere diverse da quelle per findMinAndMax.

La funzione di inizializzazione deve creare un valore di identità. In altre parole, se I e A sono elementi di dati dell'accumulatore inizializzati dalla funzione di inizializzazione e I non è mai stato passato alla funzione di accumulatore (ma potrebbe esserlo stato A), then
  • combinerName(&A, &I) deve rimanere A invariato
  • combinerName(&I, &A) deve lasciare I uguale a A

Esempio: nel kernel addint, un elemento di dati dell'accumulatore viene inizializzato a zero. La funzione di combinazione per questo nucleo esegue l'addizione; zero è il valore di identità per l'addizione.

Esempio: nel kernel findMinAndMax, un elemento dati dell'accumulatore viene inizializzato a INITVAL.

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

Pertanto, INITVAL è effettivamente un valore di identità.

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

Esempio: nel kernel addint, la funzione di combinatore aggiunge i due valori dell'elemento 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.

La funzione di combinazione deve essere associativa. In altre parole, se A, B e C sono elementi di dati dell'accumulatore inizializzati dalla funzione di inizializzazione e possono essere stati passati alla funzione dell'accumulatore zero o più volte, le due seguenti sequenze di codice devono impostare A su lo stesso valore:

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

Esempio: nel kernel addint, la funzione di combinatore aggiunge i due valori dell'elemento 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, così come la funzione di combinazione.

Esempio: nel kernel findMinAndMax,

fMMCombiner(&A, &B)
è uguale a
A = minmax(A, B)
Pertanto, le due sequenze sono

  • 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 insieme devono rispettare la regola di accumulo di base. In altre parole, se A e B sono elementi di dati dell'accumulatore, A è stato inizializzato dalla funzione di inizializzazione e potrebbe essere stato passato alla funzione dell'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 dell'accumulatore, le due seguenti sequenze di codice devono impostare A su lo 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 a B = V
  • L'istruzione 4 è uguale a A += B, che è uguale a A += V

Le affermazioni 1 e 4 impostano A sullo stesso valore, pertanto questo kernel obbedisce alla 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
    B = minmax(B, IndexedVal(V, X))
    
    che, poiché B è il valore iniziale, è uguale a
    B = IndexedVal(V, X)
    
  • L'affermazione 4 è uguale a
    A = minmax(A, B)
    
    che è uguale a
    A = minmax(A, IndexedVal(V, X))
    

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

Chiamata di 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 del 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 di RenderScript verifica che tutte le allocazioni di input abbiano le stesse dimensioni e che il tipo Element di ciascuna allocazione di input corrisponda a quello dell'argomento di input corrispondente del prototipo della funzione di accumulo. Se uno di questi controlli non va a buon fine, RenderScript genera un'eccezione. Il nucleo viene eseguito su ogni coordinata in 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 di coordinate.

Il metodo 3 è uguale al metodo 1, tranne per il fatto che, anziché accettare input di allocazione, accetta input di array Java. Questa funzionalità consente di risparmiare tempo perché non è necessario 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 1-dimensionale 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 queste allocazioni temporanee.

NOTA:se la tua applicazione eseguirà più chiamate al kernel con lo stesso array o con array diversi delle stesse dimensioni e dello stesso tipo di elemento, puoi migliorare il rendimento creando, compilando e riutilizzando esplicitamente le allocazioni anziché utilizzare il metodo 3.

javaFutureType, il tipo di ritorno dei metodi di riduzione riflessi, è un 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 dal 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 e esiste un tipo Java firmato più grande, javaResultType è il tipo Java firmato più grande; in caso contrario, è il tipo Java corrispondente direttamente. Ad esempio:

  • Se resultType è int, int2 o int[15], javaResultType è int, Int2 o int[]. Tutti i valori di resultType possono essere rappresentati da javaResultType.
  • Se resultType è uint, uint2 o uint[15], javaResultType è long, Long2 o long[]. Tutti i valori di resultType possono essere rappresentati da javaResultType.
  • Se resultType è ulong, ulong2 o ulong[15], javaResultType è long, Long2 o long[]. 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, javaFutureType è result_resultType.
  • Se resultType è un array di lunghezza Count con elementi di tipo memberType, 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() 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 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 direttamente corrispondente al tipo scalare con segno della stessa dimensione. Se inXType è un tipo di vettore firmato, 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 delle stesse dimensioni del tipo di componente del vettore. Ad esempio:

  • Se inXType è int, devecSiInXType è int.
  • Se inXType è int2, devecSiInXType è int. L'array è una rappresentazione appiattata: ha il doppio degli elementi scalari dell'allocazione di elementi vettoriali a due componenti. Funziona nello stesso modo dei metodi copyFrom() di Allocation.
  • Se inXType è uint, deviceSiInXType è int. Un valore firmato nell'array Java viene interpretato come un valore non firmato dello stesso pattern di bit nell'allocazione. Funzionano nello stesso modo dei metodi copyFrom() di Allocation.
  • Se inXType è uint2, deviceSiInXType è int. Si tratta di una combinazione del modo in cui vengono gestiti int2 e uint: l'array è una rappresentazione appiattita e i valori firmati dell'array Java vengono interpretati come valori Element non firmati 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 appiattito lato Java, mentre il risultato vettoriale di uno script no.
  • L'input senza segno di uno script è rappresentato come un input con segno della stessa dimensione lato Java, mentre il risultato senza segno di uno script è rappresentato come un tipo con segno ampliato 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.