刚开始接触Android图形上的一些东东,为了学习这里翻译一篇官网博客上的文章,增加了一些内容,并实现了一下Android RenderScript的一个例子,开发的环境是Android Studio,API 23. RenderScript跟图形关系也不很大,有一点关系其实就是在GPGPU的概念上。


原文链接:http://android-developers.blogspot.tw/2011/03/renderscript.html


RenderScript的设计目标

RS有3个主要的设计目标,以下按照重要性从大到小介绍.


可移植性:应用程序需要能够运行在不同的设备上,这些有可能是采用的完全不同的硬件。ARM架构现在就有多种不同的硬件--有或者没有VFP,有或者没有NEON,不同数量的寄存器。除了ARM,还有X86架构,多种GPU架构,甚至更多的DSP架构。


性能:第2个目标是在满足可移植的条件下尽可能的提升性能。对于RS而言,我们需要在性能上比现有的解决方案更好。


易用性:第3个目标是尽可能简化开发。尽可能使用自动化过程来避免耦合的代码和繁重的工作。


为了实现这3个目标,我们在设计上做了一些权衡。这些权衡首先将RS从现有的Android架构方法中进行了剥离,比如Dalvik或NDK,这些是用来解决不同问题的不同工具。


核心设计的选择

第一个需要做出的抉择是采用什么语言?有很多语言可以选择,其中Shader语言,C或者C++都是可以考虑的。最后我们放弃了Shader,因为Shader需要操作的数据结构跟图形应用绑定太紧,并且缺少指针和递归限制了易用性。C++从一方面来讲很不错但是它的问题是可移植性欠缺。高级的C++特性很难运行在没有CPU的硬件上。最后我们选择了C99,因为它提供了提供了跟其他选择一样的性能,并且很容易被开发者理解,而且能够在众多设备上运行。


另一个权衡是RS的流程。具体说就是如何将源码转换成机器码。我们考虑了多个方案并且在开发RS过程中实现了2种不同的方案。早先版本中(2.3)在设备上编译C代码生成机器码,这样做有一些好处,例如应用程序可以快速地编译,但是也却也带来了易用性上的问题。必须要先编译你的App,安装运行,然后才能发现你的语法错误,这是件非常痛苦的事情。而且低端的CPU会限制对代码的分析和优化。


所以我们转而考虑LLVM,采用一个修改过的clang版本将脚本的编译和分析放在开发端。我们在这个阶段中进行了高层次的优化,生成LLVM字节码。从LLVM中间字节码生成机器码,依然是在设备上(附带额外的特定设备的优化)


我们最后一个比较大的权衡是线程的启动。主要权衡的是性能和可移植性。现有的并行计算方案允许开发者在特定设备上进行调优,但是可能却会影响其他设备的性能。如果有足够的时间和资源开发者肯定能够对所有硬件设备都进行调优。然而测试和调优有的时候却无法进行,你无法在未发布的硬件上或者你没有的硬件上进行调优。另一个更可移植的方法是把调优放在运行时,牺牲一点最高性能,而提供足够优秀的平均性能。考虑到我们的第一目标是可移植,所以我们选择将调优放在了运行时。


将线程启动的管理放到运行时带来的另一个影响是决定在哪里运行你的脚本。例如,有些硬件支持指针和递归,有的却不支持。我们选择不允许这些事情,而提供给开发者一个最小的通用标准API,我们选择在运行时对脚本进行分析。这允许开发者能够最大限度地发挥支持这些特性的硬件,因为总是会认为会有一个全特性的CPU可以依赖。所以,开发者可以聚焦在写出更好的App,硬件开发商也由于竞争,而制作出更多特性更高性能的硬件。一个新的硬件特性出现,应用程序也不用去修改已有的代码。


易用性是RS设计中的主要驱动力。大部分现有的并行计算和图形平台需要在App中开发复杂的逻辑代码来实现高性能,这样的代码很容易出bug,写起来也很痛苦。开发端的静态代码分析有助于解决这个问题。每个RS脚本生成一个对应的java类。命名和成员都是从RS脚本中提取出来极大简化了RS脚本的使用。


例子: RS应用

一个简单的RS应用程序是什么样子的?在这个非常简单的例子中,我们将获取一个Bitmap对象,通过运行一段RS脚本,将其转化为一个单色调的Bitmap。在介绍RS脚本之前,先看下应用程序的代码,这段代码来自HelloCompute SDK样例。

(代码与原文有些改变)

public class MainActivity extends AppCompatActivity {

    private Bitmap inbmp;
    private Bitmap outbmp;
    private ImageView inImage;
    private ImageView outImage;
    private RenderScript rs;
    private Allocation inAlloc;
    private Allocation outAlloc;
    private ScriptC_mono script;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        inImage = (ImageView)findViewById(R.id.inimg);
        outImage = (ImageView)findViewById(R.id.outimg);

        Resources res = getResources();
        inbmp = BitmapFactory.decodeResource(res, R.drawable.a);
        outbmp = inbmp.copy(Bitmap.Config.ARGB_8888, false);

        createScript();

        outImage.setImageBitmap(outbmp);
    }

    private void createScript() {
        rs = RenderScript.create(this);

        inAlloc = Allocation.createFromBitmap(rs, inbmp,
                Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT);
        outAlloc = Allocation.createTyped(rs, inAlloc.getType());
        script = new ScriptC_mono(rs);
        script.set_gIn(inAlloc);
        script.set_gOut(outAlloc);
        script.set_gScript(script);
        script.invoke_filter();
        outAlloc.copyTo(outbmp);
    }
}

RS应用需要的第一个对象是context,context是核心对象用来创建和管理所有其他的RS对象。通过RenderScript.Create创建一个context对象。在RS应用运行期间,context必须一直存在。


下面从Bitmap中创建了两个Allocation。RS有自己的存储分配器,因为存储空间很可能被多个处理器共享或者存在于不同的存储空间中。当一个Allocation创建时,它所有可能的用途需要被列举出来,这样系统才能够选择正确的存储来满足其用途。


createFromBitmap创建一个RS Allocation,并将Bitmap内容复制到该Allocation。Allocation是RS应用中内存使用的单元。createTyped生成了另一个Allocation与前面生成的有相同的结构。Allocation结构的定义可以通过getType来查询。RS类型定义了Allocation的结构。这个例子中,RS类型包含了height,width和bitmap的格式。


下面加载了RS脚本,脚本名为mono.rs, 注意ScriptC_mono是根据mono.rs自动生成的java类。

下面3行使用自动生成的java类ScriptC_mono设置脚本的属性。


现在所有都准备好了,函数invoke_filter则是实际计算的部分。它触发脚本中filter()C函数,这里也可以传入参数。由于函数调用是异步的,返回值在这里是不允许的。


最后一行复制计算结果到另一张Bitmap中,RS机制内部有内置的同步机制,来确保脚本运行完毕才执行复制。


例子: The Script

下面是mono.rs脚本

#pragma version(1)
#pragma rs java_package_name(com.example.xubo.hellocompute)

rs_allocation gIn;
rs_allocation gOut;
rs_script gScript;

const static float3 gMonoMult = {0.299f, 0.587f, 0.114f};

void root(const uchar4 *v_in, uchar4 *v_out, const void *usrData, uint32_t x, uint32_t y) {
    float4 f4 = rsUnpackColor8888(*v_in);
    float3 mono = dot(f4.rgb, gMonoMult);
    *v_out = rsPackColorTo8888(mono);
}

void filter() {
    rsForEach(gScript, gIn, gOut, 0, 0);
}

第1行简单告诉编译器使用哪一个版本的RS API。第2行控制自动生成的java代码。


3个全局变量列举了在脚本中使用到的3个变量,gMonoMult被设为静态。非静态,const, globals都是允许的,但仅生成一个get反射方法(???),用来在同步时持有静态变量。


root()是特别的方法,相当于C里面的main函数。当RS被唤醒时,root()将被调用。也可以传递参数进去。在这里参数分别是传入的像素和传出的像素。这里也可以传递用户指针地址和长度进去。我们这里的例子中忽略了指针参数。


root()函数中的代码分别解包RGBA_8888的像素格式到一个4float的vector中。第2行使用了内置的数学点积函数,通过将输入的像素乘上单色常亮获得灰度像素。注意点积的返回值是单个float,这里简单使用float3来接收返回值,点积运算的结果会分别设置到float3的x,y,z中。最后我们使用另一个内置函数来封装float3到一个32位像素中。该例子也说明rsPackColorTo8888入参可以试RGB(float3)或者RGBA(float4).如果Alpha没有提供,没有alpha值是1.0.


filter()函数会在java代码中调用,它会在allocation的每个元素上并行启动计算。第1个参数是需要启动哪个RS脚本--该脚本的root()函数将在每个元素上执行。第2个和第3个参数分别表示输入的allocation和输出的allocation。最后两个参数是指向用户数据的指针和数据长度。


如果设备有多个处理器,forEach函数将会在对个线程执行。将来forEach可以提供一个转移点,可以在多个处理器之间进行转换。我们的例子中有理由相信filter()函数将在CPU上执行,而root()将可能在GPU或DSP上执行。


我希望这个例子可以粗略探究到RS的设计和RS如何简单运用。


补充的例子代码和说明

RS脚本放在哪里?Eclipse和Android Studio好像不太一样,以Android Studio为例,放app\src\main\rs中。


运行结果

wKioL1cXFB7zw0-MAASwp73esnc970.png