Présentation de RenderScript

RenderScript est un framework qui permet d'exécuter des tâches nécessitant beaucoup de ressources de calcul sur Android. RenderScript est principalement destiné à être utilisé avec le calcul parallèle des données, même si les charges de travail en série peuvent également en bénéficier. L'environnement d'exécution RenderScript exécute les tâches en parallèle sur les processeurs disponibles sur l'appareil tels que les GPU et les processeurs multicœurs. Vous pouvez ainsi vous concentrer sur l'expression d'algorithmes plutôt que sur la planification des tâches. RenderScript est particulièrement utile pour les applications de traitement d'images, de photographie computationnelle ou de vision par ordinateur.

Pour commencer à utiliser RenderScript, vous devez comprendre deux concepts principaux :

  • Le langage lui-même est un langage dérivé de C99, qui permet d'écrire du code de calcul hautes performances. La section Écrire un noyau RenderScript explique comment utiliser le framework pour écrire des noyaux de calcul.
  • L'API de contrôle permet de gérer la durée de vie des ressources RenderScript et de contrôler l'exécution du noyau. Elle est disponible dans trois langages différents : Java, C++ sous Android NDK et le langage dérivé du C99 lui-même. Utiliser RenderScript depuis le code Java et RenderScript source unique décrivent respectivement la première et la troisième option.

Écrire un noyau RenderScript

Un noyau RenderScript se trouve généralement dans un fichier .rs du répertoire <project_root>/src/rs. Chaque fichier .rs est appelé script. Chaque script contient son propre ensemble de noyaux, de fonctions et de variables. Un script peut contenir :

  • Une déclaration pragma (#pragma version(1)) qui indique la version du langage de noyau RenderScript utilisé dans ce script. Actuellement, 1 est la seule valeur valide.
  • Une déclaration pragma (#pragma rs java_package_name(com.example.app)) qui indique le nom du package des classes Java reflétées par ce script. Notez que votre fichier .rs doit faire partie de votre package d'application, et non se trouver dans un projet de bibliothèque.
  • Aucune, une ou plusieurs fonctions invocables. Une fonction invocable est une fonction RenderScript monofilaire que vous pouvez invoquer à partir de votre code Java avec des arguments arbitraires. Elles sont souvent utiles pour la configuration initiale ou pour les calculs en série dans un pipeline de traitement plus important.
  • Aucun, un ou plusieurs éléments généraux de script. Un élément général de script est semblable à une variable globale en C. Vous pouvez accéder aux éléments généraux de script à partir du code Java. Ils sont souvent utilisés pour transmettre des paramètres aux noyaux RenderScript. Pour en savoir plus sur les éléments généraux de script, cliquez ici.

  • Aucun, un ou plusieurs noyaux de calcul. Un noyau de calcul est une fonction ou un ensemble de fonctions que vous pouvez ordonner à l'environnement d'exécution RenderScript d'exécuter en parallèle sur un ensemble de données. Il existe deux types de noyaux de calcul : les noyaux de mappage (également appelés noyaux foreach) et les noyaux de réduction.

    Un noyau de mappage est une fonction parallèle qui agit sur un ensemble de Allocations de mêmes dimensions. Par défaut, il s'exécute une fois pour chaque coordonnée de ces dimensions. Il est généralement (mais pas exclusivement) utilisé pour transformer un ensemble de Allocations d'entrée en Allocation de sortie un Element à la fois.

    • Voici un exemple de noyau de mappage simple :

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

      Dans la plupart des cas, cette valeur est identique à celle d'une fonction C standard. La propriété RS_KERNEL appliquée au prototype de la fonction spécifie qu'elle est un noyau de mappage RenderScript et non une fonction invocable. L'argument in est automatiquement renseigné en fonction de l'entrée Allocation transmise au lancement du noyau. Les arguments x et y sont abordés ci-dessous. La valeur renvoyée par le noyau est automatiquement écrite à l'emplacement approprié dans le Allocation de sortie. Par défaut, ce noyau est exécuté sur l'ensemble du Allocation d'entrée, avec une exécution de la fonction de noyau par Element dans Allocation.

      Un noyau de mappage peut comporter un ou plusieurs Allocations d'entrée, un seul Allocation de sortie ou les deux. L'environnement d'exécution RenderScript vérifie que toutes les allocations d'entrée et de sortie ont les mêmes dimensions, et que les types de Element des allocations d'entrée et de sortie correspondent au prototype du noyau. Si l'une de ces vérifications échoue, RenderScript génère une exception.

      REMARQUE : Dans les versions antérieures à Android 6.0 (niveau d'API 23), un noyau de mappage ne peut comporter plus d'un Allocation d'entrée.

      Si vous avez besoin de plus de Allocations d'entrée ou de sortie que ceux disponibles sur le noyau, ces objets doivent être liés aux éléments généraux de script de rs_allocation et être accessibles depuis un noyau ou une fonction invocable via rsGetElementAt_type() ou rsSetElementAt_type().

      REMARQUE : RS_KERNEL est une macro définie automatiquement par RenderScript pour plus de confort :

      #define RS_KERNEL __attribute__((kernel))
      

    Un noyau de réduction est une famille de fonctions qui opère sur un ensemble de Allocations d'entrée de même dimension. Par défaut, sa fonction d'accumulateur s'exécute une fois pour chaque coordonnée de ces dimensions. Il est généralement (mais pas exclusivement) utilisé pour "réduire" un ensemble de Allocations d'entrée à une seule valeur.

    • Voici un exemple de noyau de réduction qui ajoute le Elements de son entrée :

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

      Un noyau de réduction consiste en une ou plusieurs fonctions écrites par l'utilisateur. #pragma rs reduce permet de définir le noyau en spécifiant son nom (addint dans cet exemple), ainsi que les noms et les rôles des fonctions qui le composent (la fonction addintAccum d'un accumulator, dans cet exemple). Toutes ces fonctions doivent être static. Un noyau de réduction nécessite toujours une fonction accumulator. Il peut également avoir d'autres fonctions, selon l'usage que vous destinez au noyau.

      Une fonction d'accumulateur de noyau de réduction doit renvoyer void et doit comporter au moins deux arguments. Le premier argument (accum, dans cet exemple) pointe vers un élément de données d'accumulateur, et le deuxième argument (val, dans cet exemple) est automatiquement renseigné en fonction du Allocation d'entrée transmis au lancement du noyau. L'élément de données de l'accumulateur est créé par l'environnement d'exécution RenderScript. Par défaut, il est défini à zéro. Par défaut, ce noyau est exécuté sur l'ensemble du Allocation d'entrée, avec une exécution de la fonction d'accumulateur par Element dans Allocation. Par défaut, la valeur finale de l'élément de données de l'accumulateur est traitée comme le résultat de la réduction et renvoyée à Java. L'environnement d'exécution RenderScript vérifie que le type Element de l'allocation d'entrée correspond au prototype de la fonction d'accumulateur. Si ce n'est pas le cas, RenderScript génère une exception.

      Un noyau de réduction comporte un ou plusieurs Allocations d'entrée, mais pas d'Allocations de sortie.

      Pour en savoir plus sur les noyaux de réduction, cliquez ici.

      Les noyaux de réduction sont compatibles à partir d'Android version 7.0 (niveau d'API 24).

    Une fonction de noyau de mappage ou une fonction d'accumulateur de noyau de réduction peut accéder aux coordonnées de l'exécution actuelle à l'aide des arguments spéciaux x, y et z, qui doivent être de type int ou uint32_t. Ces arguments sont facultatifs.

    Une fonction de noyau de mappage ou une fonction d'accumulateur de réduction de noyau peut également utiliser l'argument spécial facultatif context de type rs_kernel_context. Elle est nécessaire à une famille d'API d'exécution utilisées pour interroger certaines propriétés de l'exécution en cours, par exemple rsGetDimX. (L'argument context est disponible à partir d'Android 6.0 (niveau d'API 23)).

  • Une fonction init() facultative. La fonction init() est un type spécial de fonction invocable. Elle est exécutée par RenderScript lors de la première instanciation du script. Elle permet d'effectuer automatiquement certains calculs lors de la création du script.
  • Aucun, un ou plusieurs éléments généraux de script et fonctions statiques. Un élément général de script statique correspond à un élément général de script, mais qui est inaccessible à partir du code Java. Une fonction statique est une fonction C standard pouvant être appelée à partir de n'importe quel noyau ou fonction invocable dans le script, mais invisible par l'API Java. Si un élément général de script ou une fonction n'a pas besoin d'être accessible depuis le code Java, il est fortement recommandé de les déclarer static.

Définir la précision à virgule flottante

Vous pouvez contrôler le niveau de précision à virgule flottante requis dans un script. Cela peut s'avérer utile si la norme IEEE 754-2008 complète (utilisée par défaut) n'est pas requise. Les déclarations pragma suivantes peuvent définir un niveau différent de précision à virgule flottante :

  • #pragma rs_fp_full (par défaut si rien n'est spécifié) : pour les applications qui nécessitent une précision à virgule flottante, comme décrit dans la norme IEEE 754-2008.
  • #pragma rs_fp_relaxed : pour les applications qui ne nécessitent pas une conformité stricte avec la norme IEEE 754-2008 et peuvent tolérer moins de précision. Ce mode permet le flush-to-zero en hors normes et le round-towards-zero.
  • #pragma rs_fp_imprecise : pour les applications sans exigences de précision strictes. Ce mode active tout dans rs_fp_relaxed, ainsi que les éléments suivants :
    • Les opérations donnant la valeur -0.0 peuvent renvoyer +0.0 à la place.
    • Les opérations sur INF et NAN ne sont pas définies.

La plupart des applications peuvent utiliser rs_fp_relaxed sans aucun effet secondaire. Cela peut s'avérer très utile sur certaines architectures, car d'autres optimisations ne sont disponibles qu'avec une précision flexible (par exemple, des instructions concernant le processeur SIMD).

Accéder aux API RenderScript depuis Java

Lorsque vous développez une application Android qui utilise RenderScript, vous pouvez accéder à son API à partir de Java de l'une des deux manières suivantes :

  • android.renderscript : les API de ce package de classe sont disponibles sur les appareils sous Android version 3.0 (niveau d'API 11) ou ultérieure.
  • android.support.v8.renderscript : les API de ce package sont disponibles via une bibliothèque Support, ce qui vous permet de les utiliser sur des appareils sous Android version 2.3 (niveau 9 d'API) et ultérieures.

Voici les compromis :

  • Si vous utilisez les API de la bibliothèque Support, la partie RenderScript de votre application sera compatible avec les appareils sous Android version 2.3 (niveau 9 d'API) ou ultérieure, quelles que soient les fonctionnalités RenderScript que vous utilisez. Votre application peut ainsi fonctionner sur plus d'appareils qu'avec les API natives (android.renderscript).
  • Certaines fonctionnalités de RenderScript ne sont pas disponibles via les API de la bibliothèque Support.
  • Si vous utilisez les API de la bibliothèque Support, vous obtiendrez des APK plus (peut-être très) volumineux que si vous utilisez les API natives (android.renderscript).

Utiliser les API de la bibliothèque Support de RenderScript

Pour pouvoir utiliser les API de la bibliothèque Support de RenderScript, vous devez configurer votre environnement de développement de manière à pouvoir y accéder. Les SDK Tools pour Android suivants sont requis pour utiliser ces API :

  • SDK Tools pour Android version révisée 22.2 ou ultérieure
  • SDK Build Tools pour Android version révisée 18.1.0 ou ultérieure

À noter qu'à partir de SDK Build-Tools pour Android version 24.0.0, Android 2.2 (niveau d'API 8) n'est plus compatible.

Vous pouvez vérifier et mettre à jour la version installée de ces outils via le Android SDK Manager.

Pour utiliser les API RenderScript de la bibliothèque Support :

  1. Assurez-vous d'avoir installé la version requise du SDK Android.
  2. Mettez à jour les paramètres du processus de compilation d'Android afin d'inclure les paramètres RenderScript :
    • Ouvrez le fichier build.gradle dans le dossier de votre module d'application.
    • Ajoutez les paramètres RenderScript suivants au fichier :

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

      Les paramètres susmentionnés contrôlent un comportement spécifique du processus de compilation d'Android :

      • renderscriptTargetApi : spécifie la version du bytecode à générer. Nous vous recommandons de définir cette valeur au niveau d'API le plus faible afin de fournir toutes les fonctionnalités que vous utilisez et de définir renderscriptSupportModeEnabled sur true. Les valeurs valides pour ce paramètre sont des nombres entiers compris entre 11 et le niveau d'API le plus récent. Si la version minimale du SDK indiquée dans le fichier manifeste de votre application est définie sur une valeur différente, cette valeur est ignorée, et la valeur cible du fichier de compilation est utilisée pour définir la version minimale du SDK.
      • renderscriptSupportModeEnabled : indique que le bytecode généré doit revenir à une version compatible si l'appareil sur lequel il est exécuté n'est pas compatible avec la version cible.
  3. Dans vos classes d'application utilisant RenderScript, ajoutez une importation pour les classes de la bibliothèque Support :

    Kotlin

    import android.support.v8.renderscript.*
    

    Java

    import android.support.v8.renderscript.*;
    

Utiliser RenderScript à partir de codes Java ou Kotlin

L'utilisation de RenderScript à partir de codes Java ou Kotlin repose sur les classes d'API situées dans le package android.renderscript ou android.support.v8.renderscript. La plupart des applications suivent le même schéma d'utilisation de base :

  1. Initialisez un contexte RenderScript. Le contexte RenderScript, créé avec create(Context), garantit que RenderScript peut être utilisé et fournit un objet permettant de contrôler la durée de vie de tous les objets RenderScript ultérieurs. Vous devez prendre en compte que la création d'un contexte peut prendre du temps, car elle peut créer des ressources sur différents composants matériels. Elle ne peut, si possible, pas se trouver dans le chemin critique d'une application. En général, une application ne comporte qu'un seul contexte RenderScript à la fois.
  2. Créez au moins un Allocation à transmettre à un script. Un Allocation est un objet RenderScript qui fournit de l'espace de stockage pour une quantité déterminée de données. Les noyaux des scripts acceptent les objets Allocation en entrée et en sortie, et les objets Allocation sont accessibles dans les noyaux à l'aide de rsGetElementAt_type() et rsSetElementAt_type() lorsqu'ils sont liés en tant qu'éléments généraux de script. Les objets Allocation permettent de transmettre des tableaux du code Java au code RenderScript, et inversement. Les objets Allocation sont généralement créés à l'aide de createTyped() ou createFromBitmap().
  3. Créez les scripts nécessaires. Lorsque vous utilisez RenderScript, vous avez le choix entre deux types de scripts :
    • ScriptC : scripts définis par l'utilisateur, comme décrit dans la section Écrire un noyau RenderScript susmentionnée. Chaque script dispose d'une classe Java renvoyée par le compilateur RenderScript afin de faciliter l'accès au script à partir du code Java. Cette classe s'appelle ScriptC_filename. Par exemple, si le noyau de mappage ci-dessus était situé dans invert.rs et qu'un contexte RenderScript se trouvait déjà dans mRenderScript, le code Java ou Kotlin d'instanciation du script serait :

      Kotlin

      val invert = ScriptC_invert(renderScript)
      

      Java

      ScriptC_invert invert = new ScriptC_invert(renderScript);
      
    • ScriptIntrinsic : il s'agit de noyaux RenderScript intégrés pour les opérations courantes telles que le flou gaussien, la convolution et le mélange d'images. Pour en savoir plus, consultez les sous-classes de ScriptIntrinsic.
  4. Renseignez les allocations avec des données. À l'exception des allocations créées avec createFromBitmap(), les allocations nouvellement créées sont renseignées par des données vides. Pour renseigner une allocation, utilisez l'une des méthodes "copy" dans Allocation. Les méthodes "copy" s'effectuent de manière synchrone.
  5. Définissez les éléments généraux de script nécessaires. Vous pouvez définir des données globales à l'aide de méthodes de la même classe ScriptC_filename nomméeset_globalname. Par exemple, pour définir une variable int nommée threshold, utilisez la méthode Java set_threshold(int). Pour définir une variable rs_allocation nommée lookup, utilisez la méthode Java set_lookup(Allocation). Les méthodes set s'effectuent de manière asynchrone.
  6. Lancez les noyaux et les fonctions invocables adéquats.

    Les méthodes de lancement d'un noyau donné sont renvoyées dans la même classe ScriptC_filename avec des méthodes nommées forEach_mappingKernelName() ou reduce_reductionKernelName(). Ces lancements sont asynchrones. Selon les arguments du noyau, la méthode accepte une ou plusieurs allocations, qui doivent toutes avoir les mêmes dimensions. Par défaut, un noyau s'exécute sur toutes les coordonnées de ces dimensions. Pour exécuter un noyau sur un sous-ensemble de ces coordonnées, transmettez un Script.LaunchOptions adéquat en tant que dernier argument de la méthode forEach ou reduce.

    Lancez des fonctions invocables à l'aide des méthodes invoke_functionName renvoyées dans la même classe ScriptC_filename. Ces lancements sont asynchrones.

  7. Récupérez les données d'objets Allocation et d'objets javaFutureType. Pour accéder aux données d'un Allocation à partir du code Java, vous devez copier ces données dans Java à l'aide de l'une des méthodes "copy" dans Allocation. Pour obtenir le résultat d'un noyau de réduction, vous devez utiliser la méthode javaFutureType.get(). Les méthodes "copy" et get() s'effectuent de manière synchrone.
  8. Détruisez le contexte RenderScript. Vous pouvez détruire le contexte RenderScript avec destroy() ou en autorisant la récupération de mémoire pour l'objet de contexte RenderScript. Toute autre utilisation d'un objet dans ce contexte génère une exception.

Modèle d'exécution asynchrone

Les méthodes forEach, invoke, reduce et set qui sont renvoyées sont asynchrones. Chaque méthode peut revenir à Java avant d'effectuer l'action demandée. Cependant, les actions individuelles sont sérialisées dans l'ordre dans lequel elles sont lancées.

La classe Allocation fournit des méthodes "copy" permettant de copier des données vers et à partir d'allocations. Une méthode "copy" est synchrone et sérialisée par rapport à l'une des actions asynchrones ci-dessus en lien avec la même allocation.

Les classes javaFutureType renvoyées fournissent une méthode get() pour obtenir le résultat d'une réduction. get() est synchrone et sérialisé par rapport à la réduction (qui est asynchrone).

RenderScript à source unique

Android 7.0 (niveau d'API 24) introduit une nouvelle fonctionnalité de programmation appelée RenderScript à source unique, dans laquelle les noyaux sont lancés à partir du script où ils sont définis plutôt qu'à partir de Java. Cette approche se limite actuellement au mappage des noyaux, simplement appelés "noyaux" dans cette section pour plus de concision. Cette nouvelle fonctionnalité permet également de créer des allocations de type rs_allocation à partir du script. Il est maintenant possible d'appliquer un algorithme complet uniquement dans un script, même si plusieurs lancements de noyau sont nécessaires. L'avantage est double : un code plus lisible, car l'application d'un algorithme s'effectue dans une seule langue, et potentiellement plus rapide, car le nombre de transitions entre Java et RenderScript sur plusieurs lancements de noyau est réduit.

Dans RenderScript à source unique, vous écrivez des noyaux comme expliqué dans la section Écrire un noyau RenderScript. Ensuite, vous écrivez une fonction invocable qui appelle rsForEach() pour les lancer. Cette API utilise une fonction de noyau comme premier paramètre, suivie des allocations d'entrée et de sortie. Une API similaire rsForEachWithOptions() accepte un argument supplémentaire de type rs_script_call_t, qui définit un sous-ensemble des éléments d'allocations d'entrée et de sortie pour que la fonction du noyau soit traitée.

Pour démarrer le calcul RenderScript, vous appelez la fonction invocable depuis Java. Suivez la procédure décrite dans la section Utiliser RenderScript à partir du code Java. À l'étape Lancer les noyaux appropriés, appelez la fonction invocable à l'aide de invoke_function_name(), qui démarre l'ensemble du calcul, y compris le lancement des noyaux.

Les allocations sont souvent nécessaires pour enregistrer et transmettre les résultats intermédiaires d'un lancement de noyau à un autre. Vous pouvez les créer à l'aide de la méthode rsCreateAllocation(). Une forme simple d'utilisation de cette API est rsCreateAllocation_<T><W>(…), où T est le type de données d'un élément et W est la largeur du vecteur pour cet élément. L'API utilise les tailles X, Y et Z comme arguments. Pour les répartitions 1D ou 2D, la taille de la dimension Y ou Z peut être omise. Par exemple, rsCreateAllocation_uchar4(16384) crée une allocation 1D de 16 384 éléments, chacun de type uchar4.

Les allocations sont gérées automatiquement par le système. Vous n'avez pas besoin de les publier ni de les libérer explicitement. Toutefois, vous pouvez appeler rsClearObject(rs_allocation* alloc) pour indiquer que vous n'avez plus besoin de l'identifiant alloc pour l'allocation sous-jacente, afin que le système puisse libérer des ressources le plus tôt possible.

La section Écrire un noyau RenderScript contient un exemple de noyau qui inverse une image. L'exemple ci-dessous permet d'appliquer plusieurs effets à une image à l'aide de RenderScript à source unique. Il inclut un autre noyau, greyscale, qui transforme une image en couleur en noir et blanc. Une fonction invocable process() applique ensuite ces deux noyaux de manière consécutive à une image d'entrée et génère une image de sortie. Les allocations d'entrée et de sortie sont transmises en tant qu'arguments de type 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);
}

Vous pouvez appeler la fonction process() à partir de Java ou Kotlin comme suit :

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

Cet exemple montre comment appliquer entièrement un algorithme impliquant deux lancements de noyau dans le langage RenderScript lui-même. Sans RenderScript à source unique, il aurait fallu lancer les deux noyaux à partir du code Java, en séparant les lancements de noyau des définitions du noyau et en compliquant la compréhension de l'algorithme dans son ensemble. Non seulement le code RenderScript à source unique est plus facile à lire, mais il élimine également la transition entre Java et le script lors du lancement du noyau. Certains algorithmes itératifs peuvent lancer des noyaux des centaines de fois, ce qui rend la surcharge d'une telle transition considérable.

Éléments généraux de script

Un élément général de script est une variable globale non-static ordinaire dans un fichier de script (.rs). Pour un élément général de script nommévar défini dans le fichier filename.rs, il existe une méthode get_var renvoyée dans la classe ScriptC_filename. Une méthode set_var est également disponible, sauf si la valeur globale est const.

Un élément général de script spécifique comporte deux valeurs distinctes : une valeur Java et une valeur script. Ces valeurs se comportent comme suit :

  • Si var contient un initialiseur statique dans le script, il précise la valeur initiale de var à la fois dans Java et dans le script. Sinon, cette valeur initiale est égale à zéro.
  • Les accès à var dans le script lisent et écrivent sa valeur de script.
  • La méthode get_var lit la valeur Java.
  • La méthode set_var, lorsqu'elle existe, écrit immédiatement la valeur Java et la valeur de script de manière asynchrone.

REMARQUE : Cela signifie qu'à l'exception de tout initialiseur statique du script, Java n'a pas accès aux valeurs écrites dans un élément général à partir d'un script.

Noyaux de réduction en profondeur

La réduction est le processus qui combine un ensemble de données en une seule valeur. Cette primitive est utile en programmation parallèle, pour des utilisations telles que les suivantes :

  • Calcul de la somme ou du produit de toutes les données
  • Calcul d'opérations logiques (and, or, xor) avec toutes les données
  • Détermination de la valeur minimale ou maximale dans les données
  • Recherche d'une valeur spécifique ou de la coordonnée d'une valeur spécifique dans les données

À partir d'Android 7.0 (niveau d'API 24), RenderScript accepte les noyaux de réduction pour autoriser les algorithmes de réduction efficaces écrits par les utilisateurs. Vous pouvez lancer des noyaux de réduction sur des entrées avec 1, 2 ou 3 dimensions.

L'exemple ci-dessus illustre un noyau de réduction addint simple. Voici un noyau de réduction findMinAndMax plus complexe qui trouve les emplacements des valeurs long minimales et maximales dans un Allocation à une dimension :

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

REMARQUE : Vous trouverez d'autres noyaux de réduction sur cette page.

Pour exécuter un noyau de réduction, l'environnement d'exécution RenderScript crée une ou plusieurs variables appelées éléments de données d'accumulateur. qui indiquent l'état du processus de réduction. L'environnement d'exécution RenderScript sélectionne le nombre d'éléments de données d'accumulateur de manière à optimiser les performances. Le type des éléments de données d'accumulateur (accumType) est déterminé par la fonction d'accumulateur du noyau. Le premier argument de cette fonction pointe vers un élément de données d'accumulateur. Par défaut, tous les éléments de données d'accumulateur sont initialisés sur zéro (comme pour un memset). Cependant, vous pouvez écrire une fonction d'initialisation pour faire autre chose.

Exemple : Dans le noyau addint, les éléments de données d'accumulateur (de type int) sont utilisés pour additionner les valeurs d'entrée. Comme il n'y a pas de fonction d'initialisation, chaque élément de données de l'accumulateur est initialisé sur zéro.

Exemple : Dans le noyau findMinAndMax, les éléments de données d'accumulateur (de type MinAndMax) sont utilisés pour suivre les valeurs minimales et maximales trouvées jusque-là. Une fonction d'initialisation permet de les définir respectivement sur LONG_MAX et LONG_MIN et de définir les emplacements de ces valeurs sur -1, ce qui indique que les valeurs ne sont pas vraiment présentes dans la partie (vide) de l'entrée traitée.

RenderScript appelle votre fonction d'accumulateur une fois pour chaque coordonnée dans la ou les entrées. En règle générale, votre fonction doit mettre à jour l'élément de données de l'accumulateur d'une manière ou d'une autre en fonction de l'entrée.

Exemple : Dans le noyau addint, la fonction d'accumulateur ajoute la valeur d'un élément d'entrée à l'élément de données de l'accumulateur.

Exemple : Dans le noyau findMinAndMax, la fonction d'accumulateur vérifie si la valeur d'un élément d'entrée est inférieure ou égale à la valeur minimale enregistrée dans l'élément de données de l'accumulateur et/ou supérieure ou égale à la valeur maximale enregistrée dans l'élément de données de l'accumulateur et met à jour l'élément de données de l'accumulateur en conséquence.

Une fois que la fonction d'accumulateur a été appelée une fois pour chaque coordonnée dans la ou les entrées, RenderScript doit combiner les éléments de données d'accumulateur en un seul. Pour ce faire, vous pouvez écrire une fonction de combinaison. Si la fonction d'accumulateur n'a qu'une seule entrée et aucun argument spécial, il n'est pas nécessaire d'écrire de fonction. RenderScript utilisera la fonction d'accumulateur pour combiner les éléments de données d'accumulateur. (Vous pouvez toujours écrire une fonction de combinaison si ce comportement par défaut ne correspond pas à vos attentes.)

Exemple : Dans le noyau addint, il n'y a pas de fonction de combinaison. La fonction d'accumulateur sera donc utilisée. C'est le comportement correct, car si nous divisons un ensemble de valeurs en deux parties et que nous additionnons les valeurs de ces deux parties séparément, ces deux sommes cumulées égalent la somme de l'ensemble des valeurs.

Exemple : Dans le noyau findMinAndMax, la fonction de combinaison vérifie si la valeur minimale enregistrée dans l'élément de données d'accumulateur "source" *val est inférieure à la valeur minimale enregistrée dans l'élément de données d'accumulateur "destination" *accum et met à jour *accum en conséquence. Elle effectue un travail similaire pour la valeur maximale. Elle met à jour *accum dans la version qu'il aurait eue si toutes les valeurs d'entrée avaient été cumulées dans *accum plutôt que certaines dans *accum et d'autres dans *val.

Une fois tous les éléments de données de l'accumulateur combinés, RenderScript détermine le résultat de la réduction à renvoyer à Java. Pour ce faire, vous pouvez écrire une fonction de conversion. Vous n'avez pas besoin d'écrire de fonction de conversion en sortie si vous souhaitez que la valeur finale des éléments de données de l'accumulateur combinés soit le résultat de la réduction.

Exemple : Le noyau addint ne comporte pas de fonction de conversion. La valeur finale des éléments de données combinés correspond à la somme de tous les éléments de l'entrée, qui est la valeur que nous voulons renvoyer.

Exemple : Dans le noyau findMinAndMax, la fonction de conversion initialise une valeur de résultat int2 pour contenir les emplacements des valeurs minimale et maximale résultant de la combinaison de tous les éléments de données d'accumulateur.

Écrire un noyau de réduction

#pragma rs reduce définit un noyau de réduction en précisant son nom, ainsi que les noms et les rôles des fonctions qui le composent. Toutes ces fonctions doivent être static. Un noyau de réduction nécessite toujours une fonction accumulator. Vous pouvez omettre tout ou partie des autres fonctions selon ce que vous souhaitez faire avec le noyau.

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

Le sens des éléments du #pragma est le suivant :

  • reduce(kernelName) (obligatoire) : indique qu'un noyau de réduction est en cours de définition. Une méthode Java reduce_kernelName renvoyée va lancer le noyau.
  • initializer(initializerName) (facultatif) : indique le nom de la fonction d'initialisation pour ce noyau de réduction. Lorsque vous lancez le noyau, RenderScript appelle cette fonction une fois pour chaque élément de données d'accumulateur. La fonction doit être définie comme suit :

    static void initializerName(accumType *accum) { … }

    accum pointe vers un élément de données d'accumulateur pour que cette fonction s'initialise.

    Si vous ne fournissez pas de fonction d'initialisation, RenderScript initialise chaque élément de données d'accumulateur à zéro (comme pour un memset) et se comporte comme s'il existait une fonction d'initialisation semblable à la suivante :

    static void initializerName(accumType *accum) {
      memset(accum, 0, sizeof(*accum));
    }
  • accumulator(accumulatorName) (obligatoire) : indique le nom de la fonction d'accumulateur pour ce noyau de réduction. Lorsque vous lancez le noyau, RenderScript appelle cette fonction une fois pour chaque coordonnée dans la ou les entrées afin de mettre à jour un élément de données d'accumulateur d'une manière ou d'une autre en fonction des entrées. La fonction doit être définie comme suit :

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

    accum pointe vers un élément de données d'accumulateur que cette fonction peut modifier. in1 via inN sont un ou plusieurs arguments renseignés automatiquement en fonction des entrées transmises au lancement du noyau, à raison d'un argument par entrée. La fonction d'accumulateur peut éventuellement accepter l'un des arguments spéciaux.

    dotProduct est un exemple de noyau à plusieurs entrées.

  • combiner(combinerName)

    (Facultatif) : indique le nom de la fonction de combinaison pour ce noyau de réduction. Une fois que RenderScript a appelé la fonction d'accumulateur pour chaque coordonnée dans la ou les entrées, il appelle cette fonction autant de fois que nécessaire pour combiner tous les éléments de données d'accumulateur en un seul. La fonction doit être définie comme suit :

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

    accum pointe vers un élément de données d'accumulateur de "destination" que cette fonction doit modifier. other pointe vers un élément de données d'accumulateur "source" pour que cette fonction "combine" les données en *accum.

    REMARQUE : Il est possible que *accum, *other ou les deux aient été initialisées, mais n'aient jamais été transmises à la fonction d'accumulateur, c'est-à-dire que l'une ou les deux n'ont jamais été mises à jour en fonction des données d'entrée. Par exemple, dans le noyau findMinAndMax, la fonction de combinaison fMMCombiner effectue explicitement une vérification pour idx < 0, car cela indique un élément de données d'accumulateur de valeur INITVAL.

    Si vous ne fournissez pas de fonction de combinaison, RenderScript utilise la fonction d'accumulation à la place, comme s'il existait une fonction de combinaison semblable à la suivante :

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

    Une fonction de combinaison est obligatoire si le noyau comporte plusieurs entrées, si le type de données d'entrée est différent du type de données de l'accumulateur ou si la fonction d'accumulateur prend un ou plusieursarguments spéciaux.

  • outconverter(outconverterName) (facultatif) : indique le nom de la fonction de conversion pour ce noyau de réduction. Une fois que RenderScript a combiné tous les éléments de données d'accumulateur, il appelle cette fonction pour déterminer le résultat de la réduction à renvoyer à Java. La fonction doit être définie comme suit :

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

    result pointe vers un élément de données de résultat (attribué, mais non initialisé par l'environnement d'exécution RenderScript) pour que cette fonction s'initialise avec le résultat de la réduction. resultType est le type de cet élément de données, qui ne doit pas nécessairement être identique à accumType. accum pointe vers l'élément de données d'accumulateur calculé par la fonction de combinaison.

    Si vous ne fournissez pas de fonction de conversion, RenderScript copie l'élément de données d'accumulateur final dans l'élément de données de résultat, en se comportant comme s'il existait une fonction de conversion semblable à la suivante :

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

    Si vous voulez un type de résultat différent de celui des données d'accumulateur, la fonction de conversion est obligatoire.

Notez qu'un noyau comporte des types d'entrée, un type d'élément de données d'accumulateur et un type de résultat, dont aucun n'a besoin d'être identique. Par exemple, dans le noyau findMinAndMax, le type d'entrée long, le type d'élément de données d'accumulateur MinAndMax et le type de résultat int2 sont tous différents.

Que ne pouvez-vous pas supposer ?

Vous ne devez pas vous fier au nombre d'éléments de données d'accumulateur créés par RenderScript pour un lancement de noyau spécifique. Il n'existe aucune garantie que deux lancements du même noyau avec la/les même(s) entrée(s) créeront le même nombre d'éléments de données d'accumulateur.

Vous ne devez pas vous fier à l'ordre dans lequel RenderScript appelle les fonctions d'initialisation, d'accumulateur et de combinaison. Certaines d'entre elles peuvent même être exécutées en parallèle. Il n'existe aucune garantie que deux lancements du même noyau avec la même entrée suivront le même ordre. La seule garantie est que seule la fonction d'initialisation verra un élément de données d'accumulateur non initialisé. Par exemple :

  • Il n'existe aucune garantie que tous les éléments de données d'accumulateur seront initialisés avant que la fonction d'accumulateur soit appelée, bien qu'elle ne soit appelée que sur un élément de données d'accumulateur initialisé.
  • L'ordre dans lequel les éléments d'entrée sont transmis à la fonction d'accumulateur n'est pas garanti.
  • Il n'y a aucune garantie que la fonction d'accumulateur ait été appelée pour tous les éléments d'entrée avant l'appel de la fonction de combinaison.

L'une des conséquences est que le noyau findMinAndMax ne détermine rien : si l'entrée contient plusieurs occurrences de la même valeur minimale ou maximale, vous ne pouvez pas savoir quelle occurrence sera trouvée par le noyau.

Que devez-vous garantir ?

Étant donné que le système RenderScript peut choisir d'exécuter un noyau de nombreuses façons différentes, vous devez suivre certaines règles pour vous assurer que votre noyau se comporte comme vous le souhaitez. Si vous ne les respectez pas, vous risquez d'obtenir des résultats incorrects, un comportement imprévisible ou des erreurs d'exécution.

Les règles ci-dessous indiquent souvent que deux éléments de données d'accumulateur doivent avoir la même valeur. Qu'est-ce que cela signifie ? Tout dépend de ce que vous voulez faire avec le noyau. Pour une réduction mathématique telle que addint, il est en général logique que "identique" signifie "mathématiquement égal". Pour une recherche "n'importe lequel" telle que findMinAndMax ("trouver l'emplacement des valeurs minimale et maximale d'entrée"), où il peut y avoir plusieurs occurrences de valeurs d'entrée identiques, tous les emplacements d'une valeur d'entrée spécifique doivent être considérés comme "identiques". Vous pouvez écrire un noyau similaire pour "trouver l'emplacement des valeurs minimale et maximale les plus à gauche", où (par exemple) une valeur minimale à l'emplacement 100 est préférable à une valeur minimale identique à l'emplacement 200. Pour ce noyau, "identique" signifie que l'emplacement est identique, et non de même valeur, et que les fonctions d'accumulateur et de combinaison doivent être différentes de celles de findMinAndMax.

La fonction d'initialisation doit créer une valeur d'identité. Autrement dit, si I et A sont des éléments de données d'accumulateur initialisés par la fonction d'initialisation et que I n'a jamais été transmis à la fonction d'accumulateur (mais A peut l'avoir été), alors
  • combinerName(&A, &I) doit conserver A identique
  • combinerName(&I, &A) doit conserver I identique à A

Exemple : Dans le noyau addint, un élément de données d'accumulateur est initialisé sur zéro. La fonction de combinaison pour ce noyau effectue des additions. Zéro est la valeur de l'identité d'une addition.

Exemple : Dans le noyau findMinAndMax, un élément de données d'accumulateur est initialisé sur INITVAL.

  • fMMCombiner(&A, &I) conserve la valeur de A, car I est INITVAL.
  • fMMCombiner(&I, &A) définit I sur A, car I est INITVAL.

INITVAL est donc bien une valeur d'identité.

La fonction de combinaison doit être commutative. Autrement dit, si A et B sont des éléments de données d'accumulateur initialisés par la fonction d'initialisation qui ont pu être transmis à la fonction d'accumulateur zéro ou plusieurs fois, alors combinerName(&A, &B) doit définir A sur la même valeur que combinerName(&B, &A) définit B.

Exemple : Dans le noyau addint, la fonction de combinaison additionne les deux valeurs d'éléments de données d'accumulateur. L'addition est commutative.

Exemple : Dans le noyau findMinAndMax, fMMCombiner(&A, &B) est identique à A = minmax(A, B) et minmax est commutatif, donc fMMCombiner l'est aussi.

La fonction de combinaison doit être associative. Autrement dit, si A, B et C sont des éléments de données d'accumulateur initialisés par la fonction d'initialisation qui peuvent avoir été transmis à la fonction d'accumulateur zéro ou plusieurs fois, alors les deux séquences de code suivantes doivent définir A sur la même valeur :

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

Exemple : Dans le noyau addint, la fonction de combinaison additionne les valeurs des deux éléments de données d'accumulateur :

  • 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'addition est associative, de même que la fonction de combinaison.

Exemple : Dans le noyau findMinAndMax,

fMMCombiner(&A, &B)
est identique à
A = minmax(A, B)
Les deux séquences sont donc :

  • 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 est associatif, donc fMMCombiner l'est aussi.

La fonction d'accumulateur et la fonction de combinaison doivent respecter la règle de pliage de base. Autrement dit, si A et B sont des éléments de données d'accumulateur, A a été initialisé par la fonction d'initialisation et peut avoir été transmis à la fonction d'accumulateur zéro ou plusieurs fois, B n'a pas été initialisé et args est la liste des arguments d'entrée et des arguments spéciaux pour un appel spécifique à la fonction d'accumulateur. Les deux séquences de code suivantes doivent ensuite définir A sur la même valeur :

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

Exemple : Dans le noyau addint, pour une valeur d'entrée V :

  • L'instruction 1 est identique à A += V
  • L'instruction 2 est identique à B = 0
  • L'instruction 3 est identique à B += V, qui est identique à B = V
  • L'instruction 4 est identique à A += B, qui est identique à A += V

Les instructions 1 et 4 définissent A sur la même valeur. Ce noyau respecte donc la règle de pliage de base.

Exemple : Dans le noyau findMinAndMax, pour une valeur d'entrée V au point X :

  • L'instruction 1 est identique à A = minmax(A, IndexedVal(V, X))
  • L'instruction 2 est identique à B = INITVAL
  • L'instruction 3 est identique à
    B = minmax(B, IndexedVal(V, X))
    
    qui, ayant B comme valeur initiale, est identique à
    B = IndexedVal(V, X)
    
  • L'instruction 4 est identique à
    A = minmax(A, B)
    
    , qui est identique à
    A = minmax(A, IndexedVal(V, X))
    

Les instructions 1 et 4 définissent A sur la même valeur. Ce noyau respecte donc la règle de pliage de base.

Appeler un noyau de réduction à partir d'un code Java

Pour un noyau de réduction nommé kernelName défini dans le fichier filename.rs, trois méthodes sont renvoyées dans la 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);

Voici quelques exemples d'appels du noyau 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();

La méthode 1 comporte un argument d'entrée Allocation pour chaque argument d'entrée de la fonction d'accumulateur du noyau. L'environnement d'exécution RenderScript vérifie que toutes les allocations d'entrée ont les mêmes dimensions et que le type Element de chaque allocation d'entrée correspond à celui de l'argument d'entrée correspondant de la fonction d'accumulateur prototype. Si l'une de ces vérifications échoue, RenderScript renvoie une exception. Le noyau s'exécute sur toutes les coordonnées de ces dimensions.

La méthode 2 est identique à la méthode 1, mais elle utilise un argument supplémentaire sc qui peut être utilisé pour limiter l'exécution du noyau à un sous-ensemble des coordonnées.

La méthode 3 est identique à la méthode 1, mais elle accepte des entrées de tableau Java au lieu de recevoir des entrées d'allocation. Cela vous évite d'avoir à écrire du code pour créer explicitement une allocation et y copier des données à partir d'un tableau Java. Toutefois, l'utilisation de la méthode 3 au lieu de la méthode 1 n'augmente pas les performances du code. Pour chaque tableau d'entrée, la méthode 3 crée une allocation temporaire à une dimension avec le type Element adéquat et le setAutoPadding(boolean) activé, puis copie le tableau dans l'allocation comme pour la méthode copyFrom() de Allocation. Elle appelle ensuite la méthode 1 en transmettant ces allocations temporaires.

REMARQUE : Si votre application effectue plusieurs appels du noyau avec le même tableau, ou avec des tableaux différents de mêmes dimensions et type d'élément, vous pouvez améliorer les performances en créant, en insérant et en réutilisant explicitement les allocations vous-même, plutôt qu'au moyen de la méthode 3.

javaFutureType, le type renvoyé des méthodes de réduction prises en compte, est une classe statique imbriquée renvoyée dans la classe ScriptC_filename. Il représente le résultat futur d'une exécution du noyau de réduction. Pour obtenir le résultat réel de l'exécution, appelez la méthode get() de cette classe, qui renvoie une valeur de type javaResultType. get() est synchrone.

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

La valeur de javaResultType est déterminée à partir du resultType de la fonction de conversion. À moins que resultType ne soit un type non signé (scalaire, vecteur ou tableau), javaResultType est le type Java qui correspond directement. Si resultType est un type non signé et qu'il existe un type signé plus important de Java, alors javaResultType est ce type signé plus important de Java. Sinon, il s'agit du type Java qui correspond directement. Par exemple :

  • Si resultType est int, int2 ou int[15], alors javaResultType est int, Int2 ou int[]. Toutes les valeurs de resultType peuvent être représentées par javaResultType.
  • Si resultType est uint, uint2 ou uint[15], alors javaResultType est long, Long2 ou long[]. Toutes les valeurs de resultType peuvent être représentées par javaResultType.
  • Si resultType est ulong, ulong2 ou ulong[15], alors javaResultType est long, Long2 ou long[]. Certaines valeurs de resultType ne peuvent être représentées par javaResultType.

javaFutureType est le type de résultat futur correspondant au resultType de la fonction de conversion.

  • Si resultType n'est pas un type de tableau, alors javaFutureType est result_resultType.
  • Si resultType est un tableau dont la longueur correspond à Count avec des membres de type memberType, alors javaFutureType est resultArrayCount_memberType

Par exemple :

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

Si javaResultType est un type d'objet (y compris un type de tableau), chaque appel au javaFutureType.get() sur la même instance renvoie le même objet.

Si javaResultType ne peut représenter toutes les valeurs de type resultType et qu'un noyau de réduction génère une valeur non représentable, alors javaFutureType.get() génère une exception.

Méthode 3 et devecSiInXType

devecSiInXType est le type Java correspondant au inXType de l'argument correspondant de la fonction d'accumulateur. À moins que inXType ne soit un type non signé ou un type de vecteur, devecSiInXType est le type Java correspondant directement. Si inXType est un type scalaire non signé, alors devecSiInXType est le type Java correspondant directement au type scalaire signé de même taille. Si inXType est un type de vecteur signé, alors devecSiInXType est le type Java qui correspond directement au type de composant vectoriel. Si inXType est un type de vecteur non signé, alors devecSiInXType est le type Java correspondant directement au type scalaire signé de même taille que le type de composant vectoriel. Par exemple :

  • Si inXType est int, alors devecSiInXType est int.
  • Si inXType est int2, alors devecSiInXType est int. Le tableau est une représentation aplatie. Il comporte deux fois plus d'éléments scalaires que l'allocation, qui comporte des éléments vectoriels à deux composants. Il en va de même pour les méthodes copyFrom() de tâche Allocation.
  • Si inXType est uint, alors deviceSiInXType est int. Une valeur signée dans le tableau Java est interprétée comme une valeur non signée du même schéma de bits dans l'allocation. C'est le même principe que les méthodes copyFrom() de tâche Allocation.
  • Si inXType est uint2, alors deviceSiInXType est int. Il s'agit d'une combinaison des modes de traitement de int2 et de uint : le tableau est une représentation aplatie, et les valeurs signées du tableau Java sont interprétées comme des valeurs d'élément non signées RenderScript.

Notez que pour la méthode 3, les types d'entrée sont traités différemment des types de résultats :

  • L'entrée vectorielle d'un script est aplatie du côté Java, contrairement au résultat vectoriel d'un script.
  • L'entrée non signée d'un script est représentée par une entrée signée de la même taille du côté Java, tandis que le résultat non signé d'un script est représenté sous la forme d'un type signé élargi côté Java (sauf dans le cas de ulong).

Plus d'exemples de noyaux de réduction

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

Autres exemples de code

Les exemples BasicRenderScript, RenderScriptIntrinsic et Hello Compute illustrent davantage l'utilisation des API abordées sur cette page.