RendererContextI
随便翻翻代码,我想分析 Metal 的实现,自然就找到 renderer_mtl.mm。之后根据代码,知道 RendererContextMtl 实现了 RendererContextI 接口。
于是知道 bgfx 平台相关的渲染接口定义在 bgfx_p.h 的 RendererContextI 中
struct BX_NO_VTABLE RendererContextI
{
virtual ~RendererContextI() = 0;
virtual RendererType::Enum getRendererType() const = 0;
virtual const char* getRendererName() const = 0;
virtual bool isDeviceRemoved() = 0;
virtual void flip() = 0;
virtual void createIndexBuffer(IndexBufferHandle _handle, const Memory* _mem, uint16_t _flags) = 0;
xxxx
}中。不同的渲染平台,需要实现这些接口函数。比如
- Metal,在 renderer_mtl.mm 文件中实现了 RendererContextMtl。
- OpenGL, 在 renderer_gl.cpp 文件中实现了 RendererContextGL。
- D3D12, 在 renderer_d3d12.cpp 文件中实现了 RendererContextD3D12。
如此类推。RendererContextMtl、RendererContextGL、RendererContextD3D12 都是 RendererContextI 接口的具体实现类。根据不同的平台和参数,创建不同的渲染对象,外部统一使用 RendererContextI 接口来操作。
我主要关心 Metal 的实现,也就是对应于 RendererContextMtl 对象的创建。
RendererContextMtl 的创建
在 RendererContextMtl 的构造函数设置断点。在 Mac 平台打开 examples.xcodeproj,运行。断点触发后有下面的堆栈
经过分析,可以知道 bgfx 的 Context 使用了命令队列,程序其它地方往队列中塞命令,而 runloop 被唤醒去执行命令。
Context::renderFrame 执行一帧。Context::rendererExecCommands 执行具体的命令。
从 CommandBuffer 首先读取 uint8_t,这是命令的 type。当 type 为 RendererInit 时,就再读取 bgfx::Init 结构。根据 bgfx::Init 的具体参数调用
RendererContextI* rendererCreate(const Init& _init)创建出平台相关的 RendererContextI。
这时需要分析两点
rendererCreate(const Init& _init)的具体实现。- bgfx::Init 结构是如何初始化的,什么时候被放到命令队列中。
rendererCreate
按道理,macOS 是可以同时支持 Metal 和 OpenGL 的,因而也就有个问题,bgfx 是根据何种机制去创建 Metal 的实现,而不是 GL 的实现。怎么才能从 Metal 切换到 GL。
bgfx.cpp 的 rendererCreate 函数使用到一个表格。
struct RendererCreator
{
RendererCreateFn createFn;
RendererDestroyFn destroyFn;
const char* name;
bool supported;
};
static RendererCreator s_rendererCreator[] =
{
{ noop::rendererCreate, noop::rendererDestroy, BGFX_RENDERER_NOOP_NAME, true }, // Noop
{ d3d9::rendererCreate, d3d9::rendererDestroy, BGFX_RENDERER_DIRECT3D9_NAME, !!BGFX_CONFIG_RENDERER_DIRECT3D9 }, // Direct3D9
{ d3d11::rendererCreate, d3d11::rendererDestroy, BGFX_RENDERER_DIRECT3D11_NAME, !!BGFX_CONFIG_RENDERER_DIRECT3D11 }, // Direct3D11
{ d3d12::rendererCreate, d3d12::rendererDestroy, BGFX_RENDERER_DIRECT3D12_NAME, !!BGFX_CONFIG_RENDERER_DIRECT3D12 }, // Direct3D12
{ gnm::rendererCreate, gnm::rendererDestroy, BGFX_RENDERER_GNM_NAME, !!BGFX_CONFIG_RENDERER_GNM }, // GNM
#if BX_PLATFORM_OSX || BX_PLATFORM_IOS
{ mtl::rendererCreate, mtl::rendererDestroy, BGFX_RENDERER_METAL_NAME, !!BGFX_CONFIG_RENDERER_METAL }, // Metal
#else
{ noop::rendererCreate, noop::rendererDestroy, BGFX_RENDERER_NOOP_NAME, false }, // Noop
#endif // BX_PLATFORM_OSX || BX_PLATFORM_IOS
{ nvn::rendererCreate, nvn::rendererDestroy, BGFX_RENDERER_NVN_NAME, !!BGFX_CONFIG_RENDERER_NVN }, // NVN
{ gl::rendererCreate, gl::rendererDestroy, BGFX_RENDERER_OPENGL_NAME, !!BGFX_CONFIG_RENDERER_OPENGLES }, // OpenGLES
{ gl::rendererCreate, gl::rendererDestroy, BGFX_RENDERER_OPENGL_NAME, !!BGFX_CONFIG_RENDERER_OPENGL }, // OpenGL
{ vk::rendererCreate, vk::rendererDestroy, BGFX_RENDERER_VULKAN_NAME, !!BGFX_CONFIG_RENDERER_VULKAN }, // Vulkan
};
BX_STATIC_ASSERT(BX_COUNTOF(s_rendererCreator) == RendererType::Count);rendererCreate 实现其实在挑选表格中对应的项,根据表格项调用其 createFn 创建具体的渲染后端。
for (uint32_t ii = 0; ii < RendererType::Count; ++ii)
{
RendererType::Enum renderer = RendererType::Enum(ii);
if (s_rendererCreator[ii].supported)这段代码中,将表格的索引 ii, 强转成 RendererType::Enum 了。再看看
struct RendererType
{
/// Renderer types:
enum Enum
{
Noop, //!< No rendering.
Direct3D9, //!< Direct3D 9.0
Direct3D11, //!< Direct3D 11.0
Direct3D12, //!< Direct3D 12.0
Gnm, //!< GNM
Metal, //!< Metal
Nvn, //!< NVN
OpenGLES, //!< OpenGL ES 2.0+
OpenGL, //!< OpenGL 2.1+
Vulkan, //!< Vulkan
Count
};
};可以看到 RendererType 的定义跟 s_rendererCreator 表格项是一一对应的。
rendererCreate 在挑选 RendererType,以 RendererType 作为索引,访问 s_rendererCreator 对应的表格项。假如平台可以同时支持多个 RendererType,比如 Mac 同时支持
RendererType::Noop
RendererType::Metal
RendererType::OpenGL就给每个支持的 RendererType 一个分数(score)。之后按照分数排序,优先选择高分的。bgfx 用了个小技巧,将 RendererType(低 8 位)和 score 打包放到一个 int32_t 了。
score += RendererType::Metal == renderer ? 20 : 0;
score += RendererType::OpenGL == renderer ? 10 : 0;Metal 有 20 分,OpenGL 有 10 分。假如 Init 中没有指定 RendererType, 自然就挑选了 Metal 的实现。
if (_init.type == renderer)
{
score += 1000;
}假如在 init 中指定了 RendererType, 一下子有 1000 分了。自然就使用 init 指定的 RendererType。
bgfx::Init 结构
在工程中搜索 CommandBuffer::RendererInit,发现在 Context::init(const Init& _init) 的实现中,有这两行
CommandBuffer& cmdbuf = getCommandBuffer(CommandBuffer::RendererInit);
cmdbuf.write(_init);这两行代码,将 RendererInit 命令放到队列当中,之后由另一线程的 rendererExecCommands 来执行。
CommandBuffer 的 write 和 read 函数,使用了 bx::memCopy, 可知 struct Init 必须是平坦的数据类型,不能包含指针。
设置断点,最终是例子代码中 ExampleHelloWorld::init 函数调用了 bgfx::init, 之后再调用 Context::init,将 Init 结构放到命令队列当中。
void init(int32_t _argc, const char* const* _argv, uint32_t _width, uint32_t _height) override
{
Args args(_argc, _argv);
m_width = _width;
m_height = _height;
m_debug = BGFX_DEBUG_TEXT;
m_reset = BGFX_RESET_VSYNC;
bgfx::Init init;
init.type = args.m_type;
init.vendorId = args.m_pciId;
init.resolution.width = m_width;
init.resolution.height = m_height;
init.resolution.reset = m_reset;
bgfx::init(init);上面中,有个 Args 的类,使用 argc, argv 来初始化,之后得到 args.m_type 类型为 RendererType。RendererType 用于挑选不同渲染后端,argc, argv 看名字就知道是程序启动时候的命令行参数,根据不同的参数,可以切换 GL 或者 Metal。
切换到 GL
Args::Args 中有代码
bx::CommandLine cmdLine(_argc, (const char**)_argv);
if (cmdLine.hasArg("gl") )
{
m_type = bgfx::RendererType::OpenGL;
}
xxxx
else if (BX_ENABLED(BX_PLATFORM_OSX) )
{
if (cmdLine.hasArg("mtl") )
{
m_type = bgfx::RendererType::Metal;
}
}在看看 CommandLine::find 的实现。
for (int32_t ii = 0; ii < m_argc && 0 != strCmp(m_argv[ii], "--"); ++ii)不用细看,就可以猜测知道。假如命令参数中有 --gl,会对应于 RendererType::OpenGL,就会选择 GL 渲染后端。命令行参数中有 --mtl,会对应于 RendererType::Metal,就会对应 Metal 渲染后端。当都不指定,因为 Metal 的分数比 GL 高,就会选择 Metal 渲染后端。
在 Xcode,配置命令行参数,启动后,可以切换到 GL。