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

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

פורמטים אופטימליים עומדים בדרישות הבאות:

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

פורמטים של נקודה צפה

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

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

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

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

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

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

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

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

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

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

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

תמיכה בחשבון

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

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

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

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

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

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

תמיכה בנפח האחסון

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

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

  • אובייקטים של מאגר אחסון
  • אובייקטים של מאגר אחיד
  • דחיפת בלוקים של ערכי קבוע
  • ממשקי קלט ופלט של Shader

רוב המכשירים עם Vulkan 1.1 ב-Android תומכים בפורמטים של 16 ביט באובייקטים של מאגרי אחסון, אבל לא כולם. אל תניחו שיש תמיכה על סמך דגם ה-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;
  }
}

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

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

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

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

סוגי הנתונים הבאים לא מומלצים לייצוג ב-float עם דיוק חצי:

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

דיוק בקוד של שפת השיז'ר

שפות התכנות של שיבוטים (shaders) OpenGL Shading Language‏ (GLSL) ו-High-level Shader Language‏ (HLSL) תומכות בהגדרת רמת דיוק מופחתת או רמת דיוק מפורשת לסוגי נתונים מספריים. רמת הדיוק המותרת נחשבת להמלצה למהדר של שפת השיז'ר. רמת הדיוק המפורשת היא דרישה של רמת הדיוק שצוינה. בדרך כלל, במכשירי 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

משתמשים במאפיין המזהה 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

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

#extension GL_EXT_shader_explicit_arithmetic_types_float16 : require

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

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

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

int16_t     i16vec2     i16vec3    i16vec4
uint16_t    u16vec2     u16vec3    u16vec4

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

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

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

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

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

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