Vertex डेटा मैनेजमेंट

अच्छे शीर्ष डेटा लेआउट और संपीड़न किसी भी ग्राफ़िकल ऐप्लिकेशन के प्रदर्शन के लिए अत्यंत ज़रूरी है, चाहे कोई ऐप्लिकेशन 2D उपयोगकर्ता इंटरफ़ेस से बना हो या कोई बड़ा 3D ओपन वर्ल्ड गेम हो. दर्जनों लोकप्रिय Android गेम पर Android जीपीयू इंस्पेक्टर के फ़्रेम प्रोफ़ाइलर की इंटरनल टेस्टिंग से पता चलता है कि वर्टेक्स डेटा मैनेजमेंट को बेहतर बनाने के लिए, बहुत कुछ किया जा सकता है. हमने देखा है कि वर्टेक्स डेटा के लिए, पूरी सटीक जानकारी, सभी वर्टेक्स एट्रिब्यूट के लिए 32-बिट फ़्लोट वैल्यू, और वर्टेक्स बफ़र लेआउट का इस्तेमाल करना आम बात है. यह ऐसे वर्टेक्स बफ़र लेआउट का इस्तेमाल करता है जो पूरी तरह से इंटरलीव्ड एट्रिब्यूट के साथ फ़ॉर्मैट किए गए स्ट्रक्चर के कलेक्शन का इस्तेमाल करता है.

इस लेख में बताया गया है कि नीचे दी गई तकनीकों का इस्तेमाल करके, अपने Android ऐप्लिकेशन के ग्राफ़िक परफ़ॉर्मेंस को कैसे ऑप्टिमाइज़ किया जा सकता है:

  • वर्टेक्स कंप्रेशन
  • Vertex स्ट्रीम को समय के हिसाब से बांटें

इन तकनीकों को लागू करने से वर्टेक्स मेमोरी बैंडविड्थ के इस्तेमाल को 50% तक बेहतर बनाया जा सकता है, सीपीयू से मेमोरी बस के विवाद को कम किया जा सकता है, सिस्टम मेमोरी पर स्टॉल कम किए जा सकते हैं, और बैटरी लाइफ़ बेहतर हो सकती है; ये सभी बातें डेवलपर और असली उपयोगकर्ताओं, दोनों के लिए फ़ायदेमंद हैं!

यहां दिखाया गया सारा डेटा, स्टैटिक सीन का उदाहरण है. इसमें Pixel 4 पर चलने वाले करीब 1,90,00,000 वर्टेक्स शामिल हैं:

छह रिंग और 19 मीटर के वर्टेक्स वाले सैंपल सीन

इमेज 1: छह अंगूठियों और 19 मीटर के वर्टेक्स वाले सैंपल सीन

वर्टेक्स कंप्रेशन

वर्टेक्स कंप्रेशन, कम नुकसान वाली कंप्रेशन तकनीकों के लिए इस्तेमाल किया जाने वाला आम तौर पर इस्तेमाल किया जाने वाला शब्द है रनटाइम और स्टोरेज, दोनों के दौरान वर्टेक्स डेटा का साइज़ कम करने के लिए, कुशल पैकिंग का इस्तेमाल करें. वर्टेक्स का साइज़ कम करने के कई फ़ायदे हैं. इनमें जीपीयू पर मेमोरी बैंडविथ कम करना (बैंडविड्थ के लिए कंप्यूट करके, कैश मेमोरी का बेहतर तरीके से इस्तेमाल करना), और हो सकता है कि स्पिलिंग रजिस्टर होने का जोखिम कम हो.

वर्टेक्स कंप्रेशन के सामान्य तरीकों में ये शामिल हैं:

  • वर्टेक्स डेटा एट्रिब्यूट की न्यूमेरिक (संख्या वाली) सटीक जानकारी को कम करना (उदाहरण: 32-बिट फ़्लोट से 16-बिट फ़्लोट)
  • अलग-अलग फ़ॉर्मैट में एट्रिब्यूट दिखाना

उदाहरण के लिए, अगर कोई वर्टेक्स, पोज़िशन (vec3), सामान्य (vec3), और टेक्सचर कोऑर्डिनेट (vec2) के लिए पूरे 32-बिट फ़्लोट का इस्तेमाल करता है, तो इन सभी को 16-बिट फ़्लोट से बदलने पर वर्टेक्स का साइज़ 50% (औसत 32 बाइट वर्टेक्स पर 16 बाइट) कम हो जाएगा.

वर्टेक्स पोज़िशन

वर्टेक्स पोज़िशन डेटा को ज़्यादातर मेश में, पूरी तरह सटीक 32-बिट फ़्लोटिंग पॉइंट वैल्यू से आधा सटीक 16-बिट फ़्लोटिंग पॉइंट वैल्यू तक कंप्रेस किया जा सकता है. साथ ही, हाफ़ फ़्लोट करीब सभी मोबाइल डिवाइसों पर हार्डवेयर में काम करते हैं. float32 से float16 तक जाने वाला कन्वर्ज़न फ़ंक्शन, कुछ ऐसा दिखता है (इस गाइड से लिया गया):

uint16_t f32_to_f16(float f) {
  uint32_t x = (uint32_t)f;
  uint32_t sign = (unsigned short)(x >> 31);
  uint32_t mantissa;
  uint32_t exp;
  uint16_t hf;

  mantissa = x & ((1 << 23) - 1);
  exp = x & (0xFF << 23);
  if (exp >= 0x47800000) {
    // check if the original number is a NaN
    if (mantissa && (exp == (0xFF << 23))) {
      // single precision NaN
      mantissa = (1 << 23) - 1;
    } else {
      // half-float will be Inf
      mantissa = 0;
    }
    hf = (((uint16_t)sign) << 15) | (uint16_t)((0x1F << 10)) |
         (uint16_t)(mantissa >> 13);
  }
  // check if exponent is <= -15
  else if (exp <= 0x38000000) {
    hf = 0;  // too small to be represented
  } else {
    hf = (((uint16_t)sign) << 15) | (uint16_t)((exp - 0x38000000) >> 13) |
         (uint16_t)(mantissa >> 13);
  }

  return hf;
}

इस तरीके की एक सीमा है; जब वर्टेक्स मूल से दूर की ओर बढ़ता है, तो वह सटीक तरीके से कम हो जाता है. इस वजह से, यह जगह के हिसाब से बहुत बड़े जालीदार (ऐसे वर्टेक्स जिनके एलिमेंट 1024 से ज़्यादा होते हैं) के लिए सही नहीं रहते. इसे ठीक करने के लिए, मेश को छोटे-छोटे टुकड़ों में बांटें, हर हिस्से को मॉडल के ऑरिजिन के बीच में रखें, और स्केल करें, ताकि हर हिस्से के सभी वर्टेक्स [-1, 1] रेंज में फ़िट हो सकें. इस रेंज में फ़्लोटिंग पॉइंट वैल्यू की सटीक जानकारी होती है. कंप्रेशन के लिए स्यूडोकोड इस तरह दिखता है:

for each position p in Mesh:
   p -= center_of_bounding_box // Moves Mesh back to the center of model space
   p /= half_size_bounding_box // Fits the mesh into a [-1, 1] cube
   vec3<float16> result = vec3(f32_to_f16(p.x), f32_to_f16(p.y), f32_to_f16(p.z));

आपको रेंडर करते समय, वर्टेक्स डेटा को डीकंप्रेस करने के लिए, स्केल फ़ैक्टर और अनुवाद को मॉडल मैट्रिक्स में बेक करना होता है. ध्यान रखें कि आप मानदंडों को बदलने के लिए इस समान मॉडल मैट्रिक्स का उपयोग नहीं करना चाहते, क्योंकि उन पर वही कंप्रेशन लागू नहीं किया गया है. आपको नॉर्मल वैल्यू के लिए इन डीकंप्रेशन ट्रांसफ़ॉर्मेशन के बिना मैट्रिक्स की ज़रूरत होगी या फिर बेस मॉडल मैट्रिक्स का इस्तेमाल किया जा सकता है (इसका इस्तेमाल नॉर्मल के लिए किया जा सकता है) और फिर शेडर के अंदर मॉडल मैट्रिक्स में अतिरिक्त डीकंप्रेशन ट्रांसफ़ॉर्मेशन ऐक्शन लागू करें. एक उदाहरण:

vec3 in in_pos;

void main() {
   ...
   // bounding box data packed into uniform buffer
   vec3 decompress_pos = in_pos * half_size_bounding_box + center_of_bounding_box;
   gl_Position = proj * view * model * decompress_pos;
}

दूसरा तरीका साइन्ड नॉर्मलाइज़्ड इंटीजर (SNORM) का इस्तेमाल करना है. SNORM डेटा टाइप में [-1, 1] के बीच की वैल्यू दिखाने के लिए, फ़्लोटिंग पॉइंट के बजाय पूर्णांक का इस्तेमाल किया जाता है. पोज़िशन के लिए 16-बिट SNORM का इस्तेमाल करने पर, आपको float16 जितनी ही मेमोरी की बचत करने में मदद मिलती है. साथ ही, गैर-एक जैसे डिस्ट्रिब्यूशन की कोई कमी भी नहीं आती. SNORM का इस्तेमाल करने के लिए, हमारा सुझाव है कि इसे लागू करने का तरीका कुछ ऐसा दिखता है:

const int BITS = 16

for each position p in Mesh:
   p -= center_of_bounding_box // Moves Mesh back to the center of model space
   p /= half_size_bounding_box // Fits the mesh into a [-1, 1] cube
   // float to integer value conversion
   p = clamp(p * (2^(BITS - 1) - 1), -2^(BITS - 1), 2^(BITS - 1) - 1) 
फ़ॉर्मैट करें साइज़
पहले vec4<float32> 16 बाइट
बाद में Vc3<float16/SNORM16> 6 बाइट

वर्टेक्स नॉर्मल और टैंजेंट स्पेस

वर्टेक्स नॉर्मल को रोशनी के लिए ज़रूरी है और सामान्य मैपिंग जैसी ज़्यादा मुश्किल तकनीकों के लिए टैंजेंट स्पेस की ज़रूरत होती है.

टैनजेंट स्पेस

टैंजेंट स्पेस एक कॉर्डिनेट सिस्टम होता है, जहां हर वर्टेक्स में सामान्य, टैंजेंट, और बिटेंजेंट वेक्टर होता है. ये तीनों सदिश, आम तौर पर एक-दूसरे से समकोणीय होते हैं. इसलिए, हमें उनमें से सिर्फ़ दो को संग्रहित करना होगा और वर्टेक्स शेडर में, अन्य दो का क्रॉस गुणनफल लेकर तीसरे की गणना कर सकते हैं.

आम तौर पर, इन वेक्टर को 16-बिट फ़्लोट का इस्तेमाल करके दिखाया जा सकता है. साथ ही, विज़ुअल फ़िडेलिटी में कोई नुकसान नहीं होता. इसलिए, शुरुआत करने के लिए यह सही जगह है!

हम QTangents नाम की एक तकनीक की मदद से और भी ज़्यादा कंप्रेस कर सकते हैं, जो पूरे टैंजेंट स्पेस को एक क्वाटरनियन स्टोर में सेव करती है. क्वाटर्नियन का इस्तेमाल रोटेशन का प्रतिनिधित्व करने के लिए किया जा सकता है, इसलिए टैंजेंट स्पेस वेक्टर को रोटेशन का प्रतिनिधित्व करने वाले 3x3 मैट्रिक्स के कॉलम वेक्टर के तौर पर मानकर क्वाटर्नियन (इस मामले में मॉडल स्पेस से टैंजेंट स्पेस में) को हम इन दोनों के बीच कन्वर्ट कर सकते हैं! क्वाटर्नियन को vec4 डेटा के मुताबिक माना जा सकता है. ऊपर लिंक किए गए पेपर के आधार पर और यहां लागू किए गए तरीके से टैंजेंट स्पेस वेक्टर से क्यूटैंजेंट में बदलने को इस तरह समझा जा सकता है:

const int BITS = 16

quaternion tangent_space_to_quat(vec3 normal, vec3 tangent, vec3 bitangent) {
   mat3 tbn = {normal, tangent, bitangent};
   quaternion qTangent(tbn);
   qTangent.normalize();

   //Make sure QTangent is always positive
   if (qTangent.w < 0)
       qTangent = -qTangent;

   const float bias = 1.0 / (2^(BITS - 1) - 1);

   //Because '-0' sign information is lost when using integers,
   //we need to apply a "bias"; while making sure the Quaternion
   //stays normalized.
   // ** Also our shaders assume qTangent.w is never 0. **
   if (qTangent.w < bias) {
       Real normFactor = Math::Sqrt( 1 - bias * bias );
       qTangent.w = bias;
       qTangent.x *= normFactor;
       qTangent.y *= normFactor;
       qTangent.z *= normFactor;
   }

   //If it's reflected, then make sure .w is negative.
   vec3 naturalBinormal = cross_product(tangent, normal);
   if (dot_product(naturalBinormal, binormal) <= 0)
       qTangent = -qTangent;
   return qTangent;
}

क्वाटर्नियन को नॉर्मलाइज़ किया जाएगा और इसे SNORMs का इस्तेमाल करके कंप्रेस किया जा सकेगा. 16-बिट SNORMs से, स्टोरेज की साफ़-साफ़ जानकारी मिलती है और मेमोरी की बचत होती है. 8-बिट SNORMs से और भी ज़्यादा बचत हो सकती है, लेकिन इसकी वजह से बहुत ज़्यादा स्पेक्ट्रल मटीरियल में आर्टफ़ैक्ट भी पैदा हो सकते हैं. दोनों को आज़माएं और देखें कि आपके एसेट के लिए कौनसा तरीका सबसे सही है! क्वाटर्नियन को कोड में बदलने का तरीका ऐसा दिखता है:

for each vertex v in mesh:
   quaternion res = tangent_space_to_quat(v.normal, v.tangent, v.bitangent);
   // Once we have the quaternion we can compress it
   res = clamp(res * (2^(BITS - 1) - 1), -2^(BITS - 1), 2^(BITS - 1) - 1);

वर्टेक्स शेडर में क्वाटर्नियन को डिकोड करने के लिए (यहां से ऑप्टिमाइज़ किया गया):

vec3 xAxis( vec4 qQuat )
{
  float fTy  = 2.0 * qQuat.y;
  float fTz  = 2.0 * qQuat.z;
  float fTwy = fTy * qQuat.w;
  float fTwz = fTz * qQuat.w;
  float fTxy = fTy * qQuat.x;
  float fTxz = fTz * qQuat.x;
  float fTyy = fTy * qQuat.y;
  float fTzz = fTz * qQuat.z;

  return vec3( 1.0-(fTyy+fTzz), fTxy+fTwz, fTxz-fTwy );
}

vec3 yAxis( vec4 qQuat )
{
  float fTx  = 2.0 * qQuat.x;
  float fTy  = 2.0 * qQuat.y;
  float fTz  = 2.0 * qQuat.z;
  float fTwx = fTx * qQuat.w;
  float fTwz = fTz * qQuat.w;
  float fTxx = fTx * qQuat.x;
  float fTxy = fTy * qQuat.x;
  float fTyz = fTz * qQuat.y;
  float fTzz = fTz * qQuat.z;

  return vec3( fTxy-fTwz, 1.0-(fTxx+fTzz), fTyz+fTwx );
}

void main() {
  vec4 qtangent = normalize(in_qtangent); //Needed because 16-bit quantization
  vec3 normal = xAxis(qtangent);
  vec3 tangent = yAxis(qtangent);
  float biNormalReflection = sign(in_qtangent.w); //ensured qtangent.w != 0
  vec3 binormal = cross(normal, tangent) * biNormalReflection;
  ...
}
फ़ॉर्मैट करें साइज़
पहले Vc3<float32> + vec3<float32> + vec3<float32> 36 बाइट
बाद में vec4<SNORM16> 8 बाइट

सिर्फ़ नॉर्मल

अगर आपको सिर्फ़ सामान्य वेक्टर को स्टोर करने की ज़रूरत है, तो एक अलग तरीका है जिससे ज़्यादा बचत हो सकती है - सामान्य वेक्टर को कंप्रेस करने के लिए कार्टिज़न कोऑर्डिनेट के बजाय यूनिट वेक्टर की ऑक्टाहेड्रल मैपिंग का इस्तेमाल करना. ऑक्टाहेड्रल मैपिंग के तहत, यूनिट स्फ़ीयर को ऑक्टाहेड्रॉन पर प्रोजेक्ट किया जाता है और फिर ऑक्टाहेड्रॉन को 2D प्लेन में प्रोजेक्ट किया जाता है. इसका नतीजा यह होता है कि आप किसी भी सामान्य सदिश को सिर्फ़ दो संख्याओं का उपयोग करके प्रस्तुत कर सकते हैं. इन दो संख्याओं को बनावट निर्देशांक माना जा सकता है जिनका इस्तेमाल हम उस 2D प्लेन का "सैंपल" करने के लिए करते हैं जिस पर हमने स्फ़ीयर का अनुमान लगाया है. इससे हमें ओरिजनल वेक्टर को वापस पाने में मदद मिलती है. इसके बाद, इन दोनों नंबरों को SNORM8 में सेव किया जा सकता है.

यूनिट स्फ़ीयर को ऑक्टाहेड्रॉन पर प्रोजेक्ट करना और ऑक्टाहेड्रॉन को 2D प्लेन पर प्रक्षेप करना

इमेज 2: ऑक्टाहेड्रल मैपिंग को विज़ुअलाइज़ किया गया (सोर्स)

const int BITS = 8

// Assumes the vector is unit length
// sign() function should return positive for 0
for each normal n in mesh:
  float invL1Norm = 1.0 / (abs(n.x) + abs(n.y) + abs(n.z));
  vec2 res;
  if (n.z < 0.0) {
    res.x = (1.0 - abs(n.y * invL1Norm)) * sign(n.x);
    res.y = (1.0 - abs(n.x * invL1Norm)) * sign(n.y);
  } else {
    res.x = n.x * invL1Norm;
    res.y = n.y * invL1Norm;
  }
  res = clamp(res * (2^(BITS - 1) - 1), -2^(BITS - 1), 2^(BITS - 1) - 1)

वर्टेक्स शेडर में डिकंप्रेशन (वापस कार्टेशियन कोऑर्डिनेट में बदलने के लिए) सस्ता होता है; ज़्यादातर आधुनिक मोबाइल डिवाइसों के मामले में, इस तकनीक को लागू करते समय हमें प्रदर्शन में कोई बड़ी गिरावट नहीं दिखाई दी. वर्टेक्स शेडर में डिकंप्रेशन:

//Additional Optimization: twitter.com/Stubbesaurus/status/937994790553227264
vec3 oct_to_vec(vec2 e):
  vec3 v = vec3(e.xy, 1.0 - abs(e.x) - abs(e.y));
  float t = max(-v.z, 0.0);
  v.xy += t * -sign(v.xy);
  return v;

इस तरीके का इस्तेमाल, पूरे टैंजेंट स्पेस को स्टोर करने के लिए भी किया जा सकता है. इसके लिए, इस तकनीक से vec2 की मदद से, नॉर्मल और टैंजेंट वेक्टर को स्टोर किया जा सकता है<SNORM8> हालांकि, आपको बिटेंजेंट की दिशा सेव करने का तरीका खोजना होगा. यह उस सामान्य स्थिति के लिए ज़रूरी है जहां आपने मॉडल के यूवी कोऑर्डिनेट को मिरर किया है. इसे लागू करने का एक तरीका यह है कि अपने टैंजेंट वेक्टर एन्कोडिंग के किसी कॉम्पोनेंट को हमेशा पॉज़िटिव रहने के लिए मैप करें. इसके बाद, अगर आपको बिटेंजेंट की दिशा को फ़्लिप करना है और वर्टेक्स शेडर में उसकी जांच करनी है, तो इसके साइन को फ़्लिप करें:

const int BITS = 8
const float bias = 1.0 / (2^(BITS - 1) - 1)

// Compressing
for each normal n in mesh:
  //encode to octahedron, result in range [-1, 1]
  vec2 res = vec_to_oct(n);

  // map y to always be positive
  res.y = res.y * 0.5 + 0.5;

  // add a bias so that y is never 0 (sign in the vertex shader)
  if (res.y < bias)
    res.y = bias;

  // Apply the sign of the binormal to y, which was computed elsewhere
  if (binormal_sign < 0)
    res.y *= -1;

  res = clamp(res * (2^(BITS - 1) - 1), -2^(BITS - 1), 2^(BITS - 1) - 1)
// Vertex shader decompression
vec2 encode = vec2(tangent_encoded.x, abs(tangent_encoded.y) * 2.0 - 1.0));
vec3 tangent_real = oct_to_vec3(encode);
float binormal_sign = sign(tangent_encode.y);
फ़ॉर्मैट करें साइज़
पहले Vc3<float32> 12 बाइट
बाद में दस्तावेज़ 2<SNORM8> 2 बाइट

वर्टेक्स यूवी कोऑर्डिनेट

यूवी कोऑर्डिनेट को टेक्सचर मैपिंग (अन्य चीज़ों के साथ) के लिए इस्तेमाल किया जाता है. इन्हें आम तौर पर, 32 बिट फ़्लोट का इस्तेमाल करके स्टोर किया जाता है. उन्हें 16 बिट फ़्लोट के साथ कंप्रेस करने से 1024x1024 से बड़े टेक्सचर के लिए सटीक समस्याएं आती हैं; [0.5, 1.0] के बीच फ़्लोटिंग-पॉइंट सटीक होने का मतलब है कि वैल्यू 1 पिक्सल से ज़्यादा बढ़ जाएंगी!

साइन न किए गए नॉर्मलाइज़्ड इंटीजर (UNORM) का इस्तेमाल करना बेहतर तरीका है. खास तौर पर, UNORM16; यह 65536x65536 तक की बनावट को सपोर्ट करते हुए पूरी टेक्सचर कोऑर्डिनेट रेंज में एक समान डिस्ट्रिब्यूशन देता है! इसमें माना जाता है कि टेक्सचर कोऑर्डिनेट हर एलिमेंट के लिए [0.0, 1.0] की रेंज में हैं. ऐसा हो सकता है कि मेश को देखकर ऐसा न हो (उदाहरण के लिए, दीवार पर 1.0 से आगे जाने वाले रैपिंग टेक्सचर कोऑर्डिनेट का इस्तेमाल किया जा सकता है). इसलिए, इस तकनीक को देखते समय इस बात का ध्यान रखें. कन्वर्ज़न फ़ंक्शन कुछ ऐसा दिखेगा:

const int BITS = 16

for each vertex_uv V in mesh:
  V *= clamp(2^BITS - 1, 0, 2^BITS - 1);  // float to integer value conversion
फ़ॉर्मैट करें साइज़
पहले दस्तावेज़ 2<float32> 8 बाइट
बाद में दस्तावेज़ 2<UNORM16> 4 बाइट

Vertex कंप्रेशन के नतीजे

वर्टेक्स कंप्रेशन की इन तकनीकों से वर्टेक्स मेमोरी स्टोरेज में 66% की कमी आई और यह 48 बाइट से घटकर 16 बाइट हो गया. इसने खुद को इस तरह पेश किया:

  • Vertex Memory रीड बैंडविथ:
    • बिनिंग: 27GB/s से 9GB/s
    • रेंडरिंग: 4.5B/s से 1.5GB/s
  • Vertex फे़च स्टॉल:
    • बिनिंग: 50% से 0%
    • रेंडरिंग: 90% से 90%
  • औसत बाइट/वर्टेक्स:
    • बिनिंग: 48B से 16B
    • रेंडरिंग: 52B से 18B

बिना कंप्रेस किए हुए वर्टेक्स का Android जीपीयू इंस्पेक्टर व्यू

इमेज 3: बिना कंप्रेस किए हुए वर्टेक्स का Android जीपीयू इंस्पेक्टर व्यू

कंप्रेस किए गए वर्टेक्स का Android जीपीयू इंस्पेक्टर व्यू

इमेज 4: कंप्रेस किए गए वर्टिकल का Android जीपीयू इंस्पेक्टर व्यू

Vertex स्ट्रीम को समय के हिसाब से बांटें

Vertex Stream विभाजन, वर्टेक्स बफ़र में डेटा के संगठन को ऑप्टिमाइज़ करता है. यह कैश मेमोरी की परफ़ॉर्मेंस को ऑप्टिमाइज़ करने का एक तरीका है. इससे आम तौर पर Android डिवाइसों में मिलने वाले टाइल-आधारित जीपीयू पर असर पड़ता है. खास तौर पर, यह रेंडरिंग प्रोसेस के बिनिंग चरण के दौरान.

टाइल-आधारित जीपीयू एक शेडर बनाते हैं, जो बिनिंग करने के लिए दिए गए वर्टेक्स शेडर के आधार पर, नॉर्मलाइज़ किए गए डिवाइस कोऑर्डिनेट को कैलकुलेट करता है. इसे सीन के हर वर्टेक्स पर सबसे पहले चलाया जाता है, चाहे वह दिख रहा हो या नहीं. इसलिए, मेमोरी में वर्टेक्स पोज़िशन डेटा को आस-पास रखना. अन्य जगहों पर वर्टेक्स स्ट्रीम लेआउट, शैडो पास के लिए फ़ायदेमंद हो सकता है. आम तौर पर, आपको शैडो पास के लिए सिर्फ़ पोज़िशन डेटा की ज़रूरत होती है. साथ ही, डेप्थ प्रीपास की ज़रूरत होती है. इस तकनीक का इस्तेमाल आम तौर पर कंसोल/डेस्कटॉप रेंडरिंग के लिए किया जाता है; वर्टेक्स स्ट्रीम लेआउट, रेंडरिंग इंजन के कई क्लास के लिए फ़ायदेमंद हो सकता है!

स्ट्रीम को अलग-अलग करने में वर्टेक्स बफ़र को सेट अप किया जाता है, जिसमें वर्टेक्स पोज़िशन डेटा के कॉन्टिअस सेक्शन वाला सेक्शन होता है और दूसरे सेक्शन में इंटरलीव्ड वर्टेक्स एट्रिब्यूट होते हैं. ज़्यादातर ऐप्लिकेशन आम तौर पर सभी एट्रिब्यूट को शामिल करते हुए, अपने बफ़र को पूरी तरह से सेट अप करते हैं. इस विज़ुअल में अंतर के बारे में बताया गया है:

Before:
|Position1/Normal1/Tangent1/UV1/Position2/Normal2/Tangent2/UV2......|

After:
|Position1/Position2...|Normal1/Tangent1/UV1/Normal2/Tangent2/UV2...|

यह देखने से कि जीपीयू, वर्टेक्स डेटा को कैसे फ़ेच करता है, अलग-अलग समय पर बांटें. तर्क के लिए यह मानकर:

  • 32 बाइट वाली कैश लाइनें (ये काफ़ी आम तौर पर इस्तेमाल होती हैं)
  • Vertex फ़ॉर्मैट में यह शामिल है:
    • स्थिति, vec3<float32> = 12 बाइट
    • सामान्य vec3<float32> = 12 बाइट
    • यूवी कोऑर्डिनेट vec2<float32> = 8 बाइट
    • कुल साइज़ = 32 बाइट

जब जीपीयू बिनिंग के लिए, मेमोरी से डेटा फ़ेच करता है, तो काम करने के लिए वह 32-बाइट वाली कैश लाइन खींचेगा. वर्टेक्स स्ट्रीम को स्प्लिट किए बिना, यह बिन करने के लिए इस कैश लाइन के सिर्फ़ पहले 12 बाइट का इस्तेमाल करेगा और बाकी 20 बाइट को खारिज कर देगा, क्योंकि यह अगले वर्टेक्स को फ़ेच करता है. वर्टेक्स स्ट्रीम को स्प्लिट करने से, वर्टेक्स की पोज़िशन मेमोरी में आपस में सटी रहेंगी. इसलिए, जब 32-बाइट वाले हिस्से को कैश मेमोरी में डाला जाएगा, तो उसमें ऑपरेट करने के लिए दो पूरे वर्टेक्स पोज़िशन होंगी. इससे ज़्यादा डेटा फ़ेच करने के लिए मुख्य मेमोरी में वापस जाना पड़ेगा, जिससे 2 गुना सुधार होता है!

अब, अगर हम वर्टेक्स स्ट्रीम को वर्टेक्स कंप्रेशन के साथ स्प्लिट करते हैं, तो हम एक वर्टेक्स पोज़िशन का साइज़ कम करके 6 बाइट कर देंगे. इससे, सिस्टम मेमोरी से निकाली गई एक 32-बाइट कैश लाइन के लिए 5 पूरे वर्टेक्स पोज़िशन पर काम होगा, जिससे 5 गुना सुधार होगा!

Vertex स्ट्रीम को स्ट्रीम करने के नतीजे

  • Vertex Memory रीड बैंडविथ:
    • बिनिंग: 27 जीबी/से॰ से 6.5 जीबी/सेकंड
    • रेंडरिंग: 4.5 जीबी/से॰ से 4.5 जीबी/सेकंड
  • Vertex फे़च स्टॉल:
    • बिनिंग: 40% से 0%
    • रेंडरिंग: 90% से 90%
  • औसत बाइट/वर्टेक्स:
    • बिनिंग: 48B से 12B
    • रेंडरिंग: 52B से 52B

Android जीपीयू इंस्पेक्टर व्यू, जिसमें अलग-अलग वर्टेक्स स्ट्रीम दिखाई गई हैं

इमेज 5: Android जीपीयू इंस्पेक्टर व्यू में, अलग-अलग वर्टेक्स स्ट्रीम को

स्प्लिट वर्टेक्स स्ट्रीम का Android जीपीयू इंस्पेक्टर व्यू

इमेज 6: स्प्लिट वर्टेक्स स्ट्रीम का Android जीपीयू इंस्पेक्टर व्यू

कंपाउंड नतीजे

  • Vertex Memory रीड बैंडविथ:
    • बिनिंग: 25 जीबी/से॰ से 4.5 जीबी/सेकंड
    • रेंडरिंग: 4.5 जीबी/से॰ से 1.7 जीबी/सेकंड
  • Vertex फे़च स्टॉल:
    • बिनिंग: 41% से 0%
    • रेंडरिंग: 90% से 90%
  • औसत बाइट/वर्टेक्स:
    • बिनिंग: 48B से 8B
    • रेंडरिंग: 52B से 19B

Android जीपीयू इंस्पेक्टर व्यू, जिसमें अलग-अलग वर्टेक्स स्ट्रीम दिखाई गई हैं

इमेज 7: Android जीपीयू इंस्पेक्टर व्यू में, अलग-अलग और बिना कंप्रेस किए गए वर्टेक्स स्ट्रीम

Android जीपीयू इंस्पेक्टर व्यू, जिसमें अलग-अलग वर्टेक्स स्ट्रीम दिखाई गई हैं

इमेज 8: स्प्लिट, कंप्रेस किए गए वर्टेक्स स्ट्रीम का Android जीपीयू इंस्पेक्टर व्यू

अन्य बातें

16 बनाम 32 बिट इंडेक्स बफ़र डेटा

  • मेश को हमेशा अलग-अलग हिस्सों में बांटें, ताकि वे 16-बिट इंडेक्स बफ़र (ज़्यादा से ज़्यादा 65536 यूनीक वर्टेक्स) में फ़िट हो सकें. इससे मोबाइल पर इंडेक्स की गई रेंडरिंग में मदद मिलेगी, क्योंकि वर्टेक्स डेटा फ़ेच करना सस्ता होता है और इसमें कम बिजली खर्च होती है.

काम न करने वाले वर्टेक्स बफ़र एट्रिब्यूट फ़ॉर्मैट

  • SSCALED वर्टेक्स फ़ॉर्मैट को मोबाइल पर ज़्यादा इस्तेमाल नहीं किया जा सकता. साथ ही, इनका इस्तेमाल करने पर ड्राइवर की परफ़ॉर्मेंस खराब हो सकती है. ये ऐसे ड्राइवर होते हैं जो हार्डवेयर के साथ काम नहीं करते हैं. ऐसे में, इन फ़ॉर्मैट को अपनाने की कोशिश की जाती है. हमेशा SNORM पर जाएं और कंप्रेस करने के बाद, कम से कम ALU कीमत चुकाएं.