高级 RenderScript

由于使用 RenderScript 的应用仍在 Android 虚拟机内运行,因此您可以使用自己熟悉的所有框架 API,同时在适当的时候按需使用 RenderScript。为了推动框架与 RenderScript 运行时之间的这种交互,我们还提供了一个中间代码层,以促进这两个代码层之间的通信和内存管理。本文档详细介绍了这些不同的代码层,以及如何在 Android 虚拟机和 RenderScript 运行时之间共享内存。

RenderScript 运行时层

RenderScript 代码在紧凑且明确定义的运行时层进行编译和执行。RenderScript 的运行时 API 支持密集型计算,它们具有可移植性,并且能够根据处理器上可用的核心数量自动扩缩。

注意:必须保证 NDK 中的标准 C 函数能在 CPU 上运行,因为 RenderScript 的设计目标之一就是要能够在不同类型的处理器上运行,所以无法使用这些库。

您可以在 Android 项目的 src/ 目录中的 .rs.rsh 文件内定义您的 RenderScript 代码。作为 Android 构建流程的环节之一运行的 llvm 编译器会将这些代码编译为中间字节码。当您的应用在设备上运行时,这些字节码随后会由设备上的另一个 llvm 编译器即时编译为机器代码。这些机器代码针对设备进行了优化,并进行了缓存;因此在后续使用支持 RenderScript 的应用时,系统不会重新编译字节码。

RenderScript 运行时库的一些主要功能包括:

  • 内存分配请求功能
  • 一个由数学函数组成的大型集合,包括许多常见例程的标量型和矢量型重载版本。可以使用加法、乘法、点积和叉积等运算以及原子算法和比较函数。
  • 适用于基元数据类型和矢量的转换例程、矩阵例程以及日期和时间例程
  • 支持 RenderScript 系统的数据类型和结构(例如,用于定义二矢量、三矢量或四矢量的矢量类型)。
  • 日志记录功能

如需详细了解可用的函数,请参阅 RenderScript 运行时 API 参考文档。

反射层

反射层是 Android 构建工具生成的一组类,可让您从 Android 框架访问 RenderScript 运行时。此层还提供了多种方法和构造函数,可用于为 RenderScript 代码中定义的指针分配和使用内存。以下列表介绍了反射的主要组件:

  • 您创建的每个 .rs 文件都会生成到名为 project_root/gen/package/name/ScriptC_renderscript_filename 且类型为 ScriptC 的类中。此文件是您的 .rs 文件的 .java 版本,您可以从 Android 框架中调用它。此类包含从 .rs 文件中反射的以下几项:
    • 非静态函数
    • 非静态的全局 RenderScript 变量。系统针对每个变量生成了访问器方法,因此您可以从 Android 框架读取和写入 RenderScript 变量。如果全局变量在 RenderScript 运行时层进行了初始化,这些值就可用于初始化 Android 框架层中相应的值。如果全局变量标记为 const,就不会生成 set 方法。如需了解详情,请参阅此处的内容

    • 全局指针
  • struct 将反射到其自己名为 project_root/gen/package/name/ScriptField_struct_name 的类中,该类扩展了 Script.FieldBase。此类表示 struct 的数组,可用于为此 struct 的一个或多个实例分配内存。

函数

函数会反射到位于 project_root/gen/package/name/ScriptC_renderscript_filename 的脚本类中。例如,如果您在 RenderScript 代码中定义了以下函数:

void touch(float x, float y, float pressure, int id) {
    if (id >= 10) {
        return;
    }

    touchPos[id].x = x;
    touchPos[id].y = y;
    touchPressure[id] = pressure;
}

系统就会生成以下 Java 代码:

public void invoke_touch(float x, float y, float pressure, int id) {
    FieldPacker touch_fp = new FieldPacker(16);
    touch_fp.addF32(x);
    touch_fp.addF32(y);
    touch_fp.addF32(pressure);
    touch_fp.addI32(id);
    invoke(mExportFuncIdx_touch, touch_fp);
}

函数不能具有返回值,因为 RenderScript 系统被设计为异步执行。当 Android 框架代码调用 RenderScript 时,系统会将该调用加入队列并尽可能执行。这一限制使 RenderScript 系统可以正常运行而不会经常中断,同时提高效率。如果允许函数具有返回值,系统就会屏蔽该调用,直到返回值。

如果您希望 RenderScript 代码将值发回给 Android 框架,请使用 rsSendToClient() 函数。

变量

属于受支持类型的变量会反射到位于 project_root/gen/package/name/ScriptC_renderscript_filename 的脚本类中。系统会为每个变量生成一组访问器方法。例如,如果您在 RenderScript 代码中定义了以下变量:

uint32_t unsignedInteger = 1;

系统就会生成以下 Java 代码:

private long mExportVar_unsignedInteger;
public void set_unsignedInteger(long v){
    mExportVar_unsignedInteger = v;
    setVar(mExportVarIdx_unsignedInteger, v);
}

public long get_unsignedInteger(){
    return mExportVar_unsignedInteger;
}
  

结构体

结构体会反射到它们自己位于 <project_root>/gen/com/example/renderscript/ScriptField_struct_name 的类中。此类表示 struct 的数组,可用于为指定数量的 struct 分配内存。例如,如果您定义了以下结构体:

typedef struct Point {
    float2 position;
    float size;
} Point_t;

系统就会在 ScriptField_Point.java 中生成以下代码:

package com.example.android.rs.hellocompute;

import android.renderscript.*;
import android.content.res.Resources;

  /**
  * @hide
  */
public class ScriptField_Point extends android.renderscript.Script.FieldBase {

    static public class Item {
        public static final int sizeof = 12;

        Float2 position;
        float size;

        Item() {
            position = new Float2();
        }
    }

    private Item mItemArray[];
    private FieldPacker mIOBuffer;
    public static Element createElement(RenderScript rs) {
        Element.Builder eb = new Element.Builder(rs);
        eb.add(Element.F32_2(rs), "position");
        eb.add(Element.F32(rs), "size");
        return eb.create();
    }

    public  ScriptField_Point(RenderScript rs, int count) {
        mItemArray = null;
        mIOBuffer = null;
        mElement = createElement(rs);
        init(rs, count);
    }

    public  ScriptField_Point(RenderScript rs, int count, int usages) {
        mItemArray = null;
        mIOBuffer = null;
        mElement = createElement(rs);
        init(rs, count, usages);
    }

    private void copyToArray(Item i, int index) {
        if (mIOBuffer == null) mIOBuffer = new FieldPacker(Item.sizeof * getType().getX()/* count
        */);
        mIOBuffer.reset(index * Item.sizeof);
        mIOBuffer.addF32(i.position);
        mIOBuffer.addF32(i.size);
    }

    public void set(Item i, int index, boolean copyNow) {
        if (mItemArray == null) mItemArray = new Item[getType().getX() /* count */];
        mItemArray[index] = i;
        if (copyNow)  {
            copyToArray(i, index);
            mAllocation.setFromFieldPacker(index, mIOBuffer);
        }
    }

    public Item get(int index) {
        if (mItemArray == null) return null;
        return mItemArray[index];
    }

    public void set_position(int index, Float2 v, boolean copyNow) {
        if (mIOBuffer == null) mIOBuffer = new FieldPacker(Item.sizeof * getType().getX()/* count */);
        if (mItemArray == null) mItemArray = new Item[getType().getX() /* count */];
        if (mItemArray[index] == null) mItemArray[index] = new Item();
        mItemArray[index].position = v;
        if (copyNow) {
            mIOBuffer.reset(index * Item.sizeof);
            mIOBuffer.addF32(v);
            FieldPacker fp = new FieldPacker(8);
            fp.addF32(v);
            mAllocation.setFromFieldPacker(index, 0, fp);
        }
    }

    public void set_size(int index, float v, boolean copyNow) {
        if (mIOBuffer == null) mIOBuffer = new FieldPacker(Item.sizeof * getType().getX()/* count */);
        if (mItemArray == null) mItemArray = new Item[getType().getX() /* count */];
        if (mItemArray[index] == null) mItemArray[index] = new Item();
        mItemArray[index].size = v;
        if (copyNow)  {
            mIOBuffer.reset(index * Item.sizeof + 8);
            mIOBuffer.addF32(v);
            FieldPacker fp = new FieldPacker(4);
            fp.addF32(v);
            mAllocation.setFromFieldPacker(index, 1, fp);
        }
    }

    public Float2 get_position(int index) {
        if (mItemArray == null) return null;
        return mItemArray[index].position;
    }

    public float get_size(int index) {
        if (mItemArray == null) return 0;
        return mItemArray[index].size;
    }

    public void copyAll() {
        for (int ct = 0; ct < mItemArray.length; ct++) copyToArray(mItemArray[ct], ct);
        mAllocation.setFromFieldPacker(0, mIOBuffer);
    }

    public void resize(int newSize) {
        if (mItemArray != null)  {
            int oldSize = mItemArray.length;
            int copySize = Math.min(oldSize, newSize);
            if (newSize == oldSize) return;
            Item ni[] = new Item[newSize];
            System.arraycopy(mItemArray, 0, ni, 0, copySize);
            mItemArray = ni;
        }
        mAllocation.resize(newSize);
        if (mIOBuffer != null) mIOBuffer = new FieldPacker(Item.sizeof * getType().getX()/* count */);
    }
}

系统为您提供生成的代码,是为了便于您为 RenderScript 运行时所请求的结构体分配内存,并与内存中的 struct 进行互动。每个 struct 的类都定义了以下方法和构造函数:

  • 可用于分配内存的重载构造函数。ScriptField_struct_name(RenderScript rs, int count) 构造函数可让您使用 count 参数定义要为其分配内存的结构的数量。ScriptField_struct_name(RenderScript rs, int count, int usages) 构造函数定义了一个额外参数 usages,该参数用于指定此内存分配的内存空间。您可以指定以下四种内存空间:

    使用按位 OR 运算符可指定多个内存空间。执行此操作会告知 RenderScript 运行时您要访问指定内存空间中的数据。以下示例展示了如何在脚本和顶点内存空间中为自定义数据类型分配内存:

    Kotlin

    val touchPoints: ScriptField_Point = ScriptField_Point(
            myRenderScript,
            2,
            Allocation.USAGE_SCRIPT or Allocation.USAGE_GRAPHICS_VERTEX
    )
    

    Java

    ScriptField_Point touchPoints = new ScriptField_Point(myRenderScript, 2,
            Allocation.USAGE_SCRIPT | Allocation.USAGE_GRAPHICS_VERTEX);
    
  • 静态嵌套类 Item 用于以对象形式创建 struct 的实例。如果在 Android 代码中使用 struct 更有意义,此嵌套类会非常有用。操控对象之后,您可以通过调用 set(Item i, int index, boolean copyNow) 并将 Item 设置到数组中的所需位置,将对象推送到已分配的内存中。RenderScript 运行时自动拥有对新写入的内存的访问权限。
  • 访问器方法用于获取和设置结构体中每个字段的值。每个访问器方法都有一个 index 参数,用于指定要读取或写入的数组中的 struct。每个 setter 方法还有一个 copyNow 参数,用于指定是否立即将此内存同步到 RenderScript 运行时。如需同步任何尚未同步的内存,请调用 copyAll()
  • createElement() 方法可在内存中创建结构体的说明。分配由一个或多个元素组成的内存时会用到此说明。
  • resize() 的工作原理与 C 中的 realloc() 十分类似,可用于扩展先前分配的内存,并维护之前创建的当前值。
  • copyAll() 可将在框架级别设置的内存同步到 RenderScript 运行时。对某个成员调用 set 访问器方法时,可以指定一个 copyNow 布尔值参数(可选)。在调用该方法时指定 true 即可同步内存。如果您指定 false,则可以调用 copyAll() 一次,并且它会同步所有尚未同步的属性的内存。

指针

全局指针会反射到位于 project_root/gen/package/name/ScriptC_renderscript_filename 的脚本类中。您可以声明指向 struct 或属于任何支持的 RenderScript 类型的指针,但 struct 无法包含指针或嵌套数组。例如,如果您定义以下指向 structint32_t 的指针:

typedef struct Point {
    float2 position;
    float size;
} Point_t;

Point_t *touchPoints;
int32_t *intPointer;

系统就会生成以下 Java 代码:

private ScriptField_Point mExportVar_touchPoints;
public void bind_touchPoints(ScriptField_Point v) {
    mExportVar_touchPoints = v;
    if (v == null) bindAllocation(null, mExportVarIdx_touchPoints);
    else bindAllocation(v.getAllocation(), mExportVarIdx_touchPoints);
}

public ScriptField_Point get_touchPoints() {
    return mExportVar_touchPoints;
}

private Allocation mExportVar_intPointer;
public void bind_intPointer(Allocation v) {
    mExportVar_intPointer = v;
    if (v == null) bindAllocation(null, mExportVarIdx_intPointer);
    else bindAllocation(v, mExportVarIdx_intPointer);
}

public Allocation get_intPointer() {
    return mExportVar_intPointer;
}
  

系统生成了一个 get 方法和一个名为 bind_pointer_name 的特殊方法(而不是 set() 方法)。bind_pointer_name 方法用于将 Android 虚拟机中分配的内存绑定到 RenderScript 运行时(无法在 .rs 文件中分配内存)。如需了解详情,请参阅使用分配的内存

内存分配 API

使用 RenderScript 的应用仍在 Android 虚拟机中运行。不过,实际的 RenderScript 代码会以原生方式运行,并且需要对 Android 虚拟机中分配的内存的访问权限。为此,您必须将虚拟机中分配的内存附加到 RenderScript 运行时。此过程称为绑定,可以让 RenderScript 运行时无缝使用其已请求但无法明确分配的内存。最终结果与在 C 中调用 malloc 的结果基本相同。这还可以带来额外的好处:Android 虚拟机能够执行垃圾回收并与 RenderScript 运行时层共享内存。仅动态分配的内存才需要绑定。在编译时,系统会为您的 RenderScript 代码自动创建静态分配的内存。如需详细了解内存分配的工作原理,请参见图 1

为了支持此内存分配系统,我们提供了一组 API,这些 API 使 Android 虚拟机能够分配内存并提供与 malloc 调用类似的功能。这些类实质上描述了应该如何分配内存以及如何执行分配。为了更好地理解这些类的工作原理,最好将它们与简单的 malloc 调用联系起来,如下所示:

array = (int *)malloc(sizeof(int)*10);

malloc 调用可分为两部分:分配的内存大小 (sizeof(int)),以及应分配的内存单元数 (10)。Android 框架为这两个部分提供了多个类,以及一个表示 malloc 本身的类。

Element 类表示 malloc 调用的 (sizeof(int)) 部分,并会封装内存分配的一个单元(例如,单个浮点值或结构体)。Type 类可封装 Element 以及要分配的元素数量(在我们的示例中为 10 个)。您可以将 Type 视为 Element 的数组。Allocation 类根据指定的 Type 执行实际的内存分配任务,并表示实际分配的内存。

在大多数情况下,您无需直接调用这些内存分配 API。反射层类会生成代码以自动使用这些 API,而您只需调用在某个反射层类中声明的构造函数,然后将生成的内存 Allocation 绑定到 RenderScript,即可分配内存。在某些情况下,您不妨直接使用这些类来自行分配内存,例如从资源加载位图或者在需要为指向基元类型的指针分配内存时。您可以在分配动态内存并将其绑定到 RenderScript 部分了解如何实现这一点。下表更详细地介绍了这三种内存管理类:

Android 对象类型 说明
Element

元素用于描述内存分配中的一个单元,它可以有两种形式:基本或复杂。

基本元素包含属于任何有效的 RenderScript 数据类型的单个数据组件。 例如,基本元素数据类型包括单个 float 值、一个 float4 矢量或单个 RGB-565 颜色。

复杂元素包含一系列基本元素,并基于您在 RenderScript 代码中声明的 struct 而创建。例如,一个分配可以包含内存中按顺序排列的多个 struct。每个结构体都被视为其自己的元素,而非该结构体中的各个数据类型。

Type

类型是内存分配模板,由一个元素和一个或多个维度组成。它描述了内存布局(基本上是 Element 的数组),但没有为其描述的数据分配内存。

类型由五个维度组成:X、Y、Z、LOD(详细程度)和面(立方体贴图的面)。您可以将 X、Y、Z 维度设置为可用内存限制内的任意正整数值。单一维度分配的 X 维度大于零,而 Y 和 Z 维度为零,即表示不存在相应维度。例如,x=10、y=1 的分配被视为二维,而 x=10、y=0 则被视为一维。LOD 和面维度为布尔值,用于表示存在或不存在。

Allocation

分配可根据由 Type 表示的内存描述为应用提供内存。分配的内存可同时存在于多个内存空间中。如果在一个空间中修改了内存,必须明确同步该内存,以便有该内存存在的其他所有空间都相应进行更新。

分配数据主要通过以下两种方式之一上传:Checked 类型和 Unchecked 类型。对于简单的数组,copyFrom() 函数可从 Android 系统中获取一个数组并将其复制到原生层内存存储。Unchecked 变体允许 Android 系统复制结构数组,因为它不支持结构。例如,如果存在一个分配,它是由 n 个浮点数组成的数组,那么就可以复制 float[n] 数组或 byte[n*4] 数组中包含的数据。

使用内存

系统会在编译时为您在 RenderScript 中声明的非静态全局变量分配内存。您可以直接在 RenderScript 代码中使用这些变量,无需在 Android 框架级别为其分配内存。Android 框架层还可以使用所提供的在反射层类中生成的访问器方法来访问这些变量。如果这些变量在 RenderScript 运行时层进行了初始化,这些值就可用于初始化 Android 框架层中相应的值。如果将全局变量标记为 const,就不会生成 set 方法。如需了解详情,请参阅此处的内容

注意:如果您使用某些包含指针的 RenderScript 结构(例如,rs_program_fragmentrs_allocation),您必须先获取相应的 Android 框架类的对象,然后为该结构调用 set 方法,以将内存绑定到 RenderScript 运行时。您无法在 RenderScript 运行时层直接操控这些结构。这一限制不适用于用户定义的包含指针的结构,因为它们一开始就无法导出到反射层类。如果您尝试声明包含指针的非静态全局结构体,会生成编译器错误。

RenderScript 也支持指针,但您必须在 Android 框架代码中明确分配内存。在 .rs 文件中声明全局指针时,您可以通过相应的反射层类分配内存并将该内存绑定到原生 RenderScript 层。您可以通过 Android 框架层以及 RenderScript 层与此内存交互,这样您便可以灵活地在最合适的层中修改变量。

分配动态内存并将其绑定到 RenderScript

如需分配动态内存,您需要调用 Script.FieldBase 类的构造函数,这是最常见的方式。另一种方式是手动创建 Allocation,这是基元类型指针等对象所需的操作。为简单起见,您应该尽可能使用 Script.FieldBase 类构造函数。获取内存分配后,调用指针的反射 bind 方法可将分配的内存绑定到 RenderScript 运行时。

以下示例展示了如何为基元类型指针 intPointer 和指向结构体的指针 touchPoints 分配内存。它还展示了如何将内存绑定到 RenderScript:

Kotlin

private lateinit var myRenderScript: RenderScript
private lateinit var script: ScriptC_example
private lateinit var resources: Resources

public fun init(rs: RenderScript, res: Resources) {
    myRenderScript = rs
    resources = res

    // allocate memory for the struct pointer, calling the constructor
    val touchPoints = ScriptField_Point(myRenderScript, 2)

    // Create an element manually and allocate memory for the int pointer
    val intPointer: Allocation = Allocation.createSized(myRenderScript, Element.I32(myRenderScript), 2)

    // create an instance of the RenderScript, pointing it to the bytecode resource
    script = ScriptC_point(myRenderScript/*, resources, R.raw.example*/)

    // bind the struct and int pointers to the RenderScript
    script.bind_touchPoints(touchPoints)
    script.bind_intPointer(intPointer)

   ...
}

Java

private RenderScript myRenderScript;
private ScriptC_example script;
private Resources resources;

public void init(RenderScript rs, Resources res) {
    myRenderScript = rs;
    resources = res;

    // allocate memory for the struct pointer, calling the constructor
    ScriptField_Point touchPoints = new ScriptField_Point(myRenderScript, 2);

    // Create an element manually and allocate memory for the int pointer
    intPointer = Allocation.createSized(myRenderScript, Element.I32(myRenderScript), 2);

    // create an instance of the RenderScript, pointing it to the bytecode resource
    script = new ScriptC_example(myRenderScript, resources, R.raw.example);

    // bind the struct and int pointers to the RenderScript
    script.bind_touchPoints(touchPoints);
    script.bind_intPointer(intPointer);

   ...
}

读取和写入内存

您可以在 RenderScript 运行时和 Android 框架层读取和写入静态与动态分配的内存。

静态分配的内存在 RenderScript 运行时级别受到单向通信限制。当 RenderScript 代码更改变量的值时,为了提高效率,它不会回过头与 Android 框架层通信。在调用 get 方法期间,系统始终会返回通过 Android 框架设置的最后一个值。不过,当 Android 框架代码修改变量时,系统会自动将该更改传达给 RenderScript 运行时,或者稍后同步更改。如果需要 RenderScript 运行时将数据发送到 Android 框架层,您可以使用 rsSendToClient() 函数来解决此限制问题。

使用动态分配的内存时,如果您使用关联的指针修改了内存分配,那么 RenderScript 运行时层的所有更改都会回传至 Android 框架层。在 Android 框架层修改对象会立即将该更改回传至 RenderScript 运行时层。

读取和写入全局变量

读取和写入全局变量的过程非常简单。您可以在 Android 框架级别使用访问器方法,或者直接在 RenderScript 代码中进行设置。请注意,您在 RenderScript 代码中所做的任何更改都不会回传到 Android 框架层(如需了解详情,请参阅此处的内容)。

例如,假定在名为 rsfile.rs 的文件中声明了以下结构体:

typedef struct Point {
    int x;
    int y;
} Point_t;

Point_t point;

您可以像下面这样直接在 rsfile.rs 中为结构体分配值。这些值不会回传到 Android 框架级别:

point.x = 1;
point.y = 1;

您可以像下面这样在 Android 框架层为结构体分配值。这些值会异步回传到 RenderScript 运行时级别:

Kotlin

val script: ScriptC_rsfile = ...

...

script._point = ScriptField_Point.Item().apply {
    x = 1
    y = 1
}

Java

ScriptC_rsfile script;

...

Item i = new ScriptField_Point.Item();
i.x = 1;
i.y = 1;
script.set_point(i);

您可以读取 RenderScript 代码中的值,具体代码如下所示:

rsDebug("Printing out a Point", point.x, point.y);

您可以使用以下代码读取 Android 框架层中的值。请注意,只有在 Android 框架级别设置了值的情况下,此代码才会返回值。如果仅在 RenderScript 运行时级别设置值,就会出现 null 指针异常:

Kotlin

Log.i("TAGNAME", "Printing out a Point: ${mScript._point.x} ${mScript._point.y}")
println("${point.x} ${point.y}")

Java

Log.i("TAGNAME", "Printing out a Point: " + script.get_point().x + " " + script.get_point().y);
System.out.println(point.get_x() + " " + point.get_y());

读取和写入全局指针

假设已在 Android 框架级别分配内存并将其绑定到 RenderScript 运行时,您可以通过使用指针的 getset 方法从 Android 框架级别读取和写入内存。在 RenderScript 运行时层中,您可以照常通过指针读取和写入内存,而这些更改会回传到 Android 框架层,这点与静态分配的内存不同。

例如,假定以下指针指向名为 rsfile.rs 的文件中的 struct

typedef struct Point {
    int x;
    int y;
} Point_t;

Point_t *point;

假设您已在 Android 框架层分配了内存,您可以照常访问 struct 中的值。您通过结构体的指针变量对结构体进行的任何更改都会自动提供给 Android 框架层:

Kotlin

point[index].apply {
    x = 1
    y = 1
}

Java

point[index].x = 1;
point[index].y = 1;

您还可以在 Android 框架层从指针读取值和将值写入指针:

Kotlin

val i = ScriptField_Point.Item().apply {
    x = 100
    y = 100
}
val p = ScriptField_Point(rs, 1).apply {
    set(i, 0, true)
}
script.bind_point(p)

p.get_x(0)            //read x and y from index 0
p.get_y(0)

Java

ScriptField_Point p = new ScriptField_Point(rs, 1);
Item i = new ScriptField_Point.Item();
i.x=100;
i.y = 100;
p.set(i, 0, true);
script.bind_point(p);

p.get_x(0);            //read x and y from index 0
p.get_y(0);

绑定好内存之后,您便无需在每次更改某个值后将内存重新绑定到 RenderScript 运行时。