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 文件称为脚本。每个脚本都包含自己的一组内核、函数和变量。脚本可以包含

  • 一个编译指示声明(#pragma version(1)),它声明此脚本中使用的 RenderScript 内核语言的版本。目前,1 是唯一有效的值。
  • 一个编译指示声明(#pragma rs java_package_name(com.example.app)),它声明从此脚本反射的 Java 类的包名称。请注意,您的 .rs 文件必须是应用程序包的一部分,而不是库项目的一部分。
  • 零个或多个可调用函数。可调用函数是您可以从 Java 代码中使用任意参数调用的单线程 RenderScript 函数。这些通常用于初始设置或较大处理管道中的串行计算。

  • 零个或多个脚本全局变量。脚本全局变量类似于 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()函数是一种特殊的可调用函数,RenderScript 在脚本首次实例化时运行它。这允许在脚本创建时自动执行一些计算。
  • 零个或多个静态脚本全局变量和函数。静态脚本全局变量等效于脚本全局变量,只是它无法从 Java 代码访问。静态函数是标准 C 函数,可以从脚本中的任何内核或可调用函数中调用,但不会公开给 Java API。如果脚本全局变量或函数不需要从 Java 代码访问,则强烈建议将其声明为static

设置浮点精度

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

  • #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 部分将与运行 Android 2.3(API 级别 9)及更高版本的设备兼容,无论您使用哪些 RenderScript 功能。这使您的应用程序可以在比使用原生(android.renderscript)API 更多的设备上运行。
  • 某些 RenderScript 功能无法通过支持库 API 使用。
  • 如果使用支持库 API,您将获得比使用原生(android.renderscript)API 大(可能大得多)的 APK。

使用 RenderScript 支持库 API

为了使用支持库 RenderScript API,您必须配置开发环境以能够访问它们。使用这些 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 设置。
    • 打开应用程序模块的 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 的应用程序类中,添加对 Support Library 类的导入。

    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 以传递给脚本。 Allocation 是一个 RenderScript 对象,它为固定数量的数据提供存储空间。脚本中的内核将 Allocation 对象作为其输入和输出,并且当绑定为脚本全局变量时,可以使用 rsGetElementAt_type()rsSetElementAt_type() 在内核中访问 Allocation 对象。 Allocation 对象允许在 Java 代码和 RenderScript 代码之间传递数组。 Allocation 对象通常使用 createTyped()createFromBitmap() 创建。
  3. 创建任何必要的脚本。 使用 RenderScript 时,有两种类型的脚本可用。
    • ScriptC:这些是如上文 编写 RenderScript 内核 中所述的用户定义脚本。每个脚本都有一个 Java 类,该类由 RenderScript 编译器反映,以便于从 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 中的“复制”方法之一。“复制”方法是 同步的
  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 的新编程功能,其中内核从其定义的脚本中启动,而不是从 Java 中启动。此方法目前仅限于映射内核,在本节中为简洁起见,将其简称为“内核”。此新功能还支持从脚本内部创建类型为 rs_allocation 的分配。现在可以完全在脚本中实现整个算法,即使需要多次启动内核也是如此。这样做的好处有两个:更易读的代码,因为它将算法的实现保留在一种语言中;以及潜在的更快代码,因为在多次启动内核时,Java 和 RenderScript 之间的转换次数更少。

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

要启动 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) 来指示您不再需要指向基础 Allocation 的句柄 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 在脚本中具有静态初始化器,则它指定 var 在 Java 和脚本中的初始值。否则,该初始值为零。
  • 对脚本中 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 调用初始化器、累加器和组合器函数的顺序;它甚至可能并行调用其中一些函数。无法保证两次使用相同输入启动同一个内核会遵循相同的顺序。唯一保证的是,只有初始化器函数才能看到未初始化的累加器数据项。例如

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

其结果之一是 findMinAndMax 内核不是确定性的:如果输入包含多个相同最小值或最大值的出现,则您无法知道内核将找到哪个出现。

您必须保证什么?

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

以下规则通常表示两个累加器数据项必须具有“相同的值”。这是什么意思?这取决于您希望内核执行的操作。对于诸如 addint 之类的数学归约,通常“相同”表示数学上的相等是有意义的。对于诸如 findMinAndMax(“查找最小和最大输入值的位置”)之类的“选择任意”搜索,其中可能存在多个相同输入值的出现,则必须将给定输入值的所有位置视为“相同”。您可以编写一个类似的内核来“查找最左边的最小和最大输入值的位置”,其中(例如)位置 100 处的最小值优于位置 200 处的相同最小值;对于此内核,“相同”将意味着相同位置,而不仅仅是相同,累加器和组合器函数必须与 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 运行时会检查以确保所有输入 Allocation 具有相同的维度,并且每个输入 Allocation 的Element类型与累加器函数原型中相应输入参数的类型匹配。如果任何一项检查失败,RenderScript 将抛出异常。内核将在这些维度中的每个坐标上执行。

方法 2 与方法 1 相同,只是方法 2 额外接受一个参数sc,该参数可用于将内核执行限制到坐标的子集。

方法 3 与方法 1 相同,只是它不使用 Allocation 输入,而是使用 Java 数组输入。这是一种便利,可以避免您编写代码来显式创建 Allocation 并将数据从 Java 数组复制到其中。但是,使用方法 3 而不是方法 1 不会提高代码的性能。对于每个输入数组,方法 3 会创建一个具有适当Element类型和启用setAutoPadding(boolean)的临时一维 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() {}
  }
}

javaResultTypeoutconverter 函数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 是与outconverter 函数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 无符号 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 的用法。