RenderScript 概览

RenderScript 是一个用于在 Android 上高性能运行计算密集型任务的框架。RenderScript 主要用于数据并行计算,尽管串行工作负载也能从中受益。RenderScript 运行时会将工作并行化到设备上可用的处理器(例如多核 CPU 和 GPU)上。这使您可以专注于表达算法,而不是安排工作。RenderScript 对于执行图像处理、计算摄影或计算机视觉的应用特别有用。

要开始使用 RenderScript,您应该理解两个主要概念

  • 语言本身是一种派生自 C99 的语言,用于编写高性能计算代码。编写 RenderScript 内核描述了如何使用它编写计算内核。
  • 控制 API 用于管理 RenderScript 资源的生命周期和控制内核执行。它有三种不同的语言版本:Java、Android NDK 中的 C++ 以及派生自 C99 的内核语言本身。从 Java 代码中使用 RenderScript单源 RenderScript 分别描述了第一种和第三种选项。

编写 RenderScript 内核

RenderScript 内核通常位于 <project_root>/src/rs 目录中的 .rs 文件中;每个 .rs 文件称为一个 script。每个 script 都包含自己的一组内核、函数和变量。一个 script 可以包含

  • 一个 pragma 声明 (#pragma version(1)),声明此 script 中使用的 RenderScript 内核语言版本。目前,1 是唯一有效的值。
  • 一个 pragma 声明 (#pragma rs java_package_name(com.example.app)),声明由此 script 反射的 Java 类的包名。请注意,您的 .rs 文件必须是应用包的一部分,而不是库项目的一部分。
  • 零个或多个可调用函数。可调用函数是一个单线程 RenderScript 函数,您可以从 Java 代码中以任意参数调用它。这些函数通常用于大型处理管道中的初始设置或串行计算。
  • 零个或多个脚本全局变量。脚本全局变量类似于 C 语言中的全局变量。您可以从 Java 代码访问脚本全局变量,它们通常用于向 RenderScript 内核传递参数。脚本全局变量的详细说明请参阅此处

  • 零个或多个计算内核。计算内核是一个函数或一组函数,您可以指示 RenderScript 运行时在数据集上并行执行它们。有两种计算内核:映射内核(也称为 foreach 内核)和归约内核。

    映射内核是一个并行函数,用于处理一组具有相同维度的 Allocation。默认情况下,它会对这些维度中的每个坐标执行一次。它通常(但不限于)用于将一组输入 Allocation 转换为输出 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 映射内核,而不是可调用函数。in 参数根据传递给内核启动的输入 Allocation 自动填充。参数 xy 将在下方讨论。从内核返回的值将自动写入输出 Allocation 中的适当位置。默认情况下,此内核在其整个输入 Allocation 上运行,对于 Allocation 中的每个 Element,内核函数执行一次。

      映射内核可以有一个或多个输入 Allocation、一个输出 Allocation,或两者兼有。RenderScript 运行时会检查以确保所有输入和输出 Allocation 具有相同的维度,并且输入和输出 Allocation 的 Element 类型与内核的原型匹配;如果其中任何一个检查失败,RenderScript 会抛出异常。

      注意:在 Android 6.0(API 级别 23)之前,映射内核不能拥有一个以上的输入 Allocation

      如果需要比内核拥有的更多的输入或输出 Allocation,这些对象应绑定到 rs_allocation 脚本全局变量,并通过 rsGetElementAt_type()rsSetElementAt_type() 从内核或可调用函数访问。

      注意: RS_KERNEL 是 RenderScript 为了方便您而自动定义的一个宏

      #define RS_KERNEL __attribute__((kernel))

    归约内核是一系列函数,用于处理一组具有相同维度的输入 Allocation。默认情况下,其累加器函数对这些维度中的每个坐标执行一次。它通常(但不限于)用于将一组输入 Allocation“归约”为一个单一值。

    • 这是一个简单的归约内核示例,它将其输入的 Element 相加

      #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 运行时会检查以确保输入 Allocation 的 Element 类型与累加器函数的原型匹配;如果它不匹配,RenderScript 会抛出异常。

      归约内核有一个或多个输入 Allocation,但没有输出 Allocation

      归约内核的详细说明请参阅此处

      Android 7.0(API 级别 24)及更高版本支持归约内核。

    映射内核函数或归约内核累加器函数可以使用特殊参数 xyz 访问当前执行的坐标,这些参数必须是 intuint32_t 类型。这些参数是可选的。

    映射内核函数或归约内核累加器函数还可以接受类型为 rs_kernel_context 的可选特殊参数 context。某些运行时 API 需要它来查询当前执行的特定属性,例如 rsGetDimX。(context 参数在 Android 6.0(API 级别 23)及更高版本中可用。)

  • 一个可选的 init() 函数。init() 函数是一种特殊类型的可调用函数,当 script 首次实例化时,RenderScript 会运行它。这允许在 script 创建时自动进行一些计算。
  • 零个或多个静态脚本全局变量和函数。静态脚本全局变量等同于脚本全局变量,但不能从 Java 代码访问。静态函数是可以从 script 中的任何内核或可调用函数调用的标准 C 函数,但不向 Java API 公开。如果无需从 Java 代码访问某个脚本全局变量或函数,强烈建议将其声明为 static

设置浮点精度

您可以控制脚本中所需的浮点精度级别。如果不需要完整的 IEEE 754-2008 标准(默认使用),这会很有用。以下 pragma 可以设置不同的浮点精度级别

  • #pragma rs_fp_full(未指定时的默认值):适用于需要 IEEE 754-2008 标准规定的浮点精度的应用。
  • #pragma rs_fp_relaxed:适用于不需要严格遵循 IEEE 754-2008 标准且可以容忍较低精度的应用。此模式对非正常数启用 flush-to-zero,并对舍入启用 round-towards-zero。
  • #pragma rs_fp_imprecise:适用于对精度要求不严格的应用。此模式除了启用 rs_fp_relaxed 中的所有功能外,还启用以下功能:
    • 结果为 -0.0 的操作可以返回 +0.0。
    • 对 INF 和 NAN 的操作未定义。

大多数应用都可以使用 rs_fp_relaxed 而不会产生任何副作用。在某些架构上,这可能非常有用,因为只有在放宽精度的情况下才会有额外的优化(例如 SIMD CPU 指令)。

从 Java 访问 RenderScript API

在开发使用 RenderScript 的 Android 应用时,您可以通过两种方式从 Java 访问其 API

权衡如下

  • 如果您使用支持库 API,则应用的 RenderScript 部分将与运行 Android 2.3(API 级别 9)及更高版本的设备兼容,无论您使用哪些 RenderScript 功能。与使用原生 (android.renderscript) API 相比,这允许您的应用在更多设备上运行。
  • 某些 RenderScript 功能无法通过支持库 API 使用。
  • 如果您使用支持库 API,您会得到(可能显著)更大的 APK,而不是使用原生 (android.renderscript) API。

使用 RenderScript 支持库 API

为了使用支持库 RenderScript API,您必须配置您的开发环境以便能够访问它们。使用这些 API 需要以下 Android SDK 工具

  • Android SDK Tools 版本 22.2 或更高
  • Android SDK Build-tools 版本 18.1.0 或更高

请注意,从 Android SDK Build-tools 24.0.0 开始,不再支持 Android 2.2(API 级别 8)。

您可以在Android SDK 管理器中检查和更新这些工具的已安装版本。

使用支持库 RenderScript API

  1. 确保您已安装所需的 Android SDK 版本。
  2. 更新 Android 构建流程的设置以包含 RenderScript 设置
    • 打开应用模块的 app 文件夹中的 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. 创建至少一个 Allocation 以传递给 script。 Allocation 是一个 RenderScript 对象,它提供固定量数据的存储空间。script 中的内核将 Allocation 对象作为输入和输出,并且当绑定为脚本全局变量时,可以使用 rsGetElementAt_type()rsSetElementAt_type() 在内核中访问 Allocation 对象。Allocation 对象允许将数组从 Java 代码传递到 RenderScript 代码,反之亦然。Allocation 对象通常使用 createTyped()createFromBitmap() 创建。
  3. 创建所有必要的 script。使用 RenderScript 时,您可以使用两种类型的 script:
    • ScriptC:这些是上面编写 RenderScript 内核中描述的用户定义的 script。每个 script 都有一个由 RenderScript 编译器反射的 Java 类,以便于从 Java 代码访问该 script;此类的名称为 ScriptC_filename。例如,如果上面的映射内核位于 invert.rs 中,并且 RenderScript 上下文已位于 mRenderScript 中,则用于实例化该 script 的 Java 或 Kotlin 代码如下:

      Kotlin

      val invert = ScriptC_invert(renderScript)

      Java

      ScriptC_invert invert = new ScriptC_invert(renderScript);
    • ScriptIntrinsic:这些是内置的 RenderScript 内核,用于执行常见操作,例如高斯模糊、卷积和图像混合。如需了解更多信息,请参阅 ScriptIntrinsic 的子类。
  4. 用数据填充 Allocation。除了使用 createFromBitmap() 创建的 Allocation 外,Allocation 在首次创建时会填充空数据。要填充 Allocation,请使用 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()。这些启动是异步的。根据内核的参数,该方法接受一个或多个 Allocation,所有 Allocation 都必须具有相同的维度。默认情况下,内核对这些维度中的每个坐标执行;要在这些坐标的子集上执行内核,请将适当的 Script.LaunchOptions 作为最后一个参数传递给 forEachreduce 方法。

    使用反映在同一 ScriptC_filename 类中的 invoke_functionName 方法启动可调用函数。这些启动是异步的。

  7. Allocation 对象和 javaFutureType 对象检索数据。为了从 Java 代码访问 Allocation 中的数据,您必须使用 Allocation 中的“复制”方法之一将数据复制回 Java。为了获取归约内核的结果,您必须使用 javaFutureType.get() 方法。“复制”方法和 get() 方法是同步的。
  8. 销毁 RenderScript 上下文。您可以使用 destroy() 销毁 RenderScript 上下文,或者让 RenderScript 上下文对象被垃圾回收。这会导致属于该上下文的任何对象的任何后续使用都会抛出异常。

异步执行模型

反射的 forEachinvokereduceset 方法是异步的——每个方法可能在完成请求的操作之前就返回到 Java。但是,各个操作是按照启动顺序进行序列化的。

Allocation 类提供了“复制”方法,用于在 Allocation 之间复制数据。“复制”方法是同步的,并且与影响同一 Allocation 的任何上述异步操作进行序列化。

反射的 javaFutureType 类提供了 get() 方法,用于获取归约的结果。get() 是同步的,并且与归约(它是异步的)进行序列化。

单源 RenderScript

Android 7.0(API 级别 24)引入了一个新的编程特性,称为单源 RenderScript,其中内核从定义它们的 script 中启动,而不是从 Java 中启动。这种方法目前仅限于映射内核,在本节中为简洁起见,它们简称为“内核”。此新特性还支持在 script 内部创建类型为 rs_allocation 的 allocation。现在可以在一个 script 中完全实现整个算法,即使需要多次内核启动。这样做有两个好处:代码更具可读性,因为它将算法的实现保留在一种语言中;并且可能更快,因为在多次内核启动中,Java 和 RenderScript 之间的转换更少。

在单源 RenderScript 中,您按照编写 RenderScript 内核中所述编写内核。然后,您编写一个可调用函数,该函数调用 rsForEach() 来启动它们。该 API 以内核函数作为第一个参数,后跟输入和输出 allocation。类似的 API rsForEachWithOptions() 接受一个类型为 rs_script_call_t 的额外参数,该参数指定了输入和输出 allocation 中的元素子集供内核函数处理。

要启动 RenderScript 计算,您需要从 Java 中调用可调用函数。按照从 Java 代码中使用 RenderScript 中的步骤操作。在启动相应的内核这一步中,使用 invoke_function_name() 调用可调用函数,这将启动整个计算,包括启动内核。

通常需要 Allocation 来保存和传递从一个内核启动到另一个内核启动的中间结果。您可以使用 rsCreateAllocation() 创建它们。该 API 的一种易于使用的形式是 rsCreateAllocation_<T><W>(…),其中 T 是元素的 数据类型,W 是元素的向量宽度。该 API 将 X、Y 和 Z 维度的尺寸作为参数。对于 1D 或 2D allocation,可以省略 Y 或 Z 维度的尺寸。例如,rsCreateAllocation_uchar4(16384) 创建一个包含 16384 个元素的 1D allocation,每个元素的类型都是 uchar4

Allocation 由系统自动管理。您无需显式释放或释放它们。但是,您可以调用 rsClearObject(rs_allocation* alloc) 来指示您不再需要句柄 alloc 到基础 allocation,以便系统可以尽早释放资源。

编写 RenderScript 内核一节中包含一个反转图像的示例内核。下面的示例扩展了该功能,使用单源 RenderScript 对图像应用多种效果。它包括另一个内核 greyscale,该内核将彩色图像转换为黑白图像。然后,可调用函数 process() 将这两个内核连续应用于输入图像,并生成输出图像。输入和输出的 Allocation 作为类型为 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 和 script 之间的转换。一些迭代算法可能会启动内核数百次,使得这种转换的开销相当可观。

脚本全局变量

脚本全局变量是 script (.rs) 文件中一个普通的非 static 全局变量。对于在文件 filename.rs 中定义的名为 var 的脚本全局变量,类 ScriptC_filename 中会反射一个方法 get_var。除非该全局变量是 const,否则还会有一个方法 set_var

给定脚本全局变量有两个独立的值——一个 Java 值和一个 script 值。这些值的行为如下:

  • 如果 var 在 script 中有静态初始化程序,它指定了 var 在 Java 和 script 中的初始值。否则,初始值为零。
  • 在 script 内访问 var 会读写其 script 值。
  • get_var 方法读取 Java 值。
  • set_var 方法(如果存在)立即写入 Java 值,并异步写入 script 值。

注意:这意味着除了 script 中的任何静态初始化程序外,从 script 内部写入全局变量的值对 Java 不可见。

归约内核深入探讨

归约是将数据集组合成单个值的过程。这是并行编程中一个有用的基本操作,其应用包括以下方面:

  • 计算所有数据的总和或乘积
  • 计算所有数据的逻辑运算(andorxor
  • 查找数据中的最小值或最大值
  • 搜索数据中的特定值或特定值的坐标

在 Android 7.0(API 级别 24)及更高版本中,RenderScript 支持归约内核,以便高效实现用户编写的归约算法。您可以在具有 1、2 或 3 个维度的输入上启动归约内核。

上面的示例展示了一个简单的 addint 归约内核。这是一个更复杂的 findMinAndMax 归约内核,它在 1 维 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 内核中,累加器函数将输入 Element 的值添加到累加器数据项中。

示例:findMinAndMax 内核中,累加器函数检查输入 Element 的值是否小于或等于累加器数据项中记录的最小值,和/或是否大于或等于累加器数据项中记录的最大值,并相应地更新累加器数据项。

在对输入中的每个坐标调用累加器函数一次后,RenderScript 必须将累加器数据项组合成一个单一的累加器数据项。您可以编写一个组合器函数来完成此操作。如果累加器函数只有一个输入且没有特殊参数,则无需编写组合器函数;RenderScript 将使用累加器函数来组合累加器数据项。(如果此默认行为不是您想要的,您仍然可以编写组合器函数。)

示例:addint 内核中,没有组合器函数,因此将使用累加器函数。这是正确的行为,因为如果我们将一组值分成两部分,并分别将这两部分的值相加,将这两个总和相加与将整个集合相加是相同的。

示例:findMinAndMax 内核中,组合器函数检查“源”累加器数据项 *val 中记录的最小值是否小于“目标”累加器数据项 *accum 中记录的最小值,并相应地更新 *accum。它对最大值执行类似的工作。这将 *accum 更新到如果所有输入值都累加到 *accum 中,而不是一部分到 *accum,一部分到 *val 的状态。

组合所有累加器数据项后,RenderScript 确定要返回给 Java 的归约结果。您可以编写一个输出转换函数来完成此操作。如果您希望组合后的累加器数据项的最终值即为归约结果,则无需编写输出转换函数。

示例:addint 内核中,没有输出转换函数。组合数据项的最终值是输入的全部 Element 的总和,这是我们想要返回的值。

示例:findMinAndMax 内核中,输出转换函数初始化一个 int2 结果值,用于保存所有累加器数据项组合后得到的最小值和最大值的位置。

请注意,内核有输入类型、累加器数据项类型和结果类型,它们不必都相同。例如,在 findMinAndMax 内核中,输入类型 long、累加器数据项类型 MinAndMax 和结果类型 int2 都不同。

不能假定什么?

#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 会为输入(s)中的每个坐标调用此函数一次,以根据输入(s)更新累加器数据项。该函数必须这样定义

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

    accum 是一个指向累加器数据项的指针,供此函数进行修改。in1inN 是一个或多个参数,它们根据传递给核函数启动的输入自动填充,每个输入对应一个参数。累加器函数可以可选地接受任何特殊参数

    具有多个输入的核函数示例是 dotProduct

  • combiner(combinerName)

    (可选):指定此归约核函数的组合器函数名称。在 RenderScript 为输入(s)中的每个坐标调用累加器函数一次后,它会根据需要多次调用此函数,将所有累加器数据项组合成一个累加器数据项。该函数必须这样定义

    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 调用初始化器、累加器和组合器函数的顺序;它甚至可能并行调用其中一些函数。无法保证两次使用相同输入的同一核函数启动会遵循相同的顺序。唯一的保证是只有初始化器函数会看到未初始化的累加器数据项。例如

  • 无法保证在调用累加器函数之前会初始化所有累加器数据项,尽管只会对已初始化的累加器数据项调用它。
  • 输入 Element 传递给累加器函数的顺序无法保证。
  • 无法保证在调用组合器函数之前已对所有输入 Element 调用了累加器函数。

由此产生的一个结果是,findMinAndMax 内核不是确定性的:如果输入包含多次出现相同的最小值或最大值,您无法知道内核会找到哪个出现。

您必须保证什么?

由于 RenderScript 系统可以选择以多种不同方式执行内核,您必须遵循某些规则以确保您的内核按您期望的方式运行。如果您不遵循这些规则,可能会得到不正确的结果、非确定性行为或运行时错误。

下面的规则经常说两个累加器数据项必须具有"相同的值"。这意味着什么?这取决于您希望内核做什么。对于像 addint 这样的数学归约,"相同"通常意味着数学上的相等。对于像 findMinAndMax("查找最小和最大输入值的位置")这样的"任意选取"搜索,其中可能存在多个相同的输入值,任何给定输入值的所有位置都必须被视为"相同"。您可以编写一个类似的内核来"查找最左边的最小和最大输入值的位置",其中(例如)位置 100 的最小值优先于位置 200 的相同最小值;对于此内核,"相同"将意味着相同的位置,而不仅仅是相同的值,并且累加器和组合器函数必须与 findMinAndMax 的不同。

初始化函数必须创建单位值也就是说,如果 IA 是由初始化函数初始化的累加器数据项,并且 I 从未传递给累加器函数(但 A 可能已经传递),则

示例: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 运行时会检查以确保所有输入 Allocation 具有相同的维度,并且每个输入 Allocation 的 Element 类型与累加器函数原型的相应输入参数的类型匹配。如果其中任何检查失败,RenderScript 会抛出异常。内核在这些维度中的每个坐标上执行。

方法 2 与方法 1 相同,不同之处在于方法 2 接受一个额外的参数 sc,可用于将内核执行限制在坐标的子集上。

方法 3 与方法 1 相同,不同之处在于它接受 Java 数组输入而不是 Allocation 输入。这是一种便利,使您无需编写代码来显式创建 Allocation 并从 Java 数组复制数据到其中。但是,使用方法 3 而非方法 1 并不会提高代码性能。对于每个输入数组,方法 3 创建一个具有适当 Element 类型并启用了 setAutoPadding(boolean) 的临时 1 维 Allocation,并像通过 Allocation 的相应 copyFrom() 方法一样将数组复制到该 Allocation。然后,它调用方法 1,并传递这些临时 Allocation。

注意:如果您的应用将使用相同的数组或具有相同维度和 Element 类型的不同数组进行多次内核调用,您可以通过自己显式创建、填充和重用 Allocation 来提高性能,而不是使用方法 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],则 javaResultTypeintInt2int[]resultType 的所有值都可以由 javaResultType 表示。
  • 如果 resultTypeuintuint2uint[15],则 javaResultTypelongLong2long[]resultType 的所有值都可以由 javaResultType 表示。
  • 如果 resultTypeulongulong2ulong[15],则 javaResultTypelongLong2long[]resultType 的某些值无法由 javaResultType 表示。

javaFutureType 是与输出转换器函数resultType 相对应的未来结果类型。

  • 如果 resultType 不是数组类型,则 javaFutureTyperesult_resultType
  • 如果 resultType 是一个长度为 Count 且成员类型为 memberType 的数组,则 javaFutureTyperesultArrayCount_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,则 devecSiInXTypeint
  • 如果 inXTypeint2,则 devecSiInXTypeint。数组是一个扁平化表示:它的标量元素数量是 Allocation 中 2 分量向量元素数量的两倍。这与 AllocationcopyFrom() 方法的工作方式相同。
  • 如果 inXTypeuint,则 deviceSiInXTypeint。Java 数组中的有符号值在 Allocation 中被解释为具有相同位模式的无符号值。这与 AllocationcopyFrom() 方法的工作方式相同。
  • 如果 inXTypeuint2,则 deviceSiInXTypeint。这是 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 的用法。