Omówienie RenderScript

RenderScript to platforma do wykonywania zadań wymagających dużej mocy obliczeniowej z dużą wydajnością na Androidzie. RenderScript służy przede wszystkim do obliczeń równoległych, ale wiążą się z tym również zadania szeregowe. Środowisko wykonawcze RenderScript umożliwia równolegle pracę z różnymi procesorami dostępnymi na urządzeniu, takimi jak procesory wielordzeniowe i GPU. Dzięki temu możesz skupić się na tworzeniu algorytmów, a nie na planowaniu pracy. Jest on szczególnie przydatny w aplikacjach służących do przetwarzania obrazów, fotografii obliczeniowej i rozpoznawania obrazów.

Przed rozpoczęciem pracy z użyciem RenderScriptu musisz znać 2 główne koncepcje:

  • Sam język jest językiem wywodzącym się z języka C99 i służy do pisania kodu obliczeniowego o dużej mocy. Pisanie jądra skryptu RenderScript zawiera informacje o sposobie jego wykorzystania do zapisywania jąder obliczeniowych.
  • Interfejs control API służy do zarządzania czasem przechowywania zasobów RenderScript i sterowania wykonywaniem jądra. Jest dostępne w 3 językach: Java, C++ w Androidzie NDK i języku jądra C99. Użycie RenderScriptu z kodu Java i Renderowanie z jednym źródłem to odpowiednio pierwsza i trzecia opcja.

Zapisywanie jądra skryptu renderowania

Jądro RenderScript znajduje się zwykle w pliku .rs w katalogu <project_root>/src/rs, a 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 deklaruje wersję języka jądra RenderScript używanego w tym skrypcie. Obecnie jedyną prawidłową wartością jest 1.
  • Deklaracja pragma (#pragma rs java_package_name(com.example.app)), która deklaruje nazwę pakietu klas Java odzwierciedlanych przez ten skrypt. Pamiętaj, że plik .rs musi być częścią pakietu aplikacji, a nie projektu biblioteki.
  • 0 lub więcej funkcji, które można wywołać; Funkcja wywoływana w języku Java to jednowątkowa funkcja renderowania RenderScript, którą można wywołać w kodzie Java za pomocą dowolnych argumentów. Są one często przydatne przy wstępnej konfiguracji lub obliczeniach szeregowych w większym potoku przetwarzania.
  • 0 lub więcej globalnych skryptów. Globalny skrypt jest podobny do zmiennej globalnej w C. Globalne skrypty są dostępne w kodzie Java. Są one często używane do przekazywania parametrów do jąder usługi RenderScript. Globalne skrypty są szczegółowo wyjaśnione tutaj.

  • 0 lub więcej jąder obliczeniowych. Jądro obliczeniowe to funkcja lub zbiór funkcji, które możesz uruchomić równolegle na zbiorze danych przez środowisko wykonawcze RenderScript. Istnieją 2 rodzaje jąder obliczeniowych: jądra mapowania (nazywane też jądrami foreach) i jądra redukcyjne.

    Jądro mapowania to funkcja równoległa, która działa na zbiorze obiektów Allocations o tych samych wymiarach. Domyślnie jest wykonywany raz na każdą współrzędną w tych wymiarach. Służy on zazwyczaj (ale nie wyłącznie) do przekształcania zbioru danych wejściowych Allocations w dane wyjściowe Allocation po jednym Element naraz.

    • Oto przykład prostego jądra mapowania:

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

      Pod wieloma względami jest to identyczna jak w przypadku standardowej funkcji C. Właściwość RS_KERNEL zastosowana do prototypu funkcji określa, że jest to jądro mapowania RenderScript, a nie funkcja, którą można wywołać. Argument in jest wypełniany automatycznie na podstawie danych wejściowych Allocation przekazanych do uruchomienia jądra. Argumenty x i y zostały omówione poniżej. Wartość zwrócona przez jądro jest automatycznie zapisywana w odpowiedniej lokalizacji w danych wyjściowych Allocation. Domyślnie to jądro jest uruchamiane w całej wartości wejściowej Allocation, przy czym jednorazowe uruchomienie funkcji jądra na każde Element w obiekcie Allocation.

      Jądro mapowania może zawierać co najmniej 1 lub więcej danych wejściowych Allocations, 1 wyjściową właściwość Allocation lub oba te elementy. Środowisko wykonawcze RenderScript sprawdza, czy wszystkie przydziały danych wejściowych i wyjściowych mają te same wymiary oraz czy typy przydziałów danych wejściowych i wyjściowych (Element) odpowiadają prototypowi jądra. Jeśli którykolwiek z tych testów nie powiedzie się, RenderScript zgłosi wyjątek.

      UWAGA: wcześniej niż w Androidzie 6.0 (poziom interfejsu API 23) jądro mapowania może mieć tylko 1 wejście Allocation.

      Jeśli potrzebujesz Allocations danych wejściowych lub wyjściowych, których nie ma w jądrze, te obiekty powinny być powiązane z globalnymi kluczami skryptu rs_allocation i mieć do nich dostęp przez jądro lub funkcję nieujawnianą przez rsGetElementAt_type() lub rsSetElementAt_type().

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

      #define RS_KERNEL __attribute__((kernel))
      

    Jądro redukcji to rodzina funkcji, które działają na zbiorze danych wejściowych Allocations o tych samych wymiarach. Domyślnie jej funkcja zasobnika jest wykonywana raz na każdą współrzędną w tych wymiarach. Służy on zwykle (ale nie wyłącznie) do „ograniczenia” zbioru danych wejściowych Allocations do 1 wartości.

    • Oto przykład prostego jądra redukcyjnego, które dodaje Elements jego danych wejściowych:

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

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

      Funkcja redukcji akumulatora jądra musi zwracać wartość void i musi mieć co najmniej 2 argumenty. Pierwszy argument (w tym przykładzie accum) jest wskaźnikiem elementu danych zasobnik, a drugi (w tym przykładzie val) jest wypełniany automatycznie na podstawie danych wejściowych Allocation przekazanych do uruchomienia jądra. Element danych zasobnik jest tworzony przez środowisko wykonawcze RenderScript, a domyślnie inicjowany jest wartość zero. Domyślnie to jądro jest uruchamiane na wszystkich danych wejściowych Allocation. Na każde Element w Allocation zostanie wywołane jedno uruchomienie funkcji akumulatora. Domyślnie końcowa wartość elementu danych zbiorczych jest traktowana w wyniku redukcji i zwracana do Javy. Środowisko wykonawcze RenderScript sprawdza, czy typ Element alokacji wejściowej odpowiada prototypowi funkcji akumulatora. Jeśli ten parametr nie jest zgodny, RenderScript zgłasza wyjątek.

      Jądro redukcji ma co najmniej jedną wartość wejściową Allocations, ale nie ma danych wyjściowych Allocations.

      Jądra redukcyjne są szczegółowo wyjaśnione tutaj.

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

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

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

  • Opcjonalną funkcję init(). Funkcja init() to specjalny typ funkcji wywoływanej przez RenderScript, która jest uruchamiana podczas tworzenia instancji skryptu. Dzięki temu niektóre obliczenia mogą odbywać się automatycznie podczas tworzenia skryptu.
  • 0 lub więcej środowisk i funkcji skryptów statycznych. Globalny skrypt statyczny jest odpowiednikiem globalnego skryptu z tą różnicą, że nie można uzyskać do niego dostępu za pomocą kodu Java. Funkcja statyczna to standardowa funkcja C, która może zostać wywołana z dowolnej funkcji jądra lub w skrypcie, ale nie jest dostępna dla interfejsu Java API. Jeśli do globalnego skryptu lub funkcji nie trzeba mieć dostępu z poziomu kodu Java, zdecydowanie zalecamy zadeklarowanie static.

Ustawianie dokładności liczby zmiennoprzecinkowej

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

  • #pragma rs_fp_full (wartość domyślna, jeśli nic nie jest określone): dotyczy aplikacji, które wymagają dokładności liczebności zmiennoprzecinkowej zgodnie ze standardem IEEE 754-2008.
  • #pragma rs_fp_relaxed: w przypadku aplikacji, które nie wymagają ścisłej zgodności z IEEE 754-2008 i mogą tolerować mniejszą precyzję. Ten tryb umożliwia ustawienie przesuwania do zera dla denormów i zaokrąglania do zera.
  • #pragma rs_fp_imprecise: w przypadku aplikacji, które nie mają rygorystycznych wymagań dotyczących dokładności. Ten tryb włącza wszystkie funkcje rs_fp_relaxed oraz te funkcje:
    • Operacje, które dają -0,0, mogą zamiast tego zwracać +0,0.
    • Operacje na INF i NAN są niezdefiniowane.

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

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

Tworząc aplikację na Androida, która wykorzystuje RenderScript, możesz uzyskać dostęp do jej interfejsu API w Javie na 2 sposoby:

  • android.renderscript – interfejsy API w tym pakiecie klas są dostępne na urządzeniach z Androidem 3.0 (poziom API 11) lub nowszym.
  • android.support.v8.renderscript – interfejsy API w tym pakiecie są dostępne w bibliotece pomocy, dzięki czemu można ich używać na urządzeniach z Androidem 2.3 (poziom interfejsu API 9) lub nowszym.

Oto korzyści:

  • Jeśli używasz interfejsów API biblioteki pomocy, część RenderScript w Twojej aplikacji będzie zgodna z urządzeniami z Androidem 2.3 (poziom interfejsu API 9) lub nowszym, niezależnie od używanych funkcji RenderScript. Dzięki temu aplikacja może działać na większej liczbie urządzeń niż w przypadku natywnych interfejsów API (android.renderscript).
  • Niektóre funkcje RenderScript nie są dostępne za pośrednictwem interfejsów Support Library API.
  • Jeśli używasz interfejsów API biblioteki pomocy, otrzymasz (prawdopodobnie znacznie) większe pliki APK niż przy użyciu natywnych interfejsów API (android.renderscript).

Korzystanie z interfejsów API biblioteki obsługi RenderScript

Aby korzystać z interfejsów API RenderScript biblioteki pomocy, musisz skonfigurować swoje środowisko programistyczne, żeby mieć do nich dostęp. Do korzystania z tych interfejsów API wymagane są te narzędzia pakietu Android SDK:

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

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

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

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

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

      Odlotowy

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

      Kotlin

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

      Wymienione powyżej ustawienia określają konkretne zachowanie w procesie kompilacji Androida:

      • renderscriptTargetApi – określa wersję kodu bajtowego do wygenerowania. Zalecamy ustawienie tej wartości na najniższy poziom interfejsu API, który zapewnia dostęp do wszystkich używanych funkcji, oraz ustawienie renderscriptSupportModeEnabled na true. Prawidłowe wartości tego ustawienia to dowolna liczba całkowita od 11 do najnowszego opublikowanego poziomu interfejsu API. Jeśli minimalna wersja pakietu SDK określona w pliku manifestu aplikacji jest ustawiona na inną wartość, ta wartość jest ignorowana, a wartość docelowa w pliku kompilacji jest używana do ustawienia minimalnej wersji pakietu SDK.
      • renderscriptSupportModeEnabled – określa, że wygenerowany kod bajtowy powinien być przełączany do zgodnej wersji, jeśli urządzenie, na którym działa, nie obsługuje wersji docelowej.
  3. W klasach aplikacji, które korzystają z RenderScript, dodaj import dla klas Biblioteki pomocy:

    Kotlin

    import android.support.v8.renderscript.*
    

    Java

    import android.support.v8.renderscript.*;
    

Używanie RenderScriptu z kodu Java lub Kotlin

Używanie RenderScriptu z kodu Java lub Kotlin zależy od klas interfejsu API znajdujących się w pakiecie android.renderscript lub android.support.v8.renderscript. Większość aplikacji ma ten sam podstawowy wzorzec użytkowania:

  1. Zainicjuj kontekst RenderScript. Kontekst RenderScript utworzony za pomocą create(Context) zapewnia, że skrypt RenderScript może być używany i udostępnia obiekt kontrolujący czas życia wszystkich kolejnych obiektów RenderScript. Tworzenie kontekstu za działanie może być bardzo długotrwałe, ponieważ może tworzyć zasoby na różnych urządzeniach. Jeśli to możliwe, nie powinno się ono znajdować na ścieżce krytycznej aplikacji. Zwykle aplikacja ma w danym momencie tylko jeden kontekst RenderScript.
  2. Utwórz co najmniej jeden obiekt Allocation, który zostanie przekazany do skryptu. Allocation to obiekt RenderScript, który zapewnia miejsce na dane o stałej ilości. Kernele w skryptach przyjmują jako dane wejściowe i wyjściowe obiekty Allocation, a obiekty Allocation są dostępne w jądrach za pomocą rsGetElementAt_type() i rsSetElementAt_type(), gdy są powiązane jako globalne skrypty. Obiekty Allocation umożliwiają przekazywanie tablic z kodu Java do kodu RenderScript i odwrotnie. Obiekty Allocation są zwykle tworzone za pomocą createTyped() lub createFromBitmap().
  3. Utwórz niezbędne skrypty. Gdy używasz RenderScriptu, dostępne są 2 rodzaje skryptów:
    • SkryptC: to skrypty zdefiniowane przez użytkownika zgodnie z opisem w sekcji Zapisywanie jądra skryptu renderowania powyżej. Każdy skrypt ma klasę Java odzwierciedloną przez kompilator RenderScript, aby ułatwić dostęp do skryptu z kodu Java. Klasa ta nosi nazwę ScriptC_filename. Jeśli na przykład powyższe jądro mapowania znajdowało się w lokalizacji invert.rs, a kontekst RenderScript znajdował się już w lokalizacji mRenderScript, kod Java lub Kotlin do utworzenia wystąpienia skryptu miałby postać:

      Kotlin

      val invert = ScriptC_invert(renderScript)
      

      Java

      ScriptC_invert invert = new ScriptC_invert(renderScript);
      
    • ScriptIntrinsic: to wbudowane jądra RenderScript, służące do wykonywania typowych operacji, takich jak rozmycie Gaussa, splot i mieszanie obrazów. Więcej informacji znajdziesz w podklasach ScriptIntrinsic.
  4. Uzupełnij przydziały danymi. Oprócz przydziałów utworzonych za pomocą funkcji createFromBitmap() przydział w chwili jego utworzenia jest zapełniany pustymi danymi. Aby wypełnić alokację, użyj jednej z metod „kopiowania” w narzędziu Allocation. Metody „kopiowania” są synchroniczne.
  5. Ustaw niezbędne globalne wartości skryptu. Globalne możesz ustawić 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). Aby ustawić zmienną rs_allocation o nazwie lookup, użyj metody Java set_lookup(Allocation). Metody setasynchroniczne.
  6. Uruchom odpowiednie jądra i funkcje nieuniknione.

    Metody uruchamiania danego jądra są odzwierciedlane w tej samej klasie ScriptC_filename z metodami o nazwach forEach_mappingKernelName() lub reduce_reductionKernelName(). Wdrożenie odbywa się asynchronicznie. W zależności od argumentów jądra metoda przyjmuje co najmniej 1 alokację, z których wszystkie muszą mieć te same wymiary. Domyślnie jądro wykonuje nad każdą współrzędną w tych wymiarach. Aby uruchomić jądro nad podzbiorem tych współrzędnych, przekaż odpowiednią wartość Script.LaunchOptions jako ostatni argument metody forEach lub reduce.

    Uruchom funkcje wywoływane za pomocą metod invoke_functionName, które są odzwierciedlone w tej samej klasie ScriptC_filename. Wdrożenie odbywa się asynchronicznie.

  7. Pobierz dane z obiektów Allocation i obiektów javaFutureType. Aby uzyskać dostęp do danych z elementu Allocation z kodu Java, należy je skopiować z powrotem do Javy, korzystając z jednej z metod „kopiowania” w kodzie Allocation. Aby uzyskać efekt redukcji jądra, musisz użyć metody javaFutureType.get(). Metody „copy” i get()synchroniczne.
  8. Rozdziel kontekst RenderScript. Kontekst RenderScript można zniszczyć za pomocą polecenia destroy() lub zezwolenia na zbieranie odpadów przez obiekt kontekstu RenderScript. Powoduje to, że dalsze korzystanie z obiektów należących do tego kontekstu zgłasza wyjątek.

Asynchroniczny model wykonywania

Odzwierciedlane metody forEach, invoke, reduce i set są asynchroniczne – każda z nich może wrócić do Javy przed wykonaniem żądanego działania. Jednak poszczególne działania są serializowane w kolejności, w jakiej zostały uruchomione.

Klasa Allocation udostępnia metody kopiowania danych do i z przydziałów. Metoda „kopiowania” jest synchroniczna i zserializowana w odniesieniu do dowolnego z powyższych działań asynchronicznych, które dotyczą tego samego przydziału.

Odbite klasy javaFutureType udostępniają metodę get() służącą do uzyskania wyniku redukcji. get() jest synchroniczny i zserializowany ze względu na redukcję (asynchroniczną).

RenderScript z jednym źródłem

W Androidzie 7.0 (poziom interfejsu API 24) wprowadziliśmy nową funkcję programowania o nazwie Single-Source RenderScript (Renderowanie z jednego źródła), dzięki czemu jądra uruchamia się ze skryptu, w którym są zdefiniowane, a nie z Javy. To podejście jest obecnie ograniczone do mapowania jąder, które w tej sekcji są nazywane po prostu „jądrami”, aby zwiększyć zwięzłość. Ta nowa funkcja obsługuje też tworzenie w skrypcie przydziałów typu rs_allocation. Obecnie można wdrożyć cały algorytm wyłącznie w obrębie skryptu, nawet jeśli wymagane jest kilka uruchomień jądra. Ta korzyść jest podwójna: bardziej czytelny kod, ponieważ pozwala na wdrożenie algorytmu w jednym języku, oraz potencjalnie szybszy kod ze względu na mniejszą liczbę przejść między Javą a RenderScript w trakcie wielu uruchomień jądra.

W obiekcie RenderScript z użyciem pojedynczego źródła tworzysz jądra w sposób opisany w sekcji Zapisywanie jądra skryptu renderowania. Następnie piszesz niewkroczyną funkcję, która wywołuje metodę rsForEach() w celu jej uruchomienia. Ten interfejs API wykorzystuje funkcję jądra jako pierwszy parametr, a po nim alokacje wejściowe i wyjściowe. Podobny interfejs API rsForEachWithOptions() wymaga dodatkowego argumentu typu rs_script_call_t, który określa podzbiór elementów z przydziałów na dane wejściowe i wyjściowe przez funkcję jądra.

Aby rozpocząć obliczanie skryptu RenderScript, wywołaj w języku Java funkcję niemożliwą do wywołania. Wykonaj czynności opisane w sekcji Używanie RenderScriptu z kodu Java. W kroku uruchom odpowiednie jądra i wywołaj funkcję wywoływaną za pomocą metody invoke_function_name(). Spowoduje to uruchomienie całego obliczeń, łącznie z uruchomieniem jąder.

Przydziały są często potrzebne do zapisywania i przekazywania wyników pośrednich z jednego uruchomienia jądra do innego. Aby je utworzyć, użyj funkcji rsCreateAllocation(). Prosty w użyciu interfejs API to rsCreateAllocation_<T><W>(…), gdzie T to typ danych elementu, a W to szerokość wektora elementu. Interfejs API przyjmuje jako argumenty rozmiary z wymiarów X, Y i Z. W przypadku alokacji 1D lub 2D rozmiar wymiaru Y lub Z może być pominięty. Na przykład rsCreateAllocation_uchar4(16384) tworzy przydział 1D składający się ze 16 384 elementów, z których każdy jest typu uchar4.

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

Sekcja Zapisywanie jądra skryptu renderowania zawiera przykładowe jądro, które odwraca obraz. Poniższy przykład rozwija tę zasadę, aby można było zastosować do obrazu więcej niż jeden efekt przy użyciu skryptu renderowania z jednym źródłem. Zawiera inne jądro greyscale, które zmienia kolorowy obraz w czarno-biały. Wywołana funkcja process() stosuje następnie te 2 jądra do obrazu wejściowego i generuje obraz wyjściowy. Przydziały zarówno danych wejściowych, jak i wyjściowych są przekazywane jako argumenty typu rs_allocation.

// File: singlesource.rs

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

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

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

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

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

Funkcję process() możesz wywołać w Javie lub 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 obejmuje dwa uruchomienia jądra, można w całości wdrożyć w samym języku RenderScript. Bez kodu renderowania z jednego źródła konieczne byłoby uruchamianie obu jąder z kodu Java, co oddziela uruchomienia jądra od definicji jądra i utrudnia zrozumienie całego algorytmu. Jest to nie tylko łatwiejsze do odczytania kod RenderScript z jednym źródłem, ale także eliminuje przejście między skryptami w języku Java a skryptem po uruchomieniu jądra. Niektóre algorytmy iteracyjne mogą uruchamiać ją setki razy, co znacznie upraszcza proces przenoszenia.

Globalne skryptu

Skrypt globalny to zwykła zmienna globalna niebędąca static w pliku skryptu (.rs). W przypadku globalnego skryptu o nazwie var zdefiniowanego w pliku filename.rs metoda get_var znajduje się w klasie ScriptC_filename. Jeśli globalnie nie jest const, występuje też metoda set_var.

Dany globalny skrypt ma 2 oddzielne wartości – Java i script. Wartości te zachowują się tak:

  • Jeśli zmienna var ma w skrypcie statyczny inicjator, określa wartość początkową var zarówno w Javie, jak i w skrypcie. W innym przypadku wartość początkowa wynosi zero.
  • Dostęp do zmiennej var w skrypcie odczytu i zapisu.
  • Metoda get_var odczytuje wartość w Javie.
  • Metoda set_var (jeśli istnieje) natychmiast zapisuje wartość w Javie, a wartość skryptu asynchronicznie.

UWAGA: oznacza to, że wartości zapisane w globalnym inicjatorze w skrypcie nie są widoczne dla Javy (chyba że w skrypcie jest to statyczny inicjator).

Głębokość ziaren

Redukcja to proces łączenia zbioru danych w jedną wartość. Jest to przydatny element podstawowy w programowaniu równoległym, w którym znajdują się takie aplikacje jak:

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

W Androidzie 7.0 (poziom interfejsu API 24) i nowszych RenderScript obsługuje jądra redukcji, aby umożliwić wydajne algorytmy redukcji tworzone przez użytkownika. Możesz uruchamiać jądra redukcji dla 1, 2 lub 3 wymiarów.

Powyższy przykład przedstawia jądro redukcji addint. Oto bardziej złożone jądro redukcji findMinAndMax, które znajduje lokalizacje minimalnej i maksymalnej wartości long w jednowymiarowym Allocation:

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

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

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

typedef struct {
  IndexedVal min, max;
} MinAndMax;

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

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

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

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

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

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

UWAGA: więcej przykładowych jąder redukcji znajduje się tutaj.

Aby uruchomić jądro redukcyjne, środowisko wykonawcze RenderScript tworzy co najmniej jedną zmienną o nazwie elementy danych zasobników, która przechowuje stan procesu redukcji. Środowisko wykonawcze RenderScript dobiera liczbę elementów danych zbiorczych w taki sposób, aby zmaksymalizować wydajność. Typ elementów danych zbiorczych (accumType) jest określany przez funkcję zasobnik jądra – pierwszy argument tej funkcji wskazuje element danych akumulatora. Domyślnie każdy element danych zasobnik jest inicjowany do 0 (tak jakby przez memset). Możesz jednak napisać funkcję inicjującą, aby zrobić coś innego.

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

Przykład: w jądrze findMinAndMax używane są elementy danych zasobnika (typu MinAndMax) do śledzenia znalezionych do tej pory wartości minimalnych i maksymalnych. Istnieje funkcja inicjatora, która ustawia odpowiednie wartości odpowiednio na LONG_MAX i LONG_MIN oraz ustawia lokalizacje tych wartości na -1. Oznacza to, że wartości te nie występują w (pustej) części przetworzonej wartości wejściowej.

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

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

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

Po pierwszym wywołaniu funkcji akumulatora dla każdej współrzędnych w danych wejściowych mechanizm RenderScript musi połączyć elementy danych akumulatora w jeden element danych zasobnika. W tym celu możesz napisać funkcję łączącą. Jeśli funkcja zasobnik ma pojedynczą dane wejściowe i brak argumentów specjalnych, nie musisz tworzyć funkcji łączącej. RenderScript użyje jej do łączenia elementów danych zasobnika. (Jeśli to domyślne zachowanie nie jest oczekiwane, możesz utworzyć funkcję łączącą).

Przykład: w jądrze addint nie ma funkcji łączącej, więc zostanie użyta funkcja zasobnik. To prawidłowy proces, ponieważ jeśli podzielimy zbiór wartości na 2 części i dodamy wartości w tych częściach oddzielnie, zsumowanie tych 2 sum będzie równoważne z dodaniem całego zbioru.

Przykład: w jądrze findMinAndMax funkcja łącząca sprawdza, czy minimalna wartość zarejestrowana w elemencie danych „źródłowego” akumulatora *val jest mniejsza od minimalnej wartości zarejestrowanej w elemencie danych tego zasobnika *accum, i aktualizuje się odpowiednio do *accum. Działa to podobnie w przypadku wartości maksymalnej. Spowoduje to zaktualizowanie *accum do stanu, jaki miałby, gdyby wszystkie wartości wejściowe zostały zebrane w obrębie funkcji *accum, a nie tylko do niektórych z nich *accum i elementów *val.

Po połączeniu wszystkich elementów danych zasobnika RenderScript określa wynik redukcji, aby powrócić do Javy. W tym celu możesz napisać funkcję konwertera outconverter. Nie musisz tworzyć funkcji z konwertera, jeśli chcesz, by ostateczna wartość danych zbiorczych skumulowanego zasobnika była wynikiem redukcji.

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

Przykład: w jądrze findMinAndMax funkcja outconverter inicjuje wartość wyniku int2, aby przechowywać lokalizacje wartości minimalnych i maksymalnych wynikających z kombinacji wszystkich elementów danych zbiorczych.

Zapisywanie jądra redukcyjnego

#pragma rs reduce definiuje jądro redukcyjne, podając jego nazwę oraz nazwy i role funkcji składających się na jądro. Wszystkie takie funkcje muszą spełniać warunki static. Jądro redukcyjne zawsze wymaga funkcji accumulator. Możesz pominąć niektóre lub wszystkie pozostałe funkcje, w zależności od tego, czego oczekujesz od jądra.

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

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

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

    static void initializerName(accumType *accum) { … }

    accum to wskaźnik do elementu danych zasobnika, który jest inicjowany przez tę funkcję.

    Jeśli nie podasz funkcji inicjatora, RenderScript zainicjuje do zera każdy element danych zasobnika (jakby memset), zachowując się tak, jakby istniała funkcja inicjatora podobna do tej:

    static void initializerName(accumType *accum) {
      memset(accum, 0, sizeof(*accum));
    }
  • accumulator(accumulatorName) (wymagane): określa nazwę funkcji akumulatora dla tego jądra redukcji. Po uruchomieniu jądra RenderScript wywołuje tę funkcję raz dla każdej współrzędnej strony wejściowej, by zaktualizować element danych akumulatora w zależności od danych wejściowych. 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 skumulowanego przez tę funkcję, który może zostać zmodyfikowany. Od in1 do inN to lub więcej argumentów, które są automatycznie wypełniane na podstawie danych wejściowych przekazanych do uruchomienia jądra, 1 argument na dane wejściowe. Funkcja zasobnik może opcjonalnie przyjmować dowolne argumenty specjalne.

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

  • combiner(combinerName)

    (opcjonalnie): określa nazwę funkcji łączącej w tym jądrze redukcyjnym. Gdy RenderScript wywoła funkcję zasobnika dla każdej współrzędnych w danych wejściowych, będzie ją wywoływać tyle razy, ile jest to konieczne, aby połączyć wszystkie elementy danych akumulatora w jeden element danych akumulatora. Funkcja musi być zdefiniowana w następujący sposób:

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

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

    UWAGA: możliwe, że mechanizm *accum, *other lub oba 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 żadnych danych wejściowych. Na przykład w jądrze findMinAndMax funkcja łącząca fMMCombiner wyraźnie szuka elementu idx < 0, ponieważ wskazuje on element danych zasobnika, którego wartość to INITVAL.

    Jeśli nie podasz funkcji łączenia, RenderScript użyje w jej miejscu funkcji zasobnik, działając tak, jakby istniała taka funkcja:

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

    Funkcja łącząca jest obowiązkowa, jeśli jądro ma więcej niż 1 dane wejściowe, typ danych wejściowych różni się od typu danych akumulatorów lub jeśli funkcja zasobnik przyjmuje co najmniej 1 argument specjalny.

  • outconverter(outconverterName) (opcjonalnie): określa nazwę funkcji konwertera w przypadku tego jądra redukcji. Gdy RenderScript połączy wszystkie elementy danych akumulatora, wywoła tę funkcję, aby określić wynik redukcji w celu powrotu do Javy. Funkcja musi być zdefiniowana w ten sposób:

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

    result to wskaźnik do wyniku danych (przydzielonego, ale nie zainicjowanego przez środowisko wykonawcze RenderScript), który jest inicjowany przez środowisko wykonawcze RenderScript. Jest on inicjowany w wyniku redukcji. resultType to typ danego elementu danych – nie musi być taki sam jak accumType. accum to wskaźnik do końcowego elementu danych w zasobniku obliczonego przez funkcję kombinatora.

    Jeśli nie podasz funkcji konwertera, RenderScript skopiuje końcowy element danych zasobnika do elementu danych z wynikami, działając tak, jakby istniała taka funkcja:

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

    Jeśli potrzebujesz innego typu wyniku niż typ danych skumulowanych, musisz użyć funkcji konwertera.

Zwróć uwagę, że w jądrze występują typy danych wejściowych, typ elementu danych zasobnika i typ wyniku – żaden z nich nie musi być taki sam. Na przykład w jądrze findMinAndMax inny typ danych wejściowych long, typ elementu danych zasobnika MinAndMax i typ wyniku int2 są różne.

Nie możesz przyjąć takiego założenia?

Przy każdym uruchomieniu jądra nie należy polegać na liczbie elementów danych akumulatora utworzonych przez skrypt RenderScript. Nie ma gwarancji, że 2 uruchomienia tego samego jądra z tymi samymi danymi wejściowymi spowodują utworzenie tej samej liczby elementów danych akumulatora.

Nie możesz polegać na kolejności, w której RenderScript wywołuje funkcje inicjatora, zasobnika i kombinatora. Niektóre z nich może nawet wywoływać równolegle. Nie ma gwarancji, że 2 uruchomienia tego samego jądra systemu będą miały tę samą kolejność. Jedyną gwarancją jest to, że tylko funkcja inicjatora zobaczy element danych niezainicjowanego zasobnika. Na przykład:

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

Jedną z konsekwencji jest to, że jądro findMinAndMax nie jest deterministyczne. Jeśli dane wejściowe zawierają więcej niż jedno wystąpienie tej samej wartości minimalnej lub maksymalnej, nie wiesz, które wystąpienie znajdzie jądro.

Co musisz zagwarantować?

System RenderScript może uruchamiać ją na wiele różnych sposobów, dlatego musisz przestrzegać określonych reguł, aby mieć pewność, że jądro będzie działać zgodnie z oczekiwaniami. Jeśli nie będziesz przestrzegać tych reguł, możesz otrzymać nieprawidłowe wyniki, niedeterministyczne działanie lub błędy podczas działania.

Według podanych niżej reguł 2 elementy danych zbiorczych muszą mieć „tę samą wartość”. Co to oznacza? To zależy od tego, co ma zrobić jądro. W przypadku redukcji matematycznej, takiej jak addint, słowo „to samo” zwykle oznacza równość matematyczną. W przypadku wyszukiwania typu „wybierz dowolne”, np. findMinAndMax („znajdź lokalizację minimalnej i maksymalnej wartości wejściowej”), w którym może występować więcej niż 1 wystąpienie identycznych wartości wejściowych, wszystkie lokalizacje danej wartości wejściowej muszą być uważane za „takie same”. W jądrze można użyć podobnego jądra, aby „znaleźć lokalizację najbardziej po lewej” minimalnej i maksymalnej wartości wejściowej, gdzie (np.) wartość minimalna w lokalizacji 100 będzie preferowana zamiast identycznej wartości minimalnej w lokalizacji 200. W przypadku tego jądra system „taki sam” oznaczałby identyczną lokalizację, a nie tylko identyczną wartość. W przypadku funkcji znajdującej wartość minimalną a w przypadku tego jądra argument

Funkcja inicjatora musi utworzyć wartość tożsamości. Oznacza to, że jeśli elementy danych I i A są zasobnikiem danych zainicjowanym przez funkcję inicjatora, a element I nie został nigdy przekazany do funkcji zasobnik (ale mogło tak być A), wówczas

Przykład: w jądrze addint element danych zasobnik jest zainicjowany na zero. Funkcja łącząca w tym jądrze wykonuje dodawanie; 0 to wartość tożsamości do dodania.

Przykład: w jądrze findMinAndMax zainicjowany jest element danych zasobnika INITVAL.

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

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

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

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

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

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

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

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

  • 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 działa wiążącym, więc funkcja łączenia również ma taką rolę.

Przykład: w jądrze findMinAndMax

fMMCombiner(&A, &B)
jest to samo co
A = minmax(A, B)
Te 2 sekwencje to

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

minmax jest powiązaniem, więc fMMCombiner też jest.

Funkcja zasobnik i funkcja łącząca muszą być zgodne z podstawową regułą zwijania. Oznacza to, że jeśli A i B są elementami danych zasobnika, funkcja A została zainicjowana przez funkcję inicjatora i mogła zostać przekazana do funkcji kumulatora zero lub więcej razy, B nie został zainicjowany, a argumenty to lista argumentów wejściowych i specjalnych argumentów dla określonego wywołania funkcji zasobnika. W tych 2 sekwencjach kodu A musi mieć wartość :

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

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

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

Instrukcje 1 i 4 ustawiają A na tę samą wartość, więc to jądro przestrzega podstawowej reguły zwijania.

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

  • Stwierdzenie 1 jest takie samo jak A = minmax(A, IndexedVal(V, X))
  • Stwierdzenie 2 jest takie samo jak B = INITVAL
  • Stwierdzenie 3 jest takie samo jak
    B = minmax(B, IndexedVal(V, X))
    
    , które, ponieważ B jest wartością początkową, jest tym samym co
    B = IndexedVal(V, X)
    
  • Stwierdzenie 4 jest takie samo jak
    A = minmax(A, B)
    
    czyli takie samo jak
    A = minmax(A, IndexedVal(V, X))
    

Instrukcje 1 i 4 ustawiają A na tę samą wartość, więc to jądro przestrzega podstawowej reguły zwijania.

Wywoływanie jądra redukcyjnego z kodu Java

W przypadku jądra redakcji o nazwie kernelName zdefiniowanego w pliku filename.rs dostępne są 3 metody dostępne w klasie ScriptC_filename:

Kotlin

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

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

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

Java

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

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

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

Oto kilka przykładów wywoływania jądra addint:

Kotlin

val script = ScriptC_example(renderScript)

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

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

Java

ScriptC_example script = new ScriptC_example(renderScript);

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

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

Metoda 1 ma jeden argument wejściowy Allocation dla każdego argumentu wejściowego w funkcji zasobnik jądra. Środowisko wykonawcze RenderScript sprawdza, czy wszystkie przydziały wejściowe mają te same wymiary i czy typ Element każdego z przydziałów wejściowych pasuje do odpowiedniego argumentu wejściowego prototypu funkcji akumulatora. Jeśli którykolwiek z tych testów nie powiedzie się, RenderScript zgłosi wyjątek. Jądro jest wykonywane nad każdą współrzędną w tych wymiarach.

Metoda 2 jest taka sama jak metoda 1 z tym wyjątkiem, że metoda 2 przyjmuje dodatkowy argument sc, którego można użyć, aby ograniczyć wykonywanie jądra do podzbioru współrzędnych.

Metoda 3 jest taka sama jak metoda 1 z tą różnicą, że zamiast danych wejściowych alokacji używa ona tablicy Java. Dzięki temu nie musisz pisać kodu, aby jawnie tworzyć alokację i kopiować do niej dane z tablicy Java. Jednak użycie metody 3 zamiast metody 1 nie zwiększa wydajności kodu. Dla każdej tablicy wejściowej metoda 3 tworzy tymczasową jednowymiarową alokację z odpowiednim typem Element i włączoną funkcją setAutoPadding(boolean) oraz kopiuje tablicę do alokacji tak, jakby za pomocą odpowiedniej metody copyFrom() Allocation. Następnie wywołuje metodę 1, przekazując te tymczasowe przydziały.

UWAGA: jeśli aplikacja będzie wykonywać wiele wywołań jądra z użyciem tej samej tablicy lub różnych tablic o tych samych wymiarach i typie elementu, możesz poprawić wydajność, samodzielnie tworząc, wypełniając i ponownie wykorzystując przydziały, zamiast korzystać z metody 3.

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

Kotlin

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

Java

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

Wartość javaResultType jest określana na podstawie resultType funkcji outconverter. O ile resultType nie jest typem bez znaku (skalarny, wektorowy lub tablica), javaResultType jest bezpośrednio odpowiadającym typem Javy. Jeśli resultType jest typem niepodpisanym i istnieje większy typ podpisany przy użyciu Javy, to javaResultType oznacza większy typ podpisany przy użyciu Javy. W przeciwnym razie jest to bezpośrednio odpowiadający typowi Java. Na przykład:

  • Jeśli resultType ma wartość int, int2 lub int[15], javaResultType ma wartość int, Int2 lub int[]. Wszystkie wartości resultType mogą być reprezentowane przez javaResultType.
  • Jeśli resultType ma wartość uint, uint2 lub uint[15], javaResultType ma wartość long, Long2 lub long[]. Wszystkie wartości resultType mogą być reprezentowane przez javaResultType.
  • Jeśli resultType ma wartość ulong, ulong2 lub ulong[15], javaResultType ma wartość long, Long2 lub long[]. Niektóre wartości resultType nie mogą być reprezentowane przez javaResultType.

javaFutureType to typ wyniku przyszłego 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 z członkami typu memberType, javaFutureType ma wartość resultArrayCount_memberType.

Na przykład:

Kotlin

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

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

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

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

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

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

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

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

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

Java

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

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

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

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

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

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

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

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

Jeśli javaResultType jest typem obiektu (uwzględnia typ tablicy), każde wywołanie javaFutureType.get() w tej samej instancji zwróci ten sam obiekt.

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

Metoda 3 i devecSiInXType

devecSiInXType to typ Java odpowiadający wartości inXType odpowiedniego argumentu funkcji zasobnik. O ile inXType nie jest typem bez znaku lub wektorem, devecSiInXType jest bezpośrednio odpowiadającym typem Javy. Jeśli inXType jest niepodpisanym typem skalarnym, devecSiInXType jest typem Java bezpośrednio odpowiadającym podpisanemu typowi skalarnemu o tym samym rozmiarze. Jeśli inXType jest typem wektora ze znakiem, to devecSiInXType jest typem Java, który odpowiada typowi komponentu wektorowego. Jeśli inXType jest typem wektora niepodpisanym, devecSiInXType jest typem Java, który bezpośrednio odpowiada podpisanemu typowi skalarnemu o tym samym rozmiarze co typ komponentu wektorowego. Na przykład:

  • Jeśli inXType ma wartość int, devecSiInXType ma wartość int.
  • Jeśli inXType ma wartość int2, devecSiInXType ma wartość int. Tablica jest reprezentacją spłaszczoną: zawiera dwa razy więcej elementów skalarnych niż alokacja ma dwuskładnikowe elementy wektorowe. W taki sam sposób działają metody copyFrom() elementu Allocation.
  • Jeśli inXType ma wartość uint, deviceSiInXType ma wartość int. Podpisana wartość w tablicy Java jest interpretowana jako nieoznaczona wartość o tym samym schemacie bitowym w alokacji. W taki sam sposób działają metody copyFrom() obiektu Allocation.
  • Jeśli inXType ma wartość uint2, deviceSiInXType ma wartość int. Jest to połączenie sposobu obsługi elementów int2 i uint – tablica jest spłaszczoną reprezentacją, a wartości podpisane w tablicy Java są interpretowane jako niepodpisane wartości elementu RenderScript.

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

  • Dane wejściowe skryptu są spłaszczone po stronie Javy, natomiast wynik wektorowy skryptu jest niespójny.
  • Niepodpisane dane wejściowe skryptu są reprezentowane po stronie Javy jako podpisane dane wejściowe o tym samym rozmiarze, natomiast niepodpisany wynik skryptu jest przedstawiany po stronie Javy jako poszerzony typ podpisany (z wyjątkiem przypadku ulong).

Więcej przykładowych jąder redukcji

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

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

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

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

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

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

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

#define BUCKETS 256
typedef uint32_t Histogram[BUCKETS];

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

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

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

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

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

Dodatkowe przykłady kodu

Przykłady korzystania z interfejsów BasicRenderScript, RenderScriptIntrinsic i Hello Compute dodatkowo pokazują wykorzystanie interfejsów API omówionych na tej stronie.