Omówienie RenderScript

RenderScript to framework do wykonywania wymagających pod względem obliczeniowym zadań na Androidzie. Skrypt RenderScript jest przeznaczony głównie do stosowania w obliczeniach równoległych, chociaż zadań również może być korzystne. Środowisko wykonawcze RenderScript działa równolegle działają z różnymi procesorami dostępnymi w urządzeniu, np. procesorami wielordzeniowymi i GPU. Dzięki temu na wyrażaniu algorytmów, a nie na planowaniu pracy. Obecny skrypt RenderScript szczególnie przydaje się w aplikacjach do przetwarzania obrazu, fotografii obliczeniowej lub rozpoznawania obrazów.

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

  • Sam język to język wywodzący się z C99, służący do pisania wydajnych obliczeń obliczeniowych. w kodzie. Tworzenie jądra RenderScriptu zawiera opis: jak jej używać do pisania jąder obliczeniowych.
  • Interfejs control API służy do zarządzania czasem trwania zasobów RenderScript i na sterowanie wykonywaniem jądra systemu operacyjnego. Program jest dostępny w trzech różnych językach: Java oraz C++ w Androidzie. NDK oraz język jądra pochodzącego z C99. Używanie kodu RenderScript z kodu Java i Metoda z jednego źródła RenderScript opisuje pierwsze i trzecie opcje.

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 zawierają:

  • 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 w w projekcie bibliotecznym.
  • 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. Są one często 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 uzyskać dostęp do zmiennych globalnych skryptu z kodu Java. Są one często używane do przekazywania parametrów do jąder RenderScript. Więcej informacji o zmiennych globalnych skryptu znajdziesz tutaj.

  • 0 lub więcej jąder obliczeniowych. Rdzeń obliczeniowy to funkcja lub zbiór funkcji, które możesz kierować w RenderScript Runtime do wykonywania równolegle na zbiorze danych. Są 2 rodzaje obliczeń jądra mapowania (zwane też jądrami pierwszego); i jądro redukcji.

    Jądro mapowania to funkcja równoległa, która działa na zbiorze Allocations o tych samych wymiarach. Domyślnie jest wykonywane raz 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;
      }

      Pod wieloma względami równa się standardowemu 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 dane wejściowe Allocation przekazane do uruchomienia jądra. argumenty x i y są omówiono poniżej. Wartość zwracana z jądra to automatycznie zapisane w odpowiedniej lokalizacji 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. Renderowanie w środowisku wykonawczym sprawdza, czy wszystkie przydziały wejściowe i wyjściowe mają takie same oraz że typy danych wejściowych i wyjściowych typu Element Przydziały są zgodne z prototypem jądra. jeśli któraś z tych kontroli nie powiedzie się, RenderScript zgłasza wyjątek.

      UWAGA: przed Androidem 6.0 (poziom interfejsu API 23) jądro mapowania może nie może mieć więcej niż jednej wartości wejściowej 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))

    Jądro redukcji to rodzina funkcji, które działają na zbiorze danych wejściowych Allocations o tych samych wymiarach. Domyślnie jego funkcja zasobnika jest wykonywana raz na każdą w tych wymiarach. Zwykle (ale nie zawsze) służy do „zredukowania” zbioru danych wejściowych Allocations do pojedynczej wartości.

    • Oto przykład prostej redukcji jądro, które łączy Elements dane wejściowe:

      #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ą mieć typ static. Jądro redukcji zawsze wymaga funkcji accumulator; może też pełnić inne funkcje które ma być wykonane przez jądro.

      Funkcja akumulatora redukcji musi zwracać wartość void i musi mieć co najmniej dwóch argumentów. 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 zasobnika jest tworzony przez środowisko wykonawcze RenderScript. autor: inicjowana jest wartość zerowa. Domyślnie to jądro jest uruchamiane na wszystkich danych wejściowych Allocation, z jednym wykonaniem funkcji akumulatora na Element w: Allocation. Według domyślnie, ostateczna wartość elementu danych akumulatora jest traktowana jako wynik funkcji i wraca 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.

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

    Funkcja jądra mapowania lub funkcja akumulatora jądra redukcji 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.

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

  • Opcjonalna funkcja init(). Funkcja init() to specjalny typ wywoływanej funkcji, którą RenderScript wykonuje podczas tworzenia pierwszego wystąpienia skryptu. Dzięki temu niektóre obliczenia mogą być wykonywane automatycznie podczas tworzenia skryptu.
  • 0 lub więcej globalnych wartości i funkcji skryptów statycznych. Statyczny skrypt globalny jest taki sam jak skrypt globalny, z tym wyjątkiem, że nie można uzyskać do niego dostępu z 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 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 mogą ustawić różne poziomy dokładności liczb zmiennoprzecinkowych:

  • #pragma rs_fp_full (domyślnie, jeśli nic nie zostało określone): w przypadku aplikacji, które wymagają precyzja zmiennoprzecinkowa zgodnie ze standardem IEEE 754-2008.
  • #pragma rs_fp_relaxed: dla aplikacji, które nie wymagają rygorystycznego standardu IEEE 754-2008 zgodności i tolerować mniejszą precyzję. 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 z wartością -0,0 mogą zwracać zamiast tego +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

Gdy tworzysz aplikację na Androida korzystającą z języka RenderScript, jej interfejs API możesz uzyskać za pomocą języka Java na jeden z dwóch sposobów:

Oto wady:

  • Jeśli używasz interfejsów API biblioteki pomocy, część aplikacji w języku RenderScript będzie zgodne z urządzeniami z Androidem 2.3 (poziom interfejsu API 9) lub nowszym niezależnie od tego, który skrypt RenderScript funkcji używanych przez Ciebie. 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 nie są dostępne w interfejsach API z biblioteki pomocy.
  • Jeśli używasz interfejsów API biblioteki pomocy, otrzymasz (prawdopodobnie znacznie) większe pliki APK niż jeśli używasz natywnych (android.renderscript) interfejsów API.

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

Uwaga: począwszy od pakietu Android SDK Build-tools w wersji 24.0.0, przez Androida 2.2, (Poziom 8 interfejsu API) nie jest już obsługiwany.

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

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

  1. Sprawdź, czy masz zainstalowaną wymaganą wersję pakietu Android SDK.
  2. Zaktualizuj ustawienia procesu kompilacji Androida, dodając ustawienia RenderScript:
    • Otwórz plik build.gradle w folderze aplikacji modułu aplikacji.
    • Dodaj do pliku te ustawienia RenderScript:
      GroovyKotlin
              android {
                  compileSdkVersion 33
      
                  defaultConfig {
                      minSdkVersion 9
                      targetSdkVersion 19
      
                      renderscriptTargetApi 18
                      renderscriptSupportModeEnabled true
                  }
              }
              
              android {
                  compileSdkVersion(33)
      
                  defaultConfig {
                      minSdkVersion(9)
                      targetSdkVersion(19)
      
                      renderscriptTargetApi = 18
                      renderscriptSupportModeEnabled = true
                  }
              }
              

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

      • renderscriptTargetApi – określa wersję kodu bajtowego do . 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 Twoja minimalna wersja pakietu SDK określona w pliku manifestu aplikacji jest ustawiona na inną wartość, jest ignorowany, a wartość docelowa w pliku kompilacji służy do określenia minimalnej wartości, Wersja pakietu SDK.
      • 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:
    KotlinJava
    import android.support.v8.renderscript.*
    import android.support.v8.renderscript.*;

Używanie RenderScript z Java lub kodu Kotlin

Używanie RenderScriptu za pomocą kodu Java lub Kotlin opiera się na klasach API znajdujących się w android.renderscript lub pakiet android.support.v8.renderscript. Większość aplikacje są zgodne z tym samym podstawowym wzorcem użytkowania:

  1. Zainicjuj kontekst RenderScriptu. 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. Pamiętaj, że tworzenie kontekstu może być długotrwałą operacją, ponieważ może tworzyć zasoby na różnych urządzeniach. Jeśli to możliwe, nie powinna ona znajdować się na ścieżce krytycznej aplikacji. Zazwyczaj aplikacja ma tylko 1 kontekst RenderScript naraz.
  2. Utwórz co najmniej 1 element Allocation, który zostanie przekazany do skryptu. Allocation to obiekt RenderScript, który udostępnia pamięci masowej ze stałą 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 skryptu RenderScript w kodzie i na odwrót. Obiekty Allocation są zwykle tworzone za pomocą: createTyped() lub createFromBitmap().
  3. Utwórz potrzebne skrypty. Dostępne są 2 rodzaje skryptów podczas korzystania z kodu RenderScript:
    • 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 jądro mapowania te znajdowały się w lokalizacji invert.rs, a kontekst RenderScriptu znajdował się już w mRenderScript, kod Java lub Kotlin do utworzenia wystąpienia skryptu będzie wyglądał tak:
      KotlinJava
      val invert = ScriptC_invert(renderScript)
      ScriptC_invert invert = new ScriptC_invert(renderScript);
    • ScriptIntrinsic: to wbudowane jądra RenderScriptu służące do typowych operacji. takie jak rozmycie Gaussa, splot czy przenikanie obrazów. Więcej informacji znajdziesz w sekcji z podklasami ScriptIntrinsic.
  4. Wypełnij sekcję „Podział” danymi. Z wyjątkiem przydziałów utworzonych za pomocą funkcji createFromBitmap() w polu Alokacja znajdują się puste dane, gdy jest które zostały utworzone po raz pierwszy. 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 ustawić wartości globalne za pomocą metod w sekcji ta sama klasa 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: znajdują się w tej samej klasie ScriptC_filename za pomocą metod o nazwach forEach_mappingKernelName() lub reduce_reductionKernelName(). Zmiany te są asynchroniczne. W zależności od argumentów jądra przyjmuje co najmniej jeden przydział, który musi mieć te same wymiary. Domyślnie jądro uruchamia się w przypadku wszystkich współrzędnych w tych wymiarach, do uruchomienia jądra na podzbiorze tych współrzędnych, przekazać odpowiedni Script.LaunchOptions jako ostatni argument w metodzie forEach lub reduce.

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

  7. Pobieraj dane z obiektów AllocationjavaFutureType. Aby uzyskać dostęp do danych z Allocation z kodu Java, musisz skopiować te dane z powrotem do Javy, używając jednej z metod „copy” w Allocation. Aby uzyskać wynik redukcji jądra, należy użyć metody javaFutureType.get(). Kopia i metody get()synchroniczne.
  8. Przeanalizuj kontekst RenderScriptu. Możesz zniszczyć kontekst RenderScriptu za pomocą funkcji destroy() lub przez włączenie kontekstu RenderScript jako obiekt do czyszczenia pamięci. Powoduje to dalsze korzystanie z należących do niej obiektów do zgłoszenia wyjątku.

Model wykonania asynchronicznego

Wyróżnione forEach, invoke, reduce, a metody set są asynchroniczne – każda może wrócić do Javy przed zakończeniem wymagane działanie. 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. „Kopia” jest synchroniczna i jest zserializowana względem dowolnego działań asynchronicznych powyżej, które mają wpływ na tę samą alokację.

Odzwierciedlone klasy javaFutureType udostępniają metodę get(), która umożliwia uzyskanie wyniku redukcji. get() to synchroniczna i jest zserializowana zgodnie z redukcją (asynchroniczną).

Skrypt RenderScript z jednego źródła

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 jąder mapowania, które w celu zachowania przejrzystości w tej sekcji nazywamy po prostu „jądrami”. Ta nowa funkcja obsługuje też tworzenie alokacji typu rs_allocation w skrypcie. 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 przypadku wielu uruchamianych jąder.

W języku RenderScript na jedno źródło treści zapisujesz jądra w sposób opisany tutaj: Jak napisać jądro 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 danych wejściowych i przydziały danych wyjściowych, które ma przetworzyć funkcja jądra.

Aby rozpocząć obliczenia w RenderScript, wywołujesz funkcję invokable z Javę. Wykonaj czynności opisane w artykule Używanie kodu RenderScript z poziomu kodu Java. W kroku uruchom odpowiednie jądra wywołaj funkcji wywoływanej przez funkcję invoke_function_name(), która spowoduje uruchomienie funkcji całe 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 rozmiary w wymiarów X, Y i Z jako argumentów. W przypadku alokacji 1D lub 2D można pominąć rozmiar wymiaru Y lub Z. Na przykład rsCreateAllocation_uchar4(16384) tworzy przydział 1D o wartości 16384 elementy, z których każdy jest typu uchar4.

Systemem automatycznie zarządza alokacją. Nie musisz ich wyraźnie zwalniać ani uwalniać. Możesz jednak zadzwonić rsClearObject(rs_allocation* alloc), aby wskazać, że nie potrzebujesz już nicku. alloc do przydziału bazowego, aby system mógł jak najszybciej zwolnić zasoby.

Sekcja Tworzenie jądra RenderScriptu zawiera przykład jądro, które odwraca obraz. Poniższy przykład rozwija tę opcję, aby zastosować do obrazu więcej niż 1 efekt. korzystając z języka RenderScript na pojedyncze źródło. Zawiera ono inny rdzeń, greyscale, który zamienia obraz kolorowy w czarno-biały. Funkcja wywoływalna process() stosuje te 2 jądra kolejno do obrazu wejściowego i tworzy obraz wyjściowy. Przydziały danych wejściowych i dane wyjściowe 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:

KotlinJava
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)
// File SingleSource.java

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

Ten przykład pokazuje, jak można w pełni wdrożyć algorytm, który obejmuje 2 uruchomienia jądra 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. Jest to nie tylko kod RenderScript z jednego źródła jest łatwiejszy do odczytania, a dodatkowo eliminuje między Javą a skryptem podczas uruchamiania jądra systemu operacyjnego. Niektóre iteracyjne algorytmy mogą uruchamiać jądra co sprawia, że koszty związane z takimi zmianami są znaczne.

Parametry globalne skryptu

Skrypt globalny to zwykły element inny niż static w pliku skryptu (.rs). Scenariusz globalna o nazwie var zdefiniowany w filename.rs, będzie metoda get_var odzwierciedlona w zajęcia ScriptC_filename. Jeśli wartość globalna nie jest const, dostępna jest 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 skrypcie. W przeciwnym razie wartość początkowa wynosi 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 asychroniecznie.

UWAGA: to oznacza, że oprócz static inicjator w skrypcie, wartości zapisane w wartości globalnej z są niewidoczne dla Javy.

Szczegółowe informacje o rdzeniu funkcji redukcji

Zmniejszanie to proces łączenia zbioru danych w jeden . Jest to przydatny element podstawowy przy programowaniu równoległym z aplikacjami, takimi jak :

  • obliczanie sumy lub iloczynu na wszystkich danych
  • operacje logiczne (and, or, xor) we 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 skrypt RenderScript obsługuje jądro redukcji, do wydajnych algorytmów redukcji napisanych przez użytkownika. Możesz uruchamiać jądra redukcji na danych wejściowych z 1, 2 lub 3 wymiary.

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 jąder 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 z akumulatora w taki sposób, by zmaksymalizować skuteczność. 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 polu addint jądro, elementy danych akumulatora (typu int) są używane do sumowania danych wejściowych . Nie ma funkcji inicjatora, więc każdy element danych akumulatora jest inicjowany zero.

Przykład: In jądro findMinAndMax, czyli elementy danych akumulatora (typu MinAndMax) są używane do śledzenia wartości minimalnych i maksymalnych do tej pory. 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ę zasobnika raz na każdą współrzędną w parametrze danych wejściowych. Zwykle funkcja powinna w jakiś sposób aktualizować element danych akumulatora zgodnie z danymi wejściowymi.

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

Przykład: In jądro findMinAndMax, czyli funkcja akumulatora sprawdza, czy wartość elementu wejściowego jest mniejsza czy równa minimalnej wartości wartość zarejestrowana w elemencie danych akumulatora lub większa lub równa wartości maksymalnej zarejestrowanej w elemencie danych zasobnika, a także aktualizuje ten element odpowiednio się zmienia.

Po wywołaniu funkcji zasobnika raz dla każdej współrzędnej w danych wejściowych RenderScript musi połączyć zasobnik elementów danych razem w jedną pozycję danych akumulatora. Możesz napisać kombinację . Jeśli funkcja kumulacji ma 1 argument i nie zawiera argumentów specjalnych, nie musisz pisać funkcji łączącej. RenderScript użyje funkcji kumulacji do złączenia elementów danych kumulacji. (Możesz utworzyć funkcję łączącą, jeśli to domyślne działanie nie jest tym, w pobliżu.

Przykład: w jądrze addint nie ma funkcji łącznika, więc zostanie użyta funkcja akkumulatora. To jest działa prawidłowo, ponieważ jeśli podzielimy zbiór wartości na dwa części i dodaj wartości z tych 2 części osobno. Suma tych dwóch sum jest równa w ten sposób całą kolekcję.

Przykład: In jądro findMinAndMax, czyli funkcja łącząca sprawdza, czy minimalna wartość zarejestrowana w „źródle” dane akumulatora element *val jest mniejszy niż minimalna wartość zarejestrowana w „miejscu docelowym” element danych zasobnika *accum, aktualizacje *accum odpowiednio się zmienia. Podobnie jest w przypadku wartości maksymalnej. Ta czynność aktualizuje *accum do stanu, jaki uzyskałaby, gdyby wszystkie wartości wejściowe zostały zgromadzone w *accum zamiast kilku w *accum, a jeszcze innych w *val

Po połączeniu wszystkich elementów danych akumulatora RenderScript określa w wyniku redukcji i powrót do Javy. Możesz napisać reklamodawcę zewnętrznego, . Nie musisz pisać funkcji konwertera, aby wartość końcową połączonych elementów danych akumulatora jako wynik redukcji.

Przykład: w jądrze addint nie ma funkcji konwertera zewnętrznego. Ostateczna wartość elementów połączonych danych to suma wszystkich elementów danych wejściowych, czyli wartości, które 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 kombinacji wszystkich elementów danych kumulatora.

Tworzenie jądra redukcji

#pragma rs reduce definiuje jądro redukcji przez podając jego nazwę oraz nazwy i role funkcji, które tworzą gdy ją uruchamiają. Wszystkie takie funkcje muszą być static. Jądro redukcji zawsze wymaga accumulator funkcji; możesz pominąć niektóre lub wszystkie pozostałe funkcje, w zależności od tego, czego chcesz przez jądra.

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

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

  • reduce(kernelName) (wymagane): określa, że jądro redukcji jest nie jest zdefiniowany. Odzwierciedlona metoda Java reduce_kernelName uruchomi jądro.
  • initializer(initializerName) (opcjonalny): określa nazwę funkcja inicjująca tego jądra redukcji. Po uruchomieniu jądra kod RenderScript tę funkcję raz dla każdego elementu danych akumulatora. funkcja musi być zdefiniowana w następujący sposób:

    static void initializerName(accumType *accum) {  }

    accum wskazuje element danych akumulatora dla tej funkcji, aby zainicjować.

    Jeśli nie podasz funkcji inicjalizatora, RenderScript inicjalizuje każdy element danych akumulatory na 0 (jakby 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. Po uruchomieniu jądra kod RenderScript tę funkcję raz na każdą współrzędną wejściowych, aby zaktualizować elementu danych akumulatora w jakiś sposób zgodnie z danymi wejściowymi. Funkcja musi być zdefiniowana w ten sposób:

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

    accum wskazuje element danych akumulatora dla tej funkcji, aby modyfikować. Od in1 do inN to co najmniej jeden argument, który są automatycznie wypełniane na podstawie danych wejściowych przekazywanych do uruchomienia jądra – 1 argument na dane wejściowe. Funkcja zasobnika może opcjonalnie przyjąć dowolny ze argumentów specjalnych.

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

  • combiner(combinerName)

    (opcjonalnie): określa nazwę funkcji łączenia dla tego rdzenia redukcji. Po wywołaniu przez RenderScript funkcji zasobnika raz na każdą współrzędną w danych wejściowych, wywołuje tę funkcję tyle samo by połączyć wszystkie elementy danych akumulatora w jeden elementu danych akumulatora. 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 wskazuje „źródło” element danych akumulatora dla tej funkcji do „połączenia” na *accum.

    UWAGA: to możliwe. że funkcje *accum, *other lub oba zostały zainicjowane, ale nigdy została przekazana do funkcji akumulatora; oznacza to, że któraś z nich nigdy nie została zaktualizowana zgodnie z dowolnymi danymi wejściowymi. Na przykład w polu jądro findMinAndMax, czyli składnik łączący funkcja fMMCombiner wyraźnie sprawdza funkcję idx < 0, ponieważ wskazuje taki element danych akumulatora, którego wartość to INITVAL.

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

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

    Funkcja łącząca jest wymagana, jeśli jądro ma więcej niż 1 dane wejściowe, jeśli dane wejściowe typ danych nie jest taki sam jak typ danych akumulatora lub jeśli funkcja zasobnika przyjmuje taki sam typ lub więcej specjalnych argumentów.

  • outconverter(outconverterName)(opcjonalnie): określa nazwę funkcji konwertującej wyjście w przypadku tego rdzenia redukcji. Po połączeniu przez RenderScript całego zasobnika elementów danych, wywołuje tę funkcję, aby określić wynik funkcji i powrót do języka Java. Funkcja musi być zdefiniowana w ten sposób: to:

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

    result to wskaźnik do elementu danych wyniku (przydzielonego, ale nie zainicjowanego) przez środowisko wykonawcze RenderScript), aby zainicjować tę funkcję z wynikiem funkcji . Parametr resultType określa typ danego elementu danych, który nie musi być taki sam. jako accumType. accum to wskaźnik do ostatniego elementu danych licznika obliczonego przez funkcję łączącą.

    Jeśli nie podasz funkcji outconverter, RenderScript skopiuje końcowy zasobnik elementu danych do wynikowego elementu danych, zachowując się w taki sposób, jakby istniała funkcja przekonwertowania, która wygląda tak:

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

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

Pamiętaj, że 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żesz przyjąć?

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 inicjator, zasobnik i funkcje łączące; może nawet wywoływać niektóre z nich równolegle. Nie ma gwarancji, że dwa uruchomienia tego samego jądra z tymi samymi danymi wejściowymi będą wykonywane w tej samej kolejności; Jedyna gwarantuje, że tylko funkcja inicjująca zobaczy niezainicjowany zasobnik elementu danych. Na przykład:

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

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

Co musisz zagwarantować?

System RenderScript może zdecydować się na uruchomienie jądra w wielu na różne sposoby, musisz przestrzegać pewnych reguł, aby jądro działało w dobry sposób. Jeśli nie będziesz ich przestrzegać, możesz otrzymać nieprawidłowe wyniki. niedeterministyczne zachowanie lub błędy czasu działania.

Reguły poniżej często określają, że 2 elementy danych licznika muszą mieć „tę samą wartość”. Co to oznacza? To zależy od tego, co ma robić jądro. Dla: ograniczenie matematyczne, np. addint, zwykle ma sens dla hasła „tego samego” oznacza równość matematyczną. Wybierz dowolne wyszukaj takie jako findMinAndMax („znajdź lokalizację minimalnej i maksymalnych wartości wejściowych”), w których może wystąpić więcej niż jedno wystąpienie identycznej wartości wejściowej. wszystkie lokalizacje o danej wartości wejściowej muszą być uznawane za „takie 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 inicjująca musi utworzyć wartość tożsamości. Oznacza to, że jeśli IA to elementy danych licznika zainicjowane przez funkcję inicjalizacyjną, a wartość I nigdy nie została przekazana funkcji licznika (ale A mogła zostać przekazana), to:
  • combinerName(&A, &I) musi pozostawić A bez zmian
  • combinerName(&I, &A) musi zostaw I tego samego co A

Przykład: w polu addint jądro, element danych akumulatora zostaje zainicjowany do zera. 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 do: A, bo I to INITVAL.

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

Funkcja łącząca 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 polu addint jądro, funkcja łącząca dodaje dwie wartości danych elementu danych akumulatora; dodanie to przemienne.

Przykład: w jądrze findMinAndMax fMMCombiner(&A, &B) to ta sama wartość co Atrybuty A = minmax(A, B) i minmax są przemienne, więc fMMCombiner również.

Funkcja łączenia musi być asocjacyjna. To znaczy, jeśli A, B i C to elementów danych zasobnika zainicjowanych przez funkcję inicjatora i które mogły zostać przekazane do funkcji akumulatora zero lub więcej razy, dwie poniższe sekwencje kodu muszą ustaw A na tę samą wartość:

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

Przykład: w jądrze addint para klucz-wartość 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 jest skojarzone, więc funkcja łączenia też.

Przykład: w kernelu findMinAndMax,

fMMCombiner(&A, &B)
jest taka sama jak
A = minmax(A, B)
Te dwie sekwencje są
  • A = minmax(A, B)
    A = minmax(A, C)
    // Same as
    //   A = minmax(minmax(A, B), C)
  • B = minmax(B, C)
    A = minmax(A, B)
    // Same as
    //   A = minmax(A, minmax(B, C))
    //   B = minmax(B, C)

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

Funkcja zasobnika i funkcja łączącego muszą być zgodne z podstawową funkcją za pomocą reguły składania. Oznacza to, że jeśli AB to elementy danych akumulatory, A zostało zainicjowane przez funkcję inicjalizacyjną i może zostać przekazane do funkcji akumulatory 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 akumulatory, 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:

  • Polecenie 1 jest takie samo jak A += V.
  • Zdanie 2 jest takie samo jak B = 0
  • Stwierdzenie 3 jest takie samo jak B += V, które jest identyczne z 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 jądrze findMinAndMax jako dane wejściowe wartość V w współrzędnych X:

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

Instrukcje 1 i 4 ustawiają A na tę samą wartość, więc to jądro jest zgodne z podstawowej reguły składania.

Wywołanie 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:

KotlinJava
// 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
// 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:

KotlinJava
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()
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. Środowisko wykonawcze RenderScript sprawdza, czy wszystkie przydziały wejściowe mają te same wymiary i że typ Element każdego z przydział wejściowy odpowiada temu odpowiedniemu argumentowi wejściowemu zasobnika prototyp funkcji. Jeśli któraś z tych kontroli zakończy się niepowodzeniem, RenderScript zgłosi 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 wyjątkiem: zamiast korzystać z danych wejściowych alokacji, wykorzystuje tablicowe dane w Javie. To wygoda, eliminuje konieczność pisania kodu w celu jawnego utworzenia przydziału i skopiowania do niego danych. z tablicy Java. Zastosowanie metody 3 zamiast metody 1 nie zwiększy jednak 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 Przydziały.

UWAGA: jeśli aplikacja będzie wykonywać wiele wywołań jądra za pomocą tej samej tablicy lub z różnymi tablicami o tych samych wymiarach i typie elementu, a nie przez bezpośrednie tworzenie, wypełnianie i ponowne używanie przydziałów. za pomocą metody 3.

javaFutureType, typ zwrotu odzwierciedlonych metod redukcji jest odzwierciedlony statyczna zagnieżdżona klasa w elemencie ScriptC_filename zajęcia. Przedstawia on przyszły rezultat zmniejszenia uruchomienia jądra systemu operacyjnego. Aby uzyskać rzeczywisty wynik uruchomienia, wywołaj metody get() tej klasy, która zwraca wartość typu javaResultType. Funkcja get() jest synchroniczna.

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

javaResultType jest określany na podstawie parametru resultType parametru funkcji konwertera zewnętrznego. O ile resultType nie jest typ bez znaku (skalarny, wektorowy lub tablica), javaResultType jest bezpośrednio odpowiadającym Typ Java. Jeśli resultType jest typem bez podpisu i istnieje większy typ podpisany w Javie, z kolei javaResultType to większy typ podpisany w języku Java; w przeciwnym razie jest bezpośrednio odpowiedniego typu Java. Na przykład:

  • Jeśli resultType to int, int2 lub int[15], to javaResultType to int, Int2, lub int[]. Dozwolone są wszystkie wartości parametru resultType. autorstwa javaResultType.
  • Jeśli resultType to uint, uint2 lub uint[15],javaResultType to long, Long2 lub long[]. Dozwolone są wszystkie wartości parametru resultType. autorstwa 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, który odpowiada do parametru resultType elementu 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.

Na przykład:

KotlinJava
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> =     }
}
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. O ile inXType nie jest typ bez znaku lub typ wektorowy, devecSiInXType to bezpośrednio odpowiadający mu typ wektora Java typu. Jeśli inXType jest typem skalarnym bez znaku, to devecSiInXType jest Typ Java bezpośrednio odpowiadający typowi ze znakiem skalarnym tego samego rozmiaru. Jeśli inXType jest typem wektora podpisanego, devecSiInXType jest typem wektora Java odpowiadający typowi komponentu wektorowego. Jeśli inXType to typ wektora bez znaku, devecSiInXType to typ Java bezpośrednio odpowiadający sygnalizowanemu typowi skalarnemu o tej samej wielkości co typ komponentu wektora. Na przykład:

  • Jeśli inXType ma wartość int, to devecSiInXType jest int.
  • Jeśli inXType to int2, to devecSiInXType to int. Tablica jest spłaszczoną reprezentacją: ma dwa razy więcej elementów skalarnych niż elementów wektora o 2 komponentach. Działa to tak samo jak metody copyFrom() w bibliotece Allocation.
  • Jeśli inXType ma wartość uint, to deviceSiInXType. jest int. Wartość ze znakiem w tablicy Java jest interpretowana jako wartość bez znaku o tym samym wzorze bitowym w alokacji. Działa tak samo jak copyFrom() metody 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 podpisanych tablic Java są interpretowane jako wartości elementów bez znaku w RenderScript.

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

  • Dane wejściowe skryptu są wektorowe po stronie Javy, a wynik wektorowy skryptu – nie.
  • Niepodpisane dane wejściowe skryptu są reprezentowane jako podpisane dane wejściowe o tej samej wielkości po stronie Javy, natomiast niepodpisane dane wyjściowe skryptu są reprezentowane jako rozszerzony podpisany typ po stronie Javy (z wyjątkiem typu ulong).

Więcej przykładów funkcji redukcji

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

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

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

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

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

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

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

#define BUCKETS 256
typedef uint32_t Histogram[BUCKETS];

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

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

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

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

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

Dodatkowe przykłady kodu

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