9

从零开始手敲次世代游戏引擎(七十九)

 3 years ago
source link: https://zhuanlan.zhihu.com/p/138508116
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

从零开始手敲次世代游戏引擎(七十九)

一晃又一年过去了。。。

即便这样,如我在系列开头所说,这个系列我是不会放弃的,虽然可能会很慢,但是我会努力继续下去。

因此,今天更新一篇。

其实在这一年当中我对图形部分在不断进行重构。其中一个重点就是对应DX12/Vulkan/Metal的PSO(Pipeline State Object)概念。虽然3个图形API的PSO概念在实现细节方面并不是完全一致,但是大致的概念是共通的:将GPU的管道状态,包括

  1. 渲染管道入口处的,附属在vertex(顶点)处的属性layout;
  2. 渲染管道出口处的RT/DT的格式
  3. 固定功能(Fix Function)的设置,如
    1. 深度测试是否有效,深度测试的比较函数(>, >=, ==, <, <=, always true, always false)
    2. Stencil测试是否有效,相关测试
    3. Blend的方式
    4. 三角形剔除方式
  4. Shader的绑定

等等,这些主要的概念作为一个整体进行保存。

其实,这基本上就对应现代GPGPU当中的Context,也就是上下文的概念。在以往,特别是诸如OpenGL这样的图形API当中,这些状态设置是混杂在绘图命令当中的,并没有单独分开进行存储。这就导致在需要改变渲染管道状态的时候,需要一系列的命令来对状态进行设置,并且:

  1. 要么每次把所有状态都重新设置一遍
  2. 要么需要在引擎当中自己跟踪状态的变化,计算出需要改变哪些设置

而DX12/Vulkan/Metal提供了PSO对象,该对象将渲染管道的状态与绘图指令完全分开,使得我们可以提前创建所需要的各种状态。我们在需要的时候从预先创建的PSO对象当中选出要使用的,只需要一次API的调用就可以完成所有状态的设置。

因此,我将之前的IShaderManager重构成了IPipelineStateManager,并且定义了一个引擎内部的PipelineState,来抽象不同图形API的PSO所需的信息:

#pragma once
#include "IRuntimeModule.hpp"
#include "portable.hpp"
#include "cbuffer.h"
#include <memory>
#include <string>

namespace My {
    ENUM(DEPTH_TEST_MODE)
    {
        NONE,
        LARGE,
        LARGE_EQUAL,
        EQUAL,
        LESS_EQUAL,
        LESS,
        NOT_EQUAL,
        NEVER,
        ALWAYS
    };

    ENUM(STENCIL_TEST_MODE)
    {
        NONE
    };

    ENUM(CULL_FACE_MODE)
    {
        NONE,
        FRONT,
        BACK
    };

    ENUM(PIPELINE_TYPE)
    {
        GRAPHIC,
        COMPUTE
    };

    struct PipelineState
    {
        std::string pipelineStateName;
        PIPELINE_TYPE pipelineType;

        std::string vertexShaderName;
        std::string pixelShaderName;
        std::string computeShaderName;
        std::string geometryShaderName;
        std::string tessControlShaderName;
        std::string tessEvaluateShaderName;
        std::string meshShaderName;

        DEPTH_TEST_MODE depthTestMode;
        bool    bDepthWrite;
        STENCIL_TEST_MODE stencilTestMode;
        CULL_FACE_MODE  cullFaceMode;

        A2V_TYPES a2vType;

        virtual ~PipelineState() = default;
    }; 

    Interface IPipelineStateManager : inheritance IRuntimeModule
    {
    public:
        virtual bool RegisterPipelineState(PipelineState& pipelineState) = 0;
        virtual void UnregisterPipelineState(PipelineState& pipelineState) = 0;
        virtual void Clear() = 0;

        [[nodiscard]] virtual const std::shared_ptr<PipelineState> GetPipelineState(std::string name) const = 0;
    };

    extern IPipelineStateManager* g_pPipelineStateManager;
}

这个工作还在继续,接下来预计还会有很多的变化,比如我准备移除其中的tesselation相关的部分,因为目前最新的趋势是tesselation会被mesh shader取代,而mesh shader今后在各个平台会有比较统一的支持,而不是像tesselation那样,各个平台支持情况不一,各个图形API的支持方法也不一,非常头疼。

从这个接口出发,首先实现出一个名为PipelineStateManager的类,用来实现各个图形API无关的部分,也就是共通的部分:

#include "PipelineStateManager.hpp"

using namespace My;
using namespace std;

#define VS_BASIC_SOURCE_FILE "basic.vert"
#define PS_BASIC_SOURCE_FILE "basic.frag"
#define VS_PBR_SOURCE_FILE "pbr.vert"
#define PS_PBR_SOURCE_FILE "pbr.frag"
#define CS_PBR_BRDF_SOURCE_FILE "integrateBRDF.comp"
#define VS_SHADOWMAP_SOURCE_FILE "shadowmap.vert"
#define PS_SHADOWMAP_SOURCE_FILE "shadowmap.frag"
#define VS_OMNI_SHADOWMAP_SOURCE_FILE "shadowmap_omni.vert"
#define PS_OMNI_SHADOWMAP_SOURCE_FILE "shadowmap_omni.frag"
#define GS_OMNI_SHADOWMAP_SOURCE_FILE "shadowmap_omni.geom"
#define DEBUG_VS_SHADER_SOURCE_FILE "debug.vert"
#define DEBUG_PS_SHADER_SOURCE_FILE "debug.frag"
#define VS_PASSTHROUGH_SOURCE_FILE "passthrough.vert"
#define PS_TEXTURE_SOURCE_FILE "texture.frag"
#define PS_TEXTURE_ARRAY_SOURCE_FILE "texturearray.frag"
#define VS_PASSTHROUGH_CUBEMAP_SOURCE_FILE "passthrough_cube.vert"
#define PS_CUBEMAP_SOURCE_FILE "cubemap.frag"
#define PS_CUBEMAP_ARRAY_SOURCE_FILE "cubemaparray.frag"
#define VS_SKYBOX_SOURCE_FILE "skybox.vert"
#define PS_SKYBOX_SOURCE_FILE "skybox.frag"
#define VS_TERRAIN_SOURCE_FILE "terrain.vert"
#define PS_TERRAIN_SOURCE_FILE "terrain.frag"
#define TESC_TERRAIN_SOURCE_FILE "terrain.tesc"
#define TESE_TERRAIN_SOURCE_FILE "terrain.tese"

PipelineStateManager::~PipelineStateManager()
{
    Clear();
}

bool PipelineStateManager::RegisterPipelineState(PipelineState& pipelineState)
{
    PipelineState* pPipelineState;
    pPipelineState = &pipelineState;
    if (InitializePipelineState(&pPipelineState))
    {
        m_pipelineStates.emplace(pipelineState.pipelineStateName, pPipelineState);
        return true;
    }

    return false;
}

void PipelineStateManager::UnregisterPipelineState(PipelineState& pipelineState)
{
    const auto& it = m_pipelineStates.find(pipelineState.pipelineStateName);
    if (it != m_pipelineStates.end())
    {
        DestroyPipelineState(*it->second);
    }
    m_pipelineStates.erase(it);
}

void PipelineStateManager::Clear()
{
    for (auto it = m_pipelineStates.begin(); it != m_pipelineStates.end(); it++)
    {
        if (it != m_pipelineStates.end())
        {
            DestroyPipelineState(*it->second);
        }
        m_pipelineStates.erase(it);
    }

    assert(m_pipelineStates.empty());
}

const std::shared_ptr<PipelineState> PipelineStateManager::GetPipelineState(std::string name) const
{
    const auto& it = m_pipelineStates.find(name);
    if (it != m_pipelineStates.end())
    {
        return it->second;
    }
    else
    {
        assert(!m_pipelineStates.empty());
        return m_pipelineStates.begin()->second;
    }
}

int PipelineStateManager::Initialize()
{
    PipelineState pipelineState;
    pipelineState.pipelineStateName = "BASIC";
    pipelineState.pipelineType = PIPELINE_TYPE::GRAPHIC;
    pipelineState.vertexShaderName = VS_BASIC_SOURCE_FILE;
    pipelineState.pixelShaderName  = PS_BASIC_SOURCE_FILE;
    pipelineState.depthTestMode = DEPTH_TEST_MODE::LESS_EQUAL;
    pipelineState.stencilTestMode = STENCIL_TEST_MODE::NONE;
    pipelineState.cullFaceMode = CULL_FACE_MODE::BACK;
    pipelineState.a2vType = A2V_TYPES::A2V_TYPES_SIMPLE;
    RegisterPipelineState(pipelineState);

    pipelineState.pipelineStateName = "PBR";
    pipelineState.vertexShaderName = VS_PBR_SOURCE_FILE;
    pipelineState.pixelShaderName  = PS_PBR_SOURCE_FILE;
    pipelineState.a2vType = A2V_TYPES::A2V_TYPES_FULL;
    RegisterPipelineState(pipelineState);

    pipelineState.pipelineStateName = "PBR BRDF CS";
    pipelineState.pipelineType = PIPELINE_TYPE::COMPUTE;
    pipelineState.vertexShaderName.clear();
    pipelineState.pixelShaderName.clear();
    pipelineState.computeShaderName = CS_PBR_BRDF_SOURCE_FILE;
    pipelineState.a2vType = A2V_TYPES::A2V_TYPES_NONE;
    RegisterPipelineState(pipelineState);

    pipelineState.pipelineStateName = "Omni Light Shadow Map";
    pipelineState.pipelineType = PIPELINE_TYPE::GRAPHIC;
    pipelineState.vertexShaderName = VS_OMNI_SHADOWMAP_SOURCE_FILE;
    pipelineState.pixelShaderName = PS_OMNI_SHADOWMAP_SOURCE_FILE;
    pipelineState.geometryShaderName = GS_OMNI_SHADOWMAP_SOURCE_FILE;
    pipelineState.computeShaderName.clear(); 
    pipelineState.cullFaceMode = CULL_FACE_MODE::FRONT;
    pipelineState.a2vType = A2V_TYPES::A2V_TYPES_POS_ONLY;
    RegisterPipelineState(pipelineState);

    pipelineState.pipelineStateName = "Spot Light Shadow Map";
    pipelineState.vertexShaderName = VS_SHADOWMAP_SOURCE_FILE;
    pipelineState.pixelShaderName = PS_SHADOWMAP_SOURCE_FILE;
    pipelineState.geometryShaderName.clear();
    RegisterPipelineState(pipelineState);

    pipelineState.pipelineStateName = "Area Light Shadow Map";
    RegisterPipelineState(pipelineState);

    pipelineState.pipelineStateName = "Sun Light Shadow Map";
    RegisterPipelineState(pipelineState);

    pipelineState.pipelineStateName = "Texture Debug Output";
    pipelineState.vertexShaderName = VS_PASSTHROUGH_SOURCE_FILE;
    pipelineState.pixelShaderName = PS_TEXTURE_SOURCE_FILE;
    pipelineState.cullFaceMode = CULL_FACE_MODE::BACK;
    RegisterPipelineState(pipelineState);

    pipelineState.pipelineStateName = "Texture Array Debug Output";
    pipelineState.vertexShaderName = VS_PASSTHROUGH_SOURCE_FILE;
    pipelineState.pixelShaderName = PS_TEXTURE_ARRAY_SOURCE_FILE;
    RegisterPipelineState(pipelineState);

    pipelineState.pipelineStateName = "CubeMap Debug Output";
    pipelineState.vertexShaderName = VS_PASSTHROUGH_CUBEMAP_SOURCE_FILE;
    pipelineState.pixelShaderName = PS_CUBEMAP_SOURCE_FILE;
    pipelineState.a2vType = A2V_TYPES::A2V_TYPES_CUBE;
    RegisterPipelineState(pipelineState);

    pipelineState.pipelineStateName = "CubeMap Array Debug Output";
    pipelineState.vertexShaderName = VS_PASSTHROUGH_CUBEMAP_SOURCE_FILE;
    pipelineState.pixelShaderName = PS_CUBEMAP_ARRAY_SOURCE_FILE;
    RegisterPipelineState(pipelineState);

    pipelineState.pipelineStateName = "SkyBox";
    pipelineState.vertexShaderName = VS_SKYBOX_SOURCE_FILE;
    pipelineState.pixelShaderName = PS_SKYBOX_SOURCE_FILE;
    pipelineState.depthTestMode = DEPTH_TEST_MODE::LESS_EQUAL;
    RegisterPipelineState(pipelineState);

    pipelineState.pipelineStateName = "Terrain";
    pipelineState.vertexShaderName = VS_TERRAIN_SOURCE_FILE;
    pipelineState.pixelShaderName = PS_TERRAIN_SOURCE_FILE;
    pipelineState.tessControlShaderName = TESC_TERRAIN_SOURCE_FILE;
    pipelineState.tessEvaluateShaderName = TESE_TERRAIN_SOURCE_FILE;
    pipelineState.depthTestMode = DEPTH_TEST_MODE::LESS_EQUAL;
    pipelineState.a2vType = A2V_TYPES::A2V_TYPES_POS_ONLY;
    RegisterPipelineState(pipelineState);

    return 0;
}

可以看到,这个类主要是实现引擎内的PSO对象的创建和登记(以及注销和销毁)。这个同样目前只是一个中间状态,可以看到Shader是通过#define的方式硬性编码在里面。将来,Shader应该是通过读取外部的配置文件,或者材质库得到。

这个类包含两个纯虚函数,在注册和销毁PSO的时候,会调用这两个函数。

        protected:
            virtual bool InitializePipelineState(PipelineState** ppPipelineState) = 0;
            virtual void DestroyPipelineState(PipelineState& pipelineState) = 0;

这两个函数就是更为下层的RHI当中来生成/销毁平台相关的PSO使用的。比如对于DX12:

namespace My {
    struct D3d12PipelineState : public PipelineState
    {
        D3D12_SHADER_BYTECODE vertexShaderByteCode;
        D3D12_SHADER_BYTECODE pixelShaderByteCode;
        D3D12_SHADER_BYTECODE geometryShaderByteCode;
        D3D12_SHADER_BYTECODE computeShaderByteCode;
        int32_t psoIndex{-1};

        D3d12PipelineState(PipelineState& state) : PipelineState(state)
        {
        }
    };

可以看到通过结构体的继承,我们将DX12所专有的部分追加在了末尾。利用C++的多态性,我们在D3DRHI的InitializePipelineState当中,用这个拓展版的PSO替换上层传下来的PSO:

bool D3d12PipelineStateManager::InitializePipelineState(PipelineState** ppPipelineState)
{
    D3d12PipelineState* pState = new D3d12PipelineState(**ppPipelineState);

    loadShaders(pState);

    *ppPipelineState = pState;

    return true;
}

而在上层,因为所有的PSO是通过智能指针的方式放入容器:

        protected:
            std::map<std::string, std::shared_ptr<PipelineState>> m_pipelineStates;

所以这种替换对上层是透明的。

类似地,我们对于Metal有如下定义:

namespace My {
    struct MetalPipelineState : public PipelineState
    {
        MetalPipelineState(PipelineState& rhs) : PipelineState(rhs) {}
        MetalPipelineState(PipelineState&& rhs) : PipelineState(std::move(rhs)) {}

        id<MTLRenderPipelineState> mtlRenderPipelineState;
        id<MTLComputePipelineState> mtlComputePipelineState;
        id<MTLDepthStencilState> depthState;
    };
}

同样是在Metal的InitializePipelineState当中,将上层传下来的PSO进行复制然后替换掉。

bool MetalPipelineStateManager::InitializePipelineState(PipelineState** ppPipelineState)
{
    MetalPipelineState* pState = new MetalPipelineState(**ppPipelineState);

    MTLVertexDescriptor* mtlVertexDescriptor = [[MTLVertexDescriptor alloc] init];

    initMtlVertexDescriptor(mtlVertexDescriptor, pState);

    // Load all the shader files with a metallib 
    id <MTLDevice> _device = MTLCreateSystemDefaultDevice();
    NSString *libraryFile = [[NSBundle mainBundle] pathForResource:@"Main" ofType:@"metallib"];
    NSError *error = Nil;
    id <MTLLibrary> myLibrary = [_device newLibraryWithFile:libraryFile error:&error];
    if (!myLibrary) {
        NSLog(@"Library error: %@", error);
    }

    switch (pState->pipelineType)
    {
        case PIPELINE_TYPE::GRAPHIC:
        {
            // Create pipeline state
            id<MTLFunction> vertexFunction = [myLibrary newFunctionWithName:shaderFileName2MainFuncName(pState->vertexShaderName)];
            id<MTLFunction> fragmentFunction = [myLibrary newFunctionWithName:shaderFileName2MainFuncName(pState->pixelShaderName)];

            MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
            pipelineStateDescriptor.label = [NSString stringWithCString:pState->pipelineStateName.c_str()
                                                    encoding:[NSString defaultCStringEncoding]];
            pipelineStateDescriptor.sampleCount = 4;
            pipelineStateDescriptor.vertexFunction = vertexFunction;
            pipelineStateDescriptor.fragmentFunction = fragmentFunction;
            pipelineStateDescriptor.vertexDescriptor = mtlVertexDescriptor;
            pipelineStateDescriptor.colorAttachments[0].pixelFormat = MTLPixelFormatRGBA8Unorm;
            pipelineStateDescriptor.depthAttachmentPixelFormat = MTLPixelFormatDepth32Float_Stencil8;

            pState->mtlRenderPipelineState = 
                [_device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor error:&error];
            if (!pState->mtlRenderPipelineState)
            {
                NSLog(@"Failed to created render pipeline state %@, error %@", pipelineStateDescriptor.label, error);
            }
        }
        break;
        case PIPELINE_TYPE::COMPUTE:
        {
            id<MTLFunction> compFunction = [myLibrary newFunctionWithName:shaderFileName2MainFuncName(pState->computeShaderName)];

            pState->mtlComputePipelineState =
                [_device newComputePipelineStateWithFunction:compFunction error:&error];
            if (!pState->mtlComputePipelineState)
            {
                NSLog(@"Failed to created compute pipeline state, error %@", error);
            }
        }
        break;
        default:
            assert(0);
    }

    MTLDepthStencilDescriptor *depthStateDesc = [[MTLDepthStencilDescriptor alloc] init];

    switch(pState->depthTestMode)
    {
        case DEPTH_TEST_MODE::NONE:
            depthStateDesc.depthCompareFunction = MTLCompareFunctionAlways;
            break;
        case DEPTH_TEST_MODE::LARGE:
            depthStateDesc.depthCompareFunction = MTLCompareFunctionGreater;
            break;
        case DEPTH_TEST_MODE::LARGE_EQUAL:
            depthStateDesc.depthCompareFunction = MTLCompareFunctionGreaterEqual;
            break;
        case DEPTH_TEST_MODE::LESS:
            depthStateDesc.depthCompareFunction = MTLCompareFunctionLess;
            break;
        case DEPTH_TEST_MODE::LESS_EQUAL:
            depthStateDesc.depthCompareFunction = MTLCompareFunctionLessEqual;
            break;
        case DEPTH_TEST_MODE::EQUAL:
            depthStateDesc.depthCompareFunction = MTLCompareFunctionEqual;
            break;
        case DEPTH_TEST_MODE::NOT_EQUAL:
            depthStateDesc.depthCompareFunction = MTLCompareFunctionNotEqual;
            break;
        case DEPTH_TEST_MODE::NEVER:
            depthStateDesc.depthCompareFunction = MTLCompareFunctionNever;
            break;
        case DEPTH_TEST_MODE::ALWAYS:
            depthStateDesc.depthCompareFunction = MTLCompareFunctionAlways;
            break;
        default:
            assert(0);
    }

    if(pState->bDepthWrite)
    {
        depthStateDesc.depthWriteEnabled = YES;
    }
    else
    {
        depthStateDesc.depthWriteEnabled = NO;
    }

    pState->depthState = [_device newDepthStencilStateWithDescriptor:depthStateDesc];

    *ppPipelineState = pState;

    return true;
}

而对于OpenGL,因为没有对应的PSO概念,所以我们基本沿用上层的PSO对象,只是对Shader进行预编译,然后存储编译之后的ShaderProgram的ID

namespace My {
    struct OpenGLPipelineState : public PipelineState
    {
        uint32_t shaderProgram = 0;
        OpenGLPipelineState(PipelineState& rhs) : PipelineState(rhs) {}
        OpenGLPipelineState(PipelineState&& rhs) : PipelineState(std::move(rhs)) {}
    };

bool OpenGLPipelineStateManagerCommonBase::InitializePipelineState(PipelineState** ppPipelineState)
{
    bool result;
    OpenGLPipelineState* pnew_state = new OpenGLPipelineState(**ppPipelineState);

    ShaderSourceList list; 

    if(!(*ppPipelineState)->vertexShaderName.empty())
    {
        list.emplace_back(GL_VERTEX_SHADER, (*ppPipelineState)->vertexShaderName);
    }

    if(!(*ppPipelineState)->pixelShaderName.empty())
    {
        list.emplace_back(GL_FRAGMENT_SHADER, (*ppPipelineState)->pixelShaderName);
    }

    if(!(*ppPipelineState)->geometryShaderName.empty())
    {
        list.emplace_back(GL_GEOMETRY_SHADER, (*ppPipelineState)->geometryShaderName);
    }

    if(!(*ppPipelineState)->computeShaderName.empty())
    {
        list.emplace_back(GL_COMPUTE_SHADER, (*ppPipelineState)->computeShaderName);
    }

    if(!(*ppPipelineState)->tessControlShaderName.empty())
    {
        list.emplace_back(GL_TESS_CONTROL_SHADER, (*ppPipelineState)->tessControlShaderName);
    }

    if(!(*ppPipelineState)->tessEvaluateShaderName.empty())
    {
        list.emplace_back(GL_TESS_EVALUATION_SHADER, (*ppPipelineState)->tessEvaluateShaderName);
    }

    result = LoadShaderProgram(list, pnew_state->shaderProgram);

    *ppPipelineState = pnew_state;

    return result;
}

因为实际上不同图形API对应不同的Shading Language,所以我们需要一套机制,从一种Shading Language生成其它的版本。我选用的是HLSL,在CMake当中,通过如下的自定义命令,按照一定的命名规则,生成其它版本。

add_custom_target(Engine_Asset_Shaders
        COMMAND echo "Start processing Engine Shaders"
    )

set(SHADER_SOURCES basic.vert basic.frag
            debug.vert debug.frag
            cubemap.frag cubemaparray.frag
            pbr.vert pbr.frag
            skybox.vert skybox.frag
            shadowmap.vert shadowmap.frag
            shadowmap_omni.vert shadowmap_omni.frag
            terrain.vert terrain.frag
            texture.frag texturearray.frag
            passthrough.vert passthrough_cube.vert
            integrateBRDF.comp
        )

IF(WIN32)
    set(GLSL_VALIDATOR ${PROJECT_SOURCE_DIR}/External/Windows/bin/glslangValidator.exe)
    set(SPIRV_CROSS    ${PROJECT_SOURCE_DIR}/External/Windows/bin/SPIRV-Cross.exe)
ELSE(WIN32)
	set(GLSL_VALIDATOR ${PROJECT_SOURCE_DIR}/External/${MYGE_TARGET_PLATFORM}/bin/glslangValidator)
	set(SPIRV_CROSS    ${PROJECT_SOURCE_DIR}/External/${MYGE_TARGET_PLATFORM}/bin/spirv-cross)
ENDIF(WIN32)

foreach(SHADER IN LISTS SHADER_SOURCES)
    # Convert HLSL to Others
    string(REPLACE "." ";" arguments ${SHADER})
    list(GET arguments 0 part1)
    list(GET arguments 1 part2)

    set(VULKAN_SOURCE_DIR ${PROJECT_BINARY_DIR}/Asset/Shaders/Vulkan)
    set(GLSL_SOURCE_DIR ${PROJECT_BINARY_DIR}/Asset/Shaders/OpenGL)
    set(METAL_SOURCE_DIR ${PROJECT_BINARY_DIR}/Asset/Shaders/Metal)

    add_custom_command(TARGET Engine_Asset_Shaders PRE_BUILD
        COMMAND ${CMAKE_COMMAND} -E make_directory ${VULKAN_SOURCE_DIR}
        COMMAND ${CMAKE_COMMAND} -E make_directory ${GLSL_SOURCE_DIR}
        COMMAND ${CMAKE_COMMAND} -E make_directory ${METAL_SOURCE_DIR}
    )

    add_custom_command(TARGET Engine_Asset_Shaders PRE_BUILD
        COMMENT "HLSL --> SPIR-V"
	    COMMAND ${GLSL_VALIDATOR} -V -I. -I${PROJECT_SOURCE_DIR}/Framework/Common -o ${VULKAN_SOURCE_DIR}/${part1}.${part2}.spv -e ${part1}_${part2}_main ${PROJECT_SOURCE_DIR}/Asset/Shaders/HLSL/${part1}.${part2}.hlsl
        
        COMMENT "SPIR-V --> Desktop GLSL"
	    COMMAND ${SPIRV_CROSS} --version 420 --remove-unused-variables --output ${GLSL_SOURCE_DIR}/${part1}.${part2}.glsl ${PROJECT_BINARY_DIR}/Asset/Shaders/Vulkan/${part1}.${part2}.spv
        WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}

        COMMENT "SPIR-V --> Metal"
        COMMAND ${SPIRV_CROSS} --msl --msl-version 020101 --remove-unused-variables --output ${METAL_SOURCE_DIR}/${part1}.${part2}.metal ${PROJECT_BINARY_DIR}/Asset/Shaders/Vulkan/${part1}.${part2}.spv
        WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
    )
endforeach(SHADER)

IF(WIN32)
    # Compile HLSL shader sources
    set(SHADER_BIN_DIR ${PROJECT_BINARY_DIR}/Asset/Shaders/HLSL)

    add_custom_command(TARGET Engine_Asset_Shaders PRE_BUILD
        COMMAND ${CMAKE_COMMAND} -E make_directory ${SHADER_BIN_DIR}
    )

    foreach(SHADER IN LISTS SHADER_SOURCES)
        set(SHADER_BIN ${SHADER_BIN_DIR}/${SHADER}.cso)
        if (SHADER MATCHES "^([a-zA-Z0-9_]*)\.vert$")
            add_custom_command(TARGET Engine_Asset_Shaders POST_BUILD
                COMMAND fxc /T vs_5_1 /E ${CMAKE_MATCH_1}_vert_main /I${PROJECT_SOURCE_DIR} /I ${PROJECT_SOURCE_DIR}/Framework/Common ${PROJECT_SOURCE_DIR}/Asset/Shaders/HLSL/${SHADER}.hlsl /Fo ${SHADER_BIN}
                COMMENT "Compile ${SHADER}.hlsl --> ${SHADER}.cso"
                DEPENDS ${PROJECT_SOURCE_DIR}/Asset/Shaders/HLSL/${SHADER}.hlsl
            )
        elseif (SHADER MATCHES "^([a-zA-Z0-9_]*)\.frag$")
            add_custom_command(TARGET Engine_Asset_Shaders POST_BUILD
                COMMAND fxc /T ps_5_1 /E ${CMAKE_MATCH_1}_frag_main /I${PROJECT_SOURCE_DIR} /I ${PROJECT_SOURCE_DIR}/Framework/Common ${PROJECT_SOURCE_DIR}/Asset/Shaders/HLSL/${SHADER}.hlsl /Fo ${SHADER_BIN}
                COMMENT "Compile ${SHADER}.hlsl --> ${SHADER}.cso"
                DEPENDS ${PROJECT_SOURCE_DIR}/Asset/Shaders/HLSL/${SHADER}.hlsl
            )
        elseif (SHADER MATCHES "^([a-zA-Z0-9_]*)\.comp$")
            add_custom_command(TARGET Engine_Asset_Shaders POST_BUILD
                COMMAND fxc /T cs_5_1 /E ${CMAKE_MATCH_1}_comp_main /I${PROJECT_SOURCE_DIR} /I ${PROJECT_SOURCE_DIR}/Framework/Common ${PROJECT_SOURCE_DIR}/Asset/Shaders/HLSL/${SHADER}.hlsl /Fo ${SHADER_BIN}
                COMMENT "Compile ${SHADER}.hlsl --> ${SHADER}.cso"
                DEPENDS ${PROJECT_SOURCE_DIR}/Asset/Shaders/HLSL/${SHADER}.hlsl
            )
        elseif (SHADER MATCHES "^([a-zA-Z0-9_]*)\.geom$")
            add_custom_command(TARGET Engine_Asset_Shaders POST_BUILD
                COMMAND fxc /T gs_5_1 /E ${CMAKE_MATCH_1}_geom_main /I${PROJECT_SOURCE_DIR} /I ${PROJECT_SOURCE_DIR}/Framework/Common ${PROJECT_SOURCE_DIR}/Asset/Shaders/HLSL/${SHADER}.hlsl /Fo ${SHADER_BIN}
                COMMENT "Compile ${SHADER}.hlsl --> ${SHADER}.cso"
                DEPENDS ${PROJECT_SOURCE_DIR}/Asset/Shaders/HLSL/${SHADER}.hlsl
            )
        endif ()
    endforeach(SHADER)
ENDIF(WIN32)

这里面使用了glslang和spirv-cross这两个外部的工具。

最后,对于Metal,还需要将Shader进行库化。这个同样通过CMake自定义编译命令实现:

    foreach(SHADER IN LISTS SHADER_SOURCES)
        add_custom_command(OUTPUT ${SHADER}.air
            COMMAND xcrun -sdk macosx metal -g -MO -c ${PROJECT_BINARY_DIR}/Asset/Shaders/Metal/${SHADER}.metal -o ${SHADER}.air
            COMMENT "Compile ${SHADER}.metal --> ${SHADER}.air"
            DEPENDS Engine_Asset_Shaders
            )

        list(APPEND AIRS ${SHADER}.air)
    endforeach(SHADER)

    add_custom_command(OUTPUT Main.metalar
            COMMAND xcrun -sdk macosx metal-ar rcv Main.metalar ${AIRS}
            COMMENT "Archive ${AIRS} --> Main.metalar"
            DEPENDS ${AIRS}
        )

    add_custom_command(OUTPUT Main.metallib
            COMMAND xcrun -sdk macosx metallib Main.metalar -o Main.metallib
            COMMENT "Compile Main.metalar --> Main.metallib"
            DEPENDS Main.metalar
        )

当然,目前所有shader代码名也是硬性编码在CMake当中的。这部分将来需要以配置文件(如JSON)独立出来。其实关于Shader的组织,涉及到如何设计和组织材质(Material)的问题。所以这部分目前暂且如此。

有了这些事先创建的PSO对象之后,对于不同的渲染Pass,只需要找到并激活它就可以了。比如,对于基于PBR的前向渲染,我们只需要找到名字为“PBR”的PSO,然后将其激活:

void ForwardRenderPhase::Draw(Frame& frame)
{
    auto& pPipelineState = g_pPipelineStateManager->GetPipelineState("PBR");

    // Set the color shader as the current shader program and set the matrices that it will use for rendering.
    g_pGraphicsManager->SetPipelineState(pPipelineState);
    g_pGraphicsManager->SetShadowMaps(frame);
    g_pGraphicsManager->DrawBatch(frame.batchContexts);
}

这个PSO一层一层传递到下面的RHI之后,对于DX12,是下面这样:

void D3d12GraphicsManager::SetPipelineState(const std::shared_ptr<PipelineState>& pipelineState)
{
    if (pipelineState)
    {
        std::shared_ptr<D3d12PipelineState> state = dynamic_pointer_cast<D3d12PipelineState>(pipelineState);
        if (state->psoIndex == -1)
        {
            CreatePSO(*state);
        }

        m_pCommandList[m_nFrameIndex]->SetPipelineState(m_pPipelineStates[state->psoIndex]);
    }
}

而对于Metal,是下面这样:

void Metal2GraphicsManager::SetPipelineState(const std::shared_ptr<PipelineState>& pipelineState)
{
    const std::shared_ptr<MetalPipelineState> pState = dynamic_pointer_cast<MetalPipelineState>(pipelineState);
    [m_pRenderer setPipelineState:*pState];
}

至于OpenGL,因为没有PSO概念,所以fallback成为一组API调用:

oid OpenGLGraphicsManagerCommonBase::SetPipelineState(const std::shared_ptr<PipelineState>& pipelineState)
{
    const std::shared_ptr<const OpenGLPipelineState> pPipelineState = dynamic_pointer_cast<const OpenGLPipelineState>(pipelineState);
    m_CurrentShader = pPipelineState->shaderProgram;

    // Set the color shader as the current shader program and set the matrices that it will use for rendering.
    glUseProgram(m_CurrentShader);

    // Prepare & Bind per frame constant buffer
    uint32_t blockIndex = glGetUniformBlockIndex(m_CurrentShader, "PerFrameConstants");

    if (blockIndex != GL_INVALID_INDEX)
    {
        glUniformBlockBinding(m_CurrentShader, blockIndex, 10);
        glBindBufferBase(GL_UNIFORM_BUFFER, 10, m_uboDrawFrameConstant[m_nFrameIndex]);
    }

    // Prepare per batch constant buffer binding point
    blockIndex = glGetUniformBlockIndex(m_CurrentShader, "PerBatchConstants");

    if (blockIndex != GL_INVALID_INDEX)
    {
        glUniformBlockBinding(m_CurrentShader, blockIndex, 11);
        glBindBufferBase(GL_UNIFORM_BUFFER, 11, m_uboDrawBatchConstant[m_nFrameIndex]);
    }

    // Prepare & Bind light info
    blockIndex = glGetUniformBlockIndex(m_CurrentShader, "LightInfo");

    if (blockIndex != GL_INVALID_INDEX)
    {
        glUniformBlockBinding(m_CurrentShader, blockIndex, 12);
        glBindBufferBase(GL_UNIFORM_BUFFER, 12, m_uboLightInfo[m_nFrameIndex]);
    }

    // Bind LUT table
    auto brdf_lut = GetTexture("BRDF_LUT");
    setShaderParameter("SPIRV_Cross_CombinedbrdfLUTsamp0", 6);
    glActiveTexture(GL_TEXTURE6);
    if (brdf_lut > 0) {
        glBindTexture(GL_TEXTURE_2D, brdf_lut);
    }
    else {
        glBindTexture(GL_TEXTURE_2D, 0);
    }

    // Set Sky Box
    auto texture_id = (uint32_t) m_Frames[m_nFrameIndex].skybox;
    if (texture_id >= 0)
    {
        setShaderParameter("SPIRV_Cross_Combinedskyboxsamp0", 10);
        glActiveTexture(GL_TEXTURE10);
        GLenum target;
    #if defined(OS_WEBASSEMBLY)
        target = GL_TEXTURE_2D_ARRAY;
    #else
        target = GL_TEXTURE_CUBE_MAP_ARRAY;
    #endif
        glBindTexture(target, texture_id);
    }

    // Set Terrain
    texture_id = (uint32_t) m_Frames[m_nFrameIndex].terrainHeightMap;
    if (texture_id >= 0)
    {
        setShaderParameter("SPIRV_Cross_CombinedterrainHeightMapsamp0", 11);
        glActiveTexture(GL_TEXTURE11);
        glBindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, texture_id);
    }

    switch(pipelineState->depthTestMode)
    {
        case DEPTH_TEST_MODE::NONE:
            glDisable(GL_DEPTH_TEST);
            break;
        case DEPTH_TEST_MODE::LARGE:
            glEnable(GL_GREATER);
            break;
        case DEPTH_TEST_MODE::LARGE_EQUAL:
            glEnable(GL_DEPTH_TEST);
            glDepthFunc(GL_GEQUAL);
            break;
        case DEPTH_TEST_MODE::LESS:
            glEnable(GL_DEPTH_TEST);
            glDepthFunc(GL_LESS);
            break;
        case DEPTH_TEST_MODE::LESS_EQUAL:
            glEnable(GL_DEPTH_TEST);
            glDepthFunc(GL_LEQUAL);
            break;
        case DEPTH_TEST_MODE::EQUAL:
            glEnable(GL_DEPTH_TEST);
            glDepthFunc(GL_EQUAL);
            break;
        case DEPTH_TEST_MODE::NOT_EQUAL:
            glEnable(GL_DEPTH_TEST);
            glDepthFunc(GL_NOTEQUAL);
            break;
        case DEPTH_TEST_MODE::NEVER:
            glEnable(GL_DEPTH_TEST);
            glDepthFunc(GL_NEVER);
            break;
        case DEPTH_TEST_MODE::ALWAYS:
            glEnable(GL_DEPTH_TEST);
            glDepthFunc(GL_ALWAYS);
            break;
        default:
            assert(0);
    }

    if(pipelineState->bDepthWrite)
    {
        glDepthMask(GL_TRUE);
    }
    else
    {
        glDepthMask(GL_FALSE);
    }

    switch(pipelineState->cullFaceMode)
    {
        case CULL_FACE_MODE::NONE:
            glDisable(GL_CULL_FACE);
            break;
        case CULL_FACE_MODE::FRONT:
            glEnable(GL_CULL_FACE);
            glCullFace(GL_FRONT);
            break;
        case CULL_FACE_MODE::BACK:
            glEnable(GL_CULL_FACE);
            glCullFace(GL_BACK);
            break;
        default:
            assert(0);
    }
}

这种对比也让我明显看到DX12/Metal这种新一代API的好处:比起OpenGL,在切换渲染管道状态方面,明显更加快速。这不仅仅是因为API调用次数大大减少,而且是因为固定的PSO对象更加方便底层的GPU驱动进行优化,因为整个状态都清晰固定可见。而不是像OpenGL那样,驱动永远不知道后面是否还会有状态设置命令。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK