RenderScript to framework do wykonywania wymagających pod względem obliczeniowym zadań na urządzeniach z Androidem. 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 się skupić 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.
Aby zacząć pracę z RenderScript, musisz zrozumieć 2 główne zagadnienia:
- 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 czasem trwania zasobów RenderScript i sterowania wykonywaniem 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 pragma (
#pragma version(1)
), która określa wersję języka jądra RenderScript używanego w tym skrypcie. Obecnie jedyną prawidłową wartością jest 1. - deklaracja pragma (
#pragma rs java_package_name(com.example.app)
), która deklaruje nazwę pakietu klas Java odzwierciedlonych w tym skrypcie; 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 są one przydatne do początkowej konfiguracji lub sekwencyjnych obliczeń w ramach większego potoku przetwarzania.
0 lub więcej globalnych zmiennych skryptu. Zmienne skryptu globalnego są podobne do zmiennych globalnych w języku C. Możesz uzyskiwać 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. Rdzenie obliczeniowe to funkcje lub kolekcje funkcji, które możesz kierować w RenderScript Runtime do wykonywania równolegle na zbiorze danych. Istnieją 2 rodzaje procesorów: mapowania (zwane też foreach) i 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 uruchamiania 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 ten rdzeń jest uruchamiany na całym wejściuAllocation
, z jednym wykonaniem funkcji rdzenia 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 Androidzie 6.0 (poziom interfejsu API 23) jądro 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 funkcja kumulacji 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 wartością 0. Domyślnie ten rdzeń jest uruchamiany na całym wejściuAllocation
, 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.Kernel redukcji ma co najmniej 1 wejście
Allocations
, ale nie ma wyjściaAllocations
.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 wersjach).- 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 nie określono inaczej): w przypadku aplikacji, które wymagają dokładności zmiennoprzecinkowej zgodnie ze standardem IEEE 754-2008.#pragma rs_fp_relaxed
: aplikacje, które nie wymagają ścisłego przestrzegania normy IEEE 754-2008 i mogą tolerować mniejszą dokładność. Ten tryb umożliwia wyrównywanie do zera w przypadku denormacji i zaokrąglania do zera.#pragma rs_fp_imprecise
: w przypadku aplikacji, które nie mają ścisłych wymagań dotyczących dokładności. Ten tryb włącza wszystkie opcje wrs_fp_relaxed
, a także:- Operacje, które dają wynik -0,0, mogą zamiast tego zwrócić wartość +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ęść aplikacji RenderScript 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 obsługi.
- Jeśli używasz interfejsów API biblioteki wsparcia, pliki APK będą (być może znacznie) większe niż w przypadku korzystania z natywnych interfejsów API (
android.renderscript
).
Korzystanie z interfejsów API biblioteki obsługi 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 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 pakietu Android SDK.
Aby używać interfejsów API RenderScript w bibliotece Support Library:
- Sprawdź, czy masz zainstalowaną wymaganą wersję pakietu SDK Androida.
- Zaktualizuj ustawienia procesu kompilacji na Androida, aby uwzględnić 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 RenderScript, dodaj import dla klas biblioteki Support:
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. Utwórz kontekstu może być długotrwałą operacją, ponieważ może tworzyć zasoby na różnych urządzeniach. Jeśli to możliwe, nie powinna być częścią ścieżki krytycznej aplikacji. 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 wszystkie niezbędne 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 podklasach
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 „copy” 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
. Na przykład, aby ustawić zmiennąint
o nazwiethreshold
, użyj metody Javaset_threshold(int)
, a aby ustawić zmiennąrs_allocation
o nazwielookup
, użyj metody Javaset_lookup(Allocation)
. 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()
. Te uruchomienia są asynchroniczne. W zależności od argumentów funkcji jądra metoda ta przyjmuje co najmniej 1 przydział, który musi mieć te same wymiary. Domyślnie jądro jest wykonywane na podstawie każdej współrzędnej w tych wymiarach. Aby wykonać jądro na podzbiorze tych współrzędnych, jako ostatni argument metodyforEach
lubreduce
podaj odpowiednieScript.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 redukcyjnego, 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 dalsze używanie dowolnego obiektu należącego do tego kontekstu powoduje wyjątek.
Model wykonania asynchronicznego
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 odniesieniu 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ą podwójne: kod jest czytelniejszy, ponieważ implementacja algorytmu jest zachowana w jednym języku, a potencjalnie szybszy, ponieważ jest mniej przejść między Java a RenderScript w wielu wywołaniach jądra.
W przypadku RenderScript w jednym źródle piszesz jądra zgodnie z opisem w artykule
Tworzenie jądra RenderScript. Następnie piszesz wywoływaną funkcję, która wywołuje je za pomocą funkcji
rsForEach()
. 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 przydziałów danych wejściowych i wyjściowych, które mają być przetworzone przez funkcję jądrową.
Aby rozpocząć obliczenia RenderScript, wywołaj funkcję wywołującą z Java.
Wykonaj czynności opisane w artykule Używanie kodu RenderScript z poziomu kodu Java.
Na etapie uruchamiania odpowiednich jąder wywołaj funkcję wywoływaną za pomocą funkcji invoke_function_name()
, która rozpocznie całą obliczenia, w tym uruchamianie jąder.
Alokacje są często potrzebne do zapisywania i przekazywania pośrednich wyników z jednego uruchomienia jądra do drugiego. Możesz je tworzyć za pomocą funkcji
rsCreateAllocation(). Jedna z łatwych w użyciu form tego interfejsu API to
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ż identyfikatora alloc
dla powiązanej alokacji, aby system mógł jak najszybciej zwolnić zasoby.
Sekcja Tworzenie jądra RenderScript zawiera przykładowy rdzeń, który 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. Kod RenderScript w ramach pojedynczego źródła jest nie tylko łatwiejszy do odczytania, ale też eliminuje konieczność przełączania się między Javą a skryptem podczas uruchamiania jądra. 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 skryptu globalnego o nazwie var zdefiniowanego 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 oddzielne 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.
- Dostęp do zmiennej var w ramach odczytu skryptu i zapisywania jego wartości.
- Metoda
get_var
odczytuje wartość Java. - Metoda
set_var
(jeśli istnieje) zapisuje wartość Java natychmiast, a wartość skryptu asertywnie.
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;
- wykonywanie operacji logicznych (
and
,or
,xor
) na wszystkich danych. - znajdowanie minimalnej lub maksymalnej wartości w danych;
- wyszukiwanie 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, które umożliwiają tworzenie wydajnych algorytmów redukcji przez użytkownika. Kernely redukcji możesz uruchamiać na danych wejściowych o 1, 2 lub 3 wymiarach.
Powyższy przykład pokazuje 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ładowych funkcji redukcji znajdziesz tutaj.
Aby uruchomić jądro redukcji, środowisko wykonawcze RenderScript tworzy co najmniej jedną zmienną o nazwie element danych akumulatora, która przechowuje stan procesu redukcji. Środowisko wykonawcze 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 pierwszy argument to wskaźnik do elementu danych licznika. Domyślnie każdy element danych licznika jest inicjowany wartością 0 (jakby był memset
), ale możesz napisać funkcję inicjującą, aby zrobić coś innego.
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 jądrze 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 kumulacji sprawdza, czy wartość elementu wejściowego jest mniejsza lub równa minimalnej wartości zapisanej w elemencie danych kumulacji lub większa lub równa maksymalnej wartości zapisanej w elemencie danych kumulacji, i odpowiednio aktualizuje ten element.
Po wywołaniu funkcji akumulatora raz dla każdej współrzędnej w danych RenderScript musi połączyć elementy danych akumulatora w jeden element danych akumulatora. W tym celu możesz napisać funkcję łączącą. Jeśli funkcja kumulacji ma 1 argument wejściowy 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 nie chcesz, aby domyślne działanie było takie, jak w przypadku funkcji combiner, możesz ją nadal napisać).
Przykład: w jądrze addint nie ma funkcji łącznika, więc zostanie użyta funkcja akkumulatora. To jest prawidłowe zachowanie, ponieważ jeśli podzielimy zbiór wartości na 2 części i oddzielnie zsumujemy wartości tych 2 części, otrzymamy ten sam wynik, co przy zsumowaniu całego zbioru.
Przykład: w jądrze findMinAndMax funkcja łącznika sprawdza, czy minimalna wartość zapisana w elemencie danych akumulatora „źródło” (*val
) jest mniejsza niż minimalna wartość zapisana w elemencie danych akumulatora „miejsce docelowe” (*accum
), i odpowiednio aktualizuje *accum
. Podobnie jest w przypadku wartości maksymalnej. W ten sposób *accum
zostanie zaktualizowany do stanu, jaki miałby, 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 utworzyć funkcję outconverter. Jeśli chcesz, aby ostateczna wartość elementów danych zbiorczego licznika była wynikiem redukcji, nie musisz pisać funkcji konwertującej wyjście.
Przykład: w jądrze addint nie ma funkcji outconverter. Ostateczna wartość połączonych elementów danych to suma wszystkich elementów danych 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
uruchomi jądro.initializer(initializerName)
(opcjonalnie): określa nazwę funkcji inicjalizacyjnej dla tego rdzenia 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 inicjalizatora, RenderScript zainicjalizuje każdy element danych akumulatory na wartość 0 (jakby był to element danych
memset
), zachowując się tak, jakby istniała funkcja inicjalizacji o takiej postaci: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 wywołuje ją tyle razy, ile potrzeba, 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 jest, że zmienne
*accum
i*other
zostały zainicjowane, ale nigdy nie zostały przekazane funkcji kumulacji, czyli ani jedna, ani druga nie zostały nigdy zaktualizowane zgodnie z danymi wejściowymi. Na przykład w rdzeniu findMinAndMax funkcja łączącafMMCombiner
sprawdza wyraźnie, czy istnieje zmienna danychidx < 0
, której wartość to INITVAL.Jeśli nie podasz funkcji łączącej, RenderScript użyje funkcji kumulacyjnej, zachowując się tak, jakby była to 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 dane wyjściowe w przypadku tego rdzenia redukcji. Po połą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 końcowego elementu danych licznika obliczonego przez funkcję łączącą.Jeśli nie podasz funkcji outconverter, RenderScript skopiuje ostatni element danych akumulatory 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 możesz polegać na kolejności, w jakiej RenderScript wywołuje funkcje initializer, accumulator i combiner; może nawet wywołać niektóre z nich 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 inicjalizacyjna będzie miała dostęp do niezinicjowanego elementu danych licznika. 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 kernel findMinAndMax nie jest deterministyczny: jeśli dane wejściowe zawierają więcej niż jedno wystąpienie tej samej minimalnej lub maksymalnej wartości, nie można określić, które z nich zostanie znalezione przez kernel.
Co musisz zagwarantować?
System RenderScript może uruchamiać jądro na wiele różnych sposobów, dlatego musisz przestrzegać określonych zasad, aby mieć pewność, że jądro będzie działać zgodnie z Twoimi oczekiwaniami. 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 chcesz zrobić z jądrem. 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ściowej”), gdzie 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 nad identyczną minimalną 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 inicjalizacyjna musi utworzyć wartość tożsamościową. 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 rzeczywiście wartością tożsamości.
Funkcja łączenia musi być przemienna. Oznacza to, że jeśli A
i B
to elementy danych zbiornika zainicjowane przez funkcję inicjalizacyjną i mogą być przekazane do funkcji zbiornika zero lub więcej razy, to 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 jądrze findMinAndMax,
fMMCombiner(&A, &B)jest taki sam jak
A = minmax(A, B)Dlatego te 2 sekwencje
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 kumulacji i funkcja łączenia muszą być zgodne z podstawową regułą składania. Oznacza to, że jeśli A
i B
to elementy danych licznika, A
zostało zainicjowane przez funkcję inicjalizacyjną i może zostać przekazane do funkcji licznika co najmniej 0 razy, B
nie zostało zainicjowane, a args to lista argumentów wejściowych i argumentów specjalnych dla konkretnego wywołania funkcji licznika, to A
musi być ustawiony na tę samą wartość:
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
- Zdanie 2 jest takie samo jak
B = 0
- Zdanie 3 jest takie samo jak
B += V
, który jest taki sam 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))
- Zdanie 2 jest takie samo jak
B = INITVAL
- Zdanie 3 jest takie samo jak
B = minmax(B, IndexedVal(V, X))
co, ponieważ B jest wartością początkową, jest takie samo jakB = IndexedVal(V, X)
- Zdanie 4 jest takie samo jak
A = minmax(A, B)
, które jest takie samo jakA = 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 licznika 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 zgodny z odpowiednim argumentem wejściowym. 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 ona tworzona za pomocą odpowiedniej metody 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, czyli 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 (skalarnym, wektorem ani tablicą), 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. Przykład:
- Jeśli resultType to
int
,int2
lubint[15]
, javaResultType toint
,Int2
lubint[]
. Wszystkie wartości parametru resultType mogą być reprezentowane przez parametr javaResultType. - Jeśli resultType to
uint
,uint2
lubuint[15]
,javaResultType ma wartośćlong
,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[]
. Niektóre wartości atrybutu resultType nie mogą być reprezentowane przez atrybut javaResultType.
javaFutureType to przyszły typ wyniku odpowiadający resultType funkcji outconverter.
- Jeśli resultType nie jest typem tablicy, javaFutureType ma wartość
result_resultType
. - Jeśli resultType to tablica o długości Count z elementami typu memberType, to javaFutureType to
resultArrayCount_memberType
.
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 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, 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. 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 atrybut inXType ma wartość
uint
, atrybut 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 niepodpisanych 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 sygnowane dane wejściowe o tej samej wielkości po stronie Javy, natomiast niepodpisane dane wyjściowe skryptu są reprezentowane jako rozszerzony sygnowany 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.