RenderScript – Übersicht

RenderScript ist ein Framework, mit dem rechenintensive Aufgaben auf Android-Geräten mit hoher Leistung ausgeführt werden können. RenderScript ist in erster Linie für die Verwendung mit datenparrallel ausgeführten Berechnungen gedacht, kann aber auch bei seriellen Arbeitslasten von Vorteil sein. Die RenderScript-Laufzeit parallelisiert die Arbeit auf allen auf einem Gerät verfügbaren Prozessoren, z. B. auf Multi-Core-CPUs und GPUs. So können Sie sich darauf konzentrieren, Algorithmen auszudrücken, anstatt Arbeit zu planen. RenderScript ist besonders nützlich für Anwendungen, die Bildverarbeitung, rechnergestützte Fotografie oder maschinelles Sehen nutzen.

Zu Beginn sollten Sie zwei wichtige Konzepte von RenderScript kennen:

  • Die Sprache selbst ist eine von C99 abgeleitete Sprache zum Schreiben von Hochleistungs-Computing-Code. Im Artikel RenderScript-Kernel schreiben wird beschrieben, wie Sie damit Compute-Kernel schreiben.
  • Die control API wird zum Verwalten der Lebensdauer von RenderScript-Ressourcen und zum Steuern der Kernelausführung verwendet. Es ist in drei verschiedenen Sprachen verfügbar: Java, C++ im Android NDK und die C99-basierte Kernelsprache selbst. RenderScript aus Java-Code verwenden und Single-Source RenderScript beschreiben die erste und die dritte Option.

RenderScript-Kernel schreiben

Ein RenderScript-Kernel befindet sich normalerweise in einer .rs-Datei im Verzeichnis <project_root>/src/rs. Jede .rs-Datei wird als Script bezeichnet. Jedes Script enthält eigene Kernel, Funktionen und Variablen. Ein Script kann Folgendes enthalten:

  • Eine Pragmadeklaration (#pragma version(1)), die die Version der in diesem Script verwendeten RenderScript-Kernelsprache angibt. Derzeit ist „1“ der einzige gültige Wert.
  • Eine Pragma-Deklaration (#pragma rs java_package_name(com.example.app)), die den Paketnamen der Java-Klassen angibt, die aus diesem Script abgeleitet werden. Die .rs-Datei muss Teil Ihres Anwendungspakets sein und sich nicht in einem Bibliotheksprojekt befinden.
  • Null oder mehr aufrufbare Funktionen. Eine aufrufbare Funktion ist eine einzeilige RenderScript-Funktion, die Sie mit beliebigen Argumenten aus Ihrem Java-Code aufrufen können. Sie sind oft nützlich für die Ersteinrichtung oder serielle Berechnungen innerhalb einer größeren Verarbeitungspipeline.
  • Null oder mehr Script-Globale Eine Script-Globale Variable ähnelt einer globalen Variablen in C. Sie können über Java-Code auf Script-Globale zugreifen. Diese werden häufig für die Parameterübergabe an RenderScript-Kernel verwendet. Hier finden Sie weitere Informationen zu Script-Globalen.

  • Null oder mehr Rechenkerne. Ein Compute-Kernel ist eine Funktion oder eine Sammlung von Funktionen, die die RenderScript-Laufzeit parallel auf einer Datensammlung ausführen kann. Es gibt zwei Arten von Compute-Kerneln: Mapping-Kernel (auch Foreach-Kernel genannt) und Reduktion-Kernel.

    Ein Zuordnungskern ist eine parallele Funktion, die auf einer Sammlung von Allocations mit denselben Dimensionen ausgeführt wird. Standardmäßig wird er einmal für jede Koordinate in diesen Dimensionen ausgeführt. Sie wird in der Regel (aber nicht ausschließlich) verwendet, um eine Sammlung von Eingaben Allocations in eine Ausgabe Allocation umzuwandeln, wobei jeweils ein Element Element verarbeitet wird.

    • Hier ein Beispiel für einen einfachen Mapping-Kernel:

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

      In den meisten Punkten entspricht sie einer Standard-C-Funktion. Die Eigenschaft RS_KERNEL, die auf den Funktionsprototyp angewendet wird, gibt an, dass es sich bei der Funktion um einen RenderScript-Mapping-Kernel handelt und nicht um eine aufrufbare Funktion. Das Argument in wird automatisch anhand der Eingabe Allocation ausgefüllt, die an den Kernelstart übergeben wird. Die Argumente x und y werden unten beschrieben. Der vom Kernel zurückgegebene Wert wird automatisch an den entsprechenden Speicherort in der Ausgabe Allocation geschrieben. Standardmäßig wird dieser Kernel für die gesamte EingabeAllocation ausgeführt, wobei die Kernelfunktion einmal pro Element in der Allocation ausgeführt wird.

      Ein Zuordnungskern kann eine oder mehrere Eingaben Allocations, eine einzelne Ausgabe Allocation oder beides haben. Die RenderScript-Laufzeit prüft, ob alle Eingabe- und Ausgabezuweisungen dieselben Dimensionen haben und ob die Element-Typen der Eingabe- und Ausgabezuweisungen mit dem Prototyp des Kernels übereinstimmen. Wenn eine dieser Prüfungen fehlschlägt, wirft RenderScript eine Ausnahme.

      HINWEIS:Vor Android 6.0 (API-Level 23) darf ein Zuordnungskern nicht mehr als eine Eingabe Allocation haben.

      Wenn Sie mehr Eingabe- oder Ausgabe-Allocations benötigen als der Kernel hat, sollten diese Objekte an rs_allocation-Script-Globale gebunden und über rsGetElementAt_type() oder rsSetElementAt_type() von einem Kernel oder einer aufrufbaren Funktion aus aufgerufen werden.

      HINWEIS:RS_KERNEL ist ein Makro, das automatisch von RenderScript definiert wird:

      #define RS_KERNEL __attribute__((kernel))

    Ein Reduktionskern ist eine Funktionsfamilie, die auf einer Sammlung von Eingaben Allocations mit denselben Dimensionen angewendet wird. Standardmäßig wird die Akkumulierungsfunktion einmal für jede Koordinate in diesen Dimensionen ausgeführt. Sie wird in der Regel (aber nicht ausschließlich) verwendet, um eine Sammlung von Eingaben Allocations auf einen einzelnen Wert zu reduzieren.

    • Hier ist ein Beispiel für einen einfachen Reduktionskern, der die Elements seiner Eingabe addiert:

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

      Ein Reduktionskern besteht aus einer oder mehreren vom Nutzer geschriebenen Funktionen. Mit #pragma rs reduce wird der Kernel definiert, indem sein Name (in diesem Beispiel addint) und die Namen und Rollen der Funktionen angegeben werden, aus denen der Kernel besteht (in diesem Beispiel eine accumulator-Funktion addintAccum). Alle diese Funktionen müssen static sein. Für einen Reduktionskernel ist immer eine accumulator-Funktion erforderlich. Je nach gewünschter Funktion kann er auch andere Funktionen haben.

      Eine Akkumulatorfunktion für den Reduktionskern muss void zurückgeben und mindestens zwei Argumente haben. Das erste Argument (in diesem Beispiel accum) ist ein Verweis auf ein Akkumulatordatenelement und das zweite (in diesem Beispiel val) wird automatisch basierend auf der Eingabe Allocation ausgefüllt, die an den Kernelstart übergeben wird. Das Akkumulator-Datenelement wird von der RenderScript-Laufzeit erstellt und standardmäßig auf null initialisiert. Standardmäßig wird dieser Kernel auf die gesamte EingabeAllocation angewendet, wobei die Akkumulatorfunktion einmal pro Element in der Allocation ausgeführt wird. Standardmäßig wird der Endwert des Akkumulator-Datenelements als Ergebnis der Reduzierung behandelt und an Java zurückgegeben. Die RenderScript-Laufzeit prüft, ob der Element-Typ der Eingabezuweisung mit dem Prototyp der Akkumulatorfunktion übereinstimmt. Andernfalls wirft RenderScript eine Ausnahme.

      Ein Reduktionskern hat eine oder mehrere Eingaben Allocations, aber keine Ausgabe Allocations.

      Weitere Informationen zu Reduzierungskernen

      Reduzierungskerne werden ab Android 7.0 (API-Level 24) unterstützt.

    Eine Mapping-Kernelfunktion oder eine Reducer-Kernel-Akkumulatorfunktion kann über die spezielle Argumente x, y und z auf die Koordinaten der aktuellen Ausführung zugreifen. Diese Argumente müssen vom Typ int oder uint32_t sein. Diese Argumente sind optional.

    Eine Zuordnungskernfunktion oder eine Reduktionskern-Akkumulatorfunktion kann auch das optionale Spezialargument context vom Typ rs_kernel_context annehmen. Sie wird von einer Reihe von Laufzeit-APIs benötigt, mit denen bestimmte Eigenschaften der aktuellen Ausführung abgefragt werden, z. B. rsGetDimX. Das Argument context ist ab Android 6.0 (API-Level 23) verfügbar.

  • Eine optionale init()-Funktion. Die Funktion init() ist eine spezielle Art von aufrufbarer Funktion, die von RenderScript ausgeführt wird, wenn das Script zum ersten Mal instanziiert wird. So können einige Berechnungen beim Erstellen des Scripts automatisch ausgeführt werden.
  • Null oder mehr statische Script-Globale und ‑Funktionen Ein statisches Script-Global entspricht einem Script-Global, mit der Ausnahme, dass darauf nicht über Java-Code zugegriffen werden kann. Eine statische Funktion ist eine Standard-C-Funktion, die von jedem Kernel oder jeder aufrufbaren Funktion im Script aufgerufen werden kann, aber nicht für die Java API freigegeben ist. Wenn auf eine globale Variable oder Funktion eines Scripts nicht über Java-Code zugegriffen werden muss, wird dringend empfohlen, sie als static zu deklarieren.

Gleitkommagenauigkeit festlegen

Sie können die erforderliche Gleitkommagenauigkeit in einem Script steuern. Dies ist nützlich, wenn der vollständige IEEE 754-2008-Standard (standardmäßig verwendet) nicht erforderlich ist. Mit den folgenden Pragmas kann eine andere Gleitkommagenauigkeit festgelegt werden:

  • #pragma rs_fp_full (Standardwert, wenn nichts angegeben ist): Für Apps, die die Gleitkommagenauigkeit gemäß dem IEEE 754-2008-Standard erfordern.
  • #pragma rs_fp_relaxed: Für Apps, die keine strikte Einhaltung von IEEE 754-2008 erfordern und eine geringere Genauigkeit tolerieren können. In diesem Modus wird die Nullung für Denormalisierungen und die Rundung auf Null aktiviert.
  • #pragma rs_fp_imprecise: Für Apps, die keine strengen Genauigkeitsanforderungen haben. In diesem Modus sind alle Funktionen in rs_fp_relaxed sowie die folgenden Funktionen aktiviert:
    • Bei Vorgängen, die zu -0,0 führen, kann stattdessen +0,0 zurückgegeben werden.
    • Vorgänge mit INF und NAN sind nicht definiert.

Die meisten Anwendungen können rs_fp_relaxed ohne Nebenwirkungen verwenden. Dies kann bei einigen Architekturen sehr vorteilhaft sein, da zusätzliche Optimierungen nur mit reduzierter Genauigkeit verfügbar sind (z. B. SIMD-CPU-Anweisungen).

Auf RenderScript APIs aus Java zugreifen

Wenn Sie eine Android-Anwendung entwickeln, die RenderScript verwendet, haben Sie zwei Möglichkeiten, von Java aus auf die API zuzugreifen:

Hier sind die Vor- und Nachteile:

  • Wenn Sie die APIs der Support Library verwenden, ist der RenderScript-Teil Ihrer Anwendung unabhängig von den verwendeten RenderScript-Funktionen mit Geräten mit Android 2.3 (API-Level 9) und höher kompatibel. So kann Ihre Anwendung auf mehr Geräten ausgeführt werden als bei Verwendung der nativen APIs (android.renderscript).
  • Bestimmte RenderScript-Funktionen sind über die Support Library APIs nicht verfügbar.
  • Wenn Sie die Support Library APIs verwenden, erhalten Sie (möglicherweise deutlich) größere APKs als bei Verwendung der nativen APIs (android.renderscript).

APIs der RenderScript-Unterstützungsbibliothek verwenden

Damit Sie die RenderScript APIs der Support Library verwenden können, müssen Sie Ihre Entwicklungsumgebung so konfigurieren, dass Sie darauf zugreifen können. Für die Verwendung dieser APIs sind die folgenden Android SDK-Tools erforderlich:

  • Android SDK Tools Version 22.2 oder höher
  • Android SDK Build-Tools, Version 18.1.0 oder höher

Ab den Android SDK Build-Tools 24.0.0 wird Android 2.2 (API-Level 8) nicht mehr unterstützt.

Sie können die installierte Version dieser Tools im Android SDK Manager prüfen und aktualisieren.

So verwenden Sie die RenderScript APIs der Support Library:

  1. Prüfen Sie, ob die erforderliche Android SDK-Version installiert ist.
  2. Aktualisieren Sie die Einstellungen für den Android-Buildprozess, um die RenderScript-Einstellungen aufzunehmen:
    • Öffnen Sie die Datei build.gradle im App-Ordner Ihres Anwendungsmoduls.
    • Fügen Sie der Datei die folgenden RenderScript-Einstellungen hinzu:

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

      Mit den oben aufgeführten Einstellungen wird das Verhalten des Android-Build-Prozesses gesteuert:

      • renderscriptTargetApi – Gibt die zu generierende Bytecodeversion an. Wir empfehlen, diesen Wert auf die niedrigste API-Ebene festzulegen, die alle von Ihnen verwendeten Funktionen bereitstellen kann. Legen Sie renderscriptSupportModeEnabled auf true fest. Gültige Werte für diese Einstellung sind beliebige Ganzzahlwerte von 11 bis zur aktuellsten API-Version. Wenn die im Anwendungsmanifest angegebene minimale SDK-Version auf einen anderen Wert festgelegt ist, wird dieser Wert ignoriert und die minimale SDK-Version wird anhand des Zielwerts in der Build-Datei festgelegt.
      • renderscriptSupportModeEnabled – Gibt an, dass der generierte Bytecode auf eine kompatible Version zurückfallen soll, wenn das Gerät, auf dem er ausgeführt wird, die Zielversion nicht unterstützt.
  3. Fügen Sie in Ihren Anwendungsklassen, die RenderScript verwenden, einen Import für die Klassen der Supportbibliothek hinzu:

    Kotlin

    import android.support.v8.renderscript.*

    Java

    import android.support.v8.renderscript.*;

RenderScript aus Java- oder Kotlin-Code verwenden

Die Verwendung von RenderScript aus Java- oder Kotlin-Code setzt die API-Klassen im Paket android.renderscript oder android.support.v8.renderscript voraus. Die meisten Anwendungen folgen demselben grundlegenden Nutzungsmuster:

  1. Initialisieren Sie einen RenderScript-Kontext. Der mit create(Context) erstellte RenderScript-Kontext sorgt dafür, dass RenderScript verwendet werden kann, und bietet ein Objekt, mit dem die Lebensdauer aller nachfolgenden RenderScript-Objekte gesteuert werden kann. Die Kontexterstellung sollte als potenziell langwieriger Vorgang betrachtet werden, da dadurch Ressourcen auf verschiedenen Hardwarekomponenten erstellt werden können. Sie sollte nach Möglichkeit nicht im kritischen Pfad einer Anwendung liegen. Normalerweise hat eine Anwendung jeweils nur einen einzigen RenderScript-Kontext.
  2. Erstellen Sie mindestens eine Allocation, die an ein Script übergeben werden soll. Ein Allocation ist ein RenderScript-Objekt, das Speicherplatz für eine feste Datenmenge bietet. Kernel in Scripts nehmen Allocation-Objekte als Eingabe und Ausgabe an. Auf Allocation-Objekte kann in Kerneln mit rsGetElementAt_type() und rsSetElementAt_type() zugegriffen werden, wenn sie als Script-Globale gebunden sind. Mit Allocation-Objekten können Arrays von Java-Code an RenderScript-Code und umgekehrt übergeben werden. Allocation-Objekte werden in der Regel mit createTyped() oder createFromBitmap() erstellt.
  3. Erstellen Sie alle erforderlichen Scripts. Bei der Verwendung von RenderScript stehen zwei Arten von Scripts zur Verfügung:
    • ScriptC: Dies sind die benutzerdefinierten Scripts, die oben unter RenderScript-Kernel schreiben beschrieben wurden. Jedes Script hat eine Java-Klasse, die vom RenderScript-Compiler berücksichtigt wird, um den Zugriff auf das Script über Java-Code zu vereinfachen. Diese Klasse hat den Namen ScriptC_filename. Wenn sich der Mapping-Kernel beispielsweise unter invert.rs und ein RenderScript-Kontext bereits unter mRenderScript befindet, lautet der Java- oder Kotlin-Code zum Instanziieren des Scripts:

      Kotlin

      val invert = ScriptC_invert(renderScript)

      Java

      ScriptC_invert invert = new ScriptC_invert(renderScript);
    • ScriptIntrinsic: Dies sind integrierte RenderScript-Kernel für gängige Vorgänge wie Gaußscher Weichzeichner, Convolution und Bildmischung. Weitere Informationen finden Sie in den Unterklassen von ScriptIntrinsic.
  4. Zuweisungen mit Daten füllen Mit Ausnahme von Zuordnungen, die mit createFromBitmap() erstellt wurden, werden Zuordnungen beim Erstellen mit leeren Daten ausgefüllt. Verwenden Sie eine der Kopiermethoden in Allocation, um eine Zuweisung zu erstellen. Die „copy“-Methoden sind synchron.
  5. Legen Sie alle erforderlichen Script-Globale fest. Sie können Globals mithilfe von Methoden in derselben ScriptC_filename-Klasse mit dem Namen set_globalname festlegen. Wenn Sie beispielsweise eine int-Variable mit dem Namen threshold festlegen möchten, verwenden Sie die Java-Methode set_threshold(int). Wenn Sie eine rs_allocation-Variable mit dem Namen lookup festlegen möchten, verwenden Sie die Java-Methode set_lookup(Allocation). Die set-Methoden sind asynchron.
  6. Starten Sie die entsprechenden Kernel und aufrufbaren Funktionen.

    Methoden zum Starten eines bestimmten Kernels sind in derselben ScriptC_filename-Klasse mit Methoden namens forEach_mappingKernelName() oder reduce_reductionKernelName() enthalten. Diese Einführungen sind asynchron. Je nach den Argumenten für den Kernel nimmt die Methode eine oder mehrere Zuweisungen an, die alle dieselben Dimensionen haben müssen. Standardmäßig wird ein Kernel auf allen Koordinaten in diesen Dimensionen ausgeführt. Wenn Sie einen Kernel auf einer Teilmenge dieser Koordinaten ausführen möchten, übergeben Sie der Methode forEach oder reduce als letztes Argument ein geeignetes Script.LaunchOptions.

    Sie können aufrufbare Funktionen mit den invoke_functionName-Methoden starten, die in derselben ScriptC_filename-Klasse enthalten sind. Diese Einführungen sind asynchron.

  7. Daten aus Allocation-Objekten und javaFutureType-Objekten abrufen. Wenn Sie über Java-Code auf Daten aus einer Allocation zugreifen möchten, müssen Sie diese Daten mit einer der „copy“-Methoden in Allocation zurück in Java kopieren. Um das Ergebnis eines Reduktionskerns zu erhalten, müssen Sie die Methode javaFutureType.get() verwenden. Die Methoden „copy“ und get() sind synchron.
  8. RenderScript-Kontext abbauen Sie können den RenderScript-Kontext mit destroy() löschen oder zulassen, dass das RenderScript-Kontextobjekt durch die Garbage Collection gelöscht wird. Dies führt dazu, dass bei jeder weiteren Verwendung eines Objekts, das zu diesem Kontext gehört, eine Ausnahme ausgelöst wird.

Asynchrones Ausführungsmodell

Die reflektierten Methoden forEach, invoke, reduce und set sind asynchron. Jede kann zu Java zurückkehren, bevor die angeforderte Aktion abgeschlossen ist. Die einzelnen Aktionen werden jedoch in der Reihenfolge ihrer Ausführung serialisiert.

Die Klasse Allocation bietet „copy“-Methoden zum Kopieren von Daten in und aus Zuordnungen. Eine „copy“-Methode ist synchron und wird im Hinblick auf alle der oben genannten asynchronen Aktionen serialisiert, die sich auf dieselbe Zuweisung beziehen.

Die reflektierten javaFutureType-Klassen bieten eine get()-Methode, um das Ergebnis einer Reduzierung zu erhalten. get() ist synchron und wird in Bezug auf die Reduzierung (die asynchron ist) serialisiert.

RenderScript mit einer einzelnen Quelle

Android 7.0 (API-Level 24) führt eine neue Programmierfunktion namens Single-Source-RenderScript ein. Dabei werden Kernel nicht über Java, sondern über das Script gestartet, in dem sie definiert sind. Dieser Ansatz ist derzeit auf Mapping-Kernel beschränkt, die in diesem Abschnitt aus Gründen der Übersichtlichkeit einfach als „Kernel“ bezeichnet werden. Mit dieser neuen Funktion können Sie auch Zuweisungen vom Typ rs_allocation direkt im Script erstellen. Es ist jetzt möglich, einen ganzen Algorithmus ausschließlich in einem Script zu implementieren, auch wenn mehrere Kernelstarts erforderlich sind. Der Vorteil ist zweifach: besser lesbarer Code, da die Implementierung eines Algorithmus in einer Sprache erfolgt, und potenziell schnellerer Code, da bei mehreren Kernelstarts weniger Übergänge zwischen Java und RenderScript erforderlich sind.

In Single-Source RenderScript schreiben Sie Kernel wie unter RenderScript-Kernel schreiben beschrieben. Sie schreiben dann eine aufrufbare Funktion, die rsForEach() aufruft, um sie zu starten. Diese API nimmt eine Kernelfunktion als ersten Parameter an, gefolgt von Eingabe- und Ausgabezuweisungen. Eine ähnliche API rsForEachWithOptions() nimmt ein zusätzliches Argument vom Typ rs_script_call_t an, das eine Teilmenge der Elemente aus den Eingabe- und Ausgabezuweisungen für die zu verarbeitende Kernelfunktion angibt.

Um die RenderScript-Berechnung zu starten, rufen Sie die aufrufbare Funktion aus Java auf. Folgen Sie der Anleitung unter RenderScript aus Java-Code verwenden. Rufen Sie im Schritt Die entsprechenden Kernel starten die aufrufbare Funktion mit invoke_function_name() auf. Dadurch wird die gesamte Berechnung gestartet, einschließlich des Startens der Kernel.

Zuweisungen sind oft erforderlich, um Zwischenergebnisse von einem Kernelstart an einen anderen zu speichern und weiterzugeben. Sie können sie mit rsCreateAllocation() erstellen. Eine nutzerfreundliche Form dieser API ist rsCreateAllocation_<T><W>(…), wobei T der Datentyp für ein Element und W die Vektorbreite für das Element ist. Die API akzeptiert die Größen in den Dimensionen X, Y und Z als Argumente. Bei 1D- oder 2D-Zuordnungen kann die Größe für die Dimension Y oder Z weggelassen werden. Mit rsCreateAllocation_uchar4(16384) wird beispielsweise eine 1D-Zuordnung von 16.384 Elementen erstellt, die alle vom Typ uchar4 sind.

Die Zuweisungen werden automatisch vom System verwaltet. Sie müssen sie nicht explizit freigeben. Sie können jedoch rsClearObject(rs_allocation* alloc) aufrufen, um anzugeben, dass Sie den Handle alloc für die zugrunde liegende Zuweisung nicht mehr benötigen, damit das System die Ressourcen so früh wie möglich freigeben kann.

Der Abschnitt RenderScript-Kernel schreiben enthält ein Beispiel für einen Kernel, der ein Bild invertiert. Im folgenden Beispiel wird dies erweitert, um mithilfe von Single-Source RenderScript mehr als einen Effekt auf ein Bild anzuwenden. Es enthält einen weiteren Kernel, greyscale, der ein Farbbild in Schwarz-Weiß umwandelt. Eine aufrufbare Funktion process() wendet diese beiden Kernel dann nacheinander auf ein Eingabebild an und erzeugt ein Ausgabebild. Zuweisungen für die Eingabe und die Ausgabe werden als Argumente vom Typ rs_allocation übergeben.

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

So rufen Sie die Funktion process() in Java oder Kotlin auf:

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

In diesem Beispiel wird gezeigt, wie ein Algorithmus mit zwei Kernelausführungen vollständig in der RenderScript-Sprache implementiert werden kann. Ohne Single-Source-RenderScript müssten Sie beide Kernel aus dem Java-Code starten, was die Kernelstarts von den Kerneldefinitionen trennt und das Verständnis des gesamten Algorithmus erschwert. Der Single-Source-RenderScript-Code ist nicht nur leichter zu lesen, sondern es entfällt auch die Umstellung zwischen Java und dem Script bei Kernelstarts. Einige iterative Algorithmen können Kernel hunderte Male starten, was den Overhead solcher Übergänge erheblich erhöht.

Script-Globale Variablen

Eine globale Variable ist eine gewöhnliche globale Variable, die nicht static ist, in einer Scriptdatei (.rs). Für eine globale Variable namens var, die in der Datei filename.rs definiert ist, gibt es eine Methode get_var, die in der Klasse ScriptC_filename enthalten ist. Sofern das globale Element nicht const ist, gibt es auch eine Methode set_var.

Ein bestimmtes Script-Global hat zwei separate Werte: einen Java-Wert und einen Script-Wert. Diese Werte haben folgende Auswirkungen:

  • Wenn var im Script einen statischen Initialisierer hat, wird der Anfangswert von var sowohl in Java als auch im Script festgelegt. Andernfalls ist der Anfangswert null.
  • Zugriffe auf var im Script lesen und schreiben den Scriptwert.
  • Die Methode get_var liest den Java-Wert.
  • Die Methode set_var (falls vorhanden) schreibt den Java-Wert sofort und den Scriptwert asynchron.

HINWEIS:Das bedeutet, dass Werte, die innerhalb eines Scripts in ein globales Element geschrieben werden, mit Ausnahme von statischen Initialisierern im Script für Java nicht sichtbar sind.

Reduzierungskerne im Detail

Bei der Reduzierung werden mehrere Daten zu einem einzelnen Wert kombiniert. Dies ist ein nützliches Primitive in der parallelen Programmierung, z. B. für folgende Anwendungen:

  • die Summe oder das Produkt aller Daten berechnen
  • Logische Vorgänge (and, or, xor) für alle Daten berechnen
  • den Mindest- oder Maximalwert in den Daten ermitteln
  • nach einem bestimmten Wert oder nach der Koordinate eines bestimmten Werts in den Daten suchen

Unter Android 7.0 (API-Ebene 24) und höher unterstützt RenderScript Reduktionskerne, um effiziente von Nutzern geschriebene Reduktionsalgorithmen zu ermöglichen. Sie können Kernel zur Datenreduktion auf Eingaben mit 1, 2 oder 3 Dimensionen anwenden.

Das obige Beispiel zeigt einen einfachen addint-Reduktionskernel. Hier ist ein etwas komplizierterer findMinAndMax-Reduktionskern, der die Positionen der minimalen und maximalen long-Werte in einem eindimensionalen Allocation ermittelt:

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

HINWEIS:Weitere Beispielkerne für die Reduktion finden Sie hier.

Um einen Reduktionskernel auszuführen, erstellt die RenderScript-Laufzeit eine oder mehrere Variablen namens Akkumulatordatenelemente, um den Status des Reduktionsprozesses zu speichern. Die RenderScript-Laufzeit wählt die Anzahl der Akkumulatordatenelemente so aus, dass die Leistung maximiert wird. Der Typ der Akkumulatordatenelemente (accumType) wird durch die Akkumulatorfunktion des Kernels bestimmt. Das erste Argument dieser Funktion ist ein Verweis auf ein Akkumulatordatenelement. Standardmäßig wird jedes Akkumulatordatenelement auf null initialisiert (wie bei memset). Sie können jedoch eine Initialisierungsfunktion schreiben, um etwas anderes zu tun.

Beispiel:Im Kernel addint werden die Akkumulator-Datenelemente (vom Typ int) verwendet, um Eingabewerte zu addieren. Da es keine Initialisierungsfunktion gibt, wird jedes Akkumulatordatenelement auf null initialisiert.

Beispiel:Im Kernel findMinAndMax werden die Akkumulatordatenelemente (vom Typ MinAndMax) verwendet, um die bisher gefundenen Minimal- und Maximalwerte im Blick zu behalten. Es gibt eine Initialisierungsfunktion, mit der diese Werte auf LONG_MAX bzw. LONG_MIN festgelegt und die Positionen dieser Werte auf -1 gesetzt werden können, was bedeutet, dass die Werte nicht im (leeren) Teil der verarbeiteten Eingabe vorhanden sind.

RenderScript ruft Ihre Akkumulatorfunktion einmal für jede Koordinate in den Eingaben auf. Normalerweise sollte Ihre Funktion das Akkumulatordatenelement auf irgendeine Weise entsprechend der Eingabe aktualisieren.

Beispiel:Im Kernel addint fügt die Akkumulatorfunktion dem Akkumulatordatenelement den Wert eines Eingabeelements hinzu.

Beispiel:Im findMinAndMax-Kernel prüft die Akkumulatorfunktion, ob der Wert eines Eingabeelements kleiner oder gleich dem im Akkumulatordatenelement aufgezeichneten Mindestwert und/oder größer oder gleich dem im Akkumulatordatenelement aufgezeichneten Maximalwert ist. Anschließend aktualisiert sie das Akkumulatordatenelement entsprechend.

Nachdem die Akkumulatorfunktion einmal für jede Koordinate in den Eingaben aufgerufen wurde, muss RenderScript die Akkumulatordatenelemente zusammenführen, um ein einzelnes Akkumulatordatenelement zu erhalten. Dazu können Sie eine Kombinatorische Funktion schreiben. Wenn die Akkumulatorfunktion nur eine Eingabe und keine besonderen Argumente hat, müssen Sie keine Kombinationsfunktion schreiben. RenderScript verwendet die Akkumulatorfunktion, um die Akkumulatordatenelemente zu kombinieren. Sie können aber trotzdem eine Kombinationsfunktion schreiben, wenn Sie dieses Standardverhalten nicht wünschen.

Beispiel:Im Kernel addint gibt es keine Kombinatorfunktion. Daher wird die Akkumulatorfunktion verwendet. Das ist das richtige Verhalten, denn wenn wir eine Sammlung von Werten in zwei Teile aufteilen und die Werte in diesen beiden Teilen separat addieren, ist das Ergebnis dasselbe wie die Addition der gesamten Sammlung.

Beispiel:Im findMinAndMax-Kernel prüft die Kombinationsfunktion, ob der im Akkumulatordatenelement „Quelle“ *val aufgezeichnete Mindestwert kleiner als der im Akkumulatordatenelement „Ziel“ *accum aufgezeichnete Mindestwert ist. *accum wird entsprechend aktualisiert. Ähnlich funktioniert es für den Höchstwert. Dadurch wird *accum auf den Zustand aktualisiert, den es hätte, wenn alle Eingabewerte in *accum und nicht teilweise in *accum und teilweise in *val zusammengefasst worden wären.

Nachdem alle Akkumulatordatenelemente kombiniert wurden, ermittelt RenderScript das Ergebnis der Reduzierung, das an Java zurückgegeben wird. Dazu können Sie eine Out-Converter-Funktion schreiben. Sie müssen keine Ausgabekonvertierungsfunktion schreiben, wenn der endgültige Wert der kombinierten Akkumulatordatenelemente das Ergebnis der Reduzierung sein soll.

Beispiel:Im Kernel addint gibt es keine Outconverter-Funktion. Der endgültige Wert der kombinierten Datenelemente ist die Summe aller Elemente der Eingabe. Dies ist der Wert, den wir zurückgeben möchten.

Beispiel:Im findMinAndMax-Kernel initialisiert die Outconverter-Funktion einen int2-Ergebniswert, um die Positionen des Minimum- und des Maximumwerts zu speichern, die sich aus der Kombination aller Akkumulatordatenelemente ergeben.

Einen Reduktionskernel schreiben

#pragma rs reduce definiert einen Reduktionskern, indem der Name und die Namen und Rollen der Funktionen angegeben werden, aus denen der Kern besteht. Alle diese Funktionen müssen static sein. Für einen Reduktionskernel ist immer eine accumulator-Funktion erforderlich. Je nach gewünschter Funktion können Sie einige oder alle anderen Funktionen weglassen.

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

Die Bedeutung der Elemente in #pragma ist folgende:

  • reduce(kernelName) (erforderlich): Gibt an, dass ein Reduktionskern definiert wird. Eine reflektierte Java-Methode reduce_kernelName startet den Kernel.
  • initializer(initializerName) (optional): Gibt den Namen der Initialisierungsfunktion für diesen Reduktionskern an. Wenn Sie den Kernel starten, ruft RenderScript diese Funktion einmal für jedes Akkumulatordatenelement auf. Die Funktion muss so definiert sein:

    static void initializerName(accumType *accum) {  }

    accum ist ein Verweis auf ein Akkumulatordatenelement, das von dieser Funktion initialisiert werden soll.

    Wenn Sie keine Initialisierungsfunktion angeben, initialisiert RenderScript jedes Akkumulatordatenelement auf null (wie mit memset), als gäbe es eine Initialisierungsfunktion, die so aussieht:

    static void initializerName(accumType *accum) {
      memset(accum, 0, sizeof(*accum));
    }
  • accumulator(accumulatorName) (erforderlich): Gibt den Namen der Akkumulatorfunktion für diesen Reduktionskernel an. Wenn Sie den Kernel starten, ruft RenderScript diese Funktion einmal für jede Koordinate in den Eingaben auf, um ein Akkumulatordatenelement auf irgendeine Weise gemäß den Eingaben zu aktualisieren. Die Funktion muss so definiert sein:

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

    accum ist ein Verweis auf ein Akkumulatordatenelement, das von dieser Funktion geändert werden soll. in1 bis inN sind ein oder mehrere Argumente, die automatisch basierend auf den Eingaben ausgefüllt werden, die an den Kernelstart übergeben werden, ein Argument pro Eingabe. Die Akkumulatorfunktion kann optional eines der besonderen Argumente annehmen.

    Ein Beispiel für einen Kernel mit mehreren Eingaben ist dotProduct.

  • combiner(combinerName)

    (Optional): Gibt den Namen der Kombinationsfunktion für diesen Reduktionskern an. Nachdem RenderScript die Akkumulatorfunktion einmal für jede Koordinate in den Eingaben aufgerufen hat, ruft es diese Funktion so oft wie nötig auf, um alle Akkumulatordatenelemente in einem einzigen Akkumulatordatenelement zu kombinieren. Die Funktion muss so definiert sein:

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

    accum ist ein Verweis auf ein Akkumulator-Datenelement vom Typ „Ziel“, das von dieser Funktion geändert werden soll. other ist ein Verweis auf ein Akkumulatordatenelement vom Typ „Quelle“, das von dieser Funktion mit *accum „kombiniert“ werden soll.

    HINWEIS:Es ist möglich, dass *accum, *other oder beide initialisiert, aber nie an die Akkumulatorfunktion übergeben wurden. Das bedeutet, dass eine oder beide Variablen nie anhand von Eingabedaten aktualisiert wurden. Im Kernel findMinAndMax wird beispielsweise in der Kombinationsfunktion fMMCombiner explizit nach idx < 0 gesucht, da dies auf ein solches Akkumulatordatenelement mit dem Wert INITVAL hinweist.

    Wenn Sie keine Combiner-Funktion angeben, verwendet RenderScript stattdessen die Akkumulatorfunktion. Das Verhalten entspricht dann dem einer Combiner-Funktion, die so aussieht:

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

    Eine Kombinationsfunktion ist erforderlich, wenn der Kernel mehr als eine Eingabe hat, wenn der Datentyp der Eingabe nicht mit dem Datentyp des Akkumulators übereinstimmt oder wenn die Akkumulatorfunktion ein oder mehrere besondere Argumente annimmt.

  • outconverter(outconverterName) (optional): Gibt den Namen der Ausgabekonvertierungsfunktion für diesen Reduktionskern an. Nachdem RenderScript alle Akkumulatordatenelemente kombiniert hat, ruft es diese Funktion auf, um das Ergebnis der Reduzierung zu ermitteln, das an Java zurückgegeben wird. Die Funktion muss so definiert sein:

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

    result ist ein Verweis auf ein Ergebnisdatenelement, das von der RenderScript-Laufzeit zugewiesen, aber nicht initialisiert wurde. Diese Funktion initialisiert es mit dem Ergebnis der Reduzierung. resultType ist der Typ dieses Datenelements, der nicht mit accumType übereinstimmen muss. accum ist ein Verweis auf das endgültige Akkumulatordatenelement, das von der Kombinatorfunktion berechnet wird.

    Wenn Sie keine Outconverter-Funktion angeben, kopiert RenderScript das endgültige Akkumulatordatenelement in das Ergebnisdatenelement. Es verhält sich so, als gäbe es eine Outconverter-Funktion mit folgendem Code:

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

    Wenn Sie einen anderen Ergebnistyp als den Accumulator-Datentyp benötigen, ist die Outconverter-Funktion obligatorisch.

Ein Kernel hat Eingabetypen, einen Akkumulatordatenelementtyp und einen Ergebnistyp, die nicht unbedingt identisch sein müssen. Im Kernel findMinAndMax sind beispielsweise der Eingabetyp long, der Typ des Akkumulatordatenelements MinAndMax und der Ergebnistyp int2 unterschiedlich.

Was können Sie nicht annehmen?

Sie dürfen sich nicht auf die Anzahl der Akkumulatordatenelemente verlassen, die von RenderScript für einen bestimmten Kernelstart erstellt wurden. Es gibt keine Garantie dafür, dass bei zwei Ausführungen desselben Kernels mit denselben Eingaben dieselbe Anzahl von Akkumulatordatenelementen erstellt wird.

Sie dürfen sich nicht auf die Reihenfolge verlassen, in der RenderScript die Initialisierer-, Akkumulator- und Kombinatorfunktionen aufruft. Es kann sogar sein, dass einige davon parallel aufgerufen werden. Es gibt keine Garantie dafür, dass zwei Starts desselben Kernels mit derselben Eingabe derselben Reihenfolge folgen. Die einzige Garantie besteht darin, dass nur die Initialisierungsfunktion ein nicht initialisiertes Akkumulator-Datenelement sieht. Beispiel:

  • Es gibt keine Garantie dafür, dass alle Akkumulatordatenelemente vor dem Aufruf der Akkumulatorfunktion initialisiert werden. Sie wird jedoch nur für ein initialisiertes Akkumulatordatenelement aufgerufen.
  • Die Reihenfolge, in der Eingabeelemente an die Akkumulatorfunktion übergeben werden, kann nicht garantiert werden.
  • Es gibt keine Garantie dafür, dass die Akkumulatorfunktion für alle Eingabeelemente aufgerufen wurde, bevor die Kombinatorfunktion aufgerufen wird.

Eine Folge davon ist, dass der findMinAndMax-Kernel nicht deterministisch ist: Wenn die Eingabe mehr als ein Vorkommen desselben Mindest- oder Höchstwerts enthält, können Sie nicht wissen, welches Vorkommen der Kernel findet.

Was müssen Sie garantieren?

Da das RenderScript-System einen Kernel auf viele verschiedene Arten ausführen kann, müssen Sie bestimmte Regeln einhalten, damit sich der Kernel wie gewünscht verhält. Wenn Sie diese Regeln nicht einhalten, erhalten Sie möglicherweise falsche Ergebnisse, nicht deterministisches Verhalten oder Laufzeitfehler.

In den folgenden Regeln wird häufig angegeben, dass zwei Akkumulatordatenelemente denselben Wert haben müssen. Was bedeutet das? Das hängt davon ab, was Sie mit dem Kernel tun möchten. Bei einer mathematischen Reduktion wie addint ist es in der Regel sinnvoll, wenn „das Gleiche“ mathematische Gleichheit bedeutet. Bei einer „beliebigen“ Suche wie findMinAndMax („Speicherort des minimalen und maximalen Eingabewerts ermitteln“), bei der es mehrere Vorkommen identischer Eingabewerte geben kann, müssen alle Speicherorte eines bestimmten Eingabewerts als „identisch“ betrachtet werden. Sie könnten einen ähnlichen Kernel schreiben, um den linksesten Mindest- und Höchstwert der Eingabewerte zu ermitteln, wobei ein Mindestwert an Position 100 beispielsweise einem identischen Mindestwert an Position 200 vorgezogen wird. Bei diesem Kernel würde „identisch“ den identischen Speicherort und nicht nur den identischen Wert bedeuten. Die Akkumulator- und Kombinatorfunktionen müssten sich von denen für findMinAndMax unterscheiden.

Die Initialisierfunktion muss einen Identitätswert erstellen. Wenn also I und A Akkumulatordatenelemente sind, die von der Initialisierungsfunktion initialisiert wurden, und I nie an die Akkumulatorfunktion übergeben wurde (A aber möglicherweise), gilt:

Beispiel:Im Kernel addint wird ein Akkumulatordatenelement auf Null initialisiert. Die Kombinationsfunktion für diesen Kernel führt eine Addition durch. Null ist der Identitätswert für die Addition.

Beispiel:Im Kernel findMinAndMax wird ein Akkumulatordatenelement mit INITVAL initialisiert.

  • fMMCombiner(&A, &I) lässt A unverändert, da I INITVAL ist.
  • fMMCombiner(&I, &A) legt I auf A fest, da I INITVAL ist.

Daher ist INITVAL tatsächlich ein Identitätswert.

Die Kombinationsfunktion muss kommutativ sein. Wenn A und B Akkumulatordatenelemente sind, die von der Initialisierungsfunktion initialisiert wurden und die der Akkumulatorfunktion möglicherweise null oder mehrmals übergeben wurden, muss combinerName(&A, &B) A auf denselben Wert setzen, auf den combinerName(&B, &A) B setzt.

Beispiel:Im Kernel addint addiert die Kombinationsfunktion die beiden Werte der Akkumulatordatenelemente. Die Addition ist kommutativ.

Beispiel:Im Kernel findMinAndMax ist fMMCombiner(&A, &B) mit A = minmax(A, B) identisch und da minmax kommutativ ist, gilt das auch für fMMCombiner.

Die Kombinationsfunktion muss assoziativ sein. Wenn also A, B und C Akkumulatordatenelemente sind, die von der Initialisierungsfunktion initialisiert wurden und die der Akkumulatorfunktion null oder mehrmals übergeben wurden, müssen die folgenden beiden Codefolgen A auf denselben Wert setzen:

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

Beispiel:Im Kernel addint addiert die Kombinationsfunktion die beiden Werte des Akkumulatordatenelements:

  • 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

Die Addition ist assoziativ, ebenso wie die Kombinationsfunktion.

Beispiel:Im Kernel findMinAndMax

fMMCombiner(&A, &B)
ist gleich
A = minmax(A, B)
Die beiden Sequenzen sind also:
  • 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 ist assoziativ, ebenso fMMCombiner.

Die Akkumulatorfunktion und die Kombinationsfunktion müssen zusammen der grundlegenden Faltungsregel entsprechen. Das heißt, wenn A und B Akkumulatordatenelemente sind, A von der Initialisierungsfunktion initialisiert wurde und der Akkumulatorfunktion null oder mehrmals übergeben wurde, B nicht initialisiert wurde und args die Liste der Eingabeargumente und speziellen Argumente für einen bestimmten Aufruf der Akkumulatorfunktion ist, müssen die folgenden beiden Codefolgen A auf denselben Wert setzen:

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

Beispiel:Im Kernel addint für den Eingabewert V:

  • Ausdruck 1 ist mit A += V identisch.
  • Anweisung 2 ist mit B = 0 identisch.
  • Ausdruck 3 ist mit B += V identisch, was auch für B = V gilt.
  • Aussage 4 ist mit A += B identisch, was wiederum mit A += V übereinstimmt.

In den Anweisungen 1 und 4 wird A auf denselben Wert gesetzt. Daher entspricht dieser Kernel der grundlegenden Faltregel.

Beispiel:Im findMinAndMax-Kernel für den Eingabewert V an der Koordinate X:

  • Ausdruck 1 ist mit A = minmax(A, IndexedVal(V, X)) identisch.
  • Anweisung 2 ist mit B = INITVAL identisch.
  • Aussage 3 ist dasselbe wie
    B = minmax(B, IndexedVal(V, X))
    , was da B der Anfangswert ist, dasselbe ist wie
    B = IndexedVal(V, X)
  • Aussage 4 ist dasselbe wie
    A = minmax(A, B)
    was dem entspricht
    A = minmax(A, IndexedVal(V, X))

In den Anweisungen 1 und 4 wird A auf denselben Wert gesetzt. Daher entspricht dieser Kernel der grundlegenden Faltregel.

Einen Reduktionskern aus Java-Code aufrufen

Für einen Reduktionskern namens kernelName, der in der Datei filename.rs definiert ist, sind in der Klasse ScriptC_filename drei Methoden enthalten:

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

Hier sind einige Beispiele für den Aufruf des Kernels 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();

Methode 1 hat ein Eingabeargument Allocation für jedes Eingabeargument in der Akkumulatorfunktion des Kernels. Die RenderScript-Laufzeit prüft, ob alle Eingabezuweisungen dieselben Dimensionen haben und ob der Element-Typ jeder Eingabezuweisung mit dem des entsprechenden Eingabearguments des Prototyps der Akkumulatorfunktion übereinstimmt. Wenn eine dieser Prüfungen fehlschlägt, löst RenderScript eine Ausnahme aus. Der Kernel wird für jede Koordinate in diesen Dimensionen ausgeführt.

Methode 2 entspricht Methode 1, mit der Ausnahme, dass Methode 2 ein zusätzliches Argument sc annimmt, mit dem die Kernelausführung auf eine Teilmenge der Koordinaten beschränkt werden kann.

Methode 3 entspricht Methode 1, mit der Ausnahme, dass anstelle von Zuweisungseingaben Java-Array-Eingabewerte verwendet werden. So müssen Sie keinen Code schreiben, um eine Zuweisung explizit zu erstellen und Daten aus einem Java-Array darauf zu kopieren. Die Leistung des Codes wird jedoch nicht erhöht, wenn Methode 3 anstelle von Methode 1 verwendet wird. Bei Methode 3 wird für jedes Eingabearray eine temporäre eindimensionale Zuweisung mit dem entsprechenden Element-Typ und aktivierter setAutoPadding(boolean) erstellt. Das Array wird dann in die Zuweisung kopiert, als wäre dies mit der entsprechenden copyFrom()-Methode von Allocation geschehen. Anschließend wird Methode 1 aufgerufen und die temporären Zuweisungen werden übergeben.

HINWEIS:Wenn Ihre Anwendung mehrere Kernelaufrufe mit demselben Array oder mit verschiedenen Arrays mit denselben Dimensionen und demselben Elementtyp ausführt, können Sie die Leistung verbessern, indem Sie Allokationen explizit erstellen, befüllen und wiederverwenden, anstatt Methode 3 zu verwenden.

javaFutureType, der Rückgabetyp der reflektierten Reduzierungsmethoden, ist eine reflektierte statische verschachtelte Klasse innerhalb der Klasse ScriptC_filename. Sie gibt das zukünftige Ergebnis eines reduzierten Kernellaufs an. Um das tatsächliche Ergebnis der Ausführung zu erhalten, rufen Sie die get()-Methode dieser Klasse auf. Sie gibt einen Wert vom Typ javaResultType zurück. get() ist synchron.

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 wird aus dem resultType der outconverter-Funktion ermittelt. Sofern resultType kein signierter Typ (Skalar, Vektor oder Array) ist, ist javaResultType der direkt entsprechende Java-Typ. Wenn resultType ein ungezeichenter Typ ist und es einen größeren signierten Java-Typ gibt, ist javaResultType dieser größere signierte Java-Typ. Andernfalls ist es der direkt entsprechende Java-Typ. Beispiel:

  • Wenn resultType int, int2 oder int[15] ist, ist javaResultType int, Int2 oder int[]. Alle Werte von resultType können durch javaResultType dargestellt werden.
  • Wenn resultType uint, uint2 oder uint[15] ist, ist javaResultType long, Long2 oder long[]. Alle Werte von resultType können durch javaResultType dargestellt werden.
  • Wenn resultType ulong, ulong2 oder ulong[15] ist, ist javaResultType long, Long2 oder long[]. Es gibt bestimmte Werte für resultType, die nicht durch javaResultType dargestellt werden können.

javaFutureType ist der zukünftige Ergebnistyp, der dem resultType der outconverter-Funktion entspricht.

  • Wenn resultType kein Arraytyp ist, ist javaFutureType result_resultType.
  • Wenn resultType ein Array mit der Länge Count mit Elementen vom Typ memberType ist, ist javaFutureType resultArrayCount_memberType.

Beispiel:

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

Wenn javaResultType ein Objekttyp ist (einschließlich eines Arraytyps), wird bei jedem Aufruf von javaFutureType.get() für dieselbe Instanz dasselbe Objekt zurückgegeben.

Wenn javaResultType nicht alle Werte vom Typ resultType darstellen kann und ein Reduktionskern einen nicht darstellbaren Wert erzeugt, löst javaFutureType.get() eine Ausnahme aus.

Methode 3 und devecSiInXType

devecSiInXType ist der Java-Typ, der dem inXType des entsprechenden Arguments der Akkumulierungsfunktion entspricht. Sofern inXType kein signaturloser Typ oder ein Vektortyp ist, ist devecSiInXType der direkt entsprechende Java-Typ. Wenn inXType ein ungezeichenter Skalartyp ist, ist devecSiInXType der Java-Typ, der direkt dem signierten Skalartyp derselben Größe entspricht. Wenn inXType ein signierter Vektortyp ist, ist devecSiInXType der Java-Typ, der direkt dem Vektorkomponententyp entspricht. Wenn inXType ein ungezeichner Vektortyp ist, ist devecSiInXType der Java-Typ, der direkt dem signierten Skalartyp mit derselben Größe wie der Vektorkomponententyp entspricht. Beispiel:

  • Wenn inXType int ist, ist devecSiInXType int.
  • Wenn inXType int2 ist, ist devecSiInXType int. Das Array ist eine flachte Darstellung: Es enthält doppelt so viele skaläre Elemente wie die Zuweisung Vektor-Elemente mit zwei Komponenten. Das funktioniert genauso wie bei den copyFrom()-Methoden von Allocation.
  • Wenn inXType uint ist, ist deviceSiInXType int. Ein signierter Wert im Java-Array wird als vorzeichenloser Wert mit demselben Bitmuster in der Zuweisung interpretiert. Das funktioniert genauso wie bei den copyFrom()-Methoden von Allocation.
  • Wenn inXType uint2 ist, ist deviceSiInXType int. Dies ist eine Kombination aus der Verarbeitung von int2 und uint: Das Array ist eine flache Darstellung und signierte Werte von Java-Arrays werden als nicht signierte Elementwerte von RenderScript interpretiert.

Bei Methode 3 werden Eingabetypen anders behandelt als Ergebnistypen:

  • Die Vektoreingabe eines Scripts wird auf der Java-Seite flachgelegt, das Vektorergebnis eines Scripts jedoch nicht.
  • Die ungesignierte Eingabe eines Scripts wird auf der Java-Seite als signierte Eingabe derselben Größe dargestellt, während das ungesignierte Ergebnis eines Scripts auf der Java-Seite als erweiterter signierter Typ dargestellt wird (außer bei ulong).

Weitere Beispiele für Reduktionskerne

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

Weitere Codebeispiele

Die Beispiele BasicRenderScript, RenderScriptIntrinsic und Hello Compute veranschaulichen die Verwendung der auf dieser Seite beschriebenen APIs.