ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Variable Shading Rate(VRS)
    Graphics/기본 2022. 9. 27. 00:08

    목차

    1. 개요
    2. 내용
     2.1. 디바이스 생성
     2.2. VRS 이미지 생성
     2.3. VRS 사용을 위한 Graphics Pipeline State 생성
     2.4. VRS 디버깅을 위한 코드 추가
     2.5. 구현결과

     2.6. 구현 코드
    3. 레퍼런스

     

    1. 개요

    Variable Shading Rate(VRS) 에 대해서 알아보고 구현해봅시다.

     

    VRS 는 화면의 영역별로 쉐이딩 비율을 조정하는 기능입니다. 그림1을 보면, 1x1 부터 4x4 까지 다양한 쉐이딩 비율이 있습니다. 1x1 이 VRS 를 사용하지 않은 기본 비율입니다. 그리고 2x2 는 4픽셀당 1번 쉐이딩을 합니다. 

     

    이 기능을 활용하여 사람의 시야에서 인지적으로 크게 영향을 미치지 않는 부분는 더 적은 수의 픽셀을 쉐이딩 하여 퍼포먼스를 얻을 수 있을 것입니다. 그림2에 보는바와 같이 이 기능은 VR에서 활용도가 높을 것입니다.

    그림1. VRS 를 적용한 모습 (출처 : 레퍼런스1)

     

    그림2. 시선 추적을 적용한 VRS (출처 : 레퍼런스2)

     

    2. 내용

    VRS 의 구현은 간단합니다. 화면의 쉐이딩 비율을 저장할 이미지를 하나 생성합니다. 그리고 그 이미지에 각 스크린에 맞는 쉐이딩 비율 정보를 기록합니다. VRS 의 구현은 Vulkan API 를 기준으로 알아볼 것입니다.

     

    2.1. 디바이스 생성

    VRS 를 사용하려면 VkDevice 를 생성할 때, VkPhysicalDeviceShadingRateImageFeaturesNV 를 추가해줘야 합니다.

    ...
    VkDeviceCreateInfo createInfo = {};
    createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
    ...
    
    // VRS 기능 활성
    VkPhysicalDeviceShadingRateImageFeaturesNV enabledPhysicalDeviceShadingRateImageFeaturesNV{};
    enabledPhysicalDeviceShadingRateImageFeaturesNV.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADING_RATE_IMAGE_FEATURES_NV;
    enabledPhysicalDeviceShadingRateImageFeaturesNV.shadingRateImage = VK_TRUE;
    physicalDeviceFeatures2.pNext = &enabledPhysicalDeviceShadingRateImageFeaturesNV;
    
    createInfo.pNext = &physicalDeviceFeatures2;
    ...
    
    if (!ensure(vkCreateDevice(PhysicalDevice, &createInfo, nullptr, &Device) == VK_SUCCESS))
        return false;
    ...

     

    2.2. VRS 이미지 생성

    쉐이딩 비율을 설정은 Enum 을 이미지에 기록하여 픽셀당 쉐이딩 비율을 기록합니다.

    // 쉐이딩 비율을 나타내는 Enum
    typedef enum VkShadingRatePaletteEntryNV {
    ...
        VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_PIXEL_NV = 5,
        VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_2X1_PIXELS_NV = 6,
        VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_1X2_PIXELS_NV = 7,
        VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_2X2_PIXELS_NV = 8,
        VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_4X2_PIXELS_NV = 9,
        VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_2X4_PIXELS_NV = 10,
        VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_4X4_PIXELS_NV = 11,
    ...
    } VkShadingRatePaletteEntryNV;

     

    쉐이딩 비율은 shadingRateTexelSize 당 하나씩 설정됩니다.

     

    // 쉐이딩 비율을 physicalDeviceShadingRateImagePropertiesNV.shadingRateTexelSize 당 설정됩니다.
    // 제가 사용하는 RTX3070 에서는 (16, 16) 픽셀당 쉐이딩 비율을 설정합니다.
    VkExtent3D imageExtent{};
    imageExtent.width = static_cast<uint32_t>(ceil(SCR_WIDTH / (float)physicalDeviceShadingRateImagePropertiesNV.shadingRateTexelSize.width));
    imageExtent.height = static_cast<uint32_t>(ceil(SCR_HEIGHT / (float)physicalDeviceShadingRateImagePropertiesNV.shadingRateTexelSize.height));
    imageExtent.depth = 1;

     

    레퍼런스3 을 참고하여 VRS 에 사용할 이미지를 아래와 같이 만들었습니다. 실제로 사용하게 되면 Compute shader 를 사용하여 리얼타임에서 계속해서 변경하게 만들 것 입니다만 여기서는 간단히 변하지 않는 VRS 텍스쳐를 사용합니다.

    jTexture* jRHI_Vulkan::CreateSampleVRSTexture()
    {
        if (ensure(!SampleVRSTexture))
        {
            VkPhysicalDeviceShadingRateImagePropertiesNV physicalDeviceShadingRateImagePropertiesNV{};
            physicalDeviceShadingRateImagePropertiesNV.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADING_RATE_IMAGE_PROPERTIES_NV;
            VkPhysicalDeviceProperties2 deviceProperties2{};
            deviceProperties2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2;
            deviceProperties2.pNext = &physicalDeviceShadingRateImagePropertiesNV;
            vkGetPhysicalDeviceProperties2(g_rhi_vk->PhysicalDevice, &deviceProperties2);
    
            VkExtent3D imageExtent{};
            imageExtent.width = static_cast<uint32_t>(ceil(SCR_WIDTH / (float)physicalDeviceShadingRateImagePropertiesNV.shadingRateTexelSize.width));
            imageExtent.height = static_cast<uint32_t>(ceil(SCR_HEIGHT / (float)physicalDeviceShadingRateImagePropertiesNV.shadingRateTexelSize.height));
            imageExtent.depth = 1;
    
    		auto NewVRSTexture = new jTexture_Vulkan();
    
            jVulkanBufferUtil::CreateImage(imageExtent.width, imageExtent.height, 1, (VkSampleCountFlagBits)1, GetVulkanTextureFormat(ETextureFormat::R8UI), VK_IMAGE_TILING_OPTIMAL
                , VK_IMAGE_USAGE_SHADING_RATE_IMAGE_BIT_NV | VK_IMAGE_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, VK_IMAGE_LAYOUT_UNDEFINED, *NewVRSTexture);
    
            VkDeviceSize imageSize = imageExtent.width * imageExtent.height * GetVulkanTextureComponentCount(ETextureFormat::R8UI);
            jBuffer_Vulkan stagingBuffer;
    
            jVulkanBufferUtil::CreateBuffer(VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT
                , imageSize, stagingBuffer);
    
            VkCommandBuffer commandBuffer = g_rhi_vk->BeginSingleTimeCommands();
            ensure(g_rhi_vk->TransitionImageLayout(commandBuffer, (VkImage)NewVRSTexture->GetHandle(), GetVulkanTextureFormat(ETextureFormat::R8UI), 1, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL));
    
            jVulkanBufferUtil::CopyBufferToImage(commandBuffer, stagingBuffer.Buffer, (VkImage)NewVRSTexture->GetHandle()
                , static_cast<uint32>(imageExtent.width), static_cast<uint32>(imageExtent.height));
    
            // Create a circular pattern with decreasing sampling rates outwards (max. range, pattern)
            std::map<float, VkShadingRatePaletteEntryNV> patternLookup = {
                { 1.5f * 8.0f, VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_PIXEL_NV },
                { 1.5f * 12.0f, VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_2X1_PIXELS_NV },
                { 1.5f * 16.0f, VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_1X2_PIXELS_NV },
                { 1.5f * 18.0f, VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_2X2_PIXELS_NV },
                { 1.5f * 20.0f, VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_4X2_PIXELS_NV },
                { 1.5f * 24.0f, VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_2X4_PIXELS_NV }
            };
    
            VkDeviceSize bufferSize = imageExtent.width * imageExtent.height * sizeof(uint8);
            uint8_t val = VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_4X4_PIXELS_NV;
            uint8_t* shadingRatePatternData = new uint8_t[bufferSize];
            memset(shadingRatePatternData, val, bufferSize);
            uint8_t* ptrData = shadingRatePatternData;
            for (uint32_t y = 0; y < imageExtent.height; y++) {
                for (uint32_t x = 0; x < imageExtent.width; x++) {
                    const float deltaX = (float)imageExtent.width / 2.0f - (float)x;
                    const float deltaY = ((float)imageExtent.height / 2.0f - (float)y) * ((float)SCR_WIDTH / (float)SCR_HEIGHT);
                    const float dist = std::sqrt(deltaX * deltaX + deltaY * deltaY);
                    for (auto pattern : patternLookup) {
                        if (dist < pattern.first) {
                            *ptrData = pattern.second;
                            break;
                        }
                    }
                    ptrData++;
                }
            }
    
            check(imageSize == bufferSize);
    
            stagingBuffer.UpdateBuffer(shadingRatePatternData, bufferSize);
    
            g_rhi_vk->EndSingleTimeCommands(commandBuffer);
    
            stagingBuffer.Release();
            delete[]shadingRatePatternData;
    
    		NewVRSTexture->View = jVulkanBufferUtil::CreateImageView((VkImage)NewVRSTexture->GetHandle(), GetVulkanTextureFormat(NewVRSTexture->Format)
                , VK_IMAGE_ASPECT_COLOR_BIT, 1);
    
    		SampleVRSTexture = NewVRSTexture;
        }
    	return SampleVRSTexture;
    }

     

    2.3. VRS 사용을 위한 Graphics Pipeline State 생성

    마지막으로 렌더링하기 위해 필요한 PipelineState 생성 시 어떤 쉐이딩 비율이 사용될지 각각의 뷰포트에 설정해줍니다.

    void* jPipelineStateInfo_Vulkan::CreateGraphicsPipelineState()
    {
    ...
        VkPipelineViewportStateCreateInfo viewportState = {};
        viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
    ...
        static const std::vector<VkShadingRatePaletteEntryNV> shadingRatePaletteEntries = {
            VK_SHADING_RATE_PALETTE_ENTRY_NO_INVOCATIONS_NV,
            VK_SHADING_RATE_PALETTE_ENTRY_16_INVOCATIONS_PER_PIXEL_NV,
            VK_SHADING_RATE_PALETTE_ENTRY_8_INVOCATIONS_PER_PIXEL_NV,
            VK_SHADING_RATE_PALETTE_ENTRY_4_INVOCATIONS_PER_PIXEL_NV,
            VK_SHADING_RATE_PALETTE_ENTRY_2_INVOCATIONS_PER_PIXEL_NV,
            VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_PIXEL_NV,
            VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_2X1_PIXELS_NV,
            VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_1X2_PIXELS_NV,
            VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_2X2_PIXELS_NV,
            VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_4X2_PIXELS_NV,
            VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_2X4_PIXELS_NV,
            VK_SHADING_RATE_PALETTE_ENTRY_1_INVOCATION_PER_4X4_PIXELS_NV,
        };
    
        if (PipelineStateFixed->IsUseVRS)
        {
        	// VRS 를 사용하는 경우 VRS 이미지가 사용하는 쉐이딩 비율 Enum 을 Viewport state 에 전달합니다.
            // Shading Rate Palette
            shadingRatePalette.shadingRatePaletteEntryCount = static_cast<uint32_t>(shadingRatePaletteEntries.size());
            shadingRatePalette.pShadingRatePaletteEntries = shadingRatePaletteEntries.data();
    
            pipelineViewportShadingRateImageStateCI.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_SHADING_RATE_IMAGE_STATE_CREATE_INFO_NV;
            pipelineViewportShadingRateImageStateCI.shadingRateImageEnable = VK_TRUE;
            pipelineViewportShadingRateImageStateCI.viewportCount = viewportState.viewportCount;
            pipelineViewportShadingRateImageStateCI.pShadingRatePalettes = &shadingRatePalette;
            
            // Viewport state 에 넣어 전달
            viewportState.pNext = &pipelineViewportShadingRateImageStateCI;
        }
    
    ...
    	// Pipeline state 생성
        if (!ensure(vkCreateGraphicsPipelines(g_rhi_vk->Device, g_rhi_vk->PipelineCache, 1, &pipelineInfo, nullptr, &vkPipeline) == VK_SUCCESS))
        {
            return nullptr;
        }
     ...
     }

     

    2.4. VRS 디버깅을 위한 코드 추가

    VRS 이미지 잘 적용되었는지 디버깅 하기 위해서 쉐이더를 아래와 같이 수정할 수 있습니다. 이 코드 또한 레퍼런스3 의 내용을 참고했습니다.

    float4 main(VSOutput input
    , uint shadingRate : SV_ShadingRate		// 현재 픽셀의 쉐이딩 비율 정보를 담고 있음
    ) : SV_TARGET
    {
    ...
        const uint SHADING_RATE_PER_PIXEL = 0x0;
        const uint SHADING_RATE_PER_2X1_PIXELS = 6;
        const uint SHADING_RATE_PER_1X2_PIXELS = 7;
        const uint SHADING_RATE_PER_2X2_PIXELS = 8;
        const uint SHADING_RATE_PER_4X2_PIXELS = 9;
        const uint SHADING_RATE_PER_2X4_PIXELS = 10;
    
        // 쉐이딩 비율에 맞게 컬러를 출력해줍니다.
        switch (shadingRate)
        {
            case SHADING_RATE_PER_PIXEL:
                return color * float4(0.0, 0.8, 0.4, 1.0);
            case SHADING_RATE_PER_2X1_PIXELS:
                return color * float4(0.2, 0.6, 1.0, 1.0);
            case SHADING_RATE_PER_1X2_PIXELS:
                return color * float4(0.0, 0.4, 0.8, 1.0);
            case SHADING_RATE_PER_2X2_PIXELS:
                return color * float4(1.0, 1.0, 0.2, 1.0);
            case SHADING_RATE_PER_4X2_PIXELS:
                return color * float4(0.8, 0.8, 0.0, 1.0);
            case SHADING_RATE_PER_2X4_PIXELS:
                return color * float4(1.0, 0.4, 0.2, 1.0);
            default:
                return color * float4(0.8, 0.0, 0.0, 1.0);
        }
    ...
    }

     

    2.5. 구현결과

    그림3. VRS 사용 전 이미지
    그림4. VRS 사용 이미지, 화면 외곽 부분이 그림3과는 다르게 각진 픽셀이 보이는 것을 볼 수 있음.
    그림5. VRS 영역을 디버깅 옵션 사용 결과
    그림6. VRS On 의 경우 퍼포먼스 향상이 있는 것을 볼 수 있음. (간단한 쉐이더라 0.02 정도 차이지만 더 복잡한 픽셀 쉐이더의 경우 더 큰 이득을 얻을 수 있을 것 같음)

     

    2.6. 구현 코드

    https://github.com/scahp/jEngine/tree/VariableShadingRate

     

    3. 레퍼런스

    1. VRWorks - Variable Rate Shading (VRS)

    2. Easy VRS Integration with Eye Tracking

    3. https://github.com/SaschaWillems/Vulkan

     

    댓글

Designed by Tistory & scahp.