Omówienie RenderScript

RenderScript to framework do wykonywania wymagających pod względem obliczeniowym zadań na urządzeniach z Androidem. 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 się skupić 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.

Aby zacząć pracę z RenderScript, musisz zrozumieć 2 główne zagadnienia:

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 pragma (#pragma version(1)), która określa wersję języka jądra RenderScript używanego w tym skrypcie. Obecnie jedyną prawidłową wartością jest 1.
  • deklaracja pragma (#pragma rs java_package_name(com.example.app)), która deklaruje nazwę pakietu klas Java odzwierciedlonych w tym skrypcie; Pamiętaj, że plik .rs musi być częścią pakietu aplikacji, a nie 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 są one przydatne do początkowej konfiguracji lub sekwencyjnych obliczeń w ramach większego potoku przetwarzania.
  • 0 lub więcej globalnych zmiennych skryptu. Zmienne skryptu globalnego są podobne do zmiennych globalnych w języku C. Możesz uzyskiwać 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. Rdzenie obliczeniowe to funkcje lub kolekcje funkcji, które możesz kierować w RenderScript Runtime do wykonywania równolegle na zbiorze danych. Istnieją 2 rodzaje procesorów: mapowania (zwane też foreach) i 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 uruchamiania 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 ten rdzeń jest uruchamiany na całym wejściu Allocation, z jednym wykonaniem funkcji rdzenia na ElementAllocation.

      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 Androidzie 6.0 (poziom interfejsu API 23) jądro mapowania nie może mieć więcej niż 1 wejścia Allocation.

      Jeśli potrzebujesz więcej danych wejściowych lub wyjściowych Allocations niż ma jądro, te obiekty powinny być powiązane 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 funkcja kumulacji 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 wartością 0. Domyślnie ten rdzeń jest uruchamiany na całym wejściu Allocation, z jednym wykonaniem funkcji akumulatora na ElementAllocation. 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.

      Kernel redukcji ma co najmniej 1 wejście Allocations, ale nie ma wyjścia 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 wersjach).

  • 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 nie określono inaczej): w przypadku aplikacji, które wymagają dokładności zmiennoprzecinkowej zgodnie ze standardem IEEE 754-2008.
  • #pragma rs_fp_relaxed: aplikacje, które nie wymagają ścisłego przestrzegania normy IEEE 754-2008 i mogą tolerować mniejszą dokładność. Ten tryb umożliwia wyrównywanie do zera w przypadku denormacji i zaokrąglania do zera.
  • #pragma rs_fp_imprecise: w przypadku aplikacji, które nie mają ścisłych wymagań dotyczących dokładności. Ten tryb włącza wszystkie opcje w rs_fp_relaxed, a także:
    • Operacje, które dają wynik -0,0, mogą zamiast tego zwrócić wartość +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ęść aplikacji RenderScript 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 obsługi.
  • Jeśli używasz interfejsów API biblioteki wsparcia, pliki APK będą (być może znacznie) większe niż w przypadku korzystania z natywnych interfejsów API (android.renderscript).

Korzystanie z interfejsów API biblioteki obsługi RenderScript

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

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

Pamiętaj, że od wersji 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 pakietu Android SDK.

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 na Androida, aby uwzględnić ustawienia RenderScript:
    • Otwórz plik build.gradle w folderze aplikacji w module aplikacji.
    • Dodaj do pliku te ustawienia RenderScript:

      Groovy

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

      Kotlin

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

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

      • renderscriptTargetApi – określa wersję kodu bajtowego, która ma zostać wygenerowana. Zalecamy ustawienie tej wartości na najniższy poziom interfejsu API, który zapewnia wszystkie używane przez Ciebie funkcje, 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 RenderScript, dodaj import dla klas biblioteki Support:

    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. Utwórz kontekstu może być długotrwałą operacją, ponieważ może tworzyć zasoby na różnych urządzeniach. Jeśli to możliwe, nie powinna być częścią ścieżki krytycznej aplikacji. 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 wszystkie niezbędne 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 podklasach 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 „copy” 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. Na przykład, aby ustawić zmienną int o nazwie threshold, użyj metody Java set_threshold(int), a aby ustawić zmienną rs_allocation o nazwie lookup, użyj metody Java set_lookup(Allocation). Metody 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(). Te uruchomienia są asynchroniczne. W zależności od argumentów funkcji jądra metoda ta przyjmuje co najmniej 1 przydział, który musi mieć te same wymiary. Domyślnie jądro jest wykonywane na podstawie każdej współrzędnej w tych wymiarach. Aby wykonać jądro na podzbiorze tych współrzędnych, jako ostatni argument metody forEach lub reduce podaj odpowiednie 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 redukcyjnego, 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 dalsze używanie dowolnego obiektu należącego do tego kontekstu powoduje wyjątek.

Model wykonania asynchronicznego

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 odniesieniu 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ą podwójne: kod jest czytelniejszy, ponieważ implementacja algorytmu jest zachowana w jednym języku, a potencjalnie szybszy, ponieważ jest mniej przejść między Java a RenderScript w wielu wywołaniach jądra.

W przypadku RenderScript w jednym źródle piszesz jądra zgodnie z opisem w artykule Tworzenie jądra RenderScript. Następnie piszesz wywoływaną funkcję, która wywołuje je za pomocą funkcji rsForEach(). Ten interfejs API przyjmuje jako pierwszy parametr funkcję jądra, a potem przypisuje dane wejściowe i wyjściowe. Podobny interfejs API rsForEachWithOptions() przyjmuje dodatkowy argument typu rs_script_call_t, który określa podzbiór elementów z przydziałów danych wejściowych i wyjściowych, które mają być przetworzone przez funkcję jądrową.

Aby rozpocząć obliczenia RenderScript, wywołaj funkcję wywołującą z Java. Wykonaj czynności opisane w artykule Używanie kodu RenderScript z poziomu kodu Java. Na etapie uruchamiania odpowiednich jąder wywołaj funkcję wywoływaną za pomocą funkcji invoke_function_name(), która rozpocznie całą obliczenia, w tym uruchamianie jąder.

Alokacje są często potrzebne do zapisywania i przekazywania pośrednich wyników z jednego uruchomienia jądra do drugiego. Możesz je tworzyć za pomocą funkcji rsCreateAllocation(). Jedna z łatwych w użyciu form tego interfejsu API to rsCreateAllocation_<T><W>(…), gdzie T to typ danych elementu, a W to szerokość wektora elementu. Interfejs API przyjmuje 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ż identyfikatora alloc dla powiązanej alokacji, aby system mógł jak najszybciej zwolnić zasoby.

Sekcja Tworzenie jądra RenderScript zawiera przykładowy rdzeń, który 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. Kod RenderScript w ramach pojedynczego źródła jest nie tylko łatwiejszy do odczytania, ale też eliminuje konieczność przełączania się między Javą a skryptem podczas uruchamiania jądra. 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 skryptu globalnego o nazwie var zdefiniowanego 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 oddzielne wartości: wartość Java i wartość skryptu. Te wartości działają w ten sposób:

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

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;
  • wykonywanie operacji logicznych (and, or, xor) na wszystkich danych.
  • znajdowanie minimalnej lub maksymalnej wartości w danych;
  • wyszukiwanie konkretnej wartości lub współrzędnych konkretnej wartości w danych;

W Androidzie 7.0 (poziom interfejsu API 24) i nowszych RenderScript obsługuje jądra redukcji, które umożliwiają tworzenie wydajnych algorytmów redukcji przez użytkownika. Kernely redukcji możesz uruchamiać na danych wejściowych o 1, 2 lub 3 wymiarach.

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

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

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

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

typedef struct {
  IndexedVal min, max;
} MinAndMax;

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

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

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

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

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

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

UWAGA: więcej przykładowych funkcji redukcji znajdziesz tutaj.

Aby uruchomić jądro redukcji, środowisko wykonawcze RenderScript tworzy co najmniej jedną zmienną o nazwie element danych akumulatora, która przechowuje stan procesu redukcji. Środowisko wykonawcze RenderScript wybiera liczbę elementów danych licznika w taki sposób, aby zmaksymalizować wydajność. Typ elementów danych licznika (accumType) jest określany przez funkcję licznika jądra – jej pierwszy argument to wskaźnik do elementu danych licznika. Domyślnie każdy element danych licznika jest inicjowany wartością 0 (jakby był memset), ale możesz napisać funkcję inicjującą, aby zrobić coś innego.

Przykład: w 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 jądrze 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 kumulacji sprawdza, czy wartość elementu wejściowego jest mniejsza lub równa minimalnej wartości zapisanej w elemencie danych kumulacji lub większa lub równa maksymalnej wartości zapisanej w elemencie danych kumulacji, i odpowiednio aktualizuje ten element.

Po wywołaniu funkcji akumulatora raz dla każdej współrzędnej w danych RenderScript musi połączyć elementy danych akumulatora w jeden element danych akumulatora. W tym celu możesz napisać funkcję łączącą. Jeśli funkcja kumulacji ma 1 argument wejściowy 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 nie chcesz, aby domyślne działanie było takie, jak w przypadku funkcji combiner, możesz ją nadal napisać).

Przykład: w jądrze addint nie ma funkcji łącznika, więc zostanie użyta funkcja akkumulatora. To jest prawidłowe zachowanie, ponieważ jeśli podzielimy zbiór wartości na 2 części i oddzielnie zsumujemy wartości tych 2 części, otrzymamy ten sam wynik, co przy zsumowaniu całego zbioru.

Przykład: w jądrze findMinAndMax funkcja łącznika sprawdza, czy minimalna wartość zapisana w elemencie danych akumulatora „źródło” (*val) jest mniejsza niż minimalna wartość zapisana w elemencie danych akumulatora „miejsce docelowe” (*accum), i odpowiednio aktualizuje *accum. Podobnie jest w przypadku wartości maksymalnej. W ten sposób *accum zostanie zaktualizowany do stanu, jaki miałby, 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 utworzyć funkcję outconverter. Jeśli chcesz, aby ostateczna wartość elementów danych zbiorczego licznika była wynikiem redukcji, nie musisz pisać funkcji konwertującej wyjście.

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

Przykład: w jądrze findMinAndMax funkcja outconverter 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 uruchomi jądro.
  • initializer(initializerName) (opcjonalnie): określa nazwę funkcji inicjalizacyjnej dla tego rdzenia 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 inicjalizatora, RenderScript zainicjalizuje każdy element danych akumulatory na wartość 0 (jakby był to element danych memset), zachowując się tak, jakby istniała funkcja inicjalizacji o takiej postaci:

    static void initializerName(accumType *accum) {
      memset(accum, 0, sizeof(*accum));
    }
  • accumulator(accumulatorName)(obowiązkowo): określa nazwę funkcji kumulacji dla tego rdzenia redukcji. 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 wywołuje ją tyle razy, ile potrzeba, 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 jest, że zmienne *accum*other zostały zainicjowane, ale nigdy nie zostały przekazane funkcji kumulacji, czyli ani jedna, ani druga nie zostały nigdy zaktualizowane zgodnie z danymi wejściowymi. Na przykład w rdzeniu findMinAndMax funkcja łącząca fMMCombiner sprawdza wyraźnie, czy istnieje zmienna danych idx < 0, której wartość to INITVAL.

    Jeśli nie podasz funkcji łączącej, RenderScript użyje funkcji kumulacyjnej, zachowując się tak, jakby była to funkcja łącząca o tym kształcie:

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

    Funkcja łą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 dane wyjściowe w przypadku tego rdzenia redukcji. Po połą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 końcowego elementu danych licznika obliczonego przez funkcję łączącą.

    Jeśli nie podasz funkcji outconverter, RenderScript skopiuje ostatni element danych akumulatory 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 możesz polegać na kolejności, w jakiej RenderScript wywołuje funkcje initializer, accumulator i combiner; może nawet wywołać niektóre z nich 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 inicjalizacyjna będzie miała dostęp do niezinicjowanego elementu danych licznika. 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 kernel findMinAndMax nie jest deterministyczny: jeśli dane wejściowe zawierają więcej niż jedno wystąpienie tej samej minimalnej lub maksymalnej wartości, nie można określić, które z nich zostanie znalezione przez kernel.

Co musisz zagwarantować?

System RenderScript może uruchamiać jądro na wiele różnych sposobów, dlatego musisz przestrzegać określonych zasad, aby mieć pewność, że jądro będzie działać zgodnie z Twoimi oczekiwaniami. 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 chcesz zrobić z jądrem. 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ściowej”), gdzie 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 nad identyczną minimalną wartością w pozycji 200. W tym przypadku „to samo” oznacza identyczne położenie, a nie tylko identyczną wartość, a funkcje kumulacji i łączenia muszą być inne niż w przypadku funkcji findMinAndMax.

Funkcja inicjalizacyjna musi utworzyć wartość tożsamościową. 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 rzeczywiście wartością tożsamości.

Funkcja łączenia musi być przemienna. Oznacza to, że jeśli AB to elementy danych zbiornika zainicjowane przez funkcję inicjalizacyjną i mogą być przekazane do funkcji zbiornika zero lub więcej razy, to combinerName(&A, &B) musi ustawić A na tę samą wartość, którą combinerName(&B, &A)ustawia B.

Przykład: w 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 jądrze findMinAndMax,

fMMCombiner(&A, &B)
jest taki sam jak
A = minmax(A, B)
Dlatego te 2 sekwencje

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

minmax jest asocjacyjnym, a także fMMCombiner.

Funkcja kumulacji i funkcja łączenia muszą być zgodne z podstawową regułą składania. Oznacza to, że jeśli AB to elementy danych licznika, A zostało zainicjowane przez funkcję inicjalizacyjną i może zostać przekazane do funkcji licznika co najmniej 0 razy, B nie zostało zainicjowane, a args to lista argumentów wejściowych i argumentów specjalnych dla konkretnego wywołania funkcji licznika, to A musi być ustawiony na tę samą wartość:

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

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

  • Zdanie 1 jest takie samo jak A += V
  • Zdanie 2 jest takie samo jak B = 0
  • Zdanie 3 jest takie samo jak B += V, który jest taki sam 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))
  • Zdanie 2 jest takie samo jak B = INITVAL
  • Zdanie 3 jest takie samo jak
    B = minmax(B, IndexedVal(V, X))
    
    co, ponieważ B jest wartością początkową, jest takie samo jak
    B = IndexedVal(V, X)
    
  • Zdanie 4 jest takie samo jak
    A = minmax(A, B)
    
    , które 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 licznika 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 zgodny z odpowiednim argumentem wejściowym. 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 ona tworzona za pomocą odpowiedniej metody 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, czyli 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 (skalarnym, wektorem ani tablicą), 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. 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 parametr javaResultType.
  • Jeśli resultType to uint, uint2 lub uint[15],javaResultType ma wartość long, Long2 lub long[]. Wszystkie wartości parametru resultType mogą być reprezentowane przez parametr javaResultType.
  • Jeśli resultType to ulong, ulong2 lub ulong[15], to javaResultType to long, Long2 lub long[]. Niektóre wartości atrybutu resultType nie mogą być reprezentowane przez atrybut javaResultType.

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

  • Jeśli resultType nie jest typem tablicy, javaFutureType ma wartość result_resultType.
  • Jeśli resultType to tablica o długości Count z elementami typu memberType, to javaFutureType to resultArrayCount_memberType.

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 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, 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. 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 atrybut inXType ma wartość uint, atrybut 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 niepodpisanych 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 sygnowane dane wejściowe o tej samej wielkości po stronie Javy, natomiast niepodpisane dane wyjściowe skryptu są reprezentowane jako rozszerzony sygnowany 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.