Omówienie RenderScript

RenderScript to platforma do wykonywania na Androidzie zadań wymagających dużej mocy obliczeniowej z dużą wydajnością. Metoda RenderScript jest preferowana głównie do obliczeń równoległych, chociaż zastosowanie mogą też mieć zadania szeregowe. Środowisko wykonawcze RenderScript obsługuje równolegle procesory dostępne w urządzeniu, takie jak procesory wielordzeniowe 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 służących do przetwarzania obrazu, fotografii obliczeniowej lub rozpoznawania obrazów.

Przed rozpoczęciem korzystania z języka RenderScript musisz pamiętać o 2 głównych koncepcjach:

  • Sam język to język wywodzący się z C99, który służy do pisania bardzo wydajnego kodu obliczeniowego. W artykule Pisanie jądra RenderScriptu opisujemy, jak używać go do pisania jąder obliczeniowych.
  • Interfejs control API służy do zarządzania cyklem życia zasobów RenderScript i kontrolowania wykonywania jądra. Jest on dostępny w trzech różnych językach: Java, C++ w Androidzie NDK oraz język jądra systemu C99. Użycie języka RenderScript z kodu Java i jednego źródła RenderScript zawiera opis odpowiednio pierwszej i trzeciej opcji.

Pisanie jądra RenderScriptu

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

  • Deklaracja pragmy (#pragma version(1)) deklarująca wersję języka jądra RenderScriptu używanego w tym skrypcie. Obecnie jedyną prawidłową wartością jest 1.
  • Deklaracja pragmy (#pragma rs java_package_name(com.example.app)), która deklaruje nazwę pakietu klas Javy odzwierciedlonych przez ten skrypt. Pamiętaj, że plik .rs musi być częścią pakietu aplikacji, a nie projektu biblioteki.
  • Co najmniej 0 funkcji wywoływanych. Funkcja wywoływana to jednowątkowa funkcja w języku RenderScript, którą można wywołać z poziomu kodu Java za pomocą dowolnych argumentów. Często przydają się one podczas wstępnej konfiguracji lub obliczeń szeregowych w większym potoku przetwarzania.
  • 0 lub więcej globalnych skryptów. Skrypt globalny jest podobny do zmiennej globalnej w C. Globalne wartości skryptu są dostępne w kodzie Java i często są używane do przekazywania parametrów do jąder RenderScriptu. Globalne wartości skryptu zostały szczegółowo wyjaśnione tutaj.

  • 0 lub więcej jąder obliczeniowych. Jądro obliczeniowe to funkcja lub zbiór funkcji, które możesz skierować środowisko wykonawcze RenderScript do działania równolegle w zbiorze danych. Istnieją 2 rodzaje jąder obliczeniowych: jądra do mapowania (nazywane też jednym jądra) i jądro redukcji.

    Jądro mapowania to funkcja równoległa, która działa na zbiorze Allocations o tych samych wymiarach. Domyślnie jest wykonywane raz na każdą współrzędną w tych wymiarach. Zwykle (ale nie wyłącznie) używa się go (ale nie tylko) do przekształcenia zbioru danych wejściowych Allocations w dane wyjściowe Allocation z jednym 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 ta funkcja jest identyczna jak w przypadku standardowej funkcji C. Właściwość RS_KERNEL zastosowana do prototypu funkcji wskazuje, że funkcja jest jądrem mapowania RenderScript, a nie funkcją niemożliwą. Argument in jest wypełniany automatycznie na podstawie danych wejściowych Allocation przekazanych do uruchomienia jądra. Argumenty x i y zostały omówione poniżej. Wartość zwracana przez jądra jest automatycznie zapisywana w odpowiedniej lokalizacji w danych wyjściowych Allocation. Domyślnie to jądro działa na całej ścieżce wejściowej Allocation, z jednym wykonaniem funkcji jądra na Element w Allocation.

      Jądro mapowania może mieć 1 lub więcej danych wejściowych Allocations, 1 wyjściowy Allocation lub oba te elementy. Środowisko wykonawcze RenderScript sprawdza, czy wszystkie przydziały wejściowe i wyjściowe mają te same wymiary oraz czy typy Element przydziałów wejściowych i wyjściowych są zgodne z prototypem jądra. Jeśli jedna z tych testów zakończy się niepowodzeniem, RenderScript zgłosi wyjątek.

      UWAGA: w Androidzie 6.0 (poziom interfejsu API 23) jądro mapowania nie może mieć więcej niż 1 wartości wejściowej Allocation.

      Jeśli potrzebujesz więcej danych wejściowych lub wyjściowych Allocations, niż ma jądro, musisz je powiązać z globalnymi skryptami rs_allocation i uzyskać do nich dostęp z jądra lub funkcji nieuzyskanej za pomocą rsGetElementAt_type() lub rsSetElementAt_type().

      UWAGA: RS_KERNEL to makro zdefiniowane automatycznie przez RenderScript:

      #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 jej funkcja zasobnika jest wykonywana raz dla każdej współrzędnej w tych wymiarach. Jest on zwykle (ale nie wyłącznie) używany do „zmniejszenia” zbioru danych wejściowych Allocations do jednej wartości.

    • Oto przykład prostego jądra redukcji, które dodaje wartość Elements w jej 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. Funkcja #pragma rs reduce służy do zdefiniowania jądra przez podanie jego nazwy (w tym przykładzie addint) oraz nazw i ról funkcji, które wchodzą w skład jądra (w tym przykładzie jest to funkcja accumulator addintAccum). Wszystkie takie funkcje muszą mieć typ static. Jądro redukcji zawsze wymaga funkcji accumulator. Może też mieć inne funkcje w zależności od tego, do czego ma służyć.

      Funkcja kumulatora redukcji jądra musi zwracać wartość 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 wypełniany automatycznie na podstawie danych wejściowych Allocation przekazanych do uruchomienia jądra. Element danych akumulatora jest tworzony przez środowisko wykonawcze RenderScript i domyślnie ma wartość zerową. Domyślnie to jądro działa na wszystkich danych wejściowych Allocation, z jednym wykonaniem funkcji akumulatora na Element w Allocation. Domyślnie ostateczna wartość elementu danych akumulatora jest traktowana w wyniku redukcji i zwracana do Javy. Środowisko wykonawcze RenderScript sprawdza, czy typ Element przydziału danych wejściowych jest zgodny z prototypem funkcji zasobnika. Jeśli nie, RenderScript zgłasza wyjątek.

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

      Jądro redukcji wyjaśniamy bardziej szczegółowo tutaj.

      Jądro redukcji są obsługiwane w Androidzie 7.0 (poziom interfejsu API 24) i nowszych.

    Funkcja jądra mapowania lub funkcja akumulatora redukcji jądra może uzyskać dostęp do współrzędnych bieżącego wykonania przy użyciu 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 zasobnika jądra redukcji może też przyjmować opcjonalny argument specjalny context typu rs_kernel_context. Jest potrzebny przez rodzinę interfejsów API środowiska wykonawczego, które są używane do wysyłania zapytań o określone właściwości bieżącego wykonania, na przykład rsGetDimX. (Argument context jest dostępny w Androidzie 6.0 (poziom interfejsu API 23) i nowszych.

  • Opcjonalna funkcja init(). Funkcja init() to specjalny typ funkcji, którą można wywołać, uruchamiana przez RenderScript przy tworzeniu pierwszej instancji skryptu. Dzięki temu niektóre obliczenia mogą być wykonywane automatycznie podczas tworzenia skryptu.
  • 0 lub więcej globalnych wartości i funkcji skryptów statycznych. Globalny skrypt statyczny jest odpowiednikiem globalnego skryptu z tym wyjątkiem, że nie ma do niego dostępu z poziomu kodu Java. Funkcja statyczna to standardowa funkcja C, którą można wywołać z dowolnego jądra lub niedostępnej funkcji w skrypcie, ale nie jest dostępna dla interfejsu Java API. Jeśli do globalnego lub do funkcji skryptu nie trzeba uzyskać dostępu z kodu Java, zdecydowanie zalecamy zadeklarowanie go static.

Ustawianie precyzji liczby zmiennoprzecinkowej

Możesz kontrolować wymagany poziom dokładności liczby zmiennoprzecinkowej w skrypcie. Jest to przydatne, gdy pełny standard IEEE 754-2008 (używany domyślnie) nie jest wymagany. Te pragmy mogą ustawiać inny poziom dokładności zmiennoprzecinkowej:

  • #pragma rs_fp_full (domyślnie, jeśli nic nie zostało określone): dla aplikacji, które wymagają precyzji zmiennoprzecinkowej zgodnie ze standardem IEEE 754-2008.
  • #pragma rs_fp_relaxed: dla aplikacji, które nie wymagają ścisłej zgodności z IEEE 754–2008 i mogą tolerować mniejszą precyzję. Ten tryb włącza czyszczenie do zera w przypadku denormów i zaokrąglanie do zera.
  • #pragma rs_fp_imprecise: dla aplikacji, które nie mają rygorystycznych wymagań dotyczących dokładności. Ten tryb włącza wszystko w rs_fp_relaxed oraz:
    • Operacje z wartością -0,0 mogą zwracać zamiast tego +0,0.
    • Operacje na INF i NAN są niezdefiniowane.

Większość aplikacji może korzystać z rs_fp_relaxed bez żadnych efektów ubocznych. W przypadku niektórych architektur może to być bardzo korzystne ze względu na dodatkowe optymalizacje dostępne tylko z zachowaniem swobodnej precyzji (np. instrukcje dotyczące procesora SIMD).

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

Gdy tworzysz aplikację na Androida korzystającą z RenderScriptu, możesz uzyskać dostęp do jej interfejsu API w Javie 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, co umożliwia korzystanie z nich na urządzeniach z Androidem 2.3 (poziom interfejsu API 9) lub nowszym.

Oto wady:

  • Jeśli używasz interfejsów API biblioteki pomocy, część aplikacji w języku RenderScript będzie zgodna z urządzeniami z Androidem 2.3 (poziom interfejsu API 9) lub nowszym niezależnie od używanych funkcji RenderScript. Dzięki temu aplikacja może 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 z biblioteki pomocy.
  • Jeśli używasz interfejsów API biblioteki pomocy, otrzymasz (prawdopodobnie znacznie) większe pliki APK niż w przypadku używania natywnych interfejsów API (android.renderscript).

Korzystanie z interfejsów API biblioteki pomocy RenderScript

Jeśli chcesz korzystać z interfejsów RenderScript API z biblioteki pomocy, musisz skonfigurować swoje środowisko programistyczne tak, 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
  • Android SDK Build-tools w wersji 18.1.0 lub nowszej

Uwaga: od wersji Android SDK Build-tools w wersji 24.0.0 i wersji 2.2 (poziom interfejsu API 8) nie jest już obsługiwany.

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

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

  1. Upewnij się, że masz zainstalowaną wymaganą wersję pakietu SDK na Androida.
  2. Zaktualizuj ustawienia procesu kompilacji Androida, dodając ustawienia RenderScript:
    • Otwórz plik build.gradle w folderze aplikacji modułu aplikacji.
    • Dodaj do pliku te ustawienia RenderScript:

      Odlotowe

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

      Wymienione powyżej ustawienia określają działanie w procesie kompilacji Androida:

      • renderscriptTargetApi – określa wersję kodu bajtowego do wygenerowania. Zalecamy ustawienie tej wartości na najniższy poziom interfejsu API, który zapewnia dostęp do wszystkich używanych funkcji, oraz ustawienie renderscriptSupportModeEnabled na true. Prawidłowe wartości tego ustawienia to dowolna liczba całkowita od 11 do ostatnio opublikowanego 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 wartość docelowa w pliku kompilacji służy do określenia minimalnej wersji pakietu SDK.
      • renderscriptSupportModeEnabled – określa, że wygenerowany kod bajtowy powinien wrócić do zgodnej wersji, jeśli urządzenie, na którym działa, nie obsługuje wersji docelowej.
  3. W klasach aplikacji, które korzystają z RenderScriptu, dodaj import dla klas biblioteki pomocy:

    Kotlin

    import android.support.v8.renderscript.*
    

    Java

    import android.support.v8.renderscript.*;
    

używanie RenderScriptu za pomocą języka Java lub Kotlin Code,

Używanie RenderScriptu za pomocą 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 ma ten sam podstawowy wzorzec użytkowania:

  1. Zainicjuj kontekst RenderScriptu. Kontekst RenderScript utworzony za pomocą create(Context) sprawia, że skrypt RenderScript może być używany i udostępnia obiekt do kontrolowania czasu działania wszystkich kolejnych obiektów RenderScript. Tworzenie kontekstu należy traktować jako potencjalnie długotrwałą operację, ponieważ może ono tworzyć zasoby na różnych elementach sprzętowych. Nie powinno się znajdować się na ścieżce krytycznej aplikacji, jeśli jest to możliwe. Zwykle aplikacja ma tylko jeden kontekst RenderScript naraz.
  2. Utwórz co najmniej 1 element Allocation, który będzie przekazywany do skryptu. Allocation to obiekt RenderScript, który umożliwia przechowywanie ustalonej ilości danych. Ziarna w skryptach przyjmują jako dane wejściowe i wyjściowe obiekty (Allocation), a obiekty Allocation są dostępne w jądrze za pomocą rsGetElementAt_type() i rsSetElementAt_type(), jeśli są powiązane jako globalne wartości 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 potrzebne skrypty. Korzystając z RenderScriptu, możesz korzystać z 2 typów skryptów:
    • ScriptC: są to skrypty zdefiniowane przez użytkownika zgodnie z opisem w sekcji Pisanie jądra RenderScriptu powyżej. Każdy skrypt ma klasę Java odzwierciedloną przez kompilator RenderScript, aby ułatwić dostęp do skryptu z poziomu kodu Java. Ta klasa nosi nazwę ScriptC_filename. Jeśli na przykład jądro mapowania znajdowało się powyżej w regionie invert.rs, a kontekst RenderScriptu znajdował się już w mRenderScript, kod Java lub Kotlin do utworzenia wystąpienia skryptu będzie miał postać:

      Kotlin

      val invert = ScriptC_invert(renderScript)
      

      Java

      ScriptC_invert invert = new ScriptC_invert(renderScript);
      
    • ScriptIntrinsic: to wbudowane jądra RenderScriptu służące do typowych operacji, takich jak rozmycie Gaussa, splot czy mieszanie obrazów. Więcej informacji znajdziesz w podklasach klasy ScriptIntrinsic.
  4. Wypełnianie przydziałów danymi. Z wyjątkiem przydziałów utworzonych za pomocą funkcji createFromBitmap() przy pierwszym tworzeniu przydział jest zapełniany pustymi danymi. Aby uzupełnić przydział, użyj jednej z metod „kopiowania” w funkcji Allocation. Metody „kopiowania” są synchroniczne.
  5. Ustaw niezbędne globalne wartości skryptu. Możesz ustawić wartości globalne za pomocą metod w tej samej klasie ScriptC_filename o nazwie set_globalname. Aby np. 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 set_lookup(Allocation) w Javie. Metody setasynchroniczne.
  6. Uruchom odpowiednie jądra i niemożliwe funkcje.

    Metody uruchamiania danego jądra są odzwierciedlane w tej samej klasie ScriptC_filename za pomocą metod o nazwach forEach_mappingKernelName() lub reduce_reductionKernelName(). Zmiany te są asynchroniczne. W zależności od argumentów jądra metoda przyjmuje co najmniej 1 alokację, z których wszystkie muszą mieć te same wymiary. Domyślnie jądro wykonuje działanie nad każdą współrzędną w tych wymiarach. Aby uruchomić jądro nad podzbiorem tych współrzędnych, przekaż w metodzie forEach lub reduce odpowiedni argument Script.LaunchOptions.

    Uruchom funkcje, których nie można wywołać, za pomocą metod invoke_functionName odzwierciedlonych w tej samej klasie ScriptC_filename. Zmiany te są asynchroniczne.

  7. Pobieranie danych z obiektów Allocation i obiektów javaFutureType. Aby uzyskać dostęp do danych z elementu Allocation z kodu Java, musisz skopiować je z powrotem do Javy za pomocą jednej z metod „kopiowania” w narzędziu Allocation. Aby uzyskać wynik redukcji jądra, należy użyć metody javaFutureType.get(). Metody „copy” i get()synchroniczne.
  8. Przeanalizuj kontekst RenderScriptu. Możesz zniszczyć kontekst RenderScript za pomocą parametru destroy() lub zezwolić na zbieranie danych przez obiekt kontekstu RenderScript. Spowoduje to zgłoszenie wyjątku przy użyciu dowolnego obiektu należącego do tego kontekstu.

Asynchroniczny model wykonywania

Odzwierciedlone metody forEach, invoke, reduce i set są asynchroniczne – każda może wrócić do Javy przed wykonaniem żądanego działania. Poszczególne działania są jednak szeregowane w kolejności, w jakiej zostały uruchomione.

Klasa Allocation udostępnia metody „copy” do kopiowania danych do i z przydziałów. Metoda „copy” jest synchroniczna i jest zserializowana w odniesieniu do dowolnych wymienionych powyżej działań asynchronicznych, które mają wpływ na ten sam przydział.

Odzwierciedlenie klas javaFutureType udostępnia metodę get() pozwalającą uzyskać wynik redukcji. Funkcja get() jest synchroniczna i jest zserializowana w związku z redukcją (która jest asynchroniczna).

Skrypt RenderScript z jednego źródła

Android 7.0 (poziom interfejsu API 24) wprowadza nową funkcję programowania o nazwie Jednoźródłowy kod renderowania, która polega na uruchamianiu jąder ze skryptu, w którym zostały zdefiniowane, a nie ze skryptu Java. To podejście jest obecnie ograniczone do jąder mapowania, które w tej sekcji określa się po prostu „jądrami”, aby zachować zwięzłość działania. Ta nowa funkcja obsługuje też tworzenie alokacji typu rs_allocation z poziomu skryptu. Teraz można wdrożyć cały algorytm wyłącznie w skrypcie, nawet jeśli wymagane jest uruchomienie wielu jądra systemu. Korzyści są 2 korzyści: bardziej czytelny kod, ponieważ implementacja algorytmu jest zachowana w jednym języku, oraz potencjalnie szybszy kod ze względu na mniejszą liczbę przejść między Javą a RenderScriptem podczas korzystania z wielu jądra systemu.

W języku RenderScript na jedno źródło treści zapisujesz jądra w sposób opisany w artykule Tworzenie jądra RenderScriptu. Następnie tworzysz niemożliwą funkcję, która wywołuje metodę rsForEach(), aby ją uruchomić. Ten interfejs API wykorzystuje jako pierwszy parametr funkcję jądra, a po nim alokacje wejściowe i wyjściowe. Podobny interfejs API rsForEachWithOptions() wykorzystuje 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 ma przetwarzać funkcja jądra.

Aby rozpocząć obliczenia w języku RenderScript, wywołaj funkcję niedostępną w języku Java. Wykonaj czynności opisane w sekcji Używanie skryptu RenderScript z kodu Java. W kroku uruchom odpowiednie jądra wywołaj funkcję niemożliwą za pomocą funkcji invoke_function_name(), co spowoduje uruchomienie wszystkich obliczeń, w tym uruchomienia jądra.

Przydziały są często potrzebne do zapisywania i przekazywania wyników pośrednich między uruchomieniem jądra systemu. Możesz je utworzyć za pomocą funkcji rsCreateAllocation(). Jedną z prostych w użyciu postaci tego interfejsu API jest rsCreateAllocation_<T><W>(…), gdzie T to typ danych elementu, a W to szerokość wektora elementu. Jako argumenty interfejs API przyjmuje rozmiary z wymiarów 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 przydział 1D obejmujący 16 384 elementy, z których każdy jest typu uchar4.

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

Sekcja Zapisywanie jądra RenderScriptu zawiera przykładowe jądro, które odwraca obraz. Poniższy przykład rozwija tę opcję, aby zastosować do obrazu więcej niż 1 efekt przy użyciu kodu RenderScript z jednego źródła. Zawiera też inne jądro (greyscale), które zmienia kolorowy obraz w czarno-biały. Funkcja wywoływana process() stosuje następnie kolejno te 2 jądro do obrazu wejściowego i tworzy 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 następujący 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 można w całości wdrożyć algorytm, który obejmuje 2 uruchomienia jądra, w języku RenderScript. Bez RenderScriptu z jednego źródła należałoby uruchomić oba jądro z kodu Java, oddzielając uruchomienia jądra od definicji jądra i utrudniając zrozumienie całego algorytmu. Nie tylko jest łatwiejszy do odczytania kod RenderScript z jednego źródła, lecz także eliminuje przenoszenie między Javą a skryptem podczas uruchamiania jądra systemu. Niektóre iteracyjne algorytmy mogą uruchamiać jądra setki razy, co sprawia, że koszty związane z takim przejściem są znaczne.

Globalne wartości skryptu

Skrypt globalny to zwykła zmienna globalna inna niż static w pliku skryptu (.rs). W przypadku skryptu o nazwie var zdefiniowanego w pliku filename.rs zostanie odzwierciedlona metoda get_var odzwierciedlona w klasie ScriptC_filename. Jeśli globalna wartość to const, będzie też metoda set_var.

Taki globalny skrypt ma 2 osobne wartości: Java i script. Wartości te działają w ten sposób:

  • Jeśli w skrypcie znajduje się inicjator statyczny var, określa on początkową wartość zmiennej var zarówno w Javie, jak i w skrypcie. W przeciwnym razie wartość początkowa wynosi 0.
  • Uzyskuje dostęp do zmiennej var w ramach skryptu z możliwością odczytu i zapisu jego wartości.
  • Metoda get_var odczytuje wartość w Javie.
  • Metoda set_var (jeśli istnieje) natychmiast zapisuje wartość w Javie i zapisuje wartość skryptu asynchronicznie.

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

Głębokość redukcji ziaren

Zmniejszanie to proces łączenia zbioru danych w jedną wartość. Jest to przydatny element praktyczny przy programowaniu równoległym, który umożliwia zastosowanie takich funkcji jak:

  • obliczanie sumy lub iloczynu na wszystkich danych
  • operacje logiczne (and, or, xor) na wszystkich danych
  • znajdowanie minimalnej lub maksymalnej wartości w danych
  • podczas wyszukiwania konkretnej wartości lub współrzędnych konkretnej wartości w danych

W Androidzie 7.0 (poziom interfejsu API 24) i nowszych skrypt RenderScript obsługuje jądro redukcji, co umożliwia działanie algorytmów redukcji napisanych przez użytkownika. Można uruchamiać jądra redukcji na danych wejściowych o 1, 2 lub 3 wymiarach.

Powyższy przykład przedstawia proste jądro redukcji addint. Oto bardziej skomplikowane jądro redukcji findMinAndMax, które znajduje lokalizacje minimalnej i maksymalnej wartości long w jednowymiarowej 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 jąder znajdziesz tutaj.

Aby uruchomić jądra systemu redukcji, środowisko wykonawcze RenderScript tworzy co najmniej 1 zmienną nazywane elementami danych zasobnika i przechowuje stan procesu redukcji. Środowisko wykonawcze RenderScript wybiera liczbę elementów danych z zasobnika w taki sposób, aby zmaksymalizować wydajność. Typ elementów danych akumulatora (accumType) jest określany przez funkcję zasobnika jądra. Pierwszy argument do tej funkcji to wskaźnik do elementu danych akumulatora. Domyślnie każdy element danych akumulatora jest inicjowany do zera (np. przez memset). Można jednak utworzyć funkcję inicjującą, aby zrobić coś innego.

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

Przykład: w jądrze findMinAndMax elementy danych akumulatora (typu MinAndMax) służą do śledzenia dotychczasowych i maksymalnych wartości. Dostępna jest funkcja inicjatora, która ustawia odpowiednie wartości na LONG_MAX i LONG_MIN oraz ustawia lokalizacje tych wartości na -1, co oznacza, że wartości nie występują w (pustej) części przetworzonej wartości wejściowej.

RenderScript wywołuje funkcję zasobnika raz na każdą współrzędną w danych wejściowych. Zwykle funkcja powinna w jakiś sposób aktualizować element danych akumulatora na podstawie danych wejściowych.

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

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

Po tym, jak funkcja zasobnika zostanie wywołana raz dla każdej współrzędnej w danych wejściowych, RenderScript musi połączyć elementy danych zasobnika w jedną pozycję danych zasobnika. W tym celu możesz napisać funkcję kombinacji. Jeśli funkcja zasobnika ma 1 dane wejściowe i nie zawiera specjalnych argumentów, nie musisz pisać funkcji łączącej. RenderScript użyje funkcji zasobnika, aby połączyć elementy danych z zasobnika. (Jeśli to domyślne działanie nie jest Ci potrzebne, możesz utworzyć funkcję łączącą).

Przykład: w jądrze addint nie ma funkcji łączącej, więc zostanie użyta funkcja zasobnika. Jest to prawidłowe działanie, ponieważ jeśli podzielimy zbiór wartości na 2 części, a dodamy wartości z tych 2 części z osobna, zsumowanie tych 2 sum jest równoznaczne z dodaniem całej kolekcji.

Przykład: w jądrze findMinAndMax funkcja łączenia sprawdza, czy minimalna wartość zarejestrowana w elemencie danych zasobnika „źródło” *val jest mniejsza od minimalnej wartości zarejestrowanej w elemencie danych zasobnika *accum i odpowiednio aktualizuje *accum. To samo działa w przypadku wartości maksymalnej. Spowoduje to zaktualizowanie *accum do stanu, jaki miałby, gdyby wszystkie wartości wejściowe zostały zgromadzone w elemencie *accum, a nie niektóre w *accum, a niektóre w *val.

Po połączeniu wszystkich danych akumulatora RenderScript określa wynik redukcji i powraca do Javy. W tym celu możesz utworzyć funkcję konwertera zewnętrznego. Nie musisz pisać funkcji konwertera, jeśli chcesz, aby końcowa wartość połączonych elementów danych akumulatora była wynikiem redukcji.

Przykład: w jądrze addint nie ma funkcji outconverter. Ostateczna wartość połączonych elementów danych to suma wszystkich elementów wejściowych, czyli wartość, którą chcemy zwrócić.

Przykład: w jądrze findMinAndMax funkcja outconverter inicjuje wartość wynikową int2, aby przechowywać lokalizacje minimalnej i maksymalnej wartości uzyskanej z kombinacji 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 się na nim znajdują. Wszystkie takie funkcje muszą mieć wartość static. Jądro redukcji zawsze wymaga funkcji accumulator. W zależności od przeznaczenia jądra możesz pominąć niektóre lub wszystkie inne funkcje.

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

Elementy w #pragma mają następujące znaczenie:

  • reduce(kernelName) (wymagane): określa, że jest zdefiniowane jądro redukcji. Odzwierciedlona metoda Java reduce_kernelName spowoduje uruchomienie jądra.
  • initializer(initializerName) (opcjonalnie): określa nazwę funkcji inicjującej tego jądra redukcji. Po uruchomieniu jądra RenderScript wywołuje tę funkcję raz dla każdego elementu danych zasobnika. Funkcja musi być zdefiniowana w ten sposób:

    static void initializerName(accumType *accum) { … }

    accum to wskaźnik do elementu danych akumulatora, który ma zostać zainicjowany przez tę funkcję.

    Jeśli nie podasz funkcji inicjującej, RenderScript zainicjuje każdy element danych akumulatora do 0 (jakby to funkcja memset), działając tak, jakby istniała funkcja inicjująca, która wygląda 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 na każdą współrzędną w danych wejściowych, aby w jakiś sposób zaktualizować element danych zasobnika na podstawie tych danych. Funkcja musi być zdefiniowana w ten sposób:

    static void accumulatorName(accumType *accum,
                                in1Type in1, …, inNType inN
                                [, specialArguments]) { … }
    

    accum wskazuje element danych z zasobnika, którego można użyć do modyfikacji. Od in1 do inN to 1 lub więcej argumentów, które są automatycznie wypełniane na podstawie danych wejściowych przekazywanych do uruchomienia jądra (1 argument na dane wejściowe). Funkcja zasobnika może opcjonalnie przyjąć dowolny ze argumentów specjalnych.

    Przykładowe jądro z wieloma danymi wejściowymi to dotProduct.

  • combiner(combinerName)

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

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

    accum wskazuje element danych akumulatora „miejsce docelowe”, który ma zostać zmodyfikowany przez tę funkcję. other to wskaźnik do elementu danych akumulatora „źródłowy” dla tej funkcji pozwalającej na „połączenie” z elementem *accum.

    UWAGA: możliwe, że *accum, *other lub obie te wartości zostały zainicjowane, ale nigdy nie zostały przekazane do funkcji zasobnika. Oznacza to, że co najmniej jedna z nich nigdy nie została zaktualizowana na podstawie danych wejściowych. Na przykład w jądrze findMinAndMax funkcja łącząca fMMCombiner wyraźnie sprawdza idx < 0, ponieważ wskazuje taki element danych akumulatora, którego wartość to INITVAL.

    Jeśli nie podasz funkcji łączącej, skrypt 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 dane wejściowe, gdy typ danych wejściowych nie jest taki sam jak typ danych zasobnika lub gdy funkcja zasobnika przyjmuje co najmniej 1 argument specjalny.

  • outconverter(outconverterName)(opcjonalnie): określa nazwę funkcji konwertera dla tego jądra redukcji. Po połączeniu wszystkich elementów danych akumulatora RenderScript wywołuje tę funkcję, aby określić wynik redukcji i wrócić do Javy. Funkcja musi być zdefiniowana w ten sposób:

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

    result to wskaźnik do wynikowego elementu danych (przydzielonego, ale nie zainicjowanego przez środowisko wykonawcze RenderScript) tej funkcji, która ma zostać zainicjowana z wynikiem redukcji. resultType to typ elementu danych, który nie musi być taki sam jak typ accumType. accum to wskaźnik do ostatecznego elementu danych z zasobnika obliczonego przez funkcję kombinacji.

    Jeśli nie podasz funkcji konwertera, RenderScript skopiuje ostateczny element danych akumulatora do wynikowego elementu danych, zachowując się tak, jakby istniała funkcja tego konwertera, która wygląda tak:

    static void outconverterName(accumType *result, const accumType *accum) {
      *result = *accum;
    }

    Jeśli chcesz uzyskać inny typ wyniku niż typ danych akumulatora, musisz użyć funkcji konwertera.

Pamiętaj, że jądro ma typy danych wejściowych, typ elementu danych zasobnika i typ wyniku, które nie muszą być takie same. Na przykład w jądrze findMinAndMax typy danych wejściowych long, typ elementu danych zasobnika MinAndMax i typ wyniku int2 są różne.

Czego nie możesz przyjąć?

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

Nie możesz polegać na kolejności, w jakiej RenderScript wywołuje funkcje inicjatora, zasobnika i kombinatora; niektóre z nich może być nawet wywoływane równolegle. Nie ma gwarancji, że 2 uruchomienia tego samego jądra z tymi samymi danymi wejściowymi będą miały tę samą kolejność. Jedyną gwarancją jest to, że tylko funkcja inicjująca zobaczy niezainicjowany element danych akumulatora. Na przykład:

  • Nie ma gwarancji, że wszystkie elementy danych akumulatora zostaną zainicjowane przed wywołaniem tej funkcji, ale będzie ona wywoływana tylko w przypadku zainicjowanego elementu danych akumulatora.
  • Nie ma gwarancji co do kolejności, w jakiej elementy wejściowe są przekazywane do funkcji zasobnika.
  • 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 tej sytuacji jest to, że jądro findMinAndMax nie jest deterministyczne: jeśli dane wejściowe zawierają więcej niż 1 wystąpienie o tej samej wartości minimalnej lub maksymalnej, nie można określić, którego wystąpienia ją znajdzie.

Co musisz zagwarantować?

System RenderScript może uruchamiać ją na wiele różnych sposobów, dlatego musisz przestrzegać określonych reguł, aby zagwarantować, że jądro działa w oczekiwany sposób. Jeśli nie będziesz przestrzegać tych zasad, możesz otrzymać nieprawidłowe wyniki, niedeterministyczne zachowanie lub błędy podczas działania.

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 działać jądro. W przypadku redukcji matematycznej, np. addint, zwykle sensowne jest określenie „to samo” oznacza równość matematyczną. W przypadku wyszukiwania typu „wybierz dowolne”, np. findMinAndMax („znajdź lokalizację minimalnej i maksymalnej wartości wejściowej”), w którym może wystąpić więcej niż 1 wystąpienie identycznych wartości wejściowych, wszystkie lokalizacje danej wartości wejściowej muszą być uznawane za „takie same”. Można napisać podobne jądro do „znajdowania lokalizacji minimalnej i maksymalnej wartości wejściowej po lewej”, gdzie (na przykład) preferowana jest wartość minimalna w lokalizacji 100 zamiast identycznej wartości minimalnej w lokalizacji 200. W przypadku tego jądra „ta sama” oznaczałaby identyczną lokalizację, a nie tylko identyczną lokalizację, a nie tylko identyczną wartość, a zasobniki dodatkowe będą miały różne zasobniki i kumulatory.

Funkcja inicjująca musi utworzyć wartość tożsamości. Oznacza to, że jeśli I i A są elementami danych akumulatora zainicjowanymi przez funkcję inicjującą, a element I nigdy nie został przekazany do funkcji akumulatora (ale nie A), to
  • combinerName(&A, &I) musi pozostawić A ten sam
  • combinerName(&I, &A) musi opuścić I ten sam co A

Przykład: w jądrze addint element danych akumulatora jest zainicjowany zero. Funkcja łącząca to jądro wykonuje dodawanie. „0” to wartość tożsamości przy dodaniu.

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

  • Pole fMMCombiner(&A, &I) pozostawia pole A bez zmian, ponieważ I ma wartość INITVAL.
  • fMMCombiner(&I, &A) ustawia wartość I na A, ponieważ I ma wartość INITVAL.

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

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

Przykład: w jądrze addint funkcja łącząca dodaje 2 wartości elementu danych zasobnika; dodawanie jest przemienne.

Przykład: w jądrze findMinAndMax fMMCombiner(&A, &B) to to samo co A = minmax(A, B), a minmax jest przemienne, więc fMMCombiner również.

Funkcja łącząca musi być osobna. Oznacza to, że jeśli A, B i C są elementami danych akumulatora zainicjowanymi przez funkcję inicjatora i które mogły zostać przekazane do funkcji zasobnika co najmniej 0 razy, 2 poniższe 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 asocjacyjne, tak samo jak funkcja łącząca.

Przykład: W jądrze findMinAndMax

fMMCombiner(&A, &B)
jest taki sam jak
A = minmax(A, B)
Zatem te dwie sekwencje mają postać

  • A = minmax(A, B)
    A = minmax(A, C)
    // Same as
    //   A = minmax(minmax(A, B), C)
    
  • B = minmax(B, C)
    A = minmax(A, B)
    // Same as
    //   A = minmax(A, minmax(B, C))
    //   B = minmax(B, C)
    

Komponent minmax jest powiązany, więc fMMCombiner również jest powiązany.

Funkcje zasobnika i kombinatora muszą być jednocześnie zgodne z podstawową regułą zwijania. Oznacza to, że jeśli A i B są elementami danych akumulatora, funkcja A została zainicjowana przez funkcję inicjatora i mogła zostać przekazana do funkcji akumulatora zero lub więcej razy, B nie została zainicjowana, a args to lista argumentów wejściowych i specjalnych dla określonego wywołania funkcji zasobnika, to te 2 sekwencje kodu muszą mieć ustawioną tę samą wartość :A

  • accumulatorName(&A, args);  // statement 1
    
  • initializerName(&B);        // statement 2
    accumulatorName(&B, args);  // statement 3
    combinerName(&A, &B);       // statement 4
    

Przykład: w jądrze addint dla wartości wejściowej V:

  • Polecenie 1 jest takie samo jak A += V.
  • Stwierdzenie 2 jest takie samo jak B = 0
  • Stwierdzenie 3 jest takie samo jak B += V, które jest identyczne z B = V.
  • Stwierdzenie 4 jest takie samo jak A += B, które jest identyczne z 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 we współrzędnej X:

  • Polecenie 1 jest takie samo jak A = minmax(A, IndexedVal(V, X)).
  • Stwierdzenie 2 jest takie samo jak B = INITVAL
  • Stwierdzenie 3 jest takie samo jak
    B = minmax(B, IndexedVal(V, X))
    
    , które B jest wartością początkową, więc odpowiada
    B = IndexedVal(V, X)
    
    .
  • Stwierdzenie 4 jest takie samo jak
    A = minmax(A, B)
    
    , które jest identyczne z
    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łanie jądra redukcji z kodu Java

W przypadku jądra redukcji o nazwie kernelName zdefiniowanego w pliku filename.rs klasa ScriptC_filename zawiera 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ł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 jeden argument wejściowy Allocation dla każdego argumentu wejściowego w funkcji zasobnika jądra. Środowisko wykonawcze RenderScript sprawdza, czy wszystkie przydziały wejściowe mają te same wymiary i czy typ Element w każdym z tych przydziałów jest zgodny z argumentem wejściowym prototypu funkcji zasobnika. Jeśli któraś z tych kontroli zakończy się niepowodzeniem, RenderScript zgłosi wyjątek. Jądro działa w przypadku wszystkich współrzędnych w tych wymiarach.

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

Metoda 3 jest taka sama jak metoda 1, z tą różnicą, że zamiast korzystać z danych wejściowych alokacji, potrzebne są dane wejściowe tablicy Java. To wygoda, dzięki której nie musisz pisać kodu jawnego tworzenia przydziału i kopiować do niego danych z tablicy Java. Zastosowanie metody 3 zamiast metody 1 nie zwiększa jednak wydajności kodu. Dla każdej tablicy wejściowej metoda 3 tworzy tymczasową, jednowymiarową alokację z odpowiednim typem Element i włączonym setAutoPadding(boolean), a następnie kopiuje tablicę do sekcji Przydział tak, jakby była to odpowiednia metoda copyFrom() metody Allocation. Następnie wywołuje metodę 1, przekazując te tymczasowe przydziały.

UWAGA: jeśli Twoja 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ść przez jawne tworzenie, wypełnianie i ponowne wykorzystywanie przydziałów, zamiast korzystać z metody 3.

javaFutureType, czyli zwracany typ zwracanych metod redukcji, odzwierciedla statyczną zagnieżdżoną klasę w klasie ScriptC_filename. Reprezentuje przyszły wynik uruchomienia jądra systemu. Aby uzyskać rzeczywisty wynik uruchomienia, wywołaj metodę get() tej klasy, która zwraca wartość typu javaResultType. Funkcja 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() { … }
  }
}

Wartość javaResultType jest określana na podstawie parametru resultType funkcji outconverter. Jeśli resultType nie jest typem bez znaku (skalarnym, wektorowym lub tablicą), javaResultType bezpośrednio odpowiada typowi Java. Jeśli resultType jest typem bez podpisu i istnieje większy typ podpisany w Javie, to javaResultType jest tym większym typem podpisanym w Javie. W przeciwnym razie jest to bezpośrednio odpowiadający jej typ. Na przykład:

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

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

  • Jeśli resultType nie jest typem tablicy, pole javaFutureType ma wartość result_resultType.
  • Jeśli resultType jest tablicą o długości Count, która należy do typu memberType, to javaFutureType ma wartość resultArrayCount_memberType.

Na przykład:

Kotlin

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

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

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

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

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

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

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

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

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

Java

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

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

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

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

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

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

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

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

Jeśli javaResultType jest typem obiektu (w tym tablicy), każde wywołanie funkcji javaFutureType.get() w tej samej instancji będzie zwracać ten sam obiekt.

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

Metoda 3 i devecSiInXType

devecSiInXType to typ Javy odpowiadający parametrowi inXType odpowiedniego argumentu funkcji zasobnik. O ile inXType nie jest typem bez znaku lub typem wektora, bezpośrednio odpowiadającym mu typem Javy jest devecSiInXType. Jeśli inXType jest typem skalarnym bez znaku, devecSiInXType jest typem Javy bezpośrednio odpowiadającym podpisanemu typowi skalarnego o tym samym rozmiarze. Jeśli inXType jest typem wektora podpisanego, devecSiInXType jest typem Javy bezpośrednio odpowiadającego typowi komponentu wektora. Jeśli inXType jest typem wektora bez znaku, devecSiInXType jest typem Java odpowiadającym bezpośrednio typowi wektora skalarnego o takim samym rozmiarze jak typ komponentu wektora. Na przykład:

  • Jeśli inXType ma wartość int, devecSiInXType ma wartość int.
  • Jeśli inXType ma wartość int2, devecSiInXType ma wartość int. Tablica jest spłaszczoną reprezentacją: ma dwa razy więcej elementów skalarnych niż elementy wektorowe z 2 komponentami. W ten sam sposób działają metody copyFrom() metody Allocation.
  • Jeśli inXType ma wartość uint, deviceSiInXType ma wartość int. Podpisana wartość w tablicy Java jest interpretowana jako wartość bez znaku dla tego samego wzorca bitowego w alokacji. W ten sam sposób działają metody copyFrom() metody Allocation.
  • Jeśli inXType ma wartość uint2, deviceSiInXType ma wartość int. Jest to połączenie sposobu obsługi znaczników int2 i uint: tablica jest spłaszczoną reprezentacją, a podpisane w tablicy wartości 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 skryptu są wektorowe po stronie Javy, a wynik wektorowy skryptu – nie.
  • Niepodpisane dane wejściowe skryptu są reprezentowane po stronie Javy jako podpisane dane wejściowe o tym samym rozmiarze, natomiast wynik bez podpisu skryptu jest reprezentowany po stronie Javy jako typ rozszerzonego podpisu (z wyjątkiem przypadku ulong).

Więcej przykładowych 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, RenderScriptIntrinsic i Hello Compute przedstawiają wykorzystanie interfejsów API opisanych na tej stronie.