2019年11月25日 星期一

運用 Viewport 修正 Vulkan 的Y軸繪圖空間

運用 Viewport 修正 Vulkan 的Y軸繪圖空間 

前言

  最近在學習 Vulkan ,但發現到 Vulkan 的Y軸繪圖空間跟 Direct3D 與 OpenGL 不一樣,幸運地,是有辦法將會Y軸繪圖空間修正回來,在此把學習的過程做個紀錄。

內容

  首先先來了解 Vulkan 的Y軸繪圖空間有何不一樣,如下圖
Y軸繪圖空間的差異

可以在圖中看到 Vulkan 的Y軸繪圖空間是從"下到上",但舊的 API 的Y軸繪圖空間卻是從"下到上",如果是從舊的 API 整合 Vulkan 到繪圖引擎就會發生繪製的結果上下顛倒的問題,信運地,在經過 google 後找到  [ Sascha Willems ] Flipping the Vulkan viewport ,裡面有提到解決的方法。

  解決的方法是透過 Viewport 來修正Y軸的繪圖空間,但要用這個方法時必須開啟"VK_KHR_maintenance1",這是一個 Device extension ,要在 VkDeviceCreateInfo 裡的啟動。在啟動"VK_KHR_maintenance1"後,就可以利用 Viewport  來修正Y軸了,如何修正呢?在 VkViewport::height 裡填上"負"的高度,這樣整個Y軸繪製空間就會變成"下到上",如果沒開啟"VK_KHR_maintenance1"填上負值會在 Debug 訊息回報該值不能是負值。填完"負"的高度得到的結果如下圖
填完"負"的高度的繪製結果

圖中 Render result 的部分是繪製的結果但這次的Y軸是"下到上",藍色的部分是最後會被寫到 Render target 的區域,所以還要將繪製的結果校正到這個區域才能達到目的,如何校正呢?在 VkViewport::y 裡加一個"正"的高度就可以達到目的。最後提供一個轉換的範例如下
void SetUpViewportAndCorrectYAxis(VkViewport& viewport,float x,float y,float width,float height,float minDepth,float maxDepth)
{
  viewport.x = x;
  viewport.y = -y + height;
  viewport.width = width;
  viewport.height = -height;
  viewport.minDepth = mixDepth;
  viewport.maxDepth = maxDepth;
}

參考資料

[ Sascha Willems ] Flipping the Vulkan viewport

2019年11月18日 星期一

開啟Vulkan的 Debug mode

開啟Vulkan的 Debug mode

前言

  最近在入門 Vulkan ,發現開啟 Debug mode 有一些麻煩,不像 Direct3D 只要設定旗標就可以開啟, Vulkan 需要比較多的步驟,在此把學習的過程做個紀錄。

內容

  在創建 VkInstance 會透過 VkInstanceCreateInfo 來輸入參數, VkInstanceCreateInfo 的定義如下
typedef struct VkInstanceCreateInfo {
    VkStructureType             sType;
    const void*                 pNext;
    VkInstanceCreateFlags       flags;
    const VkApplicationInfo*    pApplicationInfo;
    uint32_t                    enabledLayerCount;
    const char* const*          ppEnabledLayerNames;
    uint32_t                    enabledExtensionCount;
    const char* const*          ppEnabledExtensionNames;
} VkInstanceCreateInfo;

在 ppEnabledLayerNames 的陣列裡要加入"VK_LAYER_KHRONOS_validation",而 ppEnabledExtensionNames 的陣列要加入"VK_EXT_debug_utils",這兩個缺一不可,由於剛入門所以有踩到坑,我踩得坑是指加入"VK_EXT_debug_utils",會發生什麼事呢?我會告訴你行為不可預期, VkInstance 雖然可以正常產出,但其它的 API 可能會出問題,我發生問題狀況是 vkCreateGraphicsPipelines() 喚起時一定 Access violation 。在 pNext 的參數要設定VkDebugUtilsMessengerCreateInfoEXT ,範例如下
VkInstanceCreateInfo createInstanceInfo;
//...
//
VkDebugUtilsMessengerCreateInfoEXT debugUtilsMessengerCreateInfo;
debugUtilsMessengerCreateInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
debugUtilsMessengerCreateInfo.pNext = NULL;
debugUtilsMessengerCreateInfo.flags = 0;
debugUtilsMessengerCreateInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT|VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT;
debugUtilsMessengerCreateInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT |
                                            VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT |
                                            VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;
debugUtilsMessengerCreateInfo.pfnUserCallback = myDebugCallBack;
debugUtilsMessengerCreateInfo.pUserData = NULL;
//
createInstanceInfo.pNext = &debugUtilsMessengerCreateInfo;
//
//call vkCreateInstance() to create
//...

在設定 pfnUserCallback 時需要一個 Function pointer ,可以設定成以下( 在Windows 平台)
VkBool32 myDebugCallBack(VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,VkDebugUtilsMessageTypeFlagsEXT messageType,const VkDebugUtilsMessengerCallbackDataEXT *pCallbackData,void *pUserData)
{
  char prefix[64] = "";
  char *message = (char *)malloc(strlen(pCallbackData->pMessage) + 5000);
  //
  if (messageSeverity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT) 
  {
    strcat(prefix, "VERBOSE : ");
  } 
  else if (messageSeverity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT) 
  {
    strcat(prefix, "INFO : ");
  } 
  else if (messageSeverity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) 
  {
    strcat(prefix, "WARNING : ");
  } 
  else if (messageSeverity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) 
  {
    strcat(prefix, "ERROR : ");
  }
  //
  if (messageType & VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT) 
  {
    strcat(prefix, "GENERAL");
  } 
  else 
  {
    if (messageType & VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT) 
    {
      strcat(prefix, "VALIDATION");  
    }
    if (messageType & VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT) 
    {
      if (messageType & VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT) 
      {
        strcat(prefix, "|");
      }
      strcat(prefix, "PERFORMANCE");
    }
  }
  sprintf(message, "%s - Message Id Number: %d | Message Id Name: %s\n\t%s\n", prefix, pCallbackData->messageIdNumber,
  pCallbackData->pMessageIdName, pCallbackData->pMessage);
  if (pCallbackData->objectCount > 0) 
  {
    char tmp_message[500];
    sprintf(tmp_message, "\n\tObjects - %d\n", pCallbackData->objectCount);
    strcat(message, tmp_message);
    for (uint32_t object = 0; object < pCallbackData->objectCount; ++object) 
    {
      if (NULL != pCallbackData->pObjects[object].pObjectName && strlen(pCallbackData->pObjects[object].pObjectName) > 0) 
      {
        sprintf(tmp_message, "\t\tObject[%d] - %s, Handle %p, Name \"%s\"\n", object,
                ConvertVkObjectType(pCallbackData->pObjects[object].objectType),
                (void *)(pCallbackData->pObjects[object].objectHandle), pCallbackData->pObjects[object].pObjectName);
      } 
      else 
      {
        sprintf(tmp_message, "\t\tObject[%d] - %s, Handle %p\n", object,
                ConvertVkObjectType(pCallbackData->pObjects[object].objectType),
                (void *)(pCallbackData->pObjects[object].objectHandle));
      }
      strcat(message, tmp_message);
    }
  }
  if (pCallbackData->cmdBufLabelCount > 0) 
  {
    char tmp_message[500];
    sprintf(tmp_message, "\n\tCommand Buffer Labels - %d\n", pCallbackData->cmdBufLabelCount);
    strcat(message, tmp_message);
    for (uint32_t cmd_buf_label = 0; cmd_buf_label < pCallbackData->cmdBufLabelCount; ++cmd_buf_label) 
    {
      sprintf(tmp_message, "\t\tLabel[%d] - %s { %f, %f, %f, %f}\n", cmd_buf_label,
              pCallbackData->pCmdBufLabels[cmd_buf_label].pLabelName, pCallbackData->pCmdBufLabels[cmd_buf_label].color[0],
              pCallbackData->pCmdBufLabels[cmd_buf_label].color[1], pCallbackData->pCmdBufLabels[cmd_buf_label].color[2],
              pCallbackData->pCmdBufLabels[cmd_buf_label].color[3]);
      strcat(message, tmp_message);
    }
  }

  ::OutputDebugStringA(message);
  fflush(stdout);
  //
  free(message);

  // Don't bail out, but keep going.
  return false;
}

這個 Function 是從 Vulkan SDK 的範例裡拔出來的,紅字的部分是顯示 Debug 訊息的地方,請依不同的平台需求來改造。設定 VkInstanceCreateInfo.pNext 的部分要指向 VkDebugUtilsMessengerCreateInfoEXT ,這個用法相當特別,正常的 pNext 都會是 NULL。

  在 VkInstanceCreateInfo 裡設好參數並確實的創建 VkInstance 後需要取得 Debug mode 需要的 Function ,因為是 Extension ,所以要另外取得可以理解,範例如下
fCreateDebugUtilsMessengerEXT =(PFN_vkCreateDebugUtilsMessengerEXT)vkGetInstanceProcAddr(hInstance,"vkCreateDebugUtilsMessengerEXT");
fDestroyDebugUtilsMessengerEXT =(PFN_vkDestroyDebugUtilsMessengerEXT)vkGetInstanceProcAddr(hInstance,"vkDestroyDebugUtilsMessengerEXT");
fSubmitDebugUtilsMessageEXT =(PFN_vkSubmitDebugUtilsMessageEXT)vkGetInstanceProcAddr(hInstance,"vkSubmitDebugUtilsMessageEXT");
fCmdBeginDebugUtilsLabelEXT=(PFN_vkCmdBeginDebugUtilsLabelEXT)vkGetInstanceProcAddr(hInstance,"vkCmdBeginDebugUtilsLabelEXT");
fCmdEndDebugUtilsLabelEXT=(PFN_vkCmdEndDebugUtilsLabelEXT)vkGetInstanceProcAddr(hInstance,"vkCmdEndDebugUtilsLabelEXT");
fCmdInsertDebugUtilsLabelEXT=(PFN_vkCmdInsertDebugUtilsLabelEXT)vkGetInstanceProcAddr(hInstance,"vkCmdInsertDebugUtilsLabelEXT");
fSetDebugUtilsObjectNameEXT=(PFN_vkSetDebugUtilsObjectNameEXT)vkGetInstanceProcAddr(hInstance,"vkSetDebugUtilsObjectNameEXT");

這每個都是 Function pointer ,範例沒宣告型別名稱,要型別名稱可以看式子的右側都有 cast。取得 Function pointer 後再利用 fCreateDebugUtilsMessengerEXT() 來創建 VkDebugUtilsMessengerEXT  ,範例如下
VkDebugUtilsMessengerEXT hDebugUtilsMessenger;
//Create...
fCreateDebugUtilsMessengerEXT(hInstance,&debugUtilsMessengerCreateInfo,NULL,&hDebugUtilsMessenger);

//Destroy...
fDestroyDebugUtilsMessengerEXT(hInstance,hDebugUtilsMessenger,NULL);

在創建完後就代表 Debug mode 開啟了。既然創建了 VkDebugUtilsMessengerEXT ,在程式最後要記得透過fDestroyDebugUtilsMessengerEXT() 來銷毀 VkDebugUtilsMessengerEXT 。

  最後抱怨一下, 這個啟動過程的設計我覺得非常糟糕,流程只有兩個,創建 VkInstance 與創建 VkDebugUtilsMessengerEXT ,但兩個流程的參數都需要 VkDebugUtilsMessengerCreateInfoEXT ,設定的方法還很特別,不看範例很難記得。還有在啟動"VK_LAYER_KHRONOS_validation"與"VK_EXT_debug_utils"的部分也設計得很糟,Layer 與 Extension 的啟動有相依性,錯了也不報錯。Call back 的部分我覺得明明官方就可以給預設,但就是要自己寫一個,規格還有點複雜,這部份的設計也很奇怪。

參考資料

[ 書籍 ] Vulkan 應用開發指南,出版商:人民郵電,ISBN-13:9787115506801

2019年11月11日 星期一

關於 ID3D12GraphicsCommandList::SetDescriptorHeaps()

關於 ID3D12GraphicsCommandList::SetDescriptorHeaps()

前言

  最近在研究 Direct3D12 ,手邊的資料有 Direct3D12 的範例與 [ 書籍 ] DirectX 12 3D 游戲開發實戰,出版商:人民郵電出版社,ISBN-13:9787115479211 ,本以為就入門來說應該很充足,但在實務上會碰到一些不知道在做什麼的 Function ,就像是 ID3D12GraphicsCommandList::SetDescriptorHeaps() ,這個 Function 不論是 [ MSDN ]ID3D12GraphicsCommandList::SetDescriptorHeaps method 或是手邊的資料都沒有說得很清楚,都使是在範例裡有寫這個步驟,但都不特別解釋,所以我就對這個 Funciton 做了研究,在此做個紀錄。

內容

  ID3D12GraphicsCommandList::SetDescriptorHeaps() 這個 Function 在介面上有些設計不良,
介面如下
void SetDescriptorHeaps(
  UINT                 NumDescriptorHeaps,
  ID3D12DescriptorHeap * const *ppDescriptorHeaps
);

介面看起來會讓人很直覺得可以輸入很多個 ID3D12DescriptorHeap ,但是事實上是輸入的 DescriptorHeap 是有限制的!每個 ID3D12DescriptorHeap 在 Create 的時候會需要 D3D12_DESCRIPTOR_HEAP_TYPE,定義如下
typedef enum D3D12_DESCRIPTOR_HEAP_TYPE
{
    D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV,    // Constant buffer/Shader resource/Unordered access views
    D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER,        // Samplers
    D3D12_DESCRIPTOR_HEAP_TYPE_RTV,            // Render target view
    D3D12_DESCRIPTOR_HEAP_TYPE_DSV,            // Depth stencil view
    D3D12_DESCRIPTOR_HEAP_TYPE_NUM_TYPES       // Simply the number of descriptor heap types
} D3D12_DESCRIPTOR_HEAP_TYPE;

這個 Type 會決定 DescriptorHeap 的型態, Create 完後不能更改,而在 ID3D12GraphicsCommandList::SetDescriptorHeaps() 輸入的每個 DescriptorHeap 必須是不同的  D3D12_DESCRIPTOR_HEAP_TYPE ,加上這個 Function 其實只處理"輸入"的部分,所以D3D12_DESCRIPTOR_HEAP_TYPE_RTV 與 D3D12_DESCRIPTOR_HEAP_TYPE_DSV 就不會用到,所以實際上最多只能輸入兩個 DescriptorHeap !這些在 MSDN 上都沒有很詳述的說明,我是在輸入同一種 Type 後會出現 Debug 的錯誤訊息才知道這個 Fuction 不能輸入不同 Type 的 DescriptorHeap 。個人認為理想的介面應該如下
void SetDescriptorHeaps(
  ID3D12DescriptorHeap * pDescriptorHeapsType_CBV_SRV_UAV;
  ID3D12DescriptorHeap * pDescriptorHeapsType_SAMPLER;
);

  接著來說一下喚起的時機,如下圖
SetDescriptorHeaps() 的喚起時機

如中上方的部分說明只能接收兩種 DescriptorHeap ,接著下方的 Draw call 是指每次的繪圖會透過 SetGraphicsRootDescriptorTable() 時要注意 GPUDescriptorHandle 必須來自 SetDescriptorHeaps() 的 DescriptorHeap ,手邊的資料也都沒明說這樣的規則,這是經過多次的實驗得來的經驗。如果需要切換 DescriptorHeap 的話,要注意切換的時機要發生在 Draw call 與 Draw call 之間,不能發生在 Draw call 的裡面,舉個例來說,如果一個 Draw call 裡需要做兩次 SetGraphicsRootDescriptorTable() ,但期望來自兩個 DescriptorHeap( D3D12_DESCRIPTOR_HEAP_TYPE 是一樣的),這個時候發生需要在同一個 Draw call 裡喚起兩次 SetDescriptorHeaps(),如以下範例
ComPtr<ID3D12DescriptorHeap> m_myHeap1;
ComPtr<ID3D12DescriptorHeap> m_myHeap2;
//Init heap1 and heap2
//...
//
ID3D12DescriptorHeap* ppHeaps[] = { m_myHeap1.Get()};
m_commandList->SetDescriptorHeaps(_countof(ppHeaps), ppHeaps);
m_commandList->SetGraphicsRootDescriptorTable(0,m_myHeap1->GetGPUDescriptorHandleForHeapStart() );
ppHeaps[0] = m_myHeap2.Get();
m_commandList->SetDescriptorHeaps(_countof(ppHeaps), ppHeaps);
m_commandList->SetGraphicsRootDescriptorTable(1,m_myHeap2->GetGPUDescriptorHandleForHeapStart() );
m_commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
m_commandList->IASetVertexBuffers(0, 1, &m_vertexBufferView);
m_commandList->DrawInstanced(3, 1, 0, 0);

這個做法目前不確定是否為"正規"用法,因為這個用法在 NVIDIA GeForce GTX1050 的顯卡時可以正常工作,但在 Intel HD Graphics 630 時無法正常工作,所以不確定是否可以這樣用,
說明文件也沒說明是否可這樣用,這部分目前的解決方案是絕不在 Draw call 中切換 DescriptorHeap ,這樣可以取得較好的兼容性。

參考資料

[ MSDN ]ID3D12GraphicsCommandList::SetDescriptorHeaps method
[ 書籍 ] DirectX 12 3D 游戲開發實戰,出版商:人民郵電出版社,ISBN-13:9787115479211

2019年11月4日 星期一

在 Windows 平台建置 LLVM 專案

在 Windows 平台建置 LLVM 專案

前言

  最近買了 [ 書籍 ] LLVM編譯器實戰教程,出版發行:機械工業出版社,ISBN-13:9787111631972,想要入門 LLVM,但發現 LLVM 專案在 Windows 平台建置時不是很直覺,所以在此做個紀錄。

內容

  LLVM 的專案在 Windows 平台是用 CMake 來產生專案,可以到 CMake 官網 下載。接著是關於 LLVM 的專案如何下載?可以在 LLVM 官網  下載,如下圖
LLVM 官網的下載

圖中可以看到有很多的專案,但那些專案是需要下載的呢?其實我也不知道,因為我剛入門並不熟悉,但官方有提供 Git 的位址,可以一次下載整個 LLVM 專案, Git 的位置如下
https://github.com/llvm/llvm-project.git

透過這個 Git 可以取得 LLVM 整個專案的原始碼。

  在取得原始碼專案後,接著就是用 CMake 來產生專案,在設定好 Source code 與 binaries 的位置後,請新增兩個 Entry ,如下圖
新增 CMAKE_INSTALL_PREFIX


新增 LLVM_TARGETS_TO_BUILD

CMAKE_INSTALL_PREFIX 的位置是指建置完的 .h 、 .lib 與工具的執行檔要放的位址,增加完兩個 Entry 後,就按下 Configure 設定編譯器,接著按下 Generate ,就可以產生專案如下圖
產生專案

在圖中"1"的位置會看到紅字,請不要理會它,這是正常現象,接著是圖中"2"的位置會看到一大推 not found ,這也是正常現象,小小的抱怨一下,這種建置訊息會留下"看似"有問題的訊息實在不是很欣賞的建置。

  接著開啟用 CMake 產生的方案後,建置 INSTALL 專案,如下圖
建置 INSTALL 專案

建置完後就可以在之前 CMAKE_INSTALL_PREFIX 的位置裡得到結果。

參考資料

[ 書籍 ] LLVM編譯器實戰教程,出版發行:機械工業出版社,ISBN-13:9787111631972
CMake 官網
LLVM 官網