- 编译器
- 图形学
- 编程语言
【免费下载链接】slang
Making it easier to work with shaders
导读
本文基于 Slang 仓库中的shader-object示例,系统讲解如何在slang-gfx(即slang-rhi)图形抽象层之上使用Shader Object 模型来管理着色器参数绑定与着色器特化。示例通过一个ITransformer接口驱动计算着色器,演示了从加载模块、组合入口点、反射查找类型,到用ShaderCursor免偏移量绑定参数、以及用createShaderObject将接口参数具体化的完整链路。读完本文,你将掌握一套不依赖预处理器宏、可同时支持编译期特化与运行时多态的可复用着色器参数管理模式。
一、示例概览:一个计算着色器变换管线
shader-object示例位于 examples/shader-object/,由三个文件构成:
- shader-object.slang:定义
ITransformer接口、三种变换实现与一个计算入口点; - main.cpp:宿主端全部逻辑,负责加载、组合、绑定与回读;
- CMakeLists.txt:仅一行
example(shader-object),复用 examples/CMakeLists.txt 中定义的example()辅助函数完成目标创建、.slang/.jpg/.obj等资源的拷贝与主函数包装。
示例运行的效果是:读入一个包含 4 个浮点数{0.0f, 1.0f, 2.0f, 3.0f}的缓冲,经过变换函数处理后回写同一缓冲并打印结果。示例默认使用的变换是f(x) = x + c + 10.0f(AddTransformer在 shader-object.slang 中实现,代码里的+ 10.0f使输出可辨识),因此打印结果为11.0f、12.0f、13.0f、14.0f。
examples/README.md 对它的定位是:展示 Slang 的接口类型如何用比基于预处理器的技术更简单的逻辑实现着色器特化。这是理解本示例价值的钥匙——传统做法需要为每种变换写一套#ifdef分支并分别编译,而 Shader Object 模型把"变换函数"变成了可在运行时构造和绑定的对象。
二、着色器侧:用接口参数声明多态能力
着色器代码 shader-object.slang 是整个示例的语义核心。它定义了一个只含单个方法的最小接口:
interface ITransformer { float transform(float x); }随后给出两个直接实现与一个组合实现:
// f(x) = x + c struct AddTransformer : ITransformer { float c; float transform(float x) { return x + c + 10.0f; } }; // f(x) = x * c struct MulTransformer : ITransformer { float c; float transform(float x) { return x * c; } }; // f(x) = f0(f1(x)) struct CompositeTransformer : ITransformer { ITransformer func0; ITransformer func1; float transform(float x) { return func0.transform(func1.transform(x)); } };注意CompositeTransformer的字段本身也是ITransformer类型——接口类型可以作为结构体字段嵌套出现,这为构造任意深度的变换组合(如链式流水线)提供了可能,也直接体现接口参数的可组合性。
计算入口点把变换器声明为uniform参数:
[shader("compute")] [numthreads(4,1,1)] void computeMain( uint3 sv_dispatchThreadID : SV_DispatchThreadID, uniform RWStructuredBuffer<float> buffer, uniform ITransformer transformer) { var input = buffer[sv_dispatchThreadID.x]; buffer[sv_dispatchThreadID.x] = transformer.transform(input); }关键点在于uniform ITransformer transformer这一声明。它同时开启了两种能力,且无需改动任何着色器代码或宿主参数设置逻辑(shader-object.slang 的注释明确说明了这一点):
- 编译期特化(specialization):宿主在创建对象时若把具体类型(如
AddTransformer)作为特化参数传给编译器,就能生成只做加法的专用 kernel,性能最优; - 运行时多态:宿主也可以在运行时把任意实现了
ITransformer的对象绑定进去,让同一个 kernel 处理任意变换。
关于接口与泛型的语言层背景,可参考 docs/user-guide/06-interfaces-generics.md:接口定义类型必须提供的方法契约,struct通过: ITransformer声明遵从关系,编译器会检查实现是否满足接口要求,缺失方法将报错。
三、宿主端第一阶段:加载模块并组合程序
main.cpp从loadShaderProgram(main.cpp)开始,这一阶段完全在 Slang 编译器 API(slang.h)层面工作。
3.1 获取编译会话
示例应用使用slang-rhi图形抽象层,该层已经创建好 Slang 编译会话,宿主直接取用:
ComPtr<slang::ISession> slangSession; slangSession = device->getSlangSession();IDevice::getSlangSession在 include/slang-gfx.h 中声明,slang-rhi各后端的Device实现(如 tools/gfx/renderer-shared.h)均提供该能力。这意味着你无需自行创建slang::IGlobalSession/ISession,图形层已代为管理生命周期。
3.2 加载模块
ComPtr<slang::IBlob> diagnosticsBlob; Slang::String path = resourceBase.resolveResource("shader-object.slang"); slang::IModule* module = slangSession->loadModule(path.getBuffer(), diagnosticsBlob.writeRef()); diagnoseIfNeeded(diagnosticsBlob);loadModule的行为等价于在 shader 文件中写import shader_object;:编译器沿搜索路径查找同名.slang文件、编译并缓存,重复加载直接复用。这里有个值得注意的细节:文件名是shader-object(含连字符),不能直接作为 Slang 标识符,因此以 import 方式使用时需把连字符替换为下划线,即import shader_object;(main.cpp)。diagnoseIfNeeded用于把诊断信息打印到控制台。
3.3 查找入口点并组合组件类型
加载模块后按名字查找计算入口点:
char const* computeEntryPointName = "computeMain"; ComPtr<slang::IEntryPoint> computeEntryPoint; SLANG_RETURN_ON_FAIL( module->findEntryPointByName(computeEntryPointName, computeEntryPoint.writeRef()));这里有一个重要的约束:通过loadModule方式加载代码时,入口点函数必须带[shader("...")]属性(如[shader("compute")]),否则编译器无法唯一确定哪些函数是入口点(main.cpp)。
模块与入口点都属于 API 中的component type(组件类型),下一步把它们组合成composite:
Slang::List<slang::IComponentType*> componentTypes; componentTypes.add(module); componentTypes.add(computeEntryPoint); ComPtr<slang::IComponentType> composedProgram; SlangResult result = slangSession->createCompositeComponentType( componentTypes.getBuffer(), componentTypes.getCount(), composedProgram.writeRef(), diagnosticsBlob.writeRef());createCompositeComponentType(声明见 include/slang.h)可能因组合非法(例如重复包含同一模块)而失败,因此必须检查返回值与诊断输出。组合成功后:
composedProgram->getLayout()得到slang::ProgramLayout*反射信息,后续findTypeByName依赖它;- 示例在测试模式下还会调用
testBase.printEntrypointHashes(1, 1, composedProgram)(实现在 examples/example-base/test-base.cpp),它通过getEntryPointHash输出入口点在指定 target 下的哈希,用于回归验证 kernel 是否按预期特化——这是 CI 场景下的实用技巧。
3.4 创建 IShaderProgram
组合完成后,把它交给图形层:
ShaderProgramDesc programDesc = {}; programDesc.slangGlobalScope = composedProgram.get(); auto shaderProgram = device->createShaderProgram(programDesc);IShaderProgram是slang-rhi层的程序对象,内部持有组合后的 Slang 组件类型,供管线创建与参数布局使用。
四、宿主端第二阶段:设备、管线与缓冲
4.1 创建设备与计算管线
Slang::ComPtr<IDevice> device; DeviceDesc deviceDesc = {}; device = getRHI()->createDevice(deviceDesc); ComputePipelineDesc pipelineDesc = {}; pipelineDesc.program = shaderProgram.get(); ComPtr<IComputePipeline> pipelineState; pipelineState = device->createComputePipeline(pipelineDesc);getRHI()返回全局渲染接口单例,createDevice按DeviceDesc(默认即选用平台可用后端)创建逻辑设备,它屏蔽了 D3D11/D3D12/Vulkan/CUDA/CPU/Metal 等后端的差异。
4.2 创建输入缓冲
const int numberCount = 4; float initialData[] = {0.0f, 1.0f, 2.0f, 3.0f}; BufferDesc bufferDesc = {}; bufferDesc.size = numberCount * sizeof(float); bufferDesc.format = Format::Undefined; bufferDesc.elementSize = sizeof(float); bufferDesc.usage = BufferUsage::ShaderResource | BufferUsage::UnorderedAccess | BufferUsage::CopyDestination | BufferUsage::CopySource; bufferDesc.defaultState = ResourceState::UnorderedAccess; bufferDesc.memoryType = MemoryType::DeviceLocal; ComPtr<IBuffer> numbersBuffer; numbersBuffer = device->createBuffer(bufferDesc, (void*)initialData);该缓冲同时承担输入(ShaderResource)与输出(UnorderedAccess),且具备拷贝目的地/来源能力,便于提交后回读。defaultState指定资源初始状态为UnorderedAccess,符合计算着色器直接读写的场景。
五、Shader Object 模型:参数绑定的核心机制
这一节是本示例的精华。它回答了"绑参数到底绑什么、怎么绑"的问题。
5.1 根对象与入口点对象
命令录制阶段,绑定管线会返回一个根着色器对象:
auto queue = device->getQueue(QueueType::Graphics); auto commandEncoder = queue->createCommandEncoder(); auto encoder = commandEncoder->beginComputePass(); auto rootObject = encoder->bindPipeline(pipelineState);bindPipeline在 include/slang-gfx.h 中声明,它同时完成管线绑定与根对象创建。根对象是参数树的"根":入口点参数挂在rootObject->getEntryPoint(i)返回的入口点对象上,全局/根作用域参数则直接挂在根对象上。
IShaderObject接口(include/slang-gfx.h)提供了一组按ShaderOffset定位的绑定方法:
| 方法 | 作用 |
|---|---|
setData(offset, data, size) | 写入普通值/结构体(如float c) |
setResource(offset, resourceView) | 绑定缓冲/纹理资源视图 |
setSampler(offset, sampler) | 绑定采样器 |
setObject(offset, object) | 绑定嵌套的IShaderObject(接口参数或对象参数) |
setSpecializationArgs(offset, args, count) | 手动指定接口参数的特化类型,驱动编译器生成专用 kernel |
getEntryPoint(index)/getObject(offset) | 取子对象 |
而ShaderOffset(include/slang-gfx.h)由uniformOffset(常量缓冲内偏移)与bindingRangeIndex/bindingArrayIndex(绑定范围索引)共同描述参数位置。直接使用这些 API 意味着你必须自己用反射 API 计算每个参数的偏移,这正是ShaderCursor存在的理由。
5.2 ShaderCursor:按路径免偏移绑定的封装
ShaderCursor封装了IShaderObject的偏移计算逻辑,允许按参数名路径直接寻址。示例中的用法:
ShaderCursor entryPointCursor( rootObject->getEntryPoint(0)); // 取第一个入口点的游标 // 绑定 buffer 到入口点参数 entryPointCursor.getPath("buffer").setBinding(numbersBuffer); // 绑定 transformer 对象到入口点参数 entryPointCursor.getPath("transformer").setObject(transformer);getPath("buffer")返回指向名为buffer参数的子游标,setBinding(...)绑定缓冲资源;getPath("transformer").setObject(transformer)把之前创建的IShaderObject实例绑定到接口参数上。
ShaderCursor的头文件位于slang-rhi子模块的slang-rhi/shader-cursor.h(本示例与 tools/gfx-unit-test/root-shader-parameter.cpp 均通过#include <slang-rhi/shader-cursor.h>引入),该类在rhi命名空间下。仓库内多个单元测试都在使用同样的模式,例如 tools/gfx-unit-test/shared-buffers-tests.cpp 中的ShaderCursor rootCursor(rootObject); rootCursor.getPath("buffer").setBinding(Binding(dstBuffer));,以及 tools/gfx-unit-test/shared-textures-tests.cpp 的入口点游标用法,印证了这是slang-rhi生态的标准绑定姿势。
5.3 创建接口参数对象:从类型到实例
绑定前需要先构造一个具体类型的着色器对象。宿主通过反射按名字查类型:
slang::TypeReflection* addTransformerType = slangReflection->findTypeByName("AddTransformer"); ComPtr<IShaderObject> transformer; transformer = device->createShaderObject(addTransformerType, ShaderObjectContainerType::None);findTypeByName是slang::ProgramLayout提供的反射查询接口(include/slang.h),AddTransformer是着色器里定义的结构体类型名——宿主与着色器之间通过反射信息按名字建立联系,这正是 Slang 反射能力的典型应用。createShaderObject的第二个参数是容器类型:
enum class ShaderObjectContainerType { None, // 普通对象(结构体/接口的具体实现) Array, // 数组容器 StructuredBuffer // 结构化缓冲容器 };(定义见 include/slang-gfx.h,tools/gfx中renderer-shared.h的ShaderObjectLayout会按容器类型分派不同的创建路径。)本示例创建的AddTransformer是普通结构体对象,因此传ShaderObjectContainerType::None。
接着给对象的字段赋值——依然用ShaderCursor:
float c = 1.0f; ShaderCursor(transformer).getPath("c").setData(&c, sizeof(float));setData把c的字节写入对象内部常量缓冲的正确偏移处。这展示了 ShaderCursor 的另一优势:对结构体字段的寻址同样不需要手算偏移。
5.4 绑定的完整时间线
把 5.1–5.3 串起来,一次 dispatch 的完整流程是:
bindPipeline拿到根对象;- 反射
findTypeByName("AddTransformer")得到类型; createShaderObject创建实例,ShaderCursor写入字段c = 1.0f;- 入口点游标
getPath("buffer").setBinding(numbersBuffer)绑资源; - 入口点游标
getPath("transformer").setObject(transformer)绑接口参数; dispatchCompute(1, 1, 1)发射 4 线程(numthreads(4,1,1)),每个线程处理一个元素;end()、finish()、submit、waitOnHost完成提交与同步。
注意第 5 步绑定的对象类型在运行时才确定:slang-rhi会依据绑定的具体类型,决定是走编译期特化(生成专用 kernel)还是运行时多态(保留通用 kernel 并按对象内容驱动),宿主无需感知这一选择。
六、结果回读与运行
ComPtr<ISlangBlob> resultBlob; SLANG_RETURN_ON_FAIL( device->readBuffer(numbersBuffer, 0, numberCount * sizeof(float), resultBlob.writeRef())); auto result = reinterpret_cast<const float*>(resultBlob->getBufferPointer()); for (int i = 0; i < numberCount; i++) printf("%f\n", result[i]);readBuffer把 GPU 缓冲内容拷回宿主端ISlangBlob。因为AddTransformer::transform实现为x + c + 10.0f且c = 1.0f,预期输出为11.0f、12.0f、13.0f、14.0f。
构建与运行方式
该示例由 examples/CMakeLists.txt 的add_subdirectory(shader-object)纳入构建,构建入口沿用仓库根目录 CMakeLists.txt 的标准流程(cmake配置 + 构建,详见 docs/building.md)。构建产物为独立可执行程序,example()辅助函数(examples/CMakeLists.txt)会自动把目录下的*.slang等资源拷贝到可执行文件旁,因此可直接运行;其依赖的slang-rhi层通过 external/slang-rhi 子模块引入。运行需要可用的图形/计算设备后端(D3D11/D3D12/Vulkan/CUDA/CPU 等,取决于构建配置),程序会输出 4 个变换后的浮点数。
七、扩展与对比:这个模型解决了什么问题
7.1 对比预处理器特化
传统 HLSL/GLSL 方案要实现"多种变换可选",常见做法是#define宏 + 多次编译,每增加一种变换就要新增编译变体,宿主侧还要维护宏与 kernel 的映射表。Shader Object 模型下:
- 新增变换 = 在 shader 里加一个实现接口的
struct,宿主侧findTypeByName+createShaderObject即可绑定,着色器与宿主逻辑零改动(shader-object.slang 注释明确承诺了这一点); - 组合变换 = 构造
CompositeTransformer嵌套对象,无需新代码路径。
7.2 对比手算偏移
如果不用ShaderCursor,绑定buffer需要先用反射 API 求出ShaderOffset(uniformOffset加bindingRangeIndex),再调用setResource。ShaderCursor把这一过程封装为按名字路径查找,示例代码因此从"数十行反射样板"压缩为三行,且路径查找失败时能给出更友好的诊断。
7.3 特化与多态的取舍
slang-rhi的IShaderObject::setSpecializationArgs(include/slang-gfx.h)注释点明了机制:特化参数会被传给 shader 编译器用于特化接口参数类型。实际使用中:
- 追求极致性能且变换集合固定 → 编译期特化;
- 变换集合在运行时变化/数量庞大 → 运行时多态;
CompositeTransformer这类递归组合场景天然适合运行时多态。
两种路径共享同一套宿主绑定代码,切换成本几乎为零,这正是该示例想传达的核心工程价值。
八、延伸阅读
- 接口与泛型的语言级详解:docs/user-guide/06-interfaces-generics.md
IShaderObject完整接口与ShaderObjectContainerType:include/slang-gfx.h- 同类参数绑定模式在测试中的更多用法:tools/gfx-unit-test/root-shader-parameter.cpp、tools/gfx-unit-test/shared-buffers-tests.cpp、tools/gfx-unit-test/shared-textures-tests.cpp
- 入口点哈希回归验证的辅助实现:examples/example-base/test-base.cpp
- 其他基于组件类型组合 API 的示例:
loadShaderProgram的组合思路同样出现在 examples/ray-tracing/main.cpp 与 examples/mlp-training/mlp-training.cpp 中,可作为多入口点组合的参考。
- 编译器
- 图形学
- 编程语言
【免费下载链接】slang
Making it easier to work with shaders
相关推荐
Slang Shader Toy 示例深度解析:用接口与泛型实现着色器效果模块化组合
Slang Shader Toy 示例深度解析:用接口与泛型实现着色器效果模块化组合 导读 本文基于 Slang 官方仓库中的 shader toy 示例 ht
编译器图形学编程语言Slang 语言指南:着色语言中的模块化导入、显式参数块与泛型接口编程
Slang 语言指南:着色语言中的模块化导入、显式参数块与泛型接口编程 Slang 是一门以 HLSL 为基座、面向现代 GPU API(D3D12 / Vul
编译器图形学编程语言ComfyUI_TensorRT核心原理:TensorRT如何让AI绘图速度提升3倍?
ComfyUI_TensorRT核心原理:TensorRT如何让AI绘图速度提升3倍? ComfyUI_TensorRT是一款专为AI绘图优化的加速工具,通过集
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考