RenderScript 簡介

RenderScript 是一種架構,可用於在 Android 上以高效能執行需要進行大量運算的工作。RenderScript 主要適用於資料平行運算,但也可為序列工作負載帶來好處。RenderScript 執行階段會透過裝置上可用的各個處理器 (例如多核心 CPU 和 GPU) 平行處理工作。這可讓您專注於表示演算法,而非安排執行工作。RenderScript 特別適合用於執行圖片處理作業、計算攝影或電腦視覺的應用程式。

如要開始使用 RenderScript,您必須先瞭解兩大概念:

  • 「語言」本身是 C99 衍生語言,用於編寫高效能運算程式碼。「編寫 RenderScript 核心」一節會說明如何使用這個語言編寫運算核心。
  • 「控制 API」是用於管理 RenderScript 資源的生命週期,以及控管核心執行作業。您可以透過三種不同的語言使用這個 API:Java、Android NDK 中的 C++,以及 C99 衍生的核心語言。「透過 Java 程式碼使用 RenderScript」和「單一資源 RenderScript」章節分別說明了第一種和第三種選項。

編寫 RenderScript 核心

RenderScript 核心通常位於 <project_root>/src/rs 目錄內的 .rs 檔案中。每個 .rs 檔案都稱為「指令碼」,且每個指令碼都包含一組核心、函式和變數。指令碼可包含:

  • 一個 pragma 宣告項目 (#pragma version(1)),用於宣告這個指令碼中使用的 RenderScript 核心語言版本。目前唯一有效的值是 1。
  • 一個 pragma 宣告項目 (#pragma rs java_package_name(com.example.app)),用於宣告這個指令碼反映的 Java 類別套件名稱。請注意,.rs 檔案必須是應用程式套件 (而非程式庫專案) 的一部分。
  • 零或多個可叫用函式。可叫用函式是單一執行緒 RenderScript 函式,可使用任意引數透過 Java 程式碼呼叫,通常適合用於大型處理管線中的初始設定或序列運算作業。
  • 零或多個全域指令碼。全域指令碼與 C 中的全域變數類似。全域指令碼可透過 Java 程式碼存取,通常用於傳遞至 RenderScript 核心的參數。如要進一步瞭解全域指令碼,請按這裡

  • 零或多個運算核心。運算核心是一個或一組函式,可以指示 RenderScript 執行階段針對一組資料平行執行相關作業。運算核心有兩種,分別是「對應」核心 (又稱為「foreach」核心) 和「縮減」核心。

    「對應核心」是針對一組相同維度的 Allocations 運作的平行函式。根據預設,對應核心會針對這些維度中的每個座標分別執行一次。這類核心通常是用於將一組輸入 Allocations 轉換為輸出 Allocation (一次轉換一個 Element),但也有其他用途。

    • 以下是簡易對應核心的範例:

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

      在大部分情況下,這與標準 C 函式相同。套用至函式原型的 RS_KERNEL 屬性指出函式為 RenderScript 對應核心,而非叫用函式。系統會根據傳遞至核心啟動的輸入 Allocation 自動填入 in 引數。如要瞭解引數 xy,請參閱下方的討論。系統會在輸出 Allocation 中的適當位置自動寫入核心傳回的值。根據預設,系統會針對整個輸入 Allocation 執行這個核心,並針對 Allocation 中每個 Element 分別執行一次核心函式。

      對應核心可能有一或多個輸入 Allocations、單一輸出 Allocation 或同時有這兩種情況。RenderScript 執行階段會進行檢查,確認所有輸入和輸出配置的維度皆相同,且輸入和輸出配置的 Element 類型都與核心的原型相符。如果其中一項檢查失敗,RenderScript 就會擲回例外狀況。

      注意:在 Android 6.0 (API 級別 23) 以下版本中,單一對應核心最多只能有一個輸入 Allocation

      如果您需要的輸入或輸出 Allocations 數量比核心提供的還多,這些物件應繫結到 rs_allocation 全域指令碼,並透過 rsGetElementAt_type()rsSetElementAt_type() 使用核心或可叫用函式存取。

      附註:RS_KERNEL 是為了方便起見,由 RenderScript 自動定義的巨集:

      #define RS_KERNEL __attribute__((kernel))

    「縮減核心」是針對一組相同維度輸入 Allocations 運作的一系列函式。根據預設,系統會針對這些維度中的每個座標分別執行一次此核心的加總函式。這類核心通常是用於將一組輸入 Allocations「縮減」為單一值,但也有其他用途。

    • 以下這個簡易縮減核心範例會加總其輸入的 Elements

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

      縮減核心是由一或多個使用者編寫的函式組成。#pragma rs reduce 是用來定義核心,方法是指定其名稱 (在此範例中為 addint) 以及構成核心的函式名稱和角色 (在此範例中為 accumulator 函式 addintAccum)。所有這類函式都必須為 static。縮減核心一律需要 accumulator 函式。視您要將核心用於何種目的而定,縮減核心也可能有其他函式。

      縮減核心加總函式必須傳回 void,且至少要有兩個引數。第一個引數 (在此範例中為 accum) 是指向「加總資料項目」的指標,第二個引數 (在此範例中為 val) 則會根據傳遞至核心啟動的輸入 Allocation 自動填入相關內容。加總資料項目是由 RenderScript 執行階段建立,預設會初始化至零。根據預設,系統會針對整個輸入 Allocation 執行這個核心,並針對 Allocation 中每個 Element 分別執行一次加總函式。此外,系統預設會將加總資料項目的最終值視為縮減結果,並傳回至 Java。RenderScript 執行階段會進行檢查,確保輸入配置的 Element 類型與加總函式的原型相符。如果兩者不相符,RenderScript 就會擲回例外狀況。

      縮減核心有一或多個輸入 Allocations,但沒有任何輸出 Allocations

      如要進一步瞭解縮減核心,請按這裡

      Android 7.0 (API 級別 24) 以上版本支援縮減核心。

    對應核心函式或縮減核心加總函式可透過特殊引數 xyz (類型必須為 intuint32_t) 存取目前執行作業的座標。這些引數為選用項目。

    對應核心函式或縮減核心加總函式也可使用 rs_kernel_context 類型的選用特殊引數 context。有一系列執行階段 API 必須使用這個引數,才能用於查詢目前質詢作業的特定屬性,例如 rsGetDimXcontext 引數適用於 Android 6.0 (API 級別 23) 以上版本。

  • 選用的 init() 函式。init() 函式是特殊類型的可叫用函式,會在指令碼首次執行個體化時由 RenderScript 執行。這可讓系統在建立指令碼時自動進行某些運算。
  • 零或多個靜態全域指令碼和靜態函式。靜態全域指令碼等同於全域指令碼,只是無法透過 Java 程式碼存取。靜態函式是標準 C 函式,可透過指令碼中的任何核心或可叫用函式呼叫,但不會提供給 Java API。如果不必透過 Java 程式碼存取全域指令碼或函式,那麼強烈建議您將其宣告為 static

設定浮點精確度

您可以控制指令碼中的浮點精確度。如果不需要完整的 IEEE 754-2008 標準 (預設使用的標準),這個做法就非常實用。下列 pragma 可以設定不同的浮點精確度:

  • #pragma rs_fp_full (未指定時的預設值):適用於需要 IEEE 754-2008 標準所述浮點精確度的應用程式。
  • #pragma rs_fp_relaxed:適用於不需要嚴格遵守 IEEE 754-2008 規範、可容許較低精確度的應用程式。這個模式支援將非正規數清除至零,以及朝零捨去。
  • #pragma rs_fp_imprecise:適用於沒有嚴格精確度要求的應用程式。這個模式支援 rs_fp_relaxed 的所有功能和下列行為:
    • 產生 -0.0 的作業可改為傳回 +0.0。
    • INF 和 NAN 作業為未定義狀態。

大多數應用程式都能使用 rs_fp_relaxed,而不會產生任何副作用。由於某些額外最佳化功能 (例如 SIMD CPU 指示) 只能在採用寬鬆精確度時使用,因此這對於部分架構而言很有幫助。

透過 Java 存取 RenderScript API

開發採用 RenderScript 的 Android 應用程式時,您可以透過下列其中一種方法使用 Java 存取 RenderScript API:

這兩種方法的優缺點如下:

  • 如果選用支援資料庫 API,則無論您使用的 RenderScript 功能為何,應用程式的 RenderScript 部分都會與搭載 Android 2.3 (API 級別 9) 以上版本的裝置相容。相較於使用原生 (android.renderscript) API,這可讓應用程式在更多裝置上運作。
  • 支援資料庫 API 不提供某些 RenderScript 功能。
  • 相較於原生 (android.renderscript) API,使用支援資料庫 API 時的 APK 大小可能會大上許多。

使用 RenderScript 支援資料庫 API

如要使用支援資料庫 RenderScript API,您必須先設定開發環境,且必須使用下列 Android SDK 工具:

  • Android SDK 工具 22.2 以上版本
  • Android SDK Build Tools 18.1.0 以上修訂版本

請注意,從 Android SDK Build Tools 24.0.0 開始,Android 2.2 (API 級別 8) 已不再受支援。

您可以在 Android SDK Manager 中查看及更新這些工具的安裝版本。

如何使用支援資料庫 RenderScript API:

  1. 確認您已安裝必要的 Android SDK 版本。
  2. 更新 Android 建構程序設定,加入 RenderScript 設定:
    • 開啟應用程式模組的應用程式資料夾所含的 build.gradle 檔案。
    • 在檔案中新增以下 RenderScript 設定:

      Groovy

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

      Kotlin

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

      上述設定是用來控制 Android 建構程序中的特定行為:

      • renderscriptTargetApi:指定要產生的位元碼版本。建議您將這個值設為能夠提供所有所用功能的最低 API 級別,並將 renderscriptSupportModeEnabled 設為 true。這項設定的有效值,是介於 11 至最新發布 API 級別之間的任何整數值。如果應用程式資訊清單中指定的最低 SDK 版本設為不同的值,系統會忽略該值,並使用建構檔案中的指定值來設定最低 SDK 版本。
      • renderscriptSupportModeEnabled:指定當用於執行的裝置不支援目標版本時,所產生的位元碼應改回使用相容版本。
  3. 在使用 RenderScript 的應用程式類別中,新增支援資料庫類別匯入項目:

    Kotlin

    import android.support.v8.renderscript.*

    Java

    import android.support.v8.renderscript.*;

透過 Java 或 Kotlin 程式碼使用 RenderScript

如要透過 Java 或 Kotlin 程式碼使用 RenderScript,就必須仰賴位於 android.renderscriptandroid.support.v8.renderscript 套件中的 API 類別。大部分的應用程式都遵循相同的基本使用模式:

  1. 初始化 RenderScript 結構定義。透過 create(Context) 建立的 RenderScript 結構定義可確保 RenderScript 能夠使用,並會提供一個物件,用於控制所有後續 RenderScript 物件的生命週期。結構定義建立作業可能會在不同的硬體元件上建立資源,因此可能要長時間執行。請盡可能避免將這項作業加入應用程式的重要路徑。一般來說,應用程式一次只能有一個 RenderScript 結構定義。
  2. 建立至少一個要傳遞至指令碼的 AllocationAllocation 是 RenderScript 物件,可提供儲存空間,用於存放固定數量的資料。指令碼中的核心是使用 Allocation 物件做為輸入和輸出,而如果 Allocation 物件繫結為全域指令碼,則可在核心中透過 rsGetElementAt_type()rsSetElementAt_type() 存取。Allocation 物件可讓陣列從 Java 程式碼傳遞至 RenderScript 程式碼,反之亦然。Allocation 物件通常是使用 createTyped()createFromBitmap() 建立。
  3. 建立必要的指令碼。使用 RenderScript 時,您可以使用以下兩種指令碼:
    • ScriptC:這些是使用者定義的指令碼,如上方的「編寫 RenderScript 核心」一節所述。每個指令碼都有由 RenderScript 編譯器所反映的 Java 類別,方便您透過 Java 程式碼存取指令碼。這個類別的名稱是 ScriptC_filename。舉例來說,如果上方的對應核心位於 invert.rs,且 mRenderScript 中已有 RenderScript 結構定義,用於將指令碼執行個體化的 Java 或 Kotlin 程式碼就會如下:

      Kotlin

      val invert = ScriptC_invert(renderScript)

      Java

      ScriptC_invert invert = new ScriptC_invert(renderScript);
    • ScriptIntrinsic:這些是內建的 RenderScript 核心,適用於高斯模糊、卷積和圖像融合等一般作業。詳情請參閱 ScriptIntrinsic 的子類別。
  4. 為配置填入資料。除了透過 createFromBitmap() 建立的配置以外,任何配置在首次建立時都不包含任何資料。如要為配置填入資料,請在 Allocation 中使用其中一種「複製」方法。「複製」方法為同步性質。
  5. 設定任何必要的全域指令碼您可以使用相同 ScriptC_filename 類別的 set_globalname 方法來設定全域指令碼。舉例來說,如要設定名為 thresholdint 變數,請使用 Java 方法 set_threshold(int);如要設定名為 lookuprs_allocation 變數,請使用 Java 方法 set_lookup(Allocation)set 方法為非同步性質。
  6. 啟動適當的核心和可叫用函式。

    用於啟動特定核心的方法反映在相同的 ScriptC_filename 類別中,名稱為 forEach_mappingKernelName()reduce_reductionKernelName()。這些啟動作業都是非同步性質。視核心的引數而定,方法會使用一或多個配置,且所有配置的維度都必須相同。根據預設,系統會針對這些維度中的每個座標執行核心。如要針對一部分座標執行核心,請將適當的 Script.LaunchOptions 做為最後一個引數傳遞至 forEachreduce 方法。

    使用相同 ScriptC_filename 類別中反映的 invoke_functionName 方法啟動可叫用函式。這些啟動作業都是非同步性質。

  7. Allocation 物件和 javaFutureType 物件擷取資料。如要透過 Java 程式碼存取 Allocation 的資料,您必須使用 Allocation 中的其中一種「複製」方法,將資料複製回 Java。如要取得縮減核心的結果,您必須使用 javaFutureType.get() 方法。「複製」和 get() 方法為同步性質。
  8. 拆解 RenderScript 結構定義。如要刪除 RenderScript 結構定義,您可以使用 destroy(),或是讓系統針對 RenderScript 結構定義物件進行垃圾收集作業。刪除後,如果您進一步使用屬於該結構定義的任何物件,就會擲回例外狀況。

非同步執行模型

反映的 forEachinvokereduceset 方法為非同步性質。在完成所要求的動作前,每個方法都可能回到 Java。不過,系統會依啟動順序將個別動作序列化。

Allocation 類別提供「複製」方法,用於將資料複製到配置及複製配置中的資料。「複製」方法為同步性質,且系統會針對上述碰觸到相同配置的任何非同步動作,將「複製」方法序列化。

反映的 javaFutureType 類別會提供 get() 方法,用於取得縮減結果。get() 為同步性質,且系統會針對縮減作業 (此為非同步性質) 將其序列化。

單一來源 RenderScript

Android 7.0 (API 級別 24) 導入名為「單一來源 RenderScript」的新程式設計功能,可讓您從定義核心的指令碼 (而非 Java) 啟動核心。這個方法目前只適用於對應核心。為了精簡起見,本節將對應核心簡稱為「核心」。這項新功能還支援從指令碼內部建立 rs_allocation 類型的配置。現在您可以完全在指令碼中實作整個演算法,即使需要多次啟動核心也沒問題。這種做法可帶來兩個好處。首先,由於只需使用單一語言實作演算法,因此程式碼會較易讀。此外,多次啟動核心時所需進行的 Java 和 RenderScript 轉換次數較少,因此可加快程式碼執行速度。

使用單一來源 RenderScript 時,請按照「編寫 RenderScript 核心」一節的說明編寫核心,然後編寫可叫用函式,用於呼叫 rsForEach() 來啟動核心。這個 API 會使用核心函式做為第一個參數,後續則為輸入和輸出配置。類似的 API rsForEachWithOptions() 會使用 rs_script_call_t 類型的額外引數,指定輸入和輸出配置的一部分元素,讓核心函式進行處理。

如要開始進行 RenderScript 運算,您必須透過 Java 呼叫可叫用函式。請按照「透過 Java 程式碼使用 RenderScript」一節的步驟操作。在啟動適當的核心步驟中,請使用 invoke_function_name() 呼叫可叫用函式,系統隨即會開始進行完整運算作業 (包括啟動核心)。

您通常需要配置才能儲存中繼結果,並在不同核心啟動作業之間傳遞這些結果。您可以透過 rsCreateAllocation() 建立這類配置。如要使用這個 API,一個簡單易用的格式為 rsCreateAllocation_<T><W>(…),其中「T」是元素的資料類型,「W」則是元素的向量寬度。這個 API 使用 X、Y 和 Z 維度的大小做為引數。如果是 1D 或 2D 配置,則可省略 Y 或 Z 維度的大小。舉例來說,rsCreateAllocation_uchar4(16384) 會建立 16384 個元素的 1D 配置,其中每個元素都是 uchar4 類型。

系統會自動管理配置,您不必明確釋出這類項目。不過,您可以呼叫 rsClearObject(rs_allocation* alloc),指出您不再需要處理基礎配置的 alloc,這樣系統就能盡早釋出資源。

編寫 RenderScript 核心」一節包含可反轉圖片的範例核心。下方範例則進一步延伸,使用單一來源 RenderScript 對圖片套用多個效果。這個範例包含另一個核心 greyscale,可將彩色圖片變成黑白圖片。接著,可叫用函式 process() 將這兩個核心連續套用至輸入圖片,然後產生輸出圖片,輸入和輸出內容的配置會做為 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);
}

您可以透過 Java 或 Kotlin 呼叫 process() 函式,如下所示:

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

這個範例說明如何完全在 RenderScript 語言本身中,實作涉及兩個核心啟動作業的演算法。如果不使用單一來源 RenderScript,您就必須透過 Java 程式碼啟動這兩個核心,將核心啟動作業和核心定義區隔開來,導致整個演算法難以理解。單一來源 RenderScript 程式碼不僅更易讀,還可免除啟動各個核心時的 Java 和指令碼轉換作業。部分疊代演算法可能會啟動核心幾百次,導致這類轉換作業的相關負擔大增。

全域指令碼

「全域指令碼」是指令碼 (.rs) 檔案中一般的非 static 全域變數。針對 filename.rs 檔案中定義的「var」全域指令碼,類別 ScriptC_filename 中會反映一個方法 get_var。除非全域為 const,否則還會有 set_var 方法。

特定全域指令碼有兩個不同的值:「Java」值和「指令碼」值。這些值的行為如下:

  • 如果指令碼中的 var 有靜態初始化器,就會在 Java 和指令碼中指定 var 的初始值。如果沒有,則初始值會是 0。
  • 存取指令碼中的 var 會讀取和寫入其指令碼值。
  • get_var 方法會讀取 Java 值。
  • set_var 方法 (如果有) 會立即寫入 Java 值,並以非同步方式寫入指令碼值。

注意:這表示除了指令碼中的任何靜態初始化器以外,Java 無法取得在指令碼中寫入全域的值。

深入瞭解縮減核心

「縮減」是將一組資料合併為單一值的程序。這在平行程式設計中是實用基元,並有多種應用方式,例如:

  • 計算所有資料的總和或產物
  • 對所有資料執行運算邏輯 (andorxor)
  • 尋找資料中的最小值或最大值
  • 搜尋特定值,或是資料中特定值的座標

在 Android 7.0 (API 級別 24) 以上版本中,RenderScript 支援「縮減核心」,讓您可有效率地執行使用者編寫的縮減運算法。您可以針對具有 1、2 或 3 個維度的輸入啟動縮減核心。

上方是簡易的 addint 縮減核心範例。下方是較複雜的 findMinAndMax 縮減核心,可在單一維度的 Allocation 中找出最小和最大 long 值的位置:

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

附註:如需更多縮減和新範例,請按這裡

為了執行縮減核心,RenderScript 執行階段會建立一或多個稱為加總資料項目的變數,用來保存縮減程序的狀態。RenderScript 執行階段會挑選加總資料項目的數量,盡可能提高執行效能。加總資料項目的類型 (accumType) 取決於核心的「加總函式」,該函式的第一個引數會指向加總資料項目。根據預設,每個加總資料項目都會初始化為零 (彷彿是由 memset 執行)。不過,您可以編寫「初始化函式」執行其他操作。

範例:addint 核心中,int 類型的加總資料項目會用來加總輸入值。由於沒有初始化函式,因此每個加總資料項目都會初始化為零。

範例:findMinAndMax 核心中,MinAndMax 類型的加總資料項目會用來追蹤目前找到的最小值和最大值。有一個初始化函式可將這些值分別設為 LONG_MAXLONG_MIN,以及將這些值的位置設為 -1,表示值實際上不在已處理的輸入部分中 (空白)。

RenderScript 會針對輸入中的每個座標分別呼叫一次加總函式。一般來說,函式應該會根據輸入更新加總資料項目。

範例:addint 核心中,加總函式會將輸入元素的值新增到加總資料項目。

範例:FindMinAndMax 核心中,加總函式會檢查輸入元素的值是否小於或等於加總資料項目中記錄的最小值,且/或大於或等於加總資料項目中記錄的最大值,並據此更新加總資料項目。

針對輸入中的每個座標分別呼叫一次加總函式後,RenderScript 必須將加總資料項目合併為單一加總資料項目。您可以編寫「合併函式」來完成這項作業。如果加總函式有單一輸入,且沒有特殊引數,您就不必編寫合併函式。RenderScript 會使用加總函式來合併加總資料項目。如果這個預設行為不符合您的需求,您仍可編寫合併函式。

範例:addint 核心中沒有合併函式,因此系統會使用加總函式。這是正確的行為,原因是如果我們將一組值拆成兩個部分,並分別加總這兩個部分的值,則兩個總和的相加結果會與整組值的相加結果相同。

範例:FindMinAndMax 核心中,合併函式會檢查「來源」加總資料項目 *val 中記錄的最小值是否小於「目的地」加總資料項目 *accum 中記錄的最小值,並據此更新 *accum。這個函式也會針對最大值執行類似的作業。這會將 *accum 更新為所有輸入值都加總至 *accum (而非部分值加總至 *accum、部分值加總至 *val) 時的狀態。

所有加總資料項目都合併後,RenderScript 會決定要傳回 Java 的縮減結果。您可以編寫「外轉換函式」執行這項作業。如果您希望將合併後加總資料項目的最終值做為縮減結果,就不必編寫外轉換函式。

範例:addint 核心中沒有外轉換函式。合併後資料項目的最終值是輸入所有元素的總和,也就是我們要傳回的值。

範例:findMinAndMax 核心中,外轉換函式會初始化 int2 結果值,用於保存所有加總資料項目合併後的最小值和最大值位置。

編寫縮減核心

#pragma rs reduce 會指定縮減核心名稱,以及構成核心的函式名稱和角色,藉此定義縮減核心。所有這類函式都必須為 static。縮減核心一律需要 accumulator 函式。視您要將核心用於何種目的而定,您可以省略部分或所有其他函式。

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

#pragma 中項目的含義如下:

  • reduce(kernelName) (必要):指出您正在定義縮減核心。反映的 Java 方法 reduce_kernelName 會啟動核心。
  • initializer(initializerName) (選用):指定這個縮減核心的初始化函式名稱。啟動核心時,RenderScript 會針對每個加總資料項目分別呼叫一次這個函式。這個函式必須採用以下方式定義:

    static void initializerName(accumType *accum) {  }

    accum 是一個指標,指向要讓這個函式初始化的加總資料項目。

    如未提供初始化函式,RenderScript 會將每個加總資料項目初始化為零 (彷彿是由 memset 執行),就好像有如下的初始化函式:

    static void initializerName(accumType *accum) {
      memset(accum, 0, sizeof(*accum));
    }
  • accumulator(accumulatorName) (必要):指定這個縮減核心的加總函式名稱。啟動核心時,RenderScript 會針對輸入中的每個座標分別呼叫一次這個函式,藉此根據輸入更新加總資料項目。這個函式必須定義如下:

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

    accum 是一個指標,指向要讓這個函式修改的加總資料項目。in1inN 是一或多個引數,系統會根據傳遞至核心啟動的輸入自動填入這些引數 (每個輸入一個引數)。加總函式可選用任一特殊引數

    包含多個輸入的核心範例是 dotProduct

  • combiner(combinerName)

    (選用):指定這個縮減核心的合併函式名稱。RenderScript 針對輸入中的每個座標分別呼叫一次加總函式後,就會視需要多次呼叫這個函式,將所有加總資料項目合併成單一加總資料項目。這個函式必須採用以下方式定義:

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

    accum 是一個指標,指向要讓這個函式修改的「目的地」加總資料項目。other 是一個指標,指向要讓這個函式「合併」至 *accum 的「來源」加總資料項目。

    注意:*accum 和/或 *other 有可能都已初始化,但從未傳遞至加總函式,即任一者或兩者從未根據任何輸入資料更新。舉例來說,在 findMinAndMax 核心中,合併函式 fMMCombiner 會明確檢查是否有 idx < 0 的情形,因為這代表有值為 INITVAL 的這類加總資料項目。

    如未提供合併函式,RenderScript 會改用加總函式,就好像有如下的合併函式:

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

    如果核心有多個輸入、輸入資料類型與加總資料類型不同,或是加總函式使用一或多個特殊引數,那麼就必須使用合併函式。

  • outconverter(outconverterName) (選用):指定這個縮減核心的外轉換函式名稱。RenderScript 合併所有加總資料項目後,就會呼叫這個函式來決定要傳回 Java 的縮減結果。這個函式必須採用以下方式定義:

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

    result 是一個指標,指向要讓這個函式根據縮減結果進行初始化的結果資料項目 (已分配但尚未由 RenderScript 執行階段初始化)。「resultType」是該資料項目的類型,不需要與「accumType」相同。accum 是一個指標,指向合併函式計算出的最終加總資料項目。

    如未提供外轉換函式,RenderScript 會將最終加總資料項目複製到結果資料項目中,就好像有如下的外轉換函式:

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

    如果想要與加總資料類型不同的結果類型,就必須使用外轉換器函式。

請注意,核心有輸入類型、加總資料項目類型和結果類型,三者不必相同。舉例來說,在 findMinAndMax 核心中,輸入類型 long、加總資料項目類型 MinAndMax 和結果類型 int2 都不相同。

您不能做出哪些假設?

您不得假設 RenderScript 在執行特定核心啟動作業時,一定會建立特定數量的加總資料項目數量。即使利用相同輸入啟動相同核心兩次,所建立的加總資料項目數量也未必相同。

您不得假設 RenderScript 會依特定順序呼叫初始化、加總和合併函式。RenderScript 甚至有可能平行呼叫其中幾種函式。即使利用相同輸入啟動相同核心兩次,也不保證會依特定順序呼叫這些函式。唯一能保證的是,只有初始化函式會看到未經初始化的加總資料項目。例如:

  • 雖然系統只會針對已初始化的加總資料項目呼叫加總函式,但在呼叫加總函式之前,加總資料項目不一定會經過初始化。
  • 輸入元素不一定會依特定順序傳遞至加總函式。
  • 在呼叫合併函式之前,系統未必已針對所有輸入元素呼叫加總函式。

這種情況所造成的一個結果,就是 findMinAndMax 核心具有不確定性:如果相同最小值或最大值在輸入中出現多次,您就無從得知核心會找到哪次出現的最小值或最大值。

您一定要遵循哪些規則?

由於 RenderScript 系統可以選擇以多種不同方式執行核心,因此您必須遵循特定規則,確保核心的行為符合您的期望。如未遵循這些規則,則可能會產生錯誤結果、不確定的行為,或發生執行階段錯誤。

下方的規則往往會指出兩個加總資料項目必須具有「相同的值」。這項要求的確切含義,取決於您希望核心執行的作業。如要進行數學縮減作業 (例如 addint),則「相同」通常指的是數學上的相等。如果是 findMinAndMax (「尋找最小值和最大輸入值的位置」) 等「選擇任一項目」的搜尋,由於相同輸入值可能會出現多次,因此特定輸入值的所有位置都必須視為「相同」。您可以編寫類似的核心,找出「最左側」的最小和最大輸入值位置。在這個情況下,假設有兩個相同的最小值分別位在第 100 和第 200 個位置,那麼系統會選擇位在第 100 個位置的最小值。就這個核心而言,「相同」代表相同的「位置」,而不只是相同的「值」,且加總和合併函式必須與 findMinAndMax 有所差異。

初始化函式必須建立「識別值」也就是說,如果 IA 是由初始化函式初始化的加總資料項目,且 I 從未傳遞至加總函式 (但 A 可能有),那就必須遵循以下規則:
  • combinerName(&A, &I) 必須讓 A 維持不變
  • combinerName(&I, &A) 必須讓 IA 相同

範例:addint 核心中,系統將加總資料項目初始化為零。這個核心的合併函式會執行相加作業,而相加作業的識別值為零。

範例:findMinAndMax 核心中,系統將加總資料項目初始化為 INITVAL

  • fMMCombiner(&A, &I) 會讓 A 維持不變,因為 IINITVAL
  • fMMCombiner(&I, &A) 會將 I 設為 A,因為 IINITVAL

因此,INITVAL 確實是識別值。

合併函式必須具有「可交換」性質。也就是說,如果 AB 是由初始化函式初始化的加總資料項目,且可能從未傳遞至加總函式或已傳遞多次,那麼 combinerName(&A, &B) 必須將 A 設為特定值 (該值與 combinerName(&B, &A)B 設定的值相同)。

範例:addint 核心中,合併函式會將兩個加總資料項目值相加,且相加作業為可交換性質。

範例:FindMinAndMax 核心中,fMMCombiner(&A, &B)A = minmax(A, B) 相同,且由於 minmax 具有可交換性質,因此 fMMCombiner 也一樣。

合併函式必須具有「結合」性質。也就是說,如果 ABC 是由初始化函式初始化的加總資料項目,且可能從未傳遞至加總函式或已傳遞多次,那麼以下兩個程式碼序列必須將 A 設為相同的值

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

範例:addint 核心中,合併函式會將兩個加總資料項目值相加:

  • 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

相加作業具有結合性質,因此合併函式也一樣。

範例:findMinAndMax 核心中:

fMMCombiner(&A, &B)
等同於
A = minmax(A, B)
因此,這兩個序列如下:
  • 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 具有結合性質,因此 fMMCombiner 也一樣。

加總函式和合併函式必須遵循「基本折疊規則」也就是說,如果 AB 是加總資料項目,其中 A 已由初始化函式初始化,且可能從未傳遞至加總函式或已傳遞多次,B 則尚未初始化,而 args 是特定加總函式呼叫的輸入引數和特殊引數清單,那麼以下兩個程式碼序列必須將 A 設為相同的值

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

範例:addint 核心中,針對輸入值 V

  • 陳述式 1 與 A += V 相同
  • 陳述式 2 與 B = 0 相同
  • 陳述式 3 與 B += V 相同,等同於 B = V
  • 陳述式 4 與 A += B 相同,等同於 A += V

陳述式 1 和 4 將 A 設為相同的值,因此這個核心符合基本折疊規則。

範例:findMinAndMax 核心中,針對位於座標 X 的輸入值 V

  • 陳述式 1 與 A = minmax(A, IndexedVal(V, X)) 相同
  • 陳述式 2 與 B = INITVAL 相同
  • 陳述式 3 等同於
    B = minmax(B, IndexedVal(V, X))
    ,因為「B」是初始值,因此等同於
    B = IndexedVal(V, X)
  • 陳述式 4 等同於
    A = minmax(A, B)
    這等同於
    A = minmax(A, IndexedVal(V, X))

陳述式 1 和 4 將 A 設為相同的值,因此這個核心符合基本折疊規則。

透過 Java 程式碼呼叫縮減核心

針對 filename.rs 檔案中定義的「kernelName」縮減核心,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);

以下提供一些呼叫 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();

針對核心加總函式中的每個輸入引數,方法 1 都有一個輸入 Allocation 引數。RenderScript 執行階段會進行檢查,確保所有輸入配置的維度皆相同,且每個輸入配置的 Element 類型都與加總函式原型的對應輸入引數相同。如果有任何檢查失敗,RenderScript 就會擲回例外狀況。系統會針對這些維度中的每個座標分別執行核心。

方法 2 與方法 1 相同,差別在於方法 2 會使用額外的引數 sc,只針對一部分座標執行核心。

方法 3 與方法 1 相同,差別在於使用的是 Java 陣列輸入,而非配置輸入。這樣一來,您就不必編寫程式碼來明確建立配置,並將 Java 陣列中的資料複製到配置中。不過,使用方法 3 (而非方法 1) 並不會提高程式碼的執行效能。針對每個輸入陣列,方法 3 會建立適當 Element 類型並啟用 setAutoPadding(boolean) 的臨時單一維度配置,並將陣列複製到配置中,就好像是由 Allocation 的適當 copyFrom() 方法所執行。接著,系統會呼叫方法 1 來傳遞這些臨時配置。

附註:如果應用程式會使用相同陣列,或是具有相同維度和元素類型的不同陣列發出核心呼叫多次,您可以自行明確建立、填入和重複使用配置,而不使用方法 3,藉此提升執行效能。

javaFutureType 是所反映縮減方法的傳回類型,並且是 ScriptC_filename 類別中的所反映靜態巢狀類別。這代表縮減核心未來的執行結果。如要取得實際的執行結果,請呼叫該類別的 get() 方法,系統會傳回 javaResultType 類型的值。get()同步性質。

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 取決於外轉換函式的 resultType。除非 resultType 是未簽署的類型 (純量、向量或陣列),否則 javaResultType 會是直接相對應的 Java 類型。如果 resultType 是未簽署的類型,而且有較大的 Java 已簽署類型,那麼 javaResultType 會是較大的 Java 已簽署類型。如果沒有的話,則會是直接相對應的 Java 類型。例如:

  • 如果 resultTypeintint2int[15],那麼 javaResultType 會是 intInt2int[]。resultType 的所有值都可由 javaResultType 表示。
  • 如果 resultTypeuintuint2uint[15],那麼 javaResultType 會是 longLong2long[]。resultType 的所有值都可由 javaResultType 表示。
  • 如果 resultTypeulongulong2ulong[15],那麼 javaResultType 會是 longLong2long[]。某些 javaTypeType 的值無法由 resultType 表示。

javaFutureType 是與外轉換函式的 resultType 相對應的未來結果類型。

  • 如果 resultType 不是陣列類型,那麼 javaFutureType 會是 result_resultType
  • 如果 resultType 是長度為 Count 的陣列,且成員類型為 memberType,那麼 javaFutureType 會是 resultArrayCount_memberType

例如:

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

如果 javaResultType 是物件類型 (包括陣列類型),且每次對相同執行個體呼叫 javaFutureType.get() 都會傳回相同物件。

如果 javaResultType 無法代表 resultType 類型的所有值,且縮減核心產生無法表示的值,則 javaFutureType.get() 會擲回例外狀況。

方法 3 和 devecSiInXType

devecSiInXType 是與加總函式對應引數的 inXType 相對應的 Java 類型。除非 inXType 是未簽署的類型或向量類型,否則 devecSiInXType 會是直接相對應的 Java 類型。如果 inXType 是未簽署的純量類型,則 devecSiInXType 會是與相同大小的已簽署純量類型直接相對應的 Java 類型。如果 inXType 是已簽署的向量類型,devecSiInXType 會是與向量元件類型直接相對應的 Java 類型。如果 inXType 是未簽署的向量類型,則 devecSiInXType 會是與已簽署純量類型 (大小與向量元件類型相同) 直接相對應的 Java 類型。例如:

  • 如果 inXTypeint,則 devecSiInXType 會是 int
  • 如果 inXTypeint2,則 devecSiInXType 會是 int。陣列是「扁平化」的表示法,其所包含的「純量」元素數量是配置所含雙元件「向量」元素數量的兩倍。這與 AllocationcopyFrom() 方法運作方式相同。
  • 如果 inXTypeuint,則 deviceSiInXType 會是 int。系統會將 Java 陣列中的已簽署值,解讀為配置中具有相同模式的未簽署值。這與 AllocationcopyFrom() 方法運作方式相同。
  • 如果 inXTypeuint2,則 deviceSiInXType 會是 int。這結合了 int2uint 的處理方式:陣列是扁平化的表示法,且系統會將 Java 陣列未簽署值解讀為 RenderScript 未簽署元素值。

請注意,針對方法 3,輸入類型和結果類型的處理方式並不相同:

  • 系統會在 Java 端將指令碼的向量輸入扁平化,但不會對指令碼的向量結果執行這項作業。
  • 系統會在 Java 端將指令碼的未簽署輸入表示為大小相同的已簽署輸入,但會在 Java 端將指令碼的未簽署結果表示為經過擴增的已簽署類型 (除非是 ulong)。

更多縮減核心範例

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

其他程式碼範例

BasicRenderScriptRenderScriptIntrinsicHello Compute 範例進一步示範了本頁面提及的 API 使用方式。