- 游戏开发
- 图形学
- VR
【免费下载链接】stride
Stride (formerly Xenko), a free and open-source cross-platform C# game engine.
Stride(前身 Xenko)是一款免费开源、跨平台的 C# 游戏引擎。本文围绕 sources/shaders/README.md 介绍 SDSL(Stride Shading Language)——引擎内置的着色器语言,它是 HLSL 的超集,新增了面向对象式的可扩展性、模块化与复用机制。文章将深入解析 SDSL 的语法要素、在 sources/shaders 目录中"解析(Parser)—语义分析(Analysis)—SPIR-V 构建—合并(Mixin)"的完整编译流水线,并给出可直接参考的语法示例与测试用例,帮助你理解 Stride 如何把高级着色器代码最终编译为可运行字节码。
SDSL 是什么:为游戏引擎而生的着色语言
SDSL(Stride Shading Language)是 Stride 游戏引擎专门设计的着色器语言,核心定位是HLSL 的超集:它完整继承了 HLSL 的语法与能力,同时引入面向对象编程(OOP)理念,让着色器代码获得三方面的提升:
- 可扩展性(extensibility):通过类(
shader)、继承(mixin)、组合(compose)等概念,着色器可以被轻松扩展,不必复制粘贴整段代码; - 模块化(modularity):每个着色器只聚焦一种渲染技术(例如光照、阴影、后处理),整体更易管理与组合;
- 可复用性(reusability):最大化着色器之间的代码复用,公共逻辑沉淀为基类或工具着色器,被多个效果引用。
这套实现位于 sources/shaders,由多个项目协作完成:Stride.Shaders.Parsers(词法/语法解析)、Stride.Shaders.Compilers(语义分析、SPIR-V 构建与合并)、Stride.Shaders.Spirv(SPIR-V 数据结构)、Stride.Shaders.Effects(效果编译器与缓存)、Stride.Shaders.Generators与Stride.Shaders.Generators.Internal(代码生成/内置函数生成)等。
本文所有结论均来自当前仓库实际内容。仓库为只读,以下均用于查看、运行与配置说明。
从一份 SDSL 文件看语言骨架
仓库 sources/shaders/assets/SDSL 提供了一批可直接阅读的示例。以Test.sdsl为例:
namespace Machin; shader Test { stream float4 Position : SV_POSITION; stream float4 Color : SV_TARGET; bool machin = false; void PSMain() { streams.Position.w = 1.0; } void VSMain() { int a = 0; a = 3; if(machin) streams.Color *= 2; if(a < 2) a *= 3; streams.Color = float4(1.0,0.0,1.0,1f32); } }这份代码揭示了 SDSL 的几个关键概念:
namespace:SDSL 支持命名空间组织着色器(支持namespace a.b.c;或namespace a { ... }两种写法)。在 ShaderFileParsers.cs 中,NamespaceParsers会循环读取点分标识符,并允许在命名空间内声明 shader、effect 或 params;shader:声明一个着色器类,是 SDSL 的基本组织单元;stream:声明流变量(顶点/像素阶段间传递的插值数据),后面的语义(如SV_POSITION、SV_TARGET)决定其在管线中的含义;streams.xxx:在方法体内通过streams.前缀访问流变量——这是 SDSL 相对 HLSL 在写法上的显著差异(HLSL 通常通过结构体形参传递);- 字面量后缀:
1f32表示 32 位浮点、3f16表示 16 位浮点,也支持1.(带尾点)等 HLSL 语法变体,测试用例 ParsingTests.cs 专门验证了这些边界情况。
再看Simple.sdsl中更"激进"的语法形态:
namespace machin { namespace hello; namespace other.thing { internal partial effect class.heloo { } shader MyCLass<int a, Link machin> : Hello<1,2,3.0f32,"Hello"> { patchstream float a = 5; float3x3 b; float Foo(int a, int b) { elif(a == b) { float a = Comm(); } const int a = 3; machin = 5; streams.a.machin.hello[1] = 9; } } shader other; } }这里出现了更完整的语法要素:partial分部类、带泛型参数(<int a, Link machin>)与泛型实参(Hello<1,2,3.0f32,"Hello">)的类声明、patchstream流变量、elif语句等,说明 SDSL 的语法面比传统 HLSL 宽得多。
类、继承与组合:SDSL 面向对象三要素
SDSL 的面向对象能力通过三种语法机制体现,它们分别对应"扩展、模块化、复用"三大设计目标。
继承 / mixin
shader之间通过冒号进行继承,子类可以覆盖父类方法、继承父类流变量与常量:
shader TestBasic : TestBase { stream float4 InputPosition : POSITION; stream float4 Position : SV_POSITION; stream float4 ExtraColor : COLOR; cbuffer Test123 { float4 CBufferValue1; } void Test() { streams.ColorTarget = streams.ExtraColor; SetColor(streams.ExtraColor); // 调用继承自 TestBase 的方法 } // ... }其中TestBase.sdsl定义了stream float4 ColorTarget : SV_Target0;和SetColor(float4)方法,TestBasic直接继承并复用。在源码层面,Shader.cs 中ShaderClass的Mixins字段保存了所有被 mixin 的父类;继承链的处理在编译期(ShaderMixer)完成,而不是运行时动态解析。
泛型(generic)着色器
SDSL 支持带类型参数与值参数的泛型着色器,用于生成"同一模板、多种尺寸"的代码。仓库 CompilerTests/ConstUIntGeneric.sdsl 是一个完整示例:
shader ConstUIntGenericBase<int TCount> { static const uint Count = TCount; static const uint CountPlusOne = TCount + 1; } shader ConstUIntGeneric : ConstUIntGenericBase<3> { stream float4 ColorTarget : SV_Target0; cbuffer Test { uint Selector; float Values[CountPlusOne]; // 数组长度由继承的泛型常量决定 } void PSMain() { uint count = Count; float v = Selector == count ? Values[3] : 0.0; streams.ColorTarget = float4(v, v, v, v); } }泛型实参可以是值字面量(<3>)、类型(<int a>)甚至字符串(Hello<...,"Hello">)。AST 侧由 ShaderGenericsValues.cs 的ValueTypeGenerics承载值类型泛型。这一机制配合static const常量传播,可让数组维度、循环次数等在编译期确定,类似模板元编程的效果。
组合(compose)
组合用于把一个或多个子着色器"内嵌"到当前着色器中,子着色器的方法通过引用调用:
shader ComposePlainRoot : ComposeSharedBase { RWTexture3D<float4> WriteTex; compose ComposePlainHelper helper; override void Compute() { WriteTex[streams.DispatchThreadId] = helper.Value(); // 调用组合子着色器的方法 } };compose引入的是"对象级"复用(相对于继承的"类型级"复用):组合关系在语义分析阶段被解析,ShaderMixer会把被组合的着色器代码合并进最终产物。测试目录中还包含ComposeSharedRoot、ComposePlainHelper、ComposeNumThreadsRoot等配套文件,验证组合与继承并存时的行为(例如 ComposePlainRoot.sdsl 与共享基类的对照)。
模块化封装:cbuffer、stream、stage 与 attributes
SDSL 在类体内支持多种成员声明,均在 ShaderElements.cs 中有对应 AST 节点:
| 关键字 | 含义 | 仓库示例 |
|---|---|---|
stream | 顶点/像素阶段间传递的插值数据 | TestBasic.sdsl中的Position : SV_POSITION |
patchstream | 细分阶段(hull/domain)使用的 patch 流数据 | Simple.sdsl中的patchstream float a = 5; |
stage | 声明只能被特定阶段使用的方法/变量(如 compute 入口CSMain) | StaticCallRoot.sdsl中的stage stream uint3 DispatchThreadId、stage void CSMain() |
static const | 编译期常量,可参与数组维度计算 | ConstUIntGeneric.sdsl |
cbuffer | 常量缓冲区声明 | TestBasic.sdsl中的cbuffer Test123 |
compose | 组合子着色器 | ComposePlainRoot.sdsl |
override | 覆盖父类或组合对象的方法 | ComposePlainRoot.sdsl中的override void Compute() |
abstract | 抽象方法,由子类/组合对象实现 | Parent.sdsl中的stage abstract void DoSomething(); |
attributes([...]) | 附加元数据,如布局、numthreads | Commented.sdsl中的[Layout(1, 2)] |
典型的 attribute 应用是 compute shader 的线程数声明:
[numthreads(8, 8, 8)] stage void CSMain() { WriteTex[streams.DispatchThreadId] = float4(...); }示例见 CompilerTests/StaticCallRoot.sdsl。编译器测试目录还覆盖了CSNumThreadsInherit、CSNumThreadsOverride、CSNumThreadsOnNonEntry等场景,说明numthreads在继承/覆盖/非入口函数等情况下有严格校验。
预处理与宏:与 C 兼容的条件编译
SDSL 保留并兼容 C 风格的预处理指令。Macroed.sdsl展示了条件编译:
#ifdef cond shader Machin { } #endif shader Machin2 { }预处理管线位于 Parsing/PreProcessing,由多个阶段组成(CommentPhase处理注释、CodeProcessor处理宏展开等)。解析入口 SDSLParser.cs 展示了完整调用链:
public static ParseResult Parse(string code) { var c = new CommentProcessedCode(code); return Grammar.Match<CommentProcessedCode, ShaderFileParser, ShaderFile>(c); }即:原始代码 → 注释预处理(CommentProcessedCode)→ 文法匹配(Grammar.Match)→ 得到 ShaderFile AST。
解析器架构:Parser + Grammar + AST
解析器位于Stride.Shaders.Parsers,设计上分为三层:
- 扫描器(Scanner):
Parsing/Scanners目录下的Scanner/ScannerGeneric等负责把源码切分成 token 流,并记录TextLocation/ErrorLocation用于精确报错; - 文法(Grammar):Grammar.cs 是解析核心,
Match方法创建Scanner、执行IParser<TValue>.Match,并在 token 未消耗完时追加SDSL0009错误("额外 token"类错误); - AST:
Parsing/SDSL/AST目录定义了ShaderFile、ShaderNamespace、ShaderClass、ShaderElement、Expression、Statement等节点层级。
解析结果用 ParseResult.cs 承载:ParseResult.AST保存成功解析的语法树,Errors保存ParseError(包含消息、位置与出错 token 的上下文预览)。ParseError.ToString()会基于出错位置自动摘取下一个 token,方便快速定位。
各语法要素按职责拆分到Parsing/SDSL/Parsers子目录:
Terminals/:终结符(标识符、字面量、操作符)解析;ExpressionParsers/:表达式解析;StatementParsers/:语句解析;ShaderParsers/:着色器级解析,其中 ShaderClassParser.cs 的SimpleShaderClassParser展示了最朴素的shader Name { ... }匹配流程:匹配shader关键字 → 空格 → 标识符 →{→ 循环匹配成员元素直到};DirectiveExpressions/:指令表达式解析;Common/:共享辅助解析器。
语义分析与符号解析
语法解析之后,编译进入语义分析阶段,核心组件包括:
- 符号表(SymbolTable)与符号帧(SymbolFrame):SymbolFrame.cs 实现作用域内符号的按名索引,并支持"方法组(MethodGroup)":当多个同名函数(重载)出现在同一作用域时,自动归并为
FunctionGroupType符号; - 符号类型(SymbolTypes)与类型访问器(Node.Visitors / SymbolTypes.Visitors):负责类型推断与表达式类型检查;
- 分析(Analysis):Parsing/Analysis 包含控制流图(
CFG.cs)、SDIR.cs与符号表扩展,为后续代码生成提供静态信息; - 常量求值(ConstantEvaluator):
Core/ConstantEvaluator.cs可在编译期求值常量表达式——这正是ConstUIntGeneric中CountPlusOne = TCount + 1能用于数组维度的原因。
从 AST 到 SPIR-V:SDSL 编译器的中间表示
语义分析完成后,代码被降级为SPIR-V(而不是直接生成 HLSL/GLSL),这一设计让"一份 SDSL 源码 → 多后端输出"成为可能。相关代码位于Stride.Shaders.Compilers/SDSL:
- SDSLC.cs 是单文件编译入口:
SDSLParser.Parse→ 逐个ShaderClass创建CompilerUnit→ 注册调试信息(OpString/OpSource/OpSourceHashSDSL,后者用于着色器缓存校验)→ 执行shader.Compile(table, compiler)生成 SPIR-V 指令; - ShaderMixer.cs 是合并器(mixin 编译器):创建全局
SpirvContext,写入OpCapability(Shader)、OpExtension("SPV_GOOGLE_user_type")、OpMemoryModel(GLSL450)等模块头,然后递归解析 mixin/compose 关系,把多个 shader 的 SPIR-V 合并为一个可执行模块; - 校验:ShaderMixer.Validation.cs 负责在合并后做语义校验,例如检测"在无法计算屏幕空间导数的阶段(非 Fragment/GLCompute)使用隐式 LOD 纹理采样或
ddx/ddy/fwidth",提前给出可读错误而非等后端优化器失败; - SPIR-V 数据结构与读写由
Stride.Shaders.Spirv项目提供(OpData、SpirvBuffer、SpirvReader等)。
SDSLC.CompileOptions(SDSLC.cs)还支持两个编译选项:RegisterInCache(是否将编译结果注册进缓存)与OriginalCode(预处理前的原始源码,用于OpSource调试信息)。
后端输出:SPIR-V 翻译回 HLSL 及其他
Stride.Shaders.Compilers提供了从 SPIR-V 到具体后端代码的翻译:
- SpirvTranslator.cs:
GetEntryPoints(Backend)枚举入口点(按 ExecutionModel 区分 VS/PS/CS 等),Translate(Backend, entryPoint)把 SPIR-V 翻译为目标后端文本; Backend枚举支持 HLSL 与 SPIR-V 两类目标;- EffectCompiler.cs 在编译效果(effect)时调用
translator.Translate(Backend.Hlsl, entryPoint)生成最终 HLSL; ShaderMixer.Options(ShaderMixer.cs)有两个关键开关:ResourcesRegisterSeparate(D3D11/12 下 t/b/s 寄存器各自独立编号,从 0 开始)与StripGoogleUserType(移除SPV_GOOGLE_user_type扩展与UserTypeGOOGLE装饰——Vulkan 在不支持该扩展时会拒绝)。
测试 StrideShaderTests.cs 展示了完整链路:ShaderLoader加载.sdsl→ShaderMixer.MergeSDSL产出字节码 →SpirvTranslator翻译为 HLSL → 断言数组维度[2][3][5]保持声明顺序。这类回归测试印证了"解析—合并—翻译"流水线的真实可用性。
效果文件(SDFX):组合的声明式入口
SDSL 着色器类之上还有一层SDFX(effect 文件),用声明式语法描述"如何把若干 shader mixin 组合成一个 effect"。仓库示例 assets/SDFX/BasicEffect.sdfx:
namespace Stride.Rendering.Images; shader A { int a; } shader B { float b; } params BasicParams { bool MixA; bool MixB; } effect BasicEffect { using params BasicParams; mixin compose Target1 = Test123; mixin compose Target2 += Test123; mixin macro Test = 1; mixin A; mixin (A); if(BasicParams.MixA) mixin (A); if(BasicParams.MixB) mixin (B); }mixin声明把 shader 并入 effect;mixin compose Target1 = Test123将组合绑定到指定名称;params定义可由外部(如材质)传入的参数集合;条件mixin则允许按开关组合不同 shader。SDFX 的解析器位于 Parsing/SDFX,与 SDSL 解析器共用文法基础设施。
测试体系:验证解析与编译的正确性
仓库为 SDSL 提供了分层测试,均位于Stride.Shaders.Tests:
- 解析测试:ParsingTests.cs 遍历
assets/SDSL、assets/SDFX下所有.sdsl/.sdfx文件执行SDSLParser.Parse并断言零错误;另有大量针对"悬空后置访问器"(如a.;、a[1];、1..;)的负向用例,确保这些语法错误被正确报告而非静默吞掉; - 编译/翻译测试:StrideShaderTests.cs 覆盖多维数组维度顺序、字段名与内置方法名冲突、
numthreads继承/覆盖、组合与继承共存等回归场景; - 资产语料:assets/SDSL/CompilerTests 包含 50+ 个
.sdsl文件(ArrayDims3D、SwitchDuplicateLabel、HSOverride、GeometryStreamsAssignKeepsOthers等),本身就是一份极佳的语法学习与边界行为参考;ParserTests、RenderTests、ComputeTests等目录则分别覆盖解析、渲染与计算场景。
如何贡献:本地克隆与开发
如需在本地查看、运行或继续开发这套解析/编译实现(README 中说明的贡献方式):
git clone https://gitcode.com/gh_mirrors/st/stride cd ./stride随后可打开 sources/shaders 目录下的解决方案(如Stride.Shaders.Tests测试项目)进行调试;测试项目内置了ShaderLoader(见 StrideShaderTests.cs),可指定资产目录加载.sdsl文件,是了解整个编译链路最直接的入口。
总结
SDSL 是 Stride 渲染系统的语言基石:它用shader类、继承/泛型/compose 赋予着色器代码面向对象的组织能力,用stream/stage/cbuffer/attributes 实现模块化封装,再经"预处理 → 解析 → 语义分析 → SPIR-V → 后端翻译"的流水线,最终输出 HLSL/SPIR-V 字节码供 Direct3D 或 Vulkan 使用。当前仓库的sources/shaders目录正是这套新实现(Parser + Compiler)的完整落地,从 README 出发,配合assets/SDSL的示例与Stride.Shaders.Tests的测试,开发者可以快速掌握 SDSL 的语言特性、编译原理与调试方法。
- 游戏开发
- 图形学
- VR
【免费下载链接】stride
Stride (formerly Xenko), a free and open-source cross-platform C# game engine.
相关推荐
从源码到AST:chibicc编译器如何解析C语言语法
从源码到AST:chibicc编译器如何解析C语言语法 chibicc是一款轻量级C编译器,它能够将C语言源代码转换为抽象语法树(AST),这是编译器前端的核心
编译器Quick Picture Viewer插件系统深度解析:如何扩展图片处理能力
Quick Picture Viewer插件系统深度解析:如何扩展图片处理能力 Quick Picture Viewer是一款轻量级、功能丰富的Windows桌
Roc 语言 return 语句深度解析:从语法快照测试到编译器源码实现
Roc 语言 return 语句深度解析:从语法快照测试到编译器源码实现 本篇以 Roc 编译器仓库中的快照测试文档 test/snapshots/statem
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考