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