RenderScript ist ein Framework zum Ausführen rechenintensiver Aufgaben mit hoher Leistung auf Android. RenderScript ist hauptsächlich für die Verwendung mit datenparallelen Berechnungen vorgesehen, kann aber auch für serielle Arbeitslasten von Vorteil sein. Die RenderScript-Laufzeit parallelisiert die Arbeit auf den auf einem Gerät verfügbaren Prozessoren, z. B. Mehrkern-CPUs und GPUs. So können Sie sich auf das Ausdrücken von Algorithmen konzentrieren, anstatt Arbeit zu planen. RenderScript ist besonders nützlich für Anwendungen, die Bildverarbeitung, rechengestützte Fotografie oder Computer Vision nutzen.
Wenn Sie mit RenderScript beginnen möchten, sollten Sie sich mit zwei Hauptkonzepten vertraut machen:
- Die Sprache selbst ist eine von C99 abgeleitete Sprache zum Schreiben von Hochleistungs-Computing-Code. Unter RenderScript-Kernel schreiben wird beschrieben, wie Sie damit Rechenkernel schreiben.
- Die Control API wird zum Verwalten der Lebensdauer von RenderScript-Ressourcen und zum Steuern der Kernelausführung verwendet. Sie ist in drei verschiedenen Sprachen verfügbar: Java, C++ im Android NDK und die von C99 abgeleitete Kernelsprache selbst. RenderScript über Java-Code verwenden und Single-Source RenderScript beschreiben die erste bzw. die dritte Option.
RenderScript-Kernel schreiben
Ein RenderScript-Kernel befindet sich in der Regel in einer .rs
-Datei im Verzeichnis <project_root>/src/rs
. Jede .rs
-Datei wird als Script bezeichnet. Jedes Script enthält einen eigenen Satz von Kernels, Funktionen und Variablen. Ein Skript kann Folgendes enthalten:
- Eine Pragma-Deklaration (
#pragma version(1)
), mit der die Version der RenderScript-Kernelsprache deklariert wird, die in diesem Skript verwendet wird. Derzeit ist nur der Wert 1 gültig. - Eine Pragma-Deklaration (
#pragma rs java_package_name(com.example.app)
), die den Paketnamen der Java-Klassen deklariert, die in diesem Skript enthalten sind. 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 Single-Thread-RenderScript-Funktion, die Sie mit beliebigen Argumenten aus Ihrem Java-Code aufrufen können. Sie sind oft für die Ersteinrichtung oder für serielle Berechnungen in einer größeren Verarbeitungspipeline nützlich.
Null oder mehr globale Skriptvariablen. Eine globale Variable in einem Skript ähnelt einer globalen Variable in C. Sie können über Java-Code auf globale Skriptvariablen zugreifen. Diese werden häufig verwendet, um Parameter an RenderScript-Kernels zu übergeben. Weitere Informationen zu globalen Scriptvariablen finden Sie hier.
Null oder mehr Compute-Kernels. Ein Compute-Kernel ist eine Funktion oder eine Sammlung von Funktionen, die von der RenderScript-Laufzeit parallel für eine Sammlung von Daten ausgeführt werden können. Es gibt zwei Arten von Compute-Kernels: Mapping-Kernels (auch Foreach-Kernels genannt) und Reduction-Kernels.
Ein Mapping-Kernel ist eine parallele Funktion, die für eine 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 in der Regel (aber nicht ausschließlich) verwendet, um eine Sammlung vonAllocations
-Eingaben in eineAllocation
-Ausgabe zu transformieren, wobei jeweils einElement
verarbeitet wird.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 Punkten 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-Mapping-Kernel und keine aufrufbare Funktion ist. Dasin
-Argument wird automatisch anhand der EingabeAllocation
ausgefüllt, die an den Kernel-Start übergeben wird. Die Argumentex
undy
werden unten erläutert. Der vom Kernel zurückgegebene Wert wird automatisch an die entsprechende Stelle in der AusgabeAllocation
geschrieben. Standardmäßig wird dieser Kernel für die gesamte EingabeAllocation
ausgeführt. Die Kernelfunktion wird einmal proElement
in derAllocation
ausgeführt.Ein Mapping-Kernel kann einen oder mehrere Eingabe-
Allocations
, eine einzelne Ausgabe-Allocation
oder beides haben. Die RenderScript-Laufzeit prüft, ob alle Ein- und Ausgabezuweisungen dieselben Dimensionen haben und ob dieElement
-Typen der Ein- und Ausgabezuweisungen 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 einen
Allocation
-Eingang haben.Wenn Sie mehr Ein- oder Ausgabe-
Allocations
als der Kernel hat benötigen, sollten diese Objekte anrs_allocation
-Skriptvariablen gebunden und überrsGetElementAt_type()
oderrsSetElementAt_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 für eine Sammlung von Eingabe-
Allocations
mit denselben Dimensionen ausgeführt werden. Standardmäßig wird die Akkumulatorfunktion einmal für jede Koordinate in diesen Dimensionen ausgeführt. Sie wird in der Regel (aber nicht ausschließlich) verwendet, um eine Sammlung von EingabenAllocations
auf einen einzelnen Wert zu reduzieren.Hier ist ein Beispiel für einen einfachen Kernel für die Reduzierung, der die
Elements
seiner Eingabe addiert:#pragma rs reduce(addint) accumulator(addintAccum) static void addintAccum(int *accum, int val) { *accum += val; }
Ein Reduktions-Kernel besteht aus einer oder mehreren benutzerdefinierten Funktionen.
#pragma rs reduce
wird verwendet, um den Kernel zu definieren, indem sein Name (addint
in diesem Beispiel) und die Namen und Rollen der Funktionen angegeben werden, aus denen der Kernel besteht (eineaccumulator
-FunktionaddintAccum
in diesem Beispiel). Alle diese Funktionen müssenstatic
sein. Für einen Reduktionskernel ist immer eineaccumulator
-Funktion erforderlich. Er kann auch andere Funktionen haben, je nachdem, was der Kernel tun soll.Eine Akkumulatorfunktion für den Reduktionskernel muss
void
zurückgeben und mindestens zwei Argumente haben. Das erste Argument (in diesem Beispielaccum
) ist ein Zeiger auf ein Akkumulatordatenelement und das zweite (in diesem Beispielval
) wird automatisch basierend auf der EingabeAllocation
ausgefüllt, die an den Kernelstart übergeben wird. Das Akkumulatordatenelement wird von der RenderScript-Laufzeit erstellt und standardmäßig auf null initialisiert. Standardmäßig wird dieser Kernel für die gesamte EingabeAllocation
ausgeführt, wobei die Akkumulatorfunktion einmal proElement
in derAllocation
ausgeführt wird. Standardmäßig wird der endgültige Wert des Akkumulatordatenelements als Ergebnis der Reduzierung behandelt und an Java zurückgegeben. Die RenderScript-Laufzeit prüft, ob derElement
-Typ der Eingabe-Allocation mit dem Prototyp der Akkumulatorfunktion übereinstimmt. Wenn dies nicht der Fall ist, löst RenderScript eine Ausnahme aus.Ein Reduktionskernel hat mindestens eine Eingabe-
Allocations
, aber keine Ausgabe-Allocations
.Weitere Informationen zu Reduktions-Kernels finden Sie hier.
Reduktions-Kernels werden in Android 7.0 (API-Level 24) und höher unterstützt.
Eine Mapping-Kernel-Funktion oder eine Accumulator-Funktion für die Reduzierung kann über die speziellen Argumente
x
,y
undz
auf die Koordinaten der aktuellen Ausführung zugreifen. Diese müssen vom Typint
oderuint32_t
sein. Diese Argumente sind optional.Eine Mapping-Kernel-Funktion oder eine Reduktions-Kernel-Akkumulatorfunktion kann auch das optionale spezielle Argument
context
vom Typ rs_kernel_context annehmen. Sie wird von einer Reihe von Laufzeit-APIs benötigt, mit denen bestimmte Eigenschaften der aktuellen Ausführung abgefragt werden, z. B. rsGetDimX. Dascontext
-Argument ist ab Android 6.0 (API-Level 23) verfügbar.- Eine optionale
init()
-Funktion. Die Funktioninit()
ist eine spezielle Art von aufrufbarer Funktion, die von RenderScript ausgeführt wird, wenn das Skript zum ersten Mal instanziiert wird. Dadurch können einige Berechnungen automatisch beim Erstellen des Skripts erfolgen. - Null oder mehr statische globale Variablen und Funktionen für das Script. Eine statische globale Variable für Scripts entspricht einer globalen Variable für Scripts, mit dem Unterschied, dass nicht über Java-Code darauf zugegriffen werden kann. Eine statische Funktion ist eine Standard-C-Funktion, die von jeder Kernel- oder 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 Skripts nicht über Java-Code zugegriffen werden muss, wird dringend empfohlen, sie als
static
zu deklarieren.
Gleitkommazahl-Genauigkeit festlegen
Sie können die erforderliche Gleitkommagenauigkeit in einem Skript festlegen. Dies ist nützlich, wenn der vollständige IEEE 754-2008-Standard (der standardmäßig verwendet wird) nicht erforderlich ist. Mit den folgenden Pragma-Anweisungen kann ein anderer Grad an Gleitkomma-Genauigkeit festgelegt werden:
#pragma rs_fp_full
(Standardwert, wenn nichts angegeben ist): Für Apps, die Gleitkommazahlen mit der im IEEE 754-2008-Standard beschriebenen Genauigkeit erfordern.#pragma rs_fp_relaxed
: Für Apps, die keine strenge Einhaltung von IEEE 754-2008 erfordern und weniger Präzision tolerieren können. In diesem Modus werden Denormals auf null gesetzt und es wird auf null gerundet.#pragma rs_fp_imprecise
: Für Apps, die keine strengen Anforderungen an die Genauigkeit stellen. In diesem Modus ist alles inrs_fp_relaxed
sowie Folgendes möglich:- Bei Vorgängen, die -0,0 ergeben, 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 sehr nützlich sein, da zusätzliche Optimierungen nur mit einer geringeren Genauigkeit verfügbar sind (z. B. SIMD-CPU-Befehle).
Über Java auf RenderScript-APIs zugreifen
Wenn Sie eine Android-Anwendung entwickeln, die RenderScript verwendet, können Sie auf zwei Arten von Java aus auf die API zugreifen:
android.renderscript
: Die APIs in diesem Klassenpaket sind auf Geräten mit Android 3.0 (API-Level 11) und höher verfügbar.android.support.v8.renderscript
: Die APIs in diesem Paket sind über eine Support-Bibliothek verfügbar, sodass Sie sie auf Geräten mit Android 2.3 (API-Level 9) und höher verwenden können.
Hier sind die Vor- und Nachteile:
- Wenn Sie die Support Library APIs verwenden, ist der RenderScript-Teil Ihrer Anwendung mit Geräten kompatibel, auf denen Android 2.3 (API-Level 9) und höher ausgeführt wird, unabhängig davon, welche RenderScript-Funktionen Sie verwenden. So kann Ihre Anwendung auf mehr Geräten ausgeführt werden als bei Verwendung der nativen (
android.renderscript
) APIs. - Bestimmte RenderScript-Funktionen sind nicht über die Support Library APIs verfügbar.
- Wenn Sie die Support Library APIs verwenden, sind Ihre APKs (möglicherweise deutlich) größer als bei Verwendung der nativen (
android.renderscript
) APIs.
RenderScript-Supportbibliotheks-APIs verwenden
Wenn Sie die RenderScript APIs der Support Library verwenden möchten, müssen Sie Ihre Entwicklungsumgebung so konfigurieren, dass Sie darauf zugreifen können. Für die Verwendung dieser APIs sind die folgenden Android SDK-Tools erforderlich:
- Android SDK-Tools Revision 22.2 oder höher
- Android SDK Build-tools Revision 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 Support Library:
- Prüfen Sie, ob Sie die erforderliche Android SDK-Version installiert haben.
- 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 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 von Ihnen verwendeten Funktionen bereitstellen kann, undrenderscriptSupportModeEnabled
auftrue
zu setzen. Gültige Werte für diese Einstellung sind alle Ganzzahlwerte von 11 bis zum zuletzt veröffentlichten API-Level. Wenn die in Ihrem Anwendungsmanifest angegebene Mindest-SDK-Version auf einen anderen Wert festgelegt ist, wird dieser Wert ignoriert und die Mindest-SDK-Version wird anhand des Zielwerts in der Build-Datei festgelegt.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.
- Öffnen Sie die Datei
- Fügen Sie in Ihren Anwendungs-Klassen, 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-Code in Java oder Kotlin verwenden
Wenn Sie RenderScript in Java- oder Kotlin-Code verwenden möchten, müssen Sie die API-Klassen im Paket android.renderscript
oder android.support.v8.renderscript
verwenden. Die meisten Anwendungen folgen demselben grundlegenden Nutzungsmuster:
- RenderScript-Kontext initialisieren Der mit
create(Context)
erstellteRenderScript
-Kontext sorgt dafür, dass RenderScript verwendet werden kann, und stellt ein Objekt zur Steuerung der Lebensdauer aller nachfolgenden RenderScript-Objekte bereit. Die Kontextgenerierung kann ein potenziell langwieriger Vorgang sein, da Ressourcen auf verschiedenen Hardwarekomponenten erstellt werden können. Sie sollte nach Möglichkeit nicht im kritischen Pfad einer Anwendung enthalten sein. Normalerweise hat eine Anwendung jeweils nur einen RenderScript-Kontext. - Erstellen Sie mindestens eine
Allocation
, die an ein Skript übergeben werden soll. EinAllocation
ist ein RenderScript-Objekt, das Speicher für eine feste Datenmenge bietet. Kernels in Skripts verwendenAllocation
-Objekte als Ein- und Ausgabe. AufAllocation
-Objekte kann in Kernels mitrsGetElementAt_type()
undrsSetElementAt_type()
zugegriffen werden, wenn sie als globale Skriptvariablen gebunden sind. MitAllocation
-Objekten können Arrays vom Java-Code an den RenderScript-Code und umgekehrt übergeben werden.Allocation
-Objekte werden in der Regel mitcreateTyped()
odercreateFromBitmap()
erstellt. - Erstellen Sie alle erforderlichen Skripts. Bei der Verwendung von RenderScript stehen Ihnen 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 gespiegelt wird, um den Zugriff auf das Script über Java-Code zu erleichtern. Diese Klasse hat den Namen
ScriptC_filename
. Wenn sich der oben genannte Mapping-Kernel beispielsweise ininvert.rs
befindet und sich ein RenderScript-Kontext bereits inmRenderScript
befindet, wäre der Java- oder Kotlin-Code zum Instanziieren des Scripts:Kotlin
val invert = ScriptC_invert(renderScript)
Java
ScriptC_invert invert = new ScriptC_invert(renderScript);
- ScriptIntrinsic: Dies sind integrierte RenderScript-Kernels für gängige Vorgänge wie Gaußsche Unschärfe, Faltung und Bildüberblendung. Weitere Informationen finden Sie in den Unterklassen von
ScriptIntrinsic
.
- ScriptC: Dies sind die benutzerdefinierten Skripts, die oben unter RenderScript-Kernel schreiben beschrieben werden. Jedes Script hat eine Java-Klasse, die vom RenderScript-Compiler gespiegelt wird, um den Zugriff auf das Script über Java-Code zu erleichtern. Diese Klasse hat den Namen
- Zuweisungen mit Daten füllen Mit Ausnahme von Zuweisungen, die mit
createFromBitmap()
erstellt wurden, wird eine Zuweisung beim ersten Erstellen mit leeren Daten gefüllt. Verwenden Sie eine der „copy“-Methoden inAllocation
, um eine Zuweisung zu erstellen. Die „copy“-Methoden sind synchron. - Legen Sie alle erforderlichen globalen Skriptvariablen fest. Sie können globale Variablen mit Methoden in derselben
ScriptC_filename
-Klasse namensset_globalname
festlegen. Um beispielsweise eineint
-Variable mit dem Namenthreshold
festzulegen, verwenden Sie die Java-Methodeset_threshold(int)
. Um einers_allocation
-Variable mit dem Namenlookup
festzulegen, verwenden Sie die Java-Methodeset_lookup(Allocation)
. Dieset
-Methoden sind asynchron. - Geeignete Kernels und aufrufbare Funktionen starten:
Methoden zum Starten eines bestimmten Kernels werden in derselben
ScriptC_filename
-Klasse mit den NamenforEach_mappingKernelName()
oderreduce_reductionKernelName()
abgebildet. Diese Starts sind asynchron. Je nach den Argumenten für den Kernel akzeptiert 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. Wenn Sie einen Kernel für eine Teilmenge dieser Koordinaten ausführen möchten, übergeben Sie ein entsprechendesScript.LaunchOptions
als letztes Argument an die MethodeforEach
oderreduce
.Rufen Sie aufrufbare Funktionen mit den
invoke_functionName
-Methoden auf, die in derselbenScriptC_filename
-Klasse enthalten sind. Diese Starts sind asynchron. - Daten aus
Allocation
-Objekten und javaFutureType-Objekten abrufen. Wenn Sie in Java-Code auf Daten aus einemAllocation
zugreifen möchten, müssen Sie diese Daten mit einer der „copy“-Methoden inAllocation
zurück nach Java kopieren. Um das Ergebnis eines Reduktionskernels zu erhalten, müssen Sie die MethodejavaFutureType.get()
verwenden. Die Methoden „copy“ undget()
sind synchron. - RenderScript-Kontext schließen Sie können den RenderScript-Kontext mit
destroy()
zerstören oder indem Sie zulassen, dass das RenderScript-Kontextobjekt per Garbage Collection entfernt wird. Bei jeder weiteren Verwendung eines Objekts, das zu diesem Kontext gehört, wird dann eine Ausnahme ausgelöst.
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 „copy“-Methoden zum Kopieren von Daten in und aus Zuweisungen. Die Methode „copy“ 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 zum Abrufen des Ergebnisses einer Reduzierung. get()
ist synchron und wird in Bezug auf die Reduzierung (die asynchron ist) serialisiert.
Single-Source RenderScript
Mit Android 7.0 (API-Level 24) wird eine neue Programmierfunktion namens Single-Source RenderScript eingeführt, bei der Kerne aus dem Skript gestartet werden, in dem sie definiert sind, und nicht aus Java. Dieser Ansatz ist derzeit auf Mapping-Kernels beschränkt, die in diesem Abschnitt der Einfachheit halber als „Kernels“ bezeichnet werden. Mit dieser neuen Funktion können auch Zuweisungen vom Typ
rs_allocation
im Skript erstellt werden. Es ist jetzt möglich, einen ganzen Algorithmus ausschließlich in einem Skript zu implementieren, auch wenn mehrere Kernel-Starts erforderlich sind.
Das hat zwei Vorteile: Der Code ist besser lesbar, da die Implementierung eines Algorithmus in einer Sprache erfolgt, und er ist potenziell schneller, da weniger Übergänge zwischen Java und RenderScript bei mehreren Kernel-Starts stattfinden.
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 Ein- und Ausgabezuweisungen. Eine ähnliche API
rsForEachWithOptions()
verwendet ein zusätzliches Argument vom Typ
rs_script_call_t
, das eine Teilmenge der Elemente aus den Ein- und Ausgabezuweisungen für die zu verarbeitende Kernelfunktion angibt.
Um die RenderScript-Berechnung zu starten, rufen Sie die aufrufbare Funktion aus Java auf.
Folgen Sie der Anleitung unter RenderScript über 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 Kernels.
Zuweisungen sind oft erforderlich, um Zwischenergebnisse von einem Kernel-Launch zum nächsten zu speichern und zu übergeben. Sie können sie mit
rsCreateAllocation() erstellen. Eine benutzerfreundliche Form dieser API ist
rsCreateAllocation_<T><W>(…)
, wobei T der Datentyp für ein Element und W die Vektorbreite für das Element ist. Die API akzeptiert die Größen in den Dimensionen X, Y und Z als Argumente. Bei 1D- oder 2D-Zuweisungen kann die Größe für die Dimension Y oder Z weggelassen werden. Mit rsCreateAllocation_uchar4(16384)
wird beispielsweise eine 1D-Zuweisung von 16.384 Elementen erstellt, 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.
Im Abschnitt RenderScript-Kernel schreiben finden Sie ein Beispiel für einen Kernel, der ein Bild invertiert. Im folgenden Beispiel wird das 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 gibt ein Ausgabebild aus. Zuweisungen für Ein- und 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 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 nachzuvollziehen. Der Single-Source-RenderScript-Code ist nicht nur leichter zu lesen, sondern es entfällt auch der Übergang zwischen Java und dem Script bei Kernel-Starts. Bei einigen iterativen Algorithmen werden möglicherweise Hunderte von Kernels gestartet, sodass der Aufwand für solche Übergänge erheblich ist.
Globale Variablen für Skripts
Eine globale Variable für Skripts ist eine gewöhnliche globale Variable ohne static
in einer Skriptdatei (.rs
). Für eine globale Scriptvariable mit dem Namen var, die in der Datei filename.rs
definiert ist, wird eine Methode get_var
in der Klasse ScriptC_filename
wiedergegeben. Sofern die globale Variable nicht const
ist, gibt es auch eine Methode set_var
.
Eine bestimmte globale Variable für das Script hat zwei separate Werte: einen Java-Wert und einen Script-Wert. Diese Werte verhalten sich so:
- Wenn var eine statische Initialisierung im Skript hat, wird damit der Anfangswert von var sowohl in Java als auch im Skript festgelegt. Andernfalls ist dieser Anfangswert null.
- Bei Zugriffen auf var innerhalb des Skripts wird der Skriptwert gelesen und geschrieben.
- Die Methode
get_var
liest den Java-Wert. - Die Methode
set_var
(sofern vorhanden) schreibt den Java-Wert sofort und den Script-Wert asynchron.
HINWEIS:Das bedeutet, dass Werte, die in einem Skript in eine globale Variable geschrieben werden, für Java nicht sichtbar sind. Eine Ausnahme bilden statische Initialisierer im Skript.
Reduktions-Kernels im Detail
Bei der Reduzierung wird eine Sammlung von Daten zu einem einzelnen Wert zusammengefasst. Dies ist ein nützliches Primitiv in der parallelen Programmierung, mit Anwendungen wie den folgenden:
- Summe oder Produkt aller Daten berechnen
- Logische Operationen (
and
,or
,xor
) für alle Daten berechnen - den Mindest- oder Höchstwert in den Daten ermitteln
- nach einem bestimmten Wert oder nach der Koordinate eines bestimmten Werts in den Daten suchen
In Android 7.0 (API-Level 24) und höher unterstützt RenderScript Reduktions-Kernels, um effiziente, von Nutzern geschriebene Reduktionsalgorithmen zu ermöglichen. Sie können Reduktions-Kernels für Eingaben mit 1, 2 oder 3 Dimensionen starten.
Im obigen Beispiel sehen Sie einen einfachen addint-Reduktionskernel.
Hier ist ein komplexerer findMinAndMax-Reduktionskernel, der die Positionen der minimalen und maximalen long
-Werte in einem eindimensionalen Allocation
ermittelt:
#define LONG_MAX (long)((1UL << 63) - 1) #define LONG_MIN (long)(1UL << 63) #pragma rs reduce(findMinAndMax) \ initializer(fMMInit) accumulator(fMMAccumulator) \ combiner(fMMCombiner) outconverter(fMMOutConverter) // Either a value and the location where it was found, or INITVAL. typedef struct { long val; int idx; // -1 indicates INITVAL } IndexedVal; typedef struct { IndexedVal min, max; } MinAndMax; // In discussion below, this initial value { { LONG_MAX, -1 }, { LONG_MIN, -1 } } // is called INITVAL. static void fMMInit(MinAndMax *accum) { accum->min.val = LONG_MAX; accum->min.idx = -1; accum->max.val = LONG_MIN; accum->max.idx = -1; } //---------------------------------------------------------------------- // In describing the behavior of the accumulator and combiner functions, // it is helpful to describe hypothetical functions // IndexedVal min(IndexedVal a, IndexedVal b) // IndexedVal max(IndexedVal a, IndexedVal b) // MinAndMax minmax(MinAndMax a, MinAndMax b) // MinAndMax minmax(MinAndMax accum, IndexedVal val) // // The effect of // IndexedVal min(IndexedVal a, IndexedVal b) // is to return the IndexedVal from among the two arguments // whose val is lesser, except that when an IndexedVal // has a negative index, that IndexedVal is never less than // any other IndexedVal; therefore, if exactly one of the // two arguments has a negative index, the min is the other // argument. Like ordinary arithmetic min and max, this function // is commutative and associative; that is, // // min(A, B) == min(B, A) // commutative // min(A, min(B, C)) == min((A, B), C) // associative // // The effect of // IndexedVal max(IndexedVal a, IndexedVal b) // is analogous (greater . . . never greater than). // // Then there is // // MinAndMax minmax(MinAndMax a, MinAndMax b) { // return MinAndMax(min(a.min, b.min), max(a.max, b.max)); // } // // Like ordinary arithmetic min and max, the above function // is commutative and associative; that is: // // minmax(A, B) == minmax(B, A) // commutative // minmax(A, minmax(B, C)) == minmax((A, B), C) // associative // // Finally define // // MinAndMax minmax(MinAndMax accum, IndexedVal val) { // return minmax(accum, MinAndMax(val, val)); // } //---------------------------------------------------------------------- // This function can be explained as doing: // *accum = minmax(*accum, IndexedVal(in, x)) // // This function simply computes minimum and maximum values as if // INITVAL.min were greater than any other minimum value and // INITVAL.max were less than any other maximum value. Note that if // *accum is INITVAL, then this function sets // *accum = IndexedVal(in, x) // // After this function is called, both accum->min.idx and accum->max.idx // will have nonnegative values: // - x is always nonnegative, so if this function ever sets one of the // idx fields, it will set it to a nonnegative value // - if one of the idx fields is negative, then the corresponding // val field must be LONG_MAX or LONG_MIN, so the function will always // set both the val and idx fields static void fMMAccumulator(MinAndMax *accum, long in, int x) { IndexedVal me; me.val = in; me.idx = x; if (me.val <= accum->min.val) accum->min = me; if (me.val >= accum->max.val) accum->max = me; } // This function can be explained as doing: // *accum = minmax(*accum, *val) // // This function simply computes minimum and maximum values as if // INITVAL.min were greater than any other minimum value and // INITVAL.max were less than any other maximum value. Note that if // one of the two accumulator data items is INITVAL, then this // function sets *accum to the other one. static void fMMCombiner(MinAndMax *accum, const MinAndMax *val) { if ((accum->min.idx < 0) || (val->min.val < accum->min.val)) accum->min = val->min; if ((accum->max.idx < 0) || (val->max.val > accum->max.val)) accum->max = val->max; } static void fMMOutConverter(int2 *result, const MinAndMax *val) { result->x = val->min.idx; result->y = val->max.idx; }
HINWEIS:Weitere Beispiele für Reduktions-Kernels
Um einen Reduktionskernel auszuführen, erstellt die RenderScript-Laufzeit eine oder mehrere Variablen, die als Akkumulatordaten-Elemente bezeichnet werden, um den Status des Reduktionsprozesses zu speichern. Die RenderScript-Laufzeit wählt die Anzahl der Akkumulatordaten so aus, dass die Leistung maximiert wird. Der Typ der Akkumulatordatenelemente (accumType) wird durch die Akkumulatorfunktion des Kernels bestimmt. Das erste Argument dieser Funktion ist ein Zeiger auf ein Akkumulatordatenelement. Standardmäßig wird jedes Akkumulatordatenelement mit null initialisiert (als ob durch memset
). Sie können jedoch eine Initialisierungsfunktion schreiben, um etwas anderes zu tun.
Beispiel:Im addint-Kernel werden die Akkumulatordaten (vom Typ int
) verwendet, um Eingabewerte zu addieren. Es gibt keine Initialisierungsfunktion. Daher wird jedes Akkumulatordatenelement mit null initialisiert.
Beispiel:Im findMinAndMax-Kernel werden die Akkumulatordaten (vom Typ MinAndMax
) verwendet, um die bisher gefundenen Minimal- und Maximalwerte zu erfassen. Es gibt eine Initialisierungsfunktion, um diese auf LONG_MAX
bzw. LONG_MIN
festzulegen und die Positionen dieser Werte auf -1 zu setzen. Das bedeutet, dass die Werte nicht im (leeren) Teil der Eingabe vorhanden sind, der verarbeitet wurde.
RenderScript ruft Ihre Akkumulatorfunktion einmal für jede Koordinate in den Eingaben auf. Normalerweise sollte Ihre Funktion das Akkumulatordaten-Element entsprechend der Eingabe aktualisieren.
Beispiel:Im addint-Kernel wird mit der Akkumulatorfunktion der Wert eines Eingabeelements zum Akkumulatordatenelement addiert.
Beispiel:Im findMinAndMax-Kernel wird in der Akkumulatorfunktion geprüft, 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. Das Akkumulatordatenelement wird entsprechend aktualisiert.
Nachdem die Akkumulatorfunktion einmal für jede Koordinate in den Eingaben aufgerufen wurde, muss RenderScript die Akkumulatordaten-Elemente zu einem einzelnen Akkumulatordaten-Element kombinieren. Dazu können Sie eine Combiner-Funktion schreiben. Wenn die Akkumulatorfunktion eine einzelne Eingabe und keine speziellen Argumente hat, müssen Sie keine Kombiniererfunktion schreiben. RenderScript verwendet die Akkumulatorfunktion, um die Akkumulatordaten zu kombinieren. Sie können trotzdem eine Kombinationsfunktion schreiben, wenn dieses Standardverhalten nicht Ihren Anforderungen entspricht.
Beispiel:Im addint-Kernel gibt es keine Kombinationsfunktion. 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 die Summe dieser beiden Summen dieselbe wie die Summe der gesamten Sammlung.
Beispiel:Im Kernel findMinAndMax wird mit der Kombinationsfunktion geprüft, ob der im Akkumulatordaten-Element *val
der Quelle aufgezeichnete Mindestwert kleiner ist als der im Akkumulatordaten-Element *accum
des Ziels aufgezeichnete Mindestwert. *accum
wird entsprechend aktualisiert. Ähnlich wird auch der Maximalwert berechnet. Dadurch wird *accum
in den Zustand versetzt, den es gehabt hätte, wenn alle Eingabewerte in *accum
und nicht einige in *accum
und einige in *val
zusammengefasst worden wären.
Nachdem alle Akkumulatordaten zusammengeführt wurden, ermittelt 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 endgültige Wert der kombinierten Akkumulatordaten das Ergebnis der Reduzierung sein soll.
Beispiel:Im addint-Kernel gibt es keine Outconverter-Funktion. Der endgültige Wert der kombinierten Datenelemente ist die Summe aller Elemente der Eingabe. Das ist der 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 Akkumulatordaten ergeben.
Reduktionskernel schreiben
#pragma rs reduce
definiert einen Reduktionskernel, indem der Name und die Namen und Rollen der Funktionen angegeben werden, aus denen der Kernel besteht. Alle diese Funktionen müssen static
sein. Für einen Reduktionskernel ist immer eine accumulator
-Funktion erforderlich. 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 Reduktions-Kernel definiert wird. Eine reflektierte Java-Methodereduce_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 Akkumulatordatenelement auf. Die Funktion muss so definiert werden:static void initializerName(accumType *accum) { … }
accum
ist ein Zeiger auf ein Akkumulatordatenelement, das für diese Funktion initialisiert werden soll.Wenn Sie keine Initialisierungsfunktion angeben, initialisiert RenderScript jedes Akkumulatordaten-Element mit null (als ob durch
memset
). Es wird so verarbeitet, als ob es eine Initialisierungsfunktion gäbe, 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 Akkumulatordaten-Element 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 Akkumulatordatenelement, das von dieser Funktion geändert werden soll.in1
bisinN
sind ein oder mehrere Argumente, die automatisch auf Grundlage der an den Kernel-Start übergebenen Eingaben ausgefüllt werden, ein Argument pro Eingabe. Die Akkumulatorfunktion kann optional eines der Sonderargumente verwenden.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 Akkumulatordaten in einem einzigen Akkumulatordaten-Element zu kombinieren. Die Funktion muss so definiert werden:
static void combinerName(accumType *accum, const accumType *other) { … }
accum
ist ein Zeiger auf ein „Ziel“-Akkumulatordatenelement, das von dieser Funktion geändert werden soll.other
ist ein Zeiger auf ein „Quell“-Akkumulatordatenelement, das mit dieser Funktion in*accum
„kombiniert“ werden soll.HINWEIS:Es ist möglich, dass
*accum
,*other
oder beide initialisiert wurden, aber nie an die Akkumulatorfunktion übergeben wurden. Das bedeutet, dass eine oder beide Variablen nie anhand von Eingabedaten aktualisiert wurden. Im findMinAndMax-Kernel wird beispielsweise in der KombinationsfunktionfMMCombiner
explizit nachidx < 0
gesucht, da dies auf ein solches Akkumulatordatenelement hinweist, dessen Wert INITVAL ist.Wenn Sie keine Combiner-Funktion angeben, verwendet RenderScript stattdessen die Accumulator-Funktion. Das Verhalten ist dann so, als ob es eine Combiner-Funktion gäbe, die so aussieht:
static void combinerName(accumType *accum, const accumType *other) { accumulatorName(accum, *other); }
Eine Kombinationsfunktion ist erforderlich, wenn der Kernel mehr als eine Eingabe hat, wenn der Eingabedatentyp nicht mit dem Akkumulatordatentyp übereinstimmt oder wenn die Akkumulatorfunktion ein oder mehrere Sonderargumente verwendet.
outconverter(outconverterName)
(optional): Gibt den Namen der Outconverter-Funktion für diesen Reduktionskernel an. Nachdem RenderScript alle Akkumulatordaten zusammengeführt hat, wird diese Funktion aufgerufen, um das Ergebnis der Reduzierung zu ermitteln, das an Java zurückgegeben werden soll. Die Funktion muss so definiert werden:static void outconverterName(resultType *result, const accumType *accum) { … }
result
ist ein Zeiger auf ein Ergebnisdatenelement, das von der RenderScript-Laufzeitumgebung zugewiesen, aber nicht initialisiert wird. Diese Funktion initialisiert es mit dem Ergebnis der Reduzierung. resultType ist der Typ dieses Datenelements, der nicht mit accumType übereinstimmen muss.accum
ist ein Zeiger auf das endgültige Akkumulatordatenelement, das von der Combiner-Funktion berechnet wird.Wenn Sie keine Outconverter-Funktion angeben, kopiert RenderScript das letzte Akkumulatordatumselement in das Ergebnisdatumselement. Das Verhalten ist dann so, als ob es eine Outconverter-Funktion gäbe, die so aussieht:
static void outconverterName(accumType *result, const accumType *accum) { *result = *accum; }
Wenn Sie einen anderen Ergebnistyp als den Akkumulatordatentyp wünschen, ist die Outconverter-Funktion erforderlich.
Ein Kernel hat Eingabetypen, einen Akkumulatordaten-Elementtyp und einen Ergebnistyp, die nicht alle identisch sein müssen. Im Kernel findMinAndMax sind beispielsweise der Eingabetyp long
, der Akkumulatordaten-Elementtyp MinAndMax
und der Ergebnistyp int2
unterschiedlich.
Was darfst du nicht annehmen?
Sie dürfen sich nicht auf die Anzahl der von RenderScript für einen bestimmten Kernel-Start erstellten Akkumulatordaten verlassen. Es gibt keine Garantie dafür, dass bei zwei Ausführungen desselben Kernels mit denselben Eingaben dieselbe Anzahl von Akkumulatordatenelementen erstellt wird.
Sie dürfen sich nicht darauf verlassen, in welcher Reihenfolge RenderScript die Initialisierungs-, Akkumulator- und Kombinationsfunktionen aufruft. Einige von ihnen werden möglicherweise sogar parallel aufgerufen. Es gibt keine Garantie dafür, dass zwei Ausführungen desselben Kernels mit derselben Eingabe derselben Reihenfolge folgen. Die einzige Garantie ist, dass nur die Initialisierungsfunktion jemals ein nicht initialisiertes Akkumulator-Datenelement sieht. Beispiel:
- Es gibt keine Garantie dafür, dass alle Akkumulatordaten initialisiert werden, bevor die Akkumulatorfunktion aufgerufen wird. Sie wird jedoch nur für ein initialisiertes Akkumulatordatum aufgerufen.
- Es gibt keine Garantie für die Reihenfolge, in der Eingabeelemente an die Akkumulatorfunktion übergeben werden.
- Es gibt keine Garantie dafür, dass die Akkumulatorfunktion für alle Eingabeelemente aufgerufen wurde, bevor die Kombiniererfunktion aufgerufen wird.
Eine Folge davon ist, dass der findMinAndMax-Kernel nicht deterministisch ist: Wenn die Eingabe mehrere Vorkommen desselben Minimal- oder Maximalwerts enthält, wissen Sie nicht, welches Vorkommen der Kernel findet.
Was müssen Sie garantieren?
Da das RenderScript-System einen Kernel auf viele verschiedene Arten ausführen kann, müssen Sie bestimmte Regeln einhalten, damit sich 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 heißt es oft, dass zwei Akkumulatordaten-Elemente 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, wenn „gleich“ mathematische Gleichheit bedeutet. Bei einer „pick any“-Suche wie findMinAndMax („find the location of minimum and maximum input values“), bei der es mehr als ein Vorkommen identischer Eingabewerte geben kann, müssen alle Positionen eines bestimmten Eingabewerts als „gleich“ betrachtet werden. Sie könnten einen ähnlichen Kernel schreiben, um den Ort der am weitesten links stehenden minimalen und maximalen Eingabewerte zu ermitteln. Dabei würde beispielsweise ein Minimalwert an Position 100 einem identischen Minimalwert an Position 200 vorgezogen. Für diesen Kernel würde „gleich“ nicht nur einen identischen Wert, sondern auch eine identische Position bedeuten. Die Akkumulator- und Kombiniererfunktionen müssten sich von denen für findMinAndMax unterscheiden.
Die Initialisierungsfunktion muss einen Identitätswert erstellen. WennI
und A
Akkumulatordaten sind, die von der Initialisierungsfunktion initialisiert wurden, und I
noch nie an die Akkumulatorfunktion übergeben wurde (A
aber möglicherweise schon), dann
combinerName(&A, &I)
mussA
unverändert lassen.combinerName(&I, &A)
mussI
unverändert lassen, d. h.I
=A
.
Beispiel:Im addint-Kernel 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 findMinAndMax-Kernel wird ein Akkumulator-Datenelement mit INITVAL
initialisiert.
fMMCombiner(&A, &I)
lässtA
unverändert, daI
INITVAL
ist.fMMCombiner(&I, &A)
legtI
aufA
fest, daI
gleichINITVAL
ist.
Daher ist INITVAL
tatsächlich ein Identitätswert.
Die Kombinationsfunktion muss kommutativ sein. Das heißt: Wenn A
und B
Akkumulatordaten sind, die von der Initialisierungsfunktion initialisiert wurden und die null oder mehrmals an die Akkumulatorfunktion übergeben wurden, muss combinerName(&A, &B)
A
auf denselben Wert festlegen, auf den combinerName(&B, &A)
B
festlegt.
Beispiel:Im addint-Kernel addiert die Kombinationsfunktion die beiden Akkumulatordatenwerte. Die Addition ist kommutativ.
Beispiel:Im Kernel findMinAndMax ist fMMCombiner(&A, &B)
dasselbe wie A = minmax(A, B)
und minmax
ist kommutativ, also ist auch fMMCombiner
kommutativ.
Die Kombinationsfunktion muss assoziativ sein. Das heißt: Wenn A
, B
und C
Akkumulatordaten sind, die von der Initialisierungsfunktion initialisiert wurden und die null oder mehrmals an die Akkumulatorfunktion übergeben wurden, dann muss in den folgenden beiden Codefolgen A
auf denselben Wert gesetzt werden:
combinerName(&A, &B); combinerName(&A, &C);
combinerName(&B, &C); combinerName(&A, &B);
Beispiel:Im addint-Kernel werden mit der Kombinationsfunktion die beiden Akkumulatordatenwerte addiert:
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, daher ist auch die Kombiniererfunktion assoziativ.
Beispiel:Im findMinAndMax-Kernel
fMMCombiner(&A, &B)
A = minmax(A, B)
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 auch fMMCombiner
assoziativ.
Die Akkumulatorfunktion und die Kombiniererfunktion müssen zusammen der grundlegenden Faltungsregel entsprechen. Wenn A
und B
Akkumulatordaten sind, A
von der Initialisierungsfunktion initialisiert wurde und 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 Codefolgen A
auf denselben Wert setzen:
accumulatorName(&A, args); // statement 1
initializerName(&B); // statement 2 accumulatorName(&B, args); // statement 3 combinerName(&A, &B); // statement 4
Beispiel:Im addint-Kernel für einen Eingabewert V:
- Anweisung 1 ist dieselbe wie
A += V
. - Anweisung 2 ist mit
B = 0
identisch. - Aussage 3 entspricht
B += V
, was wiederumB = V
entspricht. - Aussage 4 entspricht
A += B
, was wiederumA += V
entspricht.
In den Anweisungen 1 und 4 wird A
auf denselben Wert gesetzt. Dieser Kernel entspricht also der grundlegenden Faltungsregel.
Beispiel:Im findMinAndMax-Kernel für einen Eingabewert V an der Koordinate X:
- Anweisung 1 ist dieselbe wie
A = minmax(A, IndexedVal(V, X))
. - Anweisung 2 ist mit
B = INITVAL
identisch. - Aussage 3 entspricht
Da B der Anfangswert ist, entspricht das Folgendem:B = minmax(B, IndexedVal(V, X))
B = IndexedVal(V, X)
- Anweisung 4 entspricht
was dasselbe ist wieA = minmax(A, B)
A = minmax(A, IndexedVal(V, X))
In den Anweisungen 1 und 4 wird A
auf denselben Wert gesetzt. Dieser Kernel entspricht also der grundlegenden Faltungsregel.
Reduktionskernel über 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
enthalten sind:
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 Allocation
-Eingabeargument. Die RenderScript-Laufzeit prüft, ob alle Eingabe-Allocations dieselben Dimensionen haben und ob der Element
-Typ jeder Eingabe-Allocation mit dem des entsprechenden Eingabearguments des Prototyps der Akkumulatorfunktion übereinstimmt. Wenn eine dieser Prüfungen fehlschlägt, löst RenderScript eine Ausnahme aus. Der Kernel wird für jede Koordinate in diesen Dimensionen ausgeführt.
Methode 2 entspricht Methode 1, mit dem Unterschied, dass Methode 2 ein zusätzliches Argument sc
verwendet, mit dem die Kernelausführung auf eine Teilmenge der Koordinaten beschränkt werden kann.
Methode 3 entspricht Methode 1, mit der Ausnahme, dass anstelle von Zuweisungseingaben Java-Array-Eingaben verwendet werden. Das ist eine praktische Funktion, mit der Sie sich das Schreiben von Code zum expliziten Erstellen einer Zuweisung und zum Kopieren von Daten aus einem Java-Array in die Zuweisung sparen. 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 Zuweisung mit dem entsprechenden Element
-Typ und aktivierter setAutoPadding(boolean)
erstellt und das Array in die Zuweisung kopiert, als ob mit der entsprechenden copyFrom()
-Methode von Allocation
. Anschließend wird Methode 1 aufgerufen und die temporären Zuweisungen werden übergeben.
HINWEIS:Wenn Ihre Anwendung mehrere Kernelaufrufe mit demselben Array oder mit verschiedenen Arrays mit denselben Dimensionen und demselben Elementtyp ausführt, können Sie die Leistung verbessern, indem Sie Zuweisungen selbst explizit 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
. Sie stellt das zukünftige Ergebnis eines Reduktionskernel-Laufs dar. Um das tatsächliche Ergebnis des Laufs zu erhalten, rufen Sie die get()
-Methode 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 abgeleitet. Sofern resultType kein vorzeichenloser Typ (Skalar, Vektor oder Array) ist, ist javaResultType der direkt entsprechende Java-Typ. Wenn resultType ein vorzeichenloser Typ ist und es einen größeren signierten Java-Typ gibt, ist javaResultType dieser größere signierte Java-Typ. Andernfalls ist es der direkt entsprechende Java-Typ. Beispiel:
- Wenn resultType
int
,int2
oderint[15]
ist, ist javaResultTypeint
,Int2
oderint[]
. Alle Werte von resultType können durch javaResultType dargestellt werden. - Wenn resultType
uint
,uint2
oderuint[15]
ist, dann ist javaResultTypelong
,Long2
oderlong[]
. Alle Werte von resultType können durch javaResultType dargestellt werden. - Wenn resultType
ulong
,ulong2
oderulong[15]
ist, ist javaResultTypelong
,Long2
oderlong[]
. Bestimmte Werte von resultType können nicht durch javaResultType dargestellt werden.
javaFutureType ist der zukünftige Ergebnistyp, der dem resultType der outconverter-Funktion entspricht.
- Wenn resultType kein Array-Typ ist, ist javaFutureType
result_resultType
. - Wenn resultType ein Array der Länge Count mit Elementen vom Typ memberType ist, dann 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 Arraytyps) 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. Sofern inXType kein vorzeichenloser Typ oder Vektortyp ist, ist devecSiInXType der direkt entsprechende Java-Typ. Wenn inXType ein vorzeichenloser Skalartyp ist, ist devecSiInXType der Java-Typ, der dem vorzeichenbehafteten Skalartyp derselben Größe direkt 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 Skalartyp derselben Größe wie der Vektorkomponententyp entspricht. Beispiel:
- Wenn inXType
int
ist, dann ist devecSiInXTypeint
. - Wenn inXType
int2
ist, dann ist devecSiInXTypeint
. Das Array ist eine reduzierte Darstellung: Es hat doppelt so viele skalare Elemente wie die Zuweisung Vektorelemente mit zwei Komponenten. Das funktioniert genauso wie diecopyFrom()
-Methoden vonAllocation
. - Wenn inXType
uint
ist, ist deviceSiInXTypeint
. Ein signierter Wert im Java-Array wird als vorzeichenloser Wert mit demselben Bitmuster in der Zuweisung interpretiert. Das funktioniert genauso wie diecopyFrom()
-Methoden vonAllocation
. - Wenn inXType
uint2
ist, ist deviceSiInXTypeint
. Dies ist eine Kombination aus der Art und Weise, wieint2
unduint
verarbeitet werden: Das Array ist eine vereinfachte Darstellung und signierte Java-Array-Werte werden als nicht signierte RenderScript-Elementwerte interpretiert.
Bei Methode 3 werden Eingabetypen anders als Ergebnistypen verarbeitet:
- Die Vektoreingabe eines Skripts wird auf der Java-Seite vereinfacht, das Vektorergebnis eines Skripts jedoch nicht.
- Die nicht signierte Eingabe eines Skripts wird auf der Java-Seite als signierte Eingabe derselben Größe dargestellt, während das nicht signierte Ergebnis eines Skripts auf der Java-Seite als erweiterter signierter Typ dargestellt wird (außer bei
ulong
).
Weitere Beispiele für Reduktions-Kernels
#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.