ARM NEON скининг

в 12:30, , рубрики: game development, iphone, xcode, метки: , , ,

Что это?

Что такое ARM NEON? – ARM® NEON™ это SIMD движок … – другими словами это расширенный набор инструкций наподобие x86 CPU SSE/SSE2 но для процессоров с ARM архитектурой.

Зачем?

Всё и так было хорошо пока я не добавил поддержку FSAA. После этого фпс просел ниже чем 15.
После оптимизации у меня опять было около 25 FPS. Но в памяти засела одна функция которая потребляла 10% времени на кадр в которой я уже не знал что можно оптимизировать.

Благодаря одному моему другу, который время от времени задавал вопрос типа «А не хочешь ли ты задействовать NEON в своем движке» я таки решился (с его поддержкой) переписать эту функцию на NEON.

Оригинальный код на C для скининга (Matrix palette skinnig).

Структуры:

// ready to use with glSubData for vertex buffer
struct PN
{
  Math::Vec3f p;
  Math::Vec3f n;
};
Трансформация одного веса:

forceinline
void transformPointNormal4x3Weight_NoW(const Matrix44f& mat,const Vec3f& inV, const Vec3f& inN, BaseRenderScene::PN& outPN)
{
  outPN.p.vec[0] = (inV.vec[0]*mat.mat[0][0] + inV.vec[1]*mat.mat[1][0] + inV.vec[2]*mat.mat[2][0] + mat.mat[3][0]);
  outPN.n.vec[0] = (inN.vec[0]*mat.mat[0][0] + inN.vec[1]*mat.mat[1][0] + inN.vec[2]*mat.mat[2][0]);

  outPN.p.vec[1] = (inV.vec[0]*mat.mat[0][1] + inV.vec[1]*mat.mat[1][1] + inV.vec[2]*mat.mat[2][1] + mat.mat[3][1]);
  outPN.n.vec[1] = (inN.vec[0]*mat.mat[0][1] + inN.vec[1]*mat.mat[1][1] + inN.vec[2]*mat.mat[2][1]);

  outPN.p.vec[2] = (inV.vec[0]*mat.mat[0][2] + inV.vec[1]*mat.mat[1][2] + inV.vec[2]*mat.mat[2][2] + mat.mat[3][2]);
  outPN.n.vec[2] = (inN.vec[0]*mat.mat[0][2] + inN.vec[1]*mat.mat[1][2] + inN.vec[2]*mat.mat[2][2]);
}

forceinline
void transformPointNormal4x3Weight(const Matrix44f& mat,const Vec3f& inV, const Vec3f& inN, BaseRenderScene::PN& outPN,float w )
{
  outPN.p.vec[0] = (inV.vec[0]*mat.mat[0][0] + inV.vec[1]*mat.mat[1][0] + inV.vec[2]*mat.mat[2][0] + mat.mat[3][0])*w;
  outPN.n.vec[0] = (inN.vec[0]*mat.mat[0][0] + inN.vec[1]*mat.mat[1][0] + inN.vec[2]*mat.mat[2][0])*w;

  outPN.p.vec[1] = (inV.vec[0]*mat.mat[0][1] + inV.vec[1]*mat.mat[1][1] + inV.vec[2]*mat.mat[2][1] + mat.mat[3][1])*w;
  outPN.n.vec[1] = (inN.vec[0]*mat.mat[0][1] + inN.vec[1]*mat.mat[1][1] + inN.vec[2]*mat.mat[2][1])*w;

  outPN.p.vec[2] = (inV.vec[0]*mat.mat[0][2] + inV.vec[1]*mat.mat[1][2] + inV.vec[2]*mat.mat[2][2] + mat.mat[3][2])*w;
  outPN.n.vec[2] = (inN.vec[0]*mat.mat[0][2] + inN.vec[1]*mat.mat[1][2] + inN.vec[2]*mat.mat[2][2])*w;
}

forceinline
void transformPointNormal4x3AddWeighted(const Matrix44f& mat,const Vec3f& inV, const Vec3f& inN, BaseRenderScene::PN& outPN,float w )
{
  outPN.p.vec[0] += (inV.vec[0]*mat.mat[0][0] + inV.vec[1]*mat.mat[1][0] + inV.vec[2]*mat.mat[2][0] + mat.mat[3][0])*w;
  outPN.n.vec[0] += (inN.vec[0]*mat.mat[0][0] + inN.vec[1]*mat.mat[1][0] + inN.vec[2]*mat.mat[2][0])*w;

  outPN.p.vec[1] += (inV.vec[0]*mat.mat[0][1] + inV.vec[1]*mat.mat[1][1] + inV.vec[2]*mat.mat[2][1] + mat.mat[3][1])*w;
  outPN.n.vec[1] += (inN.vec[0]*mat.mat[0][1] + inN.vec[1]*mat.mat[1][1] + inN.vec[2]*mat.mat[2][1])*w;

  outPN.p.vec[2] += (inV.vec[0]*mat.mat[0][2] + inV.vec[1]*mat.mat[1][2] + inV.vec[2]*mat.mat[2][2] + mat.mat[3][2])*w;
  outPN.n.vec[2] += (inN.vec[0]*mat.mat[0][2] + inN.vec[1]*mat.mat[1][2] + inN.vec[2]*mat.mat[2][2])*w;
}
Трансформация одного вертекса:

const Vec3f& vx = pVerticies[v];
const Vec3f& vxN = pNormals[v];

float w = pVertexWeight[v].vec[0];
int boneIndex = pVertexBones[v].vec[0];
const Matrix44f& boneTM = pBoneTMList[boneIndex];
if( wCount==1 )
{
  transformPointNormal4x3Weight_NoW(boneTM,vx,vxN,skinTempPN[v]);
}
else
{
  // 1st vertex without add
  transformPointNormal4x3Weight_N(boneTM,vx,vxN,skinTempPN[v],w);
  for(size_t i=1;i<wCount;i++)
  {
   // other verticies
   w = pVertexWeight[v].vec[i];
   boneIndex = pVertexBones[v].vec[i];
   const Matrix44f& boneTM = pBoneTMList[boneIndex];
   transformPointNormal4x3AddWeighted_N(boneTM,vx,vxN,skinTempPN[v],w);
  }
}

Знающий человек сразу заметит, что я храню количество не нулевых весов для каждого вертекса. В моем случае около 30% вершин были с одним весом, что позволило выиграть немножко времени.

ASM with NEON(xCode style)

Код ниже это asm/С код оптимизированный с использованием ARM NEON.
Несколько ньюансов:

  • Для ARM NEON все входящие данный должны быть выровнены на 16 байт. Из за этого требования я все свои входящие позиции и нормали расширил на Vec4f.
  • Исходящие данные все еще могут быть выровнены по 4 байта. Это позволило мне заливать результат напрямую в вертекс буфер без лишних телодвижений. Для варианта с выровненными данными по 16 байт пришлось бы падить данные лишними 4+4 байтами и гонять их в вертекс буффер (а это происходит на каждом кадре).
#if defined(__ARM_NEON__)
#define USE_NEON
#endif

#if defined(USE_NEON)


#ifdef __thumb__
#error "This file should be compiled in ARM mode only."
// Note in Xcode, right click file, Get Info->Build, Other compiler flags = "-marm"
#endif

#define OP  "q0"

#define OPS0  "s0"
#define OPS1  "s1"
#define OPS2  "s2"

#define ON  "q1"

#define ONS0  "s4"
#define ONS1  "s5"
#define ONS2  "s6"


#define IP  "q2"
#define IN  "q3"

#define IPX "d4[0]"
#define IPY "d4[1]"
#define IPZ "d5[0]"
#define IPW "d5[1]"

#define INX "d6[0]"
#define INY "d6[1]"
#define INZ "d7[0]"
#define INW "d7[1]"

#define WQ "q4"
#define W0D "d8[0]"
#define W1D "d8[1]"
#define W2D "d9[0]"
#define W3D "d9[1]"

#define QM0 q8
#define QM1 q9
#define QM2 q10
#define QM3 q11

#define QT  "q14"


// outP = mt.row0*pos + mt.row1*pos + mt.row2*pos + mt.row3*pos
#define mat_pos(_RES) 
"vmul.f32 " _RES ", q8, " IPX "nt" 
"vmla.f32 " _RES ", q9, " IPY "nt" 
"vmla.f32 " _RES ", q10, " IPZ "nt" 
"vmla.f32 " _RES ", q11, " IPW "nt"

#define mat_pos_w_set(_RES,_QT,_WD) 
mat_pos(_QT) 
"vmul.f32 " _RES ", " _QT ", " _WD "nt"

#define mat_pos_w_add(_RES,_QT,_WD) 
mat_pos(_QT) 
"vmla.f32 " _RES ", " _QT ", " _WD "nt"


// outN = mt.row0*nor + mt.row1*nor + mt.row2*nor
#define mat_nor(_RES) 
"vmul.f32 " _RES ", q8, " INX "nt" 
"vmla.f32 " _RES ", q9, " INY "nt" 
"vmla.f32 " _RES ", q10, " INZ "nt"

#define mat_nor_w_set(_RES,_QT,_WD) 
mat_nor(_QT) 
"vmul.f32 " _RES ", " _QT ", " _WD "nt"

#define mat_nor_w_add(_RES,_QT,_WD) 
mat_nor(_QT) 
"vmla.f32 " _RES ", " _QT ", " _WD "nt"

#define STORE3_P3N3(_R) 
"fsts "OPS0",[" _R "] nt" 
"fsts "OPS1",[" _R ",#4] nt" 
"fsts "OPS2",[" _R ",#8] nt" 
"fsts "ONS0",[" _R ",#12] nt" 
"fsts "ONS1",[" _R ",#16] nt" 
"fsts "ONS2",[" _R ",#20] nt"


#define mat_load(_R) 
"vldmia " _R ", { q8-q11 } nt"

__attribute__((always_inline))
void clalcSkin1(
                const Matrix44f* mat0,
                const Vec4f* posnorm,
                Vec3f* outPN)
{
  //
  asm volatile
  (
   // q4-q7 need to be preserved
   "vldmia %1, { " IP " - " IN " } nt"        // pos norm
   // OP p temp
   // ON n temp
   //
   // mat0
   mat_load("%0")
   mat_pos(OP)
   mat_nor(ON)
   STORE3_P3N3("%2")
   : // no output
   : "r" (mat0), "r" (posnorm), "r" (outPN)
   : "memory", IP, IN, WQ, QT, OP, ON, "q8", "q9", "q10", "q11" //clobber
   );
}

__attribute__((always_inline))
void clalcSkin2(
                const Matrix44f* mat0,
                const Matrix44f* mat1,
                const Vec4f* posnorm,
                const Vec4f* weight,
                Vec3f* outPN)
{
  //
  asm volatile
  (
   // q4-q7 need to be preserved
   "vmov q15," WQ "nt"
   //
   "vldmia %2, { " IP " - " IN " } nt"        // pos norm
   "vldmia %3, { " WQ " } nt"     // weights
   // QT intermediate temp
   // OP p temp
   // ON n temp
   //
   // mat0
   mat_load("%0")
   mat_pos_w_set(OP,QT,W0D)
   mat_nor_w_set(ON,QT,W0D)
   // mat 1
   mat_load("%1")
   mat_pos_w_add(OP,QT,W1D)
   mat_nor_w_add(ON,QT,W1D)
   // output pos3f,norm3f
   STORE3_P3N3("%4")
   // restore q4 (WQ)
   "vmov " WQ ", q15 nt"
   : // no output
   : "r" (mat0), "r" (mat1), "r" (posnorm), "r" (weight), "r" (outPN)
   : "memory", IP, IN, WQ, QT , OP, ON, "q8", "q9", "q10", "q11", "q15" //clobber
   );
}

__attribute__((always_inline))
void clalcSkin3(
                const Matrix44f* mat0,
                const Matrix44f* mat1,
                const Matrix44f* mat2,
                const Vec4f* posnorm,
                const Vec4f* weight,
                Vec3f* outPN)
{
  //
  asm volatile
  (
   // q4-q7 need to be preserved
   "vmov q15," WQ "nt"
   //
   "vldmia %3, { " IP " - " IN " } nt"        // pos norm
   "vldmia %4, { " WQ " } nt"     // weights
   // QT intermediate temp
   // OP p temp
   // ON n temp
   //
   // mat0
   mat_load("%0")
   mat_pos_w_set(OP,QT,W0D)
   mat_nor_w_set(ON,QT,W0D)
   // mat 1
   mat_load("%1")
   mat_pos_w_add(OP,QT,W1D)
   mat_nor_w_add(ON,QT,W1D)
   // mat 2
   mat_load("%2")
   mat_pos_w_add(OP,QT,W2D)
   mat_nor_w_add(ON,QT,W2D)
   // output pos,normal
   STORE3_P3N3("%5")
   
   // restore q4 (WQ)
   "vmov " WQ ", q15 nt"
   
   : // no output
   : "r" (mat0), "r" (mat1), "r" (mat2),"r" (posnorm), "r" (weight), "r" (outPN)
   : "memory", IP, IN, WQ, QT, OP, ON, "q8", "q9", "q10", "q11", "q15" //clobber
   );
}
__attribute__((always_inline))
void clalcSkin4(
                const Matrix44f* mat0,
                const Matrix44f* mat1,
                const Matrix44f* mat2,
                const Matrix44f* mat3,
                const Vec4f* posnorm,
                const Vec4f* weight,
                Vec3f* outPN)
{
  //
  asm volatile
  (
   // q4-q7 need to be preserved
   "vmov q15," WQ "nt"
   //
   "vldmia %4, { " IP " - " IN " } nt"        // pos norm
   "vldmia %5, { " WQ " } nt"     // weights
   // QT intermediate temp
   // OP p temp
   // ON n temp
   //
   // mat0
   mat_load("%0")
   mat_pos_w_set(OP,QT,W0D)
   mat_nor_w_set(ON,QT,W0D)
   // mat 1
   mat_load("%1")
   mat_pos_w_add(OP,QT,W1D)
   mat_nor_w_add(ON,QT,W1D)
   // mat 2
   mat_load("%2")
   mat_pos_w_add(OP,QT,W2D)
   mat_nor_w_add(ON,QT,W2D)
   // mat 3
   mat_load("%3")
   mat_pos_w_add(OP,QT,W3D)
   mat_nor_w_add(ON,QT,W3D)
   // output pos,normal
   STORE3_P3N3("%6")
   
   // restore q4 (WQ)
   "vmov " WQ ", q15nt"
   
   : // no output
   : "r" (mat0), "r" (mat1), "r" (mat2), "r" (mat3), "r" (posnorm), "r" (weight), "r" (outPN)
   : "memory", IP, IN, WQ, QT, OP, ON, "q8", "q9", "q10", "q11", "q15" //clobber
   );
}

Результаты

Я не делал никаких синтетических тестов — все проверял на рабочем проекте.

502ms(c++) против 307ms(arm neon) на ~10 секундном интервале для iPhone 4 (на 39% быстрее чем на С).

Вопросы-Ответы

Попытаюсь ответить сразу на несколько вопросов?

Q: А почему не описано как работает и что такое ARM NEON?
A: Пересказывать спеки смысла нет.

Q: А почему не использовать шейдера?
A: OpenGL 1.1

Q: А почему не использовать OpenGL 2.0+?
A: Только после портации на Windows Phone 8 (там нет FF и как раз в этот момент я допишу «шейдерность» в движок и потом уже и на GL 2.0).

Q: А почему не использовать GL_OES_matrix_palette для FF?
A: Надо бить модель на группы по 11 (для iphone) матриц и на это нет времени — возможно в будущем.

Q: А где можно узнать больше и желательно с примерами?
A: Я советую посмотреть Тут. Осторожно там LGPL.

Q: А сколько это заняло?
A: Неделю — именно это и есть причина написания статьи (если кому то сэкономит время я буду счастлив).

Q: Я ничего не понял, а можно по подробнее?
A: Можно (зависит от комментариев), но я старался написать очень понятный код.

PS. Ошибки в личку.

Автор: PavloG

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js