Ringkasan RenderScript

RenderScript adalah framework yang berfungsi untuk menjalankan tugas yang intensif secara terkomputasi dengan performa tinggi di Android. RenderScript secara khusus ditujukan untuk digunakan dengan komputasi paralel data, meskipun juga dapat digunakan pada beban kerja serial. Runtime RenderScript menyejajarkan pekerjaan di seluruh prosesor yang tersedia di perangkat, seperti GPU dan CPU multi-core. Hal ini memungkinkan Anda untuk fokus mengekspresikan algoritme, bukan fokus pada penjadwalan tugas. RenderScript sangat berguna untuk aplikasi yang melakukan pemrosesan gambar, fotografi komputasional, atau computer vision.

Untuk mulai menggunakan RenderScript, ada dua konsep utama yang harus Anda pahami:

  • Bahasa RenderScript sendiri adalah bahasa turunan C99 untuk menulis kode komputasi berperforma tinggi. Menulis Kernel RenderScript menjelaskan cara Anda dapat menggunakannya untuk menulis kernel komputasi.
  • API kontrol digunakan untuk mengelola masa pakai resource RenderScript dan mengontrol eksekusi kernel. API ini tersedia dalam tiga bahasa: Java, C++ di Android NDK, dan bahasa kernel turunan C99 itu sendiri. Menggunakan RenderScript dari Kode Java dan RenderScript Sumber Tunggal masing-masing menjelaskan opsi pertama dan ketiga.

Menulis Kernel RenderScript

Kernel RenderScript biasanya berada dalam file .rs di dalam direktori <project_root>/src/rs; dan setiap file .rs disebut dengan skrip. Setiap skrip berisi kumpulan kernel, fungsi, dan variabelnya sendiri. Skrip dapat berisi:

  • Deklarasi pragma (#pragma version(1)) yang mendeklarasikan versi bahasa kernel RenderScript yang digunakan dalam skrip ini. Saat ini, 1 adalah satu-satunya nilai yang valid.
  • Deklarasi pragma (#pragma rs java_package_name(com.example.app)) yang mendeklarasikan nama paket class Java yang ditampilkan dari skrip ini. Perlu diketahui bahwa file .rs harus menjadi bagian dari paket aplikasi Anda, bukan dalam project library.
  • Nol atau beberapa fungsi yang dapat dipanggil. Fungsi yang dapat dipanggil adalah fungsi RenderScript thread tunggal yang dapat Anda panggil dari kode Java dengan argumen sembarang. Fungsi ini sering kali berguna untuk penyiapan awal atau komputasi serial dalam pipeline pemrosesan yang lebih besar.
  • Nol atau beberapa global skrip. Global skrip mirip dengan variabel global di C. Anda dapat mengakses global skrip dari kode Java, dan skrip ini sering digunakan untuk meneruskan parameter ke kernel RenderScript. Global skrip dijelaskan secara lebih mendetail di sini.

  • Nol atau beberapa kernel komputasi. Kernel komputasi adalah fungsi atau kumpulan fungsi yang dapat Anda eksekusi secara paralel pada sekumpulan data menggunakan runtime RenderScript. Terdapat dua jenis kernel komputasi: kernel pemetaan (juga disebut kernel foreach) dan kernel pengurangan.

    Kernel pemetaan adalah fungsi paralel yang beroperasi pada sekumpulan Allocations dengan dimensi yang sama. Secara default, kernel ini dieksekusi sekali untuk setiap koordinat dalam dimensi tersebut. Kernel ini biasanya (tetapi tidak secara eksklusif) digunakan untuk mengubah kumpulan Allocations input menjadi Allocation output, per satu Element.

    • Berikut adalah contoh sederhana kernel pemetaan:

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

      Secara umum, fungsi ini sama dengan fungsi C standar. Properti RS_KERNEL yang diterapkan pada prototipe fungsi menetapkan bahwa fungsi tersebut adalah kernel pemetaan RenderScript, bukan fungsi yang dapat dipanggil. Argumen in otomatis diisi berdasarkan Allocation input yang diteruskan ke peluncuran kernel. Argumen x dan y dibahas di bawah ini. Nilai yang dihasilkan dari kernel tersebut otomatis ditulis ke lokasi yang sesuai dalam Allocation output. Secara default, kernel ini dijalankan di seluruh Allocation inputnya, dengan satu eksekusi fungsi kernel per Element dalam Allocation.

      Kernel pemetaan dapat memiliki satu atau beberapa Allocations input, satu Allocation output, atau keduanya. Runtime RenderScript akan melakukan pemeriksaan untuk memastikan bahwa semua Alokasi input dan output memiliki dimensi yang sama, dan bahwa jenis Element Alokasi input dan output cocok dengan prototipe kernel; jika salah satu pemeriksaan ini gagal, RenderScript akan menampilkan pengecualian.

      CATATAN: Sebelum Android 6.0 (API level 23), kernel pemetaan tidak dapat memiliki lebih dari satu Allocation input.

      Jika membutuhkan lebih banyak Allocations input atau output dari yang dimiliki kernel, objek tersebut harus terikat ke global skrip rs_allocation dan diakses dari kernel atau fungsi yang dapat dipanggil melalui rsGetElementAt_type() atau rsSetElementAt_type().

      CATATAN: RS_KERNEL adalah makro yang otomatis ditetapkan oleh RenderScript untuk memudahkan Anda:

      #define RS_KERNEL __attribute__((kernel))
      

    Kernel pengurangan adalah kumpulan fungsi yang beroperasi pada sekumpulan Allocations input dengan dimensi yang sama. Secara default, fungsi akumulatornya dieksekusi sekali untuk setiap koordinat dalam dimensi tersebut. Kernel ini umumnya (tetapi tidak secara eksklusif) digunakan untuk "mengurangi" kumpulan Allocations input menjadi nilai tunggal.

    • Berikut adalah contoh kernel pengurangan sederhana yang melengkapi Elements inputnya:

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

      Kernel pengurangan terdiri dari satu atau beberapa fungsi yang ditulis pengguna. #pragma rs reduce digunakan untuk menentukan kernel dengan menetapkan namanya (dalam contoh ini adalah addint), serta nama dan peran fungsi yang membentuk kernel (dalam contoh ini adalah fungsi accumulator addintAccum). Semua fungsi tersebut harus bersifat static. Kernel pengurangan selalu membutuhkan fungsi accumulator; dan mungkin juga memiliki fungsi lain, bergantung apa yang Anda inginkan darinya.

      Fungsi akumulator kernel pengurangan harus mengembalikan void dan harus memiliki sedikitnya dua argumen. Argumen pertama (dalam contoh ini adalah accum) merupakan pointer ke item data akumulator, dan yang kedua otomatis (dalam contoh ini adalah val) diisi berdasarkan Allocation input yang diteruskan ke peluncuran kernel. Item data akumulator dibuat oleh runtime RenderScript; secara default, item ini diinisialisasi ke nol. Secara default, kernel ini dijalankan di seluruh Allocation inputnya, dengan satu eksekusi fungsi akumulator per Element dalam Allocation. Secara default, nilai akhir item data akumulator diperlakukan sebagai hasil dari pengurangan, dan dikembalikan ke Java. Runtime RenderScript akan melakukan pemeriksaan untuk memastikan bahwa jenis Element Alokasi input cocok dengan prototipe fungsi akumulator; jika tidak cocok, RenderScript akan menampilkan pengecualian.

      Kernel pengurangan memiliki satu atau beberapa Allocations input, tetapi tidak memiliki Allocations output.

      Kernel pengurangan dijelaskan secara lebih mendetail di sini.

      Kernel pengurangan didukung di Android 7.0 (API level 24) dan yang lebih baru.

    Fungsi kernel pemetaan atau fungsi akumulator kernel pengurangan dapat mengakses koordinat eksekusi saat ini menggunakan argumen khusus x, y, dan z, yang harus berjenis int atau uint32_t. Argumen ini bersifat opsional.

    Fungsi kernel pemetaan atau fungsi akumulator kernel pengurangan juga dapat menggunakan argumen khusus opsional context dengan jenis rs_kernel_context. Argumen tersebut diperlukan oleh rangkaian API runtime yang digunakan untuk meminta properti tertentu dari eksekusi saat ini; misalnya rsGetDimX. (Argumen context tersedia di Android 6.0 (API level 23) dan yang lebih baru.)

  • Fungsi init() opsional. Fungsi init() adalah jenis khusus dari fungsi yang dapat dipanggil, yang dijalankan oleh RenderScript saat instance skrip dibuat untuk kali pertama. Hal ini memungkinkan beberapa komputasi otomatis terjadi pada saat pembuatan skrip.
  • Nol atau beberapa fungsi dan global skrip statis. Global skrip statis setara dengan global skrip, tetapi tidak dapat diakses dari kode Java. Fungsi statis adalah fungsi C standar yang dapat dipanggil dari kernel atau fungsi apa pun yang dapat dipanggil dalam skrip, tetapi tidak diekspos ke Java API. Jika global skrip atau fungsi tidak perlu diakses dari kode Java, kami sangat merekomendasikan Anda untuk mendeklarasikannya sebagai static.

Menentukan presisi floating point

Anda dapat mengontrol level presisi floating point yang diperlukan dalam sebuah skrip. Hal ini berguna jika standar IEEE 754-2008 lengkap (digunakan secara default) tidak diperlukan. Pragma berikut dapat menetapkan level presisi floating point yang berbeda:

  • #pragma rs_fp_full (default jika tidak ada yang ditentukan): Untuk aplikasi yang memerlukan presisi floating point seperti yang diuraikan oleh standar IEEE 754-2008.
  • #pragma rs_fp_relaxed: Untuk aplikasi yang tidak memerlukan kepatuhan IEEE 754-2008 yang ketat dan dapat menerima lebih sedikit presisi. Mode ini memungkinkan flush-to-zero untuk denorm dan round-towards-zero.
  • #pragma rs_fp_imprecise: Untuk aplikasi yang tidak memiliki persyaratan presisi yang ketat. Mode ini memungkinkan semua hal dalam rs_fp_relaxed beserta hal-hal berikut:
    • Operasi yang menghasilkan -0.0 dapat mengembalikan +0.0.
    • Operasi pada INF dan NAN tidak ditentukan.

Sebagian besar aplikasi dapat menggunakan rs_fp_relaxed tanpa efek samping. Hal ini mungkin sangat bermanfaat pada beberapa arsitektur karena pengoptimalan tambahan hanya tersedia dengan presisi yang tidak ketat (seperti petunjuk CPU SIMD).

Mengakses RenderScript API dari Java

Saat mengembangkan aplikasi Android yang menggunakan RenderScript, Anda dapat mengakses API-nya dari Java dengan salah satu dari dua cara berikut:

  • android.renderscript - API dalam paket class ini tersedia di perangkat yang menjalankan Android 3.0 (API level 11) dan yang lebih tinggi.
  • android.support.v8.renderscript - API dalam paket ini tersedia melalui Support Library, yang memungkinkan Anda untuk menggunakannya di perangkat yang menjalankan Android 2.3 (API level 9) dan yang lebih baru.

Berikut konsekuensinya:

  • Jika menggunakan Support Library API, bagian RenderScript aplikasi akan kompatibel dengan perangkat yang menjalankan Android 2.3 (API level 9) dan yang lebih baru, terlepas dari fitur RenderScript yang Anda gunakan. Hal ini memungkinkan aplikasi Anda berfungsi di lebih banyak perangkat dibandingkan jika Anda menggunakan API native (android.renderscript).
  • Fitur RenderScript tertentu tidak tersedia melalui Support Library API.
  • Jika menggunakan Support Library API, APK Anda akan menjadi lebih besar (mungkin secara signifikan) dibandingkan jika Anda menggunakan API native (android.renderscript).

Menggunakan RenderScript Support Library API

Untuk menggunakan Support Library RenderScript API, Anda harus mengonfigurasi lingkungan pengembangan agar dapat mengaksesnya. Android SDK Tools berikut diperlukan untuk menggunakan API ini:

  • Android SDK Tools revisi 22.2 atau yang lebih tinggi
  • Alat Build Android SDK revisi 18.1.0 atau yang lebih tinggi

Perlu diketahui bahwa mulai dari Alat Build Android SDK 24.0.0, Android 2.2 (API level 8) tidak lagi didukung.

Anda dapat memeriksa dan mengupdate versi terinstal alat ini di Android SDK Manager.

Untuk menggunakan Support Library RenderScript API:

  1. Pastikan Anda telah menginstal versi Android SDK yang diperlukan.
  2. Update setelan proses build Android agar menyertakan setelan RenderScript:
    • Buka file build.gradle dalam folder aplikasi modul aplikasi Anda.
    • Tambahkan setelan RenderScript berikut ke file tersebut:

      Groovy

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

      Kotlin

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

      Setelan yang tercantum di atas mengontrol perilaku tertentu dalam proses build Android:

      • renderscriptTargetApi - Menentukan versi bytecode yang akan dibuat. Sebaiknya tetapkan nilai ini ke API level terendah yang dapat memberikan semua fungsionalitas yang Anda gunakan dan tetapkan renderscriptSupportModeEnabled ke true. Nilai yang valid untuk setelan ini adalah nilai bilangan bulat dari 11 sampai API level terbaru yang dirilis. Jika versi SDK minimum yang ditentukan dalam manifes aplikasi Anda ditetapkan ke nilai lain, nilai tersebut akan diabaikan dan nilai target dalam file build akan digunakan untuk menetapkan versi SDK minimum.
      • renderscriptSupportModeEnabled - Menetapkan bahwa bytecode yang dihasilkan harus dikembalikan ke versi yang kompatibel jika perangkat tempatnya berjalan tidak mendukung versi target.
  3. Dalam class aplikasi yang menggunakan RenderScript, tambahkan impor untuk class Support Library:

    Kotlin

    import android.support.v8.renderscript.*
    

    Java

    import android.support.v8.renderscript.*;
    

Menggunakan RenderScript dari Kode Java atau Kotlin

Menggunakan RenderScript dari kode Java atau Kotlin bergantung pada class API yang ada dalam paket android.renderscript atau android.support.v8.renderscript. Sebagian besar aplikasi mengikuti pola penggunaan dasar yang sama:

  1. Menginisialisasi konteks RenderScript. Konteks RenderScript, yang dibuat dengan create(Context), memastikan bahwa RenderScript dapat digunakan dan menyediakan objek untuk mengontrol masa pakai semua objek RenderScript berikutnya. Anda harus mengupayakan pembuatan konteks agar menjadi operasi yang dapat berjalan lama, karena mungkin proses ini akan membuat resource di bagian hardware lainnya; proses ini sebisa mungkin tidak boleh terjadi di lokasi penting aplikasi. Biasanya, aplikasi hanya akan memiliki satu konteks RenderScript dalam satu waktu.
  2. Membuat sedikitnya satu Allocation untuk diteruskan ke skrip. Allocation adalah objek RenderScript yang menyediakan penyimpanan untuk data dalam jumlah tertentu. Kernel dalam skrip menggunakan objek Allocation sebagai input dan output-nya, dan objek Allocation dapat diakses dalam kernel menggunakan rsGetElementAt_type() dan rsSetElementAt_type() ketika terikat sebagai global skrip. Objek Allocation memungkinkan array diteruskan dari kode Java ke kode RenderScript, juga sebaliknya. Objek Allocation biasanya dibuat menggunakan createTyped() atau createFromBitmap().
  3. Membuat skrip apa pun yang diperlukan. Terdapat dua jenis skrip yang tersedia untuk Anda saat menggunakan RenderScript:
    • ScriptC: Ini adalah skrip yang ditentukan pengguna, seperti yang dijelaskan dalam Menulis Kernel RenderScript di atas. Setiap skrip memiliki class Java yang ditampilkan oleh compiler RenderScript untuk memudahkan akses skrip dari kode Java; class ini memiliki nama ScriptC_filename. Sebagai contoh, jika kernel pemetaan di atas berada di invert.rs dan konteks RenderScript sudah ada di mRenderScript, kode Java atau Kotlin untuk membuat instance skrip akan seperti berikut:

      Kotlin

      val invert = ScriptC_invert(renderScript)
      

      Java

      ScriptC_invert invert = new ScriptC_invert(renderScript);
      
    • ScriptIntrinsic: Ini adalah kernel RenderScript bawaan untuk operasi umum, seperti pemburaman Gaussian, konvolusi, dan perpaduan gambar. Untuk informasi selengkapnya, lihat subclass ScriptIntrinsic.
  4. Mengisi Alokasi dengan data. Kecuali untuk Alokasi yang dibuat dengan createFromBitmap(), Alokasi akan diisi dengan data kosong pada saat pertama kali dibuat. Untuk mengisi Alokasi, gunakan salah satu metode “salin” dalam Allocation. Metode "salin" berjalan secara sinkron.
  5. Menetapkan setiap global skrip yang diperlukan. Anda dapat menetapkan global menggunakan metode dalam class ScriptC_filename serupa bernama set_globalname. Sebagai contoh, untuk menetapkan variabel int yang bernama threshold, gunakan metode Java set_threshold(int); dan untuk menetapkan variabel rs_allocation bernama lookup, gunakan metode Java set_lookup(Allocation). Metode set berjalan secara asinkron.
  6. Meluncurkan kernel yang sesuai dan fungsi yang dapat dipanggil.

    Metode untuk meluncurkan kernel tertentu ditampilkan dalam class ScriptC_filename yang sama dengan metode bernama forEach_mappingKernelName() atau reduce_reductionKernelName(). Peluncuran ini dilakukan secara asinkron. Bergantung pada argumen ke kernel, metode tersebut membutuhkan satu atau beberapa Alokasi, yang semuanya harus memiliki dimensi yang sama. Secara default, kernel dieksekusi pada setiap koordinat dalam dimensi tersebut; untuk mengeksekusi kernel pada subset koordinat tersebut, teruskan Script.LaunchOptions yang sesuai sebagai argumen terakhir pada metode forEach atau reduce.

    Jalankan fungsi yang dapat dipanggil menggunakan metode invoke_functionName yang ditampilkan dalam class ScriptC_filename yang sama. Peluncuran ini dilakukan secara asinkron.

  7. Mengambil data dari objek Allocation dan objek javaFutureType. Untuk mengakses data dari Allocation dari kode Java, Anda harus menyalin data tersebut kembali ke Java menggunakan salah satu metode "salin" dalam Allocation. Untuk mendapatkan hasil dari kernel pengurangan, Anda harus menggunakan metode javaFutureType.get(). Metode "salin" dan get() berjalan secara sinkron.
  8. Menghilangkan konteks RenderScript. Anda dapat menghilangkan konteks RenderScript dengan destroy() atau dengan mengizinkan objek konteks RenderScript untuk dibersihkan dari memori. Jika dilakukan, setiap penggunaan objek yang termasuk dalam konteks tersebut selanjutnya akan menampilkan pengecualian.

Model eksekusi asinkron

Metode forEach, invoke, reduce, dan set yang ditampilkan berjalan secara asinkron; masing-masing dapat kembali ke Java sebelum menyelesaikan tindakan yang diminta. Namun, tindakan individualnya diserialisasi sesuai urutan peluncurannya.

Class Allocation menyediakan metode "salin" untuk menyalin data ke dan dari Alokasi. Metode "salin" berjalan secara sinkron, dan diserialisasi sehubungan dengan setiap tindakan asinkron di atas yang menggunakan Alokasi yang sama.

Class javaFutureType yang ditampilkan menyediakan metode get() untuk mendapatkan hasil pengurangan. get() berjalan secara sinkron, dan diserialisasi sehubungan dengan pengurangan tersebut (yang berjalan secara asinkron).

RenderScript Sumber Tunggal

Android 7.0 (API level 24) memperkenalkan fitur pemrograman baru yang disebut dengan RenderScript Sumber Tunggal, dengan kernel yang diluncurkan dari skrip tempatnya ditentukan, bukan dari Java. Metode tersebut saat ini terbatas pada kernel pemetaan, yang secara singkat disebut sebagai "kernel" di bagian ini agar lebih ringkas. Fitur baru ini juga mendukung pembuatan alokasi berjenis rs_allocation dari dalam skrip. Anda kini dapat menerapkan seluruh algoritme hanya dalam skrip, meskipun beberapa peluncuran kernel diperlukan. Hal ini memberikan dua keuntungan: kode menjadi lebih mudah dibaca karena penerapan algoritme hanya menggunakan satu bahasa; dan kode menjadi lebih cepat karena transisi antara Java dan RenderScript berkurang lintas peluncuran kernel.

Dalam RenderScript Sumber Tunggal, Anda dapat menulis kernel seperti yang dijelaskan dalam Menulis Kernel RenderScript. Kemudian, tulis fungsi yang dapat dipanggil yang akan memanggil rsForEach() untuk meluncurkannya. API tersebut menggunakan fungsi kernel sebagai parameter pertama, diikuti dengan alokasi input dan output. API yang serupa, rsForEachWithOptions(), menggunakan argumen tambahan dari jenis rs_script_call_t, yang menetapkan subset elemen dari alokasi input dan output agar fungsi kernel dapat diproses.

Untuk memulai komputasi RenderScript, panggil fungsi yang dapat dipanggil dari Java. Ikuti langkah-langkah dalam Menggunakan RenderScript dari Kode Java. Pada langkah meluncurkan kernel yang sesuai, panggil fungsi yang dapat dipanggil menggunakan invoke_function_name(), yang akan memulai seluruh komputasi, termasuk meluncurkan kernel.

Alokasi biasanya diperlukan untuk menyimpan dan meneruskan hasil menengah dari satu peluncuran kernel ke peluncuran lainnya. Anda dapat membuatnya menggunakan rsCreateAllocation(). Salah satu bentuk yang mudah digunakan dari API tersebut adalah rsCreateAllocation_<T><W>(…), dengan T adalah jenis data untuk sebuah elemen, dan W adalah lebar vektor untuk elemen tersebut. API tersebut menggunakan ukuran dalam dimensi X, Y, dan Z sebagai argumen. Untuk alokasi 1D atau 2D, ukuran untuk dimensi Y atau Z dapat dihilangkan. Sebagai contoh, rsCreateAllocation_uchar4(16384) akan membuat alokasi 1D dari 16384 elemen, yang masing-masing berjenis uchar4.

Alokasi dikelola otomatis oleh sistem. Anda tidak perlu melepaskan atau mengosongkannya secara eksplisit. Namun, Anda dapat memanggil rsClearObject(rs_allocation* alloc) untuk menunjukkan bahwa Anda tidak lagi memerlukan pengendali alloc ke alokasi yang ditetapkan sehingga sistem dapat membebaskan resource sesegera mungkin.

Bagian Menulis Kernel RenderScript memuat contoh kernel yang membalik gambar. Contoh di bawah menjelaskannya secara lebih luas untuk menerapkan lebih dari satu efek ke gambar, menggunakan RenderScript Sumber Tunggal. Contoh ini juga mencakup kernel lain, greyscale, yang mengubah gambar berwarna menjadi hitam putih. Fungsi process() yang dapat dipanggil kemudian menerapkan dua kernel tersebut secara berturutan ke gambar input, dan menghasilkan gambar output. Alokasi untuk input dan output diteruskan sebagai argumen dengan jenis 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);
}

Anda dapat memanggil fungsi process() dari Java atau Kotlin seperti berikut:

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);

Contoh ini menunjukkan bagaimana algoritme yang melibatkan dua peluncuran kernel dapat diterapkan sepenuhnya dalam bahasa RenderScript sendiri. Tanpa RenderScript Sumber Tunggal, Anda harus meluncurkan kedua kernel tersebut dari kode Java, memisahkan peluncuran kernel dari definisi kernel, dan akan lebih sulit untuk memahami keseluruhan algoritmenya. Kode RenderScript Sumber Tunggal tidak hanya lebih mudah dibaca, tetapi juga mengeliminasi transisi antara Java dan skrip lintas peluncuran kernel. Beberapa algoritme iteratif mungkin meluncurkan kernel ratusan kali sehingga membuat overhead transisi tersebut menjadi cukup besar.

Global Skrip

Global skrip adalah variabel global non-static biasa dalam file skrip (.rs). Untuk global skrip bernama var yang ditentukan dalam file filename.rs, akan ada metode get_var yang ditampilkan dalam class ScriptC_filename. Kecuali global berupa const, akan tersedia juga metode set_var.

Global skrip yang ditentukan memiliki dua nilai terpisah: satu nilai Java dan satu nilai skrip. Nilai ini berperilaku seperti berikut:

  • Jika var memiliki penginisialisasi statis dalam skrip, penginisialisasi ini akan menentukan nilai awal var dalam Java dan skrip. Jika tidak, nilai awal tersebut adalah nol.
  • Akses ke var dalam skrip akan membaca dan menulis nilai skripnya.
  • Metode get_var akan membaca nilai Java-nya.
  • Metode set_var (jika ada) akan menulis nilai Java secara langsung, dan menulis nilai skripnya secara asinkron.

CATATAN: Kecuali untuk setiap penginisialisasi statis dalam skrip, hal ini berarti nilai yang ditulis ke global dari dalam skrip tidak akan terlihat di Java.

Kernel Pengurangan dalam Kedalaman

Pengurangan adalah proses penggabungan kumpulan data menjadi satu nilai. Pengurangan merupakan primitif yang berguna dalam pemrograman paralel, dengan penerapan seperti berikut:

  • menghitung jumlah atau produk di seluruh data
  • menghitung operasi logis (and, or, xor) pada semua data
  • menemukan nilai minimum atau maksimum dalam data
  • menelusuri nilai tertentu atau untuk koordinat nilai tertentu dalam data

Di Android 7.0 (API level 24) dan yang lebih baru, RenderScript mendukung kernel pengurangan untuk memungkinkan efisiensi algoritme pengurangan yang ditulis oleh pengguna. Anda dapat meluncurkan kernel pengurangan pada input dengan 1, 2, atau 3 dimensi.

Contoh di atas menunjukkan kernel pengurangan addint sederhana. Berikut adalah kernel pengurangan findMinAndMax yang lebih rumit, yang menemukan lokasi nilai long minimum dan maksimum dalam Allocation 1 dimensi:

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

CATATAN: Tersedia lebih banyak contoh kernel pengurangan di sini.

Untuk menjalankan kernel pengurangan, runtime RenderScript akan membuat satu atau beberapa variabel yang disebut item data akumulator untuk mempertahankan kondisi proses pengurangan. Runtime RenderScript akan mengambil jumlah item data akumulator dengan cara tertentu untuk memaksimalkan performa. Jenis item data akumulator (accumType) ditentukan oleh fungsi akumulator kernel; argumen pertama ke fungsi tersebut adalah pointer ke item data akumulator. Secara default, setiap item data akumulator diinisialisasi ke nol (seolah-olah oleh memset); namun, Anda dapat menulis fungsi penginisialisasi untuk melakukan hal lain.

Contoh: Dalam kernel addint, item data akumulator (dengan jenis int) digunakan untuk menambahkan nilai input. Tidak ada fungsi penginisialisasi di sini sehingga setiap item data akumulator diinisialisasi ke nol.

Contoh: Dalam kernel findMinAndMax, item data akumulator (dengan jenis MinAndMax) digunakan untuk melacak nilai minimum dan maksimum yang ditemukan sejauh ini. Terdapat fungsi penginisialisasi untuk menetapkannya masing-masing ke LONG_MAX dan LONG_MIN; serta untuk menetapkan lokasi nilai ini ke -1, yang menunjukkan bahwa nilai tersebut sebenarnya tidak ada di bagian (kosong) input yang telah diproses.

RenderScript memanggil fungsi akumulator Anda satu kali untuk setiap koordinat dalam input. Biasanya, fungsi akan mengupdate item data akumulator dengan cara tertentu menurut input yang ada.

Contoh: Dalam kernel addint, fungsi akumulator menambahkan nilai Elemen input ke item data akumulator.

Contoh: Dalam kernel findMinAndMax, fungsi akumulator akan memeriksa apakah nilai Elemen input kurang dari atau sama dengan nilai minimum yang dicatat dalam item data akumulator, dan/atau lebih besar dari atau sama dengan nilai maksimum yang dicatat dalam item data akumulator, lalu mengupdate item data akumulator sesuai dengan hasilnya.

Setelah fungsi akumulator dipanggil satu kali untuk setiap koordinat dalam input, RenderScript harus menggabungkan item data akumulator menjadi satu item data akumulator. Anda dapat menulis fungsi penggabung untuk melakukannya. Jika fungsi akumulator memiliki satu input tanpa argumen khusus, Anda tidak perlu menulis fungsi penggabung; RenderScript akan menggunakan fungsi akumulator untuk menggabungkan item data akumulator. (Anda masih dapat menulis fungsi penggabung jika perilaku default ini bukanlah yang Anda inginkan.)

Contoh: Dalam kernel addint, tidak ada fungsi penggabung sehingga fungsi akumulator akan digunakan. Ini adalah perilaku yang benar, karena jika kita membagi kumpulan nilai menjadi dua bagian lalu menambahkan nilai dalam keduanya secara terpisah, menambahkan kedua jumlah tersebut akan sama dengan menambahkan seluruh kumpulan.

Contoh: Dalam kernel findMinAndMax, fungsi penggabung akan memeriksa apakah nilai minimum yang terekam dalam item data akumulator "sumber" *val kurang dari nilai minimum yang terekam dalam item data akumulator “tujuan" *accum, lalu mengupdate *accum sesuai hasilnya. Fungsi ini akan melakukan tugas yang serupa untuk nilai maksimum. Tindakan ini akan mengupdate *accum ke kondisi yang akan dimilikinya apabila semua nilai input telah diakumulasi ke dalam *accum, bukan sebagian ke *accum dan sebagian lainnya ke *val.

Setelah semua item data akumulator digabungkan, RenderScript menentukan hasil pengurangan untuk dikembalikan ke Java. Anda dapat menulis fungsi outconverter untuk melakukannya. Anda tidak perlu menulis fungsi outconverter jika ingin menjadikan nilai akhir item data akumulator gabungan sebagai hasil pengurangan.

Contoh: Dalam kernel addint, tidak ada fungsi outconverter. Nilai akhir item data gabungan adalah jumlah dari semua Elemen input, yang merupakan nilai yang ingin kita kembalikan.

Contoh: Dalam kernel findMinAndMax, fungsi outconverter menginisialisasi nilai hasil int2 untuk mempertahankan lokasi nilai minimum dan maksimum yang dihasilkan dari penggabungan semua item data akumulator.

Menulis kernel pengurangan

#pragma rs reduce menentukan kernel pengurangan dengan menetapkan namanya serta nama dan peran fungsi yang membentuk kernel tersebut. Semua fungsi tersebut harus bersifat static. Kernel pengurangan selalu membutuhkan fungsi accumulator; Anda dapat menghilangkan beberapa atau semua fungsi lainnya, bergantung pada apa yang Anda ingin kernel lakukan.

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

Arti item dalam #pragma adalah sebagai berikut:

  • reduce(kernelName) (wajib): Menentukan bahwa kernel pengurangan sedang ditentukan. Metode Java yang ditampilkan, reduce_kernelName, akan meluncurkan kernel.
  • initializer(initializerName) (opsional): Menentukan nama fungsi penginisialisasi bagi kernel pengurangan ini. Saat Anda meluncurkan kernel, RenderScript akan memanggil fungsi ini satu kali untuk setiap item data akumulator. Fungsi ini harus ditentukan seperti berikut:

    static void initializerName(accumType *accum) { … }

    accum adalah pointer ke item data akumulator yang perlu diinisialisasi oleh fungsi ini.

    Jika Anda tidak menyediakan fungsi penginisialisasi, RenderScript akan menginisialisasi setiap item data akumulator ke nol (seolah-olah oleh memset), dan berperilaku seolah-olah terdapat fungsi penginisialisasi yang terlihat seperti berikut:

    static void initializerName(accumType *accum) {
      memset(accum, 0, sizeof(*accum));
    }
  • accumulator(accumulatorName) (wajib): Menetapkan nama fungsi akumulator untuk kernel pengurangan ini. Saat Anda meluncurkan kernel, RenderScript akan memanggil fungsi ini satu kali untuk setiap koordinat dalam input, untuk mengupdate item data akumulator dengan cara tertentu sesuai dengan input tersebut. Fungsi ini harus ditentukan seperti berikut:

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

    accum adalah pointer ke item data akumulator yang perlu diubah oleh fungsi ini. in1 hingga inN adalah satu atau beberapa argumen yang otomatis diisi berdasarkan input yang diteruskan ke peluncuran kernel, satu argumen untuk setiap input. Fungsi akumulator dapat secara opsional menggunakan salah satu dari argumen khusus yang ada.

    Contoh kernel dengan beberapa input adalah dotProduct.

  • combiner(combinerName)

    (opsional): Menetapkan nama fungsi penggabung untuk kernel pengurangan ini. Setelah memanggil fungsi akumulator satu kali untuk setiap koordinat dalam input, RenderScript akan memanggil fungsi ini sebanyak yang diperlukan untuk menggabungkan semua item data akumulator menjadi satu item data akumulator. Fungsi ini harus ditentukan sebagai berikut:

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

    accum adalah pointer ke item data akumulator "tujuan" yang perlu diubah oleh fungsi ini. other adalah pointer ke item data akumulator "sumber" yang perlu "digabungkan" oleh fungsi ini menjadi *accum.

    CATATAN: Terdapat kemungkinan bahwa *accum, *other, atau keduanya telah diinisialisasi, tetapi belum pernah diteruskan ke fungsi akumulator; yaitu, salah satu atau keduanya tidak pernah diupdate menurut data input apa pun. Misalnya, dalam kernel findMinAndMax, fungsi penggabung fMMCombiner akan secara eksplisit memeriksa idx < 0 karena menunjukkan item data akumulator, yang nilainya adalah INITVAL.

    Jika Anda tidak menyediakan fungsi penggabung, RenderScript akan menggunakan fungsi akumulator sebagai gantinya, berperilaku seolah-olah ada fungsi penggabung yang terlihat seperti berikut:

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

    Fungsi penggabung bersifat wajib jika kernel memiliki lebih dari satu input, jika jenis data input tidak sama dengan jenis data akumulator, atau jika fungsi akumulator menggunakan satu atau beberapa argumen khusus.

  • outconverter(outconverterName) (opsional): Menetapkan nama fungsi outconverter untuk kernel pengurangan ini. Setelah menggabungkan semua item data akumulator, RenderScript akan memanggil fungsi ini untuk menentukan hasil pengurangan yang akan dikembalikan ke Java. Fungsi ini harus ditentukan seperti berikut:

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

    result adalah pointer ke item data hasil (dialokasikan, tetapi tidak diinisialisasi oleh runtime RenderScript) yang perlu diinisialisasi oleh fungsi ini dengan hasil pengurangan. resultType adalah jenis item data tersebut, yang tidak harus sama dengan accumType. accum adalah pointer ke item data akumulator akhir yang dihitung oleh fungsi penggabung.

    Jika Anda tidak menyediakan fungsi outconverter, RenderScript akan menyalin item data akumulator akhir ke item data hasil, yang berperilaku seolah-olah ada fungsi outconverter yang terlihat seperti berikut:

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

    Jika Anda menginginkan jenis hasil yang berbeda dari jenis data akumulator, fungsi outconverter bersifat wajib.

Perlu diketahui bahwa kernel memiliki jenis input, jenis item data akumulator, dan jenis hasil yang semuanya tidak harus sama. Sebagai contoh, dalam kernel findMinAndMax, jenis input long, jenis item data akumulator MinAndMax, dan jenis hasil int2 semuanya berbeda.

Apa yang tidak boleh Anda asumsikan?

Anda tidak boleh mengandalkan jumlah item data akumulator yang dibuat oleh RenderScript untuk peluncuran kernel tertentu. Tidak ada jaminan bahwa dua peluncuran kernel yang sama dengan input yang sama akan membuat jumlah item data akumulator yang sama.

Anda tidak boleh bergantung pada urutan pemanggilan fungsi penginisialisasi, akumulator, dan penggabung oleh RenderScript; beberapa di antaranya bahkan dapat dipanggil oleh RenderScript secara paralel. Tidak ada jaminan bahwa dua peluncuran kernel yang sama dengan input yang sama akan mengikuti urutan yang sama. Satu-satunya jaminan adalah bahwa hanya fungsi penginisialisasi yang akan menganggap item data akumulator yang belum diinisialisasi. Contoh:

  • Tidak ada jaminan bahwa semua item data akumulator akan diinisialisasi sebelum fungsi akumulator dipanggil, meskipun fungsi ini hanya akan dipanggil pada item data akumulator yang sudah diinisialisasi.
  • Tidak ada jaminan terkait urutan penerusan Elemen input ke fungsi akumulator.
  • Tidak ada jaminan bahwa fungsi akumulator telah dipanggil untuk semua Elemen input sebelum fungsi penggabung dipanggil.

Salah satu konsekuensinya adalah bahwa kernel findMinAndMax tidak bersifat deterministik; jika input berisi lebih dari satu kemunculan nilai minimum atau maksimum yang sama, tidak ada cara untuk mengetahui kemunculan mana yang akan ditemukan oleh kernel.

Apa yang harus Anda jamin?

Sistem RenderScript dapat memilih untuk mengeksekusi kernel dalam berbagai cara. Oleh karena itu, Anda harus mengikuti aturan tertentu untuk memastikan kernel berperilaku sesuai yang Anda inginkan. Jika tidak mengikuti aturan ini, Anda mungkin mendapatkan hasil yang salah, perilaku nondeterministik, atau error runtime.

Aturan di bawah sering kali menyatakan bahwa dua item data akumulator harus memiliki "nilai yang sama". Apa maksudnya? Jawabannya bergantung pada apa yang Anda inginkan dari kernel tersebut. Untuk pengurangan matematis seperti addint, biasanya akan cukup logis untuk mengartikan "sama" sebagai kesetaraan matematika. Untuk penelusuran “pilih apa saja” seperti findMinAndMax ("cari lokasi nilai input minimum dan maksimum") dengan kemungkinan kemunculan nilai input yang sama lebih dari satu kali, semua lokasi dari nilai input yang ditentukan harus dianggap "sama". Anda dapat menulis kernel yang serupa untuk "menemukan lokasi nilai input minimum dan maksimum paling kiri", dengan (katakanlah) nilai minimum di lokasi 100 lebih diutamakan daripada nilai minimum identik di lokasi 200; untuk kernel ini, "sama" berarti lokasi identik, bukan hanya nilai identik, dan fungsi akumulator serta penggabung harus berbeda dari yang ditujukan untuk findMinAndMax.

Fungsi penginisialisasi harus membuat nilai identitas. Artinya, jika I dan A adalah item data akumulator yang diinisialisasi oleh fungsi penginisialisasi, dan I belum pernah diteruskan ke fungsi akumulator (tetapi A mungkin sudah pernah), maka
  • combinerName(&A, &I) harus membiarkan A tetap sama
  • combinerName(&I, &A) harus membiarkan I tetap sama seperti A

Contoh: Dalam kernel addint, item data akumulator diinisialisasi ke nol. Fungsi penggabung untuk kernel ini melakukan penambahan; nol adalah nilai identitas untuk penambahan.

Contoh: Dalam kernel findMinAndMax, item data akumulator diinisialisasi ke INITVAL.

  • fMMCombiner(&A, &I) membiarkan A tetap sama, karena I adalah INITVAL.
  • fMMCombiner(&I, &A) menetapkan I ke A, karena I adalah INITVAL.

Oleh karena itu, INITVAL merupakan nilai identitas.

Fungsi penggabung harus bersifat komutatif. Artinya, jika A dan B merupakan item data akumulator yang diinisialisasi oleh fungsi penginisialisasi, dan mungkin telah diteruskan ke fungsi akumulator sebanyak nol atau beberapa kali, maka combinerName(&A, &B) harus menetapkan A ke nilai yang sama yang ditetapkan oleh combinerName(&B, &A) untuk B.

Contoh: Dalam kernel addint, fungsi penggabung menambahkan dua nilai item data akumulator; penambahan bersifat komutatif.

Contoh: Dalam kernel findMinAndMax, fMMCombiner(&A, &B) sama dengan A = minmax(A, B), dan minmax bersifat komutatif sehingga fMMCombiner juga demikian.

Fungsi penggabung harus bersifat asosiatif. Artinya, jika A, B, dan C adalah item data akumulator yang diinisialisasi oleh fungsi penginisialisasi, dan mungkin telah diteruskan ke fungsi akumulator sebanyak nol atau beberapa kali, dua urutan kode berikut harus menetapkan A ke nilai yang sama:

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

Contoh: Dalam kernel addint, fungsi penggabung menambahkan dua nilai item data akumulator:

  • 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
    

Penambahan bersifat asosiatif sehingga fungsi penggabung juga demikian.

Contoh: Dalam kernel findMinAndMax,

fMMCombiner(&A, &B)
sama dengan
A = minmax(A, B)
sehingga kedua urutan adalah

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

minmax bersifat asosiatif sehingga fMMCombiner juga demikian.

Bersama-sama, fungsi akumulator dan fungsi penggabung harus mematuhi aturan folding dasar. Artinya, jika A dan B adalah item data akumulator, kemudian A telah diinisialisasi oleh fungsi penginisialisasi dan mungkin telah diteruskan ke fungsi akumulator sebanyak nol atau beberapa kali, B belum diinisialisasi, dan args adalah daftar argumen input dan argumen khusus untuk panggilan tertentu ke fungsi akumulator, maka dua urutan kode berikut harus menetapkan A ke nilai yang sama:

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

Contoh: Dalam kernel addint, untuk nilai input V:

  • Pernyataan 1 sama dengan A += V
  • Pernyataan 2 sama dengan B = 0
  • Pernyataan 3 sama dengan B += V, yang sama dengan B = V
  • Pernyataan 4 sama dengan A += B, yang sama dengan A += V

Pernyataan 1 dan 4 menetapkan A ke nilai yang sama sehingga kernel ini mematuhi aturan folding dasar.

Contoh: Dalam kernel findMinAndMax, untuk nilai input V pada koordinat X:

  • Pernyataan 1 sama dengan A = minmax(A, IndexedVal(V, X))
  • Pernyataan 2 sama dengan B = INITVAL
  • Pernyataan 3 sama dengan
    B = minmax(B, IndexedVal(V, X))
    
    yang, karena B adalah nilai awal, sama dengan
    B = IndexedVal(V, X)
    
  • Pernyataan 4 sama dengan
    A = minmax(A, B)
    
    , yang sama dengan
    A = minmax(A, IndexedVal(V, X))
    

Pernyataan 1 dan 4 menetapkan A ke nilai yang sama sehingga kernel ini mematuhi aturan folding dasar.

Memanggil kernel pengurangan dari kode Java

Untuk kernel pengurangan dengan nama kernelName yang ditentukan dalam file filename.rs, terdapat tiga metode yang ditampilkan dalam class 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);

Berikut adalah beberapa contoh untuk memanggil kernel 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();

Metode 1 memiliki satu argumen Allocation input untuk setiap argumen input dalam fungsi akumulator kernel. Runtime RenderScript akan melakukan pemeriksaan untuk memastikan bahwa semua Alokasi input memiliki dimensi yang sama, dan bahwa jenis Element dari setiap Alokasi input cocok dengan argumen input yang sesuai dari prototipe fungsi akumulator. Jika salah satu pemeriksaan ini gagal, RenderScript akan menampilkan pengecualian. Kernel dieksekusi pada setiap koordinat dalam dimensi tersebut.

Metode 2 sama dengan Metode 1, kecuali bahwa Metode 2 menggunakan argumen tambahan sc yang dapat digunakan untuk membatasi eksekusi kernel pada sebuah subset koordinat.

Metode 3 sama dengan Metode 1, tetapi metode ini menggunakan input array Java, bukan menggunakan input Alokasi. Dengan demikian, Anda tidak perlu lagi menulis kode untuk membuat Alokasi secara eksplisit dan menyalin data kepadanya dari array Java. Namun, menggunakan Metode 3, bukan Metode 1, tidak akan meningkatkan performa kode. Untuk setiap array input, Metode 3 akan membuat Alokasi 1 dimensi sementara dengan jenis Element yang sesuai dan setAutoPadding(boolean) yang diaktifkan, lalu menyalin array tersebut ke Alokasi seolah-olah menggunakan metode copyFrom() yang sesuai dari Allocation. Metode ini kemudian memanggil Metode 1, yang meneruskan Alokasi sementara tersebut.

CATATAN: Jika aplikasi Anda akan melakukan beberapa panggilan kernel dengan array yang sama, atau dengan array yang berbeda dari dimensi dan jenis Elemen yang sama, Anda dapat meningkatkan performa dengan membuat, mengisi, dan menggunakan kembali Alokasi sendiri secara eksplisit, bukan menggunakan Metode 3.

javaFutureType, jenis pengembalian metode pengurangan yang ditampilkan, adalah class bertingkat statis yang ditampilkan dalam class ScriptC_filename. Jenis ini merepresentasikan hasil eksekusi kernel pengurangan selanjutnya. Untuk mendapatkan hasil aktual eksekusi, panggil metode get() class tersebut, yang mengembalikan nilai berjenis javaResultType. get() berjalan secara sinkron.

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 ditentukan dari resultType milik fungsi outconverter. Kecuali resultType adalah jenis yang tidak ditandatangani (skalar, vektor, atau array), javaResultType adalah jenis Java yang sesuai secara langsung. Jika resultType adalah jenis yang tidak ditandatangani dan ada jenis bertanda tangan Java yang lebih besar, maka javaResultType adalah jenis bertanda tangan Java yang lebih besar tersebut; jika tidak, jenis Java yang sesuai secara langsung akan ditetapkan. Contoh:

  • Jika resultType adalah int, int2, atau int[15], maka javaResultType adalah int, Int2, atau int[]. Semua nilai resultType dapat direpresentasikan oleh javaResultType.
  • Jika resultType adalah uint, uint2, atau uint[15], maka javaResultType adalah long, Long2, atau long[]. Semua nilai resultType dapat direpresentasikan oleh javaResultType.
  • Jika resultType adalah ulong, ulong2, atau ulong[15], javaResultType adalah long, Long2, atau long[]. Ada beberapa nilai tertentu dari resultType yang tidak dapat direpresentasikan oleh javaResultType.

javaFutureType adalah jenis hasil selanjutnya yang sesuai dengan resultType milik fungsi outconverter.

  • Jika resultType bukan merupakan jenis array, maka javaFutureType adalah result_resultType.
  • Jika resultType merupakan array panjang Count dengan anggota dari jenis memberType, maka javaFutureType adalah resultArrayCount_memberType.

Contoh:

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() { … }
  }
}

Jika javaResultType adalah jenis objek (termasuk jenis array), setiap panggilan ke javaFutureType.get() pada instance yang sama akan mengembalikan objek yang sama.

Jika javaResultType tidak dapat merepresentasikan semua nilai dari jenis resultType, dan kernel pengurangan menghasilkan nilai yang tidak dapat direpresentasikan, javaFutureType.get() akan menampilkan pengecualian.

Metode 3 dan devecSiInXType

devecSiInXType adalah jenis Java yang sesuai dengan inXType dari argumen fungsi akumulator yang sesuai. Kecuali inXType adalah jenis yang tidak ditandatangani atau jenis vektor, devecSiInXType adalah jenis Java yang sesuai secara langsung. Jika inXType adalah jenis skalar yang tidak ditandatangani, maka devecSiInXType adalah jenis Java yang sesuai secara langsung dengan jenis skalar bertanda tangan yang berukuran sama. Jika inXType adalah jenis vektor yang ditandatangani, maka devecSiInXType adalah jenis Java yang sesuai secara langsung dengan jenis komponen vektor. Jika inXType adalah jenis vektor yang tidak ditandatangani, maka devecSiInXType adalah jenis Java yang sesuai secara langsung dengan jenis skalar bertanda tangan yang berukuran sama dengan jenis komponen vektor. Contoh:

  • Jika inXType adalah int, maka devecSiInXType adalah int.
  • Jika inXType adalah int2, maka devecSiInXType adalah int. Array adalah representasi yang disatukan: Array memiliki Elemen skalar dua kali lebih banyak sebagaimana Alokasi memiliki Elemen vektor 2 komponen. Cara ini sama dengan cara kerja metode copyFrom() dari Allocation.
  • Jika inXType adalah uint, maka deviceSiInXType adalah int. Nilai yang ditandatangani dalam array Java diinterpretasikan sebagai nilai yang tidak ditandatangani dari bitpattern yang sama dalam Alokasi. Cara ini sama dengan cara kerja metode copyFrom() dari Allocation.
  • Jika inXType adalah uint2, maka deviceSiInXType adalah int. Cara ini merupakan kombinasi dari penanganan int2 dan uint: Array merupakan representasi yang disatukan, dan nilai yang ditandatangani array Java diinterpretasikan sebagai nilai Elemen RenderScript yang tidak ditandatangani.

Perlu diketahui bahwa untuk Metode 3, jenis input ditangani secara berbeda dari jenis hasil:

  • Input vektor skrip disatukan pada sisi Java, sedangkan hasil vektor skrip tidak.
  • Input skrip yang tidak ditandatangani direpresentasikan sebagai input bertanda tangan yang berukuran sama di sisi Java, sedangkan hasil skrip yang tidak ditandatangani direpresentasikan sebagai jenis bertanda tangan yang diperluas di sisi Java (kecuali untuk ulong).

Contoh lain kernel pengurangan

#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];
}

Contoh kode lainnya

Contoh BasicRenderScript, RenderScriptIntrinsic, dan Hello Compute secara lebih lanjut menunjukkan cara menggunakan API yang dibahas di halaman ini.