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 函数。这些函数通常用于初始设置或较大处理管道中的串行计算。
零个或多个**脚本全局变量**(script globals)。脚本全局变量类似于 C 语言中的全局变量。您可以从 Java 代码访问脚本全局变量,它们通常用于向 RenderScript 内核传递参数。脚本全局变量将在此处详细解释。
零个或多个**计算内核**(compute kernels)。计算内核是一个函数或函数集合,您可以指示 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
自动填充。参数x
和y
将在下面讨论。内核返回的值会自动写入输出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)及更高版本中受支持。
映射内核函数或归约内核累加器函数可以使用特殊参数
x
、y
和z
访问当前执行的坐标,这些参数必须是int
或uint32_t
类型。这些参数是可选的。映射内核函数或归约内核累加器函数还可以使用类型为rs_kernel_context的可选特殊参数
context
。它由一组运行时 API 使用,这些 API 用于查询当前执行的某些属性,例如rsGetDimX。(context
参数在 Android 6.0(API 级别 23)及更高版本中可用。)- 一个可选的
init()
函数。init()
函数是一种特殊的可调用函数,RenderScript 在脚本首次实例化时运行它。这允许在脚本创建时自动执行一些计算。 - 零个或多个**静态脚本全局变量和函数**(static script globals and functions)。静态脚本全局变量等效于脚本全局变量,但不能从 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
- **
android.renderscript
** - 此类包中的 API 在运行 Android 3.0(API 级别 11)及更高版本的设备上可用。 - **
android.support.v8.renderscript
** - 此包中的 API 通过支持库提供,允许您在运行 Android 2.3(API 级别 9)及更高版本的设备上使用它们。
以下是权衡:
- 如果使用支持库 API,则应用程序的 RenderScript 部分将与运行 Android 2.3(API 级别 9)及更高版本的设备兼容,无论您使用哪些 RenderScript 功能。这允许您的应用程序在比使用原生(**
android.renderscript
**)API 时更多的设备上运行。 - 某些 RenderScript 功能不支持库 API。
- 如果使用支持库 API,与使用原生(**
android.renderscript
**)API 相比,您将获得(可能显著)更大的 APK。
使用 RenderScript 支持库 API
为了使用支持库 RenderScript API,您必须配置开发环境以能够访问它们。以下 Android SDK 工具是使用这些 API 所必需的:
- 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 管理器中检查和更新已安装的这些工具的版本。
要使用支持库 RenderScript API:
- 确保已安装所需的 Android SDK 版本。
- 更新 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
- 指定如果设备不支持目标版本,则生成的字节码应回退到兼容版本。
- 打开应用程序模块的 app 文件夹中的
- 在使用 RenderScript 的应用程序类中,添加对 Support Library 类的导入
Kotlin
import android.support.v8.renderscript.*
Java
import android.support.v8.renderscript.*;
从 Java 或 Kotlin 代码中使用 RenderScript
从 Java 或 Kotlin 代码中使用 RenderScript 依赖于位于 android.renderscript
或 android.support.v8.renderscript
包中的 API 类。大多数应用程序都遵循相同的基本使用模式
- 初始化 RenderScript 上下文。 使用
create(Context)
创建的RenderScript
上下文可确保可以使用 RenderScript,并提供一个对象来控制所有后续 RenderScript 对象的生命周期。您应该将上下文创建视为一项可能需要长时间运行的操作,因为它可能会在不同的硬件部件上创建资源;如果可能,它不应位于应用程序的关键路径中。通常,应用程序一次只会有一个 RenderScript 上下文。 - 创建至少一个
Allocation
以传递给脚本。Allocation
是一个 RenderScript 对象,它为固定数量的数据提供存储空间。脚本中的内核将Allocation
对象作为其输入和输出,并且可以在内核中使用rsGetElementAt_type()
和rsSetElementAt_type()
访问Allocation
对象(当绑定为脚本全局变量时)。Allocation
对象允许在 Java 代码和 RenderScript 代码之间传递数组。Allocation
对象通常使用createTyped()
或createFromBitmap()
创建。 - 创建任何必要的脚本。 使用 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
的子类。
- ScriptC:这些是如上文 编写 RenderScript 内核 中所述的用户定义脚本。每个脚本都有一个 Java 类,该类由 RenderScript 编译器反映,以便于从 Java 代码访问该脚本;此类的名称为
- 使用数据填充 Allocation。 除了使用
createFromBitmap()
创建的 Allocation 之外,Allocation 在首次创建时会填充空数据。要填充 Allocation,请使用Allocation
中的“复制”方法之一。“复制”方法是 同步的。 - 设置任何必要的 脚本全局变量。 您可以使用相同
ScriptC_filename
类中名为set_globalname
的方法设置全局变量。例如,为了设置名为threshold
的int
变量,请使用 Java 方法set_threshold(int)
;为了设置名为lookup
的rs_allocation
变量,请使用 Java 方法set_lookup(Allocation)
。“set”方法是 异步的。 - 启动相应的内核和可调用函数。
启动给定内核的方法反映在相同的
ScriptC_filename
类中,方法名为forEach_mappingKernelName()
或reduce_reductionKernelName()
。这些启动是 异步的。根据内核的参数,该方法接受一个或多个 Allocation,所有 Allocation 必须具有相同的维度。默认情况下,内核会在这些维度中的每个坐标上执行;要在这些维度的一部分坐标上执行内核,请将适当的Script.LaunchOptions
作为最后一个参数传递给forEach
或reduce
方法。使用在相同
ScriptC_filename
类中反映的invoke_functionName
方法启动可调用函数。这些启动是 异步的。 - 从
Allocation
对象和javaFutureType 对象中检索数据。 为了从 Java 代码访问Allocation
中的数据,您必须使用Allocation
中的“复制”方法之一将该数据复制回 Java。为了获取归约内核的结果,您必须使用javaFutureType.get()
方法。“复制”和get()
方法是 同步的。 - 拆除 RenderScript 上下文。 您可以使用
destroy()
或允许 RenderScript 上下文对象被垃圾回收来销毁 RenderScript 上下文。这会导致对属于该上下文的任何对象的任何进一步使用都抛出异常。
异步执行模型
反射的 forEach
、invoke
、reduce
和 set
方法是异步的——每个方法都可能在完成请求的操作之前返回到 Java。但是,各个操作会按照启动顺序进行序列化。
Allocation
类提供“复制”方法来复制到 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()
调用可调用函数,这将启动整个计算,包括启动内核。
分配通常需要保存并将中间结果从一个内核启动传递到另一个内核启动。您可以使用 rsCreateAllocation() 创建它们。该 API 的一种易于使用的形式是 rsCreateAllocation_<T><W>(…)
,其中T是元素的数据类型,W是元素的向量宽度。该 API 将维度 X、Y 和 Z 中的大小作为参数。对于 1D 或 2D 分配,可以省略维度 Y 或 Z 的大小。例如,rsCreateAllocation_uchar4(16384)
创建一个包含 16384 个元素的 1D 分配,每个元素的类型为 uchar4
。
分配由系统自动管理。您不必显式释放或释放它们。但是,您可以调用 rsClearObject(rs_allocation* alloc)
来指示您不再需要指向基础分配的句柄 alloc
,以便系统尽早释放资源。
编写 RenderScript 内核 部分包含一个反转图像的示例内核。下面的示例将此扩展到使用单源 RenderScript 对图像应用多个效果。它包含另一个内核 greyscale
,它将彩色图像转换为黑白图像。然后,可调用函数 process()
将这两个内核连续应用于输入图像,并生成输出图像。输入和输出的分配都作为类型为 rs_allocation
的参数传入。
// File: singlesource.rs #pragma version(1) #pragma rs java_package_name(com.android.rssample) static const float4 weight = {0.299f, 0.587f, 0.114f, 0.0f}; uchar4 RS_KERNEL invert(uchar4 in, uint32_t x, uint32_t y) { uchar4 out = in; out.r = 255 - in.r; out.g = 255 - in.g; out.b = 255 - in.b; return out; } uchar4 RS_KERNEL greyscale(uchar4 in) { const float4 inF = rsUnpackColor8888(in); const float4 outF = (float4){ dot(inF, weight) }; return rsPackColorTo8888(outF); } void process(rs_allocation inputImage, rs_allocation outputImage) { const uint32_t imageWidth = rsAllocationGetDimX(inputImage); const uint32_t imageHeight = rsAllocationGetDimY(inputImage); rs_allocation tmp = rsCreateAllocation_uchar4(imageWidth, imageHeight); rsForEach(invert, inputImage, tmp); rsForEach(greyscale, tmp, outputImage); }
您可以按如下方式从 Java 或 Kotlin 调用 process()
函数
Kotlin
val RS: RenderScript = RenderScript.create(context) val script = ScriptC_singlesource(RS) val inputAllocation: Allocation = Allocation.createFromBitmapResource( RS, resources, R.drawable.image ) val outputAllocation: Allocation = Allocation.createTyped( RS, inputAllocation.type, Allocation.USAGE_SCRIPT or Allocation.USAGE_IO_OUTPUT ) script.invoke_process(inputAllocation, outputAllocation)
Java
// File SingleSource.java RenderScript RS = RenderScript.create(context); ScriptC_singlesource script = new ScriptC_singlesource(RS); Allocation inputAllocation = Allocation.createFromBitmapResource( RS, getResources(), R.drawable.image); Allocation outputAllocation = Allocation.createTyped( RS, inputAllocation.getType(), Allocation.USAGE_SCRIPT | Allocation.USAGE_IO_OUTPUT); script.invoke_process(inputAllocation, outputAllocation);
此示例展示了一个包含两个内核启动的算法如何在 RenderScript 语言本身中完全实现。如果没有单源 RenderScript,您将不得不从 Java 代码中启动这两个内核,从而将内核启动与内核定义分开,这使得难以理解整个算法。单源 RenderScript 代码不仅更易于阅读,而且还消除了跨内核启动在 Java 和脚本之间进行转换的过程。某些迭代算法可能会启动数百次内核,这使得这种转换的开销相当可观。
脚本全局变量
脚本全局变量是脚本(.rs
)文件中一个普通的非static
全局变量。对于在文件filename.rs
中定义的名为var的脚本全局变量,将在类ScriptC_filename
中反映一个方法get_var
。除非全局变量为const
,否则还将存在一个方法set_var
。
给定的脚本全局变量有两个独立的值——一个Java值和一个脚本值。这些值的行为如下
- 如果var在脚本中具有静态初始化器,则它指定var在 Java 和脚本中的初始值。否则,该初始值为零。
- 脚本中对var的访问将读取和写入其脚本值。
- 方法
get_var
读取 Java 值。 - 方法
set_var
(如果存在)会立即写入 Java 值,并异步写入脚本值。
注意:这意味着,除了脚本中的任何静态初始化器之外,从脚本内部写入全局变量的值对 Java 都是不可见的。
深入了解归约内核
归约是将数据集合组合成单个值的进程。这是并行编程中一个有用的基元,具有以下应用
- 计算所有数据的总和或乘积
- 计算所有数据的逻辑运算(
and
、or
、xor
) - 查找数据中的最小值或最大值
- 搜索特定值或数据中特定值的坐标
在 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_MAX
和LONG_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
是指向此函数要修改的累加器数据项的指针。in1
到inN
是一个或多个参数,这些参数会根据传递给内核启动的输入自动填充,每个输入一个参数。累加器函数可以选择使用任何特殊参数。具有多个输入的示例内核为
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)
(可选):指定此归约内核的 outconverter 函数的名称。RenderScript 组合所有累加器数据项后,将调用此函数来确定要返回到 Java 的归约结果。该函数必须像这样定义static void outconverterName(resultType *result, const accumType *accum) { … }
result
是指向结果数据项的指针(由 RenderScript 运行时分配但未初始化),此函数将使用归约结果对其进行初始化。resultType 是该数据项的类型,它不必与 accumType 相同。accum
是指向由combiner 函数计算的最终累加器数据项的指针。如果您不提供 outconverter 函数,RenderScript 会将最终累加器数据项复制到结果数据项,其行为就像存在一个类似于此的 outconverter 函数一样
static void outconverterName(accumType *result, const accumType *accum) { *result = *accum; }
如果您希望结果类型与累加器数据类型不同,则必须使用 outconverter 函数。
请注意,内核具有输入类型、累加器数据项类型和结果类型,这些类型都不必相同。例如,在findMinAndMax 内核中,输入类型long
、累加器数据项类型MinAndMax
和结果类型int2
都不同。
您不能假设什么?
您不能依赖 RenderScript 为给定内核启动创建的累加器数据项的数量。无法保证对具有相同输入的相同内核进行两次启动将创建相同数量的累加器数据项。
您不能依赖 RenderScript 调用初始化程序、累加器和组合器函数的顺序;它甚至可能会并行调用其中的一些函数。无法保证对具有相同输入的相同内核进行两次启动将遵循相同的顺序。唯一保证的是,只有初始化程序函数才能看到未初始化的累加器数据项。例如
- 无法保证在调用累加器函数之前所有累加器数据项都已初始化,尽管它只会对已初始化的累加器数据项进行调用。
- 无法保证输入元素传递给累加器函数的顺序。
- 无法保证在调用组合器函数之前已为所有输入元素调用累加器函数。
这样做的一个结果是,findMinAndMax 内核不是确定性的:如果输入包含相同最小值或最大值的多个出现,则您无法知道内核将找到哪个出现。
您必须保证什么?
由于 RenderScript 系统可以选择以多种不同的方式执行内核,因此您必须遵循某些规则以确保您的内核按预期方式运行。如果您不遵循这些规则,可能会导致结果不正确、行为不确定或运行时错误。
以下规则通常表示两个累加器数据项必须具有“相同的值”。这意味着什么?这取决于您希望内核执行的操作。对于addint 等数学归约,将“相同”理解为数学相等通常是有意义的。对于findMinAndMax(“查找最小和最大输入值的位置”)等“选择任何”搜索,其中可能存在多个相同输入值的出现,则必须将给定输入值的所有位置视为“相同”。您可以编写一个类似的内核来“查找最左边的最小和最大输入值的位置”,其中(例如)位置 100 处的最小值优于位置 200 处的相同最小值;对于此内核,“相同”将意味着相同位置,而不仅仅是相同值,并且累加器和组合器函数必须与findMinAndMax 的函数不同。
初始化程序函数必须创建标识值。也就是说,如果I
和A
是由初始化程序函数初始化的累加器数据项,并且I
从未传递给累加器函数(但A
可能已传递),则
示例:在addint 内核中,累加器数据项初始化为零。此内核的组合器函数执行加法;零是加法的标识值。
示例:在findMinAndMax 内核中,累加器数据项初始化为INITVAL
。
fMMCombiner(&A, &I)
使A
保持不变,因为I
是INITVAL
。fMMCombiner(&I, &A)
将I
设置为A
,因为I
是INITVAL
。
因此,INITVAL
确实是一个标识值。
组合器函数必须是交换的。也就是说,如果A
和B
是由初始化程序函数初始化的累加器数据项,并且可能已零次或多次传递给累加器函数,则combinerName(&A, &B)
必须将A
设置为combinerName(&B, &A)
将B
设置成的相同的值。
示例:在addint 内核中,组合器函数将两个累加器数据项值相加;加法是交换的。
示例:在findMinAndMax 内核中,fMMCombiner(&A, &B)
等于A = minmax(A, B)
,并且minmax
是交换的,因此fMMCombiner
也是交换的。
组合器函数必须是结合的。也就是说,如果A
、B
和C
是由初始化程序函数初始化的累加器数据项,并且可能已零次或多次传递给累加器函数,则以下两个代码序列必须将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
也是结合的。
累加器函数和组合器函数一起必须服从基本折叠规则。也就是说,如果A
和B
是累加器数据项,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 中,就像通过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 是
int
、int2
或int[15]
,则 javaResultType 为int
、Int2
或int[]
。resultType 的所有值都可以用 javaResultType 表示。 - 如果 resultType 是
uint
、uint2
或uint[15]
,则 javaResultType 为long
、Long2
或long[]
。resultType 的所有值都可以用 javaResultType 表示。 - 如果 resultType 是
ulong
、ulong2
或ulong[15]
,则 javaResultType 为long
、Long2
或long[]
。某些 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 是对应于 累加器函数 的对应参数的 inXType 的 Java 类型。除非 inXType 是无符号类型或向量类型,否则 devecSiInXType 是直接对应的 Java 类型。如果 inXType 是无符号标量类型,则 devecSiInXType 是直接对应于相同大小的有符号标量类型的 Java 类型。如果 inXType 是有符号向量类型,则 devecSiInXType 是直接对应于向量分量类型的 Java 类型。如果 inXType 是无符号向量类型,则 devecSiInXType 是直接对应于与向量分量类型相同大小的有符号标量类型的 Java 类型。例如
- 如果 inXType 是
int
,则 devecSiInXType 是int
。 - 如果 inXType 是
int2
,则 devecSiInXType 是int
。该数组是扁平化的表示形式:它具有Allocation中2分量向量元素的两倍数量的标量元素。这与Allocation
的copyFrom()
方法的工作方式相同。 - 如果 inXType 是
uint
,则 deviceSiInXType 是int
。Java数组中的有符号值被解释为Allocation中具有相同位模式的无符号值。这与Allocation
的copyFrom()
方法的工作方式相同。 - 如果 inXType 是
uint2
,则 deviceSiInXType 是int
。这是处理int2
和uint
方式的组合:该数组是扁平化表示形式,并且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]; }
其他代码示例
BasicRenderScript、RenderScriptIntrinsic 和 Hello Compute 示例进一步演示了本页介绍的API的使用。