Omówienie RenderScript

RenderScript to platforma do wykonywania zadań wymagających dużej mocy obliczeniowej na Androidzie. RenderScript jest przeznaczony głównie do obliczeń równoległych, ale może też być przydatny w przypadku zadań szeregowych. Środowisko wykonawcze RenderScript rozdziela pracę równolegle na procesory dostępne na urządzeniu, takie jak wielordzeniowe procesory i GPU. Dzięki temu możesz skupić się na wyrażaniu algorytmów, a nie na planowaniu pracy. RenderScript jest szczególnie przydatny w aplikacjach, które przetwarzają obrazy, wykorzystują fotografię cyfrową lub rozpoznają obrazy.

Aby rozpocząć pracę z RenderScript, musisz zrozumieć 2 główne pojęcia:

  • Sam język pochodzi od C99 i służy do pisania kodu obliczeniowego o wysokiej wydajności. W artykule Pisanie jądra RenderScript opisano, jak używać tego interfejsu do pisania jądra obliczeniowego.
  • Interfejs API sterowania służy do zarządzania czasem życia zasobów RenderScript i kontrolowania wykonywania jądra. Jest dostępny w 3 językach: Java, C++ w Android NDK oraz w języku jądra pochodzącym z C99. Używanie RenderScript z kodu Java i RenderScript z jednego źródła opisują odpowiednio pierwszą i trzecią opcję.

Pisanie jądra RenderScript

Jądro RenderScript zwykle znajduje się w pliku .rs w katalogu <project_root>/src/rs. Każdy plik .rs jest nazywany skryptem. Każdy skrypt zawiera własny zestaw jądra, funkcji i zmiennych. Skrypt może zawierać:

  • Deklaracja pragma (#pragma version(1)), która deklaruje 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 Javy odzwierciedlonych w tym skrypcie. Pamiętaj, że plik .rs musi być częścią pakietu aplikacji, a nie projektu biblioteki.
  • Zero lub więcej funkcji możliwych do wywołania. 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 wstępnej konfiguracji lub obliczeń szeregowych w ramach większego potoku przetwarzania.
  • Zero lub więcej zmiennych globalnych skryptu. Zmienna globalna skryptu jest podobna do zmiennej globalnej w języku C. Do zmiennych globalnych skryptu można uzyskać dostęp z poziomu kodu w języku Java. Są one często używane do przekazywania parametrów do jądra RenderScript. Więcej informacji o globalnych zmiennych skryptu znajdziesz tutaj.

  • Zero lub więcej jąder obliczeniowych. Jądro obliczeniowe to funkcja lub zbiór funkcji, które można przekazać do środowiska wykonawczego RenderScript w celu równoległego wykonania na zbiorze danych. Istnieją 2 rodzaje jądra obliczeniowego: jądro mapowania (nazywane też jądrem foreach) i jądro redukcji.

    Jądro mapowania to funkcja równoległa, która działa na kolekcji Allocations o tych samych wymiarach. Domyślnie jest wykonywana raz dla każdej współrzędnej w tych wymiarach. Zwykle (ale nie wyłącznie) służy do przekształcania kolekcji wejściowych obiektów Allocations w wyjściowe obiekty Allocation, po jednym obiekcie Element naraz.

    • Oto przykład prostego jądra 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 przypadków jest to identyczne ze standardową funkcją C. Właściwość RS_KERNEL zastosowana do prototypu funkcji określa, że funkcja jest jądrem mapowania RenderScript, a nie funkcją wywoływalną. Argument in jest automatycznie wypełniany na podstawie danych wejściowych Allocation przekazanych do uruchomienia jądra. Argumenty x i y zostały omówione poniżej. Wartość zwrócona z jądra jest automatycznie zapisywana w odpowiednim miejscu w wyjściowym obiekcie Allocation. Domyślnie to jądro jest uruchamiane na całym wejściu Allocation, przy czym funkcja jądra jest wykonywana raz na Element w Allocation.

      Jądro mapowania może mieć co najmniej 1 wejście Allocations, jedno wyjście Allocation lub oba te elementy. Środowisko wykonawcze RenderScript sprawdza, czy wszystkie wejściowe i wyjściowe obiekty Allocation mają te same wymiary oraz czy typy Element wejściowych i wyjściowych obiektów Allocation pasują do prototypu jądra. Jeśli któryś z tych testów się nie powiedzie, RenderScript zgłasza wyjątek.

      UWAGA: w wersjach Androida starszych niż 6.0 (poziom interfejsu API 23) jądro mapowania nie może mieć więcej niż 1 wejście Allocation.

      Jeśli potrzebujesz więcej danych wejściowych lub wyjściowych Allocations, niż ma jądro, te obiekty powinny być powiązane ze zmiennymi globalnymi skryptu rs_allocation i dostępne z jądra lub funkcji wywoływalnej przez rsGetElementAt_type() lub rsSetElementAt_type().

      UWAGA: RS_KERNEL to makro definiowane 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 funkcja akumulatora jest wykonywana raz dla każdej współrzędnej w tych wymiarach. Zwykle (ale nie wyłącznie) służy do „redukowania” kolekcji wejściowych obiektów Allocations do pojedynczej wartości.

    • Oto przykład prostego jądra redukcji, które sumuje Elements danych wejściowych:

      #pragma rs reduce(addint) accumulator(addintAccum)
      
      static void addintAccum(int *accum, int val) {
        *accum += val;
      }

      Jądro redukcji składa się z co najmniej 1 funkcji napisanej przez użytkownika. #pragma rs reduce służy do definiowania jądra przez określenie jego nazwy (w tym przykładzie addint) oraz nazw i ról funkcji, które tworzą jądro (w tym przykładzie funkcja accumulator addintAccum). Wszystkie takie funkcje muszą być static. Jądro redukcji zawsze wymaga funkcji accumulator. Może też mieć inne funkcje, w zależności od tego, co ma robić.

      Funkcja akumulatora jądra redukcji musi zwracać void i musi mieć co najmniej 2 argumenty. Pierwszy argument (w tym przykładzie accum) jest wskaźnikiem do elementu danych akumulatora, a drugi (w tym przykładzie val) jest automatycznie wypełniany na podstawie danych wejściowych Allocation przekazanych do uruchomienia jądra. Element danych akumulatora jest tworzony przez środowisko wykonawcze RenderScript. Domyślnie jest inicjowany na wartość 0. Domyślnie to jądro jest uruchamiane na całym wejściu Allocation, przy czym funkcja akumulatora jest wykonywana raz na Element w Allocation. Domyślnie ostateczna wartość elementu danych akumulatora jest traktowana jako wynik redukcji i zwracana do Javy. Środowisko wykonawcze RenderScript sprawdza, czy typ Element wejściowej alokacji pasuje do prototypu funkcji akumulatora. Jeśli nie, RenderScript zgłasza wyjątek.

      Jądro redukcji ma co najmniej 1 wejście Allocations, ale nie ma wyjścia Allocations.

      Więcej informacji o jądrach redukcji znajdziesz tutaj.

      Jądra redukcyjne są obsługiwane na Androidzie 7.0 (poziom interfejsu API 24) i nowszych wersjach.

    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 i z, które muszą być typu int lub uint32_t. Te argumenty są opcjonalne.

    Funkcja jądra mapowania lub funkcja akumulatora jądra redukcji może też przyjmować opcjonalny argument specjalny context typu rs_kernel_context. Jest on wymagany przez rodzinę interfejsów API środowiska wykonawczego, które służą do wysyłania zapytań o określone właściwości bieżącego wykonania – na przykład rsGetDimX. (Argument context jest dostępny na Androidzie 6.0 (poziom interfejsu API 23) i nowszych wersjach).

  • Opcjonalna funkcja init(). Funkcja init() to specjalny typ funkcji wywoływalnej, którą RenderScript uruchamia, gdy skrypt jest tworzony po raz pierwszy. Dzięki temu niektóre obliczenia mogą być wykonywane automatycznie podczas tworzenia skryptu.
  • Zero lub więcej statycznych zmiennych globalnych i funkcji skryptu. Statyczny skrypt globalny jest odpowiednikiem skryptu globalnego, ale 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 funkcji wywoływalnej w skrypcie, ale nie jest ona dostępna dla interfejsu Java API. Jeśli nie ma potrzeby uzyskiwania dostępu do zmiennej globalnej lub funkcji skryptu z kodu Java, zdecydowanie zalecamy zadeklarowanie jej jako static.

Ustawianie precyzji zmiennoprzecinkowej

Możesz kontrolować wymagany poziom precyzji zmiennoprzecinkowej w skrypcie. Jest to przydatne, jeśli nie jest wymagany pełny standard IEEE 754-2008 (używany domyślnie). Te pragmy mogą ustawić inny poziom precyzji zmiennoprzecinkowej:

  • #pragma rs_fp_full (domyślna, jeśli nie określono inaczej): w przypadku aplikacji, które wymagają precyzji zmiennoprzecinkowej zgodnie ze standardem IEEE 754-2008.
  • #pragma rs_fp_relaxed: w przypadku aplikacji, które nie wymagają ścisłej zgodności ze standardem IEEE 754-2008 i mogą tolerować mniejszą precyzję. Ten tryb umożliwia zaokrąglanie do zera w przypadku liczb zdenormalizowanych i zaokrąglanie w kierunku zera.
  • #pragma rs_fp_imprecise: w przypadku aplikacji, które nie mają rygorystycznych wymagań dotyczących precyzji. Ten tryb umożliwia korzystanie ze wszystkich funkcji trybu rs_fp_relaxed oraz tych dodatkowych:
    • Operacje, których wynikiem jest -0.0, mogą zamiast tego zwracać +0.0.
    • Operacje na wartościach INF i NAN są niezdefiniowane.

Większość aplikacji może używać 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 przy mniejszej precyzji (np. instrukcje procesora SIMD).

Uzyskiwanie dostępu do interfejsów API RenderScript z poziomu Javy

Podczas tworzenia aplikacji na Androida, która korzysta z RenderScript, możesz uzyskać dostęp do interfejsu API z Javy na 2 sposoby:

  • 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 są dostępne w bibliotece pomocy, dzięki czemu można ich używać na urządzeniach z Androidem 2.3 (poziom interfejsu API 9) lub nowszym.

Oto kompromisy:

  • Jeśli używasz interfejsów API biblioteki obsługi, część RenderScript aplikacji będzie zgodna z urządzeniami z Androidem 2.3 (poziom interfejsu API 9) lub nowszym, niezależnie od tego, których funkcji RenderScript używasz. Dzięki temu aplikacja będzie działać na większej liczbie urządzeń niż w przypadku korzystania z natywnych interfejsów API (android.renderscript).
  • Niektóre funkcje RenderScript nie są dostępne w interfejsach API biblioteki pomocy.
  • Jeśli używasz interfejsów API biblioteki pomocy, pliki APK będą (prawdopodobnie znacznie) większe niż w przypadku natywnych interfejsów API (android.renderscript).

Korzystanie z interfejsów API biblioteki obsługi RenderScript

Aby korzystać z interfejsów API RenderScript biblioteki pomocy, musisz skonfigurować środowisko programistyczne tak, aby miało do nich dostęp. Do korzystania z tych interfejsów API wymagane są te narzędzia Android SDK:

  • Android SDK Tools w wersji 22.2 lub nowszej,
  • Narzędzia do kompilacji Android SDK w wersji 18.1.0 lub nowszej

Pamiętaj, że od wersji 24.0.0 narzędzi do kompilacji pakietu Android SDK Android 2.2 (poziom interfejsu API 8) nie jest już obsługiwany.

Zainstalowaną wersję tych narzędzi możesz sprawdzić i zaktualizować w Android SDK Manager.

Aby używać interfejsów API RenderScript z biblioteki pomocy:

  1. Sprawdź, czy masz zainstalowaną wymaganą wersję pakietu Android SDK.
  2. Zaktualizuj ustawienia procesu kompilacji na Androida, aby uwzględnić ustawienia RenderScript:
    • Otwórz plik build.gradle w folderze aplikacji modułu aplikacji.
    • Dodaj do pliku te ustawienia RenderScript:

      Dynamiczny

              android {
                  compileSdkVersion 36
      
                  defaultConfig {
                      minSdkVersion 9
                      targetSdkVersion 19
      
                      renderscriptTargetApi 18
                      renderscriptSupportModeEnabled true
                  }
              }
              

      Kotlin

              android {
                  compileSdkVersion(36)
      
                  defaultConfig {
                      minSdkVersion(9)
                      targetSdkVersion(19)
      
                      renderscriptTargetApi = 18
                      renderscriptSupportModeEnabled = true
                  }
              }
              

      Ustawienia wymienione powyżej kontrolują określone zachowania w procesie kompilacji na 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 może zapewnić wszystkie funkcje, których używasz, i ustawienie renderscriptSupportModeEnabled na true. Prawidłowe wartości tego ustawienia to dowolne liczby całkowite od 11 do najnowszego poziomu interfejsu API. Jeśli minimalna wersja pakietu SDK określona w pliku manifestu aplikacji jest ustawiona na inną wartość, ta wartość jest ignorowana, a do ustawienia minimalnej wersji pakietu SDK jest używana wartość docelowa w pliku kompilacji.
      • renderscriptSupportModeEnabled – określa, że wygenerowany kod bajtowy powinien zostać przywrócony do zgodnej wersji, jeśli urządzenie, na którym jest uruchomiony, nie obsługuje wersji docelowej.
  3. W klasach aplikacji, które używają RenderScript, dodaj import klas biblioteki pomocy:

    Kotlin

    import android.support.v8.renderscript.*

    Java

    import android.support.v8.renderscript.*;

Używanie RenderScript z kodu Java lub Kotlin

Używanie RenderScriptu z kodu Java lub Kotlin opiera się na klasach interfejsu API znajdujących się w pakiecie android.renderscript lub android.support.v8.renderscript. Większość aplikacji korzysta z tego samego podstawowego wzorca:

  1. Zainicjuj kontekst RenderScript. Kontekst RenderScript utworzony za pomocą create(Context) zapewnia możliwość używania RenderScript i udostępnia obiekt do kontrolowania czasu życia wszystkich kolejnych obiektów RenderScript. Tworzenie kontekstu należy traktować jako potencjalnie długo trwającą operację, ponieważ może ono tworzyć zasoby na różnych elementach sprzętu. Jeśli to możliwe, nie powinno ono znajdować się w ścieżce krytycznej aplikacji. Zwykle aplikacja ma tylko 1 kontekst RenderScript naraz.
  2. Utwórz co najmniej 1 Allocation, który zostanie przekazany do skryptu. Allocation to obiekt RenderScript, który zapewnia miejsce na przechowywanie stałej ilości danych. Jądra w skryptach przyjmują obiekty Allocation jako dane wejściowe i wyjściowe, a obiekty Allocation są dostępne w jądrach za pomocą rsGetElementAt_type() i rsSetElementAt_type(), gdy są powiązane jako zmienne globalne skryptu. Obiekty Allocation umożliwiają przekazywanie tablic z kodu Java do kodu RenderScript i odwrotnie. Obiekty Allocation są zwykle tworzone za pomocą createTyped() lub createFromBitmap().
  3. Utwórz niezbędne skrypty. Podczas korzystania z RenderScript dostępne są 2 rodzaje skryptów:
    • ScriptC: są to skrypty zdefiniowane przez użytkownika, opisane powyżej w sekcji Pisanie jądra RenderScript. Każdy skrypt ma klasę Java odzwierciedloną przez kompilator RenderScript, aby ułatwić dostęp do skryptu z kodu Java. Ta klasa ma nazwę ScriptC_filename. Jeśli na przykład powyższe jądro mapowania znajduje się w invert.rs, a kontekst RenderScript jest już w mRenderScript, kod w języku Java lub Kotlin do utworzenia instancji skryptu będzie wyglądać tak:

      Kotlin

      val invert = ScriptC_invert(renderScript)

      Java

      ScriptC_invert invert = new ScriptC_invert(renderScript);
    • ScriptIntrinsic: są to wbudowane jądra RenderScript do typowych operacji, takich jak rozmycie gaussowskie, splot i mieszanie obrazów. Więcej informacji znajdziesz w podklasach ScriptIntrinsic.
  4. Wprowadź dane do tabeli Allocations. Z wyjątkiem alokacji utworzonych za pomocą createFromBitmap() alokacja jest wypełniana pustymi danymi, gdy jest tworzona po raz pierwszy. Aby wypełnić przydział, użyj jednej z metod „copy” w Allocation. Metody „copy” są synchroniczne.
  5. Ustaw niezbędne zmienne globalne skryptu. Możesz ustawić zmienne globalne, używając metod w tej samej klasie ScriptC_filename o nazwie set_globalname. Na przykład, aby ustawić zmienną int o nazwie threshold, użyj metody Java set_threshold(int), a aby ustawić zmienną rs_allocation o nazwie lookup, użyj metody Java set_lookup(Allocation). Metody set są asynchroniczne.
  6. Uruchom odpowiednie jądra i funkcje, które można wywołać.

    Metody uruchamiania danego jądra są odzwierciedlone w tej samej klasie ScriptC_filename z metodami o nazwie forEach_mappingKernelName() lub reduce_reductionKernelName(). Te uruchomienia są asynchroniczne. W zależności od argumentów jądra metoda przyjmuje co najmniej 1 alokację, przy czym wszystkie muszą mieć te same wymiary. Domyślnie jądro jest wykonywane na wszystkich współrzędnych w tych wymiarach. Aby wykonać jądro na podzbiorze tych współrzędnych, przekaż odpowiedni obiekt Script.LaunchOptions jako ostatni argument metody forEach lub reduce.

    Uruchamiaj funkcje możliwe do wywołania za pomocą metod invoke_functionName odzwierciedlonych w tej samej klasie ScriptC_filename. Te uruchomienia są asynchroniczne.

  7. Pobieranie danych z obiektów Allocation i javaFutureType. Aby uzyskać dostęp do danych z Allocation z poziomu kodu Javy, musisz skopiować te dane z powrotem do Javy, używając jednej z metod „copy” w Allocation. Aby uzyskać wynik jądra redukcji, musisz użyć metody javaFutureType.get(). Metody „copy” i get() są synchroniczne.
  8. Zlikwiduj kontekst RenderScript. Kontekst RenderScript możesz zniszczyć za pomocą destroy() lub zezwalając na usunięcie obiektu kontekstu RenderScript przez moduł odzyskiwania pamięci. Spowoduje to, że każde dalsze użycie dowolnego obiektu należącego do tego kontekstu spowoduje zgłoszenie wyjątku.

Asynchroniczny model wykonywania

Odzwierciedlone metody forEach, invoke, reduce i set są asynchroniczne – każda z nich może powrócić do Javy przed zakończeniem żądanego działania. Jednak poszczególne działania są serializowane w kolejności, w jakiej są uruchamiane.

Klasa Allocation udostępnia metody „copy” do kopiowania danych do i z alokacji. Metoda „copy” jest synchroniczna i jest serializowana w odniesieniu do dowolnego z powyższych działań asynchronicznych, które dotyczą tej samej alokacji.

Odzwierciedlone klasy javaFutureType udostępniają metodę get(), która pozwala uzyskać wynik redukcji. get() jest synchroniczna i jest serializowana w odniesieniu do redukcji (która jest asynchroniczna).

RenderScript z jednego źródła

Android 7.0 (poziom interfejsu API 24) wprowadza nową funkcję programowania o nazwie Single-Source RenderScript, w której jądra są uruchamiane ze skryptu, w którym są zdefiniowane, a nie z Javy. To podejście jest obecnie ograniczone do jądra mapowania, które w tej sekcji są nazywane po prostu „jądrami”. Ta nowa funkcja obsługuje też tworzenie alokacji typu rs_allocation z poziomu skryptu. Teraz można zaimplementować cały algorytm wyłącznie w skrypcie, nawet jeśli wymagane jest wielokrotne uruchamianie jądra. Korzyści są dwojakie: bardziej czytelny kod, ponieważ implementacja algorytmu jest utrzymywana w jednym języku, oraz potencjalnie szybszy kod, ponieważ jest mniej przejść między językami Java i RenderScript w przypadku wielu uruchomień jądra.

W jednym źródle RenderScript jądra są zapisywane zgodnie z opisem w sekcji Zapisywanie jądra RenderScript. Następnie piszesz funkcję wywoływalną, która wywołuje rsForEach(), aby je uruchomić. Ten interfejs API przyjmuje funkcję jądra jako pierwszy parametr, a następnie alokacje wejściowe i wyjściowe. Podobny interfejs API rsForEachWithOptions() przyjmuje dodatkowy argument typu rs_script_call_t, który określa podzbiór elementów z alokacji wejściowych i wyjściowych do przetworzenia przez funkcję jądra.

Aby rozpocząć obliczenia RenderScript, wywołaj funkcję wywoływalną z Javy. Wykonaj czynności opisane w artykule Using RenderScript from Java Code (Używanie RenderScript z poziomu kodu w Javie). W kroku uruchamiania odpowiednich jądra wywołaj funkcję wywoływalną za pomocą invoke_function_name(), co spowoduje rozpoczęcie całego obliczenia, w tym uruchomienie jądra.

Alokacje są często potrzebne do zapisywania i przekazywania wyników pośrednich z jednego uruchomienia jądra do drugiego. Możesz je utworzyć za pomocą funkcji rsCreateAllocation(). Jedną z łatwych w użyciu form tego interfejsu API jest rsCreateAllocation_<T><W>(…), gdzie T to typ danych elementu, a W to szerokość wektora elementu. Interfejs API przyjmuje jako argumenty rozmiary w wymiarach X, Y i Z. W przypadku alokacji 1D lub 2D rozmiar wymiaru Y lub Z można pominąć. Na przykład rsCreateAllocation_uchar4(16384) tworzy jednowymiarową alokację 16 384 elementów, z których każdy jest typu uchar4.

Alokacje są zarządzane automatycznie przez system. Nie musisz ich jawnie zwalniać. Możesz jednak wywołać funkcję rsClearObject(rs_allocation* alloc), aby wskazać, że nie potrzebujesz już uchwytu alloc do podstawowej alokacji, dzięki czemu system będzie mógł jak najszybciej zwolnić zasoby.

Sekcja Pisanie jądra RenderScript zawiera przykładowe jądro, które odwraca obraz. Poniższy przykład rozszerza ten proces, aby zastosować więcej niż 1 efekt do obrazu za pomocą RenderScript z jednego źródła. Zawiera on inne jądro, greyscale, które przekształca kolorowy obraz 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. Alokacje zarówno dla danych wejściowych, jak 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 Javy lub Kotlina 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 obejmuje 2 uruchomienia jądra, można w pełni zaimplementować w języku RenderScript. Bez RenderScriptu z jednym źródłem musisz uruchomić oba jądra z kodu Java, oddzielając uruchomienia jądra od definicji jądra i utrudniając zrozumienie całego algorytmu. Kod RenderScript z jednego źródła jest nie tylko łatwiejszy do odczytania, ale też eliminuje przechodzenie między językiem Java a skryptem podczas uruchamiania jądra. Niektóre algorytmy iteracyjne mogą uruchamiać jądra setki razy, co powoduje znaczne obciążenie związane z takim przejściem.

Zmienne globalne skryptu

Zmienna globalna skryptu to zwykła zmienna globalna, która nie jest zmienną static w pliku skryptu (.rs). W przypadku globalnej zmiennej skryptu o nazwie var zdefiniowanej w pliku filename.rs w klasie ScriptC_filename będzie odzwierciedlona metoda get_var. Jeśli zmienna globalna nie jest równa const, dostępna będzie też metoda set_var.

Dana zmienna globalna skryptu ma 2 osobne wartości – Java i skrypt. Te wartości działają w następujący sposób:

  • Jeśli var ma w skrypcie statyczny inicjator, 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 skrypcie umożliwia odczyt i zapis jej wartości.
  • Metoda get_var odczytuje wartość Java.
  • Metoda set_var (jeśli istnieje) zapisuje wartość Java natychmiast, a wartość skryptu asynchronicznie.

UWAGA: oznacza to, że z wyjątkiem statycznego inicjatora w skrypcie wartości zapisane w zmiennej globalnej w skrypcie nie są widoczne dla Javy.

Szczegółowe informacje o jądrach redukcji

Redukcja to proces łączenia zbioru danych w jedną wartość. Jest to przydatny element podstawowy w programowaniu równoległym, który ma zastosowanie m.in. w tych przypadkach:

  • obliczanie sumy lub iloczynu wszystkich danych,
  • obliczanie operacji logicznych (and, or, xor) na wszystkich danych.
  • znajdowanie minimalnej lub maksymalnej wartości w danych,
  • wyszukiwanie konkretnej wartości lub współrzędnej konkretnej wartości w danych,

W Androidzie 7.0 (poziom interfejsu API 24) i nowszych RenderScript obsługuje jądra redukcji, aby umożliwić wydajne algorytmy redukcji napisane przez użytkownika. Możesz uruchamiać jądra redukcji na danych wejściowych z 1, 2 lub 3 wymiarami.

Powyższy przykład pokazuje proste jądro redukcji addint. Oto bardziej skomplikowane jądro redukcji findMinAndMax, które znajduje lokalizacje minimalnych i maksymalnych wartości long w jednowymiarowej tablicy Allocation:

#define LONG_MAX (long)((1UL << 63) - 1)
#define LONG_MIN (long)(1UL << 63)

#pragma rs reduce(findMinAndMax) \
  initializer(fMMInit) accumulator(fMMAccumulator) \
  combiner(fMMCombiner) outconverter(fMMOutConverter)

// Either a value and the location where it was found, or INITVAL.
typedef struct {
  long val;
  int idx;     // -1 indicates INITVAL
} IndexedVal;

typedef struct {
  IndexedVal min, max;
} MinAndMax;

// In discussion below, this initial value { { LONG_MAX, -1 }, { LONG_MIN, -1 } }
// is called INITVAL.
static void fMMInit(MinAndMax *accum) {
  accum->min.val = LONG_MAX;
  accum->min.idx = -1;
  accum->max.val = LONG_MIN;
  accum->max.idx = -1;
}

//----------------------------------------------------------------------
// In describing the behavior of the accumulator and combiner functions,
// it is helpful to describe hypothetical functions
//   IndexedVal min(IndexedVal a, IndexedVal b)
//   IndexedVal max(IndexedVal a, IndexedVal b)
//   MinAndMax  minmax(MinAndMax a, MinAndMax b)
//   MinAndMax  minmax(MinAndMax accum, IndexedVal val)
//
// The effect of
//   IndexedVal min(IndexedVal a, IndexedVal b)
// is to return the IndexedVal from among the two arguments
// whose val is lesser, except that when an IndexedVal
// has a negative index, that IndexedVal is never less than
// any other IndexedVal; therefore, if exactly one of the
// two arguments has a negative index, the min is the other
// argument. Like ordinary arithmetic min and max, this function
// is commutative and associative; that is,
//
//   min(A, B) == min(B, A)               // commutative
//   min(A, min(B, C)) == min((A, B), C)  // associative
//
// The effect of
//   IndexedVal max(IndexedVal a, IndexedVal b)
// is analogous (greater . . . never greater than).
//
// Then there is
//
//   MinAndMax minmax(MinAndMax a, MinAndMax b) {
//     return MinAndMax(min(a.min, b.min), max(a.max, b.max));
//   }
//
// Like ordinary arithmetic min and max, the above function
// is commutative and associative; that is:
//
//   minmax(A, B) == minmax(B, A)                  // commutative
//   minmax(A, minmax(B, C)) == minmax((A, B), C)  // associative
//
// Finally define
//
//   MinAndMax minmax(MinAndMax accum, IndexedVal val) {
//     return minmax(accum, MinAndMax(val, val));
//   }
//----------------------------------------------------------------------

// This function can be explained as doing:
//   *accum = minmax(*accum, IndexedVal(in, x))
//
// This function simply computes minimum and maximum values as if
// INITVAL.min were greater than any other minimum value and
// INITVAL.max were less than any other maximum value.  Note that if
// *accum is INITVAL, then this function sets
//   *accum = IndexedVal(in, x)
//
// After this function is called, both accum->min.idx and accum->max.idx
// will have nonnegative values:
// - x is always nonnegative, so if this function ever sets one of the
//   idx fields, it will set it to a nonnegative value
// - if one of the idx fields is negative, then the corresponding
//   val field must be LONG_MAX or LONG_MIN, so the function will always
//   set both the val and idx fields
static void fMMAccumulator(MinAndMax *accum, long in, int x) {
  IndexedVal me;
  me.val = in;
  me.idx = x;

  if (me.val <= accum->min.val)
    accum->min = me;
  if (me.val >= accum->max.val)
    accum->max = me;
}

// This function can be explained as doing:
//   *accum = minmax(*accum, *val)
//
// This function simply computes minimum and maximum values as if
// INITVAL.min were greater than any other minimum value and
// INITVAL.max were less than any other maximum value.  Note that if
// one of the two accumulator data items is INITVAL, then this
// function sets *accum to the other one.
static void fMMCombiner(MinAndMax *accum,
                        const MinAndMax *val) {
  if ((accum->min.idx < 0) || (val->min.val < accum->min.val))
    accum->min = val->min;
  if ((accum->max.idx < 0) || (val->max.val > accum->max.val))
    accum->max = val->max;
}

static void fMMOutConverter(int2 *result,
                            const MinAndMax *val) {
  result->x = val->min.idx;
  result->y = val->max.idx;
}

UWAGA: więcej przykładów jądra redukcji znajdziesz tutaj.

Aby uruchomić jądro redukcji, środowisko wykonawcze RenderScript tworzy co najmniej 1 zmienną o nazwie element danych akumulatora, która przechowuje stan procesu redukcji. Środowisko wykonawcze RenderScript wybiera liczbę elementów danych akumulatora w taki sposób, aby zmaksymalizować wydajność. Typ elementów danych akumulatora (accumType) jest określany przez funkcję akumulatora jądra – pierwszy argument tej funkcji jest wskaźnikiem do elementu danych akumulatora. Domyślnie każdy element danych akumulatora jest inicjowany na zero (jakby przez memset), ale możesz napisać funkcję inicjującą, aby wykonać inne działanie.

Przykład: w jądrze addint elementy danych akumulatora (typu int) są używane do dodawania wartości wejściowych. Nie ma funkcji inicjującej, więc każdy element danych akumulatora jest inicjowany na zero.

Przykład: w jądrze findMinAndMax elementy danych akumulatora (typu MinAndMax) służą do śledzenia minimalnych i maksymalnych wartości znalezionych do tej pory. Istnieje funkcja inicjująca, która ustawia te wartości odpowiednio na LONG_MAX i LONG_MIN, a lokalizacje tych wartości na -1, co oznacza, że wartości nie są faktycznie obecne w (pustej) części przetworzonych danych wejściowych.

RenderScript wywołuje funkcję akumulatora raz dla każdej współrzędnej w danych wejściowych. Zwykle funkcja powinna w jakiś sposób aktualizować element danych akumulatora zgodnie z danymi wejściowymi.

Przykład: w jądrze addint funkcja akumulatora dodaje wartość wejściowego elementu do elementu danych akumulatora.

Przykład: w jądrze findMinAndMax funkcja akumulatora sprawdza, czy wartość elementu wejściowego jest mniejsza lub równa minimalnej wartości zarejestrowanej w elemencie danych akumulatora lub większa lub równa maksymalnej wartości zarejestrowanej w elemencie danych akumulatora, i odpowiednio aktualizuje element danych akumulatora.

Po jednokrotnym wywołaniu funkcji akumulatora dla każdej współrzędnej w danych wejściowych RenderScript musi połączyć elementy danych akumulatora w jeden element danych akumulatora. W tym celu możesz napisać funkcję łączącą. Jeśli funkcja akumulatora ma pojedyncze dane wejściowe i nie ma argumentów specjalnych, nie musisz pisać funkcji łączącej. RenderScript użyje funkcji akumulatora do połączenia elementów danych akumulatora. (Jeśli to działanie domyślne nie jest tym, czego oczekujesz, możesz napisać funkcję łączącą).

Przykład: w jądrze addint nie ma funkcji łączącej, więc zostanie użyta funkcja akumulatora. Jest to prawidłowe zachowanie, ponieważ jeśli podzielimy zbiór wartości na 2 części i dodamy wartości w tych 2 częściach osobno, dodanie tych 2 sum da taki sam wynik jak dodanie całego zbioru.

Przykład: w jądrze findMinAndMax funkcja łącząca sprawdza, czy minimalna wartość zarejestrowana w „źródłowym” elemencie danych akumulatora *val jest mniejsza niż minimalna wartość zarejestrowana w „docelowym” elemencie danych akumulatora *accum, i odpowiednio aktualizuje *accum. Podobnie jest w przypadku wartości maksymalnej. Spowoduje to zaktualizowanie stanu *accum do stanu, który miałby, gdyby wszystkie wartości wejściowe zostały zgromadzone w *accum, a nie częściowo w *accum i częściowo w *val.

Po połączeniu wszystkich elementów danych akumulatora RenderScript określa wynik redukcji, który ma zostać zwrócony do Javy. W tym celu możesz napisać funkcję outconverter. Jeśli chcesz, aby ostateczna wartość połączonych elementów danych akumulatora była wynikiem redukcji, nie musisz pisać funkcji konwertera wyjściowego.

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 inicjuje wartość wyniku int2, aby przechowywać lokalizacje wartości minimalnych i maksymalnych wynikających z połączenia wszystkich elementów danych akumulatora.

Pisanie jądra redukcji

#pragma rs reduce definiuje jądro redukcji, określając jego nazwę oraz nazwy i role funkcji, które je tworzą. Wszystkie takie funkcje muszą być static. Jądro redukcji zawsze wymaga funkcji accumulator. Możesz pominąć niektóre lub wszystkie inne funkcje, w zależności od tego, co ma robić jądro.

#pragma rs reduce(kernelName) \
  initializer(initializerName) \
  accumulator(accumulatorName) \
  combiner(combinerName) \
  outconverter(outconverterName)

Znaczenie elementów w pliku #pragma:

  • reduce(kernelName) (obowiązkowy): określa, że definiowane jest jądro redukcji. Jądro zostanie uruchomione przez odzwierciedloną metodę Javy reduce_kernelName.
  • initializer(initializerName) (opcjonalnie): określa nazwę funkcji inicjującej dla tego jądra redukcji. Gdy uruchamiasz jądro, RenderScript wywołuje tę funkcję raz dla każdego elementu danych akumulatora. Funkcja musi być zdefiniowana w ten sposób:

    static void initializerName(accumType *accum) { … }

    accum to wskaźnik do elementu danych akumulatora, który ta funkcja ma zainicjować.

    Jeśli nie podasz funkcji inicjującej, RenderScript zainicjuje każdy element danych akumulatora na zero (jak w przypadku memset), zachowując się tak, jakby istniała funkcja inicjująca, która wygląda tak:

    static void initializerName(accumType *accum) {
      memset(accum, 0, sizeof(*accum));
    }
  • accumulator(accumulatorName) (obowiązkowy): określa nazwę funkcji akumulatora dla tego jądra redukcji. Gdy uruchamiasz 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 akumulatora 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 akumulatora, który ta funkcja ma zmodyfikować. in1 do inN to co najmniej 1 argument, który jest automatycznie wypełniany na podstawie danych wejściowych przekazanych do uruchomienia jądra, po jednym argumencie na dane wejściowe. Funkcja akumulatora może opcjonalnie przyjmować dowolne argumenty specjalne.

    Przykładem jądra z wieloma danymi wejściowymi jest dotProduct.

  • combiner(combinerName)

    (opcjonalnie): określa nazwę funkcji łączącej dla tego jądra redukcji. Gdy RenderScript wywoła funkcję akumulatora raz dla każdej współrzędnej w danych wejściowych, wywołuje tę funkcję tyle razy, ile jest to konieczne, aby połączyć wszystkie elementy danych akumulatora w jeden element. Funkcja musi być zdefiniowana w ten sposób:

    static void combinerName(accumType *accum, const accumType *other) { … }

    accum to wskaźnik do „docelowego” elementu danych akumulatora, który ta funkcja ma zmodyfikować. other to wskaźnik do „źródłowego” elementu danych akumulatora, który ta funkcja ma „połączyć” z *accum.

    UWAGA: możliwe, że zmienna *accum, *other lub obie zostały zainicjowane, ale nigdy nie zostały przekazane do funkcji akumulatora. Oznacza to, że jedna lub obie zmienne nigdy nie zostały zaktualizowane zgodnie z danymi wejściowymi. Na przykład w jądrze findMinAndMax funkcja łączenia fMMCombiner wyraźnie sprawdza, czy występuje idx < 0, ponieważ wskazuje to element danych akumulatora, którego wartość to INITVAL.

    Jeśli nie podasz funkcji łączącej, RenderScript użyje w jej miejsce funkcji akumulatora, zachowując się tak, jakby istniała funkcja łącząca, która wygląda tak:

    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 wejście, jeśli typ danych wejściowych nie jest taki sam jak typ danych akumulatora lub jeśli funkcja akumulatora przyjmuje co najmniej 1 argument specjalny.

  • outconverter(outconverterName) (opcjonalnie): określa nazwę funkcji konwertera wyjściowego dla tego jądra redukcji. Gdy RenderScript połączy wszystkie elementy danych akumulatora, wywołuje tę funkcję, aby określić wynik redukcji, który ma zostać zwrócony do Javy. Funkcja musi być zdefiniowana w ten sposób:

    static void outconverterName(resultType *result, const accumType *accum) { … }

    result to wskaźnik do elementu danych wyniku (przydzielonego, ale nie zainicjowanego przez środowisko wykonawcze RenderScript), który ta funkcja ma zainicjować z wynikiem 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 akumulatora obliczonego przez funkcję łączenia.

    Jeśli nie podasz funkcji outconverter, RenderScript skopiuje końcowy element danych akumulatora do wynikowego elementu danych, zachowując się tak, jakby istniała funkcja outconverter, 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, funkcja konwertera wyjściowego jest obowiązkowa.

Pamiętaj, że jądro ma typy danych wejściowych, typ elementu danych akumulatora i typ wyniku, z których żaden nie musi być taki sam. Na przykład w jądrze findMinAndMax typ danych wejściowych long, typ elementu danych akumulatora MinAndMax i typ wyniku int2 są różne.

Czego nie możesz zakładać?

Nie należy polegać na liczbie elementów danych akumulatora utworzonych przez RenderScript dla danego uruchomienia jądra. Nie ma gwarancji, że 2 uruchomienia tego samego jądra z tymi samymi danymi wejściowymi utworzą taką samą liczbę elementów danych akumulatora.

Nie należy polegać na kolejności, w jakiej RenderScript wywołuje funkcje inicjatora, akumulatora i kombinatora. Może nawet wywołać niektóre z nich równolegle. Nie ma gwarancji, że 2 uruchomienia tego samego jądra z tymi samymi danymi wejściowymi będą przebiegać w tej samej kolejności. Jedyną gwarancją jest to, że tylko funkcja inicjująca będzie widzieć niezainicjowany element danych akumulatora. Przykład:

  • Nie ma gwarancji, że wszystkie elementy danych akumulatora zostaną zainicjowane przed wywołaniem funkcji akumulatora, chociaż zostanie ona wywołana tylko w przypadku zainicjowanego elementu danych akumulatora.
  • Nie ma gwarancji co do kolejności, w jakiej elementy wejściowe są przekazywane do funkcji akumulatora.
  • Nie ma gwarancji, że funkcja akumulatora została wywołana dla wszystkich elementów wejściowych przed wywołaniem funkcji łączącej.

Jedną z konsekwencji 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 wykonywać jądro na wiele różnych sposobów, dlatego musisz przestrzegać pewnych zasad, aby jądro działało zgodnie z oczekiwaniami. Jeśli nie będziesz przestrzegać tych zasad, możesz uzyskać nieprawidłowe wyniki, niedeterministyczne działanie lub błędy środowiska wykonawczego.

Poniższe reguły często wskazują, że 2 elementy danych akumulatora muszą mieć tę samą wartość. Co to oznacza? To zależy od tego, co ma robić jądro. W przypadku redukcji matematycznej, takiej jak addint, zwykle „to samo” oznacza równość matematyczną. W przypadku wyszukiwania „wybierz dowolny”, takiego jak findMinAndMax („znajdź lokalizację minimalnych i maksymalnych wartości wejściowych”), gdzie może występować więcej niż 1 wystąpienie identycznych wartości wejściowych, wszystkie lokalizacje danej wartości wejściowej muszą być traktowane jako „takie same”. Możesz napisać podobne jądro, aby „znaleźć lokalizację najbardziej wysuniętych w lewo minimalnych i maksymalnych wartości wejściowych”, gdzie (powiedzmy) wartość minimalna w lokalizacji 100 jest preferowana nad identyczną wartością minimalną w lokalizacji 200. W przypadku tego jądra „to samo” oznaczałoby identyczną lokalizację, a nie tylko identyczną wartość, a funkcje akumulatora i łącznika musiałyby być inne niż w przypadku findMinAndMax.

Funkcja inicjująca musi utworzyć wartość tożsamości. Oznacza to, że jeśli I i A to elementy danych akumulatora zainicjowane przez funkcję inicjującą, a element I nigdy nie został przekazany do funkcji akumulatora (ale element A mógł zostać przekazany), to:
  • combinerName(&A, &I) musi opuścić A bez zmian
  • combinerName(&I, &A) musi pozostawić I takie samo jak A

Przykład: w jądrze addint element danych akumulatora jest inicjowany na zero. Funkcja łącząca tego jądra wykonuje dodawanie. Zero jest wartością tożsamości dla dodawania.

Przykład: w jądrze findMinAndMax element danych akumulatora jest inicjowany na wartość INITVAL.

  • fMMCombiner(&A, &I) nie zmienia A, ponieważ I to INITVAL.
  • fMMCombiner(&I, &A) ustawia I na A, ponieważ I to INITVAL.

Dlatego INITVAL jest wartością tożsamości.

Funkcja łącząca musi być przemienna. Oznacza to, że jeśli A i B to elementy danych akumulatora zainicjowane przez funkcję inicjującą, które mogły zostać przekazane do funkcji akumulatora co najmniej 0 razy, funkcja combinerName(&A, &B) musi ustawić A na tę samą wartość, którą funkcja combinerName(&B, &A) ustawia dla B.

Przykład: w funkcji jądra addint funkcja łącząca dodaje wartości 2 elementów danych akumulatora. Dodawanie jest przemienne.

Przykład: w jądrze findMinAndMax fMMCombiner(&A, &B) jest tym samym co A = minmax(A, B), a minmax jest przemienne, więc fMMCombiner też jest przemienne.

Funkcja łącząca musi być łączna. Oznacza to, że jeśli A, B i C są elementami danych zasobnika zainicjowanymi przez funkcję inicjującą, które mogły zostać przekazane do funkcji zasobnika co najmniej 0 razy, te 2 sekwencje kodu muszą ustawić A na tę samą wartość:

  • combinerName(&A, &B);
    combinerName(&A, &C);
  • combinerName(&B, &C);
    combinerName(&A, &B);

Przykład: w jądrze addint funkcja łącząca dodaje 2 wartości elementów danych akumulatora:

  • 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 łączne, więc funkcja łącząca również.

Przykład: w jądrze findMinAndMax

fMMCombiner(&A, &B)
jest taki sam jak
A = minmax(A, B)
Oto 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 łączna, więc fMMCombiner też.

Funkcja akumulatora i funkcja łącząca muszą razem być zgodne z podstawową regułą składania. Oznacza to, że jeśli A i B są elementami danych akumulatora, A został zainicjowany przez funkcję inicjującą i mógł zostać przekazany do funkcji akumulatora co najmniej 0 razy, B nie został zainicjowany, a args jest listą argumentów wejściowych i argumentów specjalnych dla konkretnego wywołania funkcji akumulatora, te 2 sekwencje kodu muszą ustawić A 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:

  • Oświadczenie 1 jest takie samo jak A += V
  • Oświadczenie 2 jest takie samo jak B = 0
  • Instrukcja 3 jest taka sama jak B += V, czyli taka sama jak B = V
  • Instrukcja 4 jest taka sama jak A += B, która jest taka sama jak A += V

Instrukcje 1 i 4 ustawiają A na tę samą wartość, więc to jądro jest zgodne z podstawową regułą zwijania.

Przykład: w jądrze findMinAndMax dla wartości wejściowej V o współrzędnej X:

  • Oświadczenie 1 jest takie samo jak A = minmax(A, IndexedVal(V, X))
  • Oświadczenie 2 jest takie samo jak B = INITVAL
  • Instrukcja 3 jest taka sama jak
    B = minmax(B, IndexedVal(V, X))
    co, ponieważ B jest wartością początkową, jest tym samym co
    B = IndexedVal(V, X)
  • Oświadczenie 4 jest takie samo jak
    A = minmax(A, B)
    , co jest równe
    A = minmax(A, IndexedVal(V, X))

Instrukcje 1 i 4 ustawiają A na tę samą wartość, więc to jądro jest zgodne z podstawową regułą zwijania.

Wywoływanie jądra redukcji z kodu w języku Java

W przypadku jądra redukcji o nazwie kernelName zdefiniowanego w pliku filename.rs w klasie ScriptC_filename odzwierciedlone są 3 metody:

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 jądra 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 1 argument wejściowy Allocation dla każdego argumentu wejściowego w funkcji akumulatora jądra. Środowisko wykonawcze RenderScript sprawdza, czy wszystkie wejściowe obiekty Allocation mają te same wymiary i czy typ Element każdego z nich odpowiada odpowiedniemu argumentowi wejściowemu prototypu funkcji akumulatora. Jeśli któryś z tych testów się nie powiedzie, RenderScript zgłasza wyjątek. Jądro jest wykonywane na każdej współrzędnej w tych wymiarach.

Metoda 2 jest taka sama jak metoda 1, ale przyjmuje dodatkowy argument sc, którego można użyć do ograniczenia wykonywania jądra do podzbioru współrzędnych.

Metoda 3 jest taka sama jak metoda 1, z tą różnicą, że zamiast danych wejściowych Allocation przyjmuje dane wejściowe tablicy Java. Jest to wygodne rozwiązanie, które pozwala uniknąć pisania kodu w celu jawnego utworzenia alokacji i skopiowania do niej danych z tablicy Java. Jednak użycie metody 3 zamiast metody 1 nie zwiększa wydajności kodu. W przypadku każdej tablicy wejściowej metoda 3 tworzy tymczasową jednowymiarową alokację z odpowiednim typem Element i włączoną opcją setAutoPadding(boolean), a następnie kopiuje tablicę do alokacji, tak jakby używała odpowiedniej metody copyFrom() klasy 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 z tą samą tablicą lub z różnymi tablicami o tych samych wymiarach i typie elementu, możesz zwiększyć wydajność, samodzielnie tworząc, wypełniając i ponownie wykorzystując alokacje, zamiast używać metody 3.

javaFutureType, zwracany typ odzwierciedlonych metod redukcji, to odzwierciedlona statyczna klasa zagnieżdżona w klasie ScriptC_filename. Reprezentuje przyszły wynik uruchomienia jądra redukcji. Aby uzyskać rzeczywisty wynik uruchomienia, wywołaj metodę get() tej klasy, która zwraca wartość typu javaResultType. get() jest synchroniczny.

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 (skalar, wektor lub tablica), javaResultType jest bezpośrednio odpowiadającym mu typem Javy. Jeśli resultType jest typem bez znaku i istnieje większy typ ze znakiem w języku Java, javaResultType jest tym większym typem ze znakiem 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 lub int[15], javaResultType to int, Int2 lub int[]. Wszystkie wartości resultType mogą być reprezentowane przez javaResultType.
  • Jeśli resultType to uint, uint2 lub uint[15], javaResultType to long, Long2 lub long[]. Wszystkie wartości resultType mogą być reprezentowane przez javaResultType.
  • Jeśli resultType to ulong, ulong2 lub ulong[15], javaResultType to long, Long2 lub long[]. Niektóre wartości resultType nie mogą być reprezentowane przez javaResultType.

javaFutureType to typ wyniku przyszłego odpowiadający typowi resultType funkcji outconverter.

  • Jeśli resultType nie jest typem tablicowym, javaFutureType to result_resultType.
  • Jeśli resultType jest tablicą o długości Count z elementami typu memberType, 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 jest typem obiektu (w tym typem tablicy), każde wywołanie funkcji javaFutureType.get() w tej samej instancji zwróci ten sam obiekt.

Jeśli javaResultType nie może reprezentować wszystkich wartości typu resultType, a jądro redukcji generuje niemożliwą do reprezentowania wartość, javaFutureType.get() zgłasza wyjątek.

Metoda 3 i devecSiInXType

devecSiInXType to typ Java odpowiadający typowi inXType odpowiedniego argumentu funkcji akumulatora. Jeśli inXType nie jest typem bez znaku ani typem wektora, devecSiInXType jest bezpośrednio odpowiadającym mu typem Javy. Jeśli inXType jest typem skalarnym bez znaku, devecSiInXType jest typem Javy bezpośrednio odpowiadającym typowi skalarnemu ze znakiem o tym samym rozmiarze. Jeśli inXType jest typem wektora ze znakiem, devecSiInXType jest typem Javy bezpośrednio odpowiadającym typowi komponentu wektora. Jeśli inXType jest typem wektora bez znaku, devecSiInXType jest typem Javy bezpośrednio odpowiadającym typowi skalarnemu ze znakiem o tym samym rozmiarze co typ komponentu wektora. Przykład:

  • Jeśli inXType to int, devecSiInXType to int.
  • Jeśli inXType to int2, devecSiInXType to int. Tablica jest reprezentacją spłaszczoną: ma 2 razy więcej elementów skalarnych niż alokacja ma 2-składnikowych elementów wektorowych. Działa to tak samo jak metody copyFrom() w Allocation.
  • Jeśli inXType to uint, deviceSiInXType to int. Wartość ze znakiem w tablicy Java jest interpretowana jako wartość bez znaku o tym samym wzorcu bitowym w alokacji. Działa to tak samo jak metody copyFrom() w Allocation.
  • Jeśli inXType to uint2, deviceSiInXType to int. Jest to połączenie sposobu obsługi int2 i uint: tablica jest spłaszczoną reprezentacją, a wartości ze znakiem tablicy Java są interpretowane jako wartości elementów bez znaku RenderScript.

Pamiętaj, że w przypadku metody 3 typy danych wejściowych są obsługiwane inaczej niż typy wyników:

  • Dane wejściowe wektora skryptu są spłaszczane po stronie Javy, ale wynik wektora skryptu nie jest spłaszczany.
  • Dane wejściowe bez znaku skryptu są reprezentowane jako dane wejściowe ze znakiem o tym samym rozmiarze po stronie Javy, natomiast wynik bez znaku skryptu jest reprezentowany jako rozszerzony typ ze znakiem po stronie Javy (z wyjątkiem przypadku ulong).

Więcej przykładowych jądra 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 korzystać z interfejsów API opisanych na tej stronie.