高级 RenderScript

由于使用 RenderScript 的应用仍在 Android VM 内部运行,因此您可以访问您熟悉的全部框架 API,但在适当情况下可以使用 RenderScript。为了促进框架和 RenderScript 运行时之间的这种交互,还存在一个中间代码层来促进这两个代码级别之间的通信和内存管理。本文档将更详细地介绍这些不同的代码层以及 Android VM 和 RenderScript 运行时之间如何共享内存。

RenderScript 运行时层

您的 RenderScript 代码在紧凑且定义明确的运行时层中编译和执行。RenderScript 运行时 API 支持密集计算,该计算可移植且可自动扩展到处理器上可用内核的数量。

注意: NDK 中的标准 C 函数必须保证在 CPU 上运行,因此 RenderScript 无法访问这些库,因为 RenderScript 旨在在不同类型的处理器上运行。

您在 Android 项目的 src/ 目录中的 .rs.rsh 文件中定义您的 RenderScript 代码。代码由作为 Android 构建的一部分运行的 llvm 编译器编译成中间字节码。当您的应用在设备上运行时,字节码随后由驻留在设备上的另一个 llvm 编译器(即时)编译成机器码。机器码针对设备进行了优化,并且还会缓存,因此随后使用启用 RenderScript 的应用不会重新编译字节码。

RenderScript 运行时库的一些关键功能包括

  • 内存分配请求功能
  • 大量数学函数,其中许多常用例程都具有标量和向量类型重载版本。加法、乘法、点积和叉积等运算可用,以及原子算术和比较函数。
  • 用于基本数据类型和向量、矩阵例程以及日期和时间例程的转换例程
  • 数据类型和结构以支持 RenderScript 系统,例如用于定义二维、三维或四维向量的 Vector 类型。
  • 日志记录功能

有关可用函数的更多信息,请参阅 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 VM 中分配的内存绑定到 RenderScript 运行时(您不能在 .rs 文件中分配内存)。有关更多信息,请参阅 使用已分配内存

内存分配 API

使用 RenderScript 的应用程序仍在 Android VM 中运行。但是,实际的 RenderScript 代码以原生方式运行,并且需要访问在 Android VM 中分配的内存。为此,您必须将 VM 中分配的内存附加到 RenderScript 运行时。此过程称为绑定,允许 RenderScript 运行时无缝地处理它请求但无法显式分配的内存。最终结果与您在 C 中调用 malloc 基本相同。额外的好处是 Android VM 可以执行垃圾回收以及与 RenderScript 运行时层共享内存。绑定仅适用于动态分配的内存。静态分配的内存在编译时会自动为您的 RenderScript 代码创建。有关内存分配方式的更多信息,请参阅 图 1

为了支持此内存分配系统,有一组 API 允许 Android VM 分配内存并提供与 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

Element 描述内存分配的一个单元,并且可以有两种形式:基本或复杂。

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

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

类型

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

一个类型包含五个维度:X、Y、Z、LOD(细节级别)和 Faces(立方体贴图的面)。您可以将 X、Y、Z 维度设置为可用内存限制内的任何正整数。单维度分配的 X 维度大于零,而 Y 和 Z 维度为零表示不存在。例如,x=10、y=1 的分配被认为是二维的,而 x=10、y=0 被认为是一维的。LOD 和 Faces 维度是布尔值,表示存在或不存在。

分配

分配根据Type表示的内存描述为应用程序提供内存。已分配的内存可以同时存在于多个内存空间中。如果在一个空间中修改了内存,则必须显式同步内存,以便将其更新到其存在的所有其他空间中。

分配数据主要通过两种方式上传:类型检查和类型未检查。对于简单的数组,有一些copyFrom()函数可以获取来自 Android 系统的数组并将其复制到原生层内存存储中。未检查的变体允许 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 框架层。从 Android 框架设置的最后一个值始终在调用get方法期间返回。但是,当 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 运行时级别设置了值,则会得到空指针异常

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 运行时。