RenderScript to framework do wykonywania wymagających pod względem obliczeniowym zadań na Androidzie. Skrypt RenderScript jest przeznaczony głównie do stosowania w obliczeniach równoległych, chociaż zadań również może być korzystne. Środowisko wykonawcze RenderScript działa równolegle działają z różnymi procesorami dostępnymi w urządzeniu, np. procesorami wielordzeniowymi i GPU. Dzięki temu na wyrażaniu algorytmów, a nie na planowaniu pracy. Obecny skrypt RenderScript szczególnie przydaje się w aplikacjach do przetwarzania obrazu, fotografii obliczeniowej lub rozpoznawania obrazów.
Aby zacząć pracę z RenderScript, musisz zrozumieć 2 główne zagadnienia:
- Sam język to język wywodzący się z C99, służący do pisania wydajnych obliczeń obliczeniowych. w kodzie. Tworzenie jądra RenderScriptu zawiera opis: jak jej używać do pisania jąder obliczeniowych.
- Interfejs control API służy do zarządzania czasem trwania zasobów RenderScript i na sterowanie wykonywaniem jądra systemu operacyjnego. Program jest dostępny w trzech różnych językach: Java oraz C++ w Androidzie. NDK oraz język jądra pochodzącego z C99. Używanie kodu RenderScript z kodu Java i Metoda z jednego źródła RenderScript opisuje pierwsze i trzecie opcje.
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
zawierają:
- 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 w w projekcie bibliotecznym. - 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. Są one często 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 uzyskać dostęp do zmiennych globalnych skryptu z kodu Java. Są one często używane do przekazywania parametrów do jąder RenderScript. Więcej informacji o zmiennych globalnych skryptu znajdziesz tutaj.
0 lub więcej jąder obliczeniowych. Rdzeń obliczeniowy to funkcja lub zbiór funkcji, które możesz kierować w RenderScript Runtime do wykonywania równolegle na zbiorze danych. Są 2 rodzaje obliczeń jądra mapowania (zwane też jądrami pierwszego); i jądro redukcji.
Jądro 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; }
Pod wieloma względami równa się standardowemu 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 dane wejścioweAllocation
przekazane do uruchomienia jądra. argumentyx
iy
są omówiono poniżej. Wartość zwracana z jądra to automatycznie zapisane w odpowiedniej lokalizacji 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. Renderowanie w środowisku wykonawczym sprawdza, czy wszystkie przydziały wejściowe i wyjściowe mają takie same oraz że typy danych wejściowych i wyjściowych typuElement
Przydziały są zgodne z prototypem jądra. jeśli któraś z tych kontroli nie powiedzie się, RenderScript zgłasza wyjątek.UWAGA: przed Androidem 6.0 (poziom interfejsu API 23) jądro mapowania może nie może mieć więcej niż jednej wartości wejściowej
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))
Jądro redukcji to rodzina funkcji, które działają na zbiorze danych wejściowych
Allocations
o tych samych wymiarach. Domyślnie jego funkcja zasobnika jest wykonywana raz na każdą w tych wymiarach. Zwykle (ale nie zawsze) służy do „zredukowania” zbioru danych wejściowychAllocations
do pojedynczej wartości.Oto przykład prostej redukcji jądro, które łączy
Elements
dane wejściowe:#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ą mieć typstatic
. Jądro redukcji zawsze wymaga funkcjiaccumulator
; może też pełnić inne funkcje które ma być wykonane przez jądro.Funkcja akumulatora redukcji musi zwracać wartość
void
i musi mieć co najmniej dwóch argumentów. 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 zasobnika jest tworzony przez środowisko wykonawcze RenderScript. autor: inicjowana jest wartość zerowa. Domyślnie to jądro jest uruchamiane na wszystkich danych wejściowychAllocation
, z jednym wykonaniem funkcji akumulatora naElement
w:Allocation
. Według domyślnie, ostateczna wartość elementu danych akumulatora jest traktowana jako wynik funkcji i wraca 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.
Jądro redukcji są obsługiwane w Androidzie 7.0 (poziom interfejsu API 24) i nowszych.
Funkcja jądra mapowania lub funkcja akumulatora jądra redukcji 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.funkcję jądra mapowania lub zasobnik jądra redukcji, funkcja może także przyjmować opcjonalny specjalny argument
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 globalnych wartości i funkcji skryptów statycznych. Statyczny skrypt globalny jest taki sam jak skrypt globalny, z tym wyjątkiem, że nie można uzyskać do niego dostępu z 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 dokładności liczby zmiennoprzecinkowej w skrypcie. Jest to przydatne, jeśli Pełny standard IEEE 754-2008 (używany domyślnie) nie jest wymagany. Te pragmy mogą ustawić różne poziomy dokładności liczb zmiennoprzecinkowych:
#pragma rs_fp_full
(domyślnie, jeśli nic nie zostało określone): w przypadku aplikacji, które wymagają precyzja zmiennoprzecinkowa zgodnie ze standardem IEEE 754-2008.#pragma rs_fp_relaxed
: dla aplikacji, które nie wymagają rygorystycznego standardu IEEE 754-2008 zgodności i tolerować mniejszą precyzję. 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 z wartością -0,0 mogą zwracać zamiast tego +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
Gdy tworzysz aplikację na Androida korzystającą z języka RenderScript, jej interfejs API możesz uzyskać za pomocą języka Java na jeden z dwóch sposobów:
android.renderscript
– interfejsy API w tym pakiecie klas są dostępne na urządzeniach z Androidem 3.0 (poziom interfejsu API 11) lub nowszym.android.support.v8.renderscript
– interfejsy API w tym pakiecie to dostępne w Centrum pomocy Biblioteka, która umożliwia korzystanie z nich na urządzeniach z Androidem 2.3 (poziom interfejsu API 9) oraz wyżej.
Oto wady:
- Jeśli używasz interfejsów API biblioteki pomocy, część aplikacji w języku RenderScript będzie
zgodne z urządzeniami z Androidem 2.3 (poziom interfejsu API 9) lub nowszym niezależnie od tego, który skrypt RenderScript
funkcji używanych przez Ciebie. 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 nie są dostępne w interfejsach API z biblioteki pomocy.
- Jeśli używasz interfejsów API biblioteki pomocy, otrzymasz (prawdopodobnie znacznie) większe pliki APK niż
jeśli używasz natywnych (
android.renderscript
) interfejsów API.
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
Uwaga: począwszy od pakietu Android SDK Build-tools w wersji 24.0.0, przez Androida 2.2, (Poziom 8 interfejsu API) nie jest już obsługiwany.
Zainstalowaną wersję tych narzędzi możesz sprawdzić i zaktualizować w Menedżer pakietów SDK na Androida
Aby użyć interfejsów RenderScript API z biblioteki pomocy:
- Sprawdź, czy masz zainstalowaną wymaganą wersję pakietu Android SDK.
- Zaktualizuj ustawienia procesu kompilacji Androida, dodając ustawienia RenderScript:
- Otwórz plik
build.gradle
w folderze aplikacji modułu aplikacji. - Dodaj do pliku te ustawienia RenderScript:
android { compileSdkVersion 33 defaultConfig { minSdkVersion 9 targetSdkVersion 19 renderscriptTargetApi 18 renderscriptSupportModeEnabled true } }
android { compileSdkVersion(33) defaultConfig { minSdkVersion(9) targetSdkVersion(19) renderscriptTargetApi = 18 renderscriptSupportModeEnabled = true } }
Wymienione powyżej ustawienia określają działanie w procesie kompilacji Androida:
renderscriptTargetApi
– określa wersję kodu bajtowego do . 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 Twoja minimalna wersja pakietu SDK określona w pliku manifestu aplikacji jest ustawiona na inną wartość, jest ignorowany, a wartość docelowa w pliku kompilacji służy do określenia minimalnej wartości, Wersja pakietu SDK.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:
Używanie RenderScript z Java lub kodu Kotlin
Używanie RenderScriptu za pomocą kodu Java lub Kotlin opiera się na klasach API znajdujących się w
android.renderscript
lub pakiet android.support.v8.renderscript
. Większość
aplikacje są zgodne z tym samym podstawowym wzorcem użytkowania:
- Zainicjuj kontekst RenderScriptu. 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. Pamiętaj, że tworzenie kontekstu może być długotrwałą operacją, ponieważ może tworzyć zasoby na różnych urządzeniach. Jeśli to możliwe, nie powinna ona znajdować się na ścieżce krytycznej aplikacji. Zazwyczaj aplikacja ma tylko 1 kontekst RenderScript naraz. - Utwórz co najmniej 1 element
Allocation
, który zostanie przekazany do skryptu.Allocation
to obiekt RenderScript, który udostępnia pamięci masowej ze stałą 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 skryptu RenderScript w kodzie i na odwrót. ObiektyAllocation
są zwykle tworzone za pomocą:createTyped()
lubcreateFromBitmap()
. - Utwórz potrzebne skrypty. Dostępne są 2 rodzaje skryptów
podczas korzystania z kodu RenderScript:
- 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 jądro mapowania te znajdowały się w lokalizacjiinvert.rs
, a kontekst RenderScriptu znajdował się już wmRenderScript
, kod Java lub Kotlin do utworzenia wystąpienia skryptu będzie wyglądał tak: - ScriptIntrinsic: to wbudowane jądra RenderScriptu służące do typowych operacji.
takie jak rozmycie Gaussa, splot czy przenikanie 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 przydziałów utworzonych za pomocą funkcji
createFromBitmap()
w polu Alokacja znajdują się puste dane, gdy jest które zostały utworzone po raz pierwszy. Aby wypełnić alokację, użyj jednej z metod „kopiowania” wAllocation
. Metody „copy” są synchroniczne. - Ustaw wszystkie niezbędne globalne zmienne skryptu. Możesz ustawić wartości globalne za pomocą metod w sekcji
ta sama klasa
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: znajdują się w tej samej klasie
ScriptC_filename
za pomocą metod o nazwachforEach_mappingKernelName()
lubreduce_reductionKernelName()
. Zmiany te są asynchroniczne. W zależności od argumentów jądra przyjmuje co najmniej jeden przydział, który musi mieć te same wymiary. Domyślnie jądro uruchamia się w przypadku wszystkich współrzędnych w tych wymiarach, do uruchomienia jądra na podzbiorze tych współrzędnych, przekazać odpowiedniScript.LaunchOptions
jako ostatni argument w metodzieforEach
lubreduce
.Uruchamianie funkcji, których nie można wywołać, za pomocą metod
invoke_functionName
w tej samej klasieScriptC_filename
. Zmiany te są asynchroniczne. - Pobieraj dane z obiektów
Allocation
i javaFutureType. Aby uzyskać dostęp do danych zAllocation
z kodu Java, musisz skopiować te dane z powrotem do Javy, używając jednej z metod „copy” wAllocation
. Aby uzyskać wynik redukcji jądra, należy użyć metodyjavaFutureType.get()
. Kopia i metodyget()
są synchroniczne. - Przeanalizuj kontekst RenderScriptu. Możesz zniszczyć kontekst RenderScriptu
za pomocą funkcji
destroy()
lub przez włączenie kontekstu RenderScript jako obiekt do czyszczenia pamięci. Powoduje to dalsze korzystanie z należących do niej obiektów do zgłoszenia wyjątku.
Model wykonania asynchronicznego
Wyróżnione forEach
, invoke
, reduce
,
a metody set
są asynchroniczne – każda może wrócić do Javy przed zakończeniem
wymagane działanie. 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. „Kopia” jest synchroniczna i jest zserializowana względem dowolnego
działań asynchronicznych powyżej, które mają wpływ na tę samą alokację.
Odzwierciedlone klasy javaFutureType udostępniają metodę get()
, która umożliwia uzyskanie wyniku redukcji. get()
to
synchroniczna i jest zserializowana zgodnie z redukcją (asynchroniczną).
Skrypt RenderScript z jednego źródła
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 jąder mapowania, które w celu zachowania przejrzystości w tej sekcji nazywamy po prostu „jądrami”. Ta nowa funkcja obsługuje też tworzenie alokacji typu
rs_allocation
w skrypcie. 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 przypadku wielu uruchamianych jąder.
W języku RenderScript na jedno źródło treści zapisujesz jądra w sposób opisany tutaj:
Jak napisać jądro 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 danych wejściowych i
przydziały danych wyjściowych, które ma przetworzyć funkcja jądra.
Aby rozpocząć obliczenia w RenderScript, wywołujesz funkcję invokable z Javę.
Wykonaj czynności opisane w artykule Używanie kodu RenderScript z poziomu kodu Java.
W kroku uruchom odpowiednie jądra wywołaj
funkcji wywoływanej przez funkcję invoke_function_name()
, która spowoduje uruchomienie funkcji
całe 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 rozmiary w
wymiarów X, Y i Z jako argumentów. W przypadku alokacji 1D lub 2D można pominąć rozmiar wymiaru Y lub Z. Na przykład rsCreateAllocation_uchar4(16384)
tworzy przydział 1D o wartości
16384 elementy, z których każdy jest typu uchar4
.
Systemem automatycznie zarządza alokacją. Nie musisz ich wyraźnie zwalniać ani uwalniać. Możesz jednak zadzwonić
rsClearObject(rs_allocation* alloc)
, aby wskazać, że nie potrzebujesz już nicku.
alloc
do przydziału bazowego,
aby system mógł jak najszybciej zwolnić zasoby.
Sekcja Tworzenie jądra RenderScriptu zawiera przykład
jądro, które odwraca obraz. Poniższy przykład rozwija tę opcję, aby zastosować do obrazu więcej niż 1 efekt.
korzystając z języka RenderScript na pojedyncze źródło. Zawiera ono inny rdzeń, greyscale
, który zamienia obraz kolorowy w czarno-biały. Funkcja wywoływalna process()
stosuje te 2 jądra kolejno do obrazu wejściowego i tworzy obraz wyjściowy. Przydziały danych wejściowych i
dane wyjściowe 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:
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)
// 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 można w pełni wdrożyć algorytm, który obejmuje 2 uruchomienia jądra 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. Jest to nie tylko kod RenderScript z jednego źródła jest łatwiejszy do odczytania, a dodatkowo eliminuje między Javą a skryptem podczas uruchamiania jądra systemu operacyjnego. Niektóre iteracyjne algorytmy mogą uruchamiać jądra co sprawia, że koszty związane z takimi zmianami są znaczne.
Parametry globalne skryptu
Skrypt globalny to zwykły element inny niż static
w pliku skryptu (.rs
). Scenariusz
globalna o nazwie var zdefiniowany w
filename.rs
, będzie
metoda get_var
odzwierciedlona w
zajęcia ScriptC_filename
. Jeśli wartość globalna nie jest const
, dostępna jest 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 skrypcie. W przeciwnym razie wartość początkowa wynosi 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 asychroniecznie.
UWAGA: to oznacza, że oprócz static inicjator w skrypcie, wartości zapisane w wartości globalnej z są niewidoczne dla Javy.
Szczegółowe informacje o rdzeniu funkcji redukcji
Zmniejszanie to proces łączenia zbioru danych w jeden . Jest to przydatny element podstawowy przy programowaniu równoległym z aplikacjami, takimi jak :
- obliczanie sumy lub iloczynu na wszystkich danych
- operacje logiczne (
and
,or
,xor
) we 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 skrypt RenderScript obsługuje jądro redukcji, do wydajnych algorytmów redukcji napisanych przez użytkownika. Możesz uruchamiać jądra redukcji na danych wejściowych z 1, 2 lub 3 wymiary.
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 jąder 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 z akumulatora w taki sposób, by zmaksymalizować skuteczność. 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 polu addint
jądro, elementy danych akumulatora (typu int
) są używane do sumowania danych wejściowych
. Nie ma funkcji inicjatora, więc każdy element danych akumulatora jest inicjowany
zero.
Przykład: In
jądro findMinAndMax, czyli elementy danych akumulatora
(typu MinAndMax
) są używane do śledzenia wartości minimalnych i maksymalnych
do tej pory. 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ę zasobnika raz na każdą współrzędną w parametrze danych wejściowych. Zwykle funkcja powinna w jakiś sposób aktualizować element danych akumulatora zgodnie z danymi wejściowymi.
Przykład: w polu addint jądro, funkcja akumulatora dodaje wartość elementu wejściowego do zasobnika elementu danych.
Przykład: In jądro findMinAndMax, czyli funkcja akumulatora sprawdza, czy wartość elementu wejściowego jest mniejsza czy równa minimalnej wartości wartość zarejestrowana w elemencie danych akumulatora lub większa lub równa wartości maksymalnej zarejestrowanej w elemencie danych zasobnika, a także aktualizuje ten element odpowiednio się zmienia.
Po wywołaniu funkcji zasobnika raz dla każdej współrzędnej w danych wejściowych RenderScript musi połączyć zasobnik elementów danych razem w jedną pozycję danych akumulatora. Możesz napisać kombinację . 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. (Możesz utworzyć funkcję łączącą, jeśli to domyślne działanie nie jest tym, w pobliżu.
Przykład: w jądrze addint nie ma funkcji łącznika, więc zostanie użyta funkcja akkumulatora. To jest działa prawidłowo, ponieważ jeśli podzielimy zbiór wartości na dwa części i dodaj wartości z tych 2 części osobno. Suma tych dwóch sum jest równa w ten sposób całą kolekcję.
Przykład: In
jądro findMinAndMax, czyli funkcja łącząca
sprawdza, czy minimalna wartość zarejestrowana w „źródle” dane akumulatora
element *val
jest mniejszy niż minimalna wartość zarejestrowana w „miejscu docelowym”
element danych zasobnika *accum
, aktualizacje *accum
odpowiednio się zmienia. Podobnie jest w przypadku wartości maksymalnej. Ta czynność aktualizuje *accum
do stanu, jaki uzyskałaby, gdyby wszystkie wartości wejściowe zostały zgromadzone w
*accum
zamiast kilku w *accum
, a jeszcze innych w
*val
Po połączeniu wszystkich elementów danych akumulatora RenderScript określa w wyniku redukcji i powrót do Javy. Możesz napisać reklamodawcę zewnętrznego, . Nie musisz pisać funkcji konwertera, aby wartość końcową połączonych elementów danych akumulatora jako wynik redukcji.
Przykład: w jądrze addint nie ma funkcji konwertera zewnętrznego. Ostateczna wartość elementów połączonych danych to suma wszystkich elementów danych wejściowych, czyli wartości, które 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 kombinacji wszystkich elementów danych kumulatora.
Tworzenie jądra redukcji
#pragma rs reduce
definiuje jądro redukcji przez
podając jego nazwę oraz nazwy i role funkcji, które tworzą
gdy ją uruchamiają. Wszystkie takie funkcje muszą być static
. Jądro redukcji zawsze wymaga accumulator
funkcji; możesz pominąć niektóre lub wszystkie pozostałe funkcje, w zależności od tego, czego chcesz
przez jądra.
#pragma rs reduce(kernelName) \ initializer(initializerName) \ accumulator(accumulatorName) \ combiner(combinerName) \ outconverter(outconverterName)
Elementy w #pragma
mają następujące znaczenie:
reduce(kernelName)
(wymagane): określa, że jądro redukcji jest nie jest zdefiniowany. Odzwierciedlona metoda Javareduce_kernelName
uruchomi jądro.initializer(initializerName)
(opcjonalny): określa nazwę funkcja inicjująca tego jądra redukcji. Po uruchomieniu jądra kod RenderScript tę funkcję raz dla każdego elementu danych akumulatora. funkcja musi być zdefiniowana w następujący sposób:static void initializerName(accumType *accum) { … }
accum
wskazuje element danych akumulatora dla tej funkcji, aby zainicjować.Jeśli nie podasz funkcji inicjalizatora, RenderScript inicjalizuje każdy element danych akumulatory na 0 (jakby
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. Po uruchomieniu jądra kod RenderScript tę funkcję raz na każdą współrzędną wejściowych, aby zaktualizować elementu danych akumulatora w jakiś sposób zgodnie z danymi wejściowymi. Funkcja musi być zdefiniowana w ten sposób:static void accumulatorName(accumType *accum, in1Type in1, …, inNType inN [, specialArguments]) { … }
accum
wskazuje element danych akumulatora dla tej funkcji, aby modyfikować. Odin1
doinN
to co najmniej jeden argument, który są automatycznie wypełniane na podstawie danych wejściowych przekazywanych do uruchomienia jądra – 1 argument na dane wejściowe. Funkcja zasobnika może opcjonalnie przyjąć dowolny ze argumentów specjalnych.Przykładowe jądro z wieloma danymi wejściowymi to
dotProduct
.combiner(combinerName)
(opcjonalnie): określa nazwę funkcji łączenia dla tego rdzenia redukcji. Po wywołaniu przez RenderScript funkcji zasobnika raz na każdą współrzędną w danych wejściowych, wywołuje tę funkcję tyle samo by połączyć wszystkie elementy danych akumulatora w jeden elementu danych akumulatora. 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
wskazuje „źródło” element danych akumulatora dla tej funkcji do „połączenia” na*accum
.UWAGA: to możliwe. że funkcje
*accum
,*other
lub oba zostały zainicjowane, ale nigdy została przekazana do funkcji akumulatora; oznacza to, że któraś z nich nigdy nie została zaktualizowana zgodnie z dowolnymi danymi wejściowymi. Na przykład w polu jądro findMinAndMax, czyli składnik łączący funkcjafMMCombiner
wyraźnie sprawdza funkcjęidx < 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 była to funkcja łącząca o tym kształcie:
static void combinerName(accumType *accum, const accumType *other) { accumulatorName(accum, *other); }
Funkcja łącząca jest wymagana, jeśli jądro ma więcej niż 1 dane wejściowe, jeśli dane wejściowe typ danych nie jest taki sam jak typ danych akumulatora lub jeśli funkcja zasobnika przyjmuje taki sam typ lub więcej specjalnych argumentów.
outconverter(outconverterName)
(opcjonalnie): określa nazwę funkcji konwertującej wyjście w przypadku tego rdzenia redukcji. Po połączeniu przez RenderScript całego zasobnika elementów danych, wywołuje tę funkcję, aby określić wynik funkcji i powrót do języka Java. Funkcja musi być zdefiniowana w ten sposób: to:static void outconverterName(resultType *result, const accumType *accum) { … }
result
to wskaźnik do elementu danych wyniku (przydzielonego, ale nie zainicjowanego) przez środowisko wykonawcze RenderScript), aby zainicjować tę funkcję z wynikiem funkcji . Parametr resultType określa typ danego elementu danych, który nie musi być taki sam. jako accumType.accum
to wskaźnik do ostatniego elementu danych licznika obliczonego przez funkcję łączącą.Jeśli nie podasz funkcji outconverter, RenderScript skopiuje końcowy zasobnik elementu danych do wynikowego elementu danych, zachowując się w taki sposób, jakby istniała funkcja przekonwertowania, która wygląda tak:
static void outconverterName(accumType *result, const accumType *accum) { *result = *accum; }
Jeśli chcesz uzyskać inny typ wyniku niż typ danych akumulatora, musisz użyć funkcji konwertera.
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żesz przyjąć?
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 inicjator, zasobnik i funkcje łączące; może nawet wywoływać niektóre z nich równolegle. Nie ma gwarancji, że dwa uruchomienia tego samego jądra z tymi samymi danymi wejściowymi będą wykonywane w tej samej kolejności; Jedyna gwarantuje, że tylko funkcja inicjująca zobaczy niezainicjowany zasobnik elementu danych. 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 akumulatora 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 zdecydować się na uruchomienie jądra w wielu na różne sposoby, musisz przestrzegać pewnych reguł, aby jądro działało w dobry sposób. Jeśli nie będziesz ich przestrzegać, możesz otrzymać nieprawidłowe wyniki. niedeterministyczne zachowanie lub błędy czasu działania.
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. Dla: ograniczenie matematyczne, np. addint, zwykle ma sens dla hasła „tego samego” oznacza równość matematyczną. Wybierz dowolne wyszukaj takie jako findMinAndMax („znajdź lokalizację minimalnej i maksymalnych wartości wejściowych”), w których może wystąpić więcej niż jedno wystąpienie identycznej wartości wejściowej. wszystkie lokalizacje o danej wartości wejściowej muszą być uznawane za „takie 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 inicjująca musi utworzyć wartość tożsamości. Oznacza to, że jeśliI
i A
to elementy danych licznika zainicjowane 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
bez zmiancombinerName(&I, &A)
musi zostawI
tego samego coA
Przykład: w polu addint jądro, element danych akumulatora zostaje zainicjowany do zera. 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
do:A
, boI
toINITVAL
.
Dlatego INITVAL
jest rzeczywiście wartością tożsamości.
Funkcja łącząca 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 polu addint jądro, funkcja łącząca dodaje dwie wartości danych elementu danych akumulatora; dodanie to przemienne.
Przykład: w jądrze findMinAndMax
fMMCombiner(&A, &B)
to ta sama wartość co
Atrybuty A = minmax(A, B)
i minmax
są przemienne, więc
fMMCombiner
również.
Funkcja łączenia musi być asocjacyjna. To znaczy,
jeśli A
, B
i C
to
elementów danych zasobnika zainicjowanych przez funkcję inicjatora i które mogły zostać przekazane
do funkcji akumulatora zero lub więcej razy, dwie poniższe sekwencje kodu muszą
ustaw A
na tę samą wartość:
combinerName(&A, &B); combinerName(&A, &C);
combinerName(&B, &C); combinerName(&A, &B);
Przykład: w jądrze addint para klucz-wartość funkcja łącząca dodaje 2 wartości elementu danych zasobnika:
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)
Komponent minmax
jest powiązany, więc fMMCombiner
również jest powiązany.
Funkcja zasobnika i funkcja łączącego muszą być zgodne z podstawową funkcją
za pomocą reguły składania. Oznacza to, że jeśli A
i B
to elementy danych akumulatory, A
zostało zainicjowane przez funkcję inicjalizacyjną i może zostać przekazane do funkcji akumulatory 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 akumulatory, 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:
- Polecenie 1 jest takie samo jak
A += V
. - Zdanie 2 jest takie samo jak
B = 0
- Stwierdzenie 3 jest takie samo jak
B += V
, które jest identyczne zB = 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 jądrze findMinAndMax jako dane wejściowe wartość V w współrzędnych X:
- Zdanie 1 jest takie samo jak
A = minmax(A, IndexedVal(V, X))
- Zdanie 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))
Instrukcje 1 i 4 ustawiają A
na tę samą wartość, więc to jądro jest zgodne z
podstawowej reguły składania.
Wywołanie 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
:
// 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
// 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:
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()
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. Środowisko wykonawcze RenderScript sprawdza, czy wszystkie przydziały wejściowe
mają te same wymiary i że typ Element
każdego z
przydział wejściowy odpowiada temu odpowiedniemu argumentowi wejściowemu zasobnika
prototyp funkcji. Jeśli któraś z tych kontroli zakończy się niepowodzeniem, RenderScript zgłosi 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 wyjątkiem:
zamiast korzystać z danych wejściowych alokacji, wykorzystuje tablicowe dane w Javie. To wygoda,
eliminuje konieczność pisania kodu w celu jawnego utworzenia przydziału i skopiowania do niego danych.
z tablicy Java. Zastosowanie metody 3 zamiast metody 1 nie zwiększy jednak
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
Przydziały.
UWAGA: jeśli aplikacja będzie wykonywać wiele wywołań jądra za pomocą tej samej tablicy lub z różnymi tablicami o tych samych wymiarach i typie elementu, a nie przez bezpośrednie tworzenie, wypełnianie i ponowne używanie przydziałów. za pomocą metody 3.
javaFutureType,
typ zwrotu odzwierciedlonych metod redukcji jest odzwierciedlony
statyczna zagnieżdżona klasa w elemencie ScriptC_filename
zajęcia. Przedstawia on przyszły rezultat zmniejszenia
uruchomienia jądra systemu operacyjnego. Aby uzyskać rzeczywisty wynik uruchomienia, wywołaj
metody get()
tej klasy, która zwraca wartość
typu javaResultType. Funkcja get()
jest synchroniczna.
class ScriptC_filename(rs: RenderScript) : ScriptC(…) { object javaFutureType { fun get(): javaResultType { … } } }
public class ScriptC_filename extends ScriptC { public static class javaFutureType { public javaResultType get() { … } } }
javaResultType jest określany na podstawie parametru resultType parametru funkcji konwertera zewnętrznego. O ile resultType nie jest typ bez znaku (skalarny, wektorowy lub tablica), javaResultType jest bezpośrednio odpowiadającym Typ Java. Jeśli resultType jest typem bez podpisu i istnieje większy typ podpisany w Javie, z kolei javaResultType to większy typ podpisany w języku Java; w przeciwnym razie jest bezpośrednio odpowiedniego typu Java. Na przykład:
- Jeśli resultType to
int
,int2
lubint[15]
, to javaResultType toint
,Int2
, lubint[]
. Dozwolone są wszystkie wartości parametru resultType. autorstwa javaResultType. - Jeśli resultType to
uint
,uint2
lubuint[15]
,javaResultType tolong
,Long2
lublong[]
. Dozwolone są wszystkie wartości parametru resultType. autorstwa 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, który odpowiada do parametru resultType elementu 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
.
Na przykład:
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> = … } }
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. O ile inXType nie jest typ bez znaku lub typ wektorowy, devecSiInXType to bezpośrednio odpowiadający mu typ wektora Java typu. Jeśli inXType jest typem skalarnym bez znaku, to devecSiInXType jest Typ Java bezpośrednio odpowiadający typowi ze znakiem skalarnym tego samego rozmiaru. Jeśli inXType jest typem wektora podpisanego, devecSiInXType jest typem wektora Java odpowiadający typowi komponentu wektorowego. 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 ma wartość
int
, to devecSiInXType jestint
. - 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
, to deviceSiInXType. jestint
. Wartość ze znakiem w tablicy Java jest interpretowana jako wartość bez znaku o tym samym wzorze bitowym w alokacji. Działa tak samo jakcopyFrom()
metodyAllocation
. - 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 podpisanych tablic 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 w inny sposób typy wyników niż:
- Dane wejściowe skryptu są wektorowe po stronie Javy, a wynik wektorowy skryptu – nie.
- 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.