Quản lý dữ liệu đỉnh (vertex)

Bố cục và nén dữ liệu đỉnh tốt là phần không thể thiếu trong hiệu năng của bất kỳ ứng dụng đồ hoạ nào, cho dù ứng dụng bao gồm giao diện người dùng 2D hay là trò chơi 3D có thế giới mở rộng lớn. Thử nghiệm nội bộ với Frame Profiler (Trình phân tích khung) của Android GPU Inspector trên nhiều trò chơi Android hàng đầu cho thấy rằng bạn có thể làm được nhiều việc để cải thiện tính năng quản lý dữ liệu đỉnh. Chúng tôi quan sát thấy rằng dữ liệu trong đỉnh thường sử dụng độ chính xác đầy đủ, giá trị số thực 32 bit cho tất cả các thuộc tính đỉnh và bố cục đệm đỉnh sử dụng một loạt các cấu trúc được định dạng với các thuộc tính đan xen đầy đủ.

Bài viết này thảo luận cách tối ưu hoá hiệu năng đồ hoạ của ứng dụng Android bằng cách sử dụng các kỹ thuật sau:

  • Nén đỉnh
  • Tách luồng đỉnh

Việc triển khai các kỹ thuật này có thể cải thiện mức sử dụng băng thông của bộ nhớ trên đỉnh lên đến 50%, giảm tranh chấp bộ nhớ bus với CPU, giảm số stall trên bộ nhớ hệ thống và cải thiện thời lượng pin; tất cả đều mang đến lợi ích cho cả nhà phát triển và người dùng cuối!

Tất cả dữ liệu được trình bày đều đến từ một cảnh tĩnh mẫu chứa ~19.000.000 đỉnh chạy trên Pixel 4:

Cảnh mẫu có 6 vòng và 19 triệu đỉnh

Hình 1: Cảnh mẫu có 6 vòng và 19 triệu đỉnh

Nén đỉnh

Vertex Compression (Nén đỉnh) là một thuật ngữ chung cho các kỹ thuật nén mất dữ liệu sử dụng tính năng đóng gói hiệu quả để giảm kích thước của dữ liệu đỉnh cả trong thời gian chạy và trong quá trình lưu trữ. Việc giảm kích thước của các đỉnh có một số lợi ích, bao gồm việc giảm băng thông bộ nhớ trên GPU (bằng cách tính toán "giao dịch" của băng thông), cải thiện việc sử dụng bộ nhớ đệm và giảm nguy cơ làm tràn thanh ghi.

Sau đây là một số phương pháp Vertex Compression phổ biến:

  • Giảm độ chính xác số của các thuộc tính dữ liệu đỉnh (ví dụ: float 32 bit xuống float 16 bit)
  • Thể hiện các thuộc tính ở nhiều định dạng

Ví dụ: nếu một đỉnh sử dụng float 32 bit đầy đủ cho vị trí (vec3), pháp tuyến (vec3) và toạ độ texture (vec2), thì việc thay thế tất cả các đỉnh này bằng float 16 bit sẽ làm giảm kích thước đỉnh 50% (16 byte trên trung bình một đỉnh 32 byte).

Vị trí các đỉnh

Dữ liệu vị trí đỉnh có thể được nén từ giá trị floating point 32 bit chính xác đầy đủ đến giá trị floating point 16 bit bán chính xác trong phần lớn lưới và các giá trị bán chính xác được hỗ trợ trong phần cứng trên hầu hết các thiết bị di động. Hàm chuyển đổi từ float32 đến float16 trông giống như sau (được điều chỉnh từ hướng dẫn này):

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

Có một giới hạn đối với phương pháp này; độ chính xác suy giảm vì đỉnh cách xa nguồn gốc nên giá trị này không phù hợp với lưới có kích thước không gian rất lớn (các đỉnh có các thành phần vượt quá ngưỡng 1024). Bạn có thể giải quyết vấn đề này bằng cách chia một lưới thành các phần nhỏ hơn, đặt từng phần xung quanh điểm gốc của mô hình và chia tỷ lệ sao cho tất cả các đỉnh của mỗi phần trên phạm vi [-1, 1] đều chứa độ chính xác cao nhất đối với các giá trị floating point. Mã giả để nén trông như sau:

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

Bạn đưa hệ số tỷ lệ và hệ số dịch vào ma trận mô hình để giải nén dữ liệu đỉnh khi hiển thị. Hãy nhớ rằng bạn sẽ không sử dụng cùng một ma trận mô hình này để biến đổi các pháp tuyến, vì các pháp tuyến này không áp dụng cùng một dạng nén. Bạn sẽ cần một ma trận không có các biến đổi giải nén này cho các pháp tuyến hoặc bạn có thể sử dụng ma trận mô hình cơ sở (có thể sử dụng cho các pháp tuyến) và sau đó áp dụng các biến đổi giải nén bổ sung cho ma trận mô hình trong trình đổ bóng. Ví dụ:

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

Một phương pháp khác bao gồm sử dụng Signed Normalized Integers (SNORM). Các loại dữ liệu SNORM sử dụng số nguyên thay vì dấu phẩy để biểu thị các giá trị trong khoảng [-1, 1]. Sử dụng SNORM 16 bit cho các vị trí giúp bạn tiết kiệm bộ nhớ giống như float16 mà không gặp các hạn chế vì các hàm phân phối không đồng nhất. Cách triển khai mà chúng tôi khuyến nghị để sử dụng SNORM trông giống như sau:

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)
Định dạng Kích thước
Trước vec4<float32> 16 byte
Sau vec3<float16/SNORM16> 6 byte

Các pháp tuyến đỉnh và không gian tiếp tuyến

Cần có các pháp tuyến đỉnh để chiếu sáng và không gian tiếp tuyến là cần thiết cho các kỹ thuật phức tạp hơn như lập bản đồ pháp tuyến.

Không gian tiếp tuyến

Không gian tiếp tuyến là một hệ toạ độ, trong đó mọi đỉnh bao gồm vectơ pháp tuyến, vectơ tiếp tuyến và vectơ góc. Vì ba vectơ này thường trực giao với nhau, nên chúng ta chỉ cần lưu trữ hai trong số đó và có thể tính vectơ thứ ba bằng cách lấy tích chéo của hai vectơ còn lại trong trình đổ bóng của đỉnh.

Các vectơ này thường có thể được biểu diễn bằng độ chính xác đơn 16 bit mà không mất đi cảm giác trung thực về hình ảnh, vì vậy nên lựa chọn các vectơ đó để bắt đầu!

Chúng tôi có thể nén thêm bằng một kỹ thuật có tên là QTangents lưu trữ toàn bộ không gian tiếp tuyến trong một quaternion (hệ thống số quaternion) duy nhất. Vì các quaternion có thể dùng để biểu diễn phép quay, bằng cách nghĩ rằng vectơ không gian tiếp tuyến là vectơ cột của ma trận 3x3 đại diện cho phép quay (trong trường hợp này là từ không gian mô hình thành không gian tiếp tuyến) nên chúng ta có thể chuyển đổi giữa hai biến! Một quaternion có thể được coi là dữ liệu vec4 và việc chuyển đổi từ vectơ không gian tiếp tuyến sang QTangent dựa trên bài viết được liên kết ở trên và được điều chỉnh từ quá trình triển khai tại đây như sau:

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

Quaternion sẽ được chuẩn hoá và sẽ có thể được nén bằng SNORMs. SNORM 16 bit mang lại độ chính xác cao và tiết kiệm bộ nhớ. SNORM 8-bit có thể tiết kiệm hơn nữa, nhưng có thể gây ra hiện tượng hình ảnh trông giả tạo đối với các chất liệu có tính đặc thù cao. Bạn có thể thử cả hai và xem cách nào hiệu quả nhất cho tài sản của mình! Mã hoá quaternion trông giống như sau:

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

Để giải mã quaternion trong trình tạo bóng đỉnh (được điều chỉnh từ đây ):

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;
  ...
}
Định dạng Kích thước
Trước vec3<float32> + vec3<float32> + vec3<float32> 36 byte
Sau vec4<SNORM16> 8 byte

Chỉ các pháp tuyến

Nếu bạn chỉ cần lưu trữ vectơ pháp tuyến, có một cách tiếp cận khác có thể dẫn đến tiết kiệm nhiều hơn - sử dụng Liên kết bát phân của vectơ đơn vị thay vì Toạ độ Cartesian để nén vectơ pháp tuyến. Tính năng Lập bản đồ bát giác hoạt động bằng cách chiếu một hình cầu đơn vị lên một hình bát diện đều và sau đó chiếu hình bát diện đều xuống một mặt phẳng 2D. Kết quả là bạn có thể biểu diễn bất kỳ vectơ pháp tuyến nào chỉ bằng hai số. Hai con số này có thể được coi là toạ độ kết cấu mà chúng tôi sử dụng để "lấy mẫu" mặt phẳng 2D mà chúng tôi chiếu hình cầu lên đó, cho phép khôi phục vectơ ban đầu. Sau đó, hai số này có thể được lưu trữ trong SNORM8.

Chiếu hình cầu đơn vị thành một hình bát diện đều và chiếu hình bát diện đều lên một mặt phẳng 2D

Hình 2: Lập bản đồ hình bát giác được hiển thị (nguồn)

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)

Giải nén trong trình đổ bóng đỉnh (để chuyển đổi về toạ độ Cartesian) là không tốn kém; với hầu hết các thiết bị di động hiện đại, chúng tôi không thấy bất kỳ sự sụt giảm hiệu năng lớn nào khi triển khai kỹ thuật này. Giải nén trong trình đổ bóng đỉnh:

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

Cách tiếp cận này cũng có thể dùng để lưu trữ toàn bộ không gian tiếp tuyến, sử dụng kỹ thuật này để lưu trữ vectơ tiếp tuyến và pháp tuyến bằng cách sử dụng vec2<SNORM8> nhưng bạn sẽ cần tìm cách lưu trữ hướng của vectơ góc (cần cho trường hợp chung mà bạn đã phản chiếu toạ độ UV trên một mô hình). Một cách để triển khai tính năng này là ánh xạ một thành phần mã hoá của vectơ tiếp tuyến của bạn để luôn dương, sau đó đổi dấu nếu bạn cần phải lật hướng véctơ góc và kiểm tra điều đó trong trình đổ bóng đỉnh:

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);
Định dạng Kích thước
Trước vec3<float32> 12 byte
Sau vec2<SNORM8> 2 byte

Các toạ độ UV đỉnh

Toạ độ UV, được dùng để ánh xạ kết cấu (cùng với các tính năng khác), thường được lưu trữ bằng float 32 bit. Việc nén chúng với số thực 16 bit gây ra các vấn đề về độ chính xác đối với các kết cấu lớn hơn 1024x1024; độ chính xác của floating point giữa [0,5, 1,0] có nghĩa là các giá trị sẽ tăng hơn 1 pixel!

Cách tiếp cận tốt hơn là sử dụng số nguyên không chuẩn hoá (UNORM), đặc biệt là UNORM16; điều này cung cấp sự phân bổ đồng nhất trên toàn bộ dải toạ độ texture, hỗ trợ texture có kích thước lên đến 65536x65536! Điều này giả định các toạ độ texture nằm trong khoảng [0,0, 1,0] cho mỗi phần tử, có thể không phải là trường hợp tuỳ thuộc vào lưới (ví dụ: các bức tường có thể sử dụng toạ độ texture bao phủ vượt quá 1,0) vì vậy hãy lưu ý điều này khi xem xét kỹ thuật này , Hàm chuyển đổi sẽ có dạng như sau:

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
Định dạng Kích thước
Trước vec2<float32> 8 byte
Sau vec2<UNORM16> 4 byte

Kết quả nén đỉnh

Các kỹ thuật nén đỉnh này giúp giảm 66% dung lượng lưu trữ bộ nhớ đỉnh, giảm từ 48 byte xuống còn 16 byte. Thông tin này đã tự xuất hiện dưới dạng:

  • Băng thông đọc bộ nhớ đỉnh:
    • Kết hợp: 27GB/s còn 9GB/s
    • Kết xuất: 4,5 B/giây còn 1,5 B/giây
  • Các stall tìm nạp đỉnh:
    • Kết hợp: 50% còn 0%
    • Kết xuất: 90% còn 90%
  • Số byte/đỉnh trung bình:
    • Kết hợp: 48B còn 16B
    • Kết xuất: 52B còn 18B

Chế độ xem trong Android GPU Inspector về các đỉnh không nén

Hình 3: Chế độ xem trong Android GPU Inspector về các đỉnh không nén

Chế độ xem trong Android GPU Inspector về các đỉnh được nén

Hình 4: Chế độ xem trong Android GPU Inspector của các đỉnh được nén

Tách luồng đỉnh

Tính năng phân tách luồng đỉnh tối ưu hoá việc sắp xếp dữ liệu trong vùng đệm đỉnh. Đây là quy trình tối ưu hoá hiệu suất bộ nhớ đệm, tạo ra sự khác biệt đối với các GPU tile-based (xử lý hình ảnh bằng cách chia thành các ô), thường có trong các Thiết bị Android, cụ thể là trong bước kết hợp trong quá trình kết xuất.

GPU tile-based tạo ra một trình đổ bóng tính toán các toạ độ của thiết bị được chuẩn hoá, rồi dựa trên trình đổ bóng đỉnh đã cung cấp từ trước để thực hiện việc kết hợp. Hoạt động này được thực thi trước tiên trên mọi đỉnh trong cảnh, cho dù có hiển thị hay không. Do đó, việc giữ cho dữ liệu vị trí đỉnh nằm liền kề trong bộ nhớ là một điểm cộng lớn. Các vị trí khác mà bố cục luồng đỉnh này có thể có lợi cho việc truyền tham số bóng, vì bạn thường chỉ cần dữ liệu vị trí để tính toán bóng đổ, cũng như độ sâu. Đây là một kỹ thuật thường dùng để kết xuất dành cho console/máy tính; bố cục luồng tối ưu này có thể là thành công của nhiều lớp công cụ hiển thị!

Tính năng phân tách luồng bao gồm thiết lập vùng đệm đỉnh, trong đó có một phần liền kề chứa dữ liệu vị trí đỉnh và một phần khác chứa các thuộc tính đỉnh đan xen. Hầu hết các ứng dụng thường thiết lập bộ đệm để bố trí các thuộc tính hoàn toàn xen kẽ. Hình ảnh này giải thích sự khác biệt:

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

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

Bằng cách xem xét việc GPU tìm nạp dữ liệu đỉnh, chúng ta hiểu được lợi ích của việc phân tách luồng. Giả định vì mục đích tranh luận:

  • Các dòng 32 byte trong bộ nhớ đệm (kích thước khá phổ biến)
  • Định dạng đỉnh bao gồm:
    • Vị trí, vec3<float32> = 12 byte
    • Pháp tuyến vec3<float32> = 12 byte
    • Toạ độ UV vec2<float32> = 8 byte
    • Tổng kích thước = 32 byte

Khi tìm nạp dữ liệu từ bộ nhớ để GPU để kết nối, GPU sẽ kéo một dòng bộ nhớ đệm 32 byte để hoạt động. Khi không có phân chia luồng đỉnh, GPU sẽ chỉ sử dụng 12 byte đầu tiên của dòng bộ nhớ đệm này để kết hợp và hủy 20 byte còn lại khi tìm nạp đỉnh tiếp theo. Với tính năng tách luồng đỉnh, các vị trí đỉnh sẽ liền nhau trong bộ nhớ, vì vậy khi phần 32 byte đó được kéo vào bộ nhớ đệm, nó sẽ thực sự chứa tổng công 2 vị trí đỉnh để hoạt động trước khi phải quay lại bộ nhớ chính để tìm nạp thêm, cải thiện gấp 2 lần!

Bây giờ, nếu chúng ta kết hợp việc chia luồng đỉnh với tính năng nén đỉnh, chúng ta sẽ giảm kích thước của một vị trí đỉnh đơn xuống còn 6 byte, vì vậy một dòng bộ nhớ đệm 32 byte được lấy từ bộ nhớ hệ thống sẽ có tổng cộng 5 vị trí đỉnh để hoạt động, cải thiện gấp 5 lần!

Kết quả phân tách luồng đỉnh

  • Băng thông đọc bộ nhớ đỉnh:
    • Kết hợp: 27GB/s còn 6,5GB/s
    • Kết xuất: 4,5 GB/giây còn 4,5 GB/giây
  • Các stall tìm nạp đỉnh:
    • Kết hợp: 40% còn 0%
    • Kết xuất: 90% còn 90%
  • Số byte/đỉnh trung bình:
    • Kết hợp: 48 B còn 12 B
    • Kết xuất: 52 B còn 52 B

Chế độ xem trong Android GPU Inspector của các luồng đỉnh không bị tách

Hình 5: Chế độ xem trong Android GPU Inspector của các luồng đỉnh không bị tách

Chế độ xem trong Android GPU Inspector của các luồng đỉnh bị tách

Hình 6: Chế độ xem trong Android GPU Inspector của các luồng đỉnh bị tách

Kết quả tổng hợp

  • Băng thông đọc bộ nhớ đỉnh:
    • Kết hợp: 25GB/s còn 4,5GB/s
    • Kết xuất: 4,5 GB/giây còn 1,7 GB/giây
  • Các stall tìm nạp đỉnh:
    • Kết hợp: 41% còn 0%
    • Kết xuất: 90% còn 90%
  • Số byte/đỉnh trung bình:
    • Kết hợp: 48 B còn 8 B
    • Kết xuất: 52 B còn 19 B

Chế độ xem trong Android GPU Inspector của các luồng đỉnh không bị tách

Hình 7: Chế độ xem trong Android GPU Inspector của các luồng đỉnh không nén, không tách

Chế độ xem trong Android GPU Inspector của các luồng đỉnh không bị tách

Hình 8: Chế độ xem trong Android GPU Inspector của các luồng đỉnh được nén, tách

Các yếu tố cần cân nhắc khác

Dữ liệu vùng đệm index 16 so với 32 bit

  • Luôn chia/chặn các lưới để chúng vừa với bộ đệm index 16 bit (tối đa 65536 đỉnh). Điều này sẽ có ích cho việc kết xuất index trên thiết bị di động vì chi phí lấy dữ liệu đỉnh sẽ rẻ hơn và tốn ít điện năng hơn.

Các định dạng thuộc tính Vertex Buffer không được hỗ trợ

  • Các định dạng đỉnh SSVEED không được hỗ trợ rộng rãi trên thiết bị di động và khi được sử dụng có thể gây ra sự đánh đổi đáng kể về hiệu năng đối với các driver khi cố gắng mô phỏng các định dạng không được phần cứng hỗ trợ. Luôn lựa chọn SNORM và chấp nhận chi phí ALU không đáng kể để giải nén.