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

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

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

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

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

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

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

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

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

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

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

יש עלות חישובית להמרה מנקודה צפה (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 את הפונקציה VkPhysicalDeviceShaderFloat16Int8Features.
  3. בודקים את השדות shaderFloat16 ו-shaderInt8 במבנה התוצאה VkPhysicalDeviceShaderFloat16Int8Features אחרי שמפעילים את vkGetPhysicalDeviceFeatures2(). אם ערך השדה הוא true, הפורמט נתמך בחשבון של תוכנית ה-shader.

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

תמיכה באחסון

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

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

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

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

  • נתוני מיקום בקואורדינטות של מרחב מקומי
  • קואורדינטות 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 נעשה שימוש במונח רמת דיוק מינימלית במקום רמת דיוק מופחתת. מילת מפתח מסוג דיוק מינימלי מציינת את הדיוק המינימלי, אבל המהדר יכול להחליף אותה בדיוק גבוה יותר אם הדיוק הגבוה יותר הוא בחירה טובה יותר לחומרה היעד. מילת המפתח min16float מציינת ערך מדויק מינימלי של 16 ביט. מילות המפתח 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;

-enable-16bit-types