RenderScript ist ein Framework zum Ausführen rechenintensiver Aufgaben mit hoher Leistung auf Android-Geräten. RenderScript ist in erster Linie für die Verwendung mit datenparrallel ausgeführten Berechnungen gedacht, kann aber auch bei seriellen Arbeitslasten von Vorteil sein. Die RenderScript-Laufzeit parallelisiert die Arbeit auf allen auf einem Gerät verfügbaren Prozessoren, z. B. auf Multi-Core-CPUs und GPUs. So können Sie sich darauf konzentrieren, Algorithmen auszudrücken, anstatt Arbeit zu planen. RenderScript ist besonders nützlich für Anwendungen, die Bildverarbeitung, rechnergestützte Fotografie oder maschinelles Sehen nutzen.
Zu Beginn sollten Sie zwei wichtige Konzepte von RenderScript kennen:
- Die Sprache selbst ist eine von C99 abgeleitete Sprache zum Schreiben von Hochleistungs-Computing-Code. Im Artikel RenderScript-Kernel schreiben wird beschrieben, wie Sie damit Compute-Kernel schreiben.
- Die control API wird zum Verwalten der Lebensdauer von RenderScript-Ressourcen und zur Steuerung der Kernelausführung verwendet. Es ist in drei verschiedenen Sprachen verfügbar: Java, C++ im Android NDK und die C99-basierte Kernelsprache selbst. RenderScript aus Java-Code verwenden und RenderScript mit einer einzigen Quelle beschreiben die erste und die dritte Option.
RenderScript-Kernel schreiben
Ein RenderScript-Kernel befindet sich normalerweise in einer .rs
-Datei im Verzeichnis <project_root>/src/rs
. Jede .rs
-Datei wird als Script bezeichnet. Jedes Script enthält eigene Kernel, Funktionen und Variablen. Ein Script kann Folgendes enthalten:
- Eine Pragmadeklaration (
#pragma version(1)
), die die Version der in diesem Script verwendeten RenderScript-Kernelsprache angibt. Derzeit ist „1“ der einzige gültige Wert. - Eine Pragma-Deklaration (
#pragma rs java_package_name(com.example.app)
), die den Paketnamen der Java-Klassen angibt, die aus diesem Script gespiegelt werden. Die.rs
-Datei muss Teil Ihres Anwendungspakets sein und sich nicht in einem Bibliotheksprojekt befinden. - Null oder mehr aufrufbare Funktionen. Eine aufrufbare Funktion ist eine einzeilige RenderScript-Funktion, die Sie mit beliebigen Argumenten aus Ihrem Java-Code aufrufen können. Sie sind oft nützlich für die Ersteinrichtung oder serielle Berechnungen innerhalb einer größeren Verarbeitungspipeline.
Null oder mehr Script-Globale Eine Script-Globale Variable ähnelt einer globalen Variablen in C. Sie können über Java-Code auf Script-Globale zugreifen. Diese werden häufig für die Parameterübergabe an RenderScript-Kernel verwendet. Hier finden Sie weitere Informationen zu Script-Globalen.
Null oder mehr Rechenkerne. Ein Compute-Kernel ist eine Funktion oder eine Sammlung von Funktionen, die die RenderScript-Laufzeit parallel auf einer Datensammlung ausführen kann. Es gibt zwei Arten von Compute-Kerneln: Mapping-Kernel (auch foreach-Kernel genannt) und Reduktion-Kernel.
Ein Zuordnungskern ist eine parallele Funktion, die auf einer Sammlung von
Allocations
mit denselben Dimensionen ausgeführt wird. Standardmäßig wird er einmal für jede Koordinate in diesen Dimensionen ausgeführt. Sie wird in der Regel (aber nicht ausschließlich) verwendet, um eine Sammlung von EingabenAllocations
in eine AusgabeAllocation
umzuwandeln, wobei jeweils ein ElementElement
verarbeitet wird.Hier ein Beispiel für einen einfachen Mapping-Kernel:
uchar4 RS_KERNEL invert(uchar4 in, uint32_t x, uint32_t y) { uchar4 out = in; out.r = 255 - in.r; out.g = 255 - in.g; out.b = 255 - in.b; return out; }
In den meisten Aspekten entspricht sie einer Standard-C-Funktion. Die Eigenschaft
RS_KERNEL
, die auf den Funktionsprototyp angewendet wird, gibt an, dass es sich bei der Funktion um einen RenderScript-Zuordnungskern handelt und nicht um eine aufrufbare Funktion. Das Argumentin
wird automatisch anhand der EingabeAllocation
ausgefüllt, die an den Kernelstart übergeben wird. Die Argumentex
undy
werden unten beschrieben. Der vom Kernel zurückgegebene Wert wird automatisch an den entsprechenden Speicherort in der AusgabeAllocation
geschrieben. Standardmäßig wird dieser Kernel für die gesamte EingabeAllocation
ausgeführt, wobei die Kernelfunktion einmal proElement
in derAllocation
ausgeführt wird.Ein Zuordnungskern kann eine oder mehrere Eingaben
Allocations
, eine einzelne AusgabeAllocation
oder beides haben. Die RenderScript-Laufzeit prüft, ob alle Eingabe- und Ausgabezuweisungen dieselben Dimensionen haben und ob dieElement
-Typen der Eingabe- und Ausgabezuweisungen mit dem Prototyp des Kernels übereinstimmen. Wenn eine dieser Prüfungen fehlschlägt, wirft RenderScript eine Ausnahme.HINWEIS:Vor Android 6.0 (API-Level 23) darf ein Zuordnungskern nicht mehr als eine Eingabe
Allocation
haben.Wenn Sie mehr Eingabe- oder Ausgabe-
Allocations
benötigen als der Kernel hat, sollten diese Objekte anrs_allocation
-Script-Globale gebunden und überrsGetElementAt_type()
oderrsSetElementAt_type()
von einem Kernel oder einer aufrufbaren Funktion aus aufgerufen werden.HINWEIS:
RS_KERNEL
ist ein Makro, das automatisch von RenderScript definiert wird:#define RS_KERNEL __attribute__((kernel))
Ein Reduktionskern ist eine Funktionsfamilie, die auf einer Sammlung von Eingaben
Allocations
derselben Dimensionen angewendet wird. Standardmäßig wird die Akkumulierungsfunktion einmal für jede Koordinate in diesen Dimensionen ausgeführt. Sie wird in der Regel (aber nicht ausschließlich) verwendet, um eine Sammlung von EingabenAllocations
auf einen einzelnen Wert zu reduzieren.Hier ist ein Beispiel für einen einfachen Reduktionskern, der die
Elements
seiner Eingabe addiert:#pragma rs reduce(addint) accumulator(addintAccum) static void addintAccum(int *accum, int val) { *accum += val; }
Ein Reduktionskern besteht aus einer oder mehreren vom Nutzer geschriebenen Funktionen. Mit
#pragma rs reduce
wird der Kernel definiert, indem sein Name (in diesem Beispieladdint
) und die Namen und Rollen der Funktionen angegeben werden, aus denen der Kernel besteht (in diesem Beispiel eineaccumulator
-FunktionaddintAccum
). Alle diese Funktionen müssenstatic
sein. Für einen Reduktionskernel ist immer eineaccumulator
-Funktion erforderlich. Je nach gewünschter Funktion kann er auch andere Funktionen haben.Eine Akkumulatorfunktion für den Reduktionskern muss
void
zurückgeben und mindestens zwei Argumente haben. Das erste Argument (in diesem Beispielaccum
) ist ein Verweis auf ein Akkumulatordatenelement und das zweite (in diesem Beispielval
) wird automatisch basierend auf der EingabeAllocation
ausgefüllt, die an den Kernelstart übergeben wird. Das Akkumulator-Datenelement wird von der RenderScript-Laufzeit erstellt und standardmäßig auf null initialisiert. Standardmäßig wird dieser Kernel auf die gesamte EingabeAllocation
angewendet, wobei die Akkumulatorfunktion einmal proElement
in derAllocation
ausgeführt wird. Der endgültige Wert des Akkumulator-Dateneintrags wird standardmäßig als Ergebnis der Reduzierung behandelt und an Java zurückgegeben. Die RenderScript-Laufzeit prüft, ob derElement
-Typ der Eingabezuweisung mit dem Prototyp der Akkumulatorfunktion übereinstimmt. Andernfalls wirft RenderScript eine Ausnahme.Ein Reduktionskern hat eine oder mehrere Eingaben
Allocations
, aber keine AusgabeAllocations
.Weitere Informationen zu Reduzierungskernen
Reduzierungskerne werden ab Android 7.0 (API-Level 24) unterstützt.
Eine Mapping-Kernelfunktion oder eine Reducer-Kernel-Akkumulatorfunktion kann über die speziellen Argumente
x
,y
undz
auf die Koordinaten der aktuellen Ausführung zugreifen. Diese Argumente müssen vom Typint
oderuint32_t
sein. Diese Argumente sind optional.Eine Zuordnungskernfunktion oder eine Reduktionskernakkumulatorfunktion kann auch das optionale Spezialargument
context
vom Typ rs_kernel_context annehmen. Sie wird von einer Reihe von Laufzeit-APIs benötigt, mit denen bestimmte Eigenschaften der aktuellen Ausführung abgefragt werden, z. B. rsGetDimX. Das Argumentcontext
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 Script zum ersten Mal instanziiert wird. So können einige Berechnungen beim Erstellen des Scripts automatisch ausgeführt werden. - Null oder mehr statische Script-Globale und ‑Funktionen Ein statisches Script-Global-Objekt entspricht einem Script-Global-Objekt, mit der Ausnahme, dass es nicht über Java-Code aufgerufen werden kann. Eine statische Funktion ist eine Standard-C-Funktion, die von jedem Kernel oder jeder aufrufbaren Funktion im Script aufgerufen werden kann, aber nicht für die Java API freigegeben ist. Wenn auf eine globale Variable oder Funktion eines Scripts nicht über Java-Code zugegriffen werden muss, wird dringend empfohlen, sie als
static
zu deklarieren.
Gleitkommagenauigkeit festlegen
Sie können die erforderliche Gleitkommagenauigkeit in einem Script steuern. Dies ist nützlich, wenn der vollständige IEEE 754-2008-Standard (standardmäßig verwendet) nicht erforderlich ist. Mit den folgenden Pragmas kann eine andere Gleitkommagenauigkeit festgelegt werden:
#pragma rs_fp_full
(Standardwert, wenn nichts angegeben ist): Für Apps, die eine Gleitkommagenauigkeit gemäß dem IEEE 754-2008-Standard erfordern.#pragma rs_fp_relaxed
: Für Apps, die nicht der strengen IEEE 754-2008-Norm entsprechen müssen und eine geringere Genauigkeit tolerieren können. In diesem Modus wird die Nullung für Denormalisierungen und die Rundung auf Null aktiviert.#pragma rs_fp_imprecise
: Für Apps, die keine strengen Anforderungen an die Genauigkeit stellen. In diesem Modus sind alle Funktionen vonrs_fp_relaxed
sowie die folgenden Funktionen aktiviert:- Bei Vorgängen, die zu -0,0 führen, kann stattdessen +0,0 zurückgegeben werden.
- Vorgänge mit INF und NAN sind nicht definiert.
Die meisten Anwendungen können rs_fp_relaxed
ohne Nebenwirkungen verwenden. Dies kann bei einigen Architekturen sehr vorteilhaft sein, da zusätzliche Optimierungen nur mit reduzierter Genauigkeit verfügbar sind (z. B. SIMD-CPU-Anweisungen).
Auf RenderScript APIs aus Java zugreifen
Wenn Sie eine Android-Anwendung entwickeln, die RenderScript verwendet, haben Sie zwei Möglichkeiten, von Java aus auf die API zuzugreifen:
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 Supportbibliothek verfügbar. Sie können sie auf Geräten mit Android 2.3 (API-Level 9) und höher verwenden.
Hier sind die Vor- und Nachteile:
- Wenn Sie die APIs der Support Library verwenden, ist der RenderScript-Teil Ihrer Anwendung unabhängig von den verwendeten RenderScript-Funktionen mit Geräten mit Android 2.3 (API-Level 9) und höher kompatibel. So kann Ihre Anwendung auf mehr Geräten ausgeführt werden als bei Verwendung der nativen APIs (
android.renderscript
). - Bestimmte RenderScript-Funktionen sind über die Support Library APIs nicht verfügbar.
- Wenn Sie die Support Library APIs verwenden, erhalten Sie (möglicherweise deutlich) größere APKs als bei Verwendung der nativen APIs (
android.renderscript
).
APIs der RenderScript-Unterstützungsbibliothek verwenden
Damit Sie die RenderScript APIs der Support Library verwenden können, müssen Sie Ihre Entwicklungsumgebung so konfigurieren, dass Sie darauf zugreifen können. Für die Verwendung dieser APIs sind die folgenden Android SDK-Tools erforderlich:
- Android SDK-Tools Version 22.2 oder höher
- Android SDK Build-Tools, Version 18.1.0 oder höher
Ab den Android SDK Build-Tools 24.0.0 wird Android 2.2 (API-Level 8) nicht mehr unterstützt.
Sie können die installierte Version dieser Tools im Android SDK Manager prüfen und aktualisieren.
So verwenden Sie die RenderScript APIs der Support Library:
- Prüfen Sie, ob die erforderliche Android SDK-Version installiert ist.
- Aktualisieren Sie die Einstellungen für den Android-Buildprozess, um die RenderScript-Einstellungen aufzunehmen:
- Öffnen Sie die Datei
build.gradle
im App-Ordner Ihres Anwendungsmoduls. - Fügen Sie der Datei die folgenden RenderScript-Einstellungen hinzu:
Groovy
android { compileSdkVersion 33 defaultConfig { minSdkVersion 9 targetSdkVersion 19 renderscriptTargetApi 18 renderscriptSupportModeEnabled true } }
Kotlin
android { compileSdkVersion(33) defaultConfig { minSdkVersion(9) targetSdkVersion(19) renderscriptTargetApi = 18 renderscriptSupportModeEnabled = true } }
Mit den oben aufgeführten Einstellungen wird das Verhalten des Android-Build-Prozesses gesteuert:
renderscriptTargetApi
– Gibt die zu generierende Bytecodeversion an. Wir empfehlen, diesen Wert auf die niedrigste API-Ebene festzulegen, die alle von Ihnen verwendeten Funktionen bereitstellen kann. Legen SierenderscriptSupportModeEnabled
auftrue
fest. Gültige Werte für diese Einstellung sind beliebige Ganzzahlwerte von 11 bis zur aktuellsten API-Version. Wenn die im Anwendungsmanifest angegebene minimale SDK-Version auf einen anderen Wert festgelegt ist, wird dieser Wert ignoriert und die minimale SDK-Version wird anhand des Zielwerts in der Build-Datei festgelegt.renderscriptSupportModeEnabled
– Gibt an, dass der generierte Bytecode auf eine kompatible Version zurückfallen soll, wenn das Gerät, auf dem er ausgeführt wird, die Zielversion nicht unterstützt.
- Öffnen Sie die Datei
- Fügen Sie in Ihren Anwendungsklassen, die RenderScript verwenden, einen Import für die Klassen der Supportbibliothek hinzu:
Kotlin
import android.support.v8.renderscript.*
Java
import android.support.v8.renderscript.*;
RenderScript aus Java- oder Kotlin-Code verwenden
Die Verwendung von RenderScript aus Java- oder Kotlin-Code setzt die API-Klassen im Paket android.renderscript
oder android.support.v8.renderscript
voraus. Die meisten Anwendungen folgen demselben grundlegenden Nutzungsmuster:
- Initialisieren Sie einen RenderScript-Kontext. Der mit
create(Context)
erstellteRenderScript
-Kontext sorgt dafür, dass RenderScript verwendet werden kann, und bietet ein Objekt, mit dem die Lebensdauer aller nachfolgenden RenderScript-Objekte gesteuert werden kann. Die Kontexterstellung sollte als potenziell langwieriger Vorgang betrachtet werden, da dadurch Ressourcen auf verschiedenen Hardwarekomponenten erstellt werden können. Sie sollte nach Möglichkeit nicht im kritischen Pfad einer Anwendung liegen. Normalerweise hat eine Anwendung jeweils nur einen einzigen RenderScript-Kontext. - Erstellen Sie mindestens eine
Allocation
, die an ein Script übergeben werden soll. EinAllocation
ist ein RenderScript-Objekt, das Speicherplatz für eine feste Datenmenge bietet. Kernel in Scripts nehmenAllocation
-Objekte als Eingabe und Ausgabe an. AufAllocation
-Objekte kann in Kerneln mitrsGetElementAt_type()
undrsSetElementAt_type()
zugegriffen werden, wenn sie als Script-Globale gebunden sind. MitAllocation
-Objekten können Arrays von Java-Code an RenderScript-Code und umgekehrt übergeben werden.Allocation
-Objekte werden in der Regel mitcreateTyped()
odercreateFromBitmap()
erstellt. - Erstellen Sie alle erforderlichen Scripts. Bei der Verwendung von RenderScript stehen zwei Arten von Scripts zur Verfügung:
- ScriptC: Dies sind die benutzerdefinierten Scripts, die oben unter RenderScript-Kernel schreiben beschrieben wurden. Jedes Script hat eine Java-Klasse, die vom RenderScript-Compiler gespiegelt wird, um den Zugriff auf das Script über Java-Code zu vereinfachen. Diese Klasse hat den Namen
ScriptC_filename
. Wenn sich der Mapping-Kernel beispielsweise unterinvert.rs
und ein RenderScript-Kontext bereits untermRenderScript
befindet, lautet der Java- oder Kotlin-Code zum Instanziieren des Scripts:Kotlin
val invert = ScriptC_invert(renderScript)
Java
ScriptC_invert invert = new ScriptC_invert(renderScript);
- ScriptIntrinsic: Dies sind integrierte RenderScript-Kernel für gängige Vorgänge wie Gaußscher Weichzeichner, Convolution und Bildmischung. Weitere Informationen finden Sie in den Unterklassen von
ScriptIntrinsic
.
- ScriptC: Dies sind die benutzerdefinierten Scripts, die oben unter RenderScript-Kernel schreiben beschrieben wurden. Jedes Script hat eine Java-Klasse, die vom RenderScript-Compiler gespiegelt wird, um den Zugriff auf das Script über Java-Code zu vereinfachen. Diese Klasse hat den Namen
- Zuweisungen mit Daten füllen Mit Ausnahme von Zuordnungen, die mit
createFromBitmap()
erstellt wurden, werden Zuordnungen beim Erstellen mit leeren Daten ausgefüllt. Verwenden Sie eine der Kopiermethoden inAllocation
, um eine Zuweisung zu füllen. Die „copy“-Methoden sind synchron. - Legen Sie alle erforderlichen Script-Globale fest. Sie können Globals mithilfe von Methoden in derselben
ScriptC_filename
-Klasse mit dem Namenset_globalname
festlegen. Wenn Sie beispielsweise eineint
-Variable mit dem Namenthreshold
festlegen möchten, verwenden Sie die Java-Methodeset_threshold(int)
. Wenn Sie einers_allocation
-Variable mit dem Namenlookup
festlegen möchten, verwenden Sie die Java-Methodeset_lookup(Allocation)
. Dieset
-Methoden sind asynchron. - Starten Sie die entsprechenden Kernel und aufrufbaren Funktionen.
Methoden zum Starten eines bestimmten Kernels sind in derselben
ScriptC_filename
-Klasse mit Methoden namensforEach_mappingKernelName()
oderreduce_reductionKernelName()
enthalten. Diese Einführungen sind asynchron. Je nach den Argumenten für den Kernel nimmt die Methode eine oder mehrere Zuweisungen an, die alle dieselben Dimensionen haben müssen. Standardmäßig wird ein Kernel auf allen Koordinaten in diesen Dimensionen ausgeführt. Wenn Sie einen Kernel auf einer Teilmenge dieser Koordinaten ausführen möchten, übergeben Sie der MethodeforEach
oderreduce
als letztes Argument ein geeignetesScript.LaunchOptions
.Sie können aufrufbare Funktionen mit den
invoke_functionName
-Methoden starten, die in derselbenScriptC_filename
-Klasse enthalten sind. Diese Einführungen sind asynchron. - Daten aus
Allocation
-Objekten und javaFutureType-Objekten abrufen. Wenn du über Java-Code auf Daten aus einerAllocation
zugreifen möchtest, musst du diese Daten mit einer der „copy“-Methoden inAllocation
zurück in Java kopieren. Um das Ergebnis eines Reduktionskerns zu erhalten, müssen Sie die MethodejavaFutureType.get()
verwenden. Die Methoden „copy“ undget()
sind synchron. - RenderScript-Kontext abbauen Sie können den RenderScript-Kontext mit
destroy()
löschen oder zulassen, dass das RenderScript-Kontextobjekt durch die Garbage Collection gelöscht wird. Dies führt dazu, dass bei jeder weiteren Verwendung eines Objekts, das zu diesem Kontext gehört, eine Ausnahme ausgelöst wird.
Asynchrones Ausführungsmodell
Die reflektierten Methoden forEach
, invoke
, reduce
und set
sind asynchron. Jede kann zu Java zurückkehren, bevor die angeforderte Aktion abgeschlossen ist. Die einzelnen Aktionen werden jedoch in der Reihenfolge ihrer Ausführung serialisiert.
Die Klasse Allocation
bietet „copy“-Methoden zum Kopieren von Daten in und aus Zuordnungen. Eine „copy“-Methode ist synchron und wird im Hinblick auf alle der oben genannten asynchronen Aktionen serialisiert, die sich auf dieselbe Zuweisung beziehen.
Die reflektierten Klassen javaFutureType bieten eine get()
-Methode, um das Ergebnis einer Reduzierung zu erhalten. get()
ist synchron und wird in Bezug auf die Reduzierung (die asynchron ist) serialisiert.
RenderScript mit einer einzelnen Quelle
Android 7.0 (API-Level 24) führt eine neue Programmierfunktion namens Single-Source-RenderScript ein. Dabei werden Kernel nicht über Java, sondern über das Script gestartet, in dem sie definiert sind. Dieser Ansatz ist derzeit auf Mapping-Kernel beschränkt, die in diesem Abschnitt aus Gründen der Übersichtlichkeit einfach als „Kernel“ bezeichnet werden. Mit dieser neuen Funktion können Sie auch Zuweisungen vom Typ
rs_allocation
direkt im Script erstellen. Es ist jetzt möglich, einen ganzen Algorithmus ausschließlich in einem Script zu implementieren, auch wenn mehrere Kernelstarts erforderlich sind.
Der Vorteil ist zweifach: besser lesbarer Code, da die Implementierung eines Algorithmus in einer Sprache erfolgt, und potenziell schnellerer Code, da bei mehreren Kernelstarts weniger Übergänge zwischen Java und RenderScript erforderlich sind.
In Single-Source RenderScript schreiben Sie Kernel wie unter
RenderScript-Kernel schreiben beschrieben. Sie schreiben dann eine aufrufbare Funktion, die
rsForEach()
aufruft, um sie zu starten. Diese API nimmt eine Kernelfunktion als ersten Parameter an, gefolgt von Eingabe- und Ausgabezuweisungen. Eine ähnliche API
rsForEachWithOptions()
nimmt ein zusätzliches Argument vom Typ
rs_script_call_t
an, das eine Teilmenge der Elemente aus den Eingabe- und Ausgabezuweisungen für die zu verarbeitende Kernelfunktion angibt.
Um die RenderScript-Berechnung zu starten, rufen Sie die aufrufbare Funktion aus Java auf.
Folgen Sie der Anleitung unter RenderScript aus Java-Code verwenden.
Rufen Sie im Schritt Die entsprechenden Kernel starten die aufrufbare Funktion mit invoke_function_name()
auf. Dadurch wird die gesamte Berechnung gestartet, einschließlich des Startens der Kernel.
Zuweisungen sind häufig erforderlich, um Zwischenergebnisse von einem Kernelstart an einen anderen zu speichern und weiterzugeben. Sie können sie mit
rsCreateAllocation() erstellen. Eine nutzerfreundliche Form dieser API ist
rsCreateAllocation_<T><W>(…)
, wobei T der Datentyp für ein Element und W die Vektorbreite für das Element ist. Die API akzeptiert die Größen in den Dimensionen X, Y und Z als Argumente. Bei 1D- oder 2D-Zuordnungen kann die Größe für die Dimension Y oder Z weggelassen werden. Mit rsCreateAllocation_uchar4(16384)
wird beispielsweise eine 1D-Zuweisung von 16.384 Elementen erstellt, die alle vom Typ uchar4
sind.
Die Zuweisungen werden automatisch vom System verwaltet. Sie müssen sie nicht explizit freigeben oder freisetzen. Sie können jedoch
rsClearObject(rs_allocation* alloc)
aufrufen, um anzugeben, dass Sie den Handle alloc
für die zugrunde liegende Zuweisung nicht mehr benötigen, damit das System die Ressourcen so früh wie möglich freigeben kann.
Der Abschnitt RenderScript-Kernel schreiben enthält ein Beispiel für einen Kernel, der ein Bild invertiert. Im folgenden Beispiel wird dies erweitert, um mithilfe von Single-Source RenderScript mehr als einen Effekt auf ein Bild anzuwenden. Es enthält einen weiteren Kernel, greyscale
, der ein Farbbild in Schwarz-Weiß umwandelt. Eine aufrufbare Funktion process()
wendet diese beiden Kernel dann nacheinander auf ein Eingabebild an und erzeugt ein Ausgabebild. Zuweisungen für die Eingabe und die Ausgabe werden als Argumente vom Typ
rs_allocation
übergeben.
// File: singlesource.rs #pragma version(1) #pragma rs java_package_name(com.android.rssample) static const float4 weight = {0.299f, 0.587f, 0.114f, 0.0f}; uchar4 RS_KERNEL invert(uchar4 in, uint32_t x, uint32_t y) { uchar4 out = in; out.r = 255 - in.r; out.g = 255 - in.g; out.b = 255 - in.b; return out; } uchar4 RS_KERNEL greyscale(uchar4 in) { const float4 inF = rsUnpackColor8888(in); const float4 outF = (float4){ dot(inF, weight) }; return rsPackColorTo8888(outF); } void process(rs_allocation inputImage, rs_allocation outputImage) { const uint32_t imageWidth = rsAllocationGetDimX(inputImage); const uint32_t imageHeight = rsAllocationGetDimY(inputImage); rs_allocation tmp = rsCreateAllocation_uchar4(imageWidth, imageHeight); rsForEach(invert, inputImage, tmp); rsForEach(greyscale, tmp, outputImage); }
So rufen Sie die Funktion process()
in Java oder Kotlin auf:
Kotlin
val RS: RenderScript = RenderScript.create(context) val script = ScriptC_singlesource(RS) val inputAllocation: Allocation = Allocation.createFromBitmapResource( RS, resources, R.drawable.image ) val outputAllocation: Allocation = Allocation.createTyped( RS, inputAllocation.type, Allocation.USAGE_SCRIPT or Allocation.USAGE_IO_OUTPUT ) script.invoke_process(inputAllocation, outputAllocation)
Java
// File SingleSource.java RenderScript RS = RenderScript.create(context); ScriptC_singlesource script = new ScriptC_singlesource(RS); Allocation inputAllocation = Allocation.createFromBitmapResource( RS, getResources(), R.drawable.image); Allocation outputAllocation = Allocation.createTyped( RS, inputAllocation.getType(), Allocation.USAGE_SCRIPT | Allocation.USAGE_IO_OUTPUT); script.invoke_process(inputAllocation, outputAllocation);
In diesem Beispiel wird gezeigt, wie ein Algorithmus mit zwei Kernelausführungen vollständig in der RenderScript-Sprache implementiert werden kann. Ohne Single-Source-RenderScript müssten Sie beide Kernel aus dem Java-Code starten, was die Kernelstarts von den Kerneldefinitionen trennt und das Verständnis des gesamten Algorithmus erschwert. Der Single-Source-RenderScript-Code ist nicht nur leichter zu lesen, sondern es entfällt auch die Umstellung zwischen Java und dem Script bei Kernelstarts. Einige iterative Algorithmen können Kernel hunderte Male starten, was den Overhead solcher Übergänge erheblich erhöht.
Script-Globale Variablen
Eine globale Variable ist eine gewöhnliche globale Variable, die nicht static
ist, in einer Scriptdatei (.rs
). Für eine globale Variable namens var, die in der Datei filename.rs
definiert ist, gibt es eine Methode get_var
, die in der Klasse ScriptC_filename
enthalten ist. Sofern das globale Element nicht const
ist, gibt es auch eine Methode set_var
.
Ein bestimmtes Script-Global hat zwei separate Werte: einen Java-Wert und einen Script-Wert. Diese Werte haben folgende Auswirkungen:
- Wenn var im Script einen statischen Initialisierer hat, wird der Anfangswert von var sowohl in Java als auch im Script festgelegt. Andernfalls ist der Anfangswert null.
- Zugriffe auf var im Script lesen und schreiben den Scriptwert.
- Die Methode
get_var
liest den Java-Wert. - Die Methode
set_var
(falls vorhanden) schreibt den Java-Wert sofort und den Scriptwert asynchron.
HINWEIS:Das bedeutet, dass Werte, die innerhalb eines Scripts in ein globales Element geschrieben werden, mit Ausnahme von statischen Initialisierern im Script für Java nicht sichtbar sind.
Reduzierungskerne im Detail
Bei der Reduzierung werden mehrere Daten zu einem einzelnen Wert kombiniert. Dies ist eine nützliche Primitive in der parallelen Programmierung, z. B. für folgende Anwendungen:
- die Summe oder das Produkt aller Daten berechnen
- Logische Vorgänge (
and
,or
,xor
) für alle Daten berechnen - den Mindest- oder Maximalwert in den Daten ermitteln
- nach einem bestimmten Wert oder nach der Koordinate eines bestimmten Werts in den Daten suchen
Unter Android 7.0 (API-Ebene 24) und höher unterstützt RenderScript Reduktionskerne, um effiziente von Nutzern geschriebene Reduktionsalgorithmen zu ermöglichen. Sie können Reduzierungskerne auf Eingaben mit 1, 2 oder 3 Dimensionen starten.
Das obige Beispiel zeigt einen einfachen addint-Reduktionskernel.
Hier ist ein etwas komplizierterer findMinAndMax-Reduktionskern, der die Positionen der minimalen und maximalen long
-Werte in einem eindimensionalen Allocation
ermittelt:
#define LONG_MAX (long)((1UL << 63) - 1) #define LONG_MIN (long)(1UL << 63) #pragma rs reduce(findMinAndMax) \ initializer(fMMInit) accumulator(fMMAccumulator) \ combiner(fMMCombiner) outconverter(fMMOutConverter) // Either a value and the location where it was found, or INITVAL. typedef struct { long val; int idx; // -1 indicates INITVAL } IndexedVal; typedef struct { IndexedVal min, max; } MinAndMax; // In discussion below, this initial value { { LONG_MAX, -1 }, { LONG_MIN, -1 } } // is called INITVAL. static void fMMInit(MinAndMax *accum) { accum->min.val = LONG_MAX; accum->min.idx = -1; accum->max.val = LONG_MIN; accum->max.idx = -1; } //---------------------------------------------------------------------- // In describing the behavior of the accumulator and combiner functions, // it is helpful to describe hypothetical functions // IndexedVal min(IndexedVal a, IndexedVal b) // IndexedVal max(IndexedVal a, IndexedVal b) // MinAndMax minmax(MinAndMax a, MinAndMax b) // MinAndMax minmax(MinAndMax accum, IndexedVal val) // // The effect of // IndexedVal min(IndexedVal a, IndexedVal b) // is to return the IndexedVal from among the two arguments // whose val is lesser, except that when an IndexedVal // has a negative index, that IndexedVal is never less than // any other IndexedVal; therefore, if exactly one of the // two arguments has a negative index, the min is the other // argument. Like ordinary arithmetic min and max, this function // is commutative and associative; that is, // // min(A, B) == min(B, A) // commutative // min(A, min(B, C)) == min((A, B), C) // associative // // The effect of // IndexedVal max(IndexedVal a, IndexedVal b) // is analogous (greater . . . never greater than). // // Then there is // // MinAndMax minmax(MinAndMax a, MinAndMax b) { // return MinAndMax(min(a.min, b.min), max(a.max, b.max)); // } // // Like ordinary arithmetic min and max, the above function // is commutative and associative; that is: // // minmax(A, B) == minmax(B, A) // commutative // minmax(A, minmax(B, C)) == minmax((A, B), C) // associative // // Finally define // // MinAndMax minmax(MinAndMax accum, IndexedVal val) { // return minmax(accum, MinAndMax(val, val)); // } //---------------------------------------------------------------------- // This function can be explained as doing: // *accum = minmax(*accum, IndexedVal(in, x)) // // This function simply computes minimum and maximum values as if // INITVAL.min were greater than any other minimum value and // INITVAL.max were less than any other maximum value. Note that if // *accum is INITVAL, then this function sets // *accum = IndexedVal(in, x) // // After this function is called, both accum->min.idx and accum->max.idx // will have nonnegative values: // - x is always nonnegative, so if this function ever sets one of the // idx fields, it will set it to a nonnegative value // - if one of the idx fields is negative, then the corresponding // val field must be LONG_MAX or LONG_MIN, so the function will always // set both the val and idx fields static void fMMAccumulator(MinAndMax *accum, long in, int x) { IndexedVal me; me.val = in; me.idx = x; if (me.val <= accum->min.val) accum->min = me; if (me.val >= accum->max.val) accum->max = me; } // This function can be explained as doing: // *accum = minmax(*accum, *val) // // This function simply computes minimum and maximum values as if // INITVAL.min were greater than any other minimum value and // INITVAL.max were less than any other maximum value. Note that if // one of the two accumulator data items is INITVAL, then this // function sets *accum to the other one. static void fMMCombiner(MinAndMax *accum, const MinAndMax *val) { if ((accum->min.idx < 0) || (val->min.val < accum->min.val)) accum->min = val->min; if ((accum->max.idx < 0) || (val->max.val > accum->max.val)) accum->max = val->max; } static void fMMOutConverter(int2 *result, const MinAndMax *val) { result->x = val->min.idx; result->y = val->max.idx; }
HINWEIS:Weitere Beispielkerne für die Reduktion finden Sie hier.
Um einen Reduktionskernel auszuführen, erstellt die RenderScript-Laufzeit eine oder mehrere Variablen, die als Akkumulatordatenelemente bezeichnet werden und den Status des Reduktionsprozesses speichern. Die RenderScript-Laufzeit wählt die Anzahl der Akkumulatordatenelemente so aus, dass die Leistung maximiert wird. Der Typ der Akkumulatordatenelemente (accumType) wird durch die Akkumulatorfunktion des Kernels bestimmt. Das erste Argument dieser Funktion ist ein Verweis auf ein Akkumulatordatenelement. Standardmäßig wird jedes Akkumulatordatenelement auf null initialisiert (wie bei memset
). Sie können jedoch eine Initialisierungsfunktion schreiben, um etwas anderes zu tun.
Beispiel:Im Kernel addint werden die Akkumulator-Datenelemente (vom Typ int
) verwendet, um Eingabewerte zu addieren. Da es keine Initialisierungsfunktion gibt, wird jedes Akkumulatordatenelement auf null initialisiert.
Beispiel:Im Kernel findMinAndMax werden die Akkumulatordatenelemente (vom Typ MinAndMax
) verwendet, um die bisher gefundenen Minimal- und Maximalwerte im Blick zu behalten. Es gibt eine Initialisierungsfunktion, mit der diese Werte auf LONG_MAX
bzw. LONG_MIN
festgelegt und die Positionen dieser Werte auf -1 gesetzt werden können, was bedeutet, dass die Werte nicht im (leeren) Teil der verarbeiteten Eingabe vorhanden sind.
RenderScript ruft Ihre Akkumulatorfunktion einmal für jede Koordinate in den Eingaben auf. Normalerweise sollte Ihre Funktion das Akkumulatordatenelement auf irgendeine Weise entsprechend der Eingabe aktualisieren.
Beispiel:Im Kernel addint fügt die Akkumulatorfunktion dem Akkumulatordatenelement den Wert eines Eingabeelements hinzu.
Beispiel:Im findMinAndMax-Kernel prüft die Akkumulatorfunktion, ob der Wert eines Eingabeelements kleiner oder gleich dem im Akkumulatordatenelement aufgezeichneten Mindestwert und/oder größer oder gleich dem im Akkumulatordatenelement aufgezeichneten Maximalwert ist. Anschließend aktualisiert sie das Akkumulatordatenelement entsprechend.
Nachdem die Akkumulatorfunktion einmal für jede Koordinate in den Eingaben aufgerufen wurde, muss RenderScript die Akkumulatordatenelemente zusammenführen, um ein einzelnes Akkumulatordatenelement zu erhalten. Dazu können Sie eine Kombinatorische Funktion schreiben. Wenn die Akkumulatorfunktion nur eine Eingabe und keine besonderen Argumente hat, müssen Sie keine Kombinationsfunktion schreiben. RenderScript verwendet die Akkumulatorfunktion, um die Akkumulatordatenelemente zu kombinieren. Sie können aber trotzdem eine Kombinationsfunktion schreiben, wenn Sie dieses Standardverhalten nicht wünschen.
Beispiel:Im Kernel addint gibt es keine Kombinatorfunktion. Daher wird die Akkumulatorfunktion verwendet. Das ist das richtige Verhalten, denn wenn wir eine Sammlung von Werten in zwei Teile aufteilen und die Werte in diesen beiden Teilen separat addieren, entspricht die Summe dieser beiden Summen der Summe der gesamten Sammlung.
Beispiel:Im findMinAndMax-Kernel prüft die Kombinationsfunktion, ob der im Akkumulatordatenelement „Quelle“ *val
aufgezeichnete Mindestwert kleiner als der im Akkumulatordatenelement „Ziel“ *accum
aufgezeichnete Mindestwert ist. *accum
wird entsprechend aktualisiert. Ähnlich funktioniert es für den Höchstwert. Dadurch wird *accum
auf den Zustand aktualisiert, den es hätte, wenn alle Eingabewerte in *accum
und nicht teilweise in *accum
und teilweise in *val
zusammengefasst worden wären.
Nachdem alle Akkumulatordatenelemente kombiniert wurden, ermittelt RenderScript das Ergebnis der Reduzierung, das an Java zurückgegeben wird. Dazu können Sie eine Out-Converter-Funktion schreiben. Sie müssen keine Ausgabekonvertierungsfunktion schreiben, wenn der Endwert der kombinierten Akkumulatordatenelemente das Ergebnis der Reduzierung sein soll.
Beispiel:Im Kernel addint gibt es keine Outconverter-Funktion. Der endgültige Wert der kombinierten Datenelemente ist die Summe aller Elemente der Eingabe. Dies ist der Wert, den wir zurückgeben möchten.
Beispiel:Im findMinAndMax-Kernel initialisiert die Outconverter-Funktion einen int2
-Ergebniswert, um die Positionen des Minimum- und des Maximumwerts zu speichern, die sich aus der Kombination aller Akkumulatordatenelemente ergeben.
Einen Reduktionskernel schreiben
#pragma rs reduce
definiert einen Reduktionskern, indem der Name und die Namen und Rollen der Funktionen angegeben werden, aus denen der Kern besteht. Alle diese Funktionen müssen static
sein. Für einen Reduktionskernel ist immer eine accumulator
-Funktion erforderlich. Je nach gewünschter Funktion können Sie einige oder alle anderen Funktionen weglassen.
#pragma rs reduce(kernelName) \ initializer(initializerName) \ accumulator(accumulatorName) \ combiner(combinerName) \ outconverter(outconverterName)
Die Bedeutung der Elemente in #pragma
ist folgende:
reduce(kernelName)
(erforderlich): Gibt an, dass ein Reduktionskern definiert wird. Eine reflektierte Java-Methodereduce_kernelName
startet den Kernel.initializer(initializerName)
(optional): Gibt den Namen der Initialisierungsfunktion für diesen Reduktionskern an. Wenn Sie den Kernel starten, ruft RenderScript diese Funktion einmal für jedes Akkumulatordatenelement auf. Die Funktion muss so definiert sein:static void initializerName(accumType *accum) { … }
accum
ist ein Verweis auf ein Akkumulatordatenelement, das von dieser Funktion initialisiert werden soll.Wenn Sie keine Initialisierungsfunktion angeben, initialisiert RenderScript jedes Akkumulatordatenelement auf null (wie mit
memset
), als gäbe es eine Initialisierungsfunktion, die so aussieht:static void initializerName(accumType *accum) { memset(accum, 0, sizeof(*accum)); }
accumulator(accumulatorName)
(erforderlich): Gibt den Namen der Akkumulatorfunktion für diesen Reduktionskernel an. Wenn Sie den Kernel starten, ruft RenderScript diese Funktion einmal für jede Koordinate in den Eingaben auf, um ein Akkumulatordatenelement auf eine bestimmte Weise entsprechend den Eingaben zu aktualisieren. Die Funktion muss so definiert sein:static void accumulatorName(accumType *accum, in1Type in1, …, inNType inN [, specialArguments]) { … }
accum
ist ein Verweis auf ein Akkumulatordatenelement, das von dieser Funktion geändert werden soll.in1
bisinN
sind ein oder mehrere Argumente, die automatisch basierend auf den Eingaben ausgefüllt werden, die an den Kernelstart übergeben werden, ein Argument pro Eingabe. Die Akkumulatorfunktion kann optional eines der besonderen Argumente annehmen.Ein Beispiel für einen Kernel mit mehreren Eingaben ist
dotProduct
.combiner(combinerName)
(Optional): Gibt den Namen der Kombinationsfunktion für diesen Reduktionskern an. Nachdem RenderScript die Akkumulatorfunktion einmal für jede Koordinate in den Eingaben aufgerufen hat, ruft es diese Funktion so oft wie nötig auf, um alle Akkumulatordatenelemente in einem einzigen Akkumulatordatenelement zu kombinieren. Die Funktion muss so definiert sein:
static void combinerName(accumType *accum, const accumType *other) { … }
accum
ist ein Verweis auf ein Akkumulator-Datenelement vom Typ „Ziel“, das von dieser Funktion geändert werden soll.other
ist ein Verweis auf ein Akkumulator-Datenelement vom Typ „Quelle“, das von dieser Funktion mit*accum
„kombiniert“ werden soll.HINWEIS:Es ist möglich, dass
*accum
,*other
oder beide initialisiert, aber nie an die Akkumulatorfunktion übergeben wurden. Das bedeutet, dass eine oder beide Variablen nie anhand von Eingabedaten aktualisiert wurden. Im Kernel findMinAndMax wird beispielsweise in der KombinationsfunktionfMMCombiner
explizit nachidx < 0
gesucht, da dies auf ein solches Akkumulatordatenelement mit dem Wert INITVAL hinweist.Wenn Sie keine Combiner-Funktion angeben, verwendet RenderScript stattdessen die Akkumulatorfunktion. Das Verhalten entspricht dann dem einer Combiner-Funktion, die so aussieht:
static void combinerName(accumType *accum, const accumType *other) { accumulatorName(accum, *other); }
Eine Kombinationsfunktion ist erforderlich, wenn der Kernel mehr als eine Eingabe hat, wenn der Datentyp der Eingabe nicht mit dem Datentyp des Akkumulators übereinstimmt oder wenn die Akkumulatorfunktion ein oder mehrere besondere Argumente annimmt.
outconverter(outconverterName)
(optional): Gibt den Namen der Ausgabekonvertierungsfunktion für diesen Reduktionskern an. Nachdem RenderScript alle Akkumulatordatenelemente kombiniert hat, ruft es diese Funktion auf, um das Ergebnis der Reduzierung zu ermitteln, das an Java zurückgegeben wird. Die Funktion muss so definiert sein:static void outconverterName(resultType *result, const accumType *accum) { … }
result
ist ein Verweis auf ein Ergebnisdatenelement, das von der RenderScript-Laufzeit zugewiesen, aber nicht initialisiert wurde. Diese Funktion initialisiert es mit dem Ergebnis der Reduzierung. resultType ist der Typ dieses Datenelements, der nicht mit accumType übereinstimmen muss.accum
ist ein Verweis auf das endgültige Akkumulatordatenelement, das von der Kombinatorfunktion berechnet wird.Wenn Sie keine Outconverter-Funktion angeben, kopiert RenderScript das endgültige Akkumulatordatenelement in das Ergebnisdatenelement. Es verhält sich so, als gäbe es eine Outconverter-Funktion mit folgendem Code:
static void outconverterName(accumType *result, const accumType *accum) { *result = *accum; }
Wenn Sie einen anderen Ergebnistyp als den Accumulator-Datentyp benötigen, ist die Outconverter-Funktion obligatorisch.
Ein Kernel hat Eingabetypen, einen Akkumulatordatenelementtyp und einen Ergebnistyp, die nicht unbedingt identisch sein müssen. Im Kernel findMinAndMax sind beispielsweise der Eingabetyp long
, der Typ des Akkumulatordatenelements MinAndMax
und der Ergebnistyp int2
unterschiedlich.
Was können Sie nicht annehmen?
Sie dürfen sich nicht auf die Anzahl der Akkumulatordatenelemente verlassen, die von RenderScript für einen bestimmten Kernelstart erstellt wurden. Es gibt keine Garantie dafür, dass bei zwei Ausführungen desselben Kernels mit denselben Eingaben dieselbe Anzahl von Akkumulatordatenelementen erstellt wird.
Sie dürfen sich nicht auf die Reihenfolge verlassen, in der RenderScript die Initialisierer-, Akkumulator- und Kombinatorfunktionen aufruft. Es kann sogar sein, dass einige davon parallel aufgerufen werden. Es gibt keine Garantie dafür, dass zwei Starts desselben Kernels mit derselben Eingabe derselben Reihenfolge folgen. Die einzige Garantie besteht darin, dass nur die Initialisierungsfunktion ein nicht initialisiertes Akkumulator-Datenelement sieht. Beispiel:
- Es kann nicht garantiert werden, dass alle Akkumulatordatenelemente vor dem Aufruf der Akkumulatorfunktion initialisiert werden. Sie wird jedoch nur für ein initialisiertes Akkumulatordatenelement aufgerufen.
- Die Reihenfolge, in der Eingabeelemente an die Akkumulatorfunktion übergeben werden, kann nicht garantiert werden.
- Es gibt keine Garantie dafür, dass die Akkumulatorfunktion für alle Eingabeelemente aufgerufen wurde, bevor die Kombinatorfunktion aufgerufen wird.
Eine Folge davon ist, dass der findMinAndMax-Kernel nicht deterministisch ist: Wenn die Eingabe mehr als ein Vorkommen desselben Mindest- oder Höchstwerts enthält, können Sie nicht wissen, welches Vorkommen der Kernel findet.
Was müssen Sie garantieren?
Da das RenderScript-System einen Kernel auf viele verschiedene Arten ausführen kann, müssen Sie bestimmte Regeln einhalten, damit sich der Kernel wie gewünscht verhält. Wenn Sie diese Regeln nicht einhalten, erhalten Sie möglicherweise falsche Ergebnisse, nicht deterministisches Verhalten oder Laufzeitfehler.
In den folgenden Regeln wird häufig angegeben, dass zwei Akkumulatordatenelemente denselben Wert haben müssen. Was bedeutet das? Das hängt davon ab, was Sie mit dem Kernel tun möchten. Bei einer mathematischen Reduzierung wie addint ist es in der Regel sinnvoll, wenn „das Gleiche“ mathematische Gleichheit bedeutet. Bei einer „beliebigen“ Suche wie findMinAndMax („Speicherort des minimalen und maximalen Eingabewerts ermitteln“), bei der es mehrere Vorkommen identischer Eingabewerte geben kann, müssen alle Speicherorte eines bestimmten Eingabewerts als „identisch“ betrachtet werden. Sie könnten einen ähnlichen Kernel schreiben, um den linksesten Mindest- und Höchstwert der Eingabewerte zu ermitteln, wobei ein Mindestwert an Position 100 beispielsweise einem identischen Mindestwert an Position 200 vorgezogen wird. Bei diesem Kernel würde „identisch“ den identischen Speicherort und nicht nur den identischen Wert bedeuten. Die Akkumulator- und Kombinatorfunktionen müssten sich von denen für findMinAndMax unterscheiden.
Die Initialisierfunktion muss einen Identitätswert erstellen. Wenn alsoI
und A
Akkumulatordatenelemente sind, die von der Initialisierungsfunktion initialisiert wurden, und I
nie an die Akkumulatorfunktion übergeben wurde (A
aber möglicherweise), gilt:
combinerName(&A, &I)
mussA
unverändert lassencombinerName(&I, &A)
mussI
unverändert lassen wieA
Beispiel:Im Kernel addint wird ein Akkumulatordatenelement auf Null initialisiert. Die Kombinationsfunktion für diesen Kernel führt eine Addition durch. Null ist der Identitätswert für die Addition.
Beispiel:Im Kernel findMinAndMax wird ein Akkumulatordatenelement mit INITVAL
initialisiert.
fMMCombiner(&A, &I)
lässtA
unverändert, daI
INITVAL
ist.fMMCombiner(&I, &A)
legtI
aufA
fest, daI
INITVAL
ist.
Daher ist INITVAL
tatsächlich ein Identitätswert.
Die Kombinationsfunktion muss kommutativ sein. Wenn A
und B
Akkumulatordatenelemente sind, die von der Initialisierfunktion initialisiert wurden und die der Akkumulatorfunktion möglicherweise null oder mehrmals übergeben wurden, muss combinerName(&A, &B)
A
auf denselben Wert setzen, auf den combinerName(&B, &A)
B
setzt.
Beispiel:Im Kernel addint addiert die Kombinationsfunktion die beiden Werte der Akkumulatordatenelemente. Die Addition ist kommutativ.
Beispiel:Im Kernel findMinAndMax ist fMMCombiner(&A, &B)
mit A = minmax(A, B)
identisch und da minmax
kommutativ ist, gilt das auch für fMMCombiner
.
Die Kombinationsfunktion muss assoziativ sein. Wenn also A
, B
und C
Akkumulatordatenelemente sind, die von der Initialisierungsfunktion initialisiert wurden und die der Akkumulatorfunktion null oder mehrmals übergeben wurden, müssen die folgenden beiden Codefolgen A
auf denselben Wert setzen:
combinerName(&A, &B); combinerName(&A, &C);
combinerName(&B, &C); combinerName(&A, &B);
Beispiel:Im Kernel addint addiert die Kombinationsfunktion die beiden Werte des Akkumulatordatenelements:
A = A + B A = A + C // Same as // A = (A + B) + C
B = B + C A = A + B // Same as // A = A + (B + C) // B = B + C
Die Addition ist assoziativ, ebenso wie die Kombinationsfunktion.
Beispiel:Im Kernel findMinAndMax ist
fMMCombiner(&A, &B)gleich
A = minmax(A, B). Die beiden Sequenzen sind also
A = minmax(A, B) A = minmax(A, C) // Same as // A = minmax(minmax(A, B), C)
B = minmax(B, C) A = minmax(A, B) // Same as // A = minmax(A, minmax(B, C)) // B = minmax(B, C)
minmax
ist assoziativ, ebenso fMMCombiner
.
Die Akkumulatorfunktion und die Kombinationsfunktion müssen zusammen der grundlegenden Faltregel entsprechen. Wenn also A
und B
Akkumulatordatenelemente sind, A
von der Initialisierungsfunktion initialisiert wurde und der Akkumulatorfunktion null oder mehrmals übergeben wurde, B
nicht initialisiert wurde und args die Liste der Eingabeargumente und speziellen Argumente für einen bestimmten Aufruf der Akkumulatorfunktion ist, müssen die folgenden beiden Codefolgen A
auf denselben Wert setzen:
accumulatorName(&A, args); // statement 1
initializerName(&B); // statement 2 accumulatorName(&B, args); // statement 3 combinerName(&A, &B); // statement 4
Beispiel:Im Kernel addint für den Eingabewert V:
- Ausdruck 1 ist mit
A += V
identisch. - Anweisung 2 ist mit
B = 0
identisch. - Ausdruck 3 ist mit
B += V
identisch, was auch fürB = V
gilt. - Aussage 4 ist mit
A += B
identisch, was wiederum mitA += V
identisch ist.
In den Anweisungen 1 und 4 wird A
auf denselben Wert gesetzt. Daher entspricht dieser Kernel der grundlegenden Faltregel.
Beispiel:Im findMinAndMax-Kernel für den Eingabewert V an der Koordinate X:
- Ausdruck 1 ist mit
A = minmax(A, IndexedVal(V, X))
identisch. - Anweisung 2 ist mit
B = INITVAL
identisch. - Aussage 3 ist dasselbe wie
B = minmax(B, IndexedVal(V, X))
, was, da B der Anfangswert ist, dasselbe ist wieB = IndexedVal(V, X)
. - Ausdruck 4 ist mit
A = minmax(A, B)
identisch, was wiederum mitA = minmax(A, IndexedVal(V, X))
übereinstimmt.
In den Anweisungen 1 und 4 wird A
auf denselben Wert gesetzt. Daher entspricht dieser Kernel der grundlegenden Faltregel.
Einen Reduktionskern aus Java-Code aufrufen
Für einen Reduktionskern namens kernelName, der in der Datei filename.rs
definiert ist, sind in der Klasse ScriptC_filename
drei Methoden enthalten:
Kotlin
// Function 1 fun reduce_kernelName(ain1: Allocation, …, ainN: Allocation): javaFutureType // Function 2 fun reduce_kernelName(ain1: Allocation, …, ainN: Allocation, sc: Script.LaunchOptions): javaFutureType // Function 3 fun reduce_kernelName(in1: Array<devecSiIn1Type>, …, inN: Array<devecSiInNType>): javaFutureType
Java
// Method 1 public javaFutureType reduce_kernelName(Allocation ain1, …, Allocation ainN); // Method 2 public javaFutureType reduce_kernelName(Allocation ain1, …, Allocation ainN, Script.LaunchOptions sc); // Method 3 public javaFutureType reduce_kernelName(devecSiIn1Type[] in1, …, devecSiInNType[] inN);
Hier sind einige Beispiele für den Aufruf des Kernels addint:
Kotlin
val script = ScriptC_example(renderScript) // 1D array // and obtain answer immediately val input1 = intArrayOf(…) val sum1: Int = script.reduce_addint(input1).get() // Method 3 // 2D allocation // and do some additional work before obtaining answer val typeBuilder = Type.Builder(RS, Element.I32(RS)).apply { setX(…) setY(…) } val input2: Allocation = Allocation.createTyped(RS, typeBuilder.create()).also { populateSomehow(it) // fill in input Allocation with data } val result2: ScriptC_example.result_int = script.reduce_addint(input2) // Method 1 doSomeAdditionalWork() // might run at same time as reduction val sum2: Int = result2.get()
Java
ScriptC_example script = new ScriptC_example(renderScript); // 1D array // and obtain answer immediately int input1[] = …; int sum1 = script.reduce_addint(input1).get(); // Method 3 // 2D allocation // and do some additional work before obtaining answer Type.Builder typeBuilder = new Type.Builder(RS, Element.I32(RS)); typeBuilder.setX(…); typeBuilder.setY(…); Allocation input2 = createTyped(RS, typeBuilder.create()); populateSomehow(input2); // fill in input Allocation with data ScriptC_example.result_int result2 = script.reduce_addint(input2); // Method 1 doSomeAdditionalWork(); // might run at same time as reduction int sum2 = result2.get();
Methode 1 hat 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, löst RenderScript eine Ausnahme aus. Der Kernel wird für jede Koordinate in diesen Dimensionen ausgeführt.
Methode 2 entspricht Methode 1, mit der Ausnahme, dass Methode 2 ein zusätzliches Argument sc
annimmt, mit dem die Kernelausführung auf eine Teilmenge der Koordinaten beschränkt werden kann.
Methode 3 entspricht Methode 1, mit der Ausnahme, dass anstelle von Zuweisungseingaben Java-Array-Eingabewerte verwendet werden. So müssen Sie keinen Code schreiben, um eine Zuweisung explizit zu erstellen und Daten aus einem Java-Array darauf zu kopieren. Die Verwendung von Methode 3 anstelle von Methode 1 erhöht jedoch nicht die Leistung des Codes. Bei Methode 3 wird für jedes Eingabearray eine temporäre eindimensionale Zuweisung mit dem entsprechenden Element
-Typ und aktivierter setAutoPadding(boolean)
erstellt. Das Array wird dann in die Zuweisung kopiert, als wäre dies mit der entsprechenden copyFrom()
-Methode von Allocation
geschehen. Anschließend wird Methode 1 aufgerufen und die temporären Zuweisungen werden übergeben.
HINWEIS:Wenn Ihre Anwendung mehrere Kernelaufrufe mit demselben Array oder mit verschiedenen Arrays mit denselben Dimensionen und demselben Elementtyp ausführt, können Sie die Leistung verbessern, indem Sie Allokationen explizit erstellen, befüllen und wiederverwenden, anstatt Methode 3 zu verwenden.
javaFutureType, der Rückgabetyp der reflektierten Reduzierungsmethoden, ist eine reflektierte statische verschachtelte Klasse innerhalb der Klasse ScriptC_filename
. Sie gibt das zukünftige Ergebnis eines reduzierten Kernellaufs an. Um das tatsächliche Ergebnis der Ausführung zu erhalten, rufen Sie die get()
-Methode dieser Klasse auf. Sie gibt einen Wert vom Typ javaResultType zurück. get()
ist synchron.
Kotlin
class ScriptC_filename(rs: RenderScript) : ScriptC(…) { object javaFutureType { fun get(): javaResultType { … } } }
Java
public class ScriptC_filename extends ScriptC { public static class javaFutureType { public javaResultType get() { … } } }
javaResultType wird aus dem resultType der outconverter-Funktion ermittelt. Sofern resultType kein signierter Typ (Skalar, Vektor oder Array) ist, ist javaResultType der direkt entsprechende Java-Typ. Wenn resultType ein ungezeichenter Typ ist und es einen größeren signierten Java-Typ gibt, ist javaResultType dieser größere signierte Java-Typ. Andernfalls ist es der direkt entsprechende Java-Typ. Beispiel:
- Wenn resultType
int
,int2
oderint[15]
ist, ist javaResultTypeint
,Int2
oderint[]
. Alle Werte von resultType können durch javaResultType dargestellt werden. - Wenn resultType
uint
,uint2
oderuint[15]
ist, 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[]
. Es gibt bestimmte Werte für resultType, die nicht durch javaResultType dargestellt werden können.
javaFutureType ist der zukünftige Ergebnistyp, der dem resultType der outconverter-Funktion entspricht.
- Wenn resultType kein Arraytyp ist, ist javaFutureType
result_resultType
. - Wenn resultType ein Array mit der Länge Count mit Elementen vom Typ memberType ist, ist javaFutureType
resultArrayCount_memberType
.
Beispiel:
Kotlin
class ScriptC_filename(rs: RenderScript) : ScriptC(…) { // for kernels with int result object result_int { fun get(): Int = … } // for kernels with int[10] result object resultArray10_int { fun get(): IntArray = … } // for kernels with int2 result // note that the Kotlin type name "Int2" is not the same as the script type name "int2" object result_int2 { fun get(): Int2 = … } // for kernels with int2[10] result // note that the Kotlin type name "Int2" is not the same as the script type name "int2" object resultArray10_int2 { fun get(): Array<Int2> = … } // for kernels with uint result // note that the Kotlin type "long" is a wider signed type than the unsigned script type "uint" object result_uint { fun get(): Long = … } // for kernels with uint[10] result // note that the Kotlin type "long" is a wider signed type than the unsigned script type "uint" object resultArray10_uint { fun get(): LongArray = … } // for kernels with uint2 result // note that the Kotlin type "Long2" is a wider signed type than the unsigned script type "uint2" object result_uint2 { fun get(): Long2 = … } // for kernels with uint2[10] result // note that the Kotlin type "Long2" is a wider signed type than the unsigned script type "uint2" object resultArray10_uint2 { fun get(): Array<Long2> = … } }
Java
public class ScriptC_filename extends ScriptC { // for kernels with int result public static class result_int { public int get() { … } } // for kernels with int[10] result public static class resultArray10_int { public int[] get() { … } } // for kernels with int2 result // note that the Java type name "Int2" is not the same as the script type name "int2" public static class result_int2 { public Int2 get() { … } } // for kernels with int2[10] result // note that the Java type name "Int2" is not the same as the script type name "int2" public static class resultArray10_int2 { public Int2[] get() { … } } // for kernels with uint result // note that the Java type "long" is a wider signed type than the unsigned script type "uint" public static class result_uint { public long get() { … } } // for kernels with uint[10] result // note that the Java type "long" is a wider signed type than the unsigned script type "uint" public static class resultArray10_uint { public long[] get() { … } } // for kernels with uint2 result // note that the Java type "Long2" is a wider signed type than the unsigned script type "uint2" public static class result_uint2 { public Long2 get() { … } } // for kernels with uint2[10] result // note that the Java type "Long2" is a wider signed type than the unsigned script type "uint2" public static class resultArray10_uint2 { public Long2[] get() { … } } }
Wenn javaResultType ein Objekttyp ist (einschließlich eines Arraytyps), wird bei jedem Aufruf von javaFutureType.get()
für dieselbe Instanz dasselbe Objekt zurückgegeben.
Wenn javaResultType nicht alle Werte vom Typ resultType darstellen kann und ein Reduktionskern einen nicht darstellbaren Wert erzeugt, löst javaFutureType.get()
eine Ausnahme aus.
Methode 3 und devecSiInXType
devecSiInXType ist der Java-Typ, der dem inXType des entsprechenden Arguments der Akkumulatorfunktion entspricht. Sofern inXType kein signaturloser Typ oder ein Vektortyp ist, ist devecSiInXType der direkt entsprechende Java-Typ. Wenn inXType ein ungezeichenter Skalartyp ist, ist devecSiInXType der Java-Typ, der direkt dem signierten Skalartyp derselben Größe entspricht. Wenn inXType ein signierter Vektortyp ist, ist devecSiInXType der Java-Typ, der direkt dem Vektorkomponententyp entspricht. Wenn inXType ein ungezeichner Vektortyp ist, ist devecSiInXType der Java-Typ, der direkt dem signierten Skalartyp mit derselben Größe wie der Vektorkomponententyp entspricht. Beispiel:
- Wenn inXType
int
ist, ist devecSiInXTypeint
. - Wenn inXType
int2
ist, ist devecSiInXTypeint
. Das Array ist eine flachte Darstellung: Es enthält doppelt so viele skaläre Elemente wie die Zuweisung Vektor-Elemente mit zwei Komponenten. Das funktioniert genauso wie bei dencopyFrom()
-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 bei dencopyFrom()
-Methoden vonAllocation
. - Wenn inXType
uint2
ist, ist deviceSiInXTypeint
. Dies ist eine Kombination aus der Verarbeitung vonint2
unduint
: Das Array ist eine flache Darstellung und signierte Werte von Java-Arrays werden als nicht signierte Elementwerte von RenderScript interpretiert.
Bei Methode 3 werden Eingabetypen anders behandelt als Ergebnistypen:
- Die Vektoreingabe eines Scripts wird auf der Java-Seite flachgelegt, das Vektorergebnis eines Scripts jedoch nicht.
- Die ungesignierte Eingabe eines Scripts wird auf der Java-Seite als signierte Eingabe derselben Größe dargestellt, während das ungesignierte Ergebnis eines Scripts auf der Java-Seite als erweiterter signierter Typ dargestellt wird (außer bei
ulong
).
Weitere Beispiele für Reduktionskerne
#pragma rs reduce(dotProduct) \ accumulator(dotProductAccum) combiner(dotProductSum) // Note: No initializer function -- therefore, // each accumulator data item is implicitly initialized to 0.0f. static void dotProductAccum(float *accum, float in1, float in2) { *accum += in1*in2; } // combiner function static void dotProductSum(float *accum, const float *val) { *accum += *val; }
// Find a zero Element in a 2D allocation; return (-1, -1) if none #pragma rs reduce(fz2) \ initializer(fz2Init) \ accumulator(fz2Accum) combiner(fz2Combine) static void fz2Init(int2 *accum) { accum->x = accum->y = -1; } static void fz2Accum(int2 *accum, int inVal, int x /* special arg */, int y /* special arg */) { if (inVal==0) { accum->x = x; accum->y = y; } } static void fz2Combine(int2 *accum, const int2 *accum2) { if (accum2->x >= 0) *accum = *accum2; }
// Note that this kernel returns an array to Java #pragma rs reduce(histogram) \ accumulator(hsgAccum) combiner(hsgCombine) #define BUCKETS 256 typedef uint32_t Histogram[BUCKETS]; // Note: No initializer function -- // therefore, each bucket is implicitly initialized to 0. static void hsgAccum(Histogram *h, uchar in) { ++(*h)[in]; } static void hsgCombine(Histogram *accum, const Histogram *addend) { for (int i = 0; i < BUCKETS; ++i) (*accum)[i] += (*addend)[i]; } // Determines the mode (most frequently occurring value), and returns // the value and the frequency. // // If multiple values have the same highest frequency, returns the lowest // of those values. // // Shares functions with the histogram reduction kernel. #pragma rs reduce(mode) \ accumulator(hsgAccum) combiner(hsgCombine) \ outconverter(modeOutConvert) static void modeOutConvert(int2 *result, const Histogram *h) { uint32_t mode = 0; for (int i = 1; i < BUCKETS; ++i) if ((*h)[i] > (*h)[mode]) mode = i; result->x = mode; result->y = (*h)[mode]; }
Weitere Codebeispiele
Die Beispiele BasicRenderScript, RenderScriptIntrinsic und Hello Compute veranschaulichen die Verwendung der auf dieser Seite beschriebenen APIs.