RenderScript – Übersicht

RenderScript ist ein Framework zum Ausführen rechenintensiver Aufgaben mit hoher Leistung unter Android. RenderScript ist in erster Linie für die datenparallele Berechnung geeignet, obwohl auch serielle Arbeitslasten davon profitieren können. Die RenderScript-Laufzeit parallelisiert die Arbeit mit Prozessoren, die auf einem Gerät verfügbar sind, z. B. Mehrkern-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, computergestützte Fotografie oder maschinelles Sehen ausführen.

Wenn Sie mit RenderScript beginnen, sollten Sie sich mit zwei Grundkonzepten vertraut machen:

  • Die Sprache selbst ist eine von C99 abgeleitete Sprache zum Schreiben von Hochleistungs-Compute-Code. Unter RenderScript-Kernel schreiben wird beschrieben, wie Sie damit Compute-Kernel schreiben.
  • Die Control API wird zur Verwaltung der Lebensdauer von RenderScript-Ressourcen und zur Steuerung der Kernel-Ausführung verwendet. Sie ist in drei verschiedenen Sprachen verfügbar: Java, C++ in Android NDK und die C99-abgeleitete Kernelsprache selbst. Unter RenderScript aus Java Code verwenden und Single-Source RenderScript werden die erste bzw. dritte Option beschrieben.

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 Skript enthält seine eigenen Kernel, Funktionen und Variablen. Ein Skript kann Folgendes enthalten:

  • Eine Pragma-Deklaration (#pragma version(1)), die die Version der in diesem Skript verwendeten RenderScript-Kernelsprache deklariert. Derzeit ist 1 der einzige gültige Wert.
  • Eine Pragma-Deklaration (#pragma rs java_package_name(com.example.app)), die den Paketnamen der von diesem Skript reflektierten Java-Klassen angibt. Die Datei .rs muss Teil des Anwendungspakets sein und darf nicht in einem Bibliotheksprojekt enthalten sein.
  • Null oder mehr aufrufbare Funktionen. Eine aufrufbare Funktion ist eine RenderScript-Funktion mit einem einzigen Thread, die Sie über Ihren Java-Code mit beliebigen Argumenten aufrufen können. Diese sind häufig nützlich für die Ersteinrichtung oder serielle Berechnungen in einer größeren Verarbeitungspipeline.
  • Null oder mehr globale Scripts. Ein globales Skript ähnelt einer globalen Variablen in C. Sie können über Java-Code auf globale Skripts zugreifen. Diese werden häufig für die Parameterübergabe an RenderScript-Kernel verwendet. Hier finden Sie weitere Informationen zu globalen Skripts.

  • Null oder mehr Compute-Kernel. Ein Compute-Kernel ist eine Funktion oder Sammlung von Funktionen, die Sie anweisen können, dass die RenderScript-Laufzeit parallel für eine Sammlung von Daten ausgeführt wird. Es gibt zwei Arten von Compute-Kerneln: mapping-Kernel (auch foreach-Kernel genannt) und Reduktions-Kernel.

    Ein Zuordnungs-Kernel ist eine parallele Funktion, die mit einer Sammlung von Allocations derselben Dimensionen arbeitet. Standardmäßig wird sie für jede Koordinate in diesen Dimensionen einmal ausgeführt. Sie wird normalerweise (aber nicht ausschließlich) verwendet, um eine Sammlung von Eingabe-Allocations in eine Ausgabe-Allocation zu transformieren, jeweils Element nach dem anderen.

    • Hier ist ein Beispiel für einen einfachen zuordnenden 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;
      }

      Dies entspricht weitgehend einer C-Standardfunktion. Die auf den Funktionsprototyp angewendete Eigenschaft RS_KERNEL gibt an, dass die Funktion ein RenderScript-Zuordnungs-Kernel anstelle einer Aufruffunktion ist. Das Argument in wird automatisch anhand der Eingabe-Allocation ausgefüllt, die an den Kernel-Start übergeben wurde. Die Argumente x und y werden unten erläutert. Der vom Kernel zurückgegebene Wert wird automatisch an die entsprechende Stelle im Ausgabe-Allocation geschrieben. Standardmäßig wird dieser Kernel über die gesamte Eingabe-Allocation mit einer Ausführung der Kernel-Funktion pro Element im Allocation ausgeführt.

      Ein Zuordnungs-Kernel kann eine oder mehrere Eingabe-Allocations, eine einzelne Ausgabe-Allocation oder beides haben. Die RenderScript-Laufzeit prüft, ob alle Ein- und Ausgabezuordnungen dieselben Dimensionen haben und ob die Element-Typen der Ein- und Ausgabezuweisungen dem Prototyp des Kernels entsprechen. Wenn eine dieser Prüfungen fehlschlägt, löst RenderScript eine Ausnahme aus.

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

      Wenn Sie mehr Ein- oder Ausgaben benötigen, Allocations als der Kernel hat, sollten diese Objekte an rs_allocation-Skript-Globals gebunden sein und von einem Kernel oder einer Aufruffunktion über rsGetElementAt_type() oder rsSetElementAt_type() aufgerufen werden.

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

      #define RS_KERNEL __attribute__((kernel))
      

    Ein Reduktions-Kernel ist eine Funktionsgruppe, die auf einer Sammlung von Eingabe-Allocations mit denselben Dimensionen arbeitet. Standardmäßig wird die zugehörige Akkumulator-Funktion einmal für jede Koordinate in diesen Dimensionen ausgeführt. Sie wird normalerweise (aber nicht ausschließlich) verwendet, um eine Sammlung von Eingabe-Allocations auf einen einzigen Wert zu „verringern“.

    • Hier ist ein Beispiel für einen einfachen Reduktionskernel, in dem das Elements seiner Eingabe addiert wird:

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

      Ein Reduktionskernel besteht aus einer oder mehreren vom Nutzer geschriebenen Funktionen. Mit #pragma rs reduce wird der Kernel definiert. Dazu werden der Name (in diesem Beispiel addint) und die Namen und Rollen der Funktionen, aus denen der Kernel besteht (in diesem Beispiel eine accumulator-Funktion addintAccum), angegeben. Alle diese Funktionen müssen static sein. Ein Reduktions-Kernel erfordert immer eine accumulator-Funktion. Je nachdem, was der Kernel tun soll, kann er auch weitere Funktionen haben.

      Eine Akkumulatorfunktion für Reduktionskernel muss void zurückgeben und mindestens zwei Argumente haben. Das erste Argument (in diesem Beispiel accum) ist ein Zeiger auf ein Akkumulatordatenelement und das zweite Argument (in diesem Beispiel val) wird automatisch anhand der Eingabe Allocation ausgefüllt, die an den Kernel-Start übergeben wurde. Das Akkumulator-Datenelement wird von der RenderScript-Laufzeit erstellt und ist standardmäßig auf null initialisiert. Standardmäßig wird dieser Kernel über die gesamte Eingabe-Allocation mit einer Ausführung der Akkumulatorfunktion pro Element im Allocation ausgeführt. Standardmäßig wird der endgültige Wert des Akkumulator-Datenelements als Ergebnis der Reduktion behandelt und an Java zurückgegeben. Die RenderScript-Laufzeit prüft, ob der Typ Element der Eingabezuordnung mit dem Prototyp der Akkumulatorfunktion übereinstimmt. Andernfalls löst RenderScript eine Ausnahme aus.

      Ein Reduktions-Kernel hat eine oder mehrere Eingabe-Allocations, aber keine Ausgabe-Allocations.

      Reduktions-Kernel werden hier ausführlicher erläutert.

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

    Eine Zuordnungs-Kernel-Funktion oder eine Reduktions-Kernel-Akkumulatorfunktion kann mithilfe der speziellen Argumente x, y und z, die vom Typ int oder uint32_t sein müssen, auf die Koordinaten der aktuellen Ausführung zugreifen. Diese Argumente sind optional.

    Eine Zuordnungs-Kernel-Funktion oder eine Reduktions-Kernel-Akkumulatorfunktion kann auch das optionale spezielle Argument context vom Typ rs_kernel_context verwenden. Er wird von einer Familie von Laufzeit-APIs benötigt, mit denen bestimmte Attribute 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 init()-Funktion ist eine spezielle Art von Aufruffunktion, die RenderScript bei der ersten Instanziierung des Skripts ausführt. Dadurch kann bereits bei der Skripterstellung ein gewisses Maß an Berechnung automatisch ausgeführt werden.
  • Null oder mehr statische Skriptglobale und -funktionen. Eine statische Skriptdatei entspricht einem globalen Skript. Der einzige Unterschied besteht darin, dass über Java-Code nicht darauf zugegriffen werden kann. Eine statische Funktion ist eine Standard-C-Funktion, die von jedem Kernel oder jeder Aufruffunktion im Skript aufgerufen werden kann, aber nicht für die Java API freigegeben wird. Wenn ein globaler Skript oder eine Funktion nicht über Java-Code aufgerufen werden muss, wird dringend empfohlen, diese als static zu deklarieren.

Gleitkommagenauigkeit festlegen

Sie können die erforderliche Gleitkommagenauigkeit in einem Skript festlegen. 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 (Standardeinstellung, wenn nichts angegeben ist): Für Anwendungen, die gemäß IEEE 754-2008 eine Gleitkommagenauigkeit erfordern.
  • #pragma rs_fp_relaxed: Für Anwendungen, die keine strenge IEEE-754-2008-Konformität erfordern und eine geringere Genauigkeit tolerieren. Dieser Modus ermöglicht eine Leerung auf null für Denorme und Runden in Richtung Null.
  • #pragma rs_fp_imprecise: Für Apps, für die keine strengen Anforderungen an die Präzision gelten. In diesem Modus werden neben Folgendem alles in rs_fp_relaxed aktiviert:
    • Vorgänge, die zu -0.0 führen, können stattdessen +0.0 zurückgeben.
    • Operationen für INF und NAN sind nicht definiert.

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

Über Java auf RenderScript-APIs zugreifen

Wenn Sie eine Android-App entwickeln, die RenderScript verwendet, haben Sie zwei Möglichkeiten, über Java auf die API zuzugreifen:

Hier sind die Vor- und Nachteile:

  • Wenn Sie die APIs der Support Library verwenden, ist der RenderScript-Teil Ihrer App mit Geräten mit Android 2.3 (API-Level 9) und höher kompatibel, unabhängig davon, welche RenderScript-Funktionen Sie verwenden. Dadurch funktioniert Ihre Anwendung auf mehr Geräten als mit den nativen APIs (android.renderscript).
  • Bestimmte RenderScript-Funktionen sind nicht über die APIs der Support Library verfügbar.
  • Wenn du die Support Library APIs verwendest, erhältst du (möglicherweise erheblich) größere APKs als bei Verwendung der nativen APIs (android.renderscript).

APIs der RenderScript Support Library verwenden

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

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

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

Die installierte Version dieser Tools kannst du 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-Build-Prozess, sodass sie die RenderScript-Einstellungen enthalten:
    • Öffnen Sie die Datei build.gradle im App-Ordner Ihres Anwendungsmoduls.
    • Fügen Sie der Datei die folgenden RenderScript-Einstellungen hinzu:

      Cool

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

      Die oben aufgeführten Einstellungen steuern ein bestimmtes Verhalten im Android-Build-Prozess:

      • renderscriptTargetApi: Gibt die zu generierende Bytecode-Version an. Wir empfehlen, diesen Wert auf die niedrigste API-Ebene festzulegen, die alle von dir verwendeten Funktionen bietet, und renderscriptSupportModeEnabled auf true setzen. Gültige Werte für diese Einstellung sind alle Ganzzahlwerte von 11 bis zum aktuellsten API-Level. Wenn die im Manifest Ihrer Anwendung angegebene SDK-Mindestversion auf einen anderen Wert festgelegt ist, wird dieser Wert ignoriert und der Zielwert in der Build-Datei wird verwendet, um die SDK-Mindestversion festzulegen.
      • renderscriptSupportModeEnabled gibt an, dass der generierte Bytecode auf eine kompatible Version zurückgreifen 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 Support Library-Klassen hinzu:

    Kotlin

    import android.support.v8.renderscript.*
    

    Java

    import android.support.v8.renderscript.*;
    

RenderScript mit Java- oder Kotlin-Code verwenden

Für die Verwendung von RenderScript aus Java- oder Kotlin-Code sind die API-Klassen im Paket android.renderscript oder android.support.v8.renderscript erforderlich. Die meisten Anwendungen folgen demselben grundlegenden Nutzungsmuster:

  1. RenderScript-Kontext initialisieren Der mit create(Context) erstellte Kontext RenderScript sorgt dafür, dass RenderScript verwendet werden kann, und bietet ein Objekt, um die Lebensdauer aller nachfolgenden RenderScript-Objekte zu steuern. Sie sollten die Kontexterstellung als einen Vorgang mit potenziell langer Ausführungszeit betrachten, da dadurch Ressourcen auf verschiedenen Hardwarekomponenten erstellt werden können. Sie sollte sich möglichst nicht im kritischen Pfad einer Anwendung befinden. Normalerweise verfügt eine Anwendung immer nur über einen einzigen RenderScript-Kontext.
  2. Erstellen Sie mindestens eine Allocation, die an ein Skript übergeben wird. Ein Allocation ist ein RenderScript-Objekt, das Speicher für eine feste Datenmenge bietet. Kernel in Skripts verwenden Allocation-Objekte als Ein- und Ausgabe. Allocation-Objekte können in Kerneln mit rsGetElementAt_type() und rsSetElementAt_type() aufgerufen werden, wenn sie als globale Skripte gebunden sind. Mit Allocation-Objekten können Arrays vom Java-Code an den RenderScript-Code übergeben werden und umgekehrt. Allocation-Objekte werden normalerweise mit createTyped() oder createFromBitmap() erstellt.
  3. Erstellen Sie alle erforderlichen Skripts. Bei der Verwendung von RenderScript stehen Ihnen zwei Arten von Skripts zur Verfügung:
    • ScriptC: Dies sind die benutzerdefinierten Skripts, wie oben im Abschnitt RenderScript-Kernel schreiben beschrieben. Jedes Skript hat eine vom RenderScript-Compiler reflektierte Java-Klasse, um den Zugriff auf das Skript aus Java-Code zu erleichtern. Diese Klasse hat den Namen ScriptC_filename. Wenn sich der Zuordnungs-Kernel oben beispielsweise in invert.rs und ein RenderScript-Kontext bereits in mRenderScript befindet, lautet der Java- oder Kotlin-Code zur Instanziierung des Skripts:

      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 die Gaußsche Weichzeichnung, Faltung und Bildüberlagerung. Weitere Informationen finden Sie in den abgeleiteten Klassen von ScriptIntrinsic.
  4. Zuweisungen mit Daten füllen. Mit Ausnahme der mit createFromBitmap() erstellten Zuweisungen werden bei der erstmaligen Erstellung einer Zuordnung leere Daten gefüllt. Um eine Zuordnung zu erstellen, verwenden Sie eine der Kopiermethoden in Allocation. Die Kopiermethoden sind synchron.
  5. Legen Sie alle erforderlichen globalen Skripte fest. Sie können globale Werte mit Methoden in derselben ScriptC_filename-Klasse namens set_globalname festlegen. Wenn Sie beispielsweise eine int-Variable namens threshold festlegen möchten, verwenden Sie die Java-Methode set_threshold(int). Um eine rs_allocation-Variable mit dem Namen lookup festzulegen, verwenden Sie die Java-Methode set_lookup(Allocation). Die set-Methoden sind asynchron.
  6. Die entsprechenden Kernel und Aufruffunktionen starten.

    Methoden zum Starten eines bestimmten Kernels finden Sie in derselben ScriptC_filename-Klasse mit Methoden namens forEach_mappingKernelName() oder reduce_reductionKernelName(). Diese Starts sind asynchron. Abhängig von 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 für alle Koordinaten in diesen Dimensionen ausgeführt. Um einen Kernel für einen Teil dieser Koordinaten auszuführen, übergeben Sie ein entsprechendes Script.LaunchOptions als letztes Argument an die Methode forEach oder reduce.

    Starten Sie aufrufbare Funktionen mit den invoke_functionName-Methoden, die in derselben ScriptC_filename-Klasse enthalten sind. Diese Starts sind asynchron.

  7. Daten aus Allocation- und javaFutureType-Objekten abrufen Um auf Daten aus einer Allocation aus Java-Code zuzugreifen, müssen Sie diese Daten mithilfe einer der Kopiermethoden in Allocation zurück in Java kopieren. Um das Ergebnis eines Reduktions-Kernels zu erhalten, müssen Sie die javaFutureType.get()-Methode verwenden. Die Methoden „copy“ und get() sind synchron.
  8. Entfernen Sie den RenderScript-Kontext. Sie können den RenderScript-Kontext mit destroy() zerstören oder zulassen, dass das RenderScript-Kontextobjekt automatisch bereinigt wird. Bei jeder weiteren Verwendung von Objekten, die zu diesem Kontext gehören, wird eine Ausnahme ausgelöst.

Asynchrones Ausführungsmodell

Die angezeigten Methoden forEach, invoke, reduce und set sind asynchron und können jeweils zu Java zurückkehren, bevor die angeforderte Aktion ausgeführt wurde. Die einzelnen Aktionen werden jedoch in der Reihenfolge serialisiert, in der sie gestartet werden.

Die Klasse Allocation bietet Kopiermethoden, um Daten in und aus Zuweisungen zu kopieren. Eine Kopiermethode ist synchron und wird in Bezug auf alle der oben genannten asynchronen Aktionen, die dieselbe Zuordnung betreffen, serialisiert.

Die zurückgegebenen javaFutureType-Klassen bieten eine get()-Methode, um das Ergebnis einer Reduktion zu erhalten. get() ist synchron und wird hinsichtlich der (asynchronen) Reduktion serialisiert.

Single Source RenderScript

Mit Android 7.0 (API-Level 24) wird eine neue Programmierfunktion namens Single-Source RenderScript eingeführt, bei der Kernel über das Skript gestartet werden, in dem sie definiert sind, und nicht über Java. Dieser Ansatz ist derzeit auf die Zuordnung von Kerneln beschränkt, die in diesem Abschnitt aus Gründen der Übersichtlichkeit einfach als "Kernel" bezeichnet werden. Diese neue Funktion unterstützt auch das Erstellen von Zuweisungen vom Typ rs_allocation aus dem Script. Es ist jetzt möglich, einen ganzen Algorithmus ausschließlich in einem Skript zu implementieren, selbst wenn mehrere Kernel-Starts erforderlich sind. Der Vorteil ist in zwei Bereichen: besser lesbarer Code, da die Implementierung eines Algorithmus in einer Sprache erfolgt, und potenziell schnellerer Code aufgrund weniger Übergänge zwischen Java und RenderScript bei mehreren Kernel-Starts.

In Single-Source-RenderScript schreiben Sie Kernel wie unter RenderScript-Kernel schreiben beschrieben. Dann schreiben Sie eine aufrufbare Funktion, die rsForEach() aufruft, um sie zu starten. Diese API verwendet als ersten Parameter eine Kernel-Funktion, gefolgt von Eingabe- und Ausgabezuweisungen. Eine ähnliche API rsForEachWithOptions() verwendet ein zusätzliches Argument vom Typ rs_script_call_t, mit dem eine Teilmenge der Elemente aus den Ein- und Ausgabezuweisungen für die Verarbeitung durch die Kernel-Funktion angegeben wird.

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

Zuweisungen werden häufig benötigt, um Zwischenergebnisse von einem Kernel-Start an einen anderen zu übergeben und zu speichern. 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 verwendet die Größen in den Dimensionen X, Y und Z als Argumente. Bei 1D- oder 2D-Zuweisungen kann die Größe für die Dimension Y oder Z weggelassen werden. Beispielsweise erstellt rsCreateAllocation_uchar4(16384) eine 1D-Zuordnung von 16.384 Elementen, von denen jedes den Typ uchar4 hat.

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 so früh wie möglich Ressourcen freigeben kann.

Der Abschnitt RenderScript-Kernel schreiben enthält einen Beispielkernel, der ein Bild invertiert. Im folgenden Beispiel wird dies erweitert, um mithilfe von Single-Source-RenderScript mehr als einen Effekt auf ein Bild anzuwenden. Sie enthält einen weiteren Kernel greyscale, der ein Farbbild in Schwarz-Weiß umwandelt. Die aufgerufene Funktion process() wendet diese beiden Kernel 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);

Dieses Beispiel zeigt, wie ein Algorithmus, der zwei Kernel-Starts umfasst, vollständig in der RenderScript-Sprache selbst implementiert werden kann. Ohne Single-Source-RenderScript müssten Sie beide Kernel aus dem Java-Code starten, wodurch die Kernel-Starts von den Kernel-Definitionen getrennt werden, wodurch es schwieriger wird, den gesamten Algorithmus zu verstehen. Der Single-Source-RenderScript-Code ist nicht nur leichter zu lesen, sondern eliminiert auch den Wechsel zwischen Java und dem Script bei Kernel-Starts. Einige iterative Algorithmen starten möglicherweise hunderte von Malen, was den Aufwand für eine solche Umstellung erheblich erschwert.

Script-Globals

Eine globale Skriptvariable ist eine gewöhnliche globale Nicht-static-Variable in einer Skriptdatei (.rs). Für ein globales Skript namens var, das in der Datei filename.rs definiert ist, ist die Methode get_var in der Klasse ScriptC_filename enthalten. Sofern der globale Wert nicht const ist, gibt es auch die Methode set_var.

Ein globaler Skriptwert hat zwei separate Werte: einen Java-Wert und einen script-Wert. Diese Werte verhalten sich folgendermaßen:

  • Wenn var einen statischen Initialisierer im Skript hat, wird der Anfangswert von var sowohl in Java als auch im Skript angegeben. Andernfalls ist dieser Anfangswert null.
  • Der Zugriff auf var innerhalb des Skripts hat Lese- und Schreibzugriff auf den zugehörigen Skriptwert.
  • Die Methode get_var liest den Java-Wert.
  • Die Methode set_var (sofern vorhanden) schreibt den Java-Wert sofort und den Skriptwert asynchron.

HINWEIS:Das bedeutet, dass Werte, die von innerhalb eines Skripts in eine globale Komponente geschrieben werden, für Java nicht sichtbar sind, mit Ausnahme von statischem Initialisierer im Skript.

Tiefenreduktionskerne

Reduktion ist der Vorgang, bei dem eine Sammlung von Daten zu einem einzigen Wert kombiniert wird. Dies ist eine nützliche Primitive bei der parallelen Programmierung mit Anwendungen wie den folgenden:

  • Berechnen der Summe oder des Produkts über alle Daten
  • Berechnung logischer Vorgänge (and, or, xor) für alle Daten
  • Ermitteln des Mindest- oder Höchstwerts in den Daten
  • Suchen nach einem bestimmten Wert oder der Koordinate eines bestimmten Werts in den Daten

Unter Android 7.0 (API-Level 24) und höher unterstützt RenderScript Reduktions-Kernel, um effiziente, vom Nutzer geschriebene Reduktionsalgorithmen zu ermöglichen. Sie können Reduktions-Kernel bei Eingaben mit 1, 2 oder 3 Dimensionen starten.

Das obige Beispiel zeigt einen einfachen Kernel für die addint-Reduktion. Hier ist ein komplexerer findMinAndMax-Reduktionskernel, der die Positionen der minimalen und maximalen long-Werte in einer eindimensionalen Allocation findet:

#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 Beispiele für Reduktions-Kernel finden Sie hier.

Zum Ausführen eines Reduktions-Kernels erstellt die RenderScript-Laufzeit eine oder mehrere Variablen, die als Akkumulatordatenelemente bezeichnet werden und den Status des Reduktionsprozesses enthalten. Die RenderScript-Laufzeit wählt die Anzahl der Akkumulationsdatenelemente so aus, dass die Leistung maximiert wird. Der Typ der Akkumulator-Datenelemente (accumType) wird durch die Akkumulator-Funktion des Kernels bestimmt. Das erste Argument für diese Funktion ist ein Verweis auf ein Akkumulator-Datenelement. Standardmäßig wird jedes Akkumulatordatenelement auf null initialisiert (wie durch memset). Sie können jedoch eine Initialisierungsfunktion schreiben, um etwas anderes zu tun.

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

Beispiel:Im Kernel findMinAndMax werden die Akkumulator-Datenelemente (vom Typ MinAndMax) verwendet, um die bisher gefundenen Mindest- und Höchstwerte nachzuverfolgen. Es gibt eine Initialisierungsfunktion, um diese auf LONG_MAX bzw. LONG_MIN und die Positionen dieser Werte auf -1 zu setzen, was darauf hinweist, dass die Werte nicht im (leeren) Teil der verarbeiteten Eingabe vorhanden sind.

RenderScript ruft die Akkumulatorfunktion einmal für jede Koordinate in den Eingaben auf. In der Regel sollte Ihre Funktion das Akkumulator-Datenelement entsprechend der Eingabe aktualisieren.

Beispiel:Im addint-Kernel fügt die Akkumulatorfunktion den Wert eines Eingabeelements zum Akkumulator-Datenelement hinzu.

Beispiel:Im Kernel findMinAndMax prüft die Akkumulatorfunktion, ob der Wert eines Eingabeelements kleiner oder gleich dem im Akkumulator-Datenelement aufgezeichneten Mindestwert ist und/oder größer oder gleich dem Höchstwert ist, der im Akkumulator-Datenelement aufgezeichnet wurde, und das Akkumulator-Datenelement entsprechend aktualisiert.

Nachdem die Akkumulatorfunktion für jede Koordinate in der Eingabe einmal aufgerufen wurde, muss RenderScript die Akkumulator-Datenelemente zu einem einzigen Akkumulator-Datenelement kombinieren. Sie können dazu eine Kombinerfunktion schreiben. Wenn die Akkumulatorfunktion eine einzelne Eingabe und keine speziellen Argumente hat, müssen Sie keine Kombinatorfunktion schreiben. RenderScript verwendet die Akkumulatorfunktion, um die Akkumulator-Datenelemente zu kombinieren. (Sie können trotzdem eine Kombinatorfunktion schreiben, wenn dieses Standardverhalten nicht Ihren Vorstellungen entspricht.)

Beispiel:Im addint-Kernel gibt es keine Kombinatorfunktion, daher wird die Akkumulator-Funktion 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, entspricht die Addition dieser beiden Summen der Summe der gesamten Sammlung.

Beispiel:Im Kernel findMinAndMax prüft die Kombinatorfunktion, ob der im Quellakkumordatenelement *val aufgezeichnete Minimalwert kleiner ist als der im Zielakkumor-Datenelement *accum aufgezeichnete Minimalwert und aktualisiert *accum entsprechend. Ähnliches gilt für den Maximalwert. Dadurch wird *accum auf den Zustand aktualisiert, den es hätte, wenn alle Eingabewerte in *accum akkumuliert worden wären, anstatt einige in *accum und einige in *val.

Nachdem alle Akkumulator-Datenelemente kombiniert wurden, ermittelt RenderScript das Ergebnis der Reduzierung, um zu Java zurückzukehren. Sie können zu diesem Zweck eine Outkonverter-Funktion schreiben. Sie müssen keine Outkonverter-Funktion schreiben, wenn der Endwert der kombinierten Akkumulatordatenelemente das Ergebnis der Reduzierung sein soll.

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

Beispiel:Im Kernel findMinAndMax initialisiert die outconverter-Funktion einen int2-Ergebniswert, der die Positionen der Mindest- und Höchstwerte enthält, die sich aus der Kombination aller Akkumulator-Datenelemente ergeben.

Reduktionskernel schreiben

#pragma rs reduce definiert einen Reduktions-Kernel durch Angabe seines Namens sowie der Namen und Rollen der Funktionen, aus denen der Kernel besteht. Alle diese Funktionen müssen static sein. Ein Reduktions-Kernel erfordert immer eine accumulator-Funktion. Sie können einige oder alle anderen Funktionen auslassen, je nachdem, was der Kernel tun soll.

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

Die Elemente im #pragma haben folgende Bedeutung:

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

    static void initializerName(accumType *accum) { … }

    accum ist ein Zeiger auf ein Akkumulator-Datenelement, das diese Funktion initialisieren soll.

    Wenn Sie keine Initialisiererfunktion angeben, initialisiert RenderScript jedes Akkumulator-Datenelement auf null (als ob durch memset), und verhält sich so, 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 Reduktions-Kernel an. Wenn Sie den Kernel starten, ruft RenderScript diese Funktion einmal für jede Koordinate in den Eingaben auf, um ein Akkumulator-Datenelement entsprechend der Eingabe zu aktualisieren. Die Funktion muss so definiert werden:

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

    accum ist ein Zeiger auf ein Akkumulator-Datenelement, das von dieser Funktion geändert werden kann. in1 bis inN sind ein oder mehrere Argumente, die basierend auf den Eingaben, die an den Kernel-Start übergeben werden, automatisch ausgefüllt werden (ein Argument pro Eingabe). Die Akkumulatorfunktion kann optional jedes der speziellen Argumente annehmen.

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

  • combiner(combinerName)

    (optional): Gibt den Namen der Kombinatorfunktion für diesen Reduktions-Kernel an. Nachdem RenderScript die Akkumulatorfunktion einmal für jede Koordinate in der Eingabe aufgerufen hat, wird diese Funktion so oft wie nötig aufgerufen, um alle Akkumulator-Datenelemente zu einem einzigen Akkumulator-Datenelement zu kombinieren. Die Funktion muss wie folgt definiert werden:

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

    accum ist ein Zeiger auf ein „Ziel“-Akkumulator-Datenelement, das von dieser Funktion geändert werden kann. other ist ein Zeiger auf ein „Quell“-Akkumulatordatenelement, mit dem diese Funktion in *accum „kombiniert“ werden kann.

    HINWEIS:Es ist möglich, dass *accum, *other oder beide initialisiert, aber nie an die Akkumulatorfunktion übergeben wurden, d. h., mindestens eine davon wurde noch nie gemäß Eingabedaten aktualisiert. Im Kernel findMinAndMax prüft die Kombinatorfunktion fMMCombiner beispielsweise explizit auf idx < 0, da dies auf ein solches Akkumulator-Datenelement mit dem Wert INITVAL hinweist.

    Wenn Sie keine Kombinatorfunktion angeben, verwendet RenderScript die Akkumulator-Funktion an ihrer Stelle und verhält sich so, als gäbe es eine Kombinatorfunktion, die so aussieht:

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

    Eine Kombinatorfunktion ist obligatorisch, wenn der Kernel mehr als eine Eingabe hat, der Eingabedatentyp nicht mit dem Akkumulator-Datentyp übereinstimmt oder wenn die Akkumulatorfunktion ein oder mehrere spezielle Argumente annimmt.

  • outconverter(outconverterName) (optional): Gibt den Namen der Outconverter-Funktion für diesen Reduktions-Kernel an. Nachdem RenderScript alle Akkumulator-Datenelemente kombiniert hat, wird diese Funktion aufgerufen, um das Ergebnis der Reduktion zu bestimmen, die an Java zurückgegeben wird. Die Funktion muss so definiert werden:

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

    result ist ein Zeiger auf ein Ergebnisdatenelement (zugewiesen, aber nicht von der RenderScript-Laufzeit initialisiert), damit diese Funktion mit dem Ergebnis der Reduktion initialisiert wird. resultType ist der Typ dieses Datenelements, das nicht mit accumType identisch sein muss. accum ist ein Zeiger auf das endgültige Akkumulator-Datenelement, das von der Kombinationsfunktion berechnet wurde.

    Wenn Sie keine Outconverter-Funktion bereitstellen, kopiert RenderScript das endgültige Akkumulator-Datenelement in das Ergebnisdatenelement und verhält sich dabei so, als gäbe es eine Outconverter-Funktion, die so aussieht:

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

    Wenn Sie einen anderen Ergebnistyp als den Akkumulator-Datentyp haben möchten, ist die Outconverter-Funktion obligatorisch.

Beachten Sie, dass ein Kernel über Eingabetypen, einen Akkumulator-Datenelementtyp und einen Ergebnistyp verfügt, von denen keiner identisch sein muss. Im Kernel findMinAndMax sind beispielsweise der Eingabetyp long, der Akkumulator-Datenelementtyp MinAndMax und der Ergebnistyp int2 unterschiedlich.

Wovon kannst du nicht ausgehen?

Sie dürfen sich nicht auf die Anzahl der Akkumulationsdatenelemente verlassen, die von RenderScript für einen bestimmten Kernel-Start erstellt werden. Es gibt keine Garantie, dass zwei Starts desselben Kernels mit denselben Eingaben dieselbe Anzahl von Akkumulatordatenelementen erzeugen.

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

  • Es gibt keine Garantie, dass alle Akkumulator-Datenelemente initialisiert werden, bevor die Akkumulatorfunktion aufgerufen wird. Sie wird jedoch nur für ein initialisiertes Akkumulator-Datenelement aufgerufen.
  • Es gibt keine Garantie für die Reihenfolge, in der Eingabeelemente an die Akkumulatorfunktion übergeben werden.
  • Es gibt keine Garantie, dass die Akkumulatorfunktion vor dem Aufruf der Kombinatorfunktion für alle Eingabeelemente aufgerufen wurde.

Eine Konsequenz daraus ist, dass der Kernel findMinAndMax nicht deterministisch ist: Wenn die Eingabe mehr als ein Vorkommen desselben Minimal- oder Maximalwerts enthält, haben Sie keine Möglichkeit zu 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 beachten, um sicherzustellen, dass sich Ihr Kernel wie gewünscht verhält. Wenn Sie diese Regeln nicht befolgen, kann dies zu falschen Ergebnissen, unbestimmtem Verhalten oder Laufzeitfehlern führen.

Die folgenden Regeln besagen häufig, dass zwei Akkumulationsdatenelemente denselben Wert haben müssen. Was bedeutet das? Das hängt davon ab, was der Kernel tun soll. Bei einer mathematischen Reduktion wie addint ist es normalerweise sinnvoll, wenn "thematisch" mathematische Gleichheit bedeutet. Bei einer Suche mit beliebiger Auswahl wie z. B. findMinAndMax („Finden Sie die Position der minimalen und maximalen Eingabewerte finden“), bei der identische Eingabewerte mehrmals vorkommen können, müssen alle Positionen eines bestimmten Eingabewerts als „gleich“ betrachtet werden. Sie könnten einen ähnlichen Kernel schreiben, um die Position der Minimal- und Maximaleingabewerte ganz links zu ermitteln", bei der (z. B.) ein Minimalwert an Position 100 gegenüber einem identischen Minimalwert an Position 200 bevorzugt wird. Für diesen Kernel bedeutet „gleicher“ denselben Standort und nicht nur einen identischen Wert und die Akkumulator- und Kombinatorfunktionen müssen sich von denen für Maxfind unterscheiden.

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

Beispiel:Im addint-Kernel wird ein Akkumulator-Datenelement auf null initialisiert. Die Kombinatorfunktion für diesen Kernel führt Additionen aus. Null ist der Identitätswert für die Addition.

Beispiel:Im Kernel findMinAndMax wird ein Akkumulator-Datenelement mit INITVAL initialisiert.

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

Daher ist INITVAL tatsächlich ein Identitätswert.

Die Kombinatorfunktion muss kommutativ sein. Wenn also A und B Akkumulator-Datenelemente sind, die von der Initialisiererfunktion initialisiert wurden und null- oder öfter an die Akkumulatorfunktion übergeben wurden, muss combinerName(&A, &B) A auf denselben Wert setzen, mit dem combinerName(&B, &A) B festlegt.

Beispiel:Im addint-Kernel fügt die Kombinatorfunktion die beiden Akkumulator-Datenelementwerte hinzu. Die Addition ist kommutativ.

Beispiel:Im Kernel findMinAndMax ist fMMCombiner(&A, &B) mit A = minmax(A, B) identisch und minmax steht für kommutative Werte, also ist fMMCombiner ebenfalls kommutativ.

Die Kombinationsfunktion muss assoziativ sein. Wenn A, B und C also Akkumulator-Datenelemente sind, die von der Initialisiererfunktion initialisiert wurden und möglicherweise ein- oder mehrmals an die Akkumulatorfunktion übergeben wurden, müssen die folgenden beiden Codesequenzen A auf denselben Wert setzen:

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

Beispiel:Im addint-Kernel fügt die Kombinatorfunktion die beiden Akkumulator-Datenelementwerte hinzu:

  • 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
    

Addition ist assoziativ, also auch die Kombinatorfunktion.

Beispiel:Im Kernel findMinAndMax ist

fMMCombiner(&A, &B)
dasselbe wie
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 und fMMCombiner ist es auch.

Die Akkumulatorfunktion und die Kombinationsfunktion müssen zusammen der grundlegenden Faltregel entsprechen. Wenn also A und B Akkumulator-Datenelemente sind, A von der Initialisiererfunktion initialisiert und möglicherweise null- oder öfter an die Akkumulatorfunktion übergeben wurde, B nicht initialisiert wurde und args die Liste der Eingabeargumente und speziellen Argumente für einen bestimmten Aufruf an die Akkumulatorfunktion ist, müssen die folgenden beiden Codesequenzen 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:

  • Anweisung 1 ist mit A += V identisch
  • Anweisung 2 ist identisch mit B = 0
  • Anweisung 3 ist identisch mit B += V und B = V.
  • Anweisung 4 ist identisch mit A += B und A += V.

Die Anweisungen 1 und 4 legen A auf denselben Wert fest, sodass dieser Kernel die grundlegende Faltregel befolgt.

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

  • Anweisung 1 ist mit A = minmax(A, IndexedVal(V, X)) identisch
  • Anweisung 2 ist identisch mit B = INITVAL
  • Anweisung 3 ist mit
    B = minmax(B, IndexedVal(V, X))
    
    identisch. Da B der Anfangswert ist, entspricht die Anweisung
    B = IndexedVal(V, X)
    
    .
  • Anweisung 4 ist identisch mit
    A = minmax(A, B)
    
    und
    A = minmax(A, IndexedVal(V, X))
    
    .

Die Anweisungen 1 und 4 legen A auf denselben Wert fest, sodass dieser Kernel die grundlegende Faltregel befolgt.

Reduktions-Kernel über Java-Code aufrufen

Für einen Reduktions-Kernel namens kernelName, der in der Datei filename.rs definiert ist, gibt es in der Klasse ScriptC_filename drei Methoden:

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 addint-Kernels:

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 Allocation-Eingabeargument für jedes Eingabeargument in der Akkumulatorfunktion des Kernels. Die RenderScript-Laufzeit prüft, ob alle Eingabezuweisungen dieselben Dimensionen haben und ob der Typ Element der einzelnen Eingabezuweisungen dem Typ des entsprechenden Eingabearguments des Prototyps der Akkumulatorfunktion entspricht. 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 ist dieselbe wie Methode 1, mit der Ausnahme, dass Methode 2 das zusätzliche Argument sc verwendet, mit dem die Kernel-Ausführung auf eine Teilmenge der Koordinaten beschränkt werden kann.

Methode 3 ist dieselbe wie Methode 1, mit dem Unterschied, dass anstelle der Zuordnungseingaben Java-Array-Eingaben verwendet werden. Das erspart Ihnen das Schreiben von Code, um eine Zuordnung explizit zu erstellen und Daten aus einem Java-Array dorthin zu kopieren. Die Verwendung von Methode 3 anstelle von Methode 1 erhöht jedoch nicht die Leistung des Codes. Für jedes Eingabe-Array erstellt Methode 3 eine temporäre eindimensionale Zuordnung mit dem entsprechenden Element-Typ und aktiviertem setAutoPadding(boolean) und kopiert das Array so in die Zuordnung, als ob dies mit der entsprechenden copyFrom()-Methode von Allocation der Fall wäre. Anschließend wird Methode 1 aufgerufen und diese temporären Zuweisungen werden übergeben.

HINWEIS:Wenn Ihre Anwendung mehrere Kernel-Aufrufe mit demselben Array oder mit unterschiedlichen Arrays mit denselben Dimensionen und Elementtypen durchführt, können Sie die Leistung verbessern, indem Sie Zuweisungen explizit selbst erstellen, ausfüllen und wiederverwenden, anstatt Methode 3 zu verwenden.

javaFutureType, der Rückgabetyp der reflektierten Reduktionsmethoden, ist eine reflektierte statische verschachtelte Klasse innerhalb der Klasse ScriptC_filename. Sie stellt das zukünftige Ergebnis einer Ausführung eines Reduktions-Kernels dar. Um das tatsächliche Ergebnis der Ausführung zu erhalten, rufen Sie die Methode get() dieser Klasse auf. Diese 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 bestimmt. Sofern resultType kein vorzeichenloser Typ (Skalar, Vektor oder Array) ist, ist javaResultType der direkt entsprechende Java-Typ. Wenn resultType ein nicht signierter Typ und ein größerer Java-signierter Typ ist, ist javaResultType dieser größere Java-signierte Typ. Andernfalls ist es der direkt entsprechende Java-Typ. Beispiel:

  • Wenn resultType int, int2 oder int[15] ist, ist javaResultType gleich int, Int2 oder int[]. Alle Werte von resultType können durch javaResultType dargestellt werden.
  • Wenn resultType uint, uint2 oder uint[15] ist, ist javaResultType gleich long, Long2 oder long[]. Alle Werte von resultType können durch javaResultType dargestellt werden.
  • Wenn resultType ulong, ulong2 oder ulong[15] ist, ist javaResultType gleich long, Long2 oder long[]. Es gibt bestimmte Werte von 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, hat javaFutureType den Wert result_resultType.
  • Wenn resultType ein Array der Länge Count mit Mitgliedern des Typs memberType ist, hat javaFutureType den Wert 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 (einschließlich Arraytyp) ist, gibt bei jedem Aufruf von javaFutureType.get() für dieselbe Instanz dasselbe Objekt zurück.

Wenn javaResultType nicht alle Werte vom Typ resultType darstellen kann und ein Reduktions-Kernel 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 Akkumulatorfunktion entspricht. Sofern inXType kein vorzeichenloser Typ oder Vektortyp ist, ist devecSiInXType der direkt entsprechende Java-Typ. Wenn inXType ein vorzeichenloser skalarer Typ ist, dann ist devecSiInXType der Java-Typ, der direkt dem signierten skalaren Typ derselben Größe entspricht. Wenn inXType ein signierter Vektortyp ist, dann ist devecSiInXType der Java-Typ, der direkt dem Vektorkomponententyp entspricht. Wenn inXType ein vorzeichenloser Vektortyp ist, ist devecSiInXType der Java-Typ, der direkt dem vorzeichenbehafteten skalaren Typ mit derselben Größe wie der Vektorkomponententyp entspricht. Beispiel:

  • Wenn inXType den Wert int hat, ist devecSiInXType gleich int.
  • Wenn inXType int2 ist, ist devecSiInXType int. Das Array ist eine abgeflachte Darstellung: Es enthält doppelt so viele skalare Elemente wie die Zuordnung aus 2-Komponenten-Vektorelementen. Dies entspricht den copyFrom()-Methoden von Allocation.
  • Wenn inXType den Wert uint hat, ist deviceSiInXType gleich int. Ein vorzeichenbehafteter Wert im Java-Array wird bei der Zuordnung als vorzeichenloser Wert desselben Bitmusters interpretiert. Auf dieselbe Weise funktionieren die copyFrom()-Methoden von Allocation.
  • Wenn inXType den Wert uint2 hat, ist deviceSiInXType gleich int. Dies ist eine Kombination der Verarbeitungsweise von int2 und uint: Das Array ist eine vereinfachte Darstellung und vorzeichenbehaftete Java-Array-Werte werden als nicht signierte RenderScript-Elementwerte interpretiert.

Beachten Sie, dass bei Methode 3 die Eingabetypen anders behandelt werden als Ergebnistypen:

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

Weitere Beispiele für Reduktionskernel

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

Zusätzliche Codebeispiele

In den Beispielen BasicRenderScript, RenderScriptIntrinsic und Hello Compute wird die Verwendung der auf dieser Seite behandelten APIs weiter demonstriert.