-
Notifications
You must be signed in to change notification settings - Fork 6
Description
SDSL uses preprocessor macros for platform specific code and other uses. It offers a similar user experience for both C# and SDSL.
Since macros can be defined through C# code and used by the preprocessor, this makes SDSL compilation impredictible.
Definitions
- Shader snippet : A piece of SDSL code that cannot be assembled as a spirv module, but can be used in inheritance or composition context. The typical
.sdslfile that user write - sdspv binary : A shader snippet that has been compiled into SDSL flavored spirv binary (invalid spirv) that is meant to be used in inheritance and composition scenarios.
- spirv binary : A valid spirv module that can be fed to the gpu driver or spirv-cross. It can be composed of many sdspv binaries thanks to the binary mixin system.
The shader compiler can compile shader snippets into sdspv and cache them for future use. This has the advantage of removing the need to parse and compile a shader snippet many times.
In an ideal scenario, if we have a shader snippet composed of many other snippets that were already compiled as sdspv, only our snippet need to be compiled, then the assembler will mix all those sdspv binaries together.
Here's a graph of how the compiler should work
flowchart TD
subgraph Spirv Compilation
A[Compile user shader A] --> B{Is shader \nalready \ncompiled ?}
B --> |yes| Spirv[Return spirv binaries]
B --> |no| C[Fetch shader code\n from asset system]
C --> ParseShader[Parse Shader]
ParseShader --> D{Does shader A \nhave compositions\nor inherited classes\nnot compiled \nto sdspv?}
D --> |no| E[Compile and mix sdspv A with \nall inherited and composed sdspv]
E --> F[Assemble shader A \nwith other snippet\n from the inherited and \ncomposed spirv shaders]
D --> |yes| G[Fetch and compile \ninherited/composed shader \nsnippets to sdspv]
G --> E
F --> Spirv
end
subgraph Parsing
Load[Load shader text \nfrom asset system] --> CommentParsing[Parse shader comments]
CommentParsing --> RemovalQ{Does shader\nhave comments ?}
RemovalQ --> |yes| Removal[build a list of ReadOnlyMemory \n of char for non-comment \ncode and use StringBuilder to \nconcatenate it into a new string]
RemovalQ --> |no| AllowedMacros{Are\nmacros\nallowed?}
AllowedMacros --> |yes| MacroRetrieve
AllowedMacros --> |no| StartCompilation
Removal --> AllowedMacros
subgraph MacroParsing
MacroRetrieve[Retrieve macro values] --> MacroObject[Replace macro objects by\ntheir defined values]
MacroObject --> MacroFunc[Replace macro function \nwith their expansions]
MacroFunc --> BuildString
BuildString[Build a string \nusing String builder] --> StartCompilation
end
end
The issue with macros
The fact that branching through shader macros can become very complex and that a user can change these shader macro values at runtime make caching shader complicated. It also breaks the type system since a single shader can be reused with different definition in different context.
Ideally a shader type would be defined once, with a specific definition across all shaders. Everything that the preprocessor can do can be done through the inheritance and composition system. Since we would have one definition per shader type, we can cache sdspv with just the identifier of the shader type. We cut short of all re-parsing of the same type, saving some precious CPU at the cost of a different UX for users.
Impacts
Both Stride and VL.Stride use macros extensively, this will need some work to rewrite things.