WhyCan Forum

本站无需注册,无需积分,无需回复可下载所有资料,如果真的喜欢小站,请您注册之后请至少回复一个帖子激活Id,谢谢支持! 站长QQ: 516333132 (挖坑网/填坑网) admin@whycan.cn

您尚未登录。

#1 2019-08-06 11:15:55

lixianjing
会员
注册时间: 2019-04-25
累计积分: 11

NanoVG 优化笔记:性能提高5倍的秘密

[size=150]# NanoVG 优化笔记[/size]

[nanovg](https://github.com/memononen/nanovg)正如其名称所示的那样,是一个非常小巧的矢量绘图函数库。相比cairo和skia的数十万行代码,nanovg不足5000行的C语言代码,称为nano也是名副其实了。nanovg的设计、接口和代码质量都堪称典范,唯一美中不足的就是性能不太理想。特别是在Android的低端机型和大屏幕的机型上,一个简单的界面每秒只能画十几帧。最近我把AWTK移植到Android上时,就碰到了这个尴尬的问题。

经过优化之后,AWTK在低端机型上,整体渲染性能有了3到5倍的提升。这里做个笔记,供有需要的朋友参考。

nanovg的性能瓶颈在于片段着色器(fragment shader),片段着色器可以认为是为GPU提供的一个回调函数,该回调函数在处理每个像素时被调用,在每一帧绘制时都会执行数百万次,可见该函数的对性能的影响是很大的。

我们先看看nanovg的片段着色器(fragment shader)代码:

   static const char* fillFragShader =
      "#ifdef GL_ES\n"
      "#if defined(GL_FRAGMENT_PRECISION_HIGH) || defined(NANOVG_GL3)\n"
      " precision highp float;\n"
      "#else\n"
      " precision mediump float;\n"
      "#endif\n"
      "#endif\n"
      "#ifdef NANOVG_GL3\n"
      "#ifdef USE_UNIFORMBUFFER\n"
      "   layout(std140) uniform frag {\n"
      "      mat3 scissorMat;\n"
      "      mat3 paintMat;\n"
      "      vec4 innerCol;\n"
      "      vec4 outerCol;\n"
      "      vec2 scissorExt;\n"
      "      vec2 scissorScale;\n"
      "      vec2 extent;\n"
      "      float radius;\n"
      "      float feather;\n"
      "      float strokeMult;\n"
      "      float strokeThr;\n"
      "      int texType;\n"
      "      int type;\n"
      "   };\n"
      "#else\n" // NANOVG_GL3 && !USE_UNIFORMBUFFER
      "   uniform vec4 frag[UNIFORMARRAY_SIZE];\n"
      "#endif\n"
      "   uniform sampler2D tex;\n"
      "   in vec2 ftcoord;\n"
      "   in vec2 fpos;\n"
      "   out vec4 outColor;\n"
      "#else\n" // !NANOVG_GL3
      "   uniform vec4 frag[UNIFORMARRAY_SIZE];\n"
      "   uniform sampler2D tex;\n"
      "   varying vec2 ftcoord;\n"
      "   varying vec2 fpos;\n"
      "#endif\n"
      "#ifndef USE_UNIFORMBUFFER\n"
      "   #define scissorMat mat3(frag[0].xyz, frag[1].xyz, frag[2].xyz)\n"
      "   #define paintMat mat3(frag[3].xyz, frag[4].xyz, frag[5].xyz)\n"
      "   #define innerCol frag[6]\n"
      "   #define outerCol frag[7]\n"
      "   #define scissorExt frag[8].xy\n"
      "   #define scissorScale frag[8].zw\n"
      "   #define extent frag[9].xy\n"
      "   #define radius frag[9].z\n"
      "   #define feather frag[9].w\n"
      "   #define strokeMult frag[10].x\n"
      "   #define strokeThr frag[10].y\n"
      "   #define texType int(frag[10].z)\n"
      "   #define type int(frag[10].w)\n"
      "#endif\n"
      "\n"
      "float sdroundrect(vec2 pt, vec2 ext, float rad) {\n"
      "   vec2 ext2 = ext - vec2(rad,rad);\n"
      "   vec2 d = abs(pt) - ext2;\n"
      "   return min(max(d.x,d.y),0.0) + length(max(d,0.0)) - rad;\n"
      "}\n"
      "\n"
      "// Scissoring\n"
      "float scissorMask(vec2 p) {\n"
      "   vec2 sc = (abs((scissorMat * vec3(p,1.0)).xy) - scissorExt);\n"
      "   sc = vec2(0.5,0.5) - sc * scissorScale;\n"
      "   return clamp(sc.x,0.0,1.0) * clamp(sc.y,0.0,1.0);\n"
      "}\n"
      "#ifdef EDGE_AA\n"
      "// Stroke - from [0..1] to clipped pyramid, where the slope is 1px.\n"
      "float strokeMask() {\n"
      "   return min(1.0, (1.0-abs(ftcoord.x*2.0-1.0))*strokeMult) * min(1.0, ftcoord.y);\n"
      "}\n"
      "#endif\n"
      "\n"
      "void main(void) {\n"
      "   vec4 result;\n"
      "   float scissor = scissorMask(fpos);\n"
      "#ifdef EDGE_AA\n"
      "   float strokeAlpha = strokeMask();\n"
      "   if (strokeAlpha < strokeThr) discard;\n"
      "#else\n"
      "   float strokeAlpha = 1.0;\n"
      "#endif\n"
      "   if (type == 0) {         // Gradient\n"
      "      // Calculate gradient color using box gradient\n"
      "      vec2 pt = (paintMat * vec3(fpos,1.0)).xy;\n"
      "      float d = clamp((sdroundrect(pt, extent, radius) + feather*0.5) / feather, 0.0, 1.0);\n"
      "      vec4 color = mix(innerCol,outerCol,d);\n"
      "      // Combine alpha\n"
      "      color *= strokeAlpha * scissor;\n"
      "      result = color;\n"
      "   } else if (type == 1) {      // Image\n"
      "      // Calculate color fron texture\n"
      "      vec2 pt = (paintMat * vec3(fpos,1.0)).xy / extent;\n"
      "#ifdef NANOVG_GL3\n"
      "      vec4 color = texture(tex, pt);\n"
      "#else\n"
      "      vec4 color = texture2D(tex, pt);\n"
      "#endif\n"
      "      if (texType == 1) color = vec4(color.xyz*color.w,color.w);"
      "      if (texType == 2) color = vec4(color.x);"
      "      // Apply color tint and alpha.\n"
      "      color *= innerCol;\n"
      "      // Combine alpha\n"
      "      color *= strokeAlpha * scissor;\n"
      "      result = color;\n"
      "   } else if (type == 2) {      // Stencil fill\n"
      "      result = vec4(1,1,1,1);\n"
      "   } else if (type == 3) {      // Textured tris\n"
      "#ifdef NANOVG_GL3\n"
      "      vec4 color = texture(tex, ftcoord);\n"
      "#else\n"
      "      vec4 color = texture2D(tex, ftcoord);\n"
      "#endif\n"
      "      if (texType == 1) color = vec4(color.xyz*color.w,color.w);"
      "      if (texType == 2) color = vec4(color.x);"
      "      color *= scissor;\n"
      "      result = color * innerCol;\n"
      "   }\n"
      "#ifdef NANOVG_GL3\n"
      "   outColor = result;\n"
      "#else\n"
      "   gl_FragColor = result;\n"
      "#endif\n"
      "}\n";

它的功能很完整也很复杂,裁剪和反走样都做了处理。仔细分析之后,我发现了几个性能问题:

### 一、颜色填充的问题

简单颜色填充和渐变颜色填充使用了相同的代码:

      "   if (type == 0) {         // Gradient\n"
      "      // Calculate gradient color using box gradient\n"
      "      vec2 pt = (paintMat * vec3(fpos,1.0)).xy;\n"
      "      float d = clamp((sdroundrect(pt, extent, radius) + feather*0.5) / feather, 0.0, 1.0);\n"
      "      vec4 color = mix(innerCol,outerCol,d);\n"
      "      // Combine alpha\n"
      "      color *= strokeAlpha * scissor;\n"
      "      result = color;\n"

#### 问题

简单颜色填充只需一条指令,而渐变颜色填充则需要数十条指令。这两种情况重用一段代码,会让简单颜色填充慢10倍以上。

#### 方案

把颜色填充分成以下几种情况,分别进行优化:

* 矩形简单颜色填充。

对于无需裁剪的矩形(这是最常见的情况),直接赋值即可,性能提高20倍以上。

      " if (type == 5) {    //fast fill color\n"
      "   result = innerCol;\n"

* 通用多边形简单颜色填充。

去掉渐变的采样函数,性能会提高一倍以上:

    " } else if(type == 7) {      // fill color\n"
      "   strokeAlpha = strokeMask();\n"
      "   if (strokeAlpha < strokeThr) discard;\n"
      "   float scissor = scissorMask(fpos);\n"
      "   vec4 color = innerCol;\n"
      "   color *= strokeAlpha * scissor;\n"
      "   result = color;\n"

* 渐变颜色填充(只占极小的部分)。

这种情况非常少见,还是使用之前的代码。

#### 效果:

平均情况,填充性能提高10倍以上!

### 二、字体的问题

对于文字而言,需要显示的像素和不显示的像素,平均算下来在1:1左右。

      "   } else if (type == 3) {      // Textured tris\n"
      "#ifdef NANOVG_GL3\n"
      "      vec4 color = texture(tex, ftcoord);\n"
      "#else\n"
      "      vec4 color = texture2D(tex, ftcoord);\n"
      "#endif\n"
      "      if (texType == 1) color = vec4(color.xyz*color.w,color.w);"
      "      if (texType == 2) color = vec4(color.x);"
      "      color *= scissor;\n"
      "      result = color * innerCol;\n"
      "   }\n"

#### 问题:

如果显示的像素和不显示的像素都走完整的流程,会浪费调一半的时间。

#### 方案:

* 当color.x < 0.02时直接跳过。
* 裁剪和反走样放到判断语句之后。

      " } else if (type == 3) {   // Textured tris\n"
      "#ifdef NANOVG_GL3\n"
      "   vec4 color = texture(tex, ftcoord);\n"
      "#else\n"
      "   vec4 color = texture2D(tex, ftcoord);\n"
      "#endif\n"
      "   if(color.x < 0.02) discard;\n"
      "   strokeAlpha = strokeMask();\n"
      "   if (strokeAlpha < strokeThr) discard;\n"
      "   float scissor = scissorMask(fpos);\n"
      "   color = vec4(color.x);"
      "   color *= scissor;\n"
      "   result = color * innerCol;\n"
      " }\n"

     

#### 效果:

字体渲染性能提高一倍!

### 三、反走样的问题

反走样的实现函数如下(其实我也不懂):

      "float strokeMask() {\n"
      "   return min(1.0, (1.0-abs(ftcoord.x*2.0-1.0))*strokeMult) * min(1.0, ftcoord.y);\n"
      "}\n"

#### 问题:

与简单的赋值操作相比,加上反走样功能,性能会下降5-10倍。但是不加反走样功能,绘制多边形时边缘效果比较差。不加不好看,加了又太慢,看起来是个两难的选择。

#### 方案:

矩形填充是可以不用反走样功能的。而90%以上的情况都是矩形填充。矩形填充单独处理,一条指令搞定,性能提高20倍以上:

      " if (type == 5) {    //fast fill color\n"
      "   result = innerCol;\n"

#### 效果:

配合裁剪和矩形的优化,性能提高10倍以上。

### 四、裁剪的问题

裁剪放到Shader中虽然合理,但是性能就要大大折扣了。

      "// Scissoring\n"
      "float scissorMask(vec2 p) {\n"
      "   vec2 sc = (abs((scissorMat * vec3(p,1.0)).xy) - scissorExt);\n"
      "   sc = vec2(0.5,0.5) - sc * scissorScale;\n"
      "   return clamp(sc.x,0.0,1.0) * clamp(sc.y,0.0,1.0);\n"
      "}\n"

#### 问题:
与简单的赋值操作相比,加上裁剪功能,性能会下降10以上倍。但是不加裁剪功能,像滚动视图这样的控件就没法实现,这看起来也是个两难的选择。

#### 方案:

而90%以上的填充都是在裁剪区域的内部的,没有必要每个像素都去判断,放在Shader之外进行判断即可。

static int glnvg__pathInScissor(const NVGpath* path, NVGscissor* scissor) {
  int32_t i = 0;
  float cx = scissor->xform[4];
  float cy = scissor->xform[5];
  float hw = scissor->extent[0];
  float hh = scissor->extent[1];

  float l = cx - hw;
  float t = cy - hh;
  float r = l + 2 * hw - 1;
  float b = t + 2 * hh - 1;

  const NVGvertex* verts = path->fill;
  for (i = 0; i < path->nfill; i++) {
    const NVGvertex* iter = verts + i;
    int x = iter->x;
    int y = iter->y;
    if (x < l || x > r || y < t || y > b) {
      return 0;
    }
  }

  return 1;
}

#### 效果:

配合裁剪和矩形的优化,性能提高10倍以上。

### 五、综合

综合裁剪、反走样和矩形,新增3个类型,进行特殊处理:

* 快速填充无需裁剪的矩形:NSVG\_SHADER\_FAST\_FILLCOLOR
* 快速填充无需裁剪的图片:NSVG\_SHADER\_FAST\_FILLIMG
* 快速用简单颜色填充多边形:NSVG\_SHADER\_FILLCOLOR
 
裁剪、反走样和矩形可以组合更多类型,进行更精细的优化。但即使只作这三种情况处理,[AWTK](https://github.com/zlgopen/awtk)在Android平台的整体性能已经有了3-5倍的提高,demoui在我们测试的机型上,都稳稳的保持在60FPS,没有必要为了性能增加它的复杂度了。

详细情况和完整代码请参考AWTK

离线

#2 2019-08-06 11:46:44

达克罗德
会员
注册时间: 2018-04-10
累计积分: 538

Re: NanoVG 优化笔记:性能提高5倍的秘密

nanovg能不能用在没有GPU的设备上?

离线

#3 2019-08-06 12:14:25

lixianjing
会员
注册时间: 2019-04-25
累计积分: 11

Re: NanoVG 优化笔记:性能提高5倍的秘密

达克罗德 说:

nanovg能不能用在没有GPU的设备上?

本来是不行的,我改造了一下,使用agge进行软件渲染。详情可以参考AWTK的代码。

离线

#4 2019-08-06 16:05:24

达克罗德
会员
注册时间: 2018-04-10
累计积分: 538

Re: NanoVG 优化笔记:性能提高5倍的秘密

lixianjing 说:
达克罗德 说:

nanovg能不能用在没有GPU的设备上?

本来是不行的,我改造了一下,使用agge进行软件渲染。详情可以参考AWTK的代码。

厉害

最近编辑记录 达克罗德 (2019-08-06 16:05:35)

离线

页脚