RenderScript 概览

RenderScript 是用于在 Android 上以高性能运行计算密集型任务的框架。RenderScript 主要用于数据并行计算,不过串行工作负载也可以从中受益。RenderScript 运行时可在设备上提供的多个处理器(如多核 CPU 和 GPU)间并行调度工作。这样您就能够专注于表达算法而不是调度工作。RenderScript 对于专注于图像处理、计算摄影或计算机视觉的应用来说尤其有用。

为了开始使用 RenderScript,您应了解以下两个主要概念:

  • 语言本身是用于编写高性能计算代码的 C99 衍生语言。编写 RenderScript 内核介绍了如何使用它来编写计算内核。
  • Control 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 映射内核,而不是可调用函数。in 参数根据传递到内核启动的输入 Allocation 自动填充。下面的部分详细介绍了参数 xy。从内核返回的值会自动写入输出 Allocation 中的相应位置。默认情况下,此内核在其整个输入 Allocation 上运行,对 Allocation 中的每个 Element 执行内核函数一次。

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

      注意:在 Android 6.0(API 级别 23)之前,映射内核最多只能有一个输入 Allocation

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

      注意RS_KERNEL 是由 RenderScript 自动定义的宏,目的在于方便您使用:

      #define RS_KERNEL __attribute__((kernel))
      

    归约内核是一系列函数,可对维度相同的一组输入 Allocations 执行操作。默认情况下,其 accumulator 函数会对这些维度中的每个坐标执行一次。归约内核通常(但并非专门)用于将一组输入 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 函数;可能还需要其他函数,具体取决于您希望内核执行的操作。

      归约内核的 accumulator 函数必须返回 void,并且必须具有至少两个参数。第一个参数(在此示例中为 accum)是一个指向 accumulator 数据项的指针,第二个参数(在此示例中为 val)根据传递到内核启动的输入 Allocation 自动填充。accumulator 数据项由 RenderScript 运行时创建,默认初始化为零。默认情况下,此内核在其整个输入 Allocation 上运行,对 Allocation 中的每个 Element 执行 accumulator 函数一次。默认情况下,accumulator 数据项的最终值被视为归约的结果,并返回给 Java。RenderScript 运行时会检查以确保 Element 类型的输入 Allocation 与 accumulator 函数的原型相匹配;如果不匹配,RenderScript 会抛出异常。

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

      如需详细了解归约内核,请点击此处

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

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

    映射内核函数或归约内核的 accumulator 函数也可以使用 rs_kernel_context 类型的可选特殊参数 context。一系列运行时 API 需要该参数来查询当前执行的特定属性,例如 rsGetDimX。(Android 6.0(API 级别 23)及更高版本支持 context 参数。)

  • 可选的 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 访问其 API:

其利弊如下所示:

  • 如果您使用支持库 API,无论您使用哪些 RenderScript 功能,应用的 RenderScript 部分都会与搭载 Android 2.3(API 级别 9)及更高版本的设备兼容。与使用原生 (android.renderscript) API 相比,它可让您的应用在更多设备上运行。
  • 某些 RenderScript 功能无法通过支持库 API 使用。
  • 如果您使用支持库 API,您开发的 APK 会比使用原生 (android.renderscript) API 开发的 APK 更大(可能会大幅增大)。

使用 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 设置:
    • 打开您的应用模块的应用文件夹中的 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 版本设置为不同的值,该值会被忽略,并且 build 文件中的目标值将用于设置最低 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 中,并且 RenderScript 上下文已经位于 mRenderScript 中,那么用于实例化脚本的 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 中的“copy”方法之一。这些“copy”方法是同步的。
  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 中的“copy”方法之一,将数据复制回 Java。为了获取归约内核的结果,您必须使用 javaFutureType.get() 方法。“copy”和 get() 方法是同步的。
  8. 拆解 RenderScript 上下文。您可以使用 destroy() 或通过对 RenderScript 上下文对象进行垃圾回收来将其销毁。这样一来,如果进一步使用属于该上下文的任何对象,会抛出异常。

异步执行模式

反映的 forEachinvokereduceset 方法是异步的,每种方法都可能在完成所请求的操作之前返回到 Java。不过,各项操作都会按其启动顺序进行序列化。

Allocation 类提供“copy”方法,用于将数据复制到 Allocation,或者从 Allocation 复制数据。“copy”方法是同步的,并会根据上述涉及同一 Allocation 的任何异步操作进行序列化。

反映的 javaFutureType 类提供 get() 方法来获取归约结果。get() 是同步的,并会根据归约进行序列化(此操作是异步的)。

单源 RenderScript

Android 7.0(API 级别 24)引入了一项名为单源 RenderScript 的新编程功能,其中,内核通过定义它们的脚本启动,而不是通过 Java 启动。此方法目前仅限映射内核(为方便起见,在本部分中简称为“内核”)使用。这项新功能还支持在脚本内创建 rs_allocation 类型的 Allocation。现在,即使需要多次启动内核,也可以仅在脚本中实现整个算法。这可谓一箭双雕:代码更易读,因为它使用一种语言来实现算法;并且编码速度可能更快,因为在多次启动内核时,Java 和 RenderScript 之间的转换更少。

在单源 RenderScript 中,您可以如编写 RenderScript 内核所述编写内核。然后,编写一个调用 rsForEach() 的可调用函数来启动内核。该 API 会将内核函数作为第一个参数,后跟输入和输出 Allocation。类似的 API rsForEachWithOptions() 使用 rs_script_call_t 类型的 extra 参数,该参数指定输入和输出 Allocation 中 Element 的子集,以供内核函数进行处理。

如需开始 RenderScript 计算,您应该从 Java 调用可调用函数。请按照通过 Java 代码使用 RenderScript 中的步骤操作。在启动适当的内核步骤中,使用 invoke_function_name() 调用可调用函数,这会开始包括启动内核在内的整个计算。

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

Allocation 由系统自动管理。您无需明确释放它们。不过,您可以调用 rsClearObject(rs_allocation* alloc) 来表明您不再需要底层 Allocation 的句柄 alloc,这样系统就可以尽早释放资源。

编写 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 和脚本之间进行转换的问题。一些迭代算法可能会启动内核数百次,从而产生相当高的转换开销。

脚本全局变量

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

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

  • 如果 var 在脚本中有静态 initializer,则它会同时在 Java 和脚本中指定 var 的初始值。否则,该初始值将为零。
  • 访问脚本中的 var 会读取和写入其脚本值。
  • get_var 方法会读取 Java 值。
  • set_var 方法(如果存在)会立即写入 Java 值,并异步写入脚本值。

注意:这意味着,除了脚本中的任何静态 initializer 之外,从脚本内写入全局变量的值对于 Java 都是不可见的。

归约内核详细说明

归约是将一组数据合并为一个值的过程。这是并行编程中的实用基元,具体应用如下:

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

在 Android 7.0(API 级别 24)及更高版本中,RenderScript 支持归约内核,以允许用户编写的高效归约算法。您可以在具有一维、二维或三维的输入上启动归约内核。

上面的示例展示了一个简单的 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 运行时会创建一个或多个名为 accumulator 数据项的变量来保留归约过程的状态。RenderScript 运行时以尽可能提升性能的方式选择 accumulator 数据项的数量。accumulator 数据项的类型 (accumType) 由内核的 accumulator 函数确定 - 该函数的第一个参数是指向 accumulator 数据项的指针。默认情况下,每个 accumulator 数据项都初始化为零(就像通过 memset 一样);不过,您可以编写一个 initializer 函数来执行其他操作。

示例:在 addint 内核中,int 类型的 accumulator 数据项用于累加输入值。由于不存在 initializer 函数,因此每个 accumulator 数据项都初始化为零。

示例:在 findMinAndMax 内核中,MinAndMax 类型的 accumulator 数据项用于跟踪目前找到的最小值和最大值。有一个 initializer 函数可将这些值分别设置为 LONG_MAXLONG_MIN;若将这些值的位置设置为 -1,则表示这些值实际上不在已处理的输入的(空)部分中。

RenderScript 会对输入中的每个坐标调用 accumulator 函数一次。通常,您的函数应根据输入以某种方式更新 accumulator 数据项。

示例:在 addint 内核中,accumulator 函数将输入 Element 的值与 accumulator 数据项相加。

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

对输入中的每个坐标调用 accumulator 函数一次后,RenderScript 必须将 accumulator 数据项合并为一个 accumulator 数据项。您可以编写 combiner 函数来完成此操作。如果 accumulator 函数只有一个输入且没有特殊参数,则无需编写 combiner 函数;RenderScript 会使用 accumulator 函数合并 accumulator 数据项。(如果此默认行为无法满足您的需要,您仍然可以编写 combiner 函数。)

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

示例:在 findMinAndMax 内核中,combiner 函数会检查“源”accumulator 数据项中记录的最小值 *val 是否小于“目标”accumulator 数据项中记录的最小值 *accum,并相应地更新 *accum。对最大值也会执行相似的操作。如果所有输入值都已累加到 *accum(而不是一些输入值累加到 *accum,另外一些输入值累加到 *val),则会将 *accum 更新为其原本该有的状态。

合并所有 accumulator 数据项之后,RenderScript 会确定返回到 Java 的归约结果。您可以编写一个 outconverter 函数来完成此操作。如果您希望合并后的 accumulator 数据项的最终值是归约的结果,无需编写 outconverter 函数。

示例:在 addint 内核中,没有 outconverter 函数。合并后的数据项的最终值是输入的所有 Element 的总和,也就是我们想要返回的值。

示例:在 findMinAndMax 内核中,outconverter 函数初始化 int2 结果值,以保留所有 accumulator 数据项合并所产生的最小值和最大值的位置。

编写归约内核

#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)(可选):指定此归约内核的 initializer 函数的名称。当您启动内核时,RenderScript 会对每个 accumulator 数据项调用此函数一次。该函数必须定义如下:

    static void initializerName(accumType *accum) { … }

    accum 是指向此函数要初始化的 accumulator 数据项的指针。

    如果您未提供 initializer 函数,那么 RenderScript 会将每个 accumulator 数据项初始化为零(就像通过 memset 一样),就好像有如下所示的 initializer 函数一样:

    static void initializerName(accumType *accum) {
      memset(accum, 0, sizeof(*accum));
    }
  • accumulator(accumulatorName)(必需):指定此归约内核的 accumulator 函数的名称。当您启动内核时,RenderScript 会对输入中的每个坐标调用此函数一次,以根据输入以某种方式更新 accumulator 数据项。该函数必须定义如下:

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

    accum 是指向此函数要修改的 accumulator 数据项的指针。in1inN 是一个或多个参数,会根据传递到内核启动的输入自动填充,每个输入一个参数。accumulator 函数可以选择性使用任何特殊参数

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

  • combiner(combinerName)

    (可选):指定此归约内核的 combiner 函数的名称。对输入中的每个坐标调用 accumulator 函数一次后,RenderScript 会根据需要多次调用此函数,以将所有 accumulator 数据项合并为一个 accumulator 数据项。该函数必须定义如下:

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

    accum 是指向此函数要修改的“目标”accumulator 数据项的指针。other 是指向此函数要“合并”为 *accum 的“源”accumulator 数据项的指针。

    注意*accum 和/或 *other 已初始化,但未传递到 accumulator 函数;也就是说,其中之一或两者都未曾根据任何输入数据进行更新。例如,在 findMinAndMax 内核中,combiner 函数 fMMCombiner 会明确检查是否是 idx < 0;如果是,则表示这样的 accumulator 数据项,其值为 INITVAL

    如果您未提供 combiner 函数,那么 RenderScript 会在 combiner 函数的位置使用 accumulator 函数,就好像有如下所示的 combiner 函数一样:

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

    如果内核具有多个输入,输入数据类型与 accumulator 数据类型不同,或 accumulator 函数采用一个或多个特殊参数,则必须使用 combiner 函数。

  • outconverter(outconverterName)(可选):指定此归约内核的 outconverter 函数的名称。合并所有 accumulator 数据项后,RenderScript 会调用此函数以确定返回到 Java 的归约结果。该函数必须定义如下:

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

    result 是一个使用归约结果指向此函数要初始化的结果数据项(RenderScript 运行时已分配但未初始化)的指针。resultType 是该数据项的类型,不必与 accumType 相同。accum 是指向 combiner 函数计算的最终 accumulator 数据项的指针。

    如果您未提供 outconverter 函数,那么 RenderScript 会将最终的 accumulator 数据项复制到结果数据项,就好像有如下所示的 outconverter 函数一样:

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

    如果您希望使用与 accumulator 数据类型不同的结果类型,那么必须使用 outconverter 函数。

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

您不能保证什么?

您不能依赖于 RenderScript 为给定内核启动所创建的 accumulator 数据项的数量。无法保证两个使用相同输入的同一内核的两次启动会创建相同数量的 accumulator 数据项。

您不得依赖于 RenderScript 调用 initializer、accumulator 和 combiner 函数的顺序;它甚至可能并行调用其中的一些函数。无法保证使用相同输入的同一内核的两次启动会按照相同的顺序启动。只能保证只有 initializer 函数能看到未初始化的 accumulator 数据项。例如:

  • 尽管只会对初始化后的 accumulator 数据项调用函数,也无法保证所有 accumulator 数据项都会在调用 accumulator 函数之前进行初始化。
  • 无法保证传递给 accumulator 函数的输入 Element 的顺序。
  • 无法保证在调用 combiner 函数之前,已为所有输入 Element 调用了 accumulator 函数。

这样做的一个后果就是 findMinAndMax 内核具有不确定性:如果输入包含多次相同的最小值或最大值,则无法得知内核会找到哪一次。

您必须保证什么?

由于 RenderScript 系统可以选择以多种不同的方式执行内核,因此您必须遵守一定的规则以确保内核以您需要的方式执行。如果您不遵守这些规则,则可能会收到不正确的结果、不确定的行为或运行时错误。

以下规则通常要求两个 accumulator 数据项必须具有“相同值”。这意味着什么?具体取决于您希望内核执行的操作。对于诸如 addint 之类的数学归约,“相同”通常意味着数学上的相等。对于诸如 findMinAndMax(“查找最小和最大输入值的位置”)之类的“任选”搜索,其中可能存在多次相同输入值,则给定输入值的所有位置必须视为“相同”。您可以编写类似的内核,以“查找最左侧最小和最大输入值的位置”,其中,(举例而言)首选位置 100 处的最小值,而不是位置 200 处的相同最小值;对于此内核,“相同”表示相同的位置,而不仅仅是相同的值,并且 accumulator 和 combiner 函数必须与 findMinAndMax 的函数不同。

initializer 函数必须创建标识值也就是说,如果 IA 是 initializer 函数初始化的 accumulator 数据项,并且从未将 I 传递给 accumulator 函数(但可能已将 A 传递给 accumulator 函数),那么
  • combinerName(&A, &I) 必须使 A 保持不变
  • combinerName(&I, &A) 必须使 IA 相同

示例:在 addint 内核中,accumulator 数据项初始化为零。此内核的 combiner 函数会执行加法;零是加法的标识值。

示例:在 findMinAndMax 内核中,accumulator 数据项初始化为 INITVAL

  • fMMCombiner(&A, &I) 使 A 的值保持不变,因为 IINITVAL
  • fMMCombiner(&I, &A)I 设置为 A,因为 IINITVAL

因此,INITVAL 实际上是一个标识值。

combiner 函数必须遵守交换律也就是说,如果 AB 是 initializer 函数初始化的 accumulator 数据项,并且可能未传递到 accumulator 函数,或者已经传递到 accumulator 函数多次,那么 combinerName(&A, &B) 必须将 A 设置为 combinerName(&B, &A) 设置 B相同值

示例:在 addint 内核中,combiner 函数添加两个 accumulator 数据项值;加法遵守交换律。

示例:在 findMinAndMax 内核中,fMMCombiner(&A, &B) 等同于 A = minmax(A, B),且 minmax 遵守交换律,因此 fMMCombiner 也遵守交换律。

combiner 函数必须遵守结合律也就是说,如果 ABC 是 initializer 函数初始化的 accumulator 数据项,并且可能未传递到 accumulator 函数,或者已经传递到 accumulator 函数多次,那么以下两个代码序列必须将 A 设置为相同的值

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

示例:在 addint 内核中,combiner 函数添加两个 accumulator 数据项值:

  • 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
    

加法遵守结合律,因此,combiner 函数也遵守结合律。

示例: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 也遵守结合律。

accumulator 函数和 combiner 函数必须遵守基本折叠规则也就是说,如果 AB 是 accumulator 数据项,A 已由 initializer 函数初始化,并且可能未传递到 accumulator 函数,或者已经传递到 accumulator 函数多次,而 B 未初始化,且 args 是对 accumulator 函数特定调用的输入参数和特殊参数的列表,那么以下两个代码序列必须将 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 使内核 accumulator 函数中的每个输入参数都有一个输入 Allocation 参数。RenderScript 运行时会检查以确保所有输入 Allocation 都具有相同的维度,并且每个输入 Allocation 的 Element 类型与 accumulator 函数原型的相应输入参数相匹配。如果这些检查失败,RenderScript 会抛出异常。内核会对这些维度中的每个坐标执行。

方法 2 与方法 1 相同,只是方法 2 使用附加参数 sc,该参数可用于将内核执行限制在坐标的一个子集内。

方法 3 与方法 1 相同,只是不使用 Allocation 输入,而是使用 Java 数组输入。这样一来,您无需编写代码来明确创建 Allocation 并从 Java 数组将数据复制给它。不过,使用方法 3 代替方法 1 并不会提高代码的性能。对于每个输入数组,方法 3 创建一个具有适当 Element 类型并启用 setAutoPadding(boolean) 的临时一维 Allocation,并将该数组复制到 Allocation,就像通过 Allocation 的适当 copyFrom() 方法一样。然后,调用方法 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 是由 outconverter 函数的 resultType 决定的。除非 resultType 是无符号类型(标量、矢量或数组),否则 javaResultType 是直接对应的 Java 类型。如果 resultType 是无符号类型且有更大的 Java 有符号类型,则 javaResultType 是更大的有符号类型 Java;否则,它是直接对应的 Java 类型。例如:

  • 如果 resultType 是 intint2int[15],那么 javaResultType 是 intInt2int[]resultType 的所有值均可由 javaResultType 表示。
  • 如果 resultType 是 uintuint2uint[15],那么 javaResultType 是 longLong2long[]resultType 的所有值均可由 javaResultType 表示。
  • 如果 resultType 是 ulongulong2ulong[15],那么 javaResultType 是 longLong2long[]resultType 的某些值无法由 javaResultType 表示。

javaFutureType是与 outconverter 函数的 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 是与 accumulator 函数对应参数的 inXType 相对应的 Java 类型。除非 inXType 是无符号类型或矢量类型,否则 devecSiInXType 是直接对应的 Java 类型。如果 inXType 是无符号标量类型,那么 devecSiInXType 是与相同大小的有符号标量类型直接对应的 Java 类型。如果 inXType 是有符号矢量类型,那么 devecSiInXType 是与矢量组件类型直接对应的 Java 类型。如果 inXType 是无符号矢量类型,那么 devecSiInXType 是与矢量组件类型大小相同的有符号标量类型直接对应的 Java 类型。例如:

  • 如果 inXType 是 int,那么 devecSiInXType 是 int
  • 如果 inXType 是 int2,那么 devecSiInXType 是 int该数组采用扁平化表示法:其标量 Element 的数量是 Allocation 的双组件矢量 Element 数量的两倍。这与 AllocationcopyFrom() 方法的工作原理相同。
  • 如果 inXType 是 uint,那么 deviceSiInXType 是 intJava 数组中的有符号值被解读为 Allocation 中相同位模式的无符号值。这与 AllocationcopyFrom() 方法的工作原理相同。
  • 如果 inXType 是 uint2,那么 deviceSiInXType 是 int这是 int2uint 的处理方式的组合:数组是扁平化表示,Java 数组有符号值被解读为 RenderScript 无符号 Element 值。

请注意,对于方法 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 的用法。