RenderScript to framework do wykonywania wymagających pod względem obliczeniowym zadań na Androidzie z wysoką wydajnością. RenderScript jest przeznaczony głównie do obliczeń równoległych, ale może też przynieść korzyści w przypadku obciążeń sekwencyjnych. Środowisko uruchomieniowe RenderScript równolegle wykonuje zadania na różnych procesorach dostępnych na urządzeniu, takich jak procesory wielordzeniowe i procesory graficzne. Dzięki temu możesz skupić się na wyrażaniu algorytmów, a nie na planowaniu pracy. RenderScript jest szczególnie przydatny w przypadku aplikacji do przetwarzania obrazów, fotografii obliczeniowej lub rozpoznawania obrazów.
Przed rozpoczęciem korzystania z języka RenderScript musisz pamiętać o 2 głównych koncepcjach:
- Język jest językiem opartym na C99, przeznaczonym do pisania wydajnego kodu obliczeniowego. W artykule Tworzenie rdzeni RenderScript opisano, jak używać tego narzędzia do tworzenia rdzeni obliczeniowych.
- Interfejs control API służy do zarządzania cyklem życia zasobów RenderScript i kontrolowania wykonywania jądra. Jest on dostępny w 3 językach: Javie, C++ w Android NDK i języku jądra wyprowadzonym z C99. Używanie RenderScript z kodu Java i RenderScript z jedno źródłowym kodem źródłowym opisują odpowiednio pierwszą i trzecią opcję.
Tworzenie jądra RenderScript
Kernel RenderScript znajduje się zwykle w pliku .rs
w katalogu <project_root>/src/rs
. Każdy plik .rs
jest nazywany skryptem. Każdy skrypt zawiera własny zestaw jąder, funkcji i zmiennych. Skrypt może zawierać:
- Deklaracja pragmy (
#pragma version(1)
) deklarująca wersję języka jądra RenderScriptu używanego w tym skrypcie. Obecnie jedyną prawidłową wartością jest 1. - Deklaracja pragmy (
#pragma rs java_package_name(com.example.app)
), która deklaruje nazwę pakietu klas Javy odzwierciedlonych przez ten skrypt. Pamiętaj, że plik.rs
musi być częścią pakietu aplikacji, a nie projektu biblioteki. - 0 lub więcej wywoływalnych funkcji. Funkcja wywoływalna to jednowątkowa funkcja RenderScript, którą można wywołać z kodu Java z dowolnymi argumentami. Często przydają się one podczas wstępnej konfiguracji lub obliczeń szeregowych w większym potoku przetwarzania.
0 lub więcej globalnych skryptów. Skrypt globalny jest podobny do zmiennej globalnej w C. Możesz uzyskać dostęp do zmiennych globalnych skryptu z kodu Java. Są one często używane do przekazywania parametrów do jądra RenderScript. Więcej informacji o zmiennych globalnych skryptu znajdziesz tutaj.
Zero lub więcej jąder obliczeniowych. Jądro obliczeniowe to funkcja lub zbiór funkcji, które możesz skierować środowisko wykonawcze RenderScript do działania równolegle w zbiorze danych. Istnieją 2 rodzaje procesorów: mapowania (nazywanych też foreach) oraz redukcji.
Kernel mapowania to funkcja równoległa, która działa na zbiorze
Allocations
o tych samych wymiarach. Domyślnie jest wykonywane raz dla każdej współrzędnej w tych wymiarach. Zwykle (ale nie tylko) służy do przekształcania zbioru danych wejściowychAllocations
w dane wyjścioweAllocation
w czasieElement
.Oto przykład prostego rdzenia mapowania:
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; }
W większości aspektów jest to identyczne z standardową funkcją C. Właściwość
RS_KERNEL
zastosowana do prototypu funkcji określa, że funkcja jest jądrem mapowania RenderScript, a nie wywoływalna funkcja. Argumentin
jest wypełniany automatycznie na podstawie danych wejściowychAllocation
przekazanych do uruchomienia jądra. Argumentyx
iy
są omówione poniżej. Wartość zwracana przez jądro jest automatycznie zapisywana w odpowiednim miejscu w danych wyjściowychAllocation
. Domyślnie to jądro działa na całej ścieżce wejściowejAllocation
, z jednym wykonaniem funkcji jądra naElement
wAllocation
.Kernel mapowania może mieć co najmniej 1 wejścia
Allocations
, 1 wyjścieAllocation
lub oba te elementy. RenderScript sprawdza w czasie wykonywania, czy wszystkie wejścia i wyjścia Allocations mają te same wymiary oraz czy typyElement
wejścia i wyjścia Allocations pasują do prototypu jądra. Jeśli któryś z tych sprawdzeń się nie powiedzie, RenderScript zgłasza wyjątek.UWAGA: w wersjach Androida 6.0 (poziom interfejsu API 23) i starszych kernel mapowania nie może mieć więcej niż 1 wejścia
Allocation
.Jeśli potrzebujesz więcej danych wejściowych lub wyjściowych
Allocations
niż ma jądro, te obiekty powinny być powiązane z globalnymi zmiennymi skrypturs_allocation
i dostępne z jądra lub funkcji wywoływalnej za pomocąrsGetElementAt_type()
lubrsSetElementAt_type()
.UWAGA:
RS_KERNEL
to makro zdefiniowane automatycznie przez RenderScript dla Twojej wygody:#define RS_KERNEL __attribute__((kernel))
Kernel redukcji to rodzina funkcji, które działają na zbiorze danych wejściowych
Allocations
o tych samych wymiarach. Domyślnie jej funkcja zasobnika jest wykonywana raz dla każdej współrzędnej w tych wymiarach. Zwykle (ale nie tylko) służy do „zredukowania” zbioru danych wejściowychAllocations
do pojedynczej wartości.Oto przykład prostego rdzenia redukcji, które zlicza
Elements
z wejścia:#pragma rs reduce(addint) accumulator(addintAccum) static void addintAccum(int *accum, int val) { *accum += val; }
Kernel funkcji redukcji składa się z co najmniej 1 funkcji napisanej przez użytkownika. Funkcja
#pragma rs reduce
służy do definiowania rdzenia przez podanie jego nazwy (w tym przykładzie jest toaddint
) oraz nazw i ról funkcji, z których składa się rdzeń (w tym przykładzie jest to funkcjaaccumulator
addintAccum
). Wszystkie takie funkcje muszą byćstatic
. Kernel redukcji zawsze wymaga funkcjiaccumulator
; może też zawierać inne funkcje, w zależności od tego, co ma on robić.Funkcja kumulacji jądra redukcji musi zwracać
void
i mieć co najmniej 2 argumenty. Pierwszy argument (w tym przykładzieaccum
) to wskaźnik do elementu danych licznika, a drugi (w tym przykładzieval
) jest wypełniany automatycznie na podstawie danych wejściowychAllocation
przekazanych do uruchamiania jądra. Element danych licznika jest tworzony przez środowisko wykonawcze RenderScript; domyślnie jest inicjowany jako 0. Domyślnie to jądro działa na wszystkich danych wejściowychAllocation
, z jednym wykonaniem funkcji akumulatora naElement
wAllocation
. Domyślnie końcowa wartość elementu danych zbiornika jest traktowana jako wynik redukcji i zwracana do Javy. Runtime RenderScript sprawdza, czy typElement
wejściowego obiektu Allocation pasuje do prototypu funkcji kumulatora. Jeśli tak nie jest, RenderScript zgłasza wyjątek.Jądro redukcji ma co najmniej 1 wejście
Allocations
, ale nie ma danych wyjściowychAllocations
.Więcej informacji o kernelach redukcji znajdziesz tutaj.
Kernele redukcji są obsługiwane w Androidzie 7.0 (poziom interfejsu API 24) i nowszych.
Funkcja jądra mapowania lub funkcja redukcji jądra akumulatora może uzyskiwać dostęp do współrzędnych bieżącego wykonania za pomocą argumentów specjalnych
x
,y
iz
, które muszą być typuint
lubuint32_t
. Te argumenty są opcjonalne.Funkcja jądra mapowania lub funkcja redukcji jądra akumulatora może też przyjmować opcjonalny argument specjalny
context
typu rs_kernel_context. Jest on potrzebny rodzinie interfejsów API czasu wykonywania, które służą do wysyłania zapytań o określone właściwości bieżącego wykonania, np. rsGetDimX. (Argumentcontext
jest dostępny w Androidzie 6.0 (poziom interfejsu API 23) i nowszych.- Opcjonalna funkcja
init()
. Funkcjainit()
to specjalny typ wywoływanej funkcji, którą RenderScript wykonuje podczas tworzenia pierwszego wystąpienia skryptu. Dzięki temu niektóre obliczenia mogą być wykonywane automatycznie podczas tworzenia skryptu. - 0 lub więcej statycznych zmiennych i funkcji globalnych skryptu. Statyczny skrypt globalny jest taki sam jak skrypt globalny, z tym że nie można uzyskać do niego dostępu z poziomu kodu Java. Funkcja statyczna to standardowa funkcja C, którą można wywołać z dowolnego jądra lub wywoływalnej funkcji w skrypcie, ale która nie jest udostępniana interfejsowi Java API. Jeśli skrypt globalny lub funkcja nie wymagają dostępu z poziomu kodu Java, zdecydowanie zalecamy ich zadeklarowanie
static
.
Ustawianie dokładności obliczeń zmiennoprzecinkowych
Możesz kontrolować wymagany poziom precyzji obliczeń zmiennoprzecinkowych w skrypcie. Jest to przydatne, jeśli nie jest wymagany pełny standard IEEE 754-2008 (używany domyślnie). Te flagi mogą ustawiać inny poziom dokładności zmiennoprzecinkowej:
#pragma rs_fp_full
(domyślnie, jeśli nic nie zostało określone): dla aplikacji, które wymagają precyzji zmiennoprzecinkowej zgodnie ze standardem IEEE 754-2008.#pragma rs_fp_relaxed
: dla aplikacji, które nie wymagają ścisłej zgodności z IEEE 754–2008 i mogą tolerować mniejszą precyzję. Ten tryb umożliwia wyrównywanie do zera w przypadku denormacji i zaokrąglania do zera.#pragma rs_fp_imprecise
: dla aplikacji, które nie mają rygorystycznych wymagań dotyczących dokładności. Ten tryb włącza wszystkie opcje wrs_fp_relaxed
, a także:- Operacje, które zwracają -0,0, mogą zamiast tego zwracać +0.0.
- Operacje na INF i NAN są nieokreślone.
Większość aplikacji może korzystać z rs_fp_relaxed
bez żadnych skutków ubocznych. Może to być bardzo korzystne w przypadku niektórych architektur ze względu na dodatkowe optymalizacje dostępne tylko w przypadku z obniżoną precyzją (np. instrukcje procesora SIMD).
Dostęp do interfejsów RenderScript API z poziomu Javy
Podczas tworzenia aplikacji na Androida, która korzysta z RenderScript, możesz uzyskać dostęp do interfejsu API z poziomu Javy na 1 z 2 sposobów:
android.renderscript
– interfejsy API w tym pakiecie klasy są dostępne na urządzeniach z Androidem 3.0 (poziom interfejsu API 11) lub nowszym.android.support.v8.renderscript
– interfejsy API z tego pakietu są dostępne w bibliotece pomocy, co umożliwia ich używanie na urządzeniach z Androidem 2.3 (poziom interfejsu API 9) lub nowszym.
Oto kompromisy:
- Jeśli używasz interfejsów API biblioteki obsługi, część RenderScript aplikacji będzie zgodna z urządzeniami z Androidem 2.3 (poziom interfejsu API 9) lub nowszym, niezależnie od tego, których funkcji RenderScript używasz. Dzięki temu aplikacja będzie działać na większej liczbie urządzeń niż w przypadku korzystania z natywnego interfejsu API (
android.renderscript
). - Niektóre funkcje RenderScript są niedostępne w interfejsach API biblioteki Support Library.
- Jeśli używasz interfejsów API biblioteki pomocy, otrzymasz (prawdopodobnie znacznie) większe pliki APK niż w przypadku używania natywnych interfejsów API (
android.renderscript
).
Korzystanie z interfejsów API biblioteki pomocy RenderScript
Aby korzystać z interfejsów API RenderScript w bibliotece Support, musisz skonfigurować środowisko programistyczne, aby uzyskać do nich dostęp. Do korzystania z tych interfejsów API wymagane są te narzędzia pakietu Android SDK:
- Android SDK Tools w wersji 22.2 lub nowszej
- Android SDK Build-tools w wersji 18.1.0 lub nowszej
Pamiętaj, że od wersji pakietu Android SDK Build-tools 24.0.0 nie obsługujemy już Androida 2.2 (poziom interfejsu API 8).
Zainstalowaną wersję tych narzędzi możesz sprawdzić i zaktualizować w Menedżerze SDK na Androida.
Aby używać interfejsów API RenderScript w bibliotece Support Library:
- Sprawdź, czy masz zainstalowaną wymaganą wersję pakietu SDK Androida.
- Zaktualizuj ustawienia procesu kompilacji Androida, dodając ustawienia RenderScript:
- Otwórz plik
build.gradle
w folderze aplikacji w module aplikacji. - Dodaj do pliku te ustawienia RenderScript:
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 } }
Ustawienia wymienione powyżej kontrolują określone zachowania w procesie kompilacji Androida:
renderscriptTargetApi
– określa wersję kodu bajtowego, która ma zostać wygenerowana. Zalecamy ustawienie tej wartości na najniższy poziom interfejsu API, który zapewnia wszystkie używane przez Ciebie funkcje, oraz ustawienie wartościrenderscriptSupportModeEnabled
natrue
. Dozwolone wartości tego ustawienia to dowolna wartość całkowita od 11 do najnowszej wersji interfejsu API. Jeśli minimalna wersja pakietu SDK określona w pliku manifestu aplikacji jest inna, ta wartość jest ignorowana, a do ustawienia minimalnej wersji pakietu SDK używana jest docelowa wartość w pliku kompilacji.renderscriptSupportModeEnabled
– określa, że wygenerowany kod bajtowy powinien zostać zastąpiony zgodną wersją, jeśli urządzenie, na którym jest uruchomiony, nie obsługuje wersji docelowej.
- Otwórz plik
- W klasach aplikacji, które korzystają z RenderScriptu, dodaj import dla klas biblioteki pomocy:
Kotlin
import android.support.v8.renderscript.*
Java
import android.support.v8.renderscript.*;
Używanie RenderScript z Java lub kodu Kotlin
Korzystanie z RenderScript z kodu Java lub Kotlin wymaga korzystania z klas interfejsu API znajdujących się w pakiecie android.renderscript
lub android.support.v8.renderscript
. Większość aplikacji stosuje ten sam podstawowy schemat użytkowania:
- Inicjalizowanie kontekstu RenderScript. Kontekst
RenderScript
utworzony za pomocą funkcjicreate(Context)
zapewnia możliwość korzystania z RenderScript i zawiera obiekt, który umożliwia kontrolowanie czasu trwania wszystkich kolejnych obiektów RenderScript. Tworzenie kontekstu należy traktować jako potencjalnie długotrwałą operację, ponieważ może ono tworzyć zasoby na różnych elementach sprzętowych. Nie powinno się znajdować się na ścieżce krytycznej aplikacji, jeśli jest to możliwe. Zazwyczaj aplikacja ma tylko 1 kontekst RenderScript naraz. - Utwórz co najmniej 1 element
Allocation
, który zostanie przekazany do skryptu. ObiektAllocation
to obiekt RenderScript, który zapewnia przechowywanie stałej ilości danych. Kernely w skryptach przyjmują obiektyAllocation
jako dane wejściowe i wyjściowe, a w kernelach można uzyskać dostęp do obiektówAllocation
za pomocą zmiennychrsGetElementAt_type()
irsSetElementAt_type()
, gdy są one powiązane jako zmienne globalne skryptu. ObiektyAllocation
umożliwiają przekazywanie tablic z kodu Java do kodu RenderScript i odwrotnie. ObiektyAllocation
są zwykle tworzone za pomocą funkcjicreateTyped()
lubcreateFromBitmap()
. - Utwórz potrzebne skrypty. W przypadku RenderScript dostępne są 2 typy skryptów:
- ScriptC: to skrypty zdefiniowane przez użytkownika, jak opisano powyżej w sekcji Tworzenie skryptu RenderScript Kernel. Każdy skrypt ma klasę Java, która jest odzwierciedlana przez kompilator RenderScript, aby ułatwić dostęp do skryptu z kodu Java. Ta klasa ma nazwę
ScriptC_filename
. Jeśli na przykład kernel mapowania z powyższego przykładu znajduje się w plikuinvert.rs
, a kontekst RenderScript jest już w plikumRenderScript
, kod Java lub Kotlin służący do utworzenia instancji skryptu będzie wyglądał tak:Kotlin
val invert = ScriptC_invert(renderScript)
Java
ScriptC_invert invert = new ScriptC_invert(renderScript);
- ScriptIntrinsic: to wbudowane w RenderScript jądra do wykonywania typowych operacji, takich jak rozmycie gaussowskie, sprzężenie i blendowanie obrazów. Więcej informacji znajdziesz w sekcji z podklasami
ScriptIntrinsic
.
- ScriptC: to skrypty zdefiniowane przez użytkownika, jak opisano powyżej w sekcji Tworzenie skryptu RenderScript Kernel. Każdy skrypt ma klasę Java, która jest odzwierciedlana przez kompilator RenderScript, aby ułatwić dostęp do skryptu z kodu Java. Ta klasa ma nazwę
- Wypełnij sekcję „Podział” danymi. Z wyjątkiem alokacji utworzonych za pomocą funkcji
createFromBitmap()
, alokacja jest wypełniana pustymi danymi w momencie jej utworzenia. Aby wypełnić alokację, użyj jednej z metod „kopiowania” wAllocation
. Metody „kopiowania” są synchroniczne. - Ustaw wszystkie niezbędne globalne zmienne skryptu. Możesz ustawiać zmienne globalne za pomocą metod w tej samej klasie
ScriptC_filename
o nazwieset_globalname
. Aby np. ustawić zmiennąint
o nazwiethreshold
, użyj metody Javaset_threshold(int)
, a aby ustawić zmiennąrs_allocation
o nazwielookup
, użyj metodyset_lookup(Allocation)
w Javie. Metodyset
są asynchroniczne. - Uruchom odpowiednie jądra i funkcje wywoływane.
Metody uruchamiania danego jądra są odzwierciedlone w tej samej klasie
ScriptC_filename
z metodami o nazwachforEach_mappingKernelName()
lubreduce_reductionKernelName()
. Zmiany te są asynchroniczne. W zależności od argumentów funkcji jądra metoda ta przyjmuje co najmniej 1 rozpód, który musi mieć te same wymiary. Domyślnie jądro wykonuje działanie nad każdą współrzędną w tych wymiarach. Aby uruchomić jądro nad podzbiorem tych współrzędnych, przekaż w metodzieforEach
lubreduce
odpowiedni argumentScript.LaunchOptions
.Uruchamianie funkcji wywoływalnych za pomocą metod
invoke_functionName
, które są odzwierciedlone w tej samej klasieScriptC_filename
. Te uruchomienia są asynchroniczne. - Pobieraj dane z obiektów
Allocation
i javaFutureType. Aby uzyskać dostęp do danych zAllocation
w kodzie Java, musisz skopiować te dane z powrotem do Javy za pomocą jednej z metod „copy” wAllocation
. Aby uzyskać wynik z kernela redukcji, musisz użyć metodyjavaFutureType.get()
. Metody „copy” iget()
są synchroniczne. - Zniszcz kontekst RenderScript. Możesz zniszczyć kontekst RenderScript za pomocą funkcji
destroy()
lub zezwalając na usunięcie obiektu kontekstu RenderScript przez mechanizm garbage collection. Powoduje to, że każde dalsze użycie dowolnego obiektu należącego do tego kontekstu spowoduje wyjątek.
Asynchroniczny model wykonywania
Odzwierciedlone metody forEach
, invoke
, reduce
i set
są asynchroniczne – każda z nich może wrócić do Javy przed wykonaniem żądanego działania. Poszczególne działania są jednak serializowane w kolejności, w jakiej zostały uruchomione.
Klasa Allocation
udostępnia metody „copy” służące do kopiowania danych do i z przydziału. Metoda „copy” jest synchroniczna i serializowana z uwzględnieniem wszystkich wymienionych powyżej działań asynchronicznych, które dotyczą tej samej alokacji.
Odzwierciedlone klasy javaFutureType udostępniają metodę get()
, która umożliwia uzyskanie wyniku redukcji. get()
jest synchroniczny i jest serializowany w stosunku do redukcji (która jest asynchroniczna).
Single-Source RenderScript
Android 7.0 (interfejs API 24) wprowadza nową funkcję programowania o nazwie RenderScript z jednym źródłem kodu, w której rdzenie są uruchamiane ze skryptu, w którym są zdefiniowane, a nie z języka Java. Obecnie to podejście jest ograniczone do mapowania jąder, które w celu zachowania zwięzłości w tej sekcji nazywamy po prostu „jądrami”. Ta nowa funkcja umożliwia też tworzenie alokacji typu
rs_allocation
z poziomu skryptu. Teraz można zaimplementować cały algorytm tylko w skrypcie, nawet jeśli wymaga to uruchomienia wielu jąder.
Korzyści są 2 korzyści: bardziej czytelny kod, ponieważ pozwala na implementację algorytmu w jednym języku, i potencjalnie szybszy kod dzięki mniejszej liczbie przejść między Javą a RenderScriptem podczas korzystania z wielu jądra systemu.
W przypadku RenderScript jednoźródłowego kody jądra piszesz zgodnie z opisem w artykule
Tworzenie kodu jądra RenderScript. Następnie tworzysz niemożliwą funkcję, która wywołuje metodę
rsForEach()
, aby ją uruchomić. Ten interfejs API przyjmuje jako pierwszy parametr funkcję jądra, a potem przypisuje dane wejściowe i wyjściowe. Podobny interfejs API
rsForEachWithOptions()
przyjmuje dodatkowy argument typu
rs_script_call_t
, który określa podzbiór elementów z alokacji danych wejściowych i wyjściowych, które ma przetworzyć funkcja jądra.
Aby rozpocząć obliczenia w języku RenderScript, wywołaj funkcję niedostępną w języku Java.
Wykonaj czynności opisane w artykule Używanie kodu RenderScript z poziomu kodu Java.
W kroku uruchom odpowiednie jądra wywołaj funkcję wywoływaną przez invoke_function_name()
, co spowoduje uruchomienie wszystkich obliczeń, w tym uruchomienia jądra.
Alokacje są często potrzebne do zapisywania i przekazywania pośrednich wyników z jednego uruchomienia jądra do drugiego. Możesz je utworzyć za pomocą funkcji
rsCreateAllocation(). Jedną z prostych w użyciu postaci tego interfejsu API jest
rsCreateAllocation_<T><W>(…)
, gdzie T to typ danych elementu, a W to szerokość wektora elementu. Interfejs API przyjmuje jako argumenty rozmiary w wymiarach X, Y i Z. W przypadku alokacji 1D lub 2D można pominąć rozmiar wymiaru Y lub Z. Na przykład rsCreateAllocation_uchar4(16384)
tworzy alokację 1D z 16 384 elementami, z których każdy ma typ uchar4
.
Systemem automatycznie zarządza alokacją. Nie musisz ich wyraźnie zwalniać ani uwalniać. Możesz jednak wywołać funkcję
rsClearObject(rs_allocation* alloc)
, aby wskazać, że nie potrzebujesz już uchwytu
alloc
do powiązanej alokacji,
aby system mógł jak najszybciej zwolnić zasoby.
Sekcja Zapisywanie jądra RenderScriptu zawiera przykładowe jądro, które odwraca obraz. Przykład poniżej rozszerza to o możliwość zastosowania do obrazu więcej niż 1 efektu za pomocą RenderScript z jednego źródła. Zawiera ono inny rdzeń, greyscale
, który zamienia obraz kolorowy w czarno-biały. Funkcja wywoływalna process()
stosuje następnie te 2 jądra kolejno do obrazu wejściowego i generuje obraz wyjściowy. Przydziały danych wejściowych i wyjściowych są przekazywane jako argumenty typu
rs_allocation
.
// 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); }
Funkcję process()
możesz wywołać z Java lub Kotlin w ten sposób:
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);
Ten przykład pokazuje, jak algorytm, który wymaga uruchomienia 2 jądra, można wdrożyć całkowicie w języku RenderScript. Bez funkcji jednoźródłowego RenderScript musiałbyś uruchamiać oba rdzenie z kodu Javy, oddzielając ich uruchamianie od definicji rdzeni, co utrudniałoby zrozumienie całego algorytmu. Nie tylko jest łatwiejszy do odczytania kod RenderScript z jednego źródła, lecz także eliminuje przenoszenie między Javą a skryptem podczas uruchamiania jądra systemu. Niektóre algorytmy iteracyjne mogą uruchamiać jądra setki razy, co powoduje znaczne obciążenie podczas takich przejść.
Parametry globalne skryptu
Zmienna globalna skryptu to zwykła zmienna globalna inna niż static
w pliku skryptu (.rs
). W przypadku zmiennej skryptu o nazwie var zdefiniowanej w pliku filename.rs
będzie dostępna metoda get_var
w klasie ScriptC_filename
. Jeśli wartość globalna nie jest const
, dostępna będzie też metoda set_var
.
Podany globalny skrypt ma 2 osobne wartości: wartość Java i wartość skryptu. Te wartości działają w ten sposób:
- Jeśli var ma statyczny inicjalizator w skrypcie, określa on początkową wartość var zarówno w języku Java, jak i w tym skrypcie. W przeciwnym razie wartość początkowa jest równa 0.
- Uzyskuje dostęp do zmiennej var w ramach skryptu z możliwością odczytu i zapisu jego wartości.
- Metoda
get_var
odczytuje wartość Java. - Metoda
set_var
(jeśli istnieje) natychmiast zapisuje wartość w Javie i zapisuje wartość skryptu asynchronicznie.
UWAGA: oznacza to, że z wyjątkiem statycznych inicjalizowanych w skrypcie wartości zapisanych w zmiennych globalnych w skrypcie nie są one widoczne dla Javy.
Szczegółowe informacje o rdzeniu funkcji redukcji
Redukowanie to proces łączenia zbioru danych w jedną wartość. Jest to przydatna funkcja w programowaniu równoległym, która ma zastosowanie w takich sytuacjach:
- obliczanie sumy lub iloczynu wszystkich danych;
- wykonywania operacji logicznych (
and
,or
,xor
) na wszystkich danych. - znajdowanie minimalnej lub maksymalnej wartości w danych;
- podczas wyszukiwania konkretnej wartości lub współrzędnych konkretnej wartości w danych
W Androidzie 7.0 (poziom interfejsu API 24) i nowszych RenderScript obsługuje jądra redukcji, aby umożliwić tworzenie wydajnych algorytmów redukcji przez użytkowników. Kernely redukcji możesz uruchamiać na danych wejściowych o 1, 2 lub 3 wymiarach.
Powyższy przykład przedstawia proste jądro redukcji addint.
Oto bardziej skomplikowany kernel redukcji findMinAndMax, który znajduje położenie minimalnej i maksymalnej wartości long
w jednowymiarowym zbiorze danych Allocation
:
#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; }
UWAGA: więcej przykładów funkcji redukcji jąder tutaj.
Aby uruchomić jądra systemu redukcji, środowisko wykonawcze RenderScript tworzy co najmniej 1 zmienną nazywane elementami danych zasobnika i przechowuje stan procesu redukcji. Środowisko uruchomieniowe RenderScript wybiera liczbę elementów danych licznika w taki sposób, aby zmaksymalizować wydajność. Typ elementów danych licznika (accumType) jest określany przez funkcję licznika jądra – jej pierwszym argumentem jest wskaźnik do elementu danych licznika. Domyślnie każdy element danych licznika jest inicjowany wartością 0 (jakby była to wartość memset
). Możesz jednak napisać funkcję inicjalizacyjną, aby wykonać inną operację.
Przykład: w jądrze addint elementy danych zbiornika (typu int
) służą do sumowania wartości wejściowych. Nie ma funkcji inicjalizacyjnej, więc każdy element danych licznika jest inicjowany do zera.
Przykład: w rdzeniu findMinAndMax elementy danych zbiornika (typu MinAndMax
) służą do śledzenia znalezionych do tej pory minimalnej i maksymalnej wartości. Funkcja inicjalizacyjna ustawia te wartości odpowiednio na LONG_MAX
i LONG_MIN
oraz na -1, co oznacza, że wartości nie występują w (pustej) części przetworzonego wejścia.
RenderScript wywołuje funkcję kumulacji raz dla każdej współrzędnej w danych wejściowych. Zazwyczaj funkcja powinna w jakiś sposób zaktualizować element danych licznika zgodnie z danymi wejściowymi.
Przykład: w jądrze addint funkcja akumulatora dodaje wartość elementu danych do elementu danych akumulatora.
Przykład: w jądrze findMinAndMax funkcja akumulatora sprawdza, czy wartość elementu wejściowego jest mniejsza czy równa minimalnej wartości zarejestrowanej w elemencie danych zasobnika i czy jest od niej większa lub równa maksymalnej wartości zarejestrowanej w elemencie danych zasobnika i odpowiednio aktualizuje element danych akumulatora.
Po tym, jak funkcja zasobnika zostanie wywołana raz dla każdej współrzędnej w danych wejściowych, RenderScript musi połączyć elementy danych zasobnika w jedną pozycję danych zasobnika. W tym celu możesz napisać funkcję łączącą. Jeśli funkcja kumulacji ma 1 argument i nie zawiera argumentów specjalnych, nie musisz pisać funkcji łączącej. RenderScript użyje funkcji kumulacji do złączenia elementów danych kumulacji. (Jeśli chcesz, aby to domyślne działanie nie spełniało Twoich oczekiwań, możesz utworzyć funkcję łączącą).
Przykład: w jądrze addint nie ma funkcji łącznika, więc zostanie użyta funkcja akkumulatora. Jest to prawidłowe działanie, ponieważ jeśli podzielimy zbiór wartości na 2 części, a dodamy wartości z tych 2 części z osobna, zsumowanie tych 2 sum jest równoznaczne z dodaniem całej kolekcji.
Przykład: w jądrze findMinAndMax funkcja łączenia sprawdza, czy minimalna wartość zarejestrowana w elemencie danych zasobnika „źródło” *val
jest mniejsza od minimalnej wartości zarejestrowanej w elemencie danych zasobnika *accum
i odpowiednio aktualizuje *accum
. Podobnie jest w przypadku wartości maksymalnej. W ten sposób *accum
zostanie zaktualizowana do stanu, jaki miałaby, gdyby wszystkie wartości wejściowe zostały zebrane w *accum
, a nie w *accum
i *val
.
Po połączeniu wszystkich elementów danych akumulatora RenderScript określa wynik redukcji, który ma zostać zwrócony do Javy. W tym celu możesz napisać funkcję outconverter. Jeśli chcesz, aby ostateczna wartość elementów danych zbiorczego licznika była wynikiem redukcji, nie musisz pisać funkcji konwertującej dane wyjściowe.
Przykład: w jądrze addint nie ma funkcji outconverter. Końcowa wartość połączonych elementów danych to suma wszystkich elementów wejściowych, czyli wartość, którą chcemy zwrócić.
Przykład: w jądrze findMinAndMax funkcja outconverter inicjalizuje wartość wyniku int2
, aby przechowywać lokalizacje wartości minimalnej i maksymalnej wynikające z połączenia wszystkich elementów danych kumulatora.
Tworzenie jądra redukcji
#pragma rs reduce
definiuje jądro redukcji, podając jego nazwę oraz nazwy i role funkcji, z których się ono składa. Wszystkie takie funkcje muszą być static
. Kernel redukcji zawsze wymaga funkcji accumulator
. W zależności od tego, co ma robić kernel, możesz pominąć niektóre lub wszystkie pozostałe funkcje.
#pragma rs reduce(kernelName) \ initializer(initializerName) \ accumulator(accumulatorName) \ combiner(combinerName) \ outconverter(outconverterName)
Elementy w #pragma
mają następujące znaczenie:
reduce(kernelName)
(wymagany): określa, że definiowana jest funkcja jądra redukcji. Odzwierciedlona metoda Javareduce_kernelName
spowoduje uruchomienie jądra.initializer(initializerName)
(opcjonalnie): określa nazwę funkcji inicjującej tego jądra redukcji. Gdy uruchomisz jądro, RenderScript wywołuje tę funkcję raz dla każdego elementu danych licznika. Funkcja musi być zdefiniowana w ten sposób:static void initializerName(accumType *accum) { … }
accum
to wskaźnik do elementu danych licznika, który ma być zainicjowany przez tę funkcję.Jeśli nie podasz funkcji inicjującej, RenderScript zainicjuje każdy element danych akumulatora do 0 (jakby to funkcja
memset
), działając tak, jakby istniała taka funkcja:static void initializerName(accumType *accum) { memset(accum, 0, sizeof(*accum)); }
accumulator(accumulatorName)
(obowiązkowo): określa nazwę funkcji kumulacji dla tego rdzenia redukcji. Gdy uruchomisz jądro, RenderScript wywołuje tę funkcję raz dla każdej współrzędnej w danych wejściowych, aby w jakiś sposób zaktualizować element danych licznika zgodnie z danymi wejściowymi. Funkcja musi być zdefiniowana w ten sposób:static void accumulatorName(accumType *accum, in1Type in1, …, inNType inN [, specialArguments]) { … }
accum
to wskaźnik do elementu danych licznika, który ma być zmodyfikowany przez tę funkcję.in1
doinN
to co najmniej 1 argument, który jest automatycznie wypełniany na podstawie danych wejściowych przekazanych do uruchamiania jądra, po jednym argumencie na dane wejściowe. Funkcja kumulacji może opcjonalnie przyjmować dowolne argumenty specjalne.Przykładem jądra z większą liczbą danych wejściowych jest
dotProduct
.combiner(combinerName)
(opcjonalnie) określa nazwę funkcji łączenia dla tego rdzenia redukcji. Po wywołaniu przez RenderScript funkcji kumulacji raz dla każdej współrzędnej w danych wejściowych wywołuje ją tyle razy, ile razy jest to konieczne, aby połączyć wszystkie elementy danych kumulacji w jeden element danych kumulacji. Funkcja musi być zdefiniowana w ten sposób:
static void combinerName(accumType *accum, const accumType *other) { … }
accum
to wskaźnik do elementu danych licznika „destination” (miejsce docelowe) w przypadku funkcji, którą chcesz zmodyfikować.other
to wskaźnik do elementu danych akumulatora „source”, który ma być „połączony” z elementem*accum
.UWAGA: możliwe, że
*accum
,*other
lub obie te wartości zostały zainicjowane, ale nigdy nie zostały przekazane do funkcji akumulatora. Oznacza to, że co najmniej jedna z nich nigdy nie została zaktualizowana na podstawie danych wejściowych. Na przykład w jądrze findMinAndMax funkcja łączącafMMCombiner
wyraźnie sprawdzaidx < 0
, ponieważ wskazuje taki element danych akumulatora, którego wartość to INITVAL.Jeśli nie podasz funkcji łączącej, RenderScript użyje funkcji kumulacyjnej, zachowując się tak, jakby istniała funkcja łącząca o tym kształcie:
static void combinerName(accumType *accum, const accumType *other) { accumulatorName(accum, *other); }
Funkcja łącznika jest wymagana, jeśli jądro ma więcej niż 1 wejście, jeśli typ danych wejściowych nie jest taki sam jak typ danych kumulatora lub jeśli funkcja kumulatora przyjmuje co najmniej 1 argument specjalny.
outconverter(outconverterName)
(opcjonalnie): określa nazwę funkcji konwertującej wyjście w przypadku tego rdzenia redukcji. Po złączeniu wszystkich elementów danych w akumulatorze RenderScript wywołuje tę funkcję, aby określić wynik redukcji, który ma zostać zwrócony do Javi. Funkcja musi być zdefiniowana w ten sposób:static void outconverterName(resultType *result, const accumType *accum) { … }
result
to wskaźnik do elementu danych wyniku (przydzielony, ale nie zainicjowany przez środowisko wykonawcze RenderScript), który ma być zainicjowany za pomocą wyniku funkcji redukcji. resultType to typ tego elementu danych, który nie musi być taki sam jak accumType.accum
to wskaźnik do ostatniego elementu danych licznika obliczonego przez funkcję łączącą.Jeśli nie podasz funkcji outconverter, RenderScript skopiuje ostatni element danych gromadzonych przez akumulator do elementu danych wyniku, zachowując się tak, jakby istniała funkcja outconverter o tym kształcie:
static void outconverterName(accumType *result, const accumType *accum) { *result = *accum; }
Jeśli chcesz uzyskać inny typ wyniku niż typ danych zbiornika, funkcja outconverter jest wymagana.
Pamiętaj, że kernel ma typy danych wejściowych, typ elementu danych licznika i typ wyniku. Żaden z nich nie musi być taki sam. Na przykład w jądrze findMinAndMax typ danych wejściowych long
, typ elementu danych licznika MinAndMax
i typ wyniku int2
są różne.
Czego nie można zakładać?
Nie możesz polegać na liczbie elementów danych licznika utworzonych przez RenderScript w ramach danego uruchomienia jądra. Nie ma gwarancji, że 2 uruchomienia tego samego jądra z tymi samymi danymi wejściowymi wygenerują taką samą liczbę elementów danych akumulatora.
Nie należy polegać na kolejności, w jakiej RenderScript wywołuje funkcje inicjatora, zasobnika i kombinatora; niektóre z nich może być nawet wywoływane równolegle. Nie ma gwarancji, że dwa uruchomienia tego samego jądra z tym samym wejściem będą się odbywać w tym samym porządku. Jedyną gwarancją jest to, że tylko funkcja inicjująca zobaczy niezainicjowany element danych akumulatora. Na przykład:
- Nie ma gwarancji, że wszystkie elementy danych kumulacji zostaną zainicjowane przed wywołaniem funkcji kumulacji, ale zostanie ona wywołana tylko w przypadku zainicjowanego elementu danych kumulacji.
- Nie ma gwarancji, że elementy wejściowe są przekazywane do funkcji kumulacji w określonej kolejności.
- Nie ma gwarancji, że funkcja kumulacji została wywołana dla wszystkich elementów wejściowych przed wywołaniem funkcji łączącej.
Jednym z efektów tego jest to, że jądro findMinAndMax nie jest deterministyczne: jeśli dane wejściowe zawierają więcej niż jedno wystąpienie tej samej wartości minimalnej lub maksymalnej, nie można określić, które wystąpienie zostanie znalezione przez jądro.
Co musisz zagwarantować?
System RenderScript może uruchamiać ją na wiele różnych sposobów, dlatego musisz przestrzegać określonych reguł, aby zagwarantować, że jądro działa w oczekiwany sposób. Jeśli nie będziesz przestrzegać tych zasad, możesz uzyskać nieprawidłowe wyniki, niedeterministyczne działanie lub błędy czasu wykonywania.
Reguły poniżej często określają, że 2 elementy danych licznika muszą mieć „tę samą wartość”. Co to oznacza? To zależy od tego, co ma robić jądro. W przypadku uproszczenia matematycznego, takiego jak addint, zwykle sensowne jest, aby „to samo” oznaczało matematyczne równość. W przypadku wyszukiwania typu „wybierz dowolne”, takiego jak findMinAndMax („znajdź lokalizację minimalnej i maksymalnej wartości wejściowych”), w którym może wystąpić więcej niż jedno wystąpienie identycznych wartości wejściowych, wszystkie lokalizacje danej wartości wejściowej muszą być uważane za „te same”. Możesz napisać podobne jądro, aby „znaleźć położenie najbardziej lewego minimum i maksymum wartości wejściowych”, gdzie (np.) preferowana jest minimalna wartość w pozycji 100 zamiast identycznej minimalnej wartości w pozycji 200. W tym przypadku „to samo” oznacza identyczne położenie, a nie tylko identyczną wartość, a funkcje kumulacji i łączenia muszą być inne niż w przypadku funkcji findMinAndMax.
Funkcja inicjująca musi utworzyć wartość tożsamości. Oznacza to, że jeśliI
i A
to elementy danych licznika zainicjalizowane przez funkcję inicjalizacyjną, a wartość I
nigdy nie została przekazana funkcji licznika (ale A
mogła zostać przekazana), to:combinerName(&A, &I)
musi pozostawićA
w tym samym staniecombinerName(&I, &A)
musi pozostawićI
taki sam jakA
Przykład: w jądrze addint element danych licznika jest inicjowany wartością 0. Funkcja łącznika w tym rdzeniu wykonuje dodawanie; zero jest wartością tożsamości dodawania.
Przykład: w jądrze findMinAndMax element danych licznika jest inicjowany wartością INITVAL
.
fMMCombiner(&A, &I)
pozostawiaA
bez zmian, ponieważI
toINITVAL
.fMMCombiner(&I, &A)
ustawiaI
naA
, ponieważI
toINITVAL
.
Dlatego INITVAL
jest wartością tożsamości.
Funkcja łącząca musi być przemienna. Oznacza to, że jeśli A
i B
są elementami danych akumulatora zainicjowanymi przez funkcję inicjatora i które mogły zostać przekazane do funkcji zasobnika co najmniej 0 razy, combinerName(&A, &B)
musi ustawić A
na tę samą wartość, którą combinerName(&B, &A)
ustawia B
.
Przykład: w jądrze addint funkcja combiner dodaje 2 wartości elementów danych z akumulatory; dodawanie jest przechodnie.
Przykład: w jądrze findMinAndMax funkcja fMMCombiner(&A, &B)
jest taka sama jak funkcja A = minmax(A, B)
, a funkcja minmax
jest przemienna, więc funkcja fMMCombiner
też.
Funkcja łączenia musi być asocjacyjna. Oznacza to, że jeśli A
, B
i C
to elementy danych zbiornika zainicjowane przez funkcję inicjalizacyjną i mogą być przekazane do funkcji zbiornika co najmniej raz, to te 2 sekwencje kodu muszą być ustawione na A
ta sama wartość:
combinerName(&A, &B); combinerName(&A, &C);
combinerName(&B, &C); combinerName(&A, &B);
Przykład: w jądrze addint funkcja combiner dodaje 2 wartości elementu danych licznika:
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
Dodawanie jest skojarzone, więc funkcja łączenia też.
Przykład: w kernelu findMinAndMax,
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
jest asocjacyjnym, a także fMMCombiner
.
Funkcja zasobnika i funkcja łączącego muszą być zgodne z podstawową regułą zwijania. Oznacza to, że jeśli A
i B
są elementami danych akumulatora, funkcja A
została zainicjowana przez funkcję inicjatora i mogła zostać przekazana do funkcji akumulatora zero lub więcej razy, B
nie została zainicjowana, a args to lista argumentów wejściowych i specjalnych dla określonego wywołania funkcji zasobnika, to te 2 sekwencje kodu muszą mieć ustawioną tę samą wartość :A
accumulatorName(&A, args); // statement 1
initializerName(&B); // statement 2 accumulatorName(&B, args); // statement 3 combinerName(&A, &B); // statement 4
Przykład: w jądrze addint dla wartości wejściowej V:
- Zdanie 1 jest takie samo jak
A += V
- Stwierdzenie 2 jest takie samo jak
B = 0
- Zdanie 3 jest takie samo jak
B += V
, czyli takie samo jakB = V
- Zdanie 4 jest takie samo jak
A += B
, czyli takie samo jakA += V
W oświadczeniach 1 i 4 parametr A
ma tę samą wartość, więc to jądro stosuje podstawową regułę składania.
Przykład: w kernelu findMinAndMax dla wartości wejściowej V w współrzędnej X:
- Zdanie 1 jest takie samo jak
A = minmax(A, IndexedVal(V, X))
- Stwierdzenie 2 jest takie samo jak
B = INITVAL
- Stwierdzenie 3 jest identyczne z
, która ze względu na to, że B jest wartością początkową, jest taka sama jakB = minmax(B, IndexedVal(V, X))
B = IndexedVal(V, X)
- Zdanie 4 jest takie samo jak
, co jest takie samo jakA = minmax(A, B)
A = minmax(A, IndexedVal(V, X))
W oświadczeniach 1 i 4 parametr A
ma tę samą wartość, więc to jądro stosuje podstawową regułę składania.
Wywoływanie jądra redukcji z kodu Java
W przypadku jądra redukcji o nazwie kernelName zdefiniowanej w pliku filename.rs
istnieją 3 metody w klasie ScriptC_filename
:
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);
Oto kilka przykładów wywołania funkcji 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();
Metoda 1 ma jeden argument wejściowy Allocation
dla każdego argumentu wejściowego w funkcji zasobnika jądra. Runtime RenderScript sprawdza, czy wszystkie wejściowe alokacje mają te same wymiary i czy typ Element
każdego z argumentów wejściowych prototypu funkcji kumulatora jest taki sam. Jeśli którykolwiek z tych testów zakończy się niepowodzeniem, RenderScript wygeneruje wyjątek. Kernel jest wykonywany dla każdej współrzędnej w tych wymiarach.
Metoda 2 jest taka sama jak metoda 1, z tym że przyjmuje dodatkowy argument sc
, który można wykorzystać do ograniczenia działania jądra do podzbioru współrzędnych.
Metoda 3 jest taka sama jak metoda 1, z tym że zamiast danych wejściowych z algorytmu przydziału używa danych wejściowych z tablicy w Javie. Dzięki temu nie musisz pisać kodu, aby wyraźnie utworzyć alokację i przekopiować do niej dane z tablicy Java. Jednak użycie metody 3 zamiast metody 1 nie zwiększa wydajności kodu. W przypadku każdego tablicowego wejścia metoda 3 tworzy tymczasową alokację jednowymiarową z odpowiednim typem Element
i włączoną funkcją setAutoPadding(boolean)
, a następnie kopiuje tablicę do alokacji, tak jakby była używana metoda copyFrom()
funkcji Allocation
. Następnie wywołuje metodę 1, przekazując te tymczasowe alokacje.
UWAGA: jeśli Twoja aplikacja będzie wykonywać wiele wywołań jądra z tym samym tablicą lub z różnymi tablicami o tych samych wymiarach i tym samym typie elementu, możesz zwiększyć wydajność, ręcznie tworząc, wypełniając i ponownie używając alokacji zamiast stosować Metodę 3.
javaFutureType, typ zwracany przez odzwierciedlone metody redukcji, to odzwierciedlona klasa statyczna zagnieżdżona w klasie ScriptC_filename
. Jest to przyszły wynik wykonania funkcji reduce kernel. Aby uzyskać rzeczywisty wynik wykonania, wywołaj metodę get()
tej klasy, która zwraca wartość typu javaResultType. get()
jest synchroniczna.
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 jest określany na podstawie resultType funkcji outconverter. Jeśli resultType nie jest typem bez znaku (skalarne, wektor lub tablica), javaResultType jest bezpośrednio odpowiadającym typem Java. Jeśli resultType jest typem bez znaku, a istnieje większy typ podpisany w języku Java, javaResultType jest tym większym typem podpisanym w języku Java; w przeciwnym razie jest to bezpośrednio odpowiadający typ w języku Java. Na przykład:
- Jeśli resultType to
int
,int2
lubint[15]
, javaResultType toint
,Int2
lubint[]
. Wszystkie wartości parametru resultType mogą być reprezentowane przez javaResultType. - Jeśli resultType ma wartość
uint
,uint2
lubuint[15]
, javaResultType tolong
,Long2
lublong[]
. Wszystkie wartości parametru resultType mogą być reprezentowane przez parametr javaResultType. - Jeśli resultType to
ulong
,ulong2
lubulong[15]
, to javaResultType tolong
,Long2
lublong[]
. Istnieją pewne wartości parametru resultType, których nie można reprezentować przez element javaResultType.
javaFutureType to przyszły typ wyniku odpowiadający parametrowi resultType funkcji outconverter.
- Jeśli resultType nie jest typem tablicy, javaFutureType ma wartość
result_resultType
. - Jeśli resultType jest tablicą o długości Count, która należy do typu memberType, to javaFutureType ma wartość
resultArrayCount_memberType
.
Na przykład:
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() { … } } }
Jeśli javaResultType to typ obiektu (w tym typ tablicy), każde wywołanie javaFutureType.get()
w tym samym wystąpieniu zwróci ten sam obiekt.
Jeśli javaResultType nie może reprezentować wszystkich wartości typu resultType, a jądro funkcji redukcji wygeneruje wartość nie do reprezentowania, javaFutureType.get()
rzuci wyjątek.
Metoda 3 i devecSiInXType
devecSiInXType to typ Java odpowiadający typowi inXType odpowiedniego argumentu funkcji akumulatory. Jeśli inXType nie jest typem bez znaku ani typem wektora, devecSiInXType jest bezpośrednio odpowiadającym typem Java. Jeśli inXType to typ skalar bez znaku, devecSiInXType to typ Java bezpośrednio odpowiadający sygnalizowanemu typowi skalarnemu o tej samej wielkości. Jeśli inXType to podpisany typ wektora, to devecSiInXType to typ Java bezpośrednio odpowiadający typowi elementu wektora. Jeśli inXType to typ wektora bez znaku, devecSiInXType to typ Java bezpośrednio odpowiadający sygnalizowanemu typowi skalarnemu o tej samej wielkości co typ komponentu wektora. Na przykład:
- Jeśli inXType to
int
, to devecSiInXType toint
. - Jeśli inXType to
int2
, to devecSiInXType toint
. Tablica jest spłaszczoną reprezentacją: ma dwa razy więcej elementów skalarnych niż elementów wektora o 2 komponentach. Działa to tak samo jak metodycopyFrom()
w biblioteceAllocation
. - Jeśli inXType ma wartość
uint
, deviceSiInXType ma wartośćint
. Wartość ze znakiem w tablicy Java jest interpretowana jako wartość bez znaku o tym samym wzorze bitowym w alokacji. Działa to tak samo jak w przypadku metodcopyFrom()
w klasieAllocation
. - Jeśli atrybut inXType ma wartość
uint2
, atrybut deviceSiInXType ma wartośćint
. Jest to połączenie sposobu obsługi funkcjiint2
iuint
: tablica jest spłaszczoną reprezentacją, a wartości podpisane tablicy Java są interpretowane jako wartości elementów bez znaku w RenderScript.
Pamiętaj, że w przypadku metody 3 typy danych wejściowych są obsługiwane inaczej niż typy wyników:
- Wektor wejściowy skryptu jest spłaszczony po stronie Java, a wynik wektorowy skryptu nie jest spłaszczony.
- Niepodpisane dane wejściowe skryptu są reprezentowane jako podpisane dane wejściowe o tej samej wielkości po stronie Javy, natomiast niepodpisane dane wyjściowe skryptu są reprezentowane jako rozszerzony podpisany typ po stronie Javy (z wyjątkiem typu
ulong
).
Więcej przykładów funkcji redukcji
#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]; }
Dodatkowe przykłady kodu
Przykłady BasicRenderScript, RenderScriptIntrinsic i Hello Compute pokazują, jak używać interfejsów API opisanych na tej stronie.