bgfx 学习笔记(2)- 创建 RendererContextI ,切换 Metal 和 GL

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

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。

这时需要分析两点

  1. rendererCreate(const Init& _init) 的具体实现。
  2. 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。