前面已经说过,要让渲染库跨平台,需要解决两个问题。
- 将同一份 shader, 编译成各平台相应的 shader。
- 提供同一套图形接口,封装各平台 3d api 的差异。
bgfx 的跨平台 shader 使用一种独特的方式。bgfx 的 shader 保留了 GLSL 的语法,添加一堆宏来处理平台差异,另外提供了一个工具 shaderc,在程序构建的时候,将同一套 shader 编译成各平台相应的 shader。
bgfx 处理 shader 跨平台,是在构建的时候完成的,并非在运行时。
比如例子程序中的 examples/01-cubes,里面有 fs_cubes.sc 和 vs_cubes.sc 两个文件。当构建的时候,会使用 shaderc 编译这两个文件。在 Metal 平台上,就对应于
examples/runtime/shaders/metal/fs_cubes.bin
examples/runtime/shaders/metal/vs_cubes.bin使用命令 vi shaders/metal/vs_cubes.bin 来打开 vs_cubes.bin,看得更清楚。
可以看到 metal/vs_cubes.bin 这个文件,最重要的内容就是 Metal Shader,再附加一些其它信息。
bgfx 在运行时载入 Shader 的逻辑十分简单,真正复杂的是 shaderc 这个转换 Shader 的工具。shaderc 工具后续文章再描述。
bgfx 用到两种 shader,
- 一种在编译 bgfx 库时,内嵌到库代码的 shader。
- 另一种是用户指定的,从文件系统读取的 shader。
两者只是获取二进制数据的方式不同。内嵌 shader 可以直接从内存本身获取数据,用户指定的 shader 从文件中读取数据。
内嵌 Shader
bgfx 使用内嵌 shader 实现 debugfont 和 clear 功能。对应于 s_embeddedShaders 这个表格。
#include "vs_debugfont.bin.h"
#include "fs_debugfont.bin.h"
#include "vs_clear.bin.h"
#include "fs_clear0.bin.h"
#include "fs_clear1.bin.h"
#include "fs_clear2.bin.h"
#include "fs_clear3.bin.h"
#include "fs_clear4.bin.h"
#include "fs_clear5.bin.h"
#include "fs_clear6.bin.h"
#include "fs_clear7.bin.h"
static const EmbeddedShader s_embeddedShaders[] =
{
BGFX_EMBEDDED_SHADER(vs_debugfont),
BGFX_EMBEDDED_SHADER(fs_debugfont),
BGFX_EMBEDDED_SHADER(vs_clear),
BGFX_EMBEDDED_SHADER(fs_clear0),
BGFX_EMBEDDED_SHADER(fs_clear1),
BGFX_EMBEDDED_SHADER(fs_clear2),
BGFX_EMBEDDED_SHADER(fs_clear3),
BGFX_EMBEDDED_SHADER(fs_clear4),
BGFX_EMBEDDED_SHADER(fs_clear5),
BGFX_EMBEDDED_SHADER(fs_clear6),
BGFX_EMBEDDED_SHADER(fs_clear7),
BGFX_EMBEDDED_SHADER_END()
};打开 vs_clear.bin.h 这个头文件,里面有下面数组。
static const uint8_t vs_clear_glsl[265] = { xxxxx }
static const uint8_t vs_clear_spv[863] = { xxxxx }
static const uint8_t vs_clear_dx9[282] = { xxxxx }
static const uint8_t vs_clear_dx11[479] = { xxxxx }
static const uint8_t vs_clear_mtl[541] = { xxxxx }数组放着二进制数据,每个平台一个数组。这是 C/C++ 程序内嵌二进制数据的常见方法。这些数组中的数据,对应于下面 vs_clear.sc 的源码,使用 shaderc 生成。
$input a_position
/*
* Copyright 2011-2019 Branimir Karadzic. All rights reserved.
* License: https://github.com/bkaradzic/bgfx#license-bsd-2-clause
*/
#include "bgfx_shader.sh"
uniform vec4 bgfx_clear_depth;
void main()
{
gl_Position = vec4(a_position.xy, bgfx_clear_depth.x, 1.0);
}在 macOS 平台将宏 BGFX_EMBEDDED_SHADER 展开,可以展开成类似下面的代码
static const EmbeddedShader s_embeddedShaders[] =
{
{"vs_debugfont",
{{bgfx::RendererType::Metal,
vs_debugfont_mtl,
sizeof(bx::CountOfRequireArrayArgumentT(vs_debugfont_mtl))},
{bgfx::RendererType::OpenGLES,
vs_debugfont_glsl,
sizeof(bx::CountOfRequireArrayArgumentT(vs_debugfont_glsl))},
{bgfx::RendererType::OpenGL,
vs_debugfont_glsl,
sizeof(bx::CountOfRequireArrayArgumentT(vs_debugfont_glsl))},
{bgfx::RendererType::Noop, (const uint8_t *)"VSH\x5\x0\x0\x0\x0\x0\x0", 10},
{bgfx::RendererType::Count, __null, 0}}},
{"fs_debugfont",
{{bgfx::RendererType::Metal,
fs_debugfont_mtl,
sizeof(bx::CountOfRequireArrayArgumentT(fs_debugfont_mtl))},
{bgfx::RendererType::OpenGLES,
fs_debugfont_glsl,
sizeof(bx::CountOfRequireArrayArgumentT(fs_debugfont_glsl))},
{bgfx::RendererType::OpenGL,
fs_debugfont_glsl,
sizeof(bx::CountOfRequireArrayArgumentT(fs_debugfont_glsl))},
{bgfx::RendererType::Noop, (const uint8_t *)"VSH\x5\x0\x0\x0\x0\x0\x0", 10},
{bgfx::RendererType::Count, __null, 0}}},
}可以看到 BGFX_EMBEDDED_SHADER 宏,目的是生成一个表格,描述平台相关的二进制数据。上例中,因为 macOS 平台只支持 Metal 和 OpenGL(包括 OpenGLES),因此把表格中没有 dx 对应项。
有了这个表格,从 RendererType 找到内嵌的 Shader 数据就很容易了。
这个 RendererType 可以从 RendererContextI 的 getRendererType 接口获取。
struct BX_NO_VTABLE RendererContextI
{
virtual ~RendererContextI() = 0;
virtual RendererType::Enum getRendererType() const = 0;
xxx
}而前文中已经描述了 RendererContextI 的创建过程。一旦平台对应的 RendererContextI 被创建出来,相应的内嵌 Shader 就固定下来了。
从文件载入
examples 例子中的 shader, 对应于 examples/runtime/shaders 目录下的文件,有各个子目录。
shaders
├── dx11
├── dx9
├── essl
├── glsl
├── metal
├── pssl
└── spirvexamples/common/bgfx_utils.cpp 也有下面的二进制载入代码,实际只是根据 RendererType 选择不同的路径。
static bgfx::ShaderHandle loadShader(bx::FileReaderI* _reader, const char* _name)
{
char filePath[512];
const char* shaderPath = "???";
switch (bgfx::getRendererType() )
{
case bgfx::RendererType::Noop:
case bgfx::RendererType::Direct3D9: shaderPath = "shaders/dx9/"; break;
case bgfx::RendererType::Direct3D11:
case bgfx::RendererType::Direct3D12: shaderPath = "shaders/dx11/"; break;
case bgfx::RendererType::Gnm: shaderPath = "shaders/pssl/"; break;
case bgfx::RendererType::Metal: shaderPath = "shaders/metal/"; break;
case bgfx::RendererType::Nvn: shaderPath = "shaders/nvn/"; break;
case bgfx::RendererType::OpenGL: shaderPath = "shaders/glsl/"; break;
case bgfx::RendererType::OpenGLES: shaderPath = "shaders/essl/"; break;
case bgfx::RendererType::Vulkan: shaderPath = "shaders/spirv/"; break;
case bgfx::RendererType::Count:
BX_CHECK(false, "You should not be here!");
break;
}
bx::strCopy(filePath, BX_COUNTOF(filePath), shaderPath);
bx::strCat(filePath, BX_COUNTOF(filePath), _name);
bx::strCat(filePath, BX_COUNTOF(filePath), ".bin");
bgfx::ShaderHandle handle = bgfx::createShader(loadMem(_reader, filePath) );
bgfx::setName(handle, _name);
return handle;
}bgfx::createShader
无论是内嵌 Shader 还是文件 Shader, 获取二进制数据后(对应 Memory 结构),后续的流程都是相同的。都调用了
ShaderHandle bgfx::createShader(const Memory* _mem)里面的实现,先检查一下数据是否有效,之后通过计算 hash 值,判断相同内容的 shader 是否已经被创建。假如之前已经创建,就直接返回 Handle。
假如还没有创建,就分配一个 Handle,再分配 Shader 所用到的 Uniforms Handle。bgfx::createShader 并没有直接创建平台相关的 Shader,只是将一个 CommandBuffer::CreateShader 命令放到 CommandBuffer 中。之后另一线程调用 rendererExecCommands,才将命令取出,再调用 RendererContextI 的
void createShader(ShaderHandle _handle, const Memory* _mem)接口。分配 Handle 本身,和创建 Handle 对应的渲染平台资源,两者是分离的。在 Metal 中,执行命令式,才会真正执行 id 的 newLibraryWithSource。
bgfx 提供给用户使用的接口,很多都采用这种这种实现方式,用户接口分配 handle,将对应命令放入 CommandBuffer。以后另一线程再异步从 CommandBuffer 取出命令执行,才真正创建出渲染用到的资源。
比如
- bgfx::init 对应 CommandBuffer::RendererInit 命令
- bgfx::createVertexBuffer 对应 CommandBuffer::CreateVertexBuffer 命令
- bgfx::createProgram 对应 CommandBuffer::CreateProgram 命令