一晃又一年过去了。。。
即便这样,如我在系列开头所说,这个系列我是不会放弃的,虽然可能会很慢,但是我会努力继续下去。
因此,今天更新一篇。
其实在这一年当中我对图形部分在不断进行重构。其中一个重点就是对应DX12/Vulkan/Metal的PSO(Pipeline State Object)概念。虽然3个图形API的PSO概念在实现细节方面并不是完全一致,但是大致的概念是共通的:将GPU的管道状态,包括
- 渲染管道入口处的,附属在vertex(顶点)处的属性layout;
- 渲染管道出口处的RT/DT的格式
- 固定功能(Fix Function)的设置,如
- 深度测试是否有效,深度测试的比较函数(>, >=, ==, <, <=, always true, always false)
- Stencil测试是否有效,相关测试
- Shader的绑定
这些主要的概念。
其实,这基本上就对应现代GPGPU当中的Context,也就是上下文的概念。在以往,特别是诸如OpenGL这样的图形API当中,这些状态设置是混杂在绘图命令当中的,并没有单独分开进行存储。这就导致在需要改变渲染管道状态的时候,需要一系列的命令来对状态进行设置,并且:
- 要么每次把所有状态都重新设置一遍
- 要么需要在引擎当中自己跟踪状态的变化,计算出需要改变哪些设置
而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那样,驱动永远不知道后面是否还会有状态设置命令。
来源:oschina
链接:https://my.oschina.net/u/4278651/blog/4269210