RenderScript – Übersicht

RenderScript ist ein Framework, mit dem rechenintensive Aufgaben unter Android mit hoher Leistung ausgeführt werden können. RenderScript ist in erster Linie für die Verwendung mit datenparallelen Berechnungen gedacht, aber auch serielle Arbeitslasten können davon profitieren. Die RenderScript-Laufzeit parallelisiert die Arbeit auf den auf einem Gerät verfügbaren Prozessoren, z. B. Multi-Core-CPUs und GPUs. So können Sie sich auf die Formulierung von Algorithmen konzentrieren, anstatt Arbeit zu planen. RenderScript ist besonders nützlich für Anwendungen, die Bildverarbeitung, computergestützte Fotografie oder Computer Vision ausführen.

Für den Einstieg in RenderScript sollten Sie zwei Hauptkonzepte kennen:

  • Die Sprache selbst ist eine von C99 abgeleitete Sprache zum Schreiben von Hochleistungs-Computing-Code. Unter RenderScript-Kernel schreiben wird beschrieben, wie Sie damit Compute-Kernel schreiben.
  • Die Control API wird verwendet, um die Lebensdauer von RenderScript-Ressourcen zu verwalten und die Kernel-Ausführung zu steuern. Sie ist in drei verschiedenen Sprachen verfügbar: Java, C++ im Android NDK und die von C99 abgeleitete Kernelsprache selbst. In den Artikeln RenderScript aus Java-Code verwenden und Single-Source-RenderScript werden die erste bzw. die 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 eigene Kernel, Funktionen und Variablen. Ein Skript kann Folgendes enthalten:

  • Eine Pragma-Deklaration (#pragma version(1)), die die Version der in diesem Script 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 Java-Klassen deklariert, die von diesem Skript reflektiert werden. Die Datei .rs muss Teil Ihres Anwendungspakets sein und darf sich nicht in einem Bibliotheksprojekt befinden.
  • Null oder mehr aufrufbare Funktionen. Eine aufrufbare Funktion ist eine RenderScript-Funktion mit einem einzelnen Thread, die Sie mit beliebigen Argumenten aus Ihrem Java-Code aufrufen können. Diese sind oft nützlich für die Ersteinrichtung oder für serielle Berechnungen innerhalb einer größeren Verarbeitungspipeline.
  • Null oder mehr globale Skriptvariablen. Eine globale Skriptvariable ähnelt einer globalen Variable in C. Sie können über Java-Code auf globale Skriptvariablen zugreifen. Diese werden häufig für die Parameterübergabe an RenderScript-Kernel verwendet. Hier finden Sie weitere Informationen zu globalen Skriptvariablen.

  • Null oder mehr Compute-Kernels. Ein Compute-Kernel ist eine Funktion oder eine Sammlung von Funktionen, die Sie von der RenderScript-Laufzeit parallel für eine Sammlung von Daten ausführen lassen können. Es gibt zwei Arten von Rechenkernels: Mapping-Kernels (auch foreach-Kernels genannt) und Reduktions-Kernels.

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

    • Hier ist 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 Fällen ist dies mit einer Standard-C-Funktion identisch. Das Attribut RS_KERNEL, das auf den Funktionsprototyp angewendet wird, gibt an, dass die Funktion ein RenderScript-Mapping-Kernel und keine aufrufbare Funktion ist. Das Argument in wird automatisch anhand der Eingabe Allocation ausgefüllt, die an den Kernelstart übergeben wird. Die Argumente x und y werden unten erläutert. Der vom Kernel zurückgegebene Wert wird automatisch an die entsprechende Stelle in der Ausgabe-Allocation geschrieben. Standardmäßig wird dieser Kernel für die gesamte Eingabe Allocation ausgeführt, wobei die Kernelfunktion einmal pro Element in der Allocation ausgeführt wird.

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

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

      Wenn Sie mehr Eingaben oder Ausgaben vom Typ Allocations benötigen, als der Kernel hat, sollten diese Objekte an globale rs_allocation-Skriptvariablen 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 von RenderScript automatisch definiert wird:

      #define RS_KERNEL __attribute__((kernel))

    Ein Reduktionskernel ist eine Familie von Funktionen, die auf einer Sammlung von Eingabe-Allocations mit denselben Dimensionen operieren. Standardmäßig wird die Akkumulatorfunktion 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 einzelnen Wert zu reduzieren.

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

      #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. #pragma rs reduce wird verwendet, um den Kernel zu definieren. Dazu werden sein Name (in diesem Beispiel addint) sowie die Namen und Rollen der Funktionen angegeben, aus denen der Kernel besteht (in diesem Beispiel eine accumulator-Funktion addintAccum). Alle diese Funktionen müssen static sein. Ein Reduktionskernel erfordert immer eine accumulator-Funktion. Je nachdem, was der Kernel tun soll, kann er auch andere Funktionen haben.

      Eine Akkumulatorfunktion für den Reduktionskernel muss void zurückgeben und mindestens zwei Argumente haben. Das erste Argument (accum in diesem Beispiel) ist ein Verweis auf ein Akkumulator-Datenelement und das zweite (val in diesem Beispiel) 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 mit null initialisiert. Standardmäßig wird dieser Kernel für die gesamte Eingabe Allocation ausgeführt, wobei die Akkumulatorfunktion einmal pro Element in der Allocation ausgeführt wird. Standardmäßig wird der endgültige Wert des Akkumulator-Datenelements als Ergebnis der Reduzierung behandelt und an Java zurückgegeben. Die RenderScript-Laufzeit prüft, ob der Element-Typ der Eingabezuordnung mit dem Prototyp der Akkumulatorfunktion übereinstimmt. Ist dies nicht der Fall, löst RenderScript eine Ausnahme aus.

      Ein Reduktionskernel hat einen oder mehrere Eingabe-Allocations, aber keine Ausgabe-Allocations.

      Reduktions-Kernel werden hier ausführlicher beschrieben.

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

    Eine Mapping-Kernelfunktion oder eine Reduktions-Kernel-Akkumulatorfunktion kann über die 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-Kernelfunktion oder eine Reduktions-Kernel-Akkumulatorfunktion kann auch das optionale spezielle Argument context vom Typ rs_kernel_context annehmen. Sie wird von einer Familie von Laufzeit-APIs benötigt, die zum Abfragen bestimmter Eigenschaften der aktuellen Ausführung verwendet 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 ein spezieller Typ von aufrufbaren Funktionen, die RenderScript ausführt, wenn das Script zum ersten Mal instanziiert wird. So können einige Berechnungen automatisch bei der Skripterstellung durchgeführt werden.
  • Null oder mehr statische Skript-Globals und -Funktionen. Eine statische globale Skriptvariable entspricht einer globalen Skriptvariablen, mit der Ausnahme, dass nicht über Java-Code darauf zugegriffen werden kann. Eine statische Funktion ist eine Standard-C-Funktion, die von jedem Kernel oder jeder aufrufbaren Funktion im Skript aufgerufen werden kann, aber nicht für die Java API verfügbar 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.

Gleitkommapräzision festlegen

Sie können die erforderliche Genauigkeit von Gleitkommazahlen 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 (Standard, wenn nichts angegeben ist): Für Apps, die eine Gleitkommagenauigkeit gemäß dem IEEE 754-2008-Standard erfordern.
  • #pragma rs_fp_relaxed: Für Anwendungen, die keine strikte IEEE 754-2008-Konformität erfordern und eine geringere Genauigkeit tolerieren können. In diesem Modus wird „Flush-to-Zero“ für denormalisierte Zahlen und „Round-towards-Zero“ aktiviert.
  • #pragma rs_fp_imprecise: Für Apps, die keine strengen Genauigkeitsanforderungen haben. In diesem Modus wird alles in rs_fp_relaxed aktiviert, zusammen mit Folgendem:
    • Vorgänge, die zu -0,0 führen, können stattdessen +0,0 zurückgeben.
    • Vorgänge für INF und NAN sind nicht definiert.

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

Über Java auf RenderScript APIs zugreifen

Wenn du eine Android-App entwickelst, die RenderScript verwendet, kannst du auf zwei Arten über Java auf die API zugreifen:

Hier sind die Vor- und Nachteile:

  • Wenn du die Support Library APIs verwendest, ist der RenderScript-Teil deiner Anwendung mit Geräten mit Android 2.3 (API-Level 9) und höher kompatibel, unabhängig davon, welche RenderScript-Funktionen du verwendest. Dadurch kann Ihre Anwendung auf mehr Geräten verwendet 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 (android.renderscript) APIs.

RenderScript-Support-Bibliothek-APIs verwenden

Wenn Sie die RenderScript APIs der Supportbibliothek verwenden möchten, müssen Sie Ihre Entwicklungsumgebung so konfigurieren, dass Sie darauf zugreifen können. Die folgenden Android SDK-Tools sind für die 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.

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

So verwenden Sie die RenderScript APIs der Supportbibliothek:

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

      Groovy

              android {
                  compileSdkVersion 36
      
                  defaultConfig {
                      minSdkVersion 9
                      targetSdkVersion 19
      
                      renderscriptTargetApi 18
                      renderscriptSupportModeEnabled true
                  }
              }
              

      Kotlin

              android {
                  compileSdkVersion(36)
      
                  defaultConfig {
                      minSdkVersion(9)
                      targetSdkVersion(19)
      
                      renderscriptTargetApi = 18
                      renderscriptSupportModeEnabled = true
                  }
              }
              

      Die oben aufgeführten Einstellungen steuern bestimmte Verhaltensweisen 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 Ihnen verwendeten Funktionen bietet, und renderscriptSupportModeEnabled auf true zu setzen. Gültige Werte für diese Einstellung sind alle Ganzzahlwerte von 11 bis zum zuletzt veröffentlichten API-Level. Wenn die im App-Manifest angegebene Mindest-SDK-Version auf einen anderen Wert festgelegt ist, wird dieser Wert ignoriert und der Zielwert in der Build-Datei wird verwendet, um die Mindest-SDK-Version 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üge in deinen 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 aus Java- oder Kotlin-Code verwenden

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

  1. RenderScript-Kontext initialisieren Der mit create(Context) erstellte RenderScript-Kontext sorgt dafür, dass RenderScript verwendet werden kann, und stellt ein Objekt bereit, mit dem die Lebensdauer aller nachfolgenden RenderScript-Objekte gesteuert werden kann. Die Kontexterstellung ist ein potenziell langwieriger Vorgang, da dabei Ressourcen auf verschiedenen Hardwarekomponenten erstellt werden können. Sie sollte nach Möglichkeit nicht im kritischen Pfad einer Anwendung liegen. In der Regel hat eine Anwendung jeweils nur einen RenderScript-Kontext.
  2. Erstellen Sie mindestens einen Allocation, der an ein Skript übergeben werden soll. Eine Allocation ist ein RenderScript-Objekt, das Speicherplatz für eine feste Datenmenge bietet. Kernel in Scripts verwenden Allocation-Objekte als Eingabe und Ausgabe. Auf Allocation-Objekte kann in Kerneln mit rsGetElementAt_type() und rsSetElementAt_type() zugegriffen werden, wenn sie als globale Script-Variablen gebunden sind. Allocation-Objekte ermöglichen die Übergabe von Arrays von Java-Code an RenderScript-Code und umgekehrt. Allocation-Objekte werden normalerweise 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 Skripts, die oben unter RenderScript-Kernel schreiben beschrieben werden. Jedes Script hat eine Java-Klasse, die vom RenderScript-Compiler widergespiegelt wird, damit das Script leicht über Java-Code aufgerufen werden kann. Diese Klasse hat den Namen ScriptC_filename. Wenn sich der obige Zuordnungskernel beispielsweise in invert.rs und ein RenderScript-Kontext bereits in mRenderScript befindet, lautet der Java- oder Kotlin-Code zum Instanziieren 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 Gaußscher Weichzeichner, Faltung und Bildüberblendung. Weitere Informationen finden Sie in den Unterklassen von ScriptIntrinsic.
  4. Füllen Sie die Zuweisungen mit Daten. Mit Ausnahme von Zuweisungen, die mit createFromBitmap() erstellt wurden, wird eine Zuweisung bei der Erstellung mit leeren Daten gefüllt. Verwenden Sie eine der „copy“-Methoden in Allocation, um eine Zuweisung zu füllen. Die „copy“-Methoden sind synchron.
  5. Legen Sie alle erforderlichen globalen Skriptvariablen fest. Sie können globale Variablen mit 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 werden in derselben ScriptC_filename-Klasse mit Methoden namens forEach_mappingKernelName() oder reduce_reductionKernelName() widergespiegelt. Diese Markteinführungen sind asynchron. Je nach den Argumenten für den Kernel verwendet die Methode eine oder mehrere Allocations, die alle dieselben Dimensionen haben müssen. Standardmäßig wird ein Kernel für jede Koordinate in diesen Dimensionen ausgeführt. Wenn Sie einen Kernel für eine Teilmenge dieser Koordinaten ausführen möchten, ü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 Markteinführungen sind asynchron.

  7. Daten aus Allocation-Objekten und javaFutureType-Objekten abrufen. Wenn Sie über Java-Code auf Daten aus einem Allocation zugreifen möchten, müssen Sie diese Daten mit einer der „copy“-Methoden in Allocation zurück nach Java kopieren. Um das Ergebnis eines Reduktionskernels zu erhalten, müssen Sie die Methode javaFutureType.get() verwenden. Die Methoden „copy“ und get() sind synchron.
  8. RenderScript-Kontext beenden Sie können den RenderScript-Kontext mit destroy() oder durch die automatische Speicherbereinigung des RenderScript-Kontextobjekts löschen. 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 serialisiert, in der sie gestartet werden.

Die Klasse Allocation bietet „Kopier“-Methoden, um Daten in und aus Allocations zu kopieren. Eine „copy“-Methode ist synchron und wird in Bezug auf alle oben genannten asynchronen Aktionen, die dieselbe Zuweisung betreffen, serialisiert.

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

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 Zuordnungskerne beschränkt, die in diesem Abschnitt der Einfachheit halber als „Kerne“ bezeichnet werden. Diese neue Funktion unterstützt auch das Erstellen von Zuweisungen des Typs rs_allocation innerhalb des Scripts. Es ist jetzt möglich, einen ganzen Algorithmus ausschließlich in einem Skript zu implementieren, auch wenn mehrere Kernel-Starts erforderlich sind. Der Vorteil ist zweifach: Der Code ist besser lesbar, da die Implementierung eines Algorithmus in einer Sprache erfolgt, und der Code ist potenziell schneller, da es bei mehreren Kernel-Starts weniger Übergänge zwischen Java und RenderScript gibt.

In Single-Source-RenderScript schreiben Sie Kernel wie unter RenderScript-Kernel schreiben beschrieben. Anschließend schreiben Sie eine aufrufbare Funktion, die rsForEach() aufruft, um sie zu starten. Diese API verwendet eine Kernelfunktion als ersten Parameter, gefolgt von Eingabe- und Ausgabezuweisungen. Eine ähnliche API rsForEachWithOptions() verwendet ein zusätzliches Argument vom Typ rs_script_call_t, 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 über Java auf. Folgen Sie der Anleitung unter RenderScript aus Java-Code verwenden. Rufen Sie im Schritt Geeignete Kernel starten die aufrufbare Funktion mit invoke_function_name() auf. Dadurch wird die gesamte Berechnung gestartet, einschließlich des Startens von Kerneln.

Zuweisungen sind oft erforderlich, um Zwischenergebnisse von einem Kernel-Start zum nächsten zu speichern und weiterzugeben. Du kannst sie mit rsCreateAllocation() erstellen. Eine einfach zu verwendende 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. Beispiel: rsCreateAllocation_uchar4(16384) erstellt eine 1D-Zuweisung von 16.384 Elementen, die jeweils vom Typ uchar4 sind.

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 das Handle alloc für die zugrunde liegende Zuweisung nicht mehr benötigen, damit das System Ressourcen so früh wie möglich freigeben kann.

Der Abschnitt RenderScript-Kernel schreiben enthält einen Beispielkernel, der ein Bild invertiert. Im folgenden Beispiel wird dies erweitert, um mit Single-Source-RenderScript mehr als einen Effekt auf ein Bild anzuwenden. Er enthält einen weiteren Kernel, greyscale, der ein Farbbild in ein Schwarz-Weiß-Bild 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);
}

Sie können die Funktion process() in Java oder Kotlin so aufrufen:

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, der zwei Kernel-Starts umfasst, vollständig in der RenderScript-Sprache selbst implementiert werden kann. Ohne Single-Source-RenderScript müssten Sie beide Kernel über den Java-Code starten. Dadurch würden die Kernel-Starts von den Kernel-Definitionen getrennt und der gesamte Algorithmus wäre schwerer zu verstehen. Der Single-Source-RenderScript-Code ist nicht nur leichter zu lesen, sondern macht auch den Wechsel zwischen Java und dem Script bei Kernel-Starts überflüssig. Einige iterative Algorithmen können Kernel hunderte Male starten, was zu einem erheblichen Overhead bei solchen Übergängen führt.

Globale Skriptvariablen

Eine globale Skriptvariable ist eine gewöhnliche globale Variable, die nicht vom Typ static ist, in einer Skriptdatei (.rs). Für eine globale Skriptvariable mit dem Namen var, die in der Datei filename.rs definiert ist, gibt es eine Methode get_var, die in der Klasse ScriptC_filename widergespiegelt wird. Sofern die globale Variable nicht const ist, gibt es auch eine Methode set_var.

Eine bestimmte globale Script-Variable hat zwei separate Werte: einen Java-Wert und einen Script-Wert. Diese Werte verhalten sich so:

  • Wenn var einen statischen Initialisierer im Script hat, wird der Anfangswert von var sowohl in Java als auch im Script angegeben. Andernfalls ist der Anfangswert null.
  • Bei Zugriffen auf var innerhalb des Scripts wird der Scriptwert gelesen und geschrieben.
  • Die Methode get_var liest den Java-Wert.
  • Die Methode set_var (falls vorhanden) schreibt den Java-Wert sofort und den Skriptwert asynchron.

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

Reduktionskernels im Detail

Reduzierung ist der Prozess, bei dem eine Sammlung von Daten zu einem einzigen Wert kombiniert wird. Dies ist ein nützliches Primitiv in der parallelen Programmierung, mit Anwendungen wie den folgenden:

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

In 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 für Eingaben mit 1, 2 oder 3 Dimensionen starten.

Das obige Beispiel zeigt einen einfachen addint-Reduktionskernel. Hier ist ein komplizierterer Reduktionskernel findMinAndMax, der die Positionen der minimalen und maximalen long-Werte in einem 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 Reduktionskernels finden Sie hier.

Um einen Reduktionskernel auszuführen, erstellt die RenderScript-Laufzeit ein oder mehrere Variablen, die als Akkumulator-Datenelemente bezeichnet werden, um den Status des Reduktionsprozesses zu speichern. Die RenderScript-Laufzeit wählt die Anzahl der Akkumulator-Datenelemente so aus, dass die Leistung maximiert wird. Der Typ der Akkumulator-Datenelemente (accumType) wird durch die Akkumulatorfunktion des Kernels bestimmt. Das erste Argument dieser Funktion ist ein Verweis auf ein Akkumulator-Datenelement. Standardmäßig wird jedes Akkumulator-Datenelement auf null initialisiert (wie durch 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. Es gibt keine Initialisierungsfunktion, daher wird jedes Akkumulator-Datenelement mit null initialisiert.

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

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

Beispiel: Im Kernel addint 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 Akkumulatordatenelement aufgezeichneten Mindestwert und/oder größer oder gleich dem im Akkumulatordatenelement aufgezeichneten Höchstwert ist, und aktualisiert das Akkumulatordatenelement entsprechend.

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

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 Addieren dieser beiden Summen dasselbe wie das Addieren der gesamten Sammlung.

Beispiel: Im Kernel findMinAndMax prüft die Kombinationsfunktion, ob der im „Quell“-Akkumulator-Datenelement *val aufgezeichnete Mindestwert kleiner ist als der im „Ziel“-Akkumulator-Datenelement *accum aufgezeichnete Mindestwert, und aktualisiert *accum entsprechend. Für den Maximalwert wird ein ähnlicher Vorgang ausgeführt. Dadurch wird *accum auf den Zustand aktualisiert, den es gehabt 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, bestimmt RenderScript das Ergebnis der Reduzierung, das an Java zurückgegeben werden soll. Dazu können Sie eine Outconverter-Funktion schreiben. Sie müssen keine Outconverter-Funktion schreiben, wenn der Endwert der kombinierten Akkumulator-Datenelemente 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. Diesen Wert möchten wir zurückgeben.

Beispiel: Im Kernel findMinAndMax initialisiert die Outconverter-Funktion einen int2-Ergebniswert, um die Positionen der Mindest- und Höchstwerte zu speichern, die sich aus der Kombination aller Akkumulator-Datenelemente ergeben.

Reduktionskernel schreiben

#pragma rs reduce definiert einen Reduktionskernel, indem der Name des Kernels sowie die Namen und Rollen der Funktionen angegeben werden, aus denen der Kernel besteht. Alle diese Funktionen müssen static sein. Ein Reduktionskernel erfordert immer eine accumulator-Funktion. Je nachdem, was der Kernel tun soll, können Sie einige oder alle anderen Funktionen weglassen.

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

Die Elemente in der #pragma haben folgende Bedeutung:

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

    static void initializerName(accumType *accum) { … }

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

    Wenn Sie keine Initialisierungsfunktion angeben, initialisiert RenderScript jedes Akkumulator-Datenelement auf null (wie bei memset). Es verhält sich dann 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 Reduktionskernel an. Wenn Sie den Kernel starten, ruft RenderScript diese Funktion einmal für jede Koordinate in den Eingaben auf, um ein Akkumulator-Datenelement gemäß den Eingaben zu aktualisieren. Die Funktion muss so definiert werden:

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

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

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

  • combiner(combinerName)

    (optional): Gibt den Namen der Kombinationsfunktion für diesen Reduktionskernel an. Nachdem RenderScript die Akkumulatorfunktion einmal für jede Koordinate in den Eingaben aufgerufen hat, wird diese Funktion so oft wie nötig aufgerufen, um alle Akkumulatordatenelemente zu 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 „Ziel“-Akkumulator-Datenelement, das von dieser Funktion geändert werden soll. other ist ein Verweis auf ein „Quell“-Akkumulator-Datenelement, das diese Funktion in *accum „kombinieren“ 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 nie gemäß den Eingabedaten aktualisiert wurden. Im Kernel findMinAndMax sucht die Kombinatorfunktion fMMCombiner beispielsweise explizit nach idx < 0, da dies auf ein solches Akkumulator-Datenelement hinweist, dessen Wert INITVAL ist.

    Wenn Sie keine Kombinationsfunktion angeben, verwendet RenderScript stattdessen die Akkumulatorfunktion und verhält sich so, als gäbe es eine Kombinationsfunktion, die so aussieht:

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

    Eine Kombinatorfunktion ist erforderlich, wenn der Kernel mehr als eine Eingabe hat, wenn der Eingabedatentyp nicht mit dem Akkumulatordatentyp übereinstimmt oder wenn die Akkumulatorfunktion ein oder mehrere spezielle Argumente akzeptiert.

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

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

    result ist ein Zeiger auf ein Ergebnisdatenelement (von der RenderScript-Laufzeit zugewiesen, aber nicht initialisiert), das von dieser Funktion mit dem Ergebnis der Reduzierung initialisiert werden soll. resultType ist der Typ dieses Datenelements, der nicht mit accumType übereinstimmen muss. accum ist ein Verweis auf das letzte Akkumulator-Datenelement, das von der Kombinationsfunktion berechnet wurde.

    Wenn Sie keine Outconverter-Funktion angeben, kopiert RenderScript das endgültige Akkumulator-Datenelement in das Ergebnisdatenelement. Es verhält sich dann 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 wünschen, ist die Outconverter-Funktion erforderlich.

Beachten Sie, dass ein Kernel Eingabetypen, einen Akkumulator-Datenelementtyp und einen Ergebnistyp hat, die nicht identisch sein müssen. Im Kernel findMinAndMax unterscheiden sich beispielsweise der Eingabetyp long, der Datenelementtyp des Akkumulators MinAndMax und der Ergebnistyp int2.

Was können Sie nicht annehmen?

Du darfst dich nicht auf die Anzahl der Akkumulatordatenelemente verlassen, die von RenderScript für einen bestimmten Kernelstart erstellt werden. Es gibt keine Garantie dafür, dass zwei Starts desselben Kernels mit denselben Eingaben dieselbe Anzahl von Akkumulator-Datenelementen erzeugen.

Sie dürfen sich nicht auf die Reihenfolge verlassen, in der RenderScript die Initialisierungs-, Akkumulator- und Kombinationsfunktionen aufruft. Einige davon werden möglicherweise sogar parallel aufgerufen. Es gibt keine Garantie dafür, dass zwei Starts desselben Kernels mit derselben Eingabe derselben Reihenfolge folgen. Es wird nur garantiert, dass nur die Initialisierungsfunktion ein nicht initialisiertes Akkumulator-Datenelement sieht. Beispiel:

  • Es gibt keine Garantie dafür, dass alle Akkumulator-Datenelemente initialisiert werden, bevor die Akkumulatorfunktion aufgerufen wird. Sie wird jedoch nur für ein initialisiertes Akkumulator-Datenelement 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 Kernel findMinAndMax 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 beachten, damit sich Ihr Kernel wie gewünscht verhält. Wenn Sie diese Regeln nicht befolgen, erhalten Sie möglicherweise falsche Ergebnisse, nicht deterministisches Verhalten oder Laufzeitfehler.

In den folgenden Regeln wird häufig angegeben, dass zwei Akkumulator-Datenelemente denselben Wert haben müssen. Was bedeutet das? Das hängt davon ab, was der Kernel tun soll. Bei einer mathematischen Reduzierung wie addint ist es in der Regel sinnvoll, dass „gleich“ mathematische Gleichheit bedeutet. Bei einer „Pick Any“-Suche wie findMinAndMax („Finde die Position der minimalen und maximalen Eingabewerte“), bei der identische Eingabewerte mehrfach 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 am weitesten links stehenden minimalen und maximalen Eingabewerte zu finden“, wobei (beispielsweise) ein Mindestwert an Position 100 einem identischen Mindestwert an Position 200 vorgezogen wird. Für diesen Kernel würde „gleich“ eine identische Position bedeuten, nicht nur einen identischen Wert, und die Akkumulator- und Kombinatorfunktionen müssten sich von denen für findMinAndMax unterscheiden.

Die Initialisierungsfunktion muss einen Identitätswert erstellen. Das heißt, wenn I und A Akkumulator-Datenelemente sind, die von der Initialisierungsfunktion initialisiert wurden, und I nie an die Akkumulatorfunktion übergeben wurde (A aber möglicherweise schon), dann gilt:

Beispiel: Im Kernel addint wird ein Akkumulator-Datenelement mit null initialisiert. Die Kombinationsfunktion für diesen Kernel führt eine Addition 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 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. Das heißt, wenn A und B Akkumulator-Datenelemente sind, die von der Initialisierungsfunktion initialisiert wurden und möglicherweise null oder mehrmals an die Akkumulatorfunktion übergeben wurden, muss combinerName(&A, &B) A auf denselben Wert setzen, den combinerName(&B, &A) für B festlegt.

Beispiel: Im Kernel addint addiert die Kombinatorfunktion die beiden Akkumulator-Datenwerte. Die Addition ist kommutativ.

Beispiel: Im Kernel findMinAndMax ist fMMCombiner(&A, &B) mit A = minmax(A, B) identisch und minmax ist kommutativ, also auch fMMCombiner.

Die Kombinationsfunktion muss assoziativ sein. Das heißt, wenn A, B und C Akkumulator-Datenelemente sind, die von der Initialisierungsfunktion initialisiert wurden und möglicherweise null 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 Kernel addint addiert die Kombinationsfunktion die beiden Akkumulator-Datenwerte:

  • 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 identisch mit
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, also ist fMMCombiner auch assoziativ.

Die Akkumulatorfunktion und die Kombinationsfunktion müssen zusammen der grundlegenden Faltungsregel entsprechen. Wenn A und B Akkumulator-Datenelemente sind, A von der Initialisierungsfunktion initialisiert wurde und möglicherweise null oder mehrmals an die Akkumulatorfunktion ü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 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 einen Eingabewert V:

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

In den Anweisungen 1 und 4 wird A auf denselben Wert festgelegt. Dieser Kernel entspricht also der grundlegenden Faltungsregel.

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 mit B = INITVAL identisch.
  • Anweisung 3 ist identisch mit
    B = minmax(B, IndexedVal(V, X))
    Da B der Anfangswert ist, ist dies dasselbe wie
    B = IndexedVal(V, X)
  • Anweisung 4 ist identisch mit
    A = minmax(A, B)
    was dasselbe ist wie
    A = minmax(A, IndexedVal(V, X))

In den Anweisungen 1 und 4 wird A auf denselben Wert festgelegt. Dieser Kernel entspricht also der grundlegenden Faltungsregel.

Reduktionskernel aus Java-Code aufrufen

Für einen Reduktionskernel mit dem Namen kernelName, der in der Datei filename.rs definiert ist, gibt es drei Methoden, die in der Klasse ScriptC_filename widergespiegelt werden:

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 für jedes Eingabeargument in der Akkumulatorfunktion des Kernels ein Eingabeargument Allocation. Die RenderScript-Laufzeit prüft, ob alle Eingabezuordnungen dieselben Dimensionen haben und ob der Element-Typ jeder Eingabezuordnung mit dem entsprechenden Eingabeargument 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 ist mit Methode 1 identisch, verwendet aber ein zusätzliches Argument sc, mit dem die Kernel-Ausführung auf eine Teilmenge der Koordinaten beschränkt werden kann.

Methode 3 entspricht Methode 1, verwendet jedoch Java-Array-Eingaben anstelle von Allocation-Eingaben. Das ist praktisch, weil Sie so keinen Code schreiben müssen, um explizit eine Zuordnung 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 Eingabearray erstellt Methode 3 eine temporäre eindimensionale Allocation mit dem entsprechenden Element-Typ und aktiviertem setAutoPadding(boolean). Das Array wird dann in die Allocation kopiert, als ob die entsprechende copyFrom()-Methode von Allocation verwendet würde. Anschließend wird Methode 1 aufgerufen und diese 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 Allocations explizit selbst erstellen, fü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. Es stellt das zukünftige Ergebnis einer Reduktionskernel-Ausführung dar. Um das tatsächliche Ergebnis der Ausführung zu erhalten, rufen Sie die Methode get() dieser Klasse auf, die einen Wert vom Typ javaResultType zurückgibt. 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. Wenn resultType kein vorzeichenloser Typ (Skalar, Vektor oder Array) ist, ist javaResultType der direkt entsprechende Java-Typ. Wenn resultType ein vorzeichenloser Typ ist und ein größerer Java-Typ mit Vorzeichen vorhanden ist, ist javaResultType dieser größere Java-Typ mit Vorzeichen. 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 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, ist javaFutureType result_resultType.
  • Wenn resultType ein Array 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 (einschließlich eines Array-Typs) ist, 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 Reduktionskernel 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. Wenn inXType kein vorzeichenloser Typ oder Vektortyp ist, ist devecSiInXType der direkt entsprechende Java-Typ. Wenn inXType ein vorzeichenloser skalarer Typ ist, ist devecSiInXType der Java-Typ, der direkt dem vorzeichenbehafteten skalaren Typ derselben Größe entspricht. Wenn inXType ein signierter Vektortyp ist, 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, hat devecSiInXType den Wert int.
  • Wenn inXType int2 ist, ist devecSiInXType int. Das Array ist eine vereinfachte Darstellung: Es hat doppelt so viele skalare Elemente wie die Zuordnung Vektorelemente mit zwei Komponenten hat. Dies entspricht der Funktionsweise der copyFrom()-Methoden von Allocation.
  • Wenn inXType den Wert uint hat, ist deviceSiInXType int. Ein signierter Wert im Java-Array wird in der Zuordnung als unsignierter Wert desselben Bitmusters interpretiert. Dies entspricht der Funktionsweise der copyFrom()-Methoden von Allocation.
  • Wenn inXType den Wert uint2 hat, ist deviceSiInXType int. Dies ist eine Kombination aus der Art und Weise, wie int2 und uint verarbeitet werden: Das Array ist eine vereinfachte Darstellung und die vorzeichenbehafteten Werte des Java-Arrays werden als vorzeichenlose RenderScript-Elementwerte interpretiert.

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

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

Weitere Beispiele für Reduktions-Kernel

#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

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