RenderScript – Übersicht

RenderScript ist ein Framework zum Ausführen rechenintensiver Aufgaben mit hoher Leistung auf Android-Geräten. RenderScript ist in erster Linie für die datenparallele Berechnung ausgelegt, obwohl auch serielle Arbeitslasten davon profitieren können. In der RenderScript-Laufzeit wird die Arbeit zwischen den auf einem Gerät verfügbaren Prozessoren parallelisiert, z. B. Mehrkern-CPUs und GPUs. So können Sie sich auf das Ausdrucken der Algorithmen konzentrieren, anstatt die Arbeit zu planen. RenderScript ist besonders nützlich für Anwendungen für die Bildverarbeitung, computergestützte Fotografie oder maschinelles Sehen.

Zunächst solltest du zwei wichtige Konzepte von RenderScript verstehen:

  • Die Sprache selbst ist eine von C99 abgeleitete Sprache zum Schreiben von Hochleistungs-Computing-Code. Unter Einen RenderScript-Kernel schreiben wird beschrieben, wie dieser Kernel zum Schreiben von Compute-Kernels verwendet wird.
  • Mit der control API können Sie die Lebensdauer von RenderScript-Ressourcen verwalten und die Kernelausführung steuern. Es ist in drei verschiedenen Sprachen verfügbar: Java, C++ im Android-NDK und die von C99 abgeleitete Kernelsprache selbst. Unter RenderScript aus Java Code 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 <project_root>/src/rs-Verzeichnis. Jede .rs-Datei wird Script genannt. Jedes Skript enthält seine eigenen Kernel, Funktionen und Variablen. Ein Skript kann Folgendes enthalten:

  • Eine Pragma-Deklaration (#pragma version(1)), in der die in diesem Skript verwendete Version der RenderScript-Kernelsprache deklariert wird Derzeit ist 1 der einzige gültige Wert.
  • Eine Pragma-Deklaration (#pragma rs java_package_name(com.example.app)), die den Paketnamen der Java-Klassen angibt, die aus diesem Skript wiedergegeben wurden. Die Datei .rs muss Teil Ihres Anwendungspakets sein und nicht in einem Bibliotheksprojekt enthalten sein.
  • Null oder mehr aufrufbare Funktionen. Eine aufrufbare Funktion ist eine RenderScript-Funktion mit einem einzigen Thread, die Sie aus Ihrem 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 Skripte zugreifen. Diese werden häufig für die Parameterübergabe an RenderScript-Kernel verwendet. Globale Scripts werden hier ausführlicher erläutert.

  • Null oder mehr Compute-Kernel. Ein Compute-Kernel ist eine Funktion oder Sammlung von Funktionen, für die die RenderScript-Laufzeit über eine Sammlung von Daten parallel ausgeführt werden kann. Es gibt zwei Arten von Compute-Kernels: Zuordnungs-Kernel (auch Foreach-Kernel genannt) und Reduktionskernel.

    Ein Zuordnungskernel ist eine parallele Funktion, die für eine Sammlung von Allocations derselben Dimensionen ausgeführt wird. Standardmäßig wird sie einmal für jede Koordinate in diesen Dimensionen ausgeführt. Es wird normalerweise (aber nicht ausschließlich) verwendet, um eine Sammlung von Eingabe-Allocations in eine Ausgabe-Allocation einer Element nach der anderen umzuwandeln.

    • Hier ist ein Beispiel für einen einfachen Zuordnungskernel:

      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. Die Eigenschaft RS_KERNEL, die auf den Funktionsprototyp angewendet wird, gibt an, dass die Funktion ein RenderScript-Zuordnungskernel und keine aufrufbare Funktion ist. Das Argument in wird anhand der Eingabe-Allocation, die an den Kernelstart übergeben wurde, automatisch ausgefüllt. 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. Dieser Kernel wird standardmäßig über die gesamte Eingabe-Allocation ausgeführt, mit einer Ausführung der Kernelfunktion pro Element in Allocation.

      Ein Zuordnungs-Kernel kann einen oder mehrere Eingabe-Allocations, eine einzelne Ausgabe-Allocation oder beides haben. Die RenderScript-Laufzeit prüft, ob alle Eingabe- und Ausgabezuweisungen dieselben Dimensionen haben und ob die Element-Typen der Ein- und Ausgabezuweisungen mit dem Prototyp des Kernels übereinstimmen. Wenn eine dieser Prüfungen fehlschlägt, gibt 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 Eingabe- oder Ausgabe-Allocations als der Kernel benötigen, sollten diese Objekte an globale rs_allocation-Skripte gebunden sein und über einen Kernel oder eine aufrufbare Funktion über rsGetElementAt_type() oder rsSetElementAt_type() darauf zugegriffen werden.

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

      #define RS_KERNEL __attribute__((kernel))
      

    Ein Reduktions-Kernel ist eine Familie von Funktionen, die auf eine Sammlung von Eingabe-Allocations derselben Dimensionen angewendet wird. Standardmäßig wird die Akkumulatorfunktion einmal für jede Koordinate in diesen Dimensionen ausgeführt. Es wird normalerweise (aber nicht ausschließlich) verwendet, um eine Sammlung von Allocations-Eingaben auf einen einzelnen Wert zu „reduzieren“.

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

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

      Ein Reduktions-Kernel besteht aus einer oder mehreren vom Nutzer geschriebenen Funktionen. Mit #pragma rs reduce wird der Kernel durch Angabe seines Namens (in diesem Beispiel addint) sowie der Namen und Rollen der Funktionen, aus denen der Kernel besteht (in diesem Beispiel eine accumulator-Funktion addintAccum), definiert. Alle derartigen Funktionen müssen static sein. Ein Reduktions-Kernel erfordert immer eine accumulator-Funktion. Je nachdem, was der Kernel tun soll, kann er auch andere Funktionen haben.

      Eine Reduktions-Kernel-Akkumulatorfunktion muss void zurückgeben und mindestens zwei Argumente haben. Das erste Argument (in diesem Beispiel accum) ist ein Zeiger auf ein Akkumulatordatenelement. Das zweite Argument (in diesem Beispiel val) wird automatisch anhand der Eingabe Allocation ausgefüllt, die an den Kernel-Start übergeben wird. Das Akkumulator-Datenelement wird von der RenderScript-Laufzeit erstellt und wird standardmäßig auf null initialisiert. Dieser Kernel wird standardmäßig über die gesamte Eingabe-Allocation ausgeführt, mit einer Ausführung der Akkumulatorfunktion pro Element in Allocation. 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 Typ Element der Eingabezuweisung mit dem Prototyp der Akkumulatorfunktion übereinstimmt. Falls er nicht übereinstimmt, löst RenderScript eine Ausnahme aus.

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

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

      Reduzierungs-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 auf die Koordinaten der aktuellen Ausführung zugreifen. Diese müssen vom Typ int oder uint32_t sein. Diese Argumente sind optional.

    Eine Kernel-Zuordnungsfunktion oder eine Reduktions-Kernel-Akkumulatorfunktion kann auch das optionale spezielle Argument context vom Typ rs_kernel_context verwenden. Es wird von einer Familie von Laufzeit-APIs benötigt, die zum Abfragen bestimmter Attribute 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 init()-Funktion ist eine spezielle Art von aufrufbarer Funktion, die RenderScript bei der ersten Instanziierung des Skripts ausführt. Dadurch wird bei der Skripterstellung ein gewisses Maß an Berechnungen automatisch ausgeführt.
  • Null oder mehr statische Skript-globale und -Funktionen. Ein globales statisches Skript entspricht einem globalen Skript, mit der Ausnahme, dass über Java-Code nicht darauf zugegriffen werden kann. Eine statische Funktion ist eine Standard-C-Funktion, die über jeden Kernel oder jede aufrufbare Funktion im Skript aufgerufen werden kann. Sie ist jedoch nicht für die Java API verfügbar. Wenn ein globales Skript oder eine Funktion nicht über Java-Code aufgerufen werden muss, wird dringend empfohlen, es als static zu deklarieren.

Gleitkommagenauigkeit festlegen

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

  • #pragma rs_fp_full (Standard, wenn nichts angegeben ist): Für Anwendungen, die eine Gleitkomma-Genauigkeit gemäß IEEE 754-2008-Standard erfordern.
  • #pragma rs_fp_relaxed: Für Anwendungen, die keine strenge IEEE 754-2008-Compliance erfordern und weniger Präzision tolerieren können. Dieser Modus ermöglicht eine Bereinigung auf null für Denorms und eine Rundung auf null.
  • #pragma rs_fp_imprecise: Für Anwendungen, die keine strengen Präzisionsanforderungen haben. Mit diesem Modus werden alles in rs_fp_relaxed sowie Folgendes aktiviert:
    • Bei Vorgängen, die zu -0.0 führen, kann stattdessen +0.0 zurückgegeben werden.
    • 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 geringerer Präzision verfügbar sind (z. B. SIMD-CPU-Befehle), von Vorteil sein.

Über Java auf RenderScript APIs zugreifen

Wenn Sie eine Android-Anwendung entwickeln, die RenderScript verwendet, können Sie über Java auf zwei Arten auf ihre API zugreifen:

Hier sind die Vor- und Nachteile:

  • Wenn du die Support Library APIs verwendest, ist der RenderScript-Teil deiner App 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 ausgeführt werden als mit den nativen APIs (android.renderscript).
  • Bestimmte RenderScript-Funktionen sind nicht über die Support Library APIs verfügbar.
  • Wenn Sie die Support Library APIs verwenden, erhalten Sie (möglicherweise deutlich) größere APKs als bei Verwendung der nativen APIs (android.renderscript).

RenderScript Support Library APIs verwenden

Wenn Sie die RenderScript APIs der Support Library verwenden möchten, müssen Sie Ihre Entwicklungsumgebung so konfigurieren, dass auf sie zugegriffen werden kann. 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 Version 24.0.0 des Android SDK wird Android 2.2 (API-Level 8) nicht mehr unterstützt.

Du kannst die installierte Version dieser Tools im Android SDK Manager prüfen und aktualisieren.

So verwenden Sie die RenderScript-APIs der Support Library:

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

      Groovig

              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 bestimmte Verhaltensweisen im Android-Build-Prozess:

      • renderscriptTargetApi: Gibt die zu generierende Bytecode-Version an. Wir empfehlen, diesen Wert auf das niedrigste API-Level festzulegen, das alle verwendeten Funktionen bieten kann. Außerdem sollte renderscriptSupportModeEnabled auf true gesetzt werden. Gültige Werte für diese Einstellung sind beliebige ganzzahlige Werte zwischen 11 und dem zuletzt veröffentlichten API-Level. Falls die in Ihrem App-Manifest 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ückgesetzt werden 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 aus Java- oder Kotlin-Code verwenden

Die Verwendung von RenderScript aus Java- oder Kotlin-Code erfordert die 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 Kontext RenderScript sorgt dafür, dass RenderScript verwendet werden kann, und stellt ein Objekt bereit, mit dem sich die Lebensdauer aller nachfolgenden RenderScript-Objekte steuern lässt. Sie sollten die Kontexterstellung als einen Vorgang mit potenziell langer Ausführungszeit betrachten, da dadurch Ressourcen auf verschiedenen Hardwarekomponenten erstellt werden können. Er sollte sich nach Möglichkeit nicht im kritischen Pfad einer Anwendung befinden. Normalerweise verfügt eine Anwendung jeweils nur über einen einzigen RenderScript-Kontext.
  2. Erstellen Sie mindestens ein Allocation-Objekt, das an ein Skript übergeben werden soll. Ein Allocation ist ein RenderScript-Objekt, das Speicher für eine feste Datenmenge bereitstellt. Kernel in Skripts verwenden Allocation-Objekte als Eingabe und Ausgabe. Auf Allocation-Objekte kann in den Kerneln mit rsGetElementAt_type() und rsSetElementAt_type() zugegriffen 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 Skripttypen zur Verfügung:
    • ScriptC: Dies sind die benutzerdefinierten Skripts, wie oben im Abschnitt RenderScript-Kernel schreiben beschrieben. Jedes Skript hat eine Java-Klasse, die vom RenderScript-Compiler wiedergegeben wird, um den Zugriff auf das Skript aus Java-Code zu erleichtern. Diese Klasse hat den Namen ScriptC_filename. Befindet sich der obige Zuordnungs-Kernel beispielsweise in invert.rs und ein RenderScript-Kontext bereits in mRenderScript, würde der Java- oder Kotlin-Code zum Instanziieren des Skripts so lauten:

      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ßsche Weichzeichner, Faltung und Bildüberblendung. Weitere Informationen finden Sie in den abgeleiteten Klassen von ScriptIntrinsic.
  4. Zuweisungen mit Daten füllen. Außer bei Zuweisungen, die mit createFromBitmap() erstellt wurden, wird eine Zuweisung bei der erstmaligen Erstellung mit leeren Daten gefüllt. Verwenden Sie eine der Kopiermethoden in Allocation, um eine Zuweisung zu füllen. Die Kopiermethoden sind synchron.
  5. Legen Sie alle erforderlichen globalen Skripte fest. Sie können globale Variablen 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 namens lookup festzulegen, verwenden Sie die Java-Methode set_lookup(Allocation). Die set-Methoden sind asynchron.
  6. Starten Sie die entsprechenden Kernel und nicht aufrufbaren Funktionen.

    Methoden zum Starten eines bestimmten Kernels werden in derselben ScriptC_filename-Klasse mit Methoden namens forEach_mappingKernelName() oder reduce_reductionKernelName() wiedergegeben. Diese Einführungen sind asynchron. Abhängig von den Argumenten für den Kernel verwendet die Methode eine oder mehrere Zuweisungen, die alle dieselben Dimensionen haben müssen. Standardmäßig wird ein Kernel für jede Koordinate in diesen Dimensionen ausgeführt. Um einen Kernel für eine Teilmenge 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 aus derselben ScriptC_filename-Klasse. Diese Einführungen sind asynchron.

  7. Daten aus Allocation-Objekten und javaFutureType-Objekten abrufen Um auf Daten aus einem Allocation aus Java-Code zuzugreifen, müssen Sie diese Daten mit einer der Kopiermethoden in Allocation zurück in 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. Entfernt den RenderScript-Kontext. Sie können den RenderScript-Kontext mit destroy() löschen oder die automatische Speicherbereinigung des RenderScript-Kontextobjekts zulassen. Dadurch wird bei jeder weiteren Verwendung eines Objekts, das zu diesem Kontext gehört, eine Ausnahme ausgelöst.

Asynchrones Ausführungsmodell

Die reflektierten Methoden forEach, invoke, reduce und set sind asynchron und können jeweils vor Abschluss der angeforderten Aktion zu Java zurückkehren. Die einzelnen Aktionen werden jedoch in der Reihenfolge aufgelistet, in der sie gestartet werden.

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

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

Single-Source-RenderScript

Mit Android 7.0 (API-Ebene 24) wird eine neue Programmierfunktion namens Single-Source RenderScript eingeführt. Damit werden Kernel über das Skript gestartet, in dem sie definiert sind, und nicht über Java. Dieser Ansatz ist derzeit auf die Zuordnung von Kernels beschränkt, die in diesem Abschnitt aus Gründen der Präzision einfach als "Kernel" bezeichnet werden. Dieses neue Feature unterstützt auch das Erstellen von Zuweisungen vom Typ rs_allocation innerhalb des Skripts. Es ist jetzt möglich, einen ganzen Algorithmus ausschließlich in einem Skript zu implementieren, selbst wenn mehrere Kernel-Starts erforderlich sind. Dies hat zwei Vorteile: besser lesbarer Code, da die Implementierung eines Algorithmus in einer Sprache aufrechterhalten wird, und potenziell schnellerer Code, da weniger Übergänge zwischen Java und RenderScript bei mehreren Kernel-Starts auftreten.

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 eine Kernelfunktion als ersten Parameter, gefolgt von Eingabe- und Ausgabezuweisungen. Eine ähnliche API wie rsForEachWithOptions() verwendet ein zusätzliches Argument vom Typ rs_script_call_t, das eine Teilmenge der Elemente aus den Eingabe- und Ausgabezuweisungen angibt, die die Kernelfunktion verarbeiten soll.

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

Zuweisungen sind häufig erforderlich, um Zwischenergebnisse von einem Kernel-Start an einen anderen zu speichern und zu übergeben. 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 des Elements 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, die jeweils den Typ uchar4 haben.

Zuweisungen werden automatisch vom System verwaltet. Sie müssen sie nicht ausdrücklich freigeben oder freigeben. Du kannst jedoch rsClearObject(rs_allocation* alloc) aufrufen, um anzugeben, dass du den Handle alloc für die zugrunde liegende Zuweisung nicht mehr benötigst, damit das System Ressourcen so früh wie möglich freigeben kann.

Der Abschnitt RenderScript-Kernel schreiben enthält einen Beispiel-Kernel, der ein Bild invertiert. Im folgenden Beispiel wird dies erweitert, sodass mithilfe von Single-Source-RenderScript mehr als ein Effekt auf ein Bild angewendet wird. Sie enthält einen weiteren Kernel, greyscale, der ein Farbbild in Schwarz-Weiß umwandelt. Anschließend wendet die aufrufbare Funktion process() diese beiden Kernel nacheinander auf ein Eingabe-Image an und erzeugt ein Ausgabe-Image. 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 process()-Funktion so über Java oder Kotlin 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);

Dieses Beispiel zeigt, wie ein Algorithmus, der zwei Kernel-Starts beinhaltet, vollständig in der RenderScript-Sprache selbst implementiert werden kann. Ohne Single-Source-RenderScript müssten Sie beide Kernel aus dem Java-Code starten, um die Kernel-Starts von den Kernel-Definitionen zu trennen und den Algorithmus zu verstehen. Der Single-Source-RenderScript-Code ist nicht nur leichter zu lesen, sondern auch der Wechsel zwischen Java und dem Skript bei Kernelstarts entfällt. Einige iterative Algorithmen können Kernel Hunderte von Malen starten, wodurch der Aufwand einer solchen Umstellung erheblich verursacht wird.

Script-Globales

Ein globales Script ist eine normale globale Nicht-static-Variable in einer Scriptdatei (.rs). Für ein globales Skript mit dem Namen var, das in der Datei filename.rs definiert ist, wird die Methode get_var in der Klasse ScriptC_filename widergespiegelt. Sofern der globale nicht const ist, gibt es auch die Methode set_var.

Ein globales Skript hat zwei separate Werte: einen Java-Wert und einen script-Wert. Diese Werte verhalten sich wie folgt:

  • Wenn das Skript für var einen statischen Initialisierer enthält, wird der Anfangswert von var sowohl in Java als auch im Skript angegeben. Andernfalls ist dieser Anfangswert Null.
  • Zugriff auf var innerhalb des Skripts lesen und schreiben seinen Skriptwert.
  • Die Methode get_var liest den Java-Wert.
  • Die Methode set_var (falls vorhanden) schreibt den Java-Wert sofort und schreibt den Skriptwert asynchron.

HINWEIS: Das bedeutet, dass mit Ausnahme von statischen Initialisierern im Skript Werte, die von innerhalb eines Skripts in einen globalen Schlüssel geschrieben werden, für Java nicht sichtbar sind.

Reduzierung der Maiskerne im Detail

Bei der Reduktion werden mehrere Daten zu einem einzigen Wert kombiniert. Dies ist eine nützliche Primitive bei der parallelen Programmierung mit Anwendungen wie den folgenden:

  • die Summe oder das Produkt aus allen Daten berechnen
  • logische Vorgänge (and, or, xor) für alle Daten berechnen
  • um den Mindest- oder Höchstwert innerhalb der Daten zu finden
  • Suche nach einem bestimmten Wert oder nach der Koordinate eines bestimmten Werts innerhalb der Daten

Ab Android 7.0 (API-Level 24) und höher unterstützt RenderScript Reduzierungs-Kernel, um effiziente, von Nutzern geschriebene Reduktionsalgorithmen zu ermöglichen. Sie können Reduzierungs-Kernel für Eingaben mit 1, 2 oder 3 Dimensionen starten.

Das obige Beispiel zeigt einen einfachen addint-Reduktionskernel. Hier ist ein komplizierterer findMinAndMax-Reduktionskernel, 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: Hier finden Sie weitere Kernel zur Beispielreduzierung.

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

Beispiel:Im addint-Kernel werden die Akkumulator-Datenelemente (vom Typ int) zum Addieren von Eingabewerten verwendet. Es gibt keine Initialisierungsfunktion, sodass jedes Akkumulator-Datenelement auf null initialisiert wird.

Beispiel:Im findMinAndMax-Kernel werden die Akkumulator-Datenelemente (vom Typ MinAndMax) verwendet, um die bisher gefundenen Mindest- und Höchstwerte zu verfolgen. Es gibt eine Initialisierungsfunktion, mit der diese Werte auf LONG_MAX bzw. LONG_MIN und die Positionen dieser Werte auf -1 gesetzt werden. Dies bedeutet, dass die Werte im (leeren) Teil der verarbeiteten Eingabe nicht tatsächlich vorhanden sind.

RenderScript ruft Ihre 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 dem Akkumulator-Datenelement hinzu.

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

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 nur eine Eingabe hat und keine speziellen Argumente vorhanden sind, müssen Sie keine Kombinatorfunktion schreiben. RenderScript verwendet die Akkumulatorfunktion, um die Akkumulator-Datenelemente zu kombinieren. (Sie können trotzdem eine Kombiniererfunktion schreiben, wenn dieses Standardverhalten nicht Ihren Vorstellungen entspricht.)

Beispiel:Im addint-Kernel gibt es keine Kombiniererfunktion. Daher wird die Akkumulatorfunktion verwendet. Dies 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 die Addition dieser beiden Summen dasselbe wie die Addition der gesamten Sammlung.

Beispiel:Im findMinAndMax-Kernel prüft die Kombinatorfunktion, ob der im „Quell“-Akkumulatordatenelement *val aufgezeichnete Mindestwert kleiner als der im „Ziel“-Akkumulatordatenelement *accum aufgezeichnete Mindestwert ist, und aktualisiert *accum entsprechend. Für den Maximalwert wird eine ähnliche Arbeit ausgeführt. Dadurch wird *accum in den Status aktualisiert, den es gehabt hätte, wenn alle Eingabewerte in *accum und nicht in *accum und einige in *val akkumuliert worden wären.

Nachdem alle Akkumulator-Datenelemente kombiniert wurden, ermittelt RenderScript das Ergebnis der Reduktion und kehrt zu Java zurück. Sie können dazu eine Outconverter-Funktion schreiben. Sie müssen keine Outconverter-Funktion schreiben, wenn der endgültige Wert der kombinierten Akkumulatordatenelemente das Ergebnis der Reduktion 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 den Wert, den wir zurückgeben möchten.

Beispiel:Im Kernel findMinAndMax initialisiert die Outconverter-Funktion einen int2-Ergebniswert, um die Positionen der Minimal- und Maximalwerte zu speichern, 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 weglassen, je nachdem, was der Kernel tun soll.

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

Die Elemente in #pragma haben folgende Bedeutung:

  • reduce(kernelName) (erforderlich): Gibt an, dass ein Reduktionskernel definiert wird. Der Kernel wird mit der reflektierten Java-Methode reduce_kernelName gestartet.
  • initializer(initializerName) (optional): Gibt den Namen der Initialisiererfunktion für diesen Reduzierungs-Kernel 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 Akkumulatordatenelement, das diese Funktion initialisieren kann.

    Wenn Sie keine Initialisierungsfunktion angeben, initialisiert RenderScript jedes Akkumulator-Datenelement auf null (wie bei memset). Dabei verhält es 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 Reduktionskernel an. Wenn Sie den Kernel starten, ruft RenderScript diese Funktion einmal für jede Koordinate in den Eingaben auf, um ein Akkumulator-Datenelement entsprechend den Eingaben 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 diese Funktion ändern kann. in1 bis inN sind ein oder mehrere Argumente, die anhand der an den Kernel-Start übergebenen Eingaben automatisch ausgefüllt werden. Ein Argument pro Eingabe. Die Akkumulatorfunktion kann optional beliebige spezielle Argumente annehmen.

    Ein Beispiel-Kernel mit mehreren Eingaben ist dotProduct.

  • combiner(combinerName)

    (optional): Gibt den Namen der Kombiniererfunktion 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 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“-Akkumulatordatenelement, das diese Funktion ändern soll. other ist ein Zeiger auf ein „Quell“-Akkumulatordatenelement, 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 heißt, eines oder beide wurden noch nie gemäß Eingabedaten aktualisiert. Im Kernel findMinAndMax prüft die Kombinatorfunktion fMMCombiner beispielsweise explizit nach idx < 0, da dies ein entsprechendes Akkumulator-Datenelement mit dem Wert INITVAL angibt.

    Wenn Sie keine Kombinationsfunktion bereitstellen, 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 obligatorisch, wenn der Kernel mehr als eine Eingabe hat, der Eingabedatentyp nicht mit dem Akkumulatordatentyp übereinstimmt oder die Akkumulatorfunktion ein oder mehrere Sonderargumente annimmt.

  • outconverter(outconverterName) (optional): Gibt den Namen der Outconverter-Funktion für diesen Reduktions-Kernel an. Nachdem RenderScript alle Akkumulator-Datenelemente kombiniert hat, ruft es diese Funktion auf, um das Ergebnis der Reduzierung zu ermitteln, um zu Java zurückzukehren. Die Funktion muss so definiert werden:

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

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

    Wenn Sie keine Outconverter-Funktion bereitstellen, kopiert RenderScript das endgültige Akkumulator-Datenelement in das Ergebnisdatenelement und verhält sich 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 obligatorisch.

Beachten Sie, dass ein Kernel Eingabetypen, einen Akkumulator-Datenelementtyp und einen Ergebnistyp hat, die nicht gleich sein müssen. Beispielsweise sind im findMinAndMax-Kernel unterschiedlich der Eingabetyp long, der Akkumulator-Datenelementtyp MinAndMax und der Ergebnistyp int2.

Was können Sie nicht erwarten?

Sie dürfen sich nicht auf die Anzahl der Akkumulator-Datenelemente verlassen, die von RenderScript für eine bestimmte Kernel-Einführung erstellt werden. Es gibt keine Garantie, dass zwei Starts desselben Kernels mit denselben Eingaben die gleiche Anzahl von Akkumulator-Datenelementen erzeugen.

Sie dürfen sich nicht auf die Reihenfolge verlassen, in der RenderScript die Initialisierer-, Akkumulator- und Merger-Funktionen aufruft. Es können sogar einige von ihnen gleichzeitig aufgerufen werden. Es gibt keine Garantie, dass zwei Starts desselben Kernels mit derselben Eingabe in derselben Reihenfolge erfolgen. Die einzige Garantie ist, dass nur die Initialisierungsfunktion ein nicht initialisiertes Akkumulator-Datenelement sieht. Beispiele:

  • Es gibt keine Garantie, dass alle Akkumulator-Datenelemente initialisiert werden, bevor die Akkumulatorfunktion aufgerufen wird. Allerdings wird sie 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 für alle Eingabeelemente aufgerufen wurde, bevor die Kombiniererfunktion 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 befolgen, damit sich Ihr Kernel wie gewünscht verhält. Wenn Sie diese Regeln nicht beachten, kann es zu falschen Ergebnissen, nicht deterministischem Verhalten oder Laufzeitfehlern kommen.

Die folgenden Regeln besagen häufig, 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 Reduktion wie addint ist es in der Regel sinnvoll, dass "same" (Gleich) die mathematische Gleichheit bedeutet. Für eine Suche wie findMinAndMax, bei der identische Eingabewerte mehr als einmal vorkommen, müssen alle Positionen eines bestimmten Eingabewerts als „gleich“ betrachtet werden. Sie könnten einen ähnlichen Kernel schreiben, um "die Position der minimalen und maximalen Eingabewerte links zu ermitteln", wobei ein Minimalwert an Position 100 gegenüber einem identischen Mindestwert an Position 200 bevorzugt wird. Für diesen Kernel bedeutet "the same" (gleicher Wert) identischen location-Wert und nicht nur identischen value. Die Akkumulator- und Kombinatorfunktionen müssen sich von denen für findMax unterscheiden.

Die Initialisierungsfunktion muss einen Identitätswert erstellen. Wenn also I und A Akkumulatordatenelemente sind, die von der Initialisiererfunktion initialisiert wurden, und I noch nie an die Akkumulatorfunktion übergeben wurde, aber A möglicherweise noch nicht übergeben wurde, gilt:
  • In combinerName(&A, &I) muss A unverändert bleiben
  • Für combinerName(&I, &A) muss I denselben Wert wie A geben

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

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

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

Daher ist INITVAL tatsächlich ein Identitätswert.

Die Kombinationsfunktion muss kommutativ sein. Wenn also A und B Akkumulatordatenelemente sind, die von der Initialisierungsfunktion initialisiert wurden und möglicherweise null- oder mehrmals an die Akkumulatorfunktion übergeben wurden, muss combinerName(&A, &B) für A denselben Wert festlegen, mit dem combinerName(&B, &A) B festgelegt hat.

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

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

Die Kombinerfunktion muss assoziativ sein. Wenn also A, B und C Akkumulatordatenelemente sind, die von der Initialisierungsfunktion initialisiert und die möglicherweise gar nicht oder mehrmals an die Akkumulatorfunktion übergeben wurden, müssen die folgenden beiden Codesequenzen A auf denselben Wert festlegen:

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

Beispiel:Im addint-Kernel fügt die Kombiniererfunktion 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 Kombiniererfunktion.

Beispiel:Im findMinAndMax-Kernel ist

fMMCombiner(&A, &B)
mit
A = minmax(A, B)
identisch. 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 fMMCombiner ist es auch.

Die Akkumulatorfunktion und die Kombiniererfunktion müssen zusammen die grundlegende Faltregel einhalten. Wenn also A und B Akkumulatordatenelemente sind, A von der Initialisierungsfunktion initialisiert und möglicherweise gar nicht 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 der Akkumulatorfunktion ist, müssen die folgenden beiden Codesequenzen A auf denselben Wert festlegen:

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

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

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

In den Anweisungen 1 und 4 wird A auf denselben Wert festgelegt, sodass dieser Kernel der grundlegenden Faltregel folgt.

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

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

In den Anweisungen 1 und 4 wird A auf denselben Wert festgelegt, sodass dieser Kernel der grundlegenden Faltregel folgt.

Reduktions-Kernel über Java-Code aufrufen

Für einen Reduktions-Kernel namens 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 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 für jedes Eingabeargument in der Akkumulatorfunktion des Kernels ein Eingabeargument Allocation. Die RenderScript-Laufzeit prüft, ob alle Eingabezuweisungen dieselben Dimensionen haben und ob der Element-Typ jeder Eingabezuweisung mit dem des entsprechenden Eingabearguments des Prototyps der Akkumulatorfunktion übereinstimmt. Wenn eine dieser Prüfungen fehlschlägt, gibt RenderScript eine Ausnahme aus. Der Kernel wird für jede Koordinate in diesen Dimensionen ausgeführt.

Method 2 ist mit Methode 1 identisch, mit der Ausnahme, dass Methode 2 ein zusätzliches Argument sc verwendet, mit dem die Kernel-Ausführung auf eine Teilmenge der Koordinaten beschränkt werden kann.

Methode 3 ist mit Methode 1 identisch, mit der Ausnahme, dass anstelle von Zuordnungseingaben Java-Arrayeingaben verwendet werden. So müssen Sie keinen Code schreiben, um explizit eine Zuweisung 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 wird mit Methode 3 eine temporäre eindimensionale Zuordnung mit dem entsprechenden Element-Typ und aktiviertem setAutoPadding(boolean) erstellt. Das Array wird dann wie mit der entsprechenden copyFrom()-Methode von Allocation in die Zuordnung kopiert. 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 verschiedenen Arrays mit denselben Dimensionen und Elementtypen durchführt, können Sie die Leistung verbessern, indem Sie Zuweisungen explizit 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. Es steht für das zukünftige Ergebnis einer Ausführung des Reduktionskernels. 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 ermittelt. Sofern resultType kein Typ ohne Vorzeichen (Skalar, Vektor oder Array) ist, ist javaResultType der direkt entsprechende Java-Typ. Wenn resultType ein nicht signierter Typ ist und es einen größeren signierten Java-Typ gibt, ist javaResultType der größere signierte Java-Typ. Andernfalls ist es der direkt entsprechende Java-Typ. Beispiele:

  • Wenn für resultType der Wert int, int2 oder int[15] festgelegt ist, dann ist javaResultType auf int, Int2 oder int[]. Alle Werte von resultType können durch javaResultType dargestellt werden.
  • Wenn für resultType der Wert uint, uint2 oder uint[15] festgelegt ist, dann ist javaResultType der Wert long, Long2 oder long[]. Alle Werte von resultType können durch javaResultType dargestellt werden.
  • Wenn für resultType der Wert ulong, ulong2 oder ulong[15] festgelegt ist, dann ist javaResultType der Wert 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, dann hat javaFutureType den Wert result_resultType.
  • Wenn resultType ein Array der Länge Count mit Mitgliedern des Typs memberType ist, ist javaFutureType der Wert resultArrayCount_memberType.

Beispiele:

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 Arraytyps) ist, wird bei jedem Aufruf von javaFutureType.get() für dieselbe Instanz dasselbe Objekt zurückgegeben.

Wenn javaResultType nicht alle Werte des Typs resultType darstellen kann und ein Reduktionskernel einen nicht darstellbaren Wert erzeugt, dann 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 unsignierter Typ oder ein Vektortyp ist, ist devecSiInXType der direkt entsprechende Java-Typ. Wenn inXType ein nicht signierter 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, ist devecSiInXType der Java-Typ, der direkt dem Typ der Vektorkomponente entspricht. Wenn inXType ein nicht signierter Vektortyp ist, dann ist devecSiInXType der Java-Typ, der direkt dem signierten skalaren Typ mit derselben Größe wie der Vektorkomponententyp entspricht. Beispiele:

  • Wenn für inXType der Wert int gilt, ist devecSiInXType auf int gesetzt.
  • Wenn für inXType der Wert int2 gilt, ist devecSiInXType auf int gesetzt. Das Array ist eine abgeflachte Darstellung: Es enthält doppelt so viele skalare Elemente wie die Zuordnung mit 2-Komponenten-Vektorelementen. Entspricht der Funktionsweise der copyFrom()-Methoden von Allocation.
  • Wenn für inXType der Wert uint gilt, ist deviceSiInXType auf int. Ein vorzeichenbehafteter Wert im Java-Array wird als vorzeichenloser Wert desselben Bitmusters in der Zuordnung interpretiert. Dies entspricht der Funktionsweise der copyFrom()-Methoden von Allocation.
  • Wenn für inXType der Wert uint2 gilt, ist deviceSiInXType auf int. Dabei handelt es sich um eine Kombination aus der Verarbeitung von int2 und uint: Das Array ist eine vereinfachte Darstellung und signierte Java-Arraywerte werden als nicht signierte Elementwerte von RenderScript interpretiert.

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

  • Die Vektoreingabe eines Skripts wird auf der Java-Seite vereinfacht, das Vektorergebnis eines Skripts hingegen nicht.
  • Die nicht signierte Eingabe eines Skripts wird auf Java als signierte Eingabe derselben Größe dargestellt, während das nicht signierte Ergebnis auf Java als erweiterten signierten Typ dargestellt wird (außer bei ulong).

Weitere Kernel für Beispielreduzierungen

#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 für BasicRenderScript, RenderScriptIntrinsic und Hello Compute wird die Verwendung der auf dieser Seite behandelten APIs weiter veranschaulicht.