bgfx 学习笔记(3) - Shader 载入

727 阅读4分钟
原文链接: zhuanlan.zhihu.com

前面已经说过,要让渲染库跨平台,需要解决两个问题。

  1. 将同一份 shader, 编译成各平台相应的 shader。
  2. 提供同一套图形接口,封装各平台 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
└── spirv

examples/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 命令