Omówienie RenderScript

RenderScript to framework do wykonywania wymagających pod względem obliczeniowym zadań na Androidzie z wysoką wydajnością. RenderScript jest przeznaczony głównie do obliczeń równoległych, ale może też przynieść korzyści w przypadku obciążeń sekwencyjnych. Środowisko uruchomieniowe RenderScript równolegle wykonuje zadania na różnych procesorach dostępnych na urządzeniu, takich jak procesory wielordzeniowe i procesory graficzne. Dzięki temu możesz skupić się na wyrażaniu algorytmów, a nie na planowaniu pracy. RenderScript jest szczególnie przydatny w przypadku aplikacji do przetwarzania obrazów, fotografii obliczeniowej lub rozpoznawania obrazów.

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

Tworzenie jądra RenderScript

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

  • Deklaracja 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.
  • 0 lub więcej wywoływalnych funkcji. Funkcja wywoływalna to jednowątkowa funkcja RenderScript, którą można wywołać z kodu Java z dowolnymi argumentami. Często 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. Możesz uzyskać dostęp do zmiennych globalnych skryptu z kodu Java. Są one często używane do przekazywania parametrów do jądra RenderScript. Więcej informacji o zmiennych globalnych skryptu znajdziesz tutaj.

  • Zero lub więcej jąder obliczeniowych. 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 procesorów: mapowania (nazywanych też foreach) oraz redukcji.

    Kernel mapowania to funkcja równoległa, która działa na zbiorze Allocations o tych samych wymiarach. Domyślnie jest wykonywane raz dla każdej współrzędnej w tych wymiarach. Zwykle (ale nie tylko) służy do przekształcania zbioru danych wejściowych Allocations w dane wyjściowe Allocation w czasie Element.

    • Oto przykład prostego rdzenia mapowania:

      uchar4 RS_KERNEL invert(uchar4 in, uint32_t x, uint32_t y) {
        uchar4 out = in;
        out.r = 255 - in.r;
        out.g = 255 - in.g;
        out.b = 255 - in.b;
        return out;
      }

      W większości aspektów jest to identyczne z standardową funkcją C. Właściwość RS_KERNEL zastosowana do prototypu funkcji określa, że funkcja jest jądrem mapowania RenderScript, a nie wywoływalna funkcja. Argument in jest wypełniany automatycznie na podstawie danych wejściowych Allocation przekazanych do uruchomienia jądra. Argumenty xy są omówione poniżej. Wartość zwracana przez jądro jest automatycznie zapisywana w odpowiednim miejscu 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.

      Kernel mapowania może mieć co najmniej 1 wejścia Allocations, 1 wyjście Allocation lub oba te elementy. RenderScript sprawdza w czasie wykonywania, czy wszystkie wejścia i wyjścia Allocations mają te same wymiary oraz czy typy Element wejścia i wyjścia Allocations pasują do prototypu jądra. Jeśli któryś z tych sprawdzeń się nie powiedzie, RenderScript zgłasza wyjątek.

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

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

      UWAGA: RS_KERNEL to makro zdefiniowane automatycznie przez RenderScript dla Twojej wygody:

      #define RS_KERNEL __attribute__((kernel))

    Kernel redukcji to rodzina funkcji, które działają na zbiorze danych wejściowych Allocations o tych samych wymiarach. Domyślnie jej funkcja zasobnika jest wykonywana raz dla każdej współrzędnej w tych wymiarach. Zwykle (ale nie tylko) służy do „zredukowania” zbioru danych wejściowych Allocations do pojedynczej wartości.

    • Oto przykład prostego rdzenia redukcji, które zlicza Elements z wejścia:

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

      Kernel funkcji redukcji składa się z co najmniej 1 funkcji napisanej przez użytkownika. Funkcja #pragma rs reduce służy do definiowania rdzenia przez podanie jego nazwy (w tym przykładzie jest to addint) oraz nazw i ról funkcji, z których składa się rdzeń (w tym przykładzie jest to funkcja accumulator addintAccum). Wszystkie takie funkcje muszą być static. Kernel redukcji zawsze wymaga funkcji accumulator; może też zawierać inne funkcje, w zależności od tego, co ma on robić.

      Funkcja kumulacji jądra redukcji musi zwracać void i mieć co najmniej 2 argumenty. Pierwszy argument (w tym przykładzie accum) to wskaźnik do elementu danych licznika, a drugi (w tym przykładzie val) jest wypełniany automatycznie na podstawie danych wejściowych Allocation przekazanych do uruchamiania jądra. Element danych licznika jest tworzony przez środowisko wykonawcze RenderScript; domyślnie jest inicjowany jako 0. Domyślnie to jądro działa na wszystkich danych wejściowych Allocation, z jednym wykonaniem funkcji akumulatora na Element w Allocation. Domyślnie końcowa wartość elementu danych zbiornika jest traktowana jako wynik redukcji i zwracana do Javy. Runtime RenderScript sprawdza, czy typ Element wejściowego obiektu Allocation pasuje do prototypu funkcji kumulatora. Jeśli tak nie jest, RenderScript zgłasza wyjątek.

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

      Więcej informacji o kernelach redukcji znajdziesz tutaj.

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

    Funkcja jądra mapowania lub funkcja redukcji jądra akumulatora może uzyskiwać dostęp do współrzędnych bieżącego wykonania za pomocą argumentów specjalnych x, yz, które muszą być typu int lub uint32_t. Te argumenty są opcjonalne.

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

  • Opcjonalna funkcja init(). Funkcja init() to specjalny typ wywoływanej funkcji, którą RenderScript wykonuje podczas tworzenia pierwszego wystąpienia skryptu. Dzięki temu niektóre obliczenia mogą być wykonywane automatycznie podczas tworzenia skryptu.
  • 0 lub więcej statycznych zmiennych i funkcji globalnych skryptu. Statyczny skrypt globalny jest taki sam jak skrypt globalny, z tym że nie można uzyskać do niego dostępu z poziomu kodu Java. Funkcja statyczna to standardowa funkcja C, którą można wywołać z dowolnego jądra lub wywoływalnej funkcji w skrypcie, ale która nie jest udostępniana interfejsowi Java API. Jeśli skrypt globalny lub funkcja nie wymagają dostępu z poziomu kodu Java, zdecydowanie zalecamy ich zadeklarowanie static.

Ustawianie dokładności obliczeń zmiennoprzecinkowych

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

  • #pragma rs_fp_full (domyślnie, jeśli 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 umożliwia wyrównywanie do zera w przypadku denormacji i zaokrąglania do zera.
  • #pragma rs_fp_imprecise: dla aplikacji, które nie mają rygorystycznych wymagań dotyczących dokładności. Ten tryb włącza wszystkie opcje w rs_fp_relaxed, a także:
    • Operacje, które zwracają -0,0, mogą zamiast tego zwracać +0.0.
    • Operacje na INF i NAN są nieokreślone.

Większość aplikacji może korzystać z rs_fp_relaxed bez żadnych skutków ubocznych. Może to być bardzo korzystne w przypadku niektórych architektur ze względu na dodatkowe optymalizacje dostępne tylko w przypadku z obniżoną precyzją (np. instrukcje procesora SIMD).

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

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

  • android.renderscript – interfejsy API w tym pakiecie klasy są dostępne na urządzeniach z Androidem 3.0 (poziom interfejsu API 11) lub nowszym.
  • android.support.v8.renderscript – interfejsy API z tego pakietu są dostępne w bibliotece pomocy, co umożliwia ich używanie na urządzeniach z Androidem 2.3 (poziom interfejsu API 9) lub nowszym.

Oto kompromisy:

  • Jeśli używasz interfejsów API biblioteki obsługi, część RenderScript aplikacji będzie zgodna z urządzeniami z Androidem 2.3 (poziom interfejsu API 9) lub nowszym, niezależnie od tego, których funkcji RenderScript używasz. Dzięki temu aplikacja będzie działać na większej liczbie urządzeń niż w przypadku korzystania z natywnego interfejsu API (android.renderscript).
  • Niektóre funkcje RenderScript są niedostępne w interfejsach API biblioteki Support Library.
  • 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

Aby korzystać z interfejsów API RenderScript w bibliotece Support, musisz skonfigurować środowisko programistyczne, aby uzyskać do nich dostęp. Do korzystania z tych interfejsów API wymagane są te narzędzia pakietu Android SDK:

  • Android SDK Tools w wersji 22.2 lub nowszej
  • Android SDK Build-tools w wersji 18.1.0 lub nowszej

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

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

Aby używać interfejsów API RenderScript w bibliotece Support Library:

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

      Groovy

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

      Kotlin

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

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

      • renderscriptTargetApi – określa wersję kodu bajtowego, która ma zostać wygenerowana. Zalecamy ustawienie tej wartości na najniższy poziom interfejsu API, który zapewnia wszystkie używane przez Ciebie funkcje, oraz ustawienie wartości renderscriptSupportModeEnabled na true. Dozwolone wartości tego ustawienia to dowolna wartość całkowita od 11 do najnowszej wersji interfejsu API. Jeśli minimalna wersja pakietu SDK określona w pliku manifestu aplikacji jest inna, ta wartość jest ignorowana, a do ustawienia minimalnej wersji pakietu SDK używana jest docelowa wartość w pliku kompilacji.
      • renderscriptSupportModeEnabled – określa, że wygenerowany kod bajtowy powinien zostać zastąpiony zgodną wersją, jeśli urządzenie, na którym jest uruchomiony, nie obsługuje wersji docelowej.
  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 RenderScript z Java lub kodu Kotlin

Korzystanie z RenderScript z kodu Java lub Kotlin wymaga korzystania z klas interfejsu API znajdujących się w pakiecie android.renderscript lub android.support.v8.renderscript. Większość aplikacji stosuje ten sam podstawowy schemat użytkowania:

  1. Inicjalizowanie kontekstu RenderScript. Kontekst RenderScript utworzony za pomocą funkcji create(Context) zapewnia możliwość korzystania z RenderScript i zawiera obiekt, który umożliwia kontrolowanie czasu trwania 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. Zazwyczaj aplikacja ma tylko 1 kontekst RenderScript naraz.
  2. Utwórz co najmniej 1 element Allocation, który zostanie przekazany do skryptu. Obiekt Allocation to obiekt RenderScript, który zapewnia przechowywanie stałej ilości danych. Kernely w skryptach przyjmują obiekty Allocation jako dane wejściowe i wyjściowe, a w kernelach można uzyskać dostęp do obiektów Allocation za pomocą zmiennych rsGetElementAt_type() i rsSetElementAt_type(), gdy są one powiązane jako zmienne globalne skryptu. Obiekty Allocation umożliwiają przekazywanie tablic z kodu Java do kodu RenderScript i odwrotnie. Obiekty Allocation są zwykle tworzone za pomocą funkcji createTyped() lub createFromBitmap().
  3. Utwórz potrzebne skrypty. W przypadku RenderScript dostępne są 2 typy skryptów:
    • ScriptC: to skrypty zdefiniowane przez użytkownika, jak opisano powyżej w sekcji Tworzenie skryptu RenderScript Kernel. Każdy skrypt ma klasę Java, która jest odzwierciedlana przez kompilator RenderScript, aby ułatwić dostęp do skryptu z kodu Java. Ta klasa ma nazwę ScriptC_filename. Jeśli na przykład kernel mapowania z powyższego przykładu znajduje się w pliku invert.rs, a kontekst RenderScript jest już w pliku mRenderScript, kod Java lub Kotlin służący do utworzenia instancji skryptu będzie wyglądał tak:

      Kotlin

      val invert = ScriptC_invert(renderScript)

      Java

      ScriptC_invert invert = new ScriptC_invert(renderScript);
    • ScriptIntrinsic: to wbudowane w RenderScript jądra do wykonywania typowych operacji, takich jak rozmycie gaussowskie, sprzężenie i blendowanie obrazów. Więcej informacji znajdziesz w sekcji z podklasami ScriptIntrinsic.
  4. Wypełnij sekcję „Podział” danymi. Z wyjątkiem alokacji utworzonych za pomocą funkcji createFromBitmap(), alokacja jest wypełniana pustymi danymi w momencie jej utworzenia. Aby wypełnić alokację, użyj jednej z metod „kopiowania” w Allocation. Metody „kopiowania” są synchroniczne.
  5. Ustaw wszystkie niezbędne globalne zmienne skryptu. Możesz ustawiać zmienne 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 funkcje wywoływane.

    Metody uruchamiania danego jądra są odzwierciedlone w tej samej klasie ScriptC_filename z metodami o nazwach forEach_mappingKernelName() lub reduce_reductionKernelName(). Zmiany te są asynchroniczne. W zależności od argumentów funkcji jądra metoda ta przyjmuje co najmniej 1 rozpód, który musi 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.

    Uruchamianie funkcji wywoływalnych za pomocą metod invoke_functionName, które są odzwierciedlone w tej samej klasie ScriptC_filename. Te uruchomienia są asynchroniczne.

  7. Pobieraj dane z obiektów AllocationjavaFutureType. 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 z kernela redukcji, musisz użyć metody javaFutureType.get(). Metody „copy” i get()synchroniczne.
  8. Zniszcz kontekst RenderScript. Możesz zniszczyć kontekst RenderScript za pomocą funkcji destroy() lub zezwalając na usunięcie obiektu kontekstu RenderScript przez mechanizm garbage collection. Powoduje to, że każde dalsze użycie dowolnego obiektu należącego do tego kontekstu spowoduje wyjątek.

Asynchroniczny model wykonywania

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

Klasa Allocation udostępnia metody „copy” służące do kopiowania danych do i z przydziału. Metoda „copy” jest synchroniczna i serializowana z uwzględnieniem wszystkich wymienionych powyżej działań asynchronicznych, które dotyczą tej samej alokacji.

Odzwierciedlone klasy javaFutureType udostępniają metodę get(), która umożliwia uzyskanie wyniku redukcji. get() jest synchroniczny i jest serializowany w stosunku do redukcji (która jest asynchroniczna).

Single-Source RenderScript

Android 7.0 (interfejs API 24) wprowadza nową funkcję programowania o nazwie RenderScript z jednym źródłem kodu, w której rdzenie są uruchamiane ze skryptu, w którym są zdefiniowane, a nie z języka Java. Obecnie to podejście jest ograniczone do mapowania jąder, które w celu zachowania zwięzłości w tej sekcji nazywamy po prostu „jądrami”. Ta nowa funkcja umożliwia też tworzenie alokacji typu rs_allocation z poziomu skryptu. Teraz można zaimplementować cały algorytm tylko w skrypcie, nawet jeśli wymaga to uruchomienia wielu jąder. Korzyści są 2 korzyści: bardziej czytelny kod, ponieważ pozwala na implementację algorytmu w jednym języku, i potencjalnie szybszy kod dzięki mniejszej liczbie przejść między Javą a RenderScriptem podczas korzystania z wielu jądra systemu.

W przypadku RenderScript jednoźródłowego kody jądra piszesz zgodnie z opisem w artykule Tworzenie kodu jądra RenderScript. Następnie tworzysz niemożliwą funkcję, która wywołuje metodę rsForEach(), aby ją uruchomić. Ten interfejs API przyjmuje jako pierwszy parametr funkcję jądra, a potem przypisuje dane wejściowe i wyjściowe. Podobny interfejs API rsForEachWithOptions() przyjmuje dodatkowy argument typu rs_script_call_t, który określa podzbiór elementów z alokacji danych wejściowych i wyjściowych, które ma przetworzyć funkcja jądra.

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

Alokacje są często potrzebne do zapisywania i przekazywania pośrednich wyników z jednego uruchomienia jądra do drugiego. 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. Interfejs API przyjmuje jako argumenty rozmiary w wymiarach X, Y i Z. W przypadku alokacji 1D lub 2D można pominąć rozmiar wymiaru Y lub Z. Na przykład rsCreateAllocation_uchar4(16384) tworzy alokację 1D z 16 384 elementami, z których każdy ma typ uchar4.

Systemem automatycznie zarządza alokacją. Nie musisz ich wyraźnie zwalniać ani uwalniać. Możesz jednak wywołać funkcję rsClearObject(rs_allocation* alloc), aby wskazać, że nie potrzebujesz już uchwytu alloc do powiązanej alokacji, aby system mógł jak najszybciej zwolnić zasoby.

Sekcja Zapisywanie jądra RenderScriptu zawiera przykładowe jądro, które odwraca obraz. Przykład poniżej rozszerza to o możliwość zastosowania do obrazu więcej niż 1 efektu za pomocą RenderScript z jednego źródła. Zawiera ono inny rdzeń, greyscale, który zamienia obraz kolorowy w czarno-biały. Funkcja wywoływalna process() stosuje następnie te 2 jądra kolejno do obrazu wejściowego i generuje obraz wyjściowy. Przydziały danych wejściowych i wyjściowych są przekazywane jako argumenty typu rs_allocation.

// File: singlesource.rs

#pragma version(1)
#pragma rs java_package_name(com.android.rssample)

static const float4 weight = {0.299f, 0.587f, 0.114f, 0.0f};

uchar4 RS_KERNEL invert(uchar4 in, uint32_t x, uint32_t y) {
  uchar4 out = in;
  out.r = 255 - in.r;
  out.g = 255 - in.g;
  out.b = 255 - in.b;
  return out;
}

uchar4 RS_KERNEL greyscale(uchar4 in) {
  const float4 inF = rsUnpackColor8888(in);
  const float4 outF = (float4){ dot(inF, weight) };
  return rsPackColorTo8888(outF);
}

void process(rs_allocation inputImage, rs_allocation outputImage) {
  const uint32_t imageWidth = rsAllocationGetDimX(inputImage);
  const uint32_t imageHeight = rsAllocationGetDimY(inputImage);
  rs_allocation tmp = rsCreateAllocation_uchar4(imageWidth, imageHeight);
  rsForEach(invert, inputImage, tmp);
  rsForEach(greyscale, tmp, outputImage);
}

Funkcję process() możesz wywołać z Java lub Kotlin w ten sposób:

Kotlin

val RS: RenderScript = RenderScript.create(context)
val script = ScriptC_singlesource(RS)
val inputAllocation: Allocation = Allocation.createFromBitmapResource(
        RS,
        resources,
        R.drawable.image
)
val outputAllocation: Allocation = Allocation.createTyped(
        RS,
        inputAllocation.type,
        Allocation.USAGE_SCRIPT or Allocation.USAGE_IO_OUTPUT
)
script.invoke_process(inputAllocation, outputAllocation)

Java

// File SingleSource.java

RenderScript RS = RenderScript.create(context);
ScriptC_singlesource script = new ScriptC_singlesource(RS);
Allocation inputAllocation = Allocation.createFromBitmapResource(
    RS, getResources(), R.drawable.image);
Allocation outputAllocation = Allocation.createTyped(
    RS, inputAllocation.getType(),
    Allocation.USAGE_SCRIPT | Allocation.USAGE_IO_OUTPUT);
script.invoke_process(inputAllocation, outputAllocation);

Ten przykład pokazuje, jak algorytm, który wymaga uruchomienia 2 jądra, można wdrożyć całkowicie w języku RenderScript. Bez funkcji jednoźródłowego RenderScript musiałbyś uruchamiać oba rdzenie z kodu Javy, oddzielając ich uruchamianie od definicji rdzeni, co utrudniałoby zrozumienie całego algorytmu. 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 algorytmy iteracyjne mogą uruchamiać jądra setki razy, co powoduje znaczne obciążenie podczas takich przejść.

Parametry globalne skryptu

Zmienna globalna skryptu to zwykła zmienna globalna inna niż static w pliku skryptu (.rs). W przypadku zmiennej skryptu o nazwie var zdefiniowanej w pliku filename.rs będzie dostępna metoda get_var w klasie ScriptC_filename. Jeśli wartość globalna nie jest const, dostępna będzie też metoda set_var.

Podany globalny skrypt ma 2 osobne wartości: wartość Java i wartość skryptu. Te wartości działają w ten sposób:

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

UWAGA: oznacza to, że z wyjątkiem statycznych inicjalizowanych w skrypcie wartości zapisanych w zmiennych globalnych w skrypcie nie są one widoczne dla Javy.

Szczegółowe informacje o rdzeniu funkcji redukcji

Redukowanie to proces łączenia zbioru danych w jedną wartość. Jest to przydatna funkcja w programowaniu równoległym, która ma zastosowanie w takich sytuacjach:

  • obliczanie sumy lub iloczynu wszystkich danych;
  • wykonywania operacji logicznych (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 RenderScript obsługuje jądra redukcji, aby umożliwić tworzenie wydajnych algorytmów redukcji przez użytkowników. Kernely redukcji możesz uruchamiać na danych wejściowych o 1, 2 lub 3 wymiarach.

Powyższy przykład przedstawia proste jądro redukcji addint. Oto bardziej skomplikowany kernel redukcji findMinAndMax, który znajduje położenie minimalnej i maksymalnej wartości long w jednowymiarowym zbiorze danych Allocation:

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

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

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

typedef struct {
  IndexedVal min, max;
} MinAndMax;

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

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

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

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

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

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

UWAGA: więcej przykładów funkcji redukcji jąder 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 uruchomieniowe RenderScript wybiera liczbę elementów danych licznika w taki sposób, aby zmaksymalizować wydajność. Typ elementów danych licznika (accumType) jest określany przez funkcję licznika jądra – jej pierwszym argumentem jest wskaźnik do elementu danych licznika. Domyślnie każdy element danych licznika jest inicjowany wartością 0 (jakby była to wartość memset). Możesz jednak napisać funkcję inicjalizacyjną, aby wykonać inną operację.

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

Przykład: w rdzeniu findMinAndMax elementy danych zbiornika (typu MinAndMax) służą do śledzenia znalezionych do tej pory minimalnej i maksymalnej wartości. Funkcja inicjalizacyjna ustawia te wartości odpowiednio na LONG_MAXLONG_MIN oraz na -1, co oznacza, że wartości nie występują w (pustej) części przetworzonego wejścia.

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

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

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

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ę łączącą. Jeśli funkcja kumulacji ma 1 argument i nie zawiera argumentów specjalnych, nie musisz pisać funkcji łączącej. RenderScript użyje funkcji kumulacji do złączenia elementów danych kumulacji. (Jeśli chcesz, aby to domyślne działanie nie spełniało Twoich oczekiwań, możesz utworzyć funkcję łączącą).

Przykład: w jądrze addint nie ma funkcji łącznika, więc zostanie użyta funkcja akkumulatora. 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. Podobnie jest w przypadku wartości maksymalnej. W ten sposób *accum zostanie zaktualizowana do stanu, jaki miałaby, gdyby wszystkie wartości wejściowe zostały zebrane w *accum, a nie w *accum*val.

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

Przykład: w jądrze addint nie ma funkcji outconverter. Końcowa 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 inicjalizuje wartość wyniku int2, aby przechowywać lokalizacje wartości minimalnej i maksymalnej wynikające z połączenia wszystkich elementów danych kumulatora.

Tworzenie jądra redukcji

#pragma rs reduce definiuje jądro redukcji, podając jego nazwę oraz nazwy i role funkcji, z których się ono składa. Wszystkie takie funkcje muszą być static. Kernel redukcji zawsze wymaga funkcji accumulator. W zależności od tego, co ma robić kernel, możesz pominąć niektóre lub wszystkie pozostałe funkcje.

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

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

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

    static void initializerName(accumType *accum) {  }

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

    Jeśli nie podasz funkcji inicjującej, RenderScript zainicjuje każdy element danych akumulatora do 0 (jakby to funkcja memset), działając tak, jakby istniała taka funkcja:

    static void initializerName(accumType *accum) {
      memset(accum, 0, sizeof(*accum));
    }
  • accumulator(accumulatorName)(obowiązkowo): określa nazwę funkcji kumulacji dla tego rdzenia redukcji. Gdy uruchomisz jądro, RenderScript wywołuje tę funkcję raz dla każdej współrzędnej w danych wejściowych, aby w jakiś sposób zaktualizować element danych licznika zgodnie z danymi wejściowymi. Funkcja musi być zdefiniowana w ten sposób:

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

    accum to wskaźnik do elementu danych licznika, który ma być zmodyfikowany przez tę funkcję. in1 do inN to co najmniej 1 argument, który jest automatycznie wypełniany na podstawie danych wejściowych przekazanych do uruchamiania jądra, po jednym argumencie na dane wejściowe. Funkcja kumulacji może opcjonalnie przyjmować dowolne argumenty specjalne.

    Przykładem jądra z większą liczbą danych wejściowych jest dotProduct.

  • combiner(combinerName)

    (opcjonalnie) określa nazwę funkcji łączenia dla tego rdzenia redukcji. Po wywołaniu przez RenderScript funkcji kumulacji raz dla każdej współrzędnej w danych wejściowych wywołuje ją tyle razy, ile razy jest to konieczne, aby połączyć wszystkie elementy danych kumulacji w jeden element danych kumulacji. Funkcja musi być zdefiniowana w ten sposób:

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

    accum to wskaźnik do elementu danych licznika „destination” (miejsce docelowe) w przypadku funkcji, którą chcesz zmodyfikować. other to wskaźnik do elementu danych akumulatora „source”, który ma być „połączony” z elementem *accum.

    UWAGA: możliwe, że *accum, *other lub obie te wartości zostały zainicjowane, ale nigdy nie zostały przekazane do funkcji akumulatora. 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, RenderScript użyje funkcji kumulacyjnej, zachowując się tak, jakby istniała funkcja łącząca o tym kształcie:

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

    Funkcja łącznika jest wymagana, jeśli jądro ma więcej niż 1 wejście, jeśli typ danych wejściowych nie jest taki sam jak typ danych kumulatora lub jeśli funkcja kumulatora przyjmuje co najmniej 1 argument specjalny.

  • outconverter(outconverterName)(opcjonalnie): określa nazwę funkcji konwertującej wyjście w przypadku tego rdzenia redukcji. Po złączeniu wszystkich elementów danych w akumulatorze RenderScript wywołuje tę funkcję, aby określić wynik redukcji, który ma zostać zwrócony do Javi. Funkcja musi być zdefiniowana w ten sposób:

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

    result to wskaźnik do elementu danych wyniku (przydzielony, ale nie zainicjowany przez środowisko wykonawcze RenderScript), który ma być zainicjowany za pomocą wyniku funkcji redukcji. resultType to typ tego elementu danych, który nie musi być taki sam jak accumType. accum to wskaźnik do ostatniego elementu danych licznika obliczonego przez funkcję łączącą.

    Jeśli nie podasz funkcji outconverter, RenderScript skopiuje ostatni element danych gromadzonych przez akumulator do elementu danych wyniku, zachowując się tak, jakby istniała funkcja outconverter o tym kształcie:

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

    Jeśli chcesz uzyskać inny typ wyniku niż typ danych zbiornika, funkcja outconverter jest wymagana.

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

Czego nie można zakładać?

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

Nie należy 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 dwa uruchomienia tego samego jądra z tym samym wejściem będą się odbywać w tym samym porządku. Jedyną gwarancją jest to, że tylko funkcja inicjująca zobaczy niezainicjowany element danych akumulatora. Na przykład:

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

Jednym z efektów tego jest to, że jądro findMinAndMax nie jest deterministyczne: jeśli dane wejściowe zawierają więcej niż jedno wystąpienie tej samej wartości minimalnej lub maksymalnej, nie można określić, które wystąpienie zostanie znalezione przez jądro.

Co musisz zagwarantować?

System RenderScript może 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 uzyskać nieprawidłowe wyniki, niedeterministyczne działanie lub błędy czasu wykonywania.

Reguły poniżej często określają, że 2 elementy danych licznika muszą mieć „tę samą wartość”. Co to oznacza? To zależy od tego, co ma robić jądro. W przypadku uproszczenia matematycznego, takiego jak addint, zwykle sensowne jest, aby „to samo” oznaczało matematyczne równość. W przypadku wyszukiwania typu „wybierz dowolne”, takiego jak findMinAndMax („znajdź lokalizację minimalnej i maksymalnej wartości wejściowych”), w którym może wystąpić więcej niż jedno wystąpienie identycznych wartości wejściowych, wszystkie lokalizacje danej wartości wejściowej muszą być uważane za „te same”. Możesz napisać podobne jądro, aby „znaleźć położenie najbardziej lewego minimum i maksymum wartości wejściowych”, gdzie (np.) preferowana jest minimalna wartość w pozycji 100 zamiast identycznej minimalnej wartości w pozycji 200. W tym przypadku „to samo” oznacza identyczne położenie, a nie tylko identyczną wartość, a funkcje kumulacji i łączenia muszą być inne niż w przypadku funkcji findMinAndMax.

Funkcja inicjująca musi utworzyć wartość tożsamości. Oznacza to, że jeśli IA to elementy danych licznika zainicjalizowane przez funkcję inicjalizacyjną, a wartość I nigdy nie została przekazana funkcji licznika (ale A mogła zostać przekazana), to:

Przykład: w jądrze addint element danych licznika jest inicjowany wartością 0. Funkcja łącznika w tym rdzeniu wykonuje dodawanie; zero jest wartością tożsamości dodawania.

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

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

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

Funkcja łącząca musi być przemienna. Oznacza to, że jeśli A i B 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 combiner dodaje 2 wartości elementów danych z akumulatory; dodawanie jest przechodnie.

Przykład: w jądrze findMinAndMax funkcja fMMCombiner(&A, &B) jest taka sama jak funkcja A = minmax(A, B), a funkcja minmax jest przemienna, więc funkcja fMMCombiner też.

Funkcja łączenia musi być asocjacyjna. Oznacza to, że jeśli A, BC to elementy danych zbiornika zainicjowane przez funkcję inicjalizacyjną i mogą być przekazane do funkcji zbiornika co najmniej raz, to te 2 sekwencje kodu muszą być ustawione na A ta sama wartość:

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

Przykład: w jądrze addint funkcja combiner dodaje 2 wartości elementu danych licznika:

  • A = A + B
    A = A + C
    // Same as
    //   A = (A + B) + C
  • B = B + C
    A = A + B
    // Same as
    //   A = A + (B + C)
    //   B = B + C

Dodawanie jest skojarzone, więc funkcja łączenia też.

Przykład: w kernelu findMinAndMax,

fMMCombiner(&A, &B)
jest taki sam jak
A = minmax(A, B)
Te dwie 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 asocjacyjnym, a także fMMCombiner.

Funkcja zasobnika i funkcja łączącego muszą być 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:

  • Zdanie 1 jest takie samo jak A += V
  • Stwierdzenie 2 jest takie samo jak B = 0
  • Zdanie 3 jest takie samo jak B += V, czyli takie samo jak B = V
  • Zdanie 4 jest takie samo jak A += B, czyli takie samo jak A += V

W oświadczeniach 1 i 4 parametr A ma tę samą wartość, więc to jądro stosuje podstawową regułę składania.

Przykład: w kernelu findMinAndMax dla wartości wejściowej V w współrzędnej X:

  • Zdanie 1 jest takie samo jak A = minmax(A, IndexedVal(V, X))
  • Stwierdzenie 2 jest takie samo jak B = INITVAL
  • Stwierdzenie 3 jest identyczne z
    B = minmax(B, IndexedVal(V, X))
    , która ze względu na to, że B jest wartością początkową, jest taka sama jak
    B = IndexedVal(V, X)
  • Zdanie 4 jest takie samo jak
    A = minmax(A, B)
    , co jest takie samo jak
    A = minmax(A, IndexedVal(V, X))

W oświadczeniach 1 i 4 parametr A ma tę samą wartość, więc to jądro stosuje podstawową regułę składania.

Wywoływanie jądra redukcji z kodu Java

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

Kotlin

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

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

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

Java

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

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

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

Oto kilka przykładów wywołania funkcji addint:

Kotlin

val script = ScriptC_example(renderScript)

// 1D array
//   and obtain answer immediately
val input1 = intArrayOf()
val sum1: Int = script.reduce_addint(input1).get()  // Method 3

// 2D allocation
//   and do some additional work before obtaining answer
val typeBuilder = Type.Builder(RS, Element.I32(RS)).apply {
    setX()
    setY()
}
val input2: Allocation = Allocation.createTyped(RS, typeBuilder.create()).also {
    populateSomehow(it) // fill in input Allocation with data
}
val result2: ScriptC_example.result_int = script.reduce_addint(input2)  // Method 1
doSomeAdditionalWork() // might run at same time as reduction
val sum2: Int = result2.get()

Java

ScriptC_example script = new ScriptC_example(renderScript);

// 1D array
//   and obtain answer immediately
int input1[] = ;
int sum1 = script.reduce_addint(input1).get();  // Method 3

// 2D allocation
//   and do some additional work before obtaining answer
Type.Builder typeBuilder =
  new Type.Builder(RS, Element.I32(RS));
typeBuilder.setX();
typeBuilder.setY();
Allocation input2 = createTyped(RS, typeBuilder.create());
populateSomehow(input2);  // fill in input Allocation with data
ScriptC_example.result_int result2 = script.reduce_addint(input2);  // Method 1
doSomeAdditionalWork(); // might run at same time as reduction
int sum2 = result2.get();

Metoda 1 ma jeden argument wejściowy Allocation dla każdego argumentu wejściowego w funkcji zasobnika jądra. Runtime RenderScript sprawdza, czy wszystkie wejściowe alokacje mają te same wymiary i czy typ Element każdego z argumentów wejściowych prototypu funkcji kumulatora jest taki sam. Jeśli którykolwiek z tych testów zakończy się niepowodzeniem, RenderScript wygeneruje wyjątek. Kernel jest wykonywany dla każdej współrzędnej w tych wymiarach.

Metoda 2 jest taka sama jak metoda 1, z tym że przyjmuje dodatkowy argument sc, który można wykorzystać do ograniczenia działania jądra do podzbioru współrzędnych.

Metoda 3 jest taka sama jak metoda 1, z tym że zamiast danych wejściowych z algorytmu przydziału używa danych wejściowych z tablicy w Javie. Dzięki temu nie musisz pisać kodu, aby wyraźnie utworzyć alokację i przekopiować do niej dane z tablicy Java. Jednak użycie metody 3 zamiast metody 1 nie zwiększa wydajności kodu. W przypadku każdego tablicowego wejścia metoda 3 tworzy tymczasową alokację jednowymiarową z odpowiednim typem Element i włączoną funkcją setAutoPadding(boolean), a następnie kopiuje tablicę do alokacji, tak jakby była używana metoda copyFrom() funkcji Allocation. Następnie wywołuje metodę 1, przekazując te tymczasowe alokacje.

UWAGA: jeśli Twoja aplikacja będzie wykonywać wiele wywołań jądra z tym samym tablicą lub z różnymi tablicami o tych samych wymiarach i tym samym typie elementu, możesz zwiększyć wydajność, ręcznie tworząc, wypełniając i ponownie używając alokacji zamiast stosować Metodę 3.

javaFutureType, typ zwracany przez odzwierciedlone metody redukcji, to odzwierciedlona klasa statyczna zagnieżdżona w klasie ScriptC_filename. Jest to przyszły wynik wykonania funkcji reduce kernel. Aby uzyskać rzeczywisty wynik wykonania, wywołaj metodę get() tej klasy, która zwraca wartość typu javaResultType. get() jest synchroniczna.

Kotlin

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

Java

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

javaResultType jest określany na podstawie resultType funkcji outconverter. Jeśli resultType nie jest typem bez znaku (skalarne, wektor lub tablica), javaResultType jest bezpośrednio odpowiadającym typem Java. Jeśli resultType jest typem bez znaku, a istnieje większy typ podpisany w języku Java, javaResultType jest tym większym typem podpisanym w języku Java; w przeciwnym razie jest to bezpośrednio odpowiadający typ w języku Java. Na przykład:

  • Jeśli resultType to 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 parametr javaResultType.
  • Jeśli resultType to ulong, ulong2 lub ulong[15], to 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, 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 to typ obiektu (w tym typ tablicy), każde wywołanie javaFutureType.get() w tym samym wystąpieniu zwróci ten sam obiekt.

Jeśli javaResultType nie może reprezentować wszystkich wartości typu resultType, a jądro funkcji redukcji wygeneruje wartość nie do reprezentowania, javaFutureType.get() rzuci wyjątek.

Metoda 3 i devecSiInXType

devecSiInXType to typ Java odpowiadający typowi inXType odpowiedniego argumentu funkcji akumulatory. Jeśli inXType nie jest typem bez znaku ani typem wektora, devecSiInXType jest bezpośrednio odpowiadającym typem Java. Jeśli inXType to typ skalar bez znaku, devecSiInXType to typ Java bezpośrednio odpowiadający sygnalizowanemu typowi skalarnemu o tej samej wielkości. Jeśli inXType to podpisany typ wektora, to devecSiInXType to typ Java bezpośrednio odpowiadający typowi elementu wektora. Jeśli inXType to typ wektora bez znaku, devecSiInXType to typ Java bezpośrednio odpowiadający sygnalizowanemu typowi skalarnemu o tej samej wielkości co typ komponentu wektora. Na przykład:

  • Jeśli inXType to int, to devecSiInXType to int.
  • Jeśli inXType to int2, to devecSiInXType to int. Tablica jest spłaszczoną reprezentacją: ma dwa razy więcej elementów skalarnych niż elementów wektora o 2 komponentach. Działa to tak samo jak metody copyFrom() w bibliotece Allocation.
  • Jeśli inXType ma wartość uint, deviceSiInXType ma wartość int. Wartość ze znakiem w tablicy Java jest interpretowana jako wartość bez znaku o tym samym wzorze bitowym w alokacji. Działa to tak samo jak w przypadku metod copyFrom() w klasie Allocation.
  • Jeśli atrybut inXType ma wartość uint2, atrybut deviceSiInXType ma wartość int. Jest to połączenie sposobu obsługi funkcji int2uint: tablica jest spłaszczoną reprezentacją, a wartości podpisane tablicy Java są interpretowane jako wartości elementów bez znaku w RenderScript.

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

  • Wektor wejściowy skryptu jest spłaszczony po stronie Java, a wynik wektorowy skryptu nie jest spłaszczony.
  • Niepodpisane dane wejściowe skryptu są reprezentowane jako podpisane dane wejściowe o tej samej wielkości po stronie Javy, natomiast niepodpisane dane wyjściowe skryptu są reprezentowane jako rozszerzony podpisany typ po stronie Javy (z wyjątkiem typu ulong).

Więcej przykładów funkcji redukcji

#pragma rs reduce(dotProduct) \
  accumulator(dotProductAccum) combiner(dotProductSum)

// Note: No initializer function -- therefore,
// each accumulator data item is implicitly initialized to 0.0f.

static void dotProductAccum(float *accum, float in1, float in2) {
  *accum += in1*in2;
}

// combiner function
static void dotProductSum(float *accum, const float *val) {
  *accum += *val;
}
// Find a zero Element in a 2D allocation; return (-1, -1) if none
#pragma rs reduce(fz2) \
  initializer(fz2Init) \
  accumulator(fz2Accum) combiner(fz2Combine)

static void fz2Init(int2 *accum) { accum->x = accum->y = -1; }

static void fz2Accum(int2 *accum,
                     int inVal,
                     int x /* special arg */,
                     int y /* special arg */) {
  if (inVal==0) {
    accum->x = x;
    accum->y = y;
  }
}

static void fz2Combine(int2 *accum, const int2 *accum2) {
  if (accum2->x >= 0) *accum = *accum2;
}
// Note that this kernel returns an array to Java
#pragma rs reduce(histogram) \
  accumulator(hsgAccum) combiner(hsgCombine)

#define BUCKETS 256
typedef uint32_t Histogram[BUCKETS];

// Note: No initializer function --
// therefore, each bucket is implicitly initialized to 0.

static void hsgAccum(Histogram *h, uchar in) { ++(*h)[in]; }

static void hsgCombine(Histogram *accum,
                       const Histogram *addend) {
  for (int i = 0; i < BUCKETS; ++i)
    (*accum)[i] += (*addend)[i];
}

// Determines the mode (most frequently occurring value), and returns
// the value and the frequency.
//
// If multiple values have the same highest frequency, returns the lowest
// of those values.
//
// Shares functions with the histogram reduction kernel.
#pragma rs reduce(mode) \
  accumulator(hsgAccum) combiner(hsgCombine) \
  outconverter(modeOutConvert)

static void modeOutConvert(int2 *result, const Histogram *h) {
  uint32_t mode = 0;
  for (int i = 1; i < BUCKETS; ++i)
    if ((*h)[i] > (*h)[mode]) mode = i;
  result->x = mode;
  result->y = (*h)[mode];
}

Dodatkowe przykłady kodu

Przykłady BasicRenderScript, RenderScriptIntrinsicHello Compute pokazują, jak używać interfejsów API opisanych na tej stronie.