08-链接时特化与模块预编译
本篇将看看如何使用Slang进行链接时特化。
链接时特化
背景
传统的,图形开发者通过预处理器#define
去特化他们的Shader代码,这种方式出现了许多问题:
- 编译时间过长:预处理器特化位于编译阶段的极早期,当一个预处理定义改变时,编译器要重做很多工作,导致编译时间过长。
- 减少代码可读性和可维护性:编译器无法检查宏的合法性,并且编写代码时候容易出现错误。
- 修改成本高:如果开发者突然想去掉一个宏,那么需要重写大量的Shader代码。
而Slang则将Shader特化从预处理阶段延后至链接阶段,通过对代码的泛化处理。Slang提供了一个三步模型:预编译,链接和目标代码生成。用户可以将.slang
代码预编译为.slang-module
二进制IR文件,并将其进行链接和目标代码生成。
链接时常量
最简单的链接时特化可以通过链接时常量实现:
// main.slang
// Define a constant whose value will be provided in another module at link time.
extern static const int kSampleCount;
float sample(int index) {...}
RWStructuredBuffer<float> output;
void main(uint tid : SV_DispatchThreadID)
{
[ForceUnroll]
for (int i = 0; i < kSampleCount; i++)
output[tid] += sample(i);
}
上述代码定义了一个计算着色器,并用kSampleCount
进行特化,该常量由其他模块提供,并在链接时进行解析。接下来在另一个模块中定义上面提到的链接时常量:
// sample-count.slang
export static const int kSampleCount = 2;
然后就能将它们预编译,并链接到一起,形成链接时特化了:
slangc main.slang -o main.slang-module
slangc sample-count.slang -o sample-count.slang-module
slangc sample-count.slang-module main.slang-module -target hlsl -entry main -profile cs_6_0 -o main.hlsl
当然也能用C++ API实现:
ComPtr<slang::ISession> slangSession = ...;
ComPtr<slang::IBlob> diagnosticsBlob;
// Load the main module from file.
slang::IModule* mainModule = slangSession->loadModule("main.slang", diagnosticsBlob.writeRef());
// Load the specialization constant module from string.
const char* sampleCountSrc = R"(export static const int kSampleCount = 2;)";
auto sampleCountModuleSrcBlob = UnownedRawBlob::create(sampleCountSrc, strlen(sampleCountSrc));
slang::IModule* sampleCountModule = slangSession->loadModuleFromSource(
"sample-count", // module name
"sample-count.slang", // synthetic module path
sampleCountModuleSrcBlob); // module source content
// Compose the modules and entry points.
ComPtr<slang::IEntryPoint> computeEntryPoint;
SLANG_RETURN_ON_FAIL(
module->findEntryPointByName(entryPointName, computeEntryPoint.writeRef()));
std::vector<slang::IComponentType*> componentTypes;
componentTypes.push_back(mainModule);
componentTypes.push_back(computeEntryPoint);
componentTypes.push_back(sampleCountModule);
ComPtr<slang::IComponentType> composedProgram;
SlangResult result = slangSession->createCompositeComponentType(
componentTypes.data(),
componentTypes.size(),
composedProgram.writeRef(),
diagnosticsBlob.writeRef());
// Link.
ComPtr<slang::IComponentType> linkedProgram;
composedProgram->link(linkedProgram.writeRef(), diagnosticsBlob.writeRef());
// Get compiled code.
ComPtr<slang::IBlob> compiledCode;
linkedProgram->getEntryPointCode(0, 0, compiledCode.writeRef(), diagnosticBlob.writeRef());
链接时类型
和链接时常量类似,也能通过链接时类型实现链接时特化:
// common.slang
interface ISampler
{
int getSampleCount();
float sample(int index);
}
struct FooSampler : ISampler
{
int getSampleCount() { return 1; }
float sample(int index) { return 0.0; }
}
struct BarSampler : ISampler
{
int getSampleCount() { return 2; }
float sample(int index) { return index * 0.5; }
}
// main.slang
import common;
extern struct Sampler : ISampler;
RWStructuredBuffer<float> output;
void main(uint tid : SV_DispatchThreadID)
{
Sampler sampler;
[ForceUnroll]
for (int i = 0; i < sampler.getSampleCount(); i++)
output[tid] += sampler.sample(i);
}
Sampler
就是链接时确认的类型,接下来需要编写一个模块,用于链接时确认类型:
// sampler.slang
import common;
export struct Sampler : ISampler = FooSampler;
其中,=
是下列代码的语法糖:
export struct Sampler : ISampler
{
FooSampler inner;
int getSampleCount() { return inner.getSampleCount(); }
float sample(int index) { return inner.sample(index); }
}
提供默认值
当定义一个extern
符号作为链接时常量/类型时,允许提供一个默认值。当其他模块没有export
同名符号时,链接时就会解析到默认值:
// main.slang
// Provide a default value when no other modules are exporting the symbol.
extern static const int kSampleCount = 2;
// ...
void main(uint tid : SV_DispatchThreadID)
{
[ForceUnroll]
for (int i = 0; i < kSampleCount; i++)
output[tid] += sample(i);
}
限制
不想预处理器,链接时常量/类型只能用于不被着色器参数布局影响到的地方。也就是说它们存在如下限制:
- 链接时常量不能被用于定义数组大小;
- 链接时类型被认为是不完整的,不能被用于
ConstantBuffer
,ParameterBlock
,uniform
等,但能被用于StructureBuffer
或GLSLStorageBuffer
;
模块预编译
C++ API
在上文我们通过slangc
进行Slang模块的预编译,实际上C++ API也能做。IModule
类提供了将其序列化到磁盘上的方法:
/// Get a serialized representation of the checked module.
SlangResult IModule::serialize(ISlangBlob** outSerializedBlob);
/// Write the serialized representation of this module to a file.
SlangResult IModule::writeToFile(char const* fileName);
并且ISession
类也提供了查询预编译模块是否和源代码的Slang版本一致、会话编译器选项一致、源代码内容一致的接口:
bool ISession::isBinaryModuleUpToDate(
const char* modulePath,
slang::IBlob* binaryModuleBlob);
如果在模块上次预编译后相关内容发生改变,该方法返回false。
在使用ISession::loadModule()
时会优先使用预编译结果.slang-module
,找不到才会现编译现用。因此如果希望编译器总是使用最新的预编译结果,可在创建会话时设置额外编译器选项CompilerOptionName::UseUpToDateBinaryModule
为1。这样编译器会先验证预编译模块的时效性,如果过期了会进行重新编译。
参考资料
- shader-slang/slang: Making it easier to work with shaders (github.com)
- Link-time Specialization and Module Precompilation | slang