Omówienie RenderScript

RenderScript to platforma do wykonywania zadań wymagających dużej mocy obliczeniowej z dużą wydajnością na urządzeniu z Androidem. Skrypt RenderScript jest przeznaczony głównie do użycia 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.

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

  • Sam język to język wywodzący się z C99, 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.

Pisanie jądra RenderScriptu

Jądro RenderScript znajduje się zwykle w pliku .rs katalog <project_root>/src/rs; każdy plik .rs jest nazywany script. Każdy skrypt zawiera własny zestaw jąder, funkcji i zmiennych. Skrypt może zawierają:

  • Deklaracja pragmy (#pragma version(1)) deklarująca wersję klucza Język jądra RenderScriptu użyty w tym skrypcie. Obecnie jedyną prawidłową wartością jest 1.
  • Deklaracja pragmy (#pragma rs java_package_name(com.example.app)), która deklaruje nazwę pakietu klas Java odzwierciedlonych przez ten skrypt. Pamiętaj, że plik .rs musi być częścią pakietu aplikacji, a nie w w projekcie bibliotecznym.
  • Co najmniej 0 funkcji wywoływanych. Funkcja wywoływana to jednowątkowy skrypt RenderScript , którą możesz wywołać z poziomu kodu Java za pomocą dowolnych argumentów. Są one często przydatne, konfiguracji początkowej lub obliczeń szeregowych w większym potoku przetwarzania.
  • 0 lub więcej globalnych skryptów. Skrypt globalny jest podobny do zmiennej globalnej w C. Dostępne opcje globalnie skryptu dostępu z kodu Java, które są często używane do przekazywania parametrów do RenderScriptu jądra systemu operacyjnego. Globalne wartości skryptu zostały szczegółowo wyjaśnione tutaj.

  • 0 lub więcej jąder obliczeniowych. Jądro obliczeniowe to funkcja lub zbiór funkcji, które można równolegle wykonywać środowisko wykonawcze RenderScript w zbiorze danych. Są 2 rodzaje obliczeń jądra systemu: mapowanie jądra (inaczej jedna lub jądra) i jądro redukcji.

    Jądro mapowania to funkcja równoległa, która działa na zbiorze Allocations o tych samych wymiarach. Domyślnie jest wykonywany raz na każdą współrzędną w tych wymiarach. Jest on zwykle (ale nie wyłącznie) używany do przekształć zbiór danych wejściowych Allocations w wyjdź Allocation jeden Element przy obecnie się znajdujesz.

    • 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 równa się standardowemu C . Właściwość RS_KERNEL zastosowana do Prototyp funkcji określa, że funkcja jest jądrem mapowania RenderScript, a nie . 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ść zwrócona z jądra to automatycznie zapisane w odpowiedniej lokalizacji danych wyjściowych Allocation. Domyślnie to jądro jest uruchamiane na wszystkich danych wejściowych Allocation, z jednym wykonaniem funkcji jądra na Element w Allocation.

      Jądro mapowania może mieć 1 lub więcej danych wejściowych Allocations, 1 wyjściowy Allocation lub oba te elementy. 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 dodatkowych danych wejściowych lub wyjściowych Allocations niż jądro, te obiekty powinny być powiązane z globalnymi zasobami skryptu rs_allocation i uzyskiwane z poziomu jądra lub niedopuszczalnej funkcji przez rsGetElementAt_type() lub rsSetElementAt_type().

      UWAGA: RS_KERNEL jest makrem 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. Jest on zwykle używany (ale nie wyłącznie) do „redukcji” w zbiór danych wejściowych Allocations do pojedynczej .

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

      Jądro redukcji składa się z co najmniej 1 funkcji napisanej przez użytkownika. Do definiowania jądra służy #pragma rs reduce – podaj jego nazwę (w tym przykładzie addint) oraz nazwy i role funkcji, które ją jądro (funkcja accumulator addintAccum, w tym ). 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) jest wskaźnikiem do element danych zasobnika, a drugi (w tym przykładzie val) to wypełnione automatycznie na podstawie danych wejściowych z pola Allocation przekazanych do uruchomienia jądra systemu operacyjnego. 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. Środowisko wykonawcze RenderScript sprawdza, czy typ Element przydziału wejściowego jest zgodny z parametrem zasobnika prototyp; Jeśli nie, RenderScript zgłasza wyjątek.

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

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

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

    Funkcja jądra mapowania lub funkcja zasobnika jądra redukcyjnego może uzyskiwać dostęp do współrzędnych. bieżącego wykonania przy użyciu specjalnych argumentów x, y i z, 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 to potrzebne przez rodzinę interfejsów API środowiska wykonawczego używanych do wysyłania zapytań określone właściwości bieżącego wykonania, na przykład rsGetDimX. (Argument context jest dostępny w Androidzie 6.0 (poziom interfejsu API 23) i nowszych.

  • Opcjonalna funkcja init(). Funkcja init() to specjalny typ funkcji Niedostępna funkcja uruchamiana przez RenderScript podczas tworzenia pierwszej instancji skryptu. Pozwala to na korzystanie z obliczeniach, aby odbywały się automatycznie podczas tworzenia skryptu.
  • 0 lub więcej globalnych wartości i funkcji skryptów statycznych. Statyczny skrypt globalny jest odpowiednikiem skrypt globalny, ale nie można uzyskać do niego dostępu z poziomu kodu Java. Funkcja statyczna to standardowa funkcja C funkcja, która może być wywoływana z dowolnego jądra lub funkcji niedostępnej w skrypcie, ale nie jest ujawniona do interfejsu Java API. Jeśli dostęp do globalnego skryptu lub funkcji nie jest potrzebny z poziomu kodu Java, można zdecydowanie zalecamy zadeklarowanie jej jako static.

Ustawianie precyzji liczby zmiennoprzecinkowej

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 włącza czyszczenie do zera w przypadku denormów i do zera.
  • #pragma rs_fp_imprecise: dla aplikacji, które nie wymagają bardzo rygorystycznej precyzji . Ten tryb włącza wszystko w usłudze rs_fp_relaxed oraz :
    • Operacje z wartością -0,0 mogą zwracać zamiast tego +0,0.
    • Operacje na INF i NAN są niezdefiniowane.

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

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

Gdy tworzysz aplikację na Androida korzystającą z 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 może działać na większej liczbie urządzeń niż w przypadku natywne (android.renderscript) interfejsy API.
  • 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 pomocy RenderScript

Aby korzystać z interfejsów API RenderScript Biblioteki pomocy, musisz skonfigurować programowanie i środowisko, aby mieć do nich dostęp. Poniższe narzędzia Android SDK są wymagane do korzystania z tych interfejsów API:

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

      Odlotowe

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

      Kotlin

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

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

      • renderscriptTargetApi – określa wersję kodu bajtowego do . Zalecamy ustawienie tej wartości na najniższy poziom interfejsu API, jaki jest dostępny wszystkich używanych funkcji i ustaw renderscriptSupportModeEnabled do: true. Dla tego ustawienia prawidłowymi wartościami są dowolne liczby całkowite z 11 do najnowszego poziomu 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 na urządzeniu, na którym działa, kod bajtowy powinien wrócić do zgodnej wersji on nie obsługuje wersji docelowej.
  3. W klasach aplikacji, które korzystają z RenderScriptu, dodaj import do biblioteki pomocy. zajęcia:

    Kotlin

    import android.support.v8.renderscript.*
    

    Java

    import android.support.v8.renderscript.*;
    

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

Używanie RenderScriptu za pomocą kodu Java lub Kotlin opiera się na klasach 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ą metody create(Context) gwarantuje, że skrypt RenderScript może być używany i udostępnia obiektu, aby kontrolować czas życia wszystkich kolejnych obiektów RenderScript. Trzeba wziąć pod uwagę kontekst jako potencjalnie długo trwającej operacji, ponieważ może ona tworzyć zasoby w różnych Sprzęt; nie powinien znajdować się na ścieżce krytycznej aplikacji, jeśli w ogóle jak to tylko możliwe. Zwykle aplikacja ma tylko jeden kontekst RenderScript naraz.
  2. Utwórz co najmniej jeden element typu Allocation, który ma być przekazywany do skrypt. Allocation to obiekt RenderScript, który udostępnia do przechowywania stałej ilości danych. Ziarna w skryptach zajmują Allocation jako dane wejściowe i wyjściowe, a obiekty Allocation mogą być dostęp w jądrze przy użyciu interfejsu rsGetElementAt_type() i rsSetElementAt_type() w przypadku powiązania jako globalnego 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ą funkcji createTyped() lub createFromBitmap().
  3. Utwórz potrzebne skrypty. Dostępne są 2 rodzaje skryptów podczas korzystania z kodu RenderScript:
    • ScriptC: są to skrypty zdefiniowane przez użytkownika zgodnie z opisem w sekcji Pisanie jądra RenderScriptu powyżej. Każdy skrypt ma klasę Java uwzględniany przez kompilator RenderScript, aby ułatwić dostęp do skryptu z poziomu kodu Java; te zajęcia mają 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:

      Kotlin

      val invert = ScriptC_invert(renderScript)
      

      Java

      ScriptC_invert invert = new ScriptC_invert(renderScript);
      
    • ScriptIntrinsic: to wbudowane jądra RenderScriptu służące do typowych operacji. takie jak rozmycie Gaussa, splot czy przenikanie obrazów. Więcej informacji znajdziesz w podklasach funkcji ScriptIntrinsic
  4. Wypełnianie przydziałów 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ć pole Przydział, użyj jednego z funkcji „Kopiuj” Allocation. Kopia są synchroniczne.
  5. Ustaw niezbędne globalne wartości skryptu. Możesz ustawić wartości globalne za pomocą metod w sekcji ta sama klasa ScriptC_filename o nazwie set_globalname. Dla: przykład, aby ustawić zmienną int o nazwie threshold, użyj funkcji Metoda Java set_threshold(int); oraz ustawić zmiennej rs_allocation o nazwie lookup, użyj Javy metoda set_lookup(Allocation). Metody setasynchroniczne.
  6. Uruchom odpowiednie jądra i niemożliwe funkcje.

    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.

    Uruchamiaj funkcje, których nie można wywołać, używając metod invoke_functionName w tej samej klasie ScriptC_filename. Zmiany te są asynchroniczne.

  7. Pobieranie danych z Allocation obiektów i javaFutureType. Aby uzyskać dostęp do danych z Allocation z kodu Java, musisz je skopiować lub wrócić do Javy za pomocą jednej z funkcji 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.

Asynchroniczny model wykonywania

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 szeregowane w kolejności, w jakiej zostały uruchomione.

Klasa Allocation zawiera polecenie „copy” metod kopiowania danych do i sekcji Alokacje. „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 zapewniają metody get() w celu uzyskania wyniku redukcji. get() to synchroniczna i jest zserializowana zgodnie z redukcją (asynchroniczną).

Skrypt RenderScript z jednego źródła

Android 7.0 (poziom API 24) wprowadza nową funkcję programowania o nazwie Jedno źródłową RenderScript, w którym jądra są uruchamiane ze skryptu, w którym zostały zdefiniowane, a nie z Javy. To podejście jest obecnie ograniczone do jąder mapowania, które określa się po prostu jako „jądro” w tej sekcji, aby zapewnić zwięzłość. Ta nowa funkcja obsługuje też tworzenie alokacji typu rs_allocation w skrypcie. Teraz można wdrożysz cały algorytm wyłącznie w skrypcie, nawet jeśli wymagane jest uruchomienie wielu jądra systemu. Korzyści są 2 korzyści: bardziej czytelny kod, ponieważ dzięki temu implementacja algorytmu jest jeden język; i potencjalnie szybszy kod ze względu na mniejszą liczbę przejść między Javą Zastosowanie obsługi języka RenderScript w przypadku wielu jądra systemu.

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 niemożliwą funkcję, która wywołuje rsForEach(), aby je uruchomić. Ten interfejs API przyjmuje funkcję jądra jako oraz alokacje 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 języku RenderScript, wywołaj funkcję niedostępną w języku Java. Wykonaj czynności opisane w sekcji Używanie skryptu RenderScript z kodu Java. W kroku uruchom odpowiednie jądra wywołaj funkcji wywoływanej przez funkcję invoke_function_name(), która spowoduje uruchomienie funkcji całe obliczenia, w tym uruchamianie jąder.

Aby zapisać i przekazać, konieczne są często przydziały wyników pośrednich między uruchomieniem jądra systemu operacyjnego. Możesz je utworzyć za pomocą rsCreateAllocation(). Prosta w użyciu forma tego interfejsu API to rsCreateAllocation_<T><W>(…), gdzie T to typ danych dla elementu, a W to szerokość wektora elementu. Interfejs API przyjmuje rozmiary w wymiarów X, Y i Z jako argumentów. W przypadku przydziałów 1D lub 2D rozmiar wymiaru Y lub Z może zostać pominięty. Na przykład rsCreateAllocation_uchar4(16384) tworzy przydział 1D o wartości 16384 elementy, z których każdy jest typu uchar4.

Przydziałami zarządza system automatycznie. Ty nie muszą być specjalnie zwalniane ani zwalniane. 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 inne jądro (greyscale), które przekształca aby uzyskać czarno-biały obraz. Wywoływana funkcja process() stosuje następnie te 2 jądro obok obrazu wejściowego i generuje 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ć w Javie lub Kotlinie w następujący sposób:

Kotlin

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

Java

// File SingleSource.java

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

Ten przykład pokazuje, jak można w pełni wdrożyć algorytm, który obejmuje 2 uruchomienia jądra w samym języku RenderScript. Bez pojedynczego źródła RenderScript, musisz uruchomić oba jądro z kodu Java, oddzielając uruchomienia jądra od definicji jądra i utrudniać 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.

Globalne wartości 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. O ile operator globalny jest const, będzie też metoda set_var.

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

  • Jeśli skrypt var ma w skrypcie inicjator statyczny, określa początkową wartość zmiennej var zarówno w Javie, jak i w tagu skrypt. W przeciwnym razie wartość początkowa wynosi 0.
  • Uzyskuje dostęp do parametru var w ramach skryptu z odczytem i zapisem jego wartości skryptu.
  • Metoda get_var odczytuje język Java .
  • Metoda set_var (jeśli istnieje) zapisuje Natychmiastowa wartość Java i zapisuje wartość skryptu asynchronicznie.

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

Głębokość redukcji ziaren

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
  • podczas wyszukiwania konkretnej wartości lub współrzędnych konkretnej wartości w danych

W Androidzie 7.0 (poziom interfejsu API 24) i nowszych skrypt RenderScript obsługuje jądro redukcji, 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 przedstawia proste jądro redukcji addint. Oto bardziej skomplikowany jądro redukcji findMinAndMax. który znajduje lokalizacje minimalnej i maksymalnej wartości long w Jednowymiarowy 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: jest więcej przykładów. jądra systemu znajdziesz tutaj.

Aby można było uruchomić jądro redukcji, środowisko wykonawcze RenderScript tworzy co najmniej jeden zmiennych nazywanymi danymi akumulatora elementów do przechowywania stanu procesu redukcji. Środowisko wykonawcze RenderScript wybiera liczbę elementów danych z akumulatora w taki sposób, by zmaksymalizować skuteczność. Typ elementów danych akumulatora (accumType) jest określany przez zasobnik jądra funkcji – pierwszy argument do tej funkcji jest wskaźnikiem do danych z akumulatora. elementu. Domyślnie każdy element danych zasobnika jest inicjowany do zera (jak w przypadku autor: memset); możesz jednak napisać funkcję inicjującą, aby to zrobić w inny sposób.

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. Dostępna jest funkcja inicjująca do ustawiania tych wartości na LONG_MAX oraz LONG_MIN; i ustawić lokalizacje tych wartości na -1, co oznacza, że wartości nie występują w (pustej) części danych wejściowych, która została przetworzono.

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 akumulatora ma tylko jeden sygnał wejściowy i brak specjalnych argumentów, nie musisz pisać łącznika funkcji; RenderScript użyje funkcji akumulatora, aby połączyć dane z zasobnika elementy(ów). (Możesz utworzyć funkcję łączącą, jeśli to domyślne działanie nie jest tym, w pobliżu.

Przykład: w polu addint jądro, nie ma funkcji łączącej, więc zostanie użyta funkcja zasobnika. 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 aby uzupełnić 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. To samo działa 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ć outconverter . 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: In jądro findMinAndMax, czyli funkcja outconverter, inicjuje wartość wynikową int2 w celu przechowywania lokalizacji minimalnych i maksymalnych wartości wynikających z połączenia wszystkich elementów danych akumulatora.

Pisanie 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 Javy 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ę po jednym razie 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 inicjującej, RenderScript zainicjuje każdy zasobnik elementu danych do zera (jak w przypadku funkcji memset), zachowując się tak, jakby miał miejsce inicjator wygląda tak:

    static void initializerName(accumType *accum) {
      memset(accum, 0, sizeof(*accum));
    }
  • accumulator(accumulatorName) (obowiązkowe): określa nazwę funkcji akumulatora dla tej jądra 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ć zdefiniowany 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 jądra 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 wskazuje „miejsce docelowe” element danych akumulatora dla tego do zmiany. 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 zasobnika w funkcji działa tak, jakby istniała funkcja łącząca, która wygląda tak:

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

    Funkcja łącząca jest wymagana, jeśli jądro ma więcej niż 1 dane wejściowe, 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 outconverter dla tej jądra 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 wskazuje wskaźnik do ostatecznego elementu danych zasobnika obliczaną przez funkcję kombinacyjną.

    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 jądro ma typy danych wejściowych, typ elementu danych zasobnika i typ wyniku, które nie muszą być takie same. Na przykład w polu jądra findMinAndMax, dane wejściowe typ long, typ elementu danych zasobnika MinAndMax i wynik typy int2 są różne.

Czego nie możesz przyjąć?

Nie możesz polegać na liczbie elementów danych akumulatora utworzonych przez RenderScript dla w przypadku danego jądra systemu operacyjnego. Nie ma gwarancji, że 2 uruchomienia tego samego jądra systemu te same dane wejściowe spowodują utworzenie tej samej liczby 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 akumulatora zostaną zainicjowane przed funkcja akumulatora jest wywoływana, ale tylko w przypadku zainicjowanego akumulatora. elementu danych.
  • Nie ma gwarancji kolejności, w jakiej elementy wejściowe są przekazywane do zasobnika .
  • Nie ma gwarancji, że funkcja akumulatora została wywołana dla wszystkich elementów wejściowych przed wywołaniem funkcji łączącej.

Jedną z konsekwencji jest to, że funkcja findMinAndMax jądro nie jest deterministyczne: jeśli dane wejściowe zawierają więcej niż jedno wystąpienie tego samego wartości minimalnej czy maksymalnej, nie wiadomo, w którym przypadku jądro znaleźć.

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.

Poniższe reguły często wskazują, że dwa elementy danych akumulatora muszą mieć „parametr ta sama 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 do „znajdowania lokalizacji minimalnej i maksymalnej wartości wejściowej po lewej stronie”. gdzie (na przykład) wartość minimalna w lokalizacji 100 jest preferowana zamiast tej samej wartości minimalnej w lokalizacji 200; „to samo” oznacza identyczną lokalizację, a nie tylko identyczną wartość, a funkcje kumulatora i łączącego powinny inne niż w przypadku findMinAndMax.

Funkcja inicjująca musi utworzyć wartość tożsamości. To znaczy, jeśli I i A to zainicjowane elementy danych akumulatora przez funkcję inicjatora, a element I nigdy nie został przekazany do funkcji akumulatora (ale A mogła być), a
  • combinerName(&A, &I) musi zostaw A to samo
  • 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 łącząca tego argumentu jądro wykonuje dodawanie; to wartość tożsamości dla dodania.

Przykład: w polu findMinAndMax jądro, element danych akumulatora zostaje zainicjowany do INITVAL.

  • fMMCombiner(&A, &I) pozostawia miejsce 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. To znaczy, jeśli A i B to zainicjowane elementy danych akumulatora przez funkcję inicjatora i która mogła zostać przekazana do funkcji zasobnika zero lub więcej razy, combinerName(&A, &B) musi ustaw A na tę samą wartość która 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 łącząca musi być osobna. 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 asocjacyjne, tak samo jak funkcja łącząca.

Przykład: w jądrze findMinAndMax

fMMCombiner(&A, &B)
jest taka sama jak
A = minmax(A, B)
Te 2 ciągi są więc

  • 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 A i B to elementy danych akumulatora, A zostało zainicjowane przez funkcję inicjatora i mogła zostać przekazana do funkcji zasobnika 0 lub więcej razy, B nie został zainicjowany, a argumenty to lista argumentów wejściowych i argumentów specjalnych dla określonego wywołania zasobnika funkcji, trzy następujące sekwencje kodu muszą ustawić atrybut A 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.
  • Stwierdzenie 2 jest takie samo jak B = 0
  • Stwierdzenie 3 jest takie samo jak B += V, które jest identyczne z B = V.
  • Stwierdzenie 4 jest takie samo jak A += B, które jest identyczne z A += V.

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

Przykład: w jądrze findMinAndMax jako dane wejściowe wartość V w współrzędnych X:

  • Polecenie 1 jest takie samo jak A = minmax(A, IndexedVal(V, X)).
  • Stwierdzenie 2 jest takie samo jak B = INITVAL
  • Stwierdzenie 3 jest identyczne z
    B = minmax(B, IndexedVal(V, X))
    
    który, ponieważ B to wartość początkowa, jest równa
    B = IndexedVal(V, X)
    
  • Stwierdzenie 4 jest takie samo jak
    A = minmax(A, B)
    
    czyli tyle samo, co
    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 zdefiniowanego w filename.rs, w pliku danych znajdziesz 3 metody zajęcia 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żdy argument wejściowy w zasobniku jądra . Środowisko wykonawcze RenderScript sprawdza, czy wszystkie przydziały wejściowe mają te same wymiary i że typ Element każdego z Przydziały wejściowe pasują do argumentu odpowiedniego argumentu wejściowego zasobnika prototyp funkcji. Jeśli któraś z tych kontroli zakończy się niepowodzeniem, RenderScript zgłosi wyjątek. gdy jądro jest wykonywane w przypadku wszystkich współrzędnych w tych wymiarach.

Metoda 2 jest taka sama jak metoda 1, ale metoda 2 wymaga dodatkowej sc, którego można użyć do ograniczenia wykonywania jądra do podzbioru .

Metoda 3 jest taka sama jak metoda 1, z tym że 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. Dla każdej tablicy wejściowej metoda 3 tworzy tymczasowy jednowymiarowa alokacja z odpowiednim typem Element i włączono setAutoPadding(boolean) i skopiuje tablicę do Przydział tak, jak gdyby 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 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.

Kotlin

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

Java

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

javaResultType jest określany na podstawie 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], to javaResultType to long, Long2, lub long[]. Dozwolone są wszystkie wartości parametru resultType. autorstwa javaResultType.
  • Jeśli resultType to ulong, ulong2, lub ulong[15], a następnie javaResultType. jest long, Long2 lub long[]. Istnieją pewne wartości parametru resultType, którego nie można reprezentować przez javaResultType.

javaFutureType to przyszły typ wyniku, który odpowiada do parametru resultType elementu outconverter, .

  • Jeśli resultType nie jest typem tablicy, to javaFutureType. jest result_resultType.
  • Jeśli resultType jest tablicą o długości Count, zawierającą elementy typu memberType, to javaFutureType to 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 (wraz z typem tablicy), każde wywołanie do javaFutureType.get() w tej samej instancji, zwróci to samo obiektu.

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

Metoda 3 i devecSiInXType

devecSiInXType to typ Javy odpowiadający wartość inXType odpowiedniego argumentu funkcji akumulatora. 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 jest bez podpisu typ wektora, devecSiInXType jest typem Javy odpowiadającym bezpośrednio funkcji typ skalarny ze znakiem o tym samym rozmiarze co typ komponentu wektora. Na przykład:

  • Jeśli inXType ma wartość int, to devecSiInXType jest int.
  • Jeśli inXType ma wartość int2, to devecSiInXType jest int. Tablica jest reprezentacją spłaszczoną: ma 2 razy wiele elementów skalarnych, ponieważ przydział ma dwuskładnikowy wektor Elementy. W ten sam sposób działają metody copyFrom() metody Allocation.
  • Jeśli inXType ma wartość uint, to deviceSiInXType. jest int. Podpisana wartość w tablicy Java jest interpretowana jako wartość bez znaku tego samego wzorca bitowego w alokacji. Działa tak samo jak copyFrom() metody Allocation.
  • Jeśli inXType ma wartość uint2, to deviceSiInXType. jest int. To połączenie metod int2 i uint są obsługiwane: tablica jest spłaszczoną reprezentacją, a wartości podpisane w Javie są interpretowane jako wartości niepodpisanych elementów w języku 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ą przedstawione w języku Java jako podpisane dane wejściowe o tym samym rozmiarze a niepodpisany wynik skryptu jest reprezentowany w języku Java jako rozszerzony typ podpisu (z wyjątkiem przypadku ulong).

Więcej przykładowych jąder redukcji

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

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

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

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

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

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

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

#define BUCKETS 256
typedef uint32_t Histogram[BUCKETS];

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

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

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

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

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

Dodatkowe przykłady kodu

Instrukcja BasicRenderScript, RenderScriptIntrinsic, i Hello Compute W przykładach szczegółowo opisano wykorzystanie interfejsów API opisanych na tej stronie.