การจัดการข้อมูล Vertex

การจัดวางและการบีบอัดข้อมูล Vertex ที่ดีเป็นส่วนสำคัญมากต่อประสิทธิภาพของแอปพลิเคชันแบบกราฟิก ไม่ว่าแอปจะประกอบไปด้วยอินเทอร์เฟซผู้ใช้แบบ 2 มิติหรือจะเป็นเกมโอเพนเวิลด์ 3 มิติขนาดใหญ่ก็ตาม การทดสอบภายในกับ Frame Profiler ของ Android GPU กับเกม Android ยอดนิยมหลายสิบเกมชี้ให้เห็นว่าสามารถทำอะไรได้บ้างเพื่อปรับปรุงการจัดการข้อมูล Vertex เราสังเกตเห็นว่าเป็นเรื่องปกติที่ข้อมูล Vertex จะใช้ความแม่นยําแบบเต็ม ค่าจำนวนลอยตัว 32 บิตสำหรับแอตทริบิวต์ Vertex ทั้งหมด และรูปแบบบัฟเฟอร์ของ Vertex ที่ใช้อาร์เรย์ของโครงสร้างที่มีการจัดรูปแบบด้วยแอตทริบิวต์ที่มีการแทรกสลับแบบเต็ม

บทความนี้อธิบายวิธีการเพิ่มประสิทธิภาพกราฟิกของแอปพลิเคชัน Android โดยใช้เทคนิคต่อไปนี้

  • การบีบอัด Vertex
  • การแยกสตรีม Vertex

การใช้เทคนิคเหล่านี้จะช่วยเพิ่มการใช้งานแบนด์วิดท์หน่วยความจำของ Vertex ได้ถึง 50% ลดการช่วงชิงบัสหน่วยความจำกับ CPU ลดการค้างในหน่วยความจำของระบบ และปรับปรุงอายุแบตเตอรี่ ซึ่งล้วนแล้วแต่ประสบความสำเร็จทั้งสำหรับนักพัฒนาแอปและผู้ใช้ปลายทาง

ข้อมูลทั้งหมดที่แสดงมาจากตัวอย่างฉากนิ่งที่มีจุดยอดประมาณ 19,000,000 จุดที่ทำงานใน Pixel 4

ตัวอย่างฉากที่มีวงแหวน 6 วงและยอดแหลมยาว 19 เมตร

รูป 1: ตัวอย่างฉากที่มีวงแหวน 6 วงและยอดแหลมยาว 19 เมตร

การบีบอัด Vertex

Vertex Compression เป็นคำรวมที่ใช้เรียกเทคนิคการบีบอัดแบบสูญเสียบางส่วนที่ ใช้การบรรจุอย่างมีประสิทธิภาพเพื่อลดขนาดข้อมูล Vertex ทั้งระหว่างรันไทม์และพื้นที่เก็บข้อมูล การลดขนาดของจุดยอดมีประโยชน์หลายประการ รวมถึงการลดแบนด์วิดท์ของหน่วยความจำใน GPU (โดยการซื้อขายค่าแบนด์วิดท์) การปรับปรุงการใช้งานแคช และอาจลดความเสี่ยงของการลงทะเบียนที่รั่วไหล

วิธีทั่วไปในการบีบอัด Vertex ได้แก่

  • ลดความแม่นยำของตัวเลขของแอตทริบิวต์ข้อมูล Vertex (เช่น ทศนิยม 32 บิตเป็นทศนิยม 16 บิต)
  • นำเสนอแอตทริบิวต์ในรูปแบบต่างๆ

ตัวอย่างเช่น หาก Vertex ใช้ Float 32 บิตแบบเต็มสำหรับตำแหน่ง (vec3), ปกติ (vec3) และพิกัดพื้นผิว (vec2) การแทนที่ทั้งหมดนี้ด้วยทศนิยม 16 บิตจะลดขนาด Vertex ลง 50% (16 ไบต์ใน Vertex 32 ไบต์โดยเฉลี่ย)

ตำแหน่งจุดยอดมุม

ข้อมูลตำแหน่ง Vertex สามารถบีบอัดจากค่าจุดลอยตัว 32 บิตที่มีความแม่นยำเต็มรูปแบบ เป็นค่าจุดลอยตัว 16 บิตที่มีความแม่นยำครึ่งหนึ่งใน Mesh ส่วนใหญ่ และครึ่ง Float ได้รับการสนับสนุนในฮาร์ดแวร์ในอุปกรณ์เคลื่อนที่เกือบทุกรุ่น ฟังก์ชัน Conversion ที่เปลี่ยนจาก Float32 ไป Float 16 มีลักษณะดังนี้ (ดัดแปลงมาจากคู่มือนี้)

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] ซึ่งมีความแม่นยำสูงสุดสำหรับค่าจุดลอยตัว Pseudocode สำหรับการบีบอัดจะมีลักษณะดังนี้

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] การใช้ SNORM แบบ 16 บิตสำหรับตำแหน่งต่างๆ จะช่วยให้คุณประหยัดหน่วยความจำได้เท่ากับ Float 16 โดยไม่มีข้อเสียของการกระจายที่ไม่สม่ำเสมอ การดำเนินการที่เราแนะนำให้ใช้กับ 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 ไบต์
หลัง vec3<float16/SNORM16> 6 ไบต์

จุดยอดมุมปกติและช่องว่างแทนเจนต์

จำเป็นต้องใช้ Vertex ปกติสำหรับการจัดแสง และต้องมีพื้นที่แทนเจนต์สำหรับเทคนิคที่ซับซ้อนมากขึ้น เช่น การแมปปกติ

ปริภูมิแทน

ปริภูมิแทนเจนต์เป็นระบบพิกัดที่ทุกจุดยอดประกอบด้วยเวกเตอร์ปกติ แทนเจนต์ และบิตเทนเจนต์ เนื่องจากเวกเตอร์ทั้ง 3 นี้มักจะเป็นแนวฉากตรงข้ามกัน เราจึงต้องจัดเก็บเวกเตอร์ไว้เพียง 2 เวกเตอร์ และช่วยคำนวณหาเวกเตอร์ที่ 3 ได้โดยการนำผลคูณไขว้ของอีก 2 เวกเตอร์ในตัวแสดงผล Vertex

โดยทั่วไปเวกเตอร์เหล่านี้สามารถแสดงโดยใช้การลอยตัว 16 บิตโดยไม่สูญเสียการรับรู้ในคุณภาพของภาพ จึงเป็นจุดที่ควรเริ่มต้น

เราสามารถบีบอัดเพิ่มเติมด้วยเทคนิคที่เรียกว่า QTangents ซึ่งเก็บพื้นที่แทนเจนต์ทั้งหมดไว้ในควอเทนเนียนเดียว เนื่องจากควอเทิร์นสามารถใช้เพื่อแสดงการหมุน โดยการคิดให้เวกเตอร์พื้นที่แทนเจนต์เป็นเวกเตอร์ของคอลัมน์ของเมทริกซ์ขนาด 3x3 ที่แสดงถึงการหมุน (ในกรณีนี้จากพื้นที่โมเดลลงในพื้นที่แทนเจนต์) เราจึงจะแปลงระหว่าง 2 ส่วนได้ ควอเทอร์เนียนอาจถือว่าเป็นข้อมูล vec4 และการแปลงจากเวกเตอร์พื้นที่แทนเจนต์เป็น QTangent โดยอิงจากเอกสารที่ลิงก์ด้านบน และปรับเปลี่ยนจากการติดตั้งใช้งานที่นี่มีดังนี้

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

ระบบจะทำให้ควอเทอร์เนียนเป็นมาตรฐานและคุณจะบีบอัดได้โดยใช้ SNORM SNORM แบบ 16 บิตช่วยให้แม่นยำและประหยัดหน่วยความจำได้เป็นอย่างดี SNORM แบบ 8 บิตช่วยประหยัดได้มากกว่า แต่อาจทำให้มีอาร์ติแฟกต์ในวัสดุที่มีข้อมูลจำเพาะสูง คุณสามารถลองใช้ทั้ง 2 รูปแบบ แล้วดูว่าแบบใดเหมาะกับชิ้นงานของคุณที่สุด การเข้ารหัสควอเทอร์เนียนมีลักษณะดังนี้

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

วิธีถอดรหัสควอเทอร์เนียนในตัวปรับแสงเงา Vertex (ดัดแปลงจากที่นี่)

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;
  ...
}
รูปแบบ ขนาด
ก่อน vec3<float32> + vec3<float32> + vec3<float32> 36 ไบต์
หลัง vec4<SNORM16> 8 ไบต์

ปกติเท่านั้น

หากคุณต้องการเก็บเฉพาะเวกเตอร์ปกติ มีวิธีอื่นที่สามารถช่วยให้คุณประหยัดได้ คือ การใช้การทำแผนที่ค่าเวกเตอร์ของหน่วยเวกเตอร์ แทนที่จะใช้พิกัดคาร์ทีเซียนเพื่อบีบอัดเวกเตอร์ปกติ การทำแผนที่ทรงแปดหน้าโดยฉายภาพทรงกลมหน่วยเป็นทรงแปดหน้า แล้วฉายภาพทรงแปดหน้าลงไปในระนาบ 2 มิติ ผลลัพธ์ก็คือ คุณสามารถแสดงเวกเตอร์ปกติใดๆ ได้โดยใช้ตัวเลขเพียง 2 ตัว ตัวเลข 2 จำนวนนี้หมายถึงพิกัดพื้นผิวที่เราใช้เพื่อ "สุ่มตัวอย่าง" ระนาบ 2 มิติที่เราฉายภาพทรงกลมเข้าไป ทำให้เราสามารถกู้คืนเวกเตอร์ต้นฉบับได้ หมายเลขทั้งสองนี้จะสามารถจัดเก็บไว้ใน SNORM8 ได้

ฉายทรงกลมที่มีหน่วยเป็นทรงแปดหน้า และฉายภาพทรงแปดหน้าไปยังระนาบ 2 มิติ

รูป 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)

การลดการบีบอัดในตัวสร้างแสงเงา (Vertex Shape) (เพื่อแปลงกลับไปเป็นพิกัดคาร์ทีเซียน) มีต้นทุนไม่แพง เราไม่เห็นถึงประสิทธิภาพที่ลดลงอย่างมากเมื่อนำเทคนิคนี้ไปใช้ในอุปกรณ์เคลื่อนที่รุ่นใหม่ส่วนใหญ่ การคลายการบีบอัดในตัวปรับแสงเงา Vertex

//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);
รูปแบบ ขนาด
ก่อน vec3<float32> 12 ไบต์
หลัง vec2<SNORM8> 2 ไบต์

พิกัดรังสียูวี Vertex

พิกัด UV ซึ่งใช้สำหรับการทำแผนที่พื้นผิว (และสิ่งอื่นๆ) มักจะจัดเก็บโดยใช้จุดลอยตัว 32 บิต การบีบอัดด้วยลอย 16 บิตทำให้เกิดปัญหาด้านความแม่นยำสำหรับพื้นผิวที่มีขนาดใหญ่กว่า 1024x1024 ความแม่นยำของจุดลอยตัวระหว่าง [0.5, 1.0] หมายความว่าค่าจะเพิ่มขึ้นได้มากกว่า 1 พิกเซล

วิธีที่ดีกว่าคือการใช้จำนวนเต็มมาตรฐานที่ไม่มีเครื่องหมาย (UNORM) โดยเฉพาะ UNORM16 วิธีนี้ทำให้มีการกระจายที่สม่ำเสมอทั่วทั้งช่วงพิกัดพื้นผิว รองรับพื้นผิวได้ถึง 65536x65536! โดยสมมติว่าพิกัดพื้นผิวอยู่ในช่วง [0.0, 1.0] ต่อองค์ประกอบ ซึ่งอาจไม่ใช่กรณี ทั้งนี้ขึ้นอยู่กับตาข่าย (ตัวอย่างเช่น ผนังสามารถใช้พิกัดพื้นผิวห่อหุ้มที่มากกว่า 1.0) ดังนั้นโปรดคำนึงถึงเรื่องนี้ด้วยเมื่อดูเทคนิคนี้ ฟังก์ชัน Conversion จะมีลักษณะดังนี้

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
รูปแบบ ขนาด
ก่อน vec2<float32> 8 ไบต์
หลัง vec2<UNORM16> 4 ไบต์

ผลการบีบอัด Vertex

เทคนิคการบีบอัด Vertex เหล่านี้ทำให้พื้นที่จัดเก็บหน่วยความจำของเวอร์เท็กซ์ลดลง 66% จาก 48 ไบต์ลงเป็น 16 ไบต์ สิ่งนี้ปรากฏว่า:

  • แบนด์วิดท์การอ่านหน่วยความจำ Vertex:
    • Binning: 27GB/s ถึง 9GB/s
    • การแสดงผล: 4.5B/วินาที ถึง 1.5 GB/วินาที
  • แผง Vertex Fetch:
    • ถังขยะ: 50% ถึง 0%
    • การแสดงผล: 90% ถึง 90%
  • ไบต์/เวอร์เท็กซ์เฉลี่ย:
    • Binning: 48B ถึง 16B
    • การแสดงผล: 52B ถึง 18B

มุมมองเครื่องมือตรวจสอบ GPU ของ Android ของจุดยอดที่ไม่บีบอัด

รูป 3: มุมมองเครื่องมือตรวจสอบ GPU ของ Android ของจุดยอดที่ไม่ได้บีบอัด

มุมมองเครื่องมือตรวจสอบ GPU ของ Android ของจุดยอดที่บีบอัด

รูป 4: มุมมองเครื่องมือตรวจสอบ GPU Android ของจุดยอดที่บีบอัด

การแยกสตรีม Vertex

Vertex Stream Splitting เพิ่มประสิทธิภาพการจัดระเบียบข้อมูลในบัฟเฟอร์ Vertex นี่เป็นการเพิ่มประสิทธิภาพการทำงานของแคชที่สร้างความแตกต่างใน GPU ตามไทล์ที่มักพบในอุปกรณ์ Android โดยเฉพาะอย่างยิ่งในระหว่างขั้นตอนการเชื่อมโยงของกระบวนการแสดงผล

GPU แบบใช้ไทล์จะสร้างตัวสร้างเฉดสีที่คำนวณพิกัดอุปกรณ์ให้เป็นมาตรฐานโดยอิงตามโหมด Vertex ที่จัดให้มีการทำ Bining ระบบจะดำเนินการกับจุดยอดมุมทุกจุดในฉากก่อน ไม่ว่าจะมองเห็นได้หรือไม่ก็ตาม การเก็บรักษาข้อมูลตำแหน่งจุดยอดมุมได้ต่อเนื่องกันในหน่วยความจำจึงเป็นข้อดีอย่างยิ่ง ส่วนอื่นๆ ที่เลย์เอาต์สตรีม Vertex นี้ใช้ได้ดีคือภาพผ่านเงา เนื่องจากปกติแล้วคุณต้องการเพียงข้อมูลตำแหน่งสำหรับการคำนวณแสงเงา ตลอดจนการข้ามล่วงหน้าของความลึก ซึ่งเป็นเทคนิคที่มักใช้ในการแสดงภาพในคอนโซล/เดสก์ท็อป การจัดวางสตรีม Vertex นี้เหมาะสำหรับเครื่องมือเรนเดอร์หลายคลาส

การแยกสตรีมเกี่ยวข้องกับการตั้งค่าบัฟเฟอร์ Vertex ที่มีส่วนที่ต่อเนื่องกันของข้อมูลตำแหน่งจุดยอดมุม และอีกส่วนหนึ่งที่มีแอตทริบิวต์ Vertex ที่มีการแทรกสลับ แอปพลิเคชันส่วนใหญ่มักจะตั้งค่าบัฟเฟอร์ให้สอดประสานแอตทริบิวต์ทั้งหมดได้อย่างสมบูรณ์ ภาพนี้จะอธิบายความแตกต่าง

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

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

การดูว่า GPU ดึงข้อมูล Vertex อย่างไรช่วยให้เราเข้าใจข้อดีของ การแยกสตรีม สมมติว่าต้องการอาร์กิวเมนต์:

  • บรรทัดแคช 32 ไบต์ (ขนาดค่อนข้างทั่วไป)
  • รูปแบบ Vertex ที่ประกอบด้วย
    • ตำแหน่ง, vec3<Flo32> = 12 ไบต์
    • vec3 ปกติ<Flo32> = 12 ไบต์
    • พิกัดรังสียูวี vec2<Flo32> = 8 ไบต์
    • ขนาดโดยรวม = 32 ไบต์

เมื่อ GPU ดึงข้อมูลจากหน่วยความจำเพื่อเชื่อมโยง GPU จะดึงบรรทัดแคชขนาด 32 ไบต์เพื่อดำเนินการ หากไม่มีการแยกสตรีม Vertex จะใช้เพียง 12 ไบต์แรกของบรรทัดแคชนี้สำหรับการเชื่อมโยง และทิ้งอีก 20 ไบต์ที่เหลือเมื่อดึงข้อมูลจุดยอดมุมถัดไป เมื่อใช้การแยกสตรีม Vertex ตำแหน่ง Vertex จะต่อเนื่องกันในหน่วยความจำ ดังนั้นเมื่อมีการดึงข้อมูลชิ้นส่วนขนาด 32 ไบต์เข้าไปในแคช ตำแหน่งดังกล่าวจะมีตำแหน่ง Vertex ทั้งหมด 2 ตำแหน่งสำหรับทำงานก่อนที่จะต้องกลับไปที่หน่วยความจำหลักเพื่อดึงข้อมูลเพิ่ม ซึ่งนับเป็นการปรับปรุง 2 เท่า!

คราวนี้ถ้ารวมการแยกสตรีม Vertex เข้ากับการบีบอัด Vertex เราจะลดขนาดของตำแหน่ง Vertex เดียวลงเหลือ 6 ไบต์ ดังนั้นบรรทัดแคช 32 ไบต์เดี่ยวที่ดึงจากหน่วยความจำของระบบจะมีตำแหน่งเวอร์เท็กซ์ทั้งหมด 5 ตำแหน่งสำหรับทำงาน ซึ่งทำให้การปรับปรุง 5 เท่า!

ผลการแยกสตรีม Vertex

  • แบนด์วิดท์การอ่านหน่วยความจำ Vertex:
    • Binning: 27GB/s ถึง 6.5GB/s
    • กำลังแสดงผล: 4.5 GB/วินาที ถึง 4.5 GB/วินาที
  • แผง Vertex Fetch:
    • ถังขยะ: 40% ถึง 0%
    • การแสดงผล: 90% ถึง 90%
  • ไบต์/เวอร์เท็กซ์เฉลี่ย:
    • Binning: 48B ถึง 12B
    • การแสดงผล: 52B ถึง 52B

มุมมองเครื่องมือตรวจสอบ GPU ของ Android ของสตรีมจุดยอดมุมที่ไม่มีการแยก

รูป 5: มุมมองเครื่องมือตรวจสอบ GPU ของ Android ของสตรีมจุดยอดมุมที่ไม่มีการแยก

มุมมองเครื่องมือตรวจสอบ GPU ของ Android ของสตรีมจุดยอดมุมแบบแยก

รูป 6: มุมมองเครื่องมือตรวจสอบ GPU ของ Android ของสตรีมจุดยอดมุมแบบแยก

ผลลัพธ์แบบผสม

  • แบนด์วิดท์การอ่านหน่วยความจำ Vertex:
    • Binning: 25GB/s ถึง 4.5GB/s
    • กำลังแสดงผล: 4.5 GB/วินาที ถึง 1.7 GB/วินาที
  • แผง Vertex Fetch:
    • ถังขยะ: 41% ถึง 0%
    • การแสดงผล: 90% ถึง 90%
  • ไบต์/เวอร์เท็กซ์เฉลี่ย:
    • กล่องเก็บ: 48B ถึง 8B
    • การแสดงผล: 52B ถึง 19B

มุมมองเครื่องมือตรวจสอบ GPU ของ Android ของสตรีมจุดยอดมุมที่ไม่มีการแยก

รูป 7: มุมมองเครื่องมือตรวจสอบ GPU ของ Android ของสตรีม Vertex ที่ไม่มีการบีบอัดและไม่มีการบีบอัด

มุมมองเครื่องมือตรวจสอบ GPU ของ Android ของสตรีมจุดยอดมุมที่ไม่มีการแยก

รูป 8: มุมมองเครื่องมือตรวจสอบ GPU ของ Android ของสตรีม Vertex ที่บีบอัดและแยก

ปัจจัยพิจารณาเพิ่มเติม

ข้อมูลบัฟเฟอร์ดัชนี 16 บิตเทียบกับ 32 บิต

  • แบ่ง/กลุ่ม Mesh เสมอเพื่อให้พอดีกับบัฟเฟอร์ดัชนี 16 บิต (จุดยอดมุมที่ไม่ซ้ำกันสูงสุด 65,536 จุด) วิธีนี้จะช่วยในการแสดงผลที่จัดทำดัชนีในอุปกรณ์เคลื่อนที่ เนื่องจากการดึงข้อมูลจุดยอดมุมมีราคาถูกกว่าและใช้พลังงานน้อยลง

รูปแบบแอตทริบิวต์บัฟเฟอร์ Vertex ที่ไม่รองรับ

  • รูปแบบจุดยอดมุมที่ฉับพลันไม่ได้รับการสนับสนุนอย่างกว้างขวางในอุปกรณ์เคลื่อนที่ และเมื่อใช้แล้วอาจส่งผลเสียต่อประสิทธิภาพซึ่งมีค่าใช้จ่ายสูงในไดรเวอร์ที่พยายามจำลองรูปแบบเหล่านี้หากไม่มีการรองรับฮาร์ดแวร์ ใช้ SNORM เสมอและจ่ายค่า ALU ที่ไม่สำคัญเลยในการขยาย