Omówienie RenderScript

RenderScript to platforma do wykonywania na Androidzie zadań wymagających dużej mocy obliczeniowej z wysoką wydajnością. RenderScript jest przeznaczony głównie do obliczeń równoległych na danych, ale może być też przydatny w przypadku obciążeń szeregowych. Środowisko wykonawcze RenderScript równolegle przetwarza zadania na procesorach dostępnych na urządzeniu, takich jak wielordzeniowe procesory CPU 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 przypadku aplikacji, które przetwarzają obrazy, wykonują obliczenia fotograficzne lub rozpoznają obrazy.

Aby zacząć korzystać z RenderScript, musisz poznać 2 główne pojęcia:

  • Język jest oparty na C99 i służy do pisania kodu obliczeniowego o wysokiej wydajności. W artykule Pisanie jądra RenderScript opisujemy, jak używać tego języka do pisania jąder obliczeniowych.
  • Interfejs Control API służy do zarządzania okresem istnienia zasobów RenderScript i sterowania wykonywaniem jądra. Jest dostępny w 3 językach: Java, C++ w Androidzie NDK i sam język jądra wywodzący się z C99. Korzystanie z RenderScriptu w kodzie JavaRenderScript 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 nazywa się skryptem. Każdy skrypt zawiera własny zestaw jąder, 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 Java odzwierciedlonych w tym skrypcie. Pamiętaj, że plik .rs musi być częścią pakietu aplikacji, a nie projektu biblioteki.
  • Zero lub więcej funkcji, które można wywołać. Funkcja wywoływalna to jednowątkowa funkcja RenderScript, którą możesz wywołać z kodu Java z dowolnymi argumentami. Często są one przydatne podczas wstępnej konfiguracji lub obliczeń seryjnych w ramach większego potoku przetwarzania.
  • Zero lub więcej globalnych zmiennych skryptu. Globalna zmienna skryptu jest podobna do zmiennej globalnej w języku C. Do zmiennych globalnych skryptu można uzyskać dostęp z kodu w Javie. Są one często używane do przekazywania parametrów do jąder 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 środowisko wykonawcze RenderScript może wykonywać równolegle na zbiorze danych. Istnieją 2 rodzaje jąder obliczeniowych: jądra mapowania (nazywane też jądrami foreach) i jądra 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żdego współrzędnego w tych wymiarach. Zazwyczaj (ale nie wyłącznie) służy do przekształcania kolekcji danych wejściowych Allocations w dane wyjściowe Allocation po jednym Element na raz.

    • 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;
      }

      Pod wieloma względami 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ą, którą można wywołać. Argument in jest wypełniany automatycznie na podstawie danych wejściowych Allocation przekazywanych do uruchomienia jądra. Argumenty xy zostały omówione poniżej. Wartość zwrócona przez jądro jest automatycznie zapisywana w odpowiednim miejscu w danych wyjściowych Allocation. Domyślnie ten kernel jest uruchamiany na całym wejściu Allocation, a funkcja kernela jest wykonywana raz na każdy element Element w Allocation.

      Jądro mapowania może mieć co najmniej 1 wejście Allocations, 1 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 są zgodne z prototypem jądra. Jeśli którekolwiek z tych sprawdzeń się nie powiedzie, RenderScript zgłosi wyjątek.

      UWAGA: przed Androidem 6.0 (poziom interfejsu API 23) jądro mapowania nie może mieć więcej niż 1 wejścia Allocation.

      Jeśli potrzebujesz więcej danych wejściowych lub wyjściowych Allocations niż ma jądro, te obiekty powinny być powiązane ze rs_allocation zmiennymi globalnymi skrypturs_allocation i dostępne z funkcji jądra lub funkcji wywoływanej za pomocą rsGetElementAt_type() lub rsSetElementAt_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ściowychAllocations o tych samych wymiarach. Domyślnie funkcja akumulatora jest wykonywana raz dla każdego współrzędnego w tych wymiarach. Jest ona zwykle (ale nie wyłącznie) używana do „redukowania” kolekcji danych wejściowych 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 podanie 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ż zawierać inne funkcje w zależności od tego, co ma robić.

      Funkcja akumulatora jądra redukcji musi zwracać void i mieć co najmniej 2 argumenty. Pierwszy argument (w tym przykładzie accum) to wskaźnik elementu danych akumulatora, a drugi (w tym przykładzie val) jest wypełniany automatycznie na podstawie danych wejściowych Allocation przekazywanych do uruchomienia jądra. Element danych akumulatora jest tworzony przez środowisko wykonawcze RenderScript. Domyślnie jest on inicjowany wartością zero. Domyślnie ten kernel jest uruchamiany na całym wejściuAllocation, a funkcja akumulatora jest wykonywana raz na każdy elementElementAllocation. Domyślnie końcowa wartość elementu danych akumulatora jest traktowana jako wynik redukcji i zwracana do Javy. Środowisko wykonawcze RenderScript sprawdza, czy typ Element wejściowego obiektu Allocation jest zgodny z prototypem funkcji akumulatora. Jeśli nie jest, 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 redukcji są obsługiwane w Androidzie 7.0 (poziom interfejsu API 24) i nowszych wersjach.

    Funkcja jądra mapowania lub funkcja akumulatora jądra redukcji może uzyskać dostęp do współrzędnych bieżącego wykonania za pomocą argumentów specjalnych x, yz, 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 potrzebny rodzinie interfejsów API środowiska wykonawczego, które służą do wysyłania zapytań o określone właściwości bieżącego wykonania, np. rsGetDimX. (Argument context jest dostępny w Androidzie 6.0 (poziom interfejsu API 23) i nowszych wersjach).

  • Opcjonalna funkcja init(). Funkcja init() to specjalny rodzaj funkcji wywoływanej, którą RenderScript uruchamia, gdy skrypt jest po raz pierwszy tworzony. 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, z tym że nie można uzyskać do niego dostępu z kodu Java. Funkcja statyczna to standardowa funkcja C, którą można wywoływać z dowolnej funkcji jądra lub funkcji wywoływanej w skrypcie, ale nie jest ona udostępniana w interfejsie Java API. Jeśli do globalnego skryptu lub funkcji nie trzeba uzyskiwać dostępu z kodu Java, zdecydowanie zalecamy zadeklarowanie go jako static.

Ustawianie precyzji liczb zmiennoprzecinkowych

W skrypcie możesz kontrolować wymagany poziom precyzji zmiennoprzecinkowej. 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 liczb zmiennoprzecinkowych:

  • #pragma rs_fp_full (domyślne, jeśli nie określono żadnej wartości): 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ę. W tym trybie włącza się zerowanie 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 włącza wszystkie funkcje w rs_fp_relaxed oraz następujące funkcje:
    • Operacje, których wynikiem jest -0,0, mogą zamiast tego zwracać +0,0.
    • Operacje na wartościach INF i NAN są nieokreślone.

Większość aplikacji może używać rs_fp_relaxed bez żadnych skutków ubocznych. W przypadku niektórych architektur może to być bardzo korzystne ze względu na dodatkowe optymalizacje dostępne tylko przy mniejszej precyzji (np. instrukcje procesora SIMD).

Dostęp do interfejsów RenderScript API z poziomu Javy

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

  • android.renderscript – interfejsy API w tym pakiecie klas są dostępne na urządzeniach z Androidem 3.0 (poziom API 11) lub nowszym.
  • android.support.v8.renderscript – interfejsy API w tym pakiecie są dostępne w bibliotece pomocy, która umożliwia korzystanie z nich na urządzeniach z Androidem 2.3 (API na poziomie 9) lub nowszym.

Oto kompromisy:

  • Jeśli używasz interfejsów API biblioteki pomocy, część aplikacji RenderScript będzie zgodna z urządzeniami z Androidem 2.3 (poziom API 9) i nowszymi, niezależnie od używanych funkcji RenderScript. 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, rozmiar plików APK będzie (prawdopodobnie znacznie) większy niż w przypadku korzystania z natywnych interfejsów API (android.renderscript).

Korzystanie z interfejsów API biblioteki pomocy RenderScript

Aby korzystać z interfejsów API RenderScript z biblioteki pomocy, musisz skonfigurować środowisko programistyczne, aby mieć 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
  • Narzędzia do kompilacji pakietu Android SDK w wersji 18.1.0 lub nowszej

Pamiętaj, że od wersji 24.0.0 pakietu Android SDK Build-tools Android 2.2 (poziom interfejsu API 8) nie jest już obsługiwany.

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

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

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

      Groovy

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

      Kotlin

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

      Ustawienia wymienione powyżej kontrolują określone zachowania w procesie kompilacji Androida:

      • renderscriptTargetApi – określa wersję kodu bajtowego, która ma zostać wygenerowana. Zalecamy ustawienie tej wartości na najniższy poziom interfejsu API, który zapewnia wszystkie używane przez Ciebie funkcje, a wartość renderscriptSupportModeEnabled ustaw na true. Prawidłowe wartości tego ustawienia to dowolna liczba całkowita od 11 do najnowszej wersji interfejsu API. Jeśli minimalna wersja pakietu SDK określona w pliku manifestu aplikacji ma inną wartość, jest ona ignorowana, a do ustawienia minimalnej wersji pakietu SDK używana jest wartość docelowa w pliku kompilacji.
      • renderscriptSupportModeEnabled – określa, że wygenerowany kod bajtowy ma być przywracany do zgodnej wersji, jeśli urządzenie, na którym jest uruchamiany, nie obsługuje wersji docelowej.
  3. W klasach aplikacji, które używają RenderScript, dodaj importowanie klas biblioteki pomocy:

    Kotlin

    import android.support.v8.renderscript.*

    Java

    import android.support.v8.renderscript.*;

Używanie RenderScript z kodu Java lub Kotlin

Korzystanie z RenderScriptu w kodzie Java lub Kotlin opiera się na klasach interfejsu API znajdujących się w pakiecie android.renderscript lub android.support.v8.renderscript. Większość aplikacji działa według tego samego podstawowego wzorca użycia:

  1. Inicjowanie kontekstu RenderScript. Kontekst RenderScript utworzony za pomocą create(Context) zapewnia możliwość korzystania z RenderScript i udostępnia obiekt do kontrolowania czasu życia wszystkich kolejnych obiektów RenderScript. Tworzenie kontekstu należy traktować jako potencjalnie długotrwałą operację, ponieważ może ona tworzyć zasoby na różnych elementach sprzętu. W miarę możliwości nie powinna ona znajdować się na ścieżce krytycznej aplikacji. Zwykle aplikacja ma w danym momencie tylko jeden kontekst RenderScript.
  2. Utwórz co najmniej 1 Allocation, który ma być przekazywany do skryptu. Allocation to obiekt RenderScript, który zapewnia miejsce na stałą ilość danych. Jądra w skryptach przyjmują jako dane wejściowe i wyjściowe obiekty Allocation, a do obiektów Allocation można uzyskać dostęp w jądrach za pomocą rsGetElementAt_type()rsSetElementAt_type(), gdy są powiązane jako zmienne globalne skryptu. 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 wszystkie niezbędne skrypty. Podczas korzystania z RenderScriptu masz do dyspozycji 2 rodzaje skryptów:
    • ScriptC: są to skrypty zdefiniowane przez użytkownika, opisane w sekcji Pisanie jądra RenderScript powyżej. 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 powyższy kernel mapowania znajduje się w invert.rs, a kontekst RenderScript znajduje się 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 wykonywania typowych operacji, takich jak rozmycie Gaussa, splot i mieszanie obrazów. Więcej informacji znajdziesz w podklasach ScriptIntrinsic.
  4. Wypełnij sekcję Alokacje danymi. Z wyjątkiem alokacji utworzonych za pomocą createFromBitmap(), alokacja jest wypełniana pustymi danymi przy pierwszym utworzeniu. Aby wypełnić pole Przydział, użyj jednej z metod „kopiowania” w Allocation. Metody „copy” są synchroniczne.
  5. Ustaw wszelkie niezbędne zmienne globalne skryptu. Zmienne globalne możesz ustawiać za pomocą metod w tej samej klasie ScriptC_filename o nazwie set_globalname. Aby na przykład 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 setasynchroniczne.
  6. Uruchamiaj odpowiednie jądra i funkcje, które można wywoływać.

    Metody uruchamiania danego jądra są odzwierciedlone w tej samej klasie ScriptC_filename z metodami o nazwach forEach_mappingKernelName() lub reduce_reductionKernelName(). Te uruchomienia są asynchroniczne. W zależności od argumentów jądra metoda przyjmuje co najmniej jedną alokację, z których wszystkie muszą mieć te same wymiary. Domyślnie jądro jest wykonywane dla każdego współrzędnego w tych wymiarach. Aby wykonać jądro dla podzbioru tych współrzędnych, przekaż odpowiedni argument Script.LaunchOptions jako ostatni argument do metody forEach lub reduce.

    Uruchamiaj funkcje wywoływane za pomocą metod invoke_functionName odzwierciedlonych w tej samej klasie ScriptC_filename. Te uruchomienia są asynchroniczne.

  7. Pobieranie danych z obiektów Allocation i obiektów javaFutureType. Aby uzyskać dostęp do danych z Allocation w kodzie Java, musisz skopiować te dane z powrotem do Javy za pomocą jednej z metod „copy” w Allocation. Aby uzyskać wynik jądra redukcji, musisz użyć metody javaFutureType.get(). Metody „copy” i get()synchroniczne.
  8. Zamyka kontekst RenderScript. Kontekst RenderScript można zniszczyć za pomocą funkcji destroy() lub zezwalając na usunięcie obiektu kontekstu RenderScript przez moduł odśmiecania pamięci. Spowoduje to, że dalsze używanie dowolnego obiektu należącego do tego kontekstu będzie powodować wyjątek.

Model wykonywania asynchronicznego

Metody forEach, invoke, reduceset są asynchroniczne – każda z nich może wrócić do Javy przed wykonaniem żądanej czynności. Poszczególne działania są jednak serializowane w kolejności, w jakiej są uruchamiane.

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

Odbite klasy javaFutureType udostępniają metodę get() do uzyskiwania wyniku redukcji. get() jest synchroniczne i jest serializowane w odniesieniu do redukcji (która jest asynchroniczna).

RenderScript z jednego źródła

W Androidzie 7.0 (interfejs API na poziomie 24) wprowadzono 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 mapowania jąder, które w tej sekcji dla zwięzłości nazywamy po prostu „jądrami”. Ta nowa funkcja umożliwia też tworzenie w skrypcie przydziałów typu rs_allocation. Można teraz zaimplementować cały algorytm wyłącznie w skrypcie, nawet jeśli wymaga to wielu uruchomień jądra. Ma to 2 zalety: bardziej czytelny kod, ponieważ implementacja algorytmu jest utrzymywana w jednym języku, oraz potencjalnie szybszy kod ze względu na mniejszą liczbę przejść między Javą a RenderScriptem w przypadku wielu uruchomień jądra.

W przypadku RenderScriptu z jednego źródła jądra pisze się w sposób opisany w artykule Pisanie jądra RenderScriptu. Następnie piszesz funkcję, którą można wywołać, aby uruchomić te funkcje za pomocą wywołania rsForEach(). 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 przydziałów wejściowych i wyjściowych, które funkcja jądra ma przetworzyć.

Aby rozpocząć obliczenia RenderScript, wywołaj funkcję wywoływalną z Javy. Wykonaj czynności opisane w artykule Using RenderScript from Java Code (Korzystanie z RenderScript z poziomu kodu Java). W kroku uruchom odpowiednie jądra wywołaj funkcję, którą można wywołać, za pomocą invoke_function_name(). Spowoduje to rozpoczęcie całego obliczenia, w tym uruchomienie jąder.

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 rozmiary w wymiarach X, Y i Z jako argumenty. W przypadku przydziałów 1D lub 2D można pominąć rozmiar wymiaru Y lub Z. Na przykład rsCreateAllocation_uchar4(16384) tworzy jednowymiarową alokację 16384 elementów, z których każdy jest typu uchar4.

System automatycznie zarządza przydziałami. Nie musisz ich wyraźnie zwalniać ani uwalniać. Możesz jednak wywołać funkcję rsClearObject(rs_allocation* alloc), aby wskazać, że nie potrzebujesz już uchwytu alloc do bazowej alokacji, dzięki czemu system może jak najszybciej zwolnić zasoby.

Sekcja Pisanie jądra RenderScript zawiera przykładowe jądro, które odwraca obraz. Poniższy przykład pokazuje, jak zastosować do obrazu więcej niż 1 efekt za pomocą RenderScriptu z jednego źródła. Zawiera on kolejny kernel, greyscale, który przekształca kolorowy obraz w czarno-biały. Funkcja wywoływalna process() stosuje kolejno te 2 jądra do obrazu wejściowego i generuje obraz wyjściowy. Przydziały zarówno 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ć w Javie lub Kotlinie 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 obejmujący 2 uruchomienia jądra może być w całości zaimplementowany w języku RenderScript. Bez Single-Source RenderScript musiałbyś uruchamiać oba jądra z kodu Java, oddzielając uruchamianie jądra od jego definicji, co utrudniałoby zrozumienie całego algorytmu. Kod Single-Source RenderScript jest nie tylko łatwiejszy do odczytania, ale też eliminuje przechodzenie między Javą a skryptem podczas uruchamiania jądra. Niektóre algorytmy iteracyjne mogą uruchamiać jądra setki razy, co znacznie zwiększa obciążenie związane z takimi przejściami.

Globalne skrypty

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

Dana globalna zmienna skryptu ma 2 oddzielne wartości: wartość Java i wartość skryptu. Wartości te działają w ten sposób:

  • Jeśli var ma w skrypcie statyczny inicjator, określa on wartość początkową zmiennej 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 odczytywanie i zapisywanie jej wartości.
  • Metoda get_var odczytuje wartość Java.
  • Metoda set_var (jeśli istnieje) natychmiast zapisuje wartość Java i zapisuje wartość skryptu asynchronicznie.

UWAGA: oznacza to, że z wyjątkiem statycznego inicjatora w skrypcie wartości zapisane w obiekcie globalnym ze skryptu 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 w programowaniu równoległym, który ma zastosowanie w takich przypadkach jak:

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

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

Przykład powyżej pokazuje prosty kernel redukcji addint. Oto bardziej skomplikowane jądro redukcji findMinAndMax, które znajduje lokalizacje minimalnych i maksymalnych wartości long w 1-wymiarowej 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ładowych funkcji jądra redukcji znajdziesz tutaj.

Aby uruchomić jądro redukcji, środowisko wykonawcze RenderScript tworzy co najmniej jedną zmienną o nazwie elementy 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 – pierwszym argumentem tej funkcji jest wskaźnik do elementu danych akumulatora. Domyślnie każdy element danych akumulatora jest inicjowany wartością zero (tak jak w przypadku memset); możesz jednak napisać funkcję inicjującą, aby wykonać inne działanie.

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

Przykład: w jądrze findMinAndMax elementy danych akumulatora (typu MinAndMax) służą do śledzenia znalezionych do tej pory wartości minimalnej i maksymalnej. Istnieje funkcja inicjująca, która ustawia te wartości odpowiednio na LONG_MAXLONG_MIN, a także ustawia lokalizacje tych wartości na -1, co oznacza, że wartości te nie występują w (pustej) części danych wejściowych, która została przetworzona.

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

Przykład: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 wartości minimalnej zarejestrowanej w elemencie danych akumulatora lub większa lub równa wartości maksymalnej zarejestrowanej w elemencie danych akumulatora, i odpowiednio aktualizuje element danych akumulatora.

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

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

Przykład: w jądrze findMinAndMax funkcja łącząca sprawdza, czy wartość minimalna zarejestrowana w elemencie danych akumulatora „source” *val jest mniejsza od wartości minimalnej zarejestrowanej w elemencie danych akumulatora „destination” *accum, i odpowiednio aktualizuje element *accum. Podobnie działa w przypadku wartości maksymalnej. Ta funkcja aktualizuje *accum do stanu, w jakim byłby, gdyby wszystkie wartości wejściowe zostały zgromadzone w *accum, a nie niektóre w *accum, a inne w *val.

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

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 minimalnej i maksymalnej uzyskane w wyniku połączenia wszystkich elementów danych akumulatora.

Pisanie jądra redukcji

#pragma rs reduce definiuje jądro redukcji, podają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 obiekcie #pragma jest następujące:

  • reduce(kernelName) (obowiązkowy): określa, że definiowane jest jądro redukcji. Odbita metoda Java reduce_kernelName uruchomi jądro.
  • initializer(initializerName) (opcjonalnie): określa nazwę funkcji inicjującej dla tego jądra redukcji. Po uruchomieniu jądra 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 elementu danych akumulatora, który ta funkcja ma zainicjować.

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

    static void initializerName(accumType *accum) {
      memset(accum, 0, sizeof(*accum));
    }
  • accumulator(accumulatorName)(obowiązkowe): określa nazwę funkcji akumulatora dla tego jądra redukcji. Po uruchomieniu jądra RenderScript wywołuje tę funkcję raz dla każdego współrzędnego w danych wejściowych, aby zaktualizować element danych akumulatora w określony 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 to wskaźnik elementu danych akumulatora, który ta funkcja ma zmodyfikować. in1inN to co najmniej 1 argument, który jest automatycznie wypełniany na podstawie danych wejściowych przekazywanych do uruchomienia jądra, po jednym argumencie na dane wejściowe. Funkcja akumulatora może opcjonalnie przyjmować dowolne argumenty specjalne.

    Przykładowy kernel z wieloma danymi wejściowymi to dotProduct.

  • combiner(combinerName)

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

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

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

    UWAGA: możliwe, że zmienne *accum lub *other (albo obie) zostały zainicjowane, ale nigdy nie zostały przekazane do funkcji akumulatora, czyli nigdy nie zostały zaktualizowane na podstawie żadnych danych wejściowych. Na przykład w jądrze findMinAndMax funkcja łącząca fMMCombiner jawnie sprawdza 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 o takiej postaci:

    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 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 outconverter dla tego jądra redukcji. Gdy RenderScript połączy wszystkie elementy danych akumulatora, wywoła 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 elementu danych wyniku (przydzielonego, ale nie zainicjowanego przez środowisko wykonawcze RenderScript) do zainicjowania przez tę funkcję 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ę łączącą.

    Jeśli nie podasz funkcji outconverter, RenderScript skopiuje końcowy element danych akumulatora do elementu danych wyniku, 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 outconverter jest obowiązkowa.

Pamiętaj, że jądro ma typy danych wejściowych, typ danych elementu 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 założyć?

Nie możesz polegać na liczbie elementów danych akumulatora utworzonych przez RenderScript w przypadku danego uruchomienia jądra. Nie ma gwarancji, że dwa uruchomienia tego samego jądra z tymi samymi danymi wejściowymi utworzą tę samą liczbę elementów danych akumulatora.

Nie możesz polegać na kolejności, w jakiej RenderScript wywołuje funkcje inicjujące, akumulujące i łączące. Może nawet wywoływać niektóre z nich równolegle. Nie ma gwarancji, że dwa uruchomienia tego samego jądra z tym samym wejściem będą przebiegać w tej samej kolejności. Jedyną gwarancją jest to, że tylko funkcja inicjująca będzie kiedykolwiek widzieć niezainicjowany element danych akumulatora. Na 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 tego jest to, że jądro findMinAndMax nie jest deterministyczne: jeśli dane wejściowe zawierają więcej niż jedno wystąpienie tej samej minimalnej lub maksymalnej wartości, nie można przewidzieć, które wystąpienie znajdzie jądro.

Co musisz zagwarantować?

System RenderScript może wykonywać jądro na wiele różnych sposobów, dlatego musisz przestrzegać określonych reguł, aby mieć pewność, że jądro działa zgodnie z Twoimi oczekiwaniami. Jeśli nie będziesz przestrzegać tych reguł, możesz uzyskać nieprawidłowe wyniki, nieokreślone zachowanie lub błędy w czasie działania.

W regułach poniżej często pojawia się stwierdzenie, ż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 operacji matematycznych, takich jak dodawanie, zwykle sensowne jest, aby „to samo” oznaczało równość matematyczną. W przypadku wyszukiwania typu „wybierz dowolne”, takiego jak findMinAndMax („znajdź lokalizację minimalnej i maksymalnej wartości wejściowej”), w którym może wystąpić więcej niż 1 identyczna wartość wejściowa, wszystkie lokalizacje danej wartości wejściowej muszą być traktowane jako „takie same”. Możesz napisać podobny kernel do „znajdź lokalizację najbardziej na lewo położonych minimalnych i maksymalnych wartości wejściowych”, w którym (powiedzmy) wartość minimalna w lokalizacji 100 jest preferowana w stosunku do identycznej wartości minimalnej w lokalizacji 200. W tym przypadku „taka sama” oznaczałoby identyczną lokalizację, a nie tylko identyczną wartość, a funkcje akumulatora i kombinatora musiałyby być inne niż w przypadku funkcji findMinAndMax.

Funkcja inicjująca musi utworzyć wartość tożsamości. Oznacza to, że jeśli IA są elementami danych akumulatora zainicjowanymi przez funkcję inicjującą, a I nigdy nie zostało przekazane do funkcji akumulatora (ale A mogło zostać przekazane), to:
  • combinerName(&A, &I) musi pozostawić A takie same
  • combinerName(&I, &A) musi pozostawić I takie same jak A

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

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

  • fMMCombiner(&A, &I) pozostawia A bez zmian, ponieważ I to INITVAL.
  • fMMCombiner(&I, &A) ustawia I na A, ponieważ I to INITVAL.

Dlatego INITVAL jest rzeczywiście wartością tożsamościową.

Funkcja łącząca musi być przemienna. Oznacza to, że jeśli AB są elementami danych akumulatora zainicjowanymi przez funkcję inicjującą i mogą być przekazywane do funkcji akumulatora zero lub więcej razy, to combinerName(&A, &B) musi ustawić A na tę samą wartość, na jaką combinerName(&B, &A) ustawia B.

Przykład: w jądrze addint funkcja łącząca dodaje 2 wartości elementu 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ż.

Funkcja łącząca musi być łączna. Oznacza to, że jeśli A, BC to elementy danych akumulatora zainicjowane przez funkcję inicjującą, które mogły być przekazywane do funkcji akumulatora zero lub więcej razy, to 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 elementu 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 też.

Przykład: w jądrze findMinAndMax

fMMCombiner(&A, &B)
jest taki sam jak
A = minmax(A, B)
Dlatego te 2 sekwencje to:
  • 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 działaniem łącznym, więc fMMCombiner też.

Funkcja akumulatora i funkcja łączenia muszą razem spełniać podstawową zasadę zwijania. Oznacza to, że jeśli AB są elementami danych akumulatora, A został zainicjowany przez funkcję inicjującą i mógł być przekazywany do funkcji akumulatora zero lub więcej razy, B nie został zainicjowany, a args to lista argumentów wejściowych i argumentów specjalnych dla konkretnego wywołania funkcji akumulatora, to 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:

  • Stwierdzenie 1 jest takie samo jak A += V
  • Wyciąg 2 jest taki sam jak B = 0
  • Instrukcja 3 jest taka sama jak B += V, która jest 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ą zmienną A na tę samą wartość, więc ten kernel jest zgodny z podstawową regułą zwijania.

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

  • Stwierdzenie 1 jest takie samo jak A = minmax(A, IndexedVal(V, X))
  • Wyciąg 2 jest taki sam jak B = INITVAL
  • Oświadczenie 3 jest takie samo jak
    B = minmax(B, IndexedVal(V, X))
    która, ponieważ B jest wartością początkową, jest taka sama jak
    B = IndexedVal(V, X)
  • Oświadczenie 4 jest takie samo jak
    A = minmax(A, B)
    czyli
    A = minmax(A, IndexedVal(V, X))

Instrukcje 1 i 4 ustawiają zmienną A na tę samą wartość, więc ten kernel jest zgodny z podstawową regułą zwijania.

Wywoływanie jądra redukcji z kodu Java

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

Kotlin

// Function 1
fun reduce_kernelName(ain1: Allocation, ,
                               ainN: Allocation): javaFutureType

// Function 2
fun reduce_kernelName(ain1: Allocation, ,
                               ainN: Allocation,
                               sc: Script.LaunchOptions): javaFutureType

// Function 3
fun reduce_kernelName(in1: Array<devecSiIn1Type>, ,
                               inN: Array<devecSiInNType>): javaFutureType

Java

// Method 1
public javaFutureType reduce_kernelName(Allocation ain1, ,
                                        Allocation ainN);

// Method 2
public javaFutureType reduce_kernelName(Allocation ain1, ,
                                        Allocation ainN,
                                        Script.LaunchOptions sc);

// Method 3
public javaFutureType reduce_kernelName(devecSiIn1Type[] in1, ,
                                        devecSiInNType[] inN);

Oto kilka przykładów wywoływania 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 dane wejściowe Allocations mają te same wymiary i czy Element typ każdego z nich jest zgodny z typem odpowiedniego argumentu wejściowego prototypu funkcji akumulatora. Jeśli którykolwiek z tych testów zakończy się niepowodzeniem, RenderScript zgłosi wyjątek. Jądro jest wykonywane dla każdego współrzędnego w tych wymiarach.

Metoda 2 jest taka sama jak Metoda 1, z tym że Metoda 2 przyjmuje dodatkowy argument sc, który może służyć do ograniczenia wykonywania jądra do podzbioru współrzędnych.

Metoda 3 jest taka sama jak metoda 1, z tym że zamiast danych wejściowych dotyczących przydzielania pamięci przyjmuje dane wejściowe w postaci tablicy Java. Jest to wygodne rozwiązanie, które zwalnia Cię z konieczności pisania kodu w celu jawnego utworzenia obiektu Allocation i skopiowania do niego 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ą 1-wymiarową 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()Allocation. Następnie wywołuje metodę 1, przekazując te tymczasowe alokacje.

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ść, tworząc, wypełniając i ponownie wykorzystując alokacje samodzielnie, zamiast korzystać z metody 3.

javaFutureType, typ zwracany przez odzwierciedlone metody redukcji, jest odzwierciedloną statyczną klasą zagnieżdżoną w klasie ScriptC_filename. Reprezentuje przyszły wynik działania jądra redukcji. Aby uzyskać rzeczywisty wynik działania, wywołaj metodę get() tej klasy, która zwraca wartość typu javaResultType. get() jest synchroniczna.

Kotlin

class ScriptC_filename(rs: RenderScript) : ScriptC(…) {
    object javaFutureType {
        fun get(): javaResultType {}
    }
}

Java

public class ScriptC_filename extends ScriptC {
  public static class javaFutureType {
    public javaResultType get() {}
  }
}

javaResultType jest określany na podstawie resultType funkcji outconverter. O ile resultType nie jest typem bez znaku (skalarnym, wektorowym lub tablicowym), javaResultType jest bezpośrednio odpowiadającym typem Java. 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 mu typ w języku Java. Na przykład:

  • Jeśli resultType ma wartość int, int2 lub int[15], javaResultType ma wartość int, Int2 lub int[]. Wszystkie wartości parametru resultType mogą być reprezentowane przez parametr javaResultType.
  • Jeśli resultType ma wartość uint, uint2 lub uint[15], javaResultType ma wartość long, Long2 lub long[]. Wszystkie wartości parametru resultType mogą być reprezentowane przez parametr javaResultType.
  • Jeśli resultType ma wartość ulong, ulong2 lub ulong[15], to javaResultType ma wartość long, Long2 lub long[]. Istnieją pewne wartości atrybutu resultType, których nie można przedstawić za pomocą atrybutu javaResultType.

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

  • Jeśli atrybut resultType nie jest typem tablicowym, atrybut javaFutureType ma wartość result_resultType.
  • Jeśli resultType jest tablicą o długości Count z elementami typu memberType, javaFutureType ma wartość resultArrayCount_memberType.

Na przykład:

Kotlin

class ScriptC_filename(rs: RenderScript) : ScriptC(…) {

    // for kernels with int result
    object result_int {
        fun get(): Int =     }

    // for kernels with int[10] result
    object resultArray10_int {
        fun get(): IntArray =     }

    // for kernels with int2 result
    //   note that the Kotlin type name "Int2" is not the same as the script type name "int2"
    object result_int2 {
        fun get(): Int2 =     }

    // for kernels with int2[10] result
    //   note that the Kotlin type name "Int2" is not the same as the script type name "int2"
    object resultArray10_int2 {
        fun get(): Array<Int2> =     }

    // for kernels with uint result
    //   note that the Kotlin type "long" is a wider signed type than the unsigned script type "uint"
    object result_uint {
        fun get(): Long =     }

    // for kernels with uint[10] result
    //   note that the Kotlin type "long" is a wider signed type than the unsigned script type "uint"
    object resultArray10_uint {
        fun get(): LongArray =     }

    // for kernels with uint2 result
    //   note that the Kotlin type "Long2" is a wider signed type than the unsigned script type "uint2"
    object result_uint2 {
        fun get(): Long2 =     }

    // for kernels with uint2[10] result
    //   note that the Kotlin type "Long2" is a wider signed type than the unsigned script type "uint2"
    object resultArray10_uint2 {
        fun get(): Array<Long2> =     }
}

Java

public class ScriptC_filename extends ScriptC {
  // for kernels with int result
  public static class result_int {
    public int get() {}
  }

  // for kernels with int[10] result
  public static class resultArray10_int {
    public int[] get() {}
  }

  // for kernels with int2 result
  //   note that the Java type name "Int2" is not the same as the script type name "int2"
  public static class result_int2 {
    public Int2 get() {}
  }

  // for kernels with int2[10] result
  //   note that the Java type name "Int2" is not the same as the script type name "int2"
  public static class resultArray10_int2 {
    public Int2[] get() {}
  }

  // for kernels with uint result
  //   note that the Java type "long" is a wider signed type than the unsigned script type "uint"
  public static class result_uint {
    public long get() {}
  }

  // for kernels with uint[10] result
  //   note that the Java type "long" is a wider signed type than the unsigned script type "uint"
  public static class resultArray10_uint {
    public long[] get() {}
  }

  // for kernels with uint2 result
  //   note that the Java type "Long2" is a wider signed type than the unsigned script type "uint2"
  public static class result_uint2 {
    public Long2 get() {}
  }

  // for kernels with uint2[10] result
  //   note that the Java type "Long2" is a wider signed type than the unsigned script type "uint2"
  public static class resultArray10_uint2 {
    public Long2[] get() {}
  }
}

Jeśli javaResultType jest typem obiektu (w tym typem tablicy), każde wywołanie javaFutureType.get() na tym samym wystąpieniu zwróci ten sam obiekt.

Jeśli javaResultType nie może reprezentować wszystkich wartości typu resultType, a jądro redukcji generuje wartość, której nie można przedstawić, funkcja javaFutureType.get() zgłasza wyjątek.

Metoda 3 i devecSiInXType

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

  • Jeśli inXType ma wartość int, to devecSiInXType ma wartość int.
  • Jeśli inXType ma wartość int2, to devecSiInXType ma wartość int. Tablica jest spłaszczoną reprezentacją: ma 2 razy więcej skalarnych elementów niż alokacja ma 2-elementowych wektorowych elementów. Działa to podobnie jak metody copyFrom() Allocation.
  • Jeśli inXType ma wartość uint, to deviceSiInXType ma wartość int. Wartość ze znakiem w tablicy Java jest interpretowana jako wartość bez znaku o tym samym wzorcu bitowym w przydziale. W ten sam sposób działają metody copyFrom() Allocation.
  • Jeśli inXType ma wartość uint2, to deviceSiInXType ma wartość int. Jest to połączenie sposobu obsługi int2 i uint: tablica jest spłaszczoną reprezentacją, a wartości ze znakiem w tablicy Java są interpretowane jako wartości bez znaku Element w 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, a wynik wektora skryptu nie jest.
  • Niepodpisane dane wejściowe skryptu są reprezentowane jako podpisane dane wejściowe o tym samym rozmiarze po stronie Javy, natomiast niepodpisany wynik skryptu jest reprezentowany jako rozszerzony typ podpisany po stronie Javy (z wyjątkiem przypadku ulong).

Więcej przykładów jąder 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, RenderScriptIntrinsicHello Compute pokazują, jak używać interfejsów API opisanych na tej stronie.