NVIDIA Variable Rate Shading


「Variable Rate Shading」(VRS)這項技術(官網),是 NVIDIA 在推出 Turing GPU 架構時(2018 的事了),針對 VR 的需求推出的一項新功能。

這項技術的基本概念,主要就是透過局部性地降低 rasterization 階段的解析度,來減少 Pixel Shader(Fragment Shader)的計算量,藉此達到加速的目的。

下面就是他的基本流程:

根據他的原理,在 Pixel Shader 複雜的應用上,善用 VRS 理論上是可以有很好的效能增益的。

而會有這樣的需求,主要也是因為 VR 顯示時的解析度越來越高、所需的計算量也越來大,導致就算是頂級的顯示卡,可能也會出現沒辦法提供足夠的更新率的關係。

但是如果按照傳統的方法、直接降低全部的解析度,又會造成畫質的降低;不過如果是透過 VRS 這種可以針對區域設定不同的解析度的話,則就可以考慮顯示畫面的性質,在必要的部分使用高畫質的運算、在非必要的部分降低解析度,藉此達到加速的效果了!

下面就是一個 NVIDIA 官方提供的示意圖:

可以看到,他就是遠景、和有車子本身使用最高的解析度下去繪製,而其他的區域,則是降低解析度來畫。

而如果是 VR 環境的話,則是可以考慮到鏡片的性質,只保留中間區域有最高的解析度,往外面則陸續降低、甚至不去繪製。

如果有搭配眼球追蹤的裝置的話,甚至可以針對視線的方向來做設定、更有效率地分配 GPU 繪製的能力。

更詳細的說明,則可以參考《Turing Variable Rate Shading in VRWorks》一文。


在 Heresy 來看,VRS 的另一個好處,是相對地好用!

只要有 Turing 架構的顯示卡(主要是 RTX 系列),就可以透過 DX11、DX12、OpenGL、Vulkan 的 API extension 來做設定;而且他的設定方法又相對簡單,基本上不會影響到本來的 render pipeline!所以要在既有的程式裡面加上這項功能,也是相當簡單的~

如果只是要在 OpenGL 中使用 VRS 的話,其實不用下載、額外安裝任何東西,只要去使用特定的 OpenGL extension 就可以了。

而如果是需要範例程式參考的話,NVIDIA 則是把它包在 NVIDIA VRWorks(官網)中,需要註冊才能下載。(不知道為什麼沒放到 GitHub 上…)

以 OpenGL 來說,相關的資料都在 variable_rate_shading_ogl 這個資料夾中,最基本的範例應該就是 demo\gl_shading_rate_image 下的程式了。

前置作業

如果所使用的 extension 工具(例如 glew)沒有支援這些 extension 的話,則就需要自己定義一些變數、並透過 wglGetProcAddress() 這類的函式,來取得這些 VRS 相關的函式。

在範例中,定義的部分包括了:

#define GL_SHADING_RATE_IMAGE_NV                           0x9563
 
#define GL_SHADING_RATE_NO_INVOCATIONS_NV                  0x9564
#define GL_SHADING_RATE_1_INVOCATION_PER_PIXEL_NV          0x9565
#define GL_SHADING_RATE_1_INVOCATION_PER_1X2_PIXELS_NV     0x9566
#define GL_SHADING_RATE_1_INVOCATION_PER_2X1_PIXELS_NV     0x9567
#define GL_SHADING_RATE_1_INVOCATION_PER_2X2_PIXELS_NV     0x9568
#define GL_SHADING_RATE_1_INVOCATION_PER_2X4_PIXELS_NV     0x9569
#define GL_SHADING_RATE_1_INVOCATION_PER_4X2_PIXELS_NV     0x956A
#define GL_SHADING_RATE_1_INVOCATION_PER_4X4_PIXELS_NV     0x956B
#define GL_SHADING_RATE_2_INVOCATIONS_PER_PIXEL_NV         0x956C
#define GL_SHADING_RATE_4_INVOCATIONS_PER_PIXEL_NV         0x956D
#define GL_SHADING_RATE_8_INVOCATIONS_PER_PIXEL_NV         0x956E
#define GL_SHADING_RATE_16_INVOCATIONS_PER_PIXEL_NV        0x956F
 
#define GL_SHADING_RATE_IMAGE_BINDING_NV                   0x955B
#define GL_SHADING_RATE_IMAGE_TEXEL_WIDTH_NV               0x955C
#define GL_SHADING_RATE_IMAGE_TEXEL_HEIGHT_NV              0x955D
#define GL_SHADING_RATE_IMAGE_PALETTE_SIZE_NV              0x955E
#define GL_MAX_COARSE_FRAGMENT_SAMPLES_NV                  0x955F
 
#define GL_SHADING_RATE_SAMPLE_ORDER_DEFAULT_NV            0x95AE
#define GL_SHADING_RATE_SAMPLE_ORDER_PIXEL_MAJOR_NV        0x95AF
#define GL_SHADING_RATE_SAMPLE_ORDER_SAMPLE_MAJOR_NV       0x95B0

其中,第二組的 GL_SHADING_RATE_*_INVOCATIONS_*_NV 就是之後要用來指定要解析度變化比例的參數。

相關的函式則有六個,定義可以寫成:

typedef void (GLAPIENTRY * PFNGLBINDSHADINGRATEIMAGENVPROC) (GLuint texture);
typedef void (GLAPIENTRY * PFNGLSHADINGRATEIMAGEPALETTENVPROC) (GLuint viewport, GLuint first, GLuint count, const GLenum *rates);
typedef void (GLAPIENTRY * PFNGLGETSHADINGRATEIMAGEPALETTENVPROC) (GLuint viewport, GLuint entry, GLenum *rate);
typedef void (GLAPIENTRY * PFNGLSHADINGRATEIMAGEBARRIERNVPROC) (GLboolean synchronize);
typedef void (GLAPIENTRY * PFNGLSHADINGRATESAMPLEORDERCUSTOMNVPROC)(GLenum rate, GLuint samples, const GLint *locations);
typedef void (GLAPIENTRY * PFNGLGETSHADINGRATESAMPLELOCATIONIVNVPROC) (GLenum rate, GLuint index, GLint *location);
 
PFNGLBINDSHADINGRATEIMAGENVPROC glBindShadingRateImageNV(nullptr);
PFNGLSHADINGRATEIMAGEPALETTENVPROC glShadingRateImagePaletteNV(nullptr);
PFNGLGETSHADINGRATEIMAGEPALETTENVPROC glGetShadingRateImagePaletteNV(nullptr);
PFNGLSHADINGRATEIMAGEBARRIERNVPROC glShadingRateImageBarrierNV(nullptr);
PFNGLSHADINGRATESAMPLEORDERCUSTOMNVPROC glShadingRateSampleOrderCustomNV(nullptr);
PFNGLGETSHADINGRATESAMPLELOCATIONIVNVPROC glGetShadingRateSampleLocationivNV(nullptr);

接下來,在初始化的時候,建議可以先透過檢查 GL_NV_shading_rate_image 這個 OpenGL extension 來確認執行環境有沒有支援 VRS,期程式可以寫成:

bool shadingRateImageExtensionFound = false;
glGetIntegerv(GL_NUM_EXTENSIONS, &numExtensions);
for (GLint i = 0; i < numExtensions && !shadingRateImageExtensionFound; ++i)
{   std::string name((const char*)glGetStringi(GL_EXTENSIONS, i));
   shadingRateImageExtensionFound = (name == "GL_NV_shading_rate_image"); }

如果有支援的話,就可以透過下面的程式來取得對應的函式了~

glBindShadingRateImageNV = (PFNGLBINDSHADINGRATEIMAGENVPROC)wglGetProcAddress("glBindShadingRateImageNV");
glShadingRateImagePaletteNV = (PFNGLSHADINGRATEIMAGEPALETTENVPROC)wglGetProcAddress("glShadingRateImagePaletteNV");
glGetShadingRateImagePaletteNV = (PFNGLGETSHADINGRATEIMAGEPALETTENVPROC)wglGetProcAddress("glGetShadingRateImagePaletteNV");
glShadingRateImageBarrierNV = (PFNGLSHADINGRATEIMAGEBARRIERNVPROC)wglGetProcAddress("glShadingRateImageBarrierNV");
glShadingRateSampleOrderCustomNV = (PFNGLSHADINGRATESAMPLEORDERCUSTOMNVPROC)wglGetProcAddress("glShadingRateSampleOrderCustomNV");
glGetShadingRateSampleLocationivNV = (PFNGLGETSHADINGRATESAMPLELOCATIONIVNVPROC)wglGetProcAddress("glGetShadingRateSampleLocationivNV");

執行後、如果其中有任何一個函式是 nullptr 的話,應該也需要視為 VRS 環境有問題了。


建立 Shading Rate Image

前置作業都準備好了之後,再來就是要先建立符合自己需求的 shading rate image;所謂 shading rate image 基本上就是一個 2D Texture,用裡面的 pixel 來代表每個區域要使用的解析度比例。

由於 shading rate image 是使用 palette 的形式,也就是圖片裡面每一個像素的值都只是一個索引值,還要再去查表才能知道他真正的意義;所以這邊還需要先建立一個對照表。

GLint iPaletteSize;
glGetIntegerv(GL_SHADING_RATE_IMAGE_PALETTE_SIZE_NV, &iPaletteSize);

GLenum* aPalette = new GLenum[iPaletteSize];
aPalette[0] = GL_SHADING_RATE_NO_INVOCATIONS_NV;
aPalette[1] = GL_SHADING_RATE_1_INVOCATION_PER_PIXEL_NV;
aPalette[2] = GL_SHADING_RATE_1_INVOCATION_PER_2X2_PIXELS_NV;
aPalette[3] = GL_SHADING_RATE_1_INVOCATION_PER_4X4_PIXELS_NV;

for (size_t i = 4; i < iPaletteSize; ++i)
     aPalette[i] = GL_SHADING_RATE_1_INVOCATION_PER_PIXEL_NV; glShadingRateImagePaletteNV(0, 0, iPaletteSize, aPalette); delete[] aPalette;

在上面的程式碼中,是要先去取得 GL_SHADING_RATE_IMAGE_PALETTE_SIZE_NV 的值,也就是 palette 的大小(iPaletteSize、目前應該是 16)。

之後就是建立出這個這張 palette 的表(aPalette),他的型別是 GLenum

在設定好之後,則是透過 glShadingRateImagePaletteNV() 這個函式,來針對每個 viewport 設定指定的 palette。(在多個 viewport 的情況下,要修改第一個參數)

Palette 設定好了後,再來就是要設定 shading image 了。

GLint iTexelHeight, iTexelWidth;
glGetIntegerv(GL_SHADING_RATE_IMAGE_TEXEL_WIDTH_NV, &iTexelWidth);
glGetIntegerv(GL_SHADING_RATE_IMAGE_TEXEL_HEIGHT_NV, &iTexelHeight);

GLsizei iWidth = 500 / iTexelWidth,
        iHeight = 500 / iTexelHeight; unsigned char* aImage = new unsigned char[iWidth * iHeight]; for (int y = 0; y < iHeight; ++y)
     for (int x = 0; x < iWidth; ++x)
     {
         int iIdx = x + y * iWidth;
         if (x > iWidth / 2) aImage[iIdx] = 1;
         else                aImage[iIdx] = 3;
     } GLuint iShadingImage = 0; glEnable(GL_TEXTURE_2D); glGenTextures(1, &iShadingImage); glBindTexture(GL_TEXTURE_2D, iShadingImage); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); glTexStorage2D(GL_TEXTURE_2D, 1, GL_R8UI, iWidth, iHeight); glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, iWidth, iHeight,
                GL_RED_INTEGER, GL_UNSIGNED_BYTE, aImage); glBindTexture(GL_TEXTURE_2D, 0); glBindShadingRateImageNV(iShadingImage); delete[] aImage;

VRS 基本上是要把多個 pixel 合併起來處理,而到底是把幾個 pixel 合併處理,則是要透過 GL_SHADING_RATE_IMAGE_TEXEL_WIDTH_NVGL_SHADING_RATE_IMAGE_TEXEL_HEIGHT_NV 來取得;以目前來說,大小應該會是 16 x 16。

而也因為是這樣合併處理,所以 shading rate image 需要的大小也會比 frame buffer 小許多;其大小基本上就是去除以前面取得的 texel 大小了。

之後,則就是要把這張圖(aImage)裡面每個點都指定一個對應到前面的 palette 的索引值,完成整個畫面中、各區域的 shading rate 了。(這邊是一半 1、一半 3,實際上並不應該這樣用)
再來,則就是要在把這張圖建立成 OpenGL 的 2D Texture,他個格式要是 GL_R8UI

最後,則是要透過 glBindShadingRateImageNV(),來設定要把這個 texture 當作 shading rate image 來用了。

而如果有需要動態調整 shading rate,則就是要在必要的時候更新這個 texture 的內容了。


啟用與關閉

前面都設定好了之後,當要在繪製時使用 VRS 的時候,則是需要透過 glEnable() 來開啟這項功能:

glEnable(GL_SHADING_RATE_IMAGE_NV);

而如果不需要這項功能的時候,則可以透過 glDisable() 來關閉:

glDisable(GL_SHADING_RATE_IMAGE_NV);

基本上,這樣就可以完成 VRS 的套用了~而比較大的重點,應該還是怎麼去設計符合自己需求的 shading rate image 了。

在 Heresy 本來的 VR 專案裡,基本上可以很簡單地套用。不過或許是因為 Heresy 這邊的 Fragment Shader 太過簡單,在效能上似乎沒有明顯的增益。

而當試著要寫一個簡單的範例來套用的時候,卻不知道為什麼一直沒能成功…也還不太確定到底是什麼問題?也因此,這次也沒能真的提供一個可以跑的範例。

總之,大致上就是這樣了。如果有興趣的話,建議直接參考官方的範例吧~


另外,VRS 還有延伸成「Variable Rate Supersampling」(VRSS)可以設定局部區域使用 Supersampling 的技術來提升畫質。

發表迴響

在下方填入你的資料或按右方圖示以社群網站登入:

WordPress.com 標誌

您的留言將使用 WordPress.com 帳號。 登出 /  變更 )

Google photo

您的留言將使用 Google 帳號。 登出 /  變更 )

Twitter picture

您的留言將使用 Twitter 帳號。 登出 /  變更 )

Facebook照片

您的留言將使用 Facebook 帳號。 登出 /  變更 )

連結到 %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.