NanoVG Optimized Notes: The Secret of Five-fold Performance Improvement

Keywords: Android Fragment less C

NanoVG Optimized Notes

nanovg As its name suggests, it is a very compact Library of vector drawing functions. Compared with hundreds of thousands of lines of code in cairo and skia, nanovg is less than 5000 lines of C language code, which is also called nano. The design, interface and code quality of nanovg are exemplary. The only drawback is that the performance is not ideal. Especially on Android's low-end models and large screen models, a simple interface can only draw more than a dozen frames per second. Recently, I have AWTK When porting to Android, this embarrassing problem comes up.

After optimization, AWTK In low-end models, the overall rendering performance has been improved by three to five times. Here is a note for your friends in need.

The performance bottleneck of nanovg lies in fragment shader, which can be considered as a callback function for GPU. The callback function is called when processing every pixel and executes millions of times in every frame drawing. It can be seen that the function has a great impact on performance.

Let's first look at the fragment shader code for nanovg:

	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";

Its function is very complete and complex, cutting and anti-aliasing have been dealt with. After careful analysis, I found several performance problems:

I. The problem of color filling

Simple color filling and gradient color filling use the same code:

		"	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"

problem

Simple color filling requires only one instruction, while gradient color filling requires dozens of instructions. In both cases, reusing a piece of code slows down the filling of simple colors by more than 10 times.

programme

The color filling is divided into the following situations and optimized separately:

  • Rectangular simple color filling.

For rectangles that do not need to be trimmed (which is the most common case), direct assignment can improve performance by more than 20 times.

      " if (type == 5) {    //fast fill color\n"
      "   result = innerCol;\n"
  • General polygon simple color filling.

By removing the gradient sampling function, the performance will be more than doubled.

    " } 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"

  • Gradient color filling (only a tiny fraction).

This is a rare case, and it's still the code that used to be.

Effect:

On average, filling performance is increased by more than 10 times!

2. The Font Problem

For text, the average number of pixels to be displayed and those not to be displayed is about 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"

Questions:

If both the displayed and non-displayed pixels follow the complete process, half the time will be wasted.

Programme:

  • Skip color.x < 0.02 directly.
  • Cut and anti-aliasing after the judgement statement.
      " } 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"

Effect:

Twice the font rendering performance!

3. Anti-aliasing

The implementation function of anti-aliasing is as follows (I don't understand it either):

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

Questions:

Compared with the simple assignment operation, with the anti-aliasing function, the performance will be reduced by 5-10 times. But without anti-aliasing function, the edge effect of drawing polygon is poor. It seems like a dilemma to add it too slowly and not too ugly.

Programme:

Rectangular filling can avoid anti-aliasing function. More than 90% of the cases are rectangular filling. Rectangular filling is processed separately and one instruction is completed. The performance is improved by more than 20 times.

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

Effect:

With cutting and rectangular optimization, the performance can be improved more than 10 times.

IV. Tailoring

Although tailoring is reasonable in Shader, performance will be greatly discounted.

		"// 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"

Questions:

Compared with the simple assignment operation, with the clipping function, the performance will be reduced by more than 10 times. But without clipping, controls like scrolling views can't be implemented, which seems to be a dilemma.

Programme:

More than 90% of the filling is inside the clipping area, so it is not necessary to judge every pixel, but it can be judged outside 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;
}

Effect:

With cutting and rectangular optimization, the performance can be improved more than 10 times.

V. COMPREHENSION

Comprehensive tailoring, anti-aliasing and rectangular, three new types, special treatment:

  • Fast filling rectangle without clipping: NSVG_SHADER_FAST_FILLCOLOR
  • Quick Filling of Uncut Pictures: NSVG_SHADER_FAST_FILLIMG
  • Fast filling polygons with simple colors: NSVG_SHADER_FILLCOLOR

Cutting, anti-aliasing and rectangular can combine more types for more refined optimization. But even if only these three cases are dealt with, AWTK The overall performance of Android platform has been improved by 3-5 times. demoui is stable at 60FPS on the models we tested. There is no need to increase its complexity for performance.

For details and complete code, please refer to AWTK

Posted by Aaron_Escobar on Mon, 05 Aug 2019 20:58:04 -0700