c++一个源文件包含一个源文件怎么写_bgfx 学习笔记(4)- shaderc 编译工具和 sc 源文件

bgfx 使用 shaderc 工具,在构建时将同一份 shader 编译成各渲染平台的 shader。shaderc 中的 c 指 compiler(编译器)。

bgfx 使用的 shader 源码,后缀名为 sc, 比如 fs_cubes.sc、vs_cubes.sc。这里的 sc 应该就是指 shaderc。sc 相当于 shaderc 的源文件。编译后输出各平台相应的二进制格式,比如在 Metal, fs_cubes.sc 编译后输出为 shaders/metal/fs_cubes.bin。这里的后缀 bin 指 binary(二进制)。

实际上,说 shaderc 将 fs_cubes.sc 编译成 metal 使用的二进制格式,有点不准确的。使用 vi shaders/metal/fs_cubes.bin 打开这个所谓的二进制文件,如下显示

485d8c12a8d7c8600201c2b1a227fc8b.png

可以看到这个 fs_cubes.bin 文件只是简单封装了 metal shader 的源码,再附加了一些额外信息。对于 mteal 和 glsl, sc 源码编译出来的也是 shader 源码。而对于 hlsl 和 spirv,编译结果是二进制。但不管结果是源码还是二进制,都是编译。

sc 源文件和 varying.def.sc

打开 examples/01-cubes/fs_cubes.sc 源文件,有如下内容:

$input v_color0

#include "../common/common.sh"

void main()
{
    gl_FragColor = v_color0;
}

首先注意到 $input v_color0 这行,$input 和 $ouput(上例中没有出现 $ouput), 是 bgfx 定义的规则,用于描述源文件的输入和输出。

sc 源文件基本就是 glsl 的语法,只是扩展了一下。多了 #include,和 $input、$ouput。#include 的含义跟 C 代码一样。

examples/01-cubes/ 目录下还有一个 varying.def.sc 的文件。

vec4 v_color0    : COLOR0    = vec4(1.0, 0.0, 0.0, 1.0);

vec3 a_position  : POSITION;
vec4 a_color0    : COLOR0;

shaderc 工具转换 fs_cubes.sc 的时候,会读取 varying.def.sc 这个文件的内容。 varying.def.sc 文件跟 fs_cubes.sc 的 $input、$ouput 合并起来,共同描述了输入输出值的类型和初始值。

为什么需要这样做呢?

shaderc 将 sc 源文件转换成不同平台的 shader,最基本有两种 shader 语法。一种是 OpenGL 使用的 glsl,另一种是 d3d 使用的 hlsl。其余平台的 shader, 比如 metal, 会先预处理成 hlsl 作为中转。

glsl 和 hlsl 对于输出输出变量的规定有所不同。

在 glsl 中,使用 varying 修饰输入输出变量(应该明白 varying.def.sc 为何叫这个名字吧)。而 hlsl 将输入输出变量定义在入口函数中(通常就是 main)。比如上述的 sc 源码

$input v_color0
#include "../common/common.sh"

void main()
{
    gl_FragColor = v_color0;
}

预处理成 glsl,会变为

varying vec4 v_color0;
xxxx

void main()
{
gl_FragColor = v_color0;
}

而预处理成 hlsl,会变为

xxxx
void main( float4 v_color0 : COLOR0 , out float4 bgfx_FragData0 : SV_TARGET0 )
{
float4 bgfx_VoidFrag = vec4_splat(0.0);
bgfx_FragData0 = v_color0;
}

bgfx 要求用户在 varying.def.sc 文件明确定义输入输出变量(包括其类型和其用途),再在 sc 源文件的最开头,用 $input、$output 明确写出此 shader 所用到的变量。有了用户明确写出的辅助信息,bgfx 的实现就可以十分简单,经过宏定义和字符串查找替换,很容易处理 glsl 和 hlsl 输入输出的不同。

假如没有用户写出 varying.def.sc 和 $input、$output 信息,原则上也可从 glsl 分析出部分信息,但实现就变复杂了,需要将 glsl 转换成语法树再提取。但也只能分析出部分,不能获取全部信息。比如

vec4 v_color0 : COLOR0 = vec4(1.0, 0.0, 0.0, 1.0);

这个 COLOR0 的用途信息,从标准的 glsl 没有办法获取。

bgfx_shader.sh

fs_cubes.sc 的源码有这一行

#include "../common/common.sh"

再查看 common.sh 的内容,如下

#include <bgfx_shader.sh>
#include "shaderlib.sh"

shaderlib.sh 在 examples/common 目录下,是些编写例子工程的辅助函数,并非核心代码。而 bgfx_shader.sh 位于 bgfx/src 目录下,是关键代码。基本上,每一个 sc 源码都会直接或间接包含这个文件。sh 后缀,在这里估计是指 shader,而非 shell。

前面已经说过,bgfx 的 sc 源码经过 C 预处理和一些字符串替换,转换为 glsl 或者 hlsl。除了输入输出变量的不同(varying.def.sc 和 $input、$ouput 解决了此问题),两者还有很多区别。

比如 2D 纹理。在 glsl 中,采样方式是纹理的属性,两者合并在一起,只需要用一个 sampler2D 类型表示。但在 hlsl 中,采用方式和纹理是分离的两个变量,采样方式用 SamplerState 表示,纹理用 Texture2D 表示。glsl 一个 sampler2D 变量,就可对应 SamplerState 和 Texture2D 两个变量。

再比如矩阵乘法。glsl 和 hlsl 两者矩阵的存储顺序是不同的,一个是列优先(olumn-major order),一个是行优先(row-major order)。矩阵乘法写起来不同,glsl 会写成 mat * vec;而 hlsl 写成 mul(vec, mtx)。

内置函数的名字也有不同,比如 glsl 中的 mix 内置函数,在 hlsl 名字叫 lerp。

bgfx 使用宏和辅助函数来处理这种不同。这些宏和辅助函数就定义在 bgfx_shader.sh,因而 bgfx_shader.sh 文件很重要,几乎每个 sc 源码都会包含。

拿 2D 纹理举例,在 sc 源码中,2D 纹理定义和访问写成

SAMPLER2D(s_texColor,  0);
texture2D(s_texColor, v_texcoord0);

经过预处理,上面语句在 glsl 中展开成

uniform sampler2D s_texColor;
texture2D(s_texColor, v_texcoord0);

sc 源码基于 glsl, 展开后两者很接近。但在 hlsl 中,上面语句会展开成

// 一些辅助函数
struct BgfxSampler2D
{
SamplerState m_sampler;
Texture2D m_texture;
};

float4 bgfxTexture2D(BgfxSampler2D _sampler, float2 _coord)
{
return _sampler.m_texture.Sample(_sampler.m_sampler, _coord);
}

xxxxxx


uniform SamplerState s_texColorSampler : register(s0); 
uniform Texture2D s_texColorTexture : register(t0); 
static BgfxSampler2D s_texColor = { s_texColorSampler, s_texColorTexture };

bgfxTexture2D(s_texColor, v_texcoord0);

可以看到,hlsl 将 SamplerState 和 Texture2D 合起来,封装成一个结构 BgfxSampler2D,方便跟 glsl 的 sampler2D 对应。

理解了 bgfx_shader.sh 的设计目的,其余细节就容易弄懂了。

shaderc 调试

上面说了 sc 文件的设计意图,接下来调试 shaderc。但实际上是反过来。我是先调试 shaderc,才慢慢弄懂 sc 文件为什么要这样设计。

打开 bgfx/.build/projects/xcode8-osx/shaderc.xcodeproj,编译运行。这时可能会编译不过,需要将【Build Settings】-【Architectures】设置成 Standard Architectures(64-bit Intel)

直接在 Xcode 中运行 shaderc, 因为没有命令行参数,只会输出使用说明:

xxxx

Usage: shaderc -f <in> -o <out> --type <v/f> --platform <platform>
Options:
  -h, --help                    Help.
  -v, --version                 Version information only.
  -f <file path>                Input file path.
  -i <include path>             Include path (for multiple paths use -i multiple times).
  -o <file path>                Output file path.

那怎么才可以知道 shaderc 应该设置什么参数呢?可以看输出的使用说明,使用说明不清楚,就去翻参考例子。比如我想调试 examples/01-cubes/fs_cubes.sc 的 metal 输出。就去打开 01-cubes/makefile,看到

BGFX_DIR=../..
RUNTIME_DIR=$(BGFX_DIR)/examples/runtime
BUILD_DIR=../../.build

include $(BGFX_DIR)/scripts/shader.mk

于是就查看 shader.mk 这文件。看到

all:
	@echo Usage: make TARGET=# [clean, all, rebuild]
	@echo "  TARGET=0 (hlsl  - d3d9)"
	@echo "  TARGET=1 (hlsl  - d3d11)"
	@echo "  TARGET=2 (essl  - nacl)"
	@echo "  TARGET=3 (essl  - android)"
	@echo "  TARGET=4 (glsl)"
	@echo "  TARGET=5 (metal)"
	@echo "  TARGET=6 (pssl)"
	@echo "  TARGET=7 (spriv)"

就去找 TARGET=5,看到

ifeq ($(TARGET), 5)
VS_FLAGS=--platform osx -p metal
FS_FLAGS=--platform osx -p metal
CS_FLAGS=--platform osx -p metal
SHADER_PATH=shaders/metal
else

之后再去搜索 VS_FLAGS。具体过程不细说,经过分析,再结合 shaderc 本身的使用说明,就可以知道调试所需的命令行参数。

在 Xcode, shaderc 的 Edit Scheme..., 【Arguments】 选项卡,Arguments Passed On Launch 添加

-i /3rd/bgfx/bgfx/src -f fs_cubes.sc -o fs_cubes.metal.bin --type f --platform osx  -p metal

另外在 [Options] 的 Working Directory 填入

/3rd/bgfx/bgfx/examples/01-cubes

这样设置后,相当于执行

cd /3rd/bgfx/bgfx/examples/01-cubes
shaderc -i /3rd/bgfx/bgfx/src -f fs_cubes.sc -o fs_cubes.metal.bin --type f --platform osx  -p metal

分析源码时,最好可以让源码调试运行,这样逐行跟踪代码,也可设置断点。这一小节中,说了调试的方式(其实有点离题了),以后尽量避免,不然会更啰嗦。

shaderc 编译流程

shaderc 启动后,首先分析命令行参数,获取编译选项。比如要编译的 shader 类型(vertex、frament、compute 等), 当前平台是 osx 还是 windows、输入路径、输出路径等。

假如是 vertex shader 或者是 frament shader,会读取 varying.def.sc 文件。shaderc 会将 shader 类型写入到输出文件中,比如 vertex shader 就写入 VSH 三个字符,frament shader 就写入 FSH。因而 bgfx 的 bgfx::createShader 接口,不用用户指定 shader 类型,从文件中就可读取这信息。

根据平台类型和 -p 参数,可以知道需要编译的 shader 语言。比如

--platform osx              // glsl(OpenGL)
--platform osx -p metal      // metal shader(Metal)
--platform ios              // essl(OpenGL ES)
--platform windows -p vs_3_0 // hlsl(dx9)
--platform windows -p ps_5_0 // hlsl(dx11)

glsl 和 essl 几乎一样,只有微小区别,比如 glsl 不用指定精度,而 es 需要用 lowp、mediump 等指定精度。两者的处理方式几乎一样。同样 dx9 和 dx11 的 hlsl 处理方式也基本相同。

于是 shaderc 主要有 5 个语言分支路线。

  • glsl
  • metal
  • hlsl
  • spirv(Vulkan)
  • pssl(orbis,PlayStation 的操作系统,这个分支还没有实现)

shaderc 的实现,分两个阶段。

  1. 预处理阶段(包括某些字符搜索替换)。
  2. 编译阶段。

其中预处理阶段是将 sc 源码,转换成 glsl 或者 hlsl。就算是 5 个语言分支,预处理阶段只输出两种语言。

拿 metal 这个分支来说,shaderc 的处理步骤是

  1. sc 源码,经过预处理(使用 fcpp 库和字符串替换),生成 hlsl 源码
  2. hlsl 源码,经过 glslang 库编译,生成 spirv 二进制
  3. spirv,经过 spirv-cross 库反编译,生成 metal shader

glslang 这个库名字虽然带有 glsl, 但也可以编译 hlsl。它在编译的时候,可以选择 hlsl 语法还是 glsl 语法。

glsl 这个分支,shaderc 的处理步骤是

  1. sc 源码,经过预处理(使用 fcpp 库和字符串替换),生成 glsl 源码
  2. glsl 源码,经过 glsl-optimizer 库,生成优化过的 glsl 源码

hlsl 这个分支,shaderc 的处理步骤是

  1. sc 源码,经过预处理(使用 fcpp 库和字符串替换),生成 hlsl 源码
  2. hlsl 源码,经过 D3DCompile 函数,生成二进制码

D3DCompile 这函数是 D3D SDK 自带的,因而 hlsl 这个语言分支只能在 Windows 上调试。

shaderc 预处理

经过 shaderc 的预处理,sc 源码会生成 hlsl 或者 glsl。实际上 shaderc 有个命令行参数 --preprocess,只会输出预处理的结果。

预处理其实也有两个阶段,调用了 fcpp 库的 fppPreProcess 两次。fcpp 库用于实现 C 语言的预处理功能(include 头文件,宏展开等)。shaderc 在实现时,用类 Preprocessor 封装了 fcpp 库,Preprocessor::run 函数调用了 fppPreProcess。

第一次 fppPreProcess,处理了所有的 #include 和所有的宏展开,会输出单一的文件内容。但这时还没有处理 $input, void main,vec2 等。比如 metal,需要预处理成 hlsl, 在第一次 fppPreProcess 后,是类似下面的结果

$input v_color0
float uintBitsToFloat(uint _x) { return asfloat(_x); }
vec2 uintBitsToFloat(uint2 _x) { return asfloat(_x); }
vec3 uintBitsToFloat(uint3 _x) { return asfloat(_x); }
vec4 uintBitsToFloat(uint4 _x) { return asfloat(_x); }

xxxxxx

void main()
{
gl_FragColor = v_color0;
}

这时还不是 hlsl 的语法。接下来 shaderc 根据 varying.def.sc 的内容,读取 $input, $ouput 的内容,就知道输出输出的信息,动态重定义 main 函数。

#define void main() 
void main( float4 v_color0 : COLOR0 , out float4 bgfx_FragData0 : SV_TARGET0 )

shaderc 还会做一些字符查找和替换,比如

gl_FragData[0] 替换成 bgfx_FragData0
gl_FragData[1] 替换成 bgfx_FragData1

查找 void main(), 在 void main() 中插入一行

float4 bgfx_VoidFrag = vec4_splat(0.0);

还会添加一系列的宏定义,将 glsl 的 vec2,名字定义成 float2。

"#define lowpn"
"#define mediumpn"
"#define highpn"
"#define ivec2 int2n"
"#define ivec3 int3n"
"#define ivec4 int4n"
"#define uvec2 uint2n"
"#define uvec3 uint3n"
"#define uvec4 uint4n"
"#define vec2 float2n"
"#define vec3 float3n"
"#define vec4 float4n"
"#define mat2 float2x2n"
"#define mat3 float3x3n"
"#define mat4 float4x4n"
    
#define gl_FragColor bgfx_FragData0

经历各种条件判断,一轮眼花缭乱的字符串查找、替换、插入、宏定义。第二次调用 fppPreProcess,将 C 语言宏定义展开,就神奇地转成 hlsl 的语法。类似下面这样子

float intBitsToFloat(int _x) { return asfloat(_x); }
float2 intBitsToFloat(uint2 _x) { return asfloat(_x); }
float3 intBitsToFloat(uint3 _x) { return asfloat(_x); }
float4 intBitsToFloat(uint4 _x) { return asfloat(_x); }
float uintBitsToFloat(uint _x) { return asfloat(_x); }
float2 uintBitsToFloat(uint2 _x) { return asfloat(_x); }

xxxxxx

void main( float4 v_color0 : COLOR0 , out float4 bgfx_FragData0 : SV_TARGET0 )
{
float4 bgfx_VoidFrag = vec4_splat(0.0);
bgfx_FragData0 = v_color0;
}

预处理生成的 hlsl 有多余的代码,但没有关系,将 hlsl 送到 glslang 库编译,会自动清除无用代码,之后再 spirv-cross 库反编译成 metal shader。这样 sc 源码就转换到 metal shader。

将 sc 源文件预处理成 glsl,也是类似的。

bgfx 没有自己来写语法分析,只用了宏定义和预处理。就将 sc 源代码变为 glsl 或者 hlsl,再调用其它第三方工具。这种跨平台 shader 编译方式,有点奇特,甚至有点偷懒。但不管如何,It works。

其它细节

shaderc 在转换 shader 的过程中,会分析 shader 所用到的 uniforms, 预先写到输出的文件中。

因此 bgfx 在载入 shader 的时候,就算没有调用渲染平台的 API,也可以直接为 Unifroms 分配 Handle

ShaderRef& sr = m_shaderRef[handle.idx];
sr.m_refCount = 1;
sr.m_hashIn   = hashIn;
sr.m_hashOut  = hashOut;
sr.m_num      = 0;
sr.m_uniforms = NULL;

UniformHandle* uniforms = (UniformHandle*)alloca(count*sizeof(UniformHandle) );

for (uint32_t ii = 0; ii < count; ++ii)
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值