ביצוע אופטימיזציה ברמת דיוק נמוכה יותר

הפורמט המספרי של נתוני הגרפיקה וחישובי ההצללה יכולים להשפיע באופן משמעותי על ביצועי המשחק.

פורמטים אופטימליים:

  • שיפור היעילות של השימוש במטמון של המעבד הגרפי
  • צמצום הצריכה של רוחב הפס של הזיכרון, חיסכון בחשמל ושיפור הביצועים
  • מקסימום תפוקת חישוב בתוכניות הצללה
  • צמצום השימוש ב-RAM של המעבד (CPU) במשחק

פורמטים של נקודה צפה (floating point)

רוב החישובים והנתונים בגרפיקה תלת-ממדית מודרנית משתמשים במספרים עם נקודה צפה. ‫Vulkan ב-Android משתמש במספרים עם נקודה צפה (floating point) בגודל 32 או 16 ביט. מספר בשיטת נקודה צפה של 32 ביט נקרא בדרך כלל דיוק יחיד או דיוק מלא, ומספר בשיטת נקודה צפה של 16 ביט נקרא דיוק חצי.

ב-Vulkan מוגדר סוג של נקודה צפה (floating point) של 64 ביט, אבל מכשירי Android עם Vulkan בדרך כלל לא תומכים בסוג הזה, ולא מומלץ להשתמש בו. מספר נקודה צפה (floating point) של 64 ביט נקרא בדרך כלל דיוק כפול (double precision).

פורמטים של מספרים שלמים

מספרים שלמים עם סימן ובלי סימן משמשים גם לנתונים ולחישובים. הגודל הסטנדרטי של מספר שלם הוא 32 ביטים. התמיכה בגדלים אחרים של סיביות תלויה במכשיר. מכשירי Vulkan עם Android בדרך כלל תומכים במספרים שלמים של 16 ביט ו-8 ביט. ‫Vulkan מגדיר סוג של מספר שלם של 64 ביט, אבל מכשירי Vulkan ב-Android לא תומכים בדרך כלל בסוג הזה, ולא מומלץ להשתמש בו.

התנהגות לא אופטימלית של דיוק חצי-כפול

ארכיטקטורות מודרניות של GPU משלבות שני ערכים של 16 ביט ביחד בזוג של 32 ביט, ומטמיעות הוראות שפועלות על הזוג. כדי להשיג ביצועים אופטימליים, אל תשתמשו במשתני float סקלריים של 16 ביט. במקום זאת, הפכו את הנתונים לווקטורים של שניים או ארבעה רכיבים. יכול להיות שהקומפיילר של ה-shader יוכל להשתמש בערכים סקלריים בפעולות וקטוריות. עם זאת, אם אתם מסתמכים על המהדר כדי לבצע אופטימיזציה של סקלרים, בדקו את פלט המהדר כדי לאמת את הווקטוריזציה.

להמרה מנקודה צפה (floating point) של 32 ביט ו-16 ביט ובחזרה יש עלות חישובית. כדי להפחית את התקורה, כדאי לצמצם את השימוש בהמרות דיוק בקוד.

השוואת ההבדלים בביצועים בין גרסאות 16 ביט ו-32 ביט של האלגוריתמים. דיוק חצי לא תמיד משפר את הביצועים, במיוחד בחישובים מורכבים. אלגוריתמים שמשתמשים הרבה בהוראות FMA (פעולת כפל וחיבור משולבת) בנתונים וקטוריים הם מועמדים טובים לשיפור הביצועים בחצי דיוק.

תמיכה בפורמט מספרי

כל מכשירי Vulkan ב-Android תומכים במספרים עם נקודה צפה (floating point) של 32 ביט ובמספרים שלמים של 32 ביט בחישובי נתונים ותוכנות הצללה (shader). לא בטוח שתהיה תמיכה בפורמטים אחרים, ואם תהיה תמיכה, לא בטוח שהיא תתאים לכל תרחישי השימוש.

ב-Vulkan יש שתי קטגוריות של תמיכה בפורמטים מספריים אופציונליים: אריתמטיקה ואחסון. לפני שמשתמשים בפורמט מסוים, צריך לוודא שהמכשיר תומך בו בשתי הקטגוריות.

תמיכה בפונקציות אריתמטיות

כדי להשתמש בפורמט מספרי בתוכנות הצללה (shader), מכשיר Vulkan צריך להצהיר על תמיכה אריתמטית בפורמט הזה. מכשירי Vulkan ב-Android תומכים בדרך כלל בפורמטים הבאים לחישובים:

  • מספר שלם של 32 ביט (חובה)
  • נקודה צפה של 32 ביט (חובה)
  • מספר שלם של 8 ביט (אופציונלי)
  • מספר שלם של 16 ביט (אופציונלי)
  • נקודה צפה (floating point) של 16 ביט בחצי דיוק (אופציונלי)

כדי לבדוק אם מכשיר Vulkan תומך במספרים שלמים של 16 ביט לפעולות אריתמטיות, מאחזרים את תכונות המכשיר באמצעות קריאה לפונקציה vkGetPhysicalDeviceFeatures2()‎ ובודקים אם השדה shaderInt16 במבנה התוצאה של VkPhysicalDeviceFeatures2 הוא True.

כדי לבדוק אם מכשיר Vulkan תומך בנקודות צפות של 16 ביט או במספרים שלמים של 8 ביט, מבצעים את השלבים הבאים:

  1. בודקים אם המכשיר תומך בתוסף Vulkan‏ VK_KHR_shader_float16_int8. התוסף נדרש לתמיכה בנקודה צפה (float) של 16 ביט ובמספר שלם (integer) של 8 ביט.
  2. אם יש תמיכה ב-VK_KHR_shader_float16_int8, מוסיפים מצביע למבנה VkPhysicalDeviceShaderFloat16Int8Features לשרשרת VkPhysicalDeviceFeatures2.pNext.
  3. בודקים את השדות shaderFloat16 ו-shaderInt8 במבנה התוצאה VkPhysicalDeviceShaderFloat16Int8Features אחרי שקוראים ל-vkGetPhysicalDeviceFeatures2(). אם ערך השדה הוא true, הפורמט נתמך לפעולות אריתמטיות של תוכנית הצללה.

למרות שזו לא דרישה ב-Vulkan 1.1 או בפרופיל הבסיס של Android מ-2022, התמיכה בתוסף VK_KHR_shader_float16_int8 נפוצה מאוד במכשירי Android.

תמיכה באחסון

מכשיר Vulkan חייב להצהיר על תמיכה בפורמט מספרי אופציונלי לסוגי אחסון ספציפיים. התוסף VK_KHR_16bit_storage מצהיר על תמיכה בפורמטים של מספרים שלמים של 16 ביט ונקודה צפה של 16 ביט. התוסף מגדיר ארבעה סוגי אחסון. מכשיר יכול לתמוך במספרים של 16 ביט באף אחד מסוגי האחסון, בחלק מהם או בכולם.

סוגי האחסון הם:

  • אובייקטים של מאגר נתונים זמני לאחסון
  • אובייקטים של מאגר נתונים זמני אחיד
  • בלוקים קבועים של דחיפה
  • ממשקי קלט ופלט של הצללה (shader)

רוב המכשירים עם Vulkan 1.1 ב-Android תומכים בפורמטים של 16 ביט באובייקטים של מאגר נתונים זמני (buffer) לאחסון, אבל לא כולם. אל תניחו שיש תמיכה על סמך דגם ה-GPU. מכשירים עם מנהלי התקנים ישנים יותר ל-GPU מסוים לא בהכרח תומכים באובייקטים של מאגר נתונים, אבל מכשירים עם מנהלי התקנים חדשים יותר כן תומכים בהם.

התמיכה בפורמטים של 16 ביט במאגרי נתונים אחידים, בלוקים קבועים של דחיפה וממשקי קלט/פלט של הצללה תלויה בדרך כלל ביצרן ה-GPU. ב-Android, בדרך כלל ה-GPU תומך בכל שלושת הסוגים האלה או באף אחד מהם.

פונקציה לדוגמה שבודקת את התמיכה של Vulkan בפורמטים של אריתמטיקה ואחסון:

struct ReducedPrecisionSupportInfo {
  // Arithmetic support
  bool has_8_bit_int_ = false;
  bool has_16_bit_int_ = false;
  bool has_16_bit_float_ = false;
  // Storage support
  bool has_16_bit_SSBO_ = false;
  bool has_16_bit_UBO_ = false;
  bool has_16_bit_push_ = false;
  bool has_16_bit_input_output_ = false;
  // Use 16-bit floats if we have arithmetic
  // support and at least SSBO storage support.
  bool use_16bit_floats_ = false;
};

void CheckFormatSupport(VkPhysicalDevice physical_device,
    ReducedPrecisionSupportInfo &info) {

  // Retrieve the device extension list so we
  // can check for our desired extensions.
  uint32_t device_extension_count;
  vkEnumerateDeviceExtensionProperties(physical_device, nullptr,
      &device_extension_count, nullptr);
  std::vector<VkExtensionProperties> device_extensions(device_extension_count);
  vkEnumerateDeviceExtensionProperties(physical_device, nullptr,
      &device_extension_count, device_extensions.data());

  bool has_16_8_extension = HasDeviceExtension("VK_KHR_shader_float16_int8",
      device_extensions);

  // Initialize the device features structure and
  // chain the storage features structure and 8/16-bit
  // support structure if applicable.
  VkPhysicalDeviceFeatures2 device_features;
  memset(&device_features, 0, sizeof(device_features));
  device_features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;

  VkPhysicalDeviceShaderFloat16Int8Features f16_int8_features;
  memset(&f16_int8_features, 0, sizeof(f16_int8_features));
  f16_int8_features.sType =
      VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FLOAT16_INT8_FEATURES_KHR;

  VkPhysicalDevice16BitStorageFeatures storage_features;
  memset(&storage_features, 0, sizeof(storage_features));
  storage_features.sType =
      VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_16BIT_STORAGE_FEATURES;
  device_features.pNext = &storage_features;

  if (has_16_8_extension) {
    storage_features.pNext = &f16_int8_features;
  }

  vkGetPhysicalDeviceFeatures2(physical_device, &device_features);

  // Parse the storage features and determine
  // what kinds of 16-bit storage access are available.
  if (storage_features.storageBuffer16BitAccess ||
      storage_features.uniformAndStorageBuffer16BitAccess) {
    info.has_16_bit_SSBO_ = true;
  }
  info.has_16_bit_UBO_ = storage_features.uniformAndStorageBuffer16BitAccess;
  info.has_16_bit_push_ = storage_features.storagePushConstant16;
  info.has_16_bit_input_output_ = storage_features.storageInputOutput16;

  info.has_16_bit_int_ = device_features.features.shaderInt16;
  if (has_16_8_extension) {
    info.has_16_bit_float_ = f16_int8_features.shaderFloat16;
    info.has_8_bit_int_ = f16_int8_features.shaderInt8;
  }

  // Get arithmetic and at least some form of storage
  // support before enabling 16-bit float usage.
  if (info.has_16_bit_float_ && info.has_16_bit_SSBO_) {
    info.use_16bit_floats_ = true;
  }
}

רמת הדיוק של הנתונים

מספר נקודה צפה (floating-point) עם דיוק חצי יכול לייצג טווח קטן יותר של ערכים בדיוק נמוך יותר מאשר מספר נקודה צפה עם דיוק יחיד. דיוק חצי הוא בדרך כלל בחירה פשוטה וללא אובדן מידע מבחינה תפיסתית, בהשוואה לדיוק יחיד. עם זאת, יכול להיות שדיוק חצי לא יהיה מעשי בכל תרחישי השימוש. בסוגים מסוימים של נתונים, הטווח והדיוק המופחתים עלולים לגרום לעיוותים גרפיים או לעיבוד שגוי.

סוגי נתונים שמתאימים לייצוג בנקודה צפה (floating point) בחצי דיוק כוללים:

  • נתוני מיקום בקואורדינטות של מרחב מקומי
  • קואורדינטות UV לטקסטורות קטנות יותר עם גלישת UV מוגבלת, שאפשר להגביל לטווח קואורדינטות של ‎-1.0 עד 1.0
  • נתונים רגילים, משיקים ודו-משיקים
  • נתוני צבע של קודקודים
  • נתונים עם דרישות דיוק נמוכות, שמתרכזים סביב 0.0

סוגי נתונים שלא מומלץ לייצג באמצעות מספרים ממשיים בחצי דיוק:

  • נתוני מיקום בקואורדינטות גלובליות
  • קואורדינטות UV של טקסטורה לתרחישי שימוש שדורשים דיוק גבוה, כמו קואורדינטות של רכיבים בממשק המשתמש בגיליון אטלס

דיוק בקוד של הצללה (shader)

שפות התכנות של שיידרים OpenGL Shading Language (GLSL)‎ ו-High-level Shader Language (HLSL) תומכות במפרט של דיוק מופחת או דיוק מפורש לסוגים מספריים. דיוק מופחת נחשב כהמלצה לקומפיילר של ה-shader. דיוק מפורש הוא דרישה של הדיוק שצוין. מכשירי Vulkan ב-Android בדרך כלל משתמשים בפורמטים של 16 ביט כשההצעה היא דיוק מופחת. מכשירים אחרים עם Vulkan, במיוחד מחשבים שולחניים עם חומרת גרפיקה שלא תומכת בפורמטים של 16 ביט, עשויים להתעלם מהדיוק המופחת ולהמשיך להשתמש בפורמטים של 32 ביט.

תוספי אחסון ב-GLSL

צריך להגדיר את התוספים המתאימים של GLSL כדי לתמוך בפורמטים מספריים של 16 ביט או 8 ביט במבנים של מאגר נתונים ושל מאגר נתונים אחיד. הצהרות התוספים הרלוונטיות הן:

// Enable 16-bit formats in storage and uniform buffers.
#extension GL_EXT_shader_16bit_storage : require
// Enable 8-bit formats in storage and uniform buffers.
#extension GL_EXT_shader_8bit_storage : require

התוספים האלה ספציפיים ל-GLSL ואין להם מקבילה ב-HLSL.

דיוק רגוע ב-GLSL

כדי להציע נקודה צפה (float) עם דיוק יחיד, משתמשים במגדיר highp לפני סוג הנקודה הצפה. כדי להציע נקודה צפה עם חצי דיוק, משתמשים במגדיר mediump. קומפיילרים של GLSL ל-Vulkan מפרשים את המאפיין lowp הקודם בתור mediump. הנה כמה דוגמאות לדיוק מופחת:

mediump vec4 my_vector; // Suggest 16-bit half precision
highp mat4 my_matrix;   // Suggest 32-bit single precision

דיוק מפורש ב-GLSL

כדי להשתמש בסוגים של נקודה צפה (floating point) של 16 ביט, צריך לכלול את התוסף GL_EXT_shader_explicit_arithmetic_types_float16 בקוד ה-GLSL:

#extension GL_EXT_shader_explicit_arithmetic_types_float16 : require

כדי להצהיר על סוגים של סקלר, וקטור ומטריצה של נקודה צפה (floating-point) של 16 ביט ב-GLSL, משתמשים במילות המפתח הבאות:

float16_t   f16vec2     f16vec3    f16vec4
f16mat2     f16mat3     f16mat4
f16mat2x2   f16mat2x3   f16mat2x4
f16mat3x2   f16mat3x3   f16mat3x4
f16mat4x2   f16mat4x3   f16mat4x4

מצהירים על סוגי סקלרים ווקטורים של מספרים שלמים של 16 ביט ב-GLSL באמצעות מילות המפתח הבאות:

int16_t     i16vec2     i16vec3    i16vec4
uint16_t    u16vec2     u16vec3    u16vec4

דיוק רגוע ב-HLSL

ב-HLSL משתמשים במונח דיוק מינימלי במקום דיוק מופחת. מילת מפתח של רמת דיוק מינימלית מציינת את הדיוק המינימלי, אבל הקומפיילר יכול להחליף אותה ברמת דיוק גבוהה יותר אם היא מתאימה יותר לחומרה של היעד. מספר נקודה צפה (float) של 16 ביט עם דיוק מינימלי מצוין באמצעות מילת המפתח min16float. מספרים שלמים של 16 ביט עם סימן ובלי סימן, עם דיוק מינימלי, מצוינים על ידי מילות המפתח min16int ו-min16uint, בהתאמה. דוגמאות נוספות להצהרות על דיוק מינימלי:

// Four element vector and four-by-four matrix types
min16float4 my_vector4;
min16float4x4 my_matrix4x4;

דיוק מפורש ב-HLSL

נקודה צפה (floating-point) בחצי דיוק מצוינת על ידי מילות המפתח half או float16_t. מספרים שלמים של 16 ביט עם סימן ובלי סימן מצוינים על ידי מילות המפתח int16_t ו-uint16_t, בהתאמה. דוגמאות נוספות להצהרות דיוק מפורשות:

// Four element vector and four-by-four matrix types
half4 my_vector4;
half4x4 my_matrix4x4;