当我们在说硬件加速渲染,具体指的是什么?

本文转载自:https://www.jianshu.com/p/afb2c6c4ce10

不管Android iOS 还是Web前端 随着UI图形界面的发展,或多或少甚至经常听到硬件加速这个术语。当通过设置某个参数,界面滑动效率变好了。我们都会说硬件加速起作用了。但是终究什么是硬件加速,又加速了什么?大多数人估计还是一知半解,网上相关资料也是比较少的,所以我们今天来探讨一下神秘的硬件加速

计算机成像的原理

计算机和显示器之间通过特定驱动协议通信,程序需要做的事情只是绘制出需要显示的位图(一般由RGBA三个8位次像素组成二维数组,格式可以通过协议互相协商。具体看硬件支持什么格式,比如Alpha通道对屏幕显示就没什么用),然后通过系统驱动接口把二进制数据发送给显示器(比如linux的 /dev/fb0 设备符号,directFB等等),由显示器的硬件把颜色显示到屏幕上去。

所以程序要做的事情就是快速生成当前需要上屏的位图。那么可能有人要问了 “我玩的3D游戏感觉好像不是这样的吧?”,实际上现有的3D渲染方式第一步就是把3D位置数据转化成2D平面数据。

位图生成的原理

问题的关键 在于程序如何生成这个位图,主流的生成算法主要有2种:

  1. 基于线性扫描算法(目前基本上和用户直接有交互的UI系统,全是基于这个算法)
  2. 光线跟踪算法(主要用来渲染真实世界,比如大家看的变形金刚电影就是采用此类算法渲染的,渲染单帧的时间可能非常非常长)
    这里主要讨论线性扫描算法,光线跟踪算法以后有机会慢慢讨论。

pic1

比如程序需要绘制上面这样的三角形,算法很简单:

1
2
3
4
5
6
7
8
9
void draw(rgba** buffer, int width, int height) {
for (int i = 0; i < width; ++i) {
for (int j = 0; j < height; ++j) {
if (is_position_in_path_region(i, j)) {
buffer[i][j] = rgba(0, 0, 0, 1);
}
}
}
}

当然这是一个简单的绘制,那么如果我们要绘制一个半透明的三角形(三角形背后已经有一个红色的圆)应该咋办?

pic2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
rgba draw_color = rgba(0, 0, 0, 0.5);
void draw(rgba** buffer, int width, int height) {
float alpha = draw_color.a;
for (int i = 0; i < width; ++i) {
for (int j = 0; j < height; ++j) {
if (is_position_in_path_region(i, j)) {
rgba back_color = get_current_back_color(i, j);
rgba blend_color;
blend_color.r = (1 - alpha) * draw_color.r + alpha * back_color.r;
blend_color.g = (1 - alpha) * draw_color.g + alpha * back_color.g;
blend_color.b = (1 - alpha) * draw_color.b + alpha * back_color.b;
buffer[i][j] = blend_color;
}
}
}
}

上面的算法解释了AlphaBlend(通过Alpha通道做颜色融合),上下这2段代码表达了简单的像素染色的算法。由于此类算法通常都是类似这样的一个循环,这个过程类似于扫描仪的扫描过程,所以又叫线扫描像素填充。由于位图主要提供给光栅显示器显示使用,所以位图又叫光栅图,那么生成光栅图的算法过程又叫光栅化过程。综上所述整个过程又叫做线扫描光栅化

Tips: 上面的算法主要是把简单的图形光栅化,那么实际情况存在的形状非常之多。那么如何能表达那么多复制的图形(比如螺旋圆环)。工业上存在众多曲线拟合算法,用来拟合各种复杂曲线。其中最好用的数学曲线叫做贝塞尔曲线(贝塞尔曲线并不是贝塞尔发明的,只是贝塞尔首次在论文里面提出使用这个曲线来拟合工业图形)。所以常规我们都喜欢使用贝塞尔曲线来描述图形,描述一个图形只要存储该图形的贝塞尔参数就可以了,并且曲线是由通过公式计算出来的,所以可以无限放缩(所以图形又叫矢量图形,矢量是2D的灵魂,贝塞尔曲线又是矢量的灵魂)。

位图生成算法性能问题

通过上面的矢量扫描填充算法,我们很容易的发现了有2个可能会出现的性能问题:

  1. 大量的循环处理,每个涉及的像素都要处理一遍(按照现在手机的分辨率,算算一帧有多少要处理的)
  2. 颜色融合其实就是浮点数插值算法,对每个次像素都要做,运算量巨大

那么如何解决这2个问题?

  1. 对于像素太多的问题,最好的办法就是脏区域渲染。用人类话来说就是每次渲染的时候,最大化的在上一帧的基础上面进行,对本次这帧没有变化的像素来说直接忽略处理。
  2. 对于浮点数运算太多,可以通过多媒体指令来加速。比如:ARM Neon Intel MMX SSE 等等。以ARM为例,ARM有 16个通用计算寄存器,16个Neon指令寄存器,16个VFP高精度浮点数运算加速器。其中Neon指令可以让程序在一个指令周期里面计算8个浮点数的运算,理想的情况下相当于比传统的浮点运算性能提升了8倍。当然这些指令主要就是用来处理多媒体的,所以又叫多媒体指令 也叫 SIMD指令(Single Instruction Multiple Data,单指令多数据流,Android的底层绘图库就有基于Neon的优化器,我也尝试过用SSE指令优化Windows平台API AlphaBlend函数)。

那么这些都被叫做传统方法,那么硬件加速具体用的是什么方式来加速这个过程的?

图形硬件和硬件的渲染方式

传统的图形硬件主要指的是GPU,渲染接口主要有5种:

title 常规
Opengl 基本在所有平台都能用,基于状态机的接口设计也是醉了
Metal 水果公司独有的,其他平台别指望了
Vulkan 新版的图形接口,未来可能取代Opengl
DirectX Windows平台特有的图形接口
OpenVG 专注于矢量加速的硬件接口,基本上没啥人用

就拿Android来说吧,作为操作系统需要兼容各种硬件。硬件接口比较通用的就是OpenGL(其实早些年SGL在定义硬件的标准的时候,2D和3D是分开定义,2D用OpenVG加速,3D用OpenGL加速。只是后面OpenVG没有成为事实标准),Windows是基于DX3D接口实现的,但是原理也是类似。

那么硬件究竟是如何利用Opengl接口操作硬件来加速光栅化过程?这个要看看Opengl定义的规范了。

Opengl的渲染加速的原理

首先Opengl只能渲染 直线 三角形(之所以硬件指定是三角形。 主要是因为光栅化也就是插值的过程, 当多边形是凸多边形的时候最容易处理,硬件也只支持凸多边形的插值。 三角形在三维变换后任然是凸多边形, 而四边形或者更高边数的形状在变换后可能会出现凹多边形。其次其他形状都可以细分成三角形。)。

pic3

对字母A的图形做三角剖分

当程序需要绘制一个多边形的时候,首先需要将这个多边形剖分成多个三角形,这个过程被称之为三角剖分(剖分算法分2大类,几何剖分点云剖分)。被剖分出来的三角形,提交给显卡。显卡对每个三角形的扫描填充和像素融合处理是像素间无关的所以这类运算完全可以并行处理。GPU的并行处理每个三角形和三角形的像素染色,每个渲染流水称之为渲染管线。CPU在处理的过程中每次最多操作一个像素,而显卡每次可能有几千上万甚至十万的管线并行计算,其次显然的浮点数运算性能比CPU高非常多。这个就是显卡为什么能够加速这个过程。

Android的HWUI的源码中摘抄了一段:

Code Path: android / platform / frameworks / base / master / . / libs / hwui / PathTessellator.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef ANDROID_HWUI_PATH_TESSELLATOR_H
#define ANDROID_HWUI_PATH_TESSELLATOR_H
#include "Matrix.h"
#include "Rect.h"
#include "Vertex.h"
#include "VertexBuffer.h"
#include <algorithm>
#include <vector>
class SkPath;
class SkPaint;
namespace android {
namespace uirenderer {
/**
* Structure used for threshold values in outline path tessellation.
*
* TODO: PaintInfo should store one of this object, and initialized all values in constructor
* depending on its type (point, line or path).
*/
struct PathApproximationInfo {
PathApproximationInfo(float invScaleX, float invScaleY, float pixelThreshold)
: thresholdSquared(pixelThreshold * pixelThreshold)
, sqrInvScaleX(invScaleX * invScaleX)
, sqrInvScaleY(invScaleY * invScaleY)
, thresholdForConicQuads(pixelThreshold * std::min(invScaleX, invScaleY) / 2.0f) {
};
const float thresholdSquared;
const float sqrInvScaleX;
const float sqrInvScaleY;
const float thresholdForConicQuads;
};
class PathTessellator {
public:
/**
* Populates scaleX and scaleY with the 'tessellation scale' of the transform - the effective X
* and Y scales that tessellation will take into account when generating the 1.0 pixel thick
* ramp.
*
* Two instances of the same shape (size, paint, etc.) will only generate the same vertices if
* their tessellation scales are equal.
*/
static void extractTessellationScales(const Matrix4& transform, float* scaleX, float* scaleY);
/**
* Populates a VertexBuffer with a tessellated approximation of the input convex path, as a single
* triangle strip. Note: joins are not currently supported.
*
* @param path The path to be approximated
* @param paint The paint the path will be drawn with, indicating AA, painting style
* (stroke vs fill), stroke width, stroke cap & join style, etc.
* @param transform The transform the path is to be drawn with, used to drive stretch-aware path
* vertex approximation, and correct AA ramp offsetting.
* @param vertexBuffer The output buffer
*/
static void tessellatePath(const SkPath& path, const SkPaint* paint,
const mat4& transform, VertexBuffer& vertexBuffer);
......

这段代码就是HWUI中对路径做的的三角剖分处理。
Opengl绘制几何图形存在的性能瓶颈 主要有以下3点:

  1. 三角剖分性能,如果我们需要绘制一个复杂的多边形,那么首先需要把这个多边形剖分成一个个三角形。这个剖发需要耗时
  2. 由于早期的GPU是作为外设链接到CPU上面的,所以GPU的RAM是和系统的内存是分开的。所以数据需要通过系统BUS发送给显卡,这过程也占据了大量的运行时间。
  3. 每次提交一次渲染就做一次DrawCal,渲染的开始是管线重启。由于Opengl的API缺陷,DrawCall非常贵重,DrawCall甚至一度被用来衡量软件渲染的性能好坏。

Opengl绘制的优势 主要有以下3点:

  1. 硬件插值器实现的光栅化算法,性能飞快
  2. GPU天生的浮点数运算能力,在前后景图Blend过程中可以飞快
  3. 硬件天生的并行运算特性

硬件就一定能加速么?

如果只是一个很简单的图形,那么CPU直接渲染的速度回更快。也就是:(CPU颜色填充时间 < 三角剖分的时间+数据通信的时间+GPU光栅的时间)。比如Android的图形基础库Skia就有基于Opengl的加速的优化模块,Google给出了基于硬件加速后的API性能和CPU运算下的API的性能比较,你会发现并不是所有的绘制接口都有速度提升,甚至有部分API速度慢了十倍以上。

以上就是硬件加速在传统图形界面中的位置,就目前来看硬件的确是加速了图形的渲染,但是是不是所有场景都能加速?硬件加速是不是图形渲染的万金油?还是要理解其中的原理,方能善用。