NanoVG 優化筆記:性能提高5倍的祕密

NanoVG 優化筆記

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在Android平臺的整體性能已經有了3-5倍的提高,demoui在我們測試的機型上,都穩穩的保持在60FPS,沒有必要爲了性能增加它的複雜度了。

詳細情況和完整代碼請參考AWTK

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章