diff --git a/docs/coming-from-glsl.md b/docs/coming-from-glsl.md index 78d6e41a..822edc0e 100644 --- a/docs/coming-from-glsl.md +++ b/docs/coming-from-glsl.md @@ -7,14 +7,14 @@ intro_image_absolute: true intro_image_hide_on_mobile: false --- -# Overview +## Overview This guide provides a reference for migrating GLSL shaders to Slang. -# Type and Syntax Reference +## Type and Syntax Reference When converting GLSL shaders to Slang, you'll need to translate GLSL types and syntax to their Slang equivalents. -## Scalar Types +### Scalar Types | GLSL Type | Slang Type | Description | |-----------|------------|-------------| @@ -31,7 +31,7 @@ When converting GLSL shaders to Slang, you'll need to translate GLSL types and s | `int64_t` | `int64_t` | 64-bit signed integer | | `uint64_t`| `uint64_t` | 64-bit unsigned integer | -## Vector Types +### Vector Types | GLSL Type | Slang Type | Description | |-----------|------------|-------------| @@ -54,7 +54,7 @@ When converting GLSL shaders to Slang, you'll need to translate GLSL types and s | `dvec3` | `double3` | 3-component double vector | | `dvec4` | `double4` | 4-component double vector | -## Matrix Types +### Matrix Types | GLSL Type | Slang Type | Description | |-----------|------------|-------------| @@ -77,7 +77,7 @@ When converting GLSL shaders to Slang, you'll need to translate GLSL types and s | `f16mat4x2` | `half4x2` | 4×2 half-precision float matrix | | `f16mat4x3` | `half4x3` | 4×3 half-precision float matrix | -## Vector/Matrix Construction +### Vector/Matrix Construction GLSL: @@ -103,7 +103,7 @@ float4x4 transform = float4x4( ); ``` -## Matrix Operations +### Matrix Operations GLSL: @@ -133,7 +133,7 @@ float4 transformed = mul(a, v); // matrix * column vector Note: Slang uses the `mul()` function for matrix multiplication rather than the `*` operator. Also, vectors are treated as column vectors in Slang, while they're treated as row vectors in GLSL, so the order of arguments is inverted. -## Shader Inputs and Outputs +### Shader Inputs and Outputs GLSL and Slang handle shader inputs and outputs differently. Here's a fragment/pixel shader example: @@ -187,11 +187,11 @@ float4 main(float2 texCoord : TEXCOORD0) : SV_Target0 { Both Slang methods produce the same result, but Method 1 is more organized for complex shaders with many inputs and outputs, while Method 2 is more concise for simple shaders. -## Built-in Variables +### Built-in Variables GLSL provides built-in variables that can be accessed directly in shaders, while Slang requires these variables to be explicitly declared as inputs or outputs in shader entry point functions. Unlike regular shader inputs and outputs which you define yourself, built-in variables provide access to system values like vertex IDs, screen positions, etc. -### Example: Minimal Vertex Shader with Built-in Variables +#### Example: Minimal Vertex Shader with Built-in Variables Here's a simplified example showing how built-in variables work in both GLSL and Slang: @@ -236,7 +236,7 @@ Notice the key differences: For more complex shaders, you can use structures for inputs and outputs as shown in the previous section. -### Built-in Variables Reference +#### Built-in Variables Reference The following table maps GLSL built-in variables to their corresponding Slang system value semantics across standard shader stages. In GLSL, these variables are accessed directly as globals, while in Slang they must be declared explicitly in function signatures with appropriate semantic annotations. @@ -277,7 +277,7 @@ The following table maps GLSL built-in variables to their corresponding Slang sy | `gl_ViewportIndex` | `SV_ViewportArrayIndex` | | `gl_WorkGroupID` | `SV_GroupID` | -### Ray Tracing Built-ins +#### Ray Tracing Built-ins Ray tracing built-ins in Slang are accessed through function calls rather than system value semantics. The following table maps GLSL ray tracing built-ins to their corresponding Slang functions: @@ -296,11 +296,11 @@ Ray tracing built-ins in Slang are accessed through function calls rather than s | `gl_WorldRayDirectionEXT` | `WorldRayDirection()` | | `gl_WorldRayOriginEXT` | `WorldRayOrigin()` | -## Built-in Functions and Operators +### Built-in Functions and Operators When porting GLSL shaders to Slang, most common mathematical functions (sin, cos, tan, pow, sqrt, abs, etc.) share identical names in both languages. However, there are several important differences to be aware of, listed below: -### Key Function Differences +#### Key Function Differences | GLSL | Slang | Description | |------|-------|-------------| @@ -313,31 +313,31 @@ When porting GLSL shaders to Slang, most common mathematical functions (sin, cos | `m1 * m2` (matrix multiply) | `mul(m1, m2)` | Matrix multiplication | | `v * m` (row vector) | `mul(m, v)` (column vector) | Vector-matrix multiplication | -# Resource Handling +## Resource Handling This section covers how to convert GLSL resource declarations to Slang, including different buffer types, textures, and specialized resources. -## Resource Binding Models +### Resource Binding Models Slang supports three different binding syntax options to accommodate both HLSL-style and GLSL-style resource declarations: -### GLSL Binding Model +#### GLSL Binding Model ```glsl // GLSL uses binding and set numbers layout(binding = 0, set = 0) uniform UniformBufferA { ... }; ``` -### Slang Binding Models +#### Slang Binding Models -#### Option 1: HLSL Register Syntax +##### Option 1: HLSL Register Syntax ```hlsl // Using register semantics with space (equivalent to set) ConstantBuffer bufferA : register(b0, space0); ``` -#### Option 2: GLSL-style Layout Syntax +##### Option 2: GLSL-style Layout Syntax ```hlsl // Using GLSL-style binding with [[vk::binding(binding, set)]] @@ -345,7 +345,7 @@ ConstantBuffer bufferA : register(b0, space0); ConstantBuffer bufferA; ``` -#### Option 3: Direct GLSL Layout Syntax +##### Option 3: Direct GLSL Layout Syntax ```hlsl // Using layout syntax identical to GLSL @@ -354,7 +354,7 @@ layout(binding = 0, set = 0) ConstantBuffer bufferA; All binding syntax options work the same way for all resource types (ConstantBuffer, Texture2D, RWStructuredBuffer, etc.). The GLSL-style options (2 and 3) can be particularly helpful when porting GLSL shaders without changing the binding model. -## Buffer Types +### Buffer Types | Resource Type | GLSL | Slang | Description | |---------------|------|-------|-------------| @@ -412,11 +412,11 @@ Key differences to note: 2. In Slang, structured buffers must be indexed even for a single element (e.g., `data[0].value`) 3. In Slang, all buffer members are accessed through the buffer variable name (e.g., `params.scale`, `data[0].value`) -## Texture Resources +### Texture Resources This section covers the declaration and usage of texture resources in Slang compared to GLSL. -### Combined Texture Samplers +#### Combined Texture Samplers GLSL: ```glsl @@ -471,7 +471,7 @@ float4 main(float4 position : SV_Position) : SV_Target0 { | `textureGather(sampler, coord)` | `sampler.Gather(coord)` | Gather four texels | | `textureGatherOffset(sampler, coord, offset)` | `sampler.GatherOffset(coord, offset)` | Gather with offset | -### Separate Texture Samplers +#### Separate Texture Samplers GLSL: ```glsl @@ -501,7 +501,7 @@ float4 main(float4 position : SV_Position) : SV_Target { } ``` -## Image Resources +### Image Resources This section covers the declaration and usage of image resources in Slang compared to GLSL, including how to perform image load/store operations. @@ -556,13 +556,13 @@ void main(uint3 dispatchThreadID : SV_DispatchThreadID) { | `imageStore(image, coord, value)` | `image[coord] = value` or `image.Store(coord, value)` | Write to image | | `imageSize(image)` | `image.GetDimensions(width, height)` | Get image dimensions | -# Shader Entry Point Syntax +## Shader Entry Point Syntax GLSL and Slang use fundamentally different approaches for shader entry points. While GLSL always uses a `void main()` function with implicit inputs/outputs through global variables, Slang uses explicitly typed entry points with decorators and function parameters/return values. This section provides a general overview of the entry point pattern differences. Detailed shader stage-specific conversion information will be covered in a later section. -## Core Entry Point Pattern +### Core Entry Point Pattern **GLSL Pattern:** ```glsl @@ -593,7 +593,7 @@ return_type main(parameter_type param : semantic) : return_semantic { } ``` -## Shader Stage Decorators +### Shader Stage Decorators | Shader Stage | Slang Decoration | |--------------|------------------| @@ -614,13 +614,13 @@ return_type main(parameter_type param : semantic) : return_semantic { Note: For detailed conversions of specific shader stages including tessellation, geometry, mesh, and ray tracing shaders, see the "Shader Stage-Specific Conversions" section later in this guide. -# Shader Stage-Specific Conversions +## Shader Stage-Specific Conversions This section provides mappings for each shader stage from GLSL to Slang, focusing on essential declarations, attributes, and stage-specific functionality. -## Vertex Shaders +### Vertex Shaders -### Core Declaration Mapping +#### Core Declaration Mapping **GLSL:** ```glsl @@ -638,13 +638,13 @@ float4 main(float3 position : POSITION) : SV_Position { } ``` -### Key Conversion Points +#### Key Conversion Points - **Declaration**: Add `[shader("vertex")]` decoration -## Fragment/Pixel Shaders +### Fragment/Pixel Shaders -### Core Declaration Mapping +#### Core Declaration Mapping **GLSL:** ```glsl @@ -669,15 +669,15 @@ float4 main(float4 position : SV_Position) : SV_Target0 { } ``` -### Key Conversion Points +#### Key Conversion Points - **Declaration**: Add `[shader("pixel")]` decoration - **Early Z testing**: Use `[earlydepthstencil]` attribute - **Multiple outputs**: Use a struct with multiple fields with `SV_Target0/1/2` semantics -## Compute Shaders +### Compute Shaders -### Core Declaration Mapping +#### Core Declaration Mapping **GLSL:** ```glsl @@ -696,15 +696,15 @@ void main(uint3 dispatchThreadID : SV_DispatchThreadID) { } ``` -### Key Conversion Points +#### Key Conversion Points - **Thread group size**: Replace `layout(local_size_x=X...) in` with `[numthreads(X,Y,Z)]` - **Shared memory**: Replace `shared` with `groupshared` - **Barriers**: Replace `barrier()` with `GroupMemoryBarrierWithGroupSync()` -## Hull Shader (Tessellation Control) +### Hull Shader (Tessellation Control) -### Core Declaration Mapping +#### Core Declaration Mapping **GLSL:** ```glsl @@ -742,7 +742,7 @@ PatchTessFactors PatchConstantFunction(InputPatch patch) { } ``` -### Key Attribute Mappings +#### Key Attribute Mappings | GLSL | Slang | Description | |------|-------|-------------| @@ -752,16 +752,16 @@ PatchTessFactors PatchConstantFunction(InputPatch patch) { | *Implicit* | `[outputtopology("triangle_cw/triangle_ccw/line")]` | Output topology | | *N/A* | `[patchconstantfunc("FunctionName")]` | Function for tessellation factors | -### Key Conversion Points +#### Key Conversion Points - **Split structure**: Separate the per-control-point calculations from patch-constant calculations - **Explicit domain/partitioning**: Add required attributes that are implicit in GLSL - **Patch constants**: Create separate function decorated with `[patchconstantfunc]` - **Input/Output patches**: Use `InputPatch` and return individual control points -## Domain Shader (Tessellation Evaluation) +### Domain Shader (Tessellation Evaluation) -### Core Declaration Mapping +#### Core Declaration Mapping **GLSL:** ```glsl @@ -788,7 +788,7 @@ float4 main( } ``` -### Key Attribute Mappings +#### Key Attribute Mappings | GLSL | Slang | Description | |------|-------|-------------| @@ -796,16 +796,16 @@ float4 main( | `layout(equal_spacing) in` | *Set in hull shader* | Tessellation spacing | | `layout(ccw) in` | *Set in hull shader* | Winding order | -### Key Conversion Points +#### Key Conversion Points - **Domain specification**: Use `[domain("tri/quad/isoline")]` attribute - **Tessellation coordinates**: Access via `SV_DomainLocation` parameter - **Control points**: Access via `OutputPatch` parameter - **Patch constants**: Access via a patch constant struct parameter -## Geometry Shader +### Geometry Shader -### Core Declaration Mapping +#### Core Declaration Mapping **GLSL:** ```glsl @@ -834,7 +834,7 @@ void main( } ``` -### Key Attribute Mappings +#### Key Attribute Mappings | GLSL | Slang | Description | |------|-------|-------------| @@ -842,7 +842,7 @@ void main( | `layout(points/line_strip/triangle_strip) out` | Output stream type | Output primitive type | | `layout(max_vertices = N) out` | `[maxvertexcount(N)]` | Maximum output vertices | -### Key Conversion Points +#### Key Conversion Points - **Declaration**: Add `[shader("geometry")]` and `[maxvertexcount(N)]` - **Input primitives**: Use primitive type prefix on input array parameter @@ -850,9 +850,9 @@ void main( - **Vertex emission**: Replace `EmitVertex()` with `outputStream.Append(v)` - **End primitive**: Replace `EndPrimitive()` with `outputStream.RestartStrip()` -## Amplification Shader (Task Shader) +### Amplification Shader (Task Shader) -### Core Declaration Mapping +#### Core Declaration Mapping **GLSL:** ```glsl @@ -881,16 +881,16 @@ void main( } ``` -### Key Conversion Points +#### Key Conversion Points - **Declaration**: Add `[shader("amplification")]` and `[numthreads(X,Y,Z)]` decorations - **Payload**: Replace `taskPayloadSharedEXT` with output parameter - **Dispatch**: Replace `EmitMeshTasksEXT` with `DispatchMesh` - **Thread IDs**: Access thread IDs through function parameters with semantics -## Mesh Shader +### Mesh Shader -### Core Declaration Mapping +#### Core Declaration Mapping **GLSL:** ```glsl @@ -928,7 +928,7 @@ void main( } ``` -### Key Attribute Mappings +#### Key Attribute Mappings | GLSL | Slang | Description | |------|-------|-------------| @@ -936,7 +936,7 @@ void main( | `layout(triangles) out` | `[outputtopology("triangle")]` | Output primitive type | | `layout(max_vertices = N, max_primitives = M) out` | Output parameters | Maximum vertices/primitives | -### Key Conversion Points +#### Key Conversion Points - **Declaration**: Add `[shader("mesh")]` and `[numthreads(X,Y,Z)]` decorations - **Output topology**: Use `[outputtopology("triangle")]` attribute @@ -944,9 +944,9 @@ void main( - **Vertex access**: Replace `gl_MeshVerticesEXT[idx]` with `verts[idx]` - **Primitive access**: Replace `gl_PrimitiveTriangleIndicesEXT[idx]` with `prims[idx]` -## Ray Tracing Shaders +### Ray Tracing Shaders -### Ray Generation Shader +#### Ray Generation Shader **GLSL:** ```glsl @@ -975,7 +975,7 @@ void main() { } ``` -### Closest Hit Shader +#### Closest Hit Shader **GLSL:** ```glsl @@ -998,7 +998,7 @@ void main(inout PayloadData payload, in BuiltInTriangleIntersectionAttributes at } ``` -### Miss Shader +#### Miss Shader **GLSL:** ```glsl @@ -1021,7 +1021,7 @@ void main(inout PayloadData payload) { } ``` -### Key Conversion Points +#### Key Conversion Points - **Declaration**: Use appropriate decorator for each shader type - **Ray payload**: Pass as `inout` parameter instead of global variable diff --git a/docs/compilation-api.md b/docs/compilation-api.md index b337e503..9910b811 100644 --- a/docs/compilation-api.md +++ b/docs/compilation-api.md @@ -7,8 +7,7 @@ intro_image_absolute: true intro_image_hide_on_mobile: false --- -Using the Slang Compilation API -=============================== +## Using the Slang Compilation API This tutorial explains the flow of calls needed to use the Slang Compilation API. The overall sequence is described in [Basic Compilation](#basic-compilation) followed by a discussion on more advanced topics. @@ -16,8 +15,7 @@ Using the compilation API offers much more control over compilation compared to The Slang compilation API is provided as a dynamic library. Linking to it, you have access to the compilation API which is organized in a Component Object Model (COM) fashion. The Slang [User Guide](https://shader-slang.com/slang/user-guide/compiling.html#using-the-compilation-api) describes Slang's "COM-lite" interface a bit more. -Table of Contents -================= +## Table of Contents * [Basic Compilation](#basic-compilation) * [Precompiled Modules](#precompiled-modules) @@ -28,8 +26,7 @@ Table of Contents * [Post-Compilation Reflection](#post-compilation-reflection) * [Complete Example](#complete-example) -Basic Compilation ------------------ +### Basic Compilation This is the overall flow needed to compile even the simplest applications. @@ -41,8 +38,8 @@ This is the overall flow needed to compile even the simplest applications. 6. [Link Program](#link) 7. [Get Target Kernel Code](#get-target-kernel-code) -## Step-by-step -### Includes +### Step-by-step +#### Includes The main header file is `slang.h`, though you also need `slang-com-ptr.h` to have the definition of Slang::ComPtr used throughout the API. `slang-com-helper.h` is nice to have, since it provides helpers for checking API return values and otherwise using COM. ```cpp #include "slang.h" @@ -50,7 +47,7 @@ The main header file is `slang.h`, though you also need `slang-com-ptr.h` to hav #include "slang-com-helper.h" ``` -### Create Global Session +#### Create Global Session The global API call to `createGlobalSession` is always going to be the first runtime step, since it establishes a connection to the Slang API implementation. ```cpp @@ -58,7 +55,7 @@ The global API call to `createGlobalSession` is always going to be the first run createGlobalSession(globalSession.writeRef()); ``` -### Create Session +#### Create Session To read more about what sessions are all about, see [About Sessions](#about-sessions). Creating a session sets the configuration for what you are going to do with the API. @@ -72,7 +69,7 @@ The `SessionDesc` object holds all the configuration for the Session. slang::SessionDesc sessionDesc = {}; ``` -#### List of enabled compilation targets +##### List of enabled compilation targets Here, only one target is enabled, `spirv_1_5`. You can enable more targets, for example, if you need to be able to compile the same source to DXIL as well as SPIRV. ```cpp @@ -84,7 +81,7 @@ Here, only one target is enabled, `spirv_1_5`. You can enable more targets, for sessionDesc.targetCount = 1; ``` -#### Preprocessor defines +##### Preprocessor defines Slang supports using the preprocessor. ```cpp @@ -97,7 +94,7 @@ Slang supports using the preprocessor. sessionDesc.preprocessorMacroCount = preprocessorMacroDesc.size(); ``` -#### Compiler options +##### Compiler options Here is where you can specify Session-wide options. Check the [User Guide](https://shader-slang.com/slang/user-guide/compiling.html#compiler-options) for info on available options. @@ -113,7 +110,7 @@ Here is where you can specify Session-wide options. Check the [User Guide](https sessionDesc.compilerOptionEntryCount = options.size(); ``` -#### Create the session +##### Create the session With a fully populated `SessionDesc`, the session can be created. @@ -122,7 +119,7 @@ With a fully populated `SessionDesc`, the session can be created. globalSession->createSession(sessionDesc, session.writeRef()); ``` -### Load Modules +#### Load Modules Modules are the granularity of shader source code that can be compiled in Slang. When using the compilation API, there are two main functions to consider. @@ -145,11 +142,11 @@ Modules are the granularity of shader source code that can be compiled in Slang. } ``` -#### Life Time of Modules +##### Life Time of Modules Modules are owned by the slang Session. Once loaded, they are valid as long as the Session is valid. -### Query Entry Points +#### Query Entry Points Slang shaders may contain many entry points, and it's necessary to be able to identify them programatically in the Compilation API in order to select which entry points to compile. @@ -173,7 +170,7 @@ A common way to query an entry-point is by using the `IModule::findEntryPointByN It is also possible to query entry-points by index, and work backwards to check the name of the entry-points that are returned at different indices. Check the [User Guide](https://shader-slang.com/slang/user-guide/reflection.html#program-reflection) for info. -### Compose Modules and Entry Points +#### Compose Modules and Entry Points Up to this point, modules have been loaded, and entry points have been identified, but to move forward with defining a GPU program, the relevant subset need to be selected for _composition_ into a unified program. @@ -197,7 +194,7 @@ Up to this point, modules have been loaded, and entry points have been identifie } ``` -### Link +#### Link Ensure that there are no missing dependencies in the composed program by using `link()`. @@ -213,7 +210,7 @@ Ensure that there are no missing dependencies in the composed program by using ` } ``` -### Get Target Kernel Code +#### Get Target Kernel Code Finally, it's time to compile the linked Slang program to the target format. @@ -269,7 +266,7 @@ Both methods cache results within the session and will return a pre-compiled blo About Sessions -------------- -### What's a session? +#### What's a session? A session is a scope for caching and reuse. As you use the Slang API, the session caches everything that is loaded in it. @@ -281,7 +278,7 @@ It's strongly recommended to use as few session objects as possible in applicati Using long-lived sessions with Slang API is a big advantage over compiling with the standalone `slangc` compiler executable, since each invocation of `slangc` creates a new session object by necessity. -### When do I need a new Session? +#### When do I need a new Session? A session does have some global state in it which currently makes it unable to cache and reuse artifacts, namely, the `#define` configurations. Unique combinations of preprocessor `#defines` used in your shaders will require unique session objects. @@ -299,16 +296,16 @@ API methods for module precompilation are described in the [User Guide](https:// Specialization -------------- -### Link-time Constants +#### Link-time Constants This form of specialization involves placing relevant constant definitions in a separate Module that can be selectively included. For example, if you have two variants of a shader that differ in constants that they use, you can create two different Modules for the constants, one for each variant. When composing one variant or the other, just select the right constants module in createCompositeComponentType(). This is described also in the [User Guide](https://shader-slang.com/slang/user-guide/link-time-specialization.html#link-time-constants) -### Link-time Types +#### Link-time Types Similar to Link-time Constants. This form of specialization simply puts different versions of user types in separate modules so that the needed implementation can be selected when creating the CompositeComponentType. [User Guide](https://shader-slang.com/slang/user-guide/link-time-specialization.html#link-time-types) -### Generics Specialization +#### Generics Specialization Say you have a shader that has a feature in it that can be in one of two states, "High Quality" and "Low Quality". One way to support both modes of operation is to use generics. Put the logic for the two modes into two structs, both conforming to an interface (e.g. `IQuality`) that can be consistently used by callers. @@ -572,7 +569,7 @@ void computeMain(uint3 threadId_0 : SV_DispatchThreadID) Notice in particular the switch case added in the function `float U_S11specialized8IQuality8getValuep0pf_0(uint2 _S1)`. -### Supporting both specialization and dynamic dispatch +#### Supporting both specialization and dynamic dispatch In the prior example, `HighQuality` and `LowQuality` are both supported in a single uber-shader, compiled to support dynamic dispatch on the call to `getValue()`. To achieve this, two `ITypeConformance` objects were added to the composite component. @@ -816,7 +813,7 @@ int main() } ``` -### Compile it (g++ directions) +#### Compile it (g++ directions) * Assumes Slang is installed in the current directory at `slang`. * Assumes program is saved to "shortest.cpp". * Assumes a release build of Slang. @@ -827,7 +824,7 @@ If any of the above assumptions are wrong in your case, adjust the paths below t g++ -I./slang/include --std=c++14 shortest.cpp -L./slang/build/Release/lib/ -l:libslang.so ``` -### Run it +#### Run it ```bat LD_LIBRARY_PATH=slang/build/Release/lib ./a.out diff --git a/docs/external/stdlib-reference b/docs/external/stdlib-reference index 2f726a94..966e9943 160000 --- a/docs/external/stdlib-reference +++ b/docs/external/stdlib-reference @@ -1 +1 @@ -Subproject commit 2f726a94fa45d56b75862d4e1932f9999602c02d +Subproject commit 966e9943b9320918e35c5f6dffa3d47f05a84a8f diff --git a/docs/parameter-blocks.md b/docs/parameter-blocks.md index 29b4348b..84f86ff4 100644 --- a/docs/parameter-blocks.md +++ b/docs/parameter-blocks.md @@ -11,9 +11,9 @@ The `ParameterBlock<>` type in the Slang core library can be used to implement e This article will introduce parameter blocks and the problem they solve, provide guidelines for how to use them in a shader codebase, and show how the Slang reflection API can be used to conveniently interface with such shader code from a host application. -# Background +## Background -## Ordinary and Opaque Types +### Ordinary and Opaque Types The uniform parameters of a GPU shader may include data of both *ordinary* and *opaque* types. @@ -24,7 +24,7 @@ Opaque types are those that might be passed via mechanisms that are specific to On some hardware, shader parameters of opaque type might be passed via GPU hardware state registers or other architecture-specific means. Even when data of an opaque type is stored in memory, the exact representation (and sometimes even the *size*) of that data is not known to application code, so that access to that data must be mediated by driver software. -## Descriptor Sets, Tables, etc. +### Descriptor Sets, Tables, etc. Many performance-oriented GPU APIs include a construct that can be used to group together shader parameter data of opaque type: @@ -43,12 +43,12 @@ GPU shader programmers are encouraged to group parameters based on their expecte Most of these APIs support defining some form of *pipeline layout*: an API object that defines the groups and their contents, independent on any particular shader program. Groups that are bound using a given pipeline layout can be re-used across multiple shader programs/pipelines, so long as they all agree on the layout. -## Challenges +### Challenges While grouping of parameter data can help improve efficiency for applications that issue many draw calls, adoption of these mechanisms in production shader codebases has been limited. There are several challenges that arise when developers try to leverage parameter groups and how they were exposed in languages like GLSL and HLSL -### Not all targets support groups +#### Not all targets support groups On some targets, such as CPUs and CUDA/OptiX, everything is "just memory" and all types are ordinary. There is no need for an API-/driver-mediated grouping construct, because GPU-accessible buffers are sufficient. @@ -62,7 +62,7 @@ Note that the way support for descriptor sets was added to GLSL (HLSL makes simi The basic idea is that grouping is only indicated via `layout` modifiers (`register` modifiers in HLSL), and applications can use preprocessor techniques to select between different modifiers based on the target (e.g., hiding the `layout(set=...)` modifiers when compiling for OpenGL). While this design choice was expedient, requiring minimal changes to the shading language and enabling simple porting of existing code, it creates its own challenges. -### Pervasive manual annotation is required +#### Pervasive manual annotation is required When compiling GLSL code for Vulkan, every shader parameter that goes into a descriptor set other than `set=0` *requires* a `layout` modifier. In practice, most shader codebases that intend to make use of groups for Vulkan, D3D12, etc. rely on manual annotation of *every* shader parameter. @@ -114,7 +114,7 @@ Developers typically group parameters that belong in the same `set` together tex Maintaining manual annotations is straightforward but tedious--exactly the kind of work that we should offload onto our tools. -### Grouping constructs have different rules per-platform +#### Grouping constructs have different rules per-platform Some platform-specific differences are relatively minor: @@ -174,7 +174,7 @@ Because of the differences in how Vulkan and D3D12 count, cross-platform HLSL co Note that both the `dxc` and `slangc` compilers support a variety of command line options (e.g., `-fvk-b-shift`, `-fvk-t-shift`, etc.) that can be used to derive set and binding indices for Vulkan (and WebGPU) from D3D-style `register` modifiers, but these options are not easy to work with and we do *not* recommend them as a general-purpose solution for portable codebases. -# Parameter Blocks +## Parameter Blocks Slang provides *parameter blocks* as an alternative to all the headaches of manual annotation. Taking the recurring example we have used and translating it to idiomatic Slang yields: @@ -208,7 +208,7 @@ Rather than manually annotating many global shader parameters to put them in the Note that despite the absence of any manual annotations in this Slang code, it produces *exactly* the same pipeline layout as the heavily-annotated HLSL at the end of the previous section, for both D3D12 and Vulkan. In addition, this code can work across *all* of the targets that the Slang compiler supports, including targets that do not have a built-in grouping construct. -## Automatic Layout and Binding +### Automatic Layout and Binding The Slang compiler is responsible for *binding* the parameters of a shader program to their locations for a particular target (where locations can include registers, bindings, slots, etc.). The number (and kind) of locations used by a parameter depends on the *layout* of its type for the target. @@ -231,7 +231,7 @@ The relative order in which entry points are visited is deterministic, based on Typically, a `ParameterBlock<>` declaration will claim the next available group index (e.g., a Vulkan `set` index or a D3D12 `space` index). There are, however, a few details that are worth going into. -### A Simple Case +#### A Simple Case Consider the `Material` type from our running example: @@ -256,7 +256,7 @@ ParameterBlock material; the fields within the `struct` will all be bound to the same `set` index (here `set=0`), and each will have a `binding` based on its relative offset. For example, `material.specularMap` will be bound to a location equivalent to `layout(set=0, binding=1)`. -### Ordinary Data +#### Ordinary Data The `Environment` type from our running example is more complicated than `Material`, in that it mixes both ordinary- and opaque-type fields: @@ -288,7 +288,7 @@ Because the contents of the block include ordinary data (96 bytes worth), the Sl The automatically-introduced constant buffer for `environment` will get `binding` zero in the set, and all the other opaque-type fields inside the block will have their relative `binding`s adjusted to account for this. For example, the `environment.envMap` field will be bound as if `layout(set=1,binding=3)` was used, despite the relative offset of `envMap` in `Environment` being only two `binding`s. -### Empty Blocks +#### Empty Blocks As a corner case, a parameter block may be declared using an empty type for its content: @@ -299,7 +299,7 @@ ParameterBlock empty; In this case, the type `Empty` contains nothing: no bytes, no `binding`s. There is no need to allocate a full `set` index for nothing, so the Slang compiler does not do so. -### Nested Blocks +#### Nested Blocks It is valid in Slang code to nest one parameter block inside another. For example: @@ -316,11 +316,11 @@ ParameterBlock outer; Most of the targets that Slang supports do not allow a group to contain references to other groups, so the Slang compiler legalizes such declarations by "flattening" them. For example, given the above code as input, the Slang compiler would assign `set=0` to the parameter `outer`, and the nested block `outer.inner` would get `set=1`. -### Targets Without Groups +#### Targets Without Groups For targets that do not have a built-in construct for grouping opaque-type parameters, the Slang compiler will treat a `ParameterBlock` exactly the same as a `ConstantBuffer`. -#### Older Targets +##### Older Targets On an older target like D3D11/DXBC, we can think of the Slang compiler taking code like this: @@ -363,18 +363,18 @@ Texture2D material_specularMap; SamplerState material_sampler; ``` -#### "It's all just memory" targets +##### "It's all just memory" targets For targets where all types are ordinary, such as CPU and CUDA/OptiX, a `ParameterBlock` is equivalent to a `ConstantBuffer` but, more importantly, both are represented as just a pointer to a `Thing` (more or less a `Thing const*` in C notation). These targets do not require automatically-introduced constant buffers (ordinary buffers can mix types like `float4` and `Texture2D`, since both are ordinary), nor do they require any flattening (nested parameter blocks are directly supported, since `ParameterBlock` is itself an ordinary type). -# Guidelines for Shader Programmers +## Guidelines for Shader Programmers The Slang language and compiler support a wide variety of idioms for declaring and using shader parameters, because so many different approaches are found in production shader codebases. However, there are guidelines that we recommend for developers who are writing new shader codebases in Slang, or who are refactoring an existing codebase to be cleaner and more maintainable. -## Avoid Manual Annotations +### Avoid Manual Annotations Whenever possible, developers should declare shader parameters in a simple, readable fashion and then allow the Slang compiler's automatic and deterministic binding rules to apply. Manual annotations are impractical to maintain at the scale of real-time rendering codebases we are now seeing as path tracing and machine-learning-based approaches become more widespread. @@ -382,7 +382,7 @@ Manual annotations are impractical to maintain at the scale of real-time renderi Typically, developers who decide to drop manual annotations end up adopting run-time reflection as part of their application codebase, but this is not the only option. Because the binding and layout algorithm used by Slang is deterministic, it is also possible for applications to use the Slang reflection API as part of an offline code generation step, to produce code or data that drives run-time application logic for setting shader parameters. -## Avoid Polluting the Global Scope +### Avoid Polluting the Global Scope It is common in many shader codebases to put global shader parameters next to the code that operates on them. For example, a `material.h` file might combine the shader parameters for a material with code to evaluate it: @@ -420,7 +420,7 @@ float4 evalMaterial(Material m, ...) { ... } This kind of design allows the programmer who is writing shader entry points to decide how parameters should be grouped (or not). -## For Compute Shaders, Use Entry-Point Parameters +### For Compute Shaders, Use Entry-Point Parameters It is common practice to define multiple compute entry points in a single file. For example, these might be entry points that work together to implement some algorithm that requires multiple dispatches. @@ -463,7 +463,7 @@ A better solution is to properly scope uniform shader parameters to the entry po In this revised code, it is not possible for the `depthTestObjects` function to accidentally reference the uniform shader parameters of `computeMinMaxDepthTiles`; those parameters are (correctly) out of scope. Furthermore, when compiling this file with the Slang compiler, automatic binding will not include parameters from one entry point in the pipeline layout of the other. -### Aside: Push Constants +#### Aside: Push Constants Note that when compiling for Vulkan, any `uniform` entry-point parameters of ordinary type are automatically bound to push-constant data. For example: @@ -497,7 +497,7 @@ struct AddParams {...} ``` -## For Multi-Stage Pipelines, Use Global Parameter Blocks +### For Multi-Stage Pipelines, Use Global Parameter Blocks When writing programs for a multi-stage pipeline such as rasterization or ray-tracing, we recommend that developers put all of the entry points that will be used together in one file (when it is practical to do so), and declare uniform shader parameters at global scope in that file. For example: @@ -521,7 +521,7 @@ Note that even though this approach makes use of the global scope, it intentiona Note that if the code for a shader program declares *any* global-scope uniform parameters that aren't themselves `ParameterBlock<>`s, then the automatic binding algorithm will claim a group to hold those parameters; this will generally always be a group with index zero (e.g., `set=0` for Vulkan). Developers are advised to know whether they intend to declare all of their parameters using parameter blocks (in which case their explicit blocks will start at index zero), or they play to mix other global scope parameters (group index zero) with parameter blocks (indices one and up). -### Aside: Shader Records +#### Aside: Shader Records When compiling ray tracing entry points for Vulkan, any `uniform` parameters of an entry point are automatically bound to the "shader record." For example: @@ -535,23 +535,23 @@ void closestHitMain( In this case, the `closestHitMain` entry point will fetch the `materialIndex` parameter from the shader record. -# Using Parameter Blocks With Reflection +## Using Parameter Blocks With Reflection In this section, we will demonstrate how an application that uses parameter blocks in its shader code can utilize the Slang reflection API to help in setting up the data structures used by a GPU API like Vulkan. -## Scope +### Scope The approach we will describe here is only appropriate for applications that abide by *one* key constraint: The shader codebase for the application must not use manual binding annotations (e.g., `layout(...)`, `register(...)`, or `[[vk::binding(...)]]`). In the absence of manual annotation, the Slang compiler will bind parameters to locations in a deterministic fashion, and an application can simply mirror that deterministic logic in its own code in order to derive the locations that parameters have been bound to. Applications that need to support the fully general case (including shader code with manual binding annotations) can still make use of parameter blocks. -In such cases, developers are encouraged to read the other [documentation](slang/user-guide/reflection) that exists for the Slang reflection API. +In such cases, developers are encouraged to read the other [documentation](user-guide/reflection) that exists for the Slang reflection API. This section will also restrict itself to the Vulkan API, for simplicity. We cover the creation of descriptor set layouts and pipeline layouts using reflection, but *not* the task of writing to them. -Developers interested in using the Slang reflection API for *writing* to descriptor sets are encouraged to read the [documentation](docs/shader-cursors) that we have provided on the "shader cursor" idiom. +Developers interested in using the Slang reflection API for *writing* to descriptor sets are encouraged to read the [documentation](shader-cursors) that we have provided on the "shader cursor" idiom. -## What Goes Into a Pipeline Layout? +### What Goes Into a Pipeline Layout? Given a Slang shader program that has been compiled and then reflected as a `slang::ProgramLayout`, our goal is ultimately to create a `VkPipelineLayout` that is compatible with that program. @@ -591,7 +591,7 @@ VkPipelineLayout PipelineLayoutBuilder::finishBuilding() } ``` -## What Goes Into a Descriptor Set Layout? +### What Goes Into a Descriptor Set Layout? In order to fill in the `setLayouts` array, we will clear need to create some `VkDescriptorSetLayout`s. Similarly to the case for pipeline layouts, this amounts to filling in a `VkDescriptorSetLayoutCreateInfo` so that we can call `vkCreateDescriptorSetLayout()`. @@ -627,7 +627,7 @@ void DescriptorSetLayoutBuilder::finishBuilding() } ``` -## Parameter Blocks +### Parameter Blocks In typical cases, a `ParameterBlock<>` in Slang shader code will translate into a descriptor set added to the pipeline layout, with one or more descriptor ranges added to that descriptor set based on the element type of the parameter block. We can summarize this logic as something like: @@ -644,7 +644,7 @@ void PipelineLayoutBuilder::addDescriptorSetParameterBlock( } ``` -### Automatically-Introduced Uniform Buffer +#### Automatically-Introduced Uniform Buffer The most important detail that needs to be accounted for is that if the element type of the parameter block (the `Thing` in `ParameterBlock`) has any amount of ordinary data in it (that is, `Thing` consumes one or more bytes), then the Slang compiler automatically introduces a uniform buffer to pass that data. The automatically-introduced uniform buffer will only be present if it was needed (that is, when the element type has a non-zero size in bytes) and it will always precede any other bindings for the parameter block. @@ -687,7 +687,7 @@ The most important detail to note here is that the Vulkan `binding` index for th Instead, this code simply takes the next available `binding` index in the descriptor set layout. This code is an example of how an application can streamline its interactions with the Slang reflection API when its shader code eschews the complexity of manual binding annotations. -### Ordering of Nested Parameter Blocks +#### Ordering of Nested Parameter Blocks When parameter blocks end up nested within one another, the Slang compiler always assigns outer blocks Vulkan `set` indices before those of inner blocks. If we naively traverse the hierarchy of parameter blocks in a depth-first order, adding them to the `descriptorSetLayouts` array when we are done with each, then they will end up in the wrong order. @@ -726,7 +726,7 @@ void DescriptorSetLayoutBuilder::finishBuilding() } ``` -### Empty Parameter Blocks +#### Empty Parameter Blocks Most parameter blocks will map to a Vulkan descriptor set, but it is possible to have a block that contains nothing but other blocks, in which case a descriptor set for the outer block would contain no descriptor ranges and thus be irrelevant. The application code we show here uses a simple strategy to account for such cases. @@ -762,7 +762,7 @@ VkPipelineLayout PipelineLayoutBuilder::finishBuilding() } ``` -## Descriptor Ranges +### Descriptor Ranges Typically, a leaf field of opaque type (each `Texture2D`, `SamplerState`, etc.) in the element type of a parameter block will translate into a range of descriptors in the resulting descriptor set layout that share a single `binding` (represented as a `VkDescriptorSetLayoutBinding`). We could write logic in the application to recursively traverse the element type and find all of these leaf fields, but getting that logic right can be tricky. @@ -844,7 +844,7 @@ VkDescriptorType mapSlangBindingTypeToVulkanDescriptorType(slang::BindingType bi } ``` -### Some Ranges Need to Be Skipped +#### Some Ranges Need to Be Skipped The Slang reflection API exposes ranges for all of the non-ordinary data in a type, independent of what API-specific mechanism is used for those ranges. For example, the reflected ranges for a type may include both ranges that should map to a `binding` in a Vulkan descriptor set and ranges that should map to push constants. @@ -867,7 +867,7 @@ void DescriptorSetLayoutBuilder::addDescriptorRange( We will account for push-constant ranges in the next section. -## Sub-Object Ranges +### Sub-Object Ranges The Slang reflection API methods we've used so far are used to query what data a type (logically) contains directly in its storage. But types can also *indirectly* reference other storage, such as when a field of type `ParameterBlock<>` is nested within another parameter block. @@ -917,7 +917,7 @@ void DescriptorSetLayoutBuilder::addRanges( } ``` -### Nested Parameter Blocks +#### Nested Parameter Blocks Any nested parameter blocks will be reflected as a sub-object range. The content of such a nested block will not be added to the current descriptor set layout, and will instead result in an additional descriptor set being added to the pipeline layout: @@ -933,7 +933,7 @@ case slang::BindingType::ParameterBlock: break; ``` -### Push Constant Ranges +#### Push Constant Ranges In Slang shader code, a push-constant range can be defined in either of two ways: @@ -979,7 +979,7 @@ Note that this code accounts for the corner case where shader code has declared A key thing to observe here is that currently the Slang compiler only supports having a single `ConstantBuffer<>` with the `[[vk::push_constant]]` attribute being in scope for each entry point (meaning either a single global buffer for all stages, or distinct per-entry-point buffers). Because there can only be a single push-constant range for each entry point, the code here can assume that each range starts at an offset of zero. -## Creating a Pipeline Layout for a Program +### Creating a Pipeline Layout for a Program At this point we have covered how to use the Slang reflection API to account for the contributions of a `ParameterBlock<>` to a Vulkan pipeline layout. What remains is to show the top-down process for creating a pipeline layout for an entire Slang shader program. @@ -1012,7 +1012,7 @@ VkPipelineLayout createPipelineLayout( Note how this logic builds a *default* descriptor set. Any top-level shader parameters that aren't themselves explicit `ParameterBlock<>`s will be added into that descriptor set. -### Global Scope +#### Global Scope The global-scope shader parameters of a program can be handled using the building blocks we have already presented: @@ -1034,7 +1034,7 @@ The main new detail to note here is that we set up the `_currentStageFlags` vari Applications that want to set more precise stage flags, taking into account which data is accessed by which stages in the compiled program binary, are encouraged to look at the more comprehensive documentation on the reflection API. -### Entry Points +#### Entry Points Adding the entry-parameters of a program amounts to adding the parameters of each entry point in turn: @@ -1080,7 +1080,7 @@ VkShaderStageFlags getShaderStageFlags(SlangStage stage) } ``` -## Conclusion +### Conclusion In this article we have attempted to cover parameter blocks from multiple perspectives: diff --git a/docs/shader-cursors.md b/docs/shader-cursors.md index a39b26ee..19d6ac81 100644 --- a/docs/shader-cursors.md +++ b/docs/shader-cursors.md @@ -7,26 +7,26 @@ intro_image_absolute: true intro_image_hide_on_mobile: false --- -# Introduction +## Introduction This document describes a comprehensive strategy for handling *parameter-passing* for GPU shader code. The presented approach is able to scale to large shader codebases which implement many different features across multiple modules. The Slang reflection API was intentionally designed to support this approach and to make it practical to adopt. -## Target Audience(s) +### Target Audience(s) Our primary audience for this document is developers who are building and maintaining a large GPU shader codebase, and who have the freedom or opportunity to consider their high-level architecture for shader parameter passing. These developers may find that the approach outlined here is a good fit for their requirements, and we hope that they will consider adopting it. Developers who are already locked in to an architectural approach may find this material enlightening or aspirational, but not immediately actionable. A second audience for this document is any developer who wants to better understand the motivations behind the design of the Slang reflection API -- especially developers who are considering contributing to the Slang project such that they might need to make changes to the interface or implementation of reflection. Because the engine approach described here motivates many of the design choices in the API, an understanding of the material in this document should help inform understanding of the reflection API design. -## This is not the only valid approach! +### This is not the only valid approach! It is important to emphasize that this document only describes *one* approach to parameter-passing. Other approaches are possible and can yield good results; indeed, these other approaches are quite common in production shader codebases. The approach we describe here was developed in collaboration with early adopters of Slang, based on their requirements and the challenges they face. We believe that this is a good general-purpose approach, and that developers can benefit from understanding it even if they do not adopt it. Developers who are not interested in the approach described here are invited to utilize other documentation on the Slang reflection API instead. -# Background: Shader Parameter Passing +## Background: Shader Parameter Passing -## The Challenge +### The Challenge In typical programming languages, a developer does not often have to think about *parameter passing*; one function simply calls another, and the values given as arguments automatically show up as the values of parameters in the called function. Things are not so simple for GPU graphics programmers, for a few key reasons. @@ -36,11 +36,11 @@ Second, the underlying mechanisms for passing parameter data from the CPU “cal Furthermore, different target platforms and GPU APIs may use drastically different mechanisms for the same type of parameter. For example, a simple global-scope texture parameter is passed via an API-defined slot in Metal, via a descriptor set for Vulkan, and as ordinary data inside a buffer for OptiX. -## Typical Approaches +### Typical Approaches Before describing our approach to shader parameter passing, we will survey how the above challenges are tackled in many existing applications and engines. -### Encapsulate Shader Features as Files +#### Encapsulate Shader Features as Files A large shader codebase is usually decomposed into a variety of different *features*: logical units of shader code that can be combined together as part of larger programs. The features of a real-time ray-tracing codebase might include various types of light sources, material models, path-tracing strategies, and reconstruction filters. @@ -62,7 +62,7 @@ float3 evaluateSkyLighting(...); A particular set of entry points for a rendering pass would then `#include` the files that define the features that will be used in that pass (perhaps conditionally, if certain features are optional and can be enabled/disabled for the given pass). -### Define Shader Parameters as Global Variables +#### Define Shader Parameters as Global Variables A given feature will typically have its own shader parameters. For example the sky lighting feature shown above might have a parameter to represent the color and intensity of the light from the sun, while the CSM feature would have parameters for shadow map textures. A typical application would declare those parameters at global scope, using code similar to the following: @@ -95,11 +95,11 @@ cbuffer SkyUniforms float3 evaluateSkyLighting(...); ``` -### Determine Where Each Global Parameter is Bound +#### Determine Where Each Global Parameter is Bound Here we find a split in how typical codebases approach shader parameter passing. In order for host application code to set the value for a shader parameter like `shadowCascadeSampler` above, it needs to know both the API-specific mechanism used to set parameters of that type (e.g., a descriptor written into a descriptor set for Vulkan) and the location that the parameter is *bound* to for that mechanism (e.g., for Vulkan the location would include the descriptor set index and binding index). -#### Manually Bind Parameters to API-Specific Locations +##### Manually Bind Parameters to API-Specific Locations Historically, shading languages like HLSL and GLSL have supported a wide variety of annotations that can be manually attached to shader parameters to specify how those parameters should be bound to locations for API-specific mechanisms. For example, an HLSL programmer might use a `register` annotation to manually specify what register and space a texture should bind to for D3D12, and a `[[vk::binding]]` annotation to manually specify what binding index and descriptor set it should bind to for Vulkan: @@ -120,7 +120,7 @@ Note that the Slang compiler supports manual binding annotations like the above; While manual specification is a superficially simple idea, it does not scale well to large shader codebases that need to run on many different platforms. Manual binding of parameters to locations is effectively a global register-allocation problem that must be done by hand; any two features that might be used together must not specify overlapping locations. Further, because of differences in the mechanisms used by different GPU APIs, and their related rules and restrictions, additional annotations are often needed for each target platform/API. -#### Use Reflection to Query How Parameters are Bound +##### Use Reflection to Query How Parameters are Bound A more scalable approach, in terms of software development effort, than manual specification of binding is to allow the compiler for GPU shader code to automatically bind shader parameters to their per-target-API locations, and then to use *reflection* to query the location that was assigned to each parameter. @@ -130,13 +130,13 @@ Because all of the parameters of *all* features are declared at the same (global Furthermore, most GPU compilers make few or no guarantees about the consistency of how parameters are automatically bound to locations when the same features (and thus the same parameters) are used in multiple programs, or when code is conditionally enabled/disabled (even just code in function bodies). The lack of such guarantees further impedes applications from efficiently using and *re-using* API constructs like descriptor sets across GPU operations. -### Summary +#### Summary We have described the way shader parameter passing is implemented today in typical codebases, and highlighted how these approaches typically suffer from either poor scalability in software-development effort, or poor runtime efficiency. Next we describe an approach that allows application developers to circumvent this trade-off. -# Use Types to Encapsulate Shader Features +## Use Types to Encapsulate Shader Features -## Each Feature Becomes a Struct in Slang +### Each Feature Becomes a Struct in Slang The essence of our approach is *type-based encapsulation*. Put simply, for each shader feature that a developer introduces, they should take *all* of its parameters and group them into a Slang `struct` type. This type will then be used to define the parameter-passing interface between the host application and GPU code. For example, to revisit our example of a cascaded shadow map feature: @@ -159,7 +159,7 @@ Encapsulating parameters into `struct` types avoids polluting the global scope w One reason why (ab)use of global variables may have been popular in shader code is that it provides the operations of a feature (like evaluateCSM() in the earlier code examples) implicit access to the shader parameters of that feature. In the revised code we show above, the global-scope function evaluateCSM() has been turned into an evaluate() method on the CascadedShadowMap type itself. The body of that method enjoys the same kind of convenient access to the parameters of the feature, through the implicit this parameter of the method. -### Nest Types When it Makes Sense +#### Nest Types When it Makes Sense When one feature depends on another (as our example sky light feature depends on the CSM feature), and there is a “has-a” relationship between those features, we encourage developers to model this relationship by nesting the relevant types: @@ -181,7 +181,7 @@ struct SkyLight Note here how the `SkyLight type` includes a field, `csm`, of type `CascadedShadowMap`. Because a single `struct` type like `CascadedShadowMap` can encapsulate all of the parameters of a feature, independent of their types, the sky light feature can be insulated from the implementation details of the CSM feature; adding or removing parameters from the CSM feature does not require changing the code in `SkyLight.slang`. -## Each Feature Also Gets a Host Type +### Each Feature Also Gets a Host Type The next key to our approach is to define a type in the host application that corresponds to each feature in the GPU shader codebase. For our running example, a host application written in C++ might define something like: @@ -204,7 +204,7 @@ Just as the Slang struct type defined earlier encapsulates the logical feature i Note that the host type does *not* need to declare the exact same members with the exact same types as the GPU code. As a simple example, the `shadowCascadeDistances` are a vector in the GPU code and an array in the CPU code. All that matters is that this host type logically owns the data that needs to be passed to the parameters of the GPU feature. -### Nesting Should be Mirrored in the Host Code +#### Nesting Should be Mirrored in the Host Code In cases where features in the GPU shader codebase logically nest, the corresponding host types should also make use of nesting: @@ -227,7 +227,7 @@ private: Here we see that the host-side representation of the sky light feature owns an object of the host-side `CascadedShadowMap` type. The nesting relationship in the shader code is mirrored in the host code. -## Encapsulate Parameter Passing in a Method +### Encapsulate Parameter Passing in a Method The next essential ingredient for our approach is a method belonging to the host type, that is responsible for writing parameter data for the corresponding GPU feature. For our running example, this would be something like: @@ -259,7 +259,7 @@ void CascadedShadowMap::writeInto(MyEngine::ShaderCursor cursor) Conceptually, this code takes a cursor, representing the destination to write to, and uses it to navigate to the individual parameters (fields of the `CascadedShadowMap` struct in the shader code) and write to them. Much of the remainder of this document is dedicated to showing how to (easily) implement the relevant operations on `ShaderCursor` such as `field()`, `element()`, and `write()`. -### When Features Nest, Re-Use the Methods +#### When Features Nest, Re-Use the Methods One of the benefits of our type-based approach to encapsulation is that we can easily re-use the logic for writing parameter data in a hierarchical fashion, when types nest. For example, the `writeInto()` method for the sky light feature might look like: @@ -277,7 +277,7 @@ void SkyLight::writeInto(MyEngine::ShaderCursor cursor) Here we see that the `SkyLight::writeInto()` method leverages the `CascadedShadowMap::writeInto()` method to write the parameter data for the `csm` field. We see again that our approach allows the CSM feature to fully encapsulate its implementation details; the sky light feature need not have any knowledge of the fields of CascadedShadowMap or their types. -## Declare Parameter Blocks on Entry Points +### Declare Parameter Blocks on Entry Points At this point a reader may be left asking where this approach actually bottoms out; it can’t be `struct`s all the way down. @@ -304,31 +304,31 @@ For example, when compiling code like this for Vulkan/D3D12, the parameter `skyL For simple compute entry points we recommend declaring `ParameterBlock<>`s as `uniform` parameters of the entry point. For other pipelines like rasterization and ray-tracing, where programs are composed from multiple entry points, `ParameterBlock<>`s should currently be declared as global-scope uniform parameters. -## What have we gained? +### What have we gained? The approach we are proposing here is so simple that it may appear naive, or like it cannot possibly provide any benefits over the existing approaches that we surveyed. In truth, however, the apparent simplicity in the application code comes from the way that this approach takes advantage of carefully designed aspects of the Slang language, compiler, and reflection API. -### Compared to Manual Binding +#### Compared to Manual Binding Compared to the approach of declaring individual global-scope shader parameters with manual binding annotations, our approach keeps the code for shader features simple and uncluttered, and supports software development practices that scale to large codebases with many features that support many target platforms. Our approach *retains* the key benefit of manual binding: the ability to group logically-related parameters for efficient parameter passing via target-specific mechanisms like descriptor sets. -### Compared to Existing Reflection-Based Approaches +#### Compared to Existing Reflection-Based Approaches Compared to the approach of declaring individual global-scope shader parameters and then using reflection to discover how those parameters were bound, our approach can support more efficient use of parameter-passing mechanisms like descriptor sets/tables. It is vital to note that the layout for the members of a struct type in Slang is stable for a given platform, so long as the contents of the struct do not change. This means that when the same `struct` type is used in different shader programs, or different variants of the same program, it will always be laid out the same. For example, because the layout of a `struct` like `SkyLight` is stable, a GPU descriptor set/table that has been allocated and filled in using `SkyLight::writeInto()` can be re-used across GPU program invocations, even for different programs or variants. -# Implementing a Shader Cursor Using Slang +## Implementing a Shader Cursor Using Slang We have shown examples of how the features in an application’s shader codebase can be encapsulated to make shader parameter passing logic clean and composable. These examples fundamentally rely on the `MyEngine::ShaderCursor` type, so we now shift our focus to showing how such a type can be implemented easily and efficiently in an application or engine codebase. As a temporary simplification to keep the presentation as simple as possible, the `ShaderCursor` type we build in this section will be specific to the Vulkan API. A later section will discuss how the approach presented here can be extended to work with *all* of the target platforms and APIs that Slang supports. -## Public Interface +### Public Interface Our earlier code examples, notably the `writeInto()` method bodies, have already given us an idea of the operations that a shader cursor needs to support. -### Navigation +#### Navigation Given a cursor that points to a location with some aggregate type (a `struct` or array), an application needs a way to navigate to a part of that aggregate: a field of a `struct` or an element of an array. The corresponding operations are: @@ -346,7 +346,7 @@ public: The `field()` operations form a cursor to a field of a `struct`, based on either the name or index of the field. The `element()` operation forms a cursor to an element of an array. -### Writing +#### Writing Once a cursor has been formed that points to a small enough piece of parameter data, such as an individual texture, an application needs a way to write a value for that parameter. The corresponding operations are: @@ -367,9 +367,9 @@ public: In this example shader cursor implementation, different overloads of the `write()` operation deal with values of ordinary types and values of opaque types. The client of this interface does not need to think about the details of how different types of parameters might be passed on different target platforms and GPU APIs. -## State +### State -### Location +#### Location A shader cursor logically represents a location to which the values of shader parameters can be written. Different GPU APIs expose different mechanisms for parameter-passing, and a given feature in a shader codebase might include parameters that get bound to any or all of those mechanisms. @@ -399,7 +399,7 @@ uint32_t m_bindingArrayIndex; Here our example `ShaderCursor` type has members to represent a location for each of the two parameter-passing mechanisms mentioned above. For ordinary data, there is a buffer and a byte offset into the buffer, representing a location within the buffer. For descriptors, there is a descriptor set, the index of a binding in that descriptor set, and an array index into the bindings at that index; together these fields represent a location within the descriptor set. -### Type +#### Type A shader cursor acts much like a pointer, but the type of the data being pointed to is determined dynamically rather than statically. Thus rather than having a type template parameter like a C++ smart pointer would, this shader cursor implementation stores the type (and layout) as a field: @@ -415,17 +415,17 @@ private: }; ``` -## Implementing the Operations +### Implementing the Operations We will now go through the operations in the public interface for the example `ShaderCursor` type, and show how each can be implemented with the help of Slang’s reflection API. Note that for simplicity of presentation, the implementation shown here does not include error checks, such as checking that a structure field actually exists or that an array element index is in bounds. -### Writing +#### Writing The implementation of writing data to a shader cursor is, in general, specific to a given GPU API, and also can depend on the type of data being written (ordinary data, textures, samplers, etc.). -#### Ordinary Data +##### Ordinary Data For the Vulkan API, shader parameters of ordinary types are passed via buffer memory. Writing such a parameter to a cursor entails writing that data to the underlying buffer: @@ -453,7 +453,7 @@ Developers are encouraged to carefully evaluate the performance trade-offs of us For the purposes of this document, the important thing to note is that all of the information needed to write to the buffer is readily available. -#### Opaque Types +##### Opaque Types For the Vulkan API, textures and other opaque types are passed via descriptor sets. Writing a texture to a cursor entails writing a descriptor into a descriptor set: @@ -480,11 +480,11 @@ void ShaderCursor::write(MyEngine::Texture* texture) Here most of the code is just the boilerplate required to set up a call to `vkUpdateDescriptorSets()`. The essential thing to note is that all of the fields of the `VkWriteDescriptorSet` type can be determined from either the state of the shader cursor, or from the texture image itself. -### Navigation +#### Navigation The operations above for writing using a shader cursor have been simple because the shader cursor tracks exactly the information that the corresponding Vulkan API operations need. The most important task remaining is to show how to compute this information correctly (and efficiently) when using a cursor to navigate to a structure field or array element. -#### Fields +##### Fields The task of navigating to a structure field by name can be reduced to the task of navigating by the field index: @@ -517,7 +517,7 @@ The byte offset of the field is the byte offset of the aggregate plus the offset The starting binding index of the field is the starting binding index of the aggregate plus the offset of the field in binding indices. This code explicitly queries the offset of the field using the `slang::ParameterCategory` that corresponds to Vulkan binding indices. -#### Elements +##### Elements Navigating a shader cursor to an array element is only slightly more complicated than navigating to a structure field: @@ -544,7 +544,7 @@ The byte offset of an array element is computed in an unsurprising manner: an ad Note that for many of the targets Slang supports the stride of a type layout is not the same as its size, whereas in C/C++ the stride between consecutive elements in an array of `T` is always `sizeof(T)`. -##### Computing the index into a binding array +###### Computing the index into a binding array While computing the byte offset of an array element is straightforward, the logic for indexing into the array elements of a descriptor set binding may require some explanation. The step where the desired element index is added to the existing index is intuitive, but the previous step where the existing index is multiplied by the element count of the array may not immediately make sense. @@ -565,7 +565,7 @@ If we start with a cursor pointing to `t`, with `m_bindingArrayElement` equal to * The `.element(x)` operation is indexing into a 3-element array, and will yield a new cursor with `m_bindingArrayElement` equal to `0*3 + x = x` * The `.element(y)` operation is indexing into a 5-element array, and will yield a new cursor with `m_bindingArrayElement` equal to `x*5 + y`, which is the desired result -# Reflecting on What Has Been Covered So Far +## Reflecting on What Has Been Covered So Far So far, this document has presented: @@ -582,11 +582,11 @@ The remainder of this document is dedicated to bridging the gaps between what ha * The Vulkan-specific shader cursor needs to be revised to support multiple platforms * An application needs to be able to allocate constant buffers and parameter blocks based on reflection data, and to kick off the parameter-passing logic for top level shader parameters (such as parameter blocks) -# Making a Multi-Platform Shader Cursor +## Making a Multi-Platform Shader Cursor Above we showed an example implementation of an engine-specific ShaderCursor type, that only supported the Vulkan API. In this section we will show how to revise that implementation to support multiple targets, with help from the Slang reflection API. -## What is the challenge? +### What is the challenge? In order for the Vulkan-specific ShaderCursor to present a clean and easy-to-use interface, it was important that it stored a representation of a location to be written to for *each* of the parameter-passing mechanisms that Vulkan supports (namely: ordinary bytes in a buffer, and descriptors in a descriptor set). When navigating a cursor to a field of a struct or element of an array, the ShaderCursor implementation needed to compute proper offsets to apply for each of these mechanisms. @@ -596,7 +596,7 @@ The full diversity of parameter-passing mechanisms for target GPU APIs can be se Trying to tackle this problem head-on by storing additional offsets/indices in an engine-specific `ShaderCursor` leads to messier and more complicated code, with a lot of target-specific logic mixed up with the core work that the `ShaderCursor` is meant to perform. -## Simplifying the problem with *binding ranges* +### Simplifying the problem with *binding ranges* The Slang reflection API provides an additional decomposition of type layouts in terms of target-independent *binding ranges*. Each binding range corresponds to a “leaf” in the layout of a type, where that leaf has a type that is opaque for at least some type. As a contrived example: @@ -621,9 +621,9 @@ In this example, the type `Material` contains two binding ranges: Every type layout can be broken down as zero or more bytes of ordinary data, and zero or more binding ranges. All of the binding ranges of a type are grouped together for counting and indexing, so that no matter how complicated of a type an application is working with a `ShaderCursor` implementation need only track two things: a byte offset for ordinary data, and an index for a binding range. -## Applying this to the example ShaderCursor implementation +### Applying this to the example ShaderCursor implementation -### Representation of Location +#### Representation of Location Our Vulkan-specific shader cursor implementation used the following state to represent the location being written to: @@ -665,7 +665,7 @@ struct ShaderCursor We now turn our attention to how that object being written into should be represented. -### RHI ShaderObject Interface +#### RHI ShaderObject Interface An application/engine that supports many target GPU APIs will typically define an *RHI* (rendering hardware interface) that abstracts over the essential API- or hardware-specific operations. Looking at the Vulkan-specific ShaderCursor implementation above, the various write() operations include direct Vulkan API calls, and thus need to be put behind an RHI in a portable engine. @@ -692,11 +692,11 @@ void ShaderCursor::write(MyEngine::Texture* texture) } ``` -## Target-Independent ShaderCursor Navigation +### Target-Independent ShaderCursor Navigation With a new representation for the index/offset information in a location, the core `ShaderCursor` operations `field()` and `element()` need to be updated. It turns out, however, that the new target-independent versions are similarly clean and simple compared to the earlier Vulkan-specific versions. -### Fields +#### Fields Here is the target-independent code for navigating to a field by index: @@ -716,7 +716,7 @@ ShaderCursor ShaderCursor::field(int index) The only substantive difference here is that instead of updating a Vulkan-specific binding index using a query for just `slang::ParameterCategory::DescriptorSlot`, the code instead uses a separate binding-range oriented part of the reflection API: `slang::TypeLayoutReflection::getFieldBindingRangeOffset()`. -### Elements +#### Elements In contrast the case for structure fields, the logic for array elements does not really change *at all*: @@ -738,11 +738,11 @@ ShaderCursor ShaderCursor::element(int index) Astute readers might have noted that nothing about the array-indexing case shown previously had appeared Vulkan-specific, and that is indeed the case. All that has changed here is renaming to account for the new `ShaderOffset` type. -### That’s Really It +#### That’s Really It The above changes to the `field()` and `element()` operations are sufficient to make the navigation operations on `ShaderCursor` target-independent. The remaining work is all in the RHI implementation of a shader object for each target. -## Target-Specific ShaderObject Implementation +### Target-Specific ShaderObject Implementation The target-API-specific shader object implementation is responsible for interacting with the underlying GPU API, and is also the place where the target-independent binding range abstraction needs to be translated over to the target-specific concepts. @@ -816,7 +816,7 @@ This RHI code is able to remain simple and clean because the Slang reflection AP * The `slang::TypeLayoutReflection::getBindingRangeIndexOffset()` method translates the target-independent binding range index into the Vulkan binding index * The `slang::TypeLayoutReflection::getBindingRangeType()` method provides information about the binding range that can be mapped directly to the `VkDescriptorType` -## Is that really all the code that is needed? +### Is that really all the code that is needed? Yet again, we have arrived at a solution so simple it might seem like nothing has been done. We took a Vulkan-specific shader cursor and split it into a target-independent shader cursor that uses the concept of binding ranges as exposed by the Slang reflection API, and then showed how the parts of the old implementation that interacted with the Vulkan API can be moved under an RHI. @@ -824,9 +824,9 @@ At this point RHI `ShaderObject` implementations comparable to `VulkanShaderObje What has been (intentionally) glossed over thus far is how to allocate `ShaderObjects` corresponding to constant buffers and parameter blocks, and how to handle the top-level shader parameters corresponding to such buffers and blocks. -# Buffers, Blocks, and Top-Level Shader Parameters +## Buffers, Blocks, and Top-Level Shader Parameters -## When to use buffers, blocks, or just plain structs +### When to use buffers, blocks, or just plain structs The Slang core library includes two constructs that allow a developer to control how shader parameters are passed: `ConstantBuffer<>` and `ParameterBlock<>`. Both of these constructs work best when shader features organize their parameters into struct types, as this document recommends. While, e.g., both `ConstantBuffer` and `ParameterBlock` are allowed and meaningful, their meanings differ in a few key ways. @@ -841,7 +841,7 @@ struct Aggregate } ``` -### Targets with Descriptor Sets +#### Targets with Descriptor Sets For a targets like Vulkan, D3D12, or WebGPU, which support descriptor sets or something equivalent: @@ -853,7 +853,7 @@ For a targets like Vulkan, D3D12, or WebGPU, which support descriptor sets or so A developer who is aggregating/nesting types in their shader code can thus pick between these three options as needed in order to match their desired policies for how shader parameter data will be re-used or not. Each successive option above introduces more indirection than the option before it, which can enable more buffers and/or descriptor sets to be re-used, but may come with other trade-offs. -### Other Targets +#### Other Targets Not all of the compilation targets that Slang supports have behavior akin to Vulkan/D3D12/WebGPU, and these additional targets have their own layout behaviors: @@ -863,13 +863,13 @@ Not all of the compilation targets that Slang supports have behavior akin to Vul Developers of portable codebases are encouraged to pick between the three options illustrated above (by-value nesting, constant buffers, or parameter blocks) based on performance considerations for targets like Vulkan/D3D12; doing so will tend to result in good performance on these other targets as well. -## Allocating Shader Objects +### Allocating Shader Objects An attentive reader may have had a lurking question from when we presented our first (Vulkan-specific) shader cursor implementation: how is one supposed to allocate a `VkDescriptorSetLayout` to correspond to some type in their Slang shader code? For example, given a `ParameterBlock` shader parameter, how does one allocate a descriptor set based on the type `SkyLight`? We have intentionally deferred discussion of this question until *after* presenting a multi-platform implementation of shader cursors, because it turns out that the binding ranges reflected by Slang provide a convenient answer to this question. -### What Information Is Needed? +#### What Information Is Needed? We are going to focus primarily on the needs of Vulkan and D3D12 here. Other target platforms and APIs tend to have similar considerations, or are simpler to work with. @@ -890,7 +890,7 @@ The more interesting challenge is the descriptor set/table. Consider the Vulkan The first of those steps is the one where support from the Slang reflection API is clearly needed. -### The Hard Way: Recursively Walking Reflected Type Layouts +#### The Hard Way: Recursively Walking Reflected Type Layouts In order to enumerate all of the bindings in an arbitrary `slang::TypeLayoutReflection`, an application needs to recursively traverse the structure of types. This can be accomplished with a helper type like: @@ -986,13 +986,13 @@ case slang::TypeReflection::Kind::Struct: break; ``` -#### An Important Assumption +##### An Important Assumption The above code is doing something subtle, that readers should note. Rather than use the Slang reflection API to query the binding indices that were assigned by the Slang compiler, this code is simply incrementing the `m_bindingIndex` member every time a suitable leaf field is encountered. This approach works because the application code shown here is mirroring the same simple and deterministic way that the Slang compiler traverses types to automatically compute layout. Such a simple approach will not work in codebases that deviate from the approach that this document advocates for, and who make heavy use of manually-specified binding. Note that these assumptions are valid in part because the Slang compiler, as a matter of policy, does not eliminate unused shader parameters (whether as top-level parameters, or nested within structures) prior to computing layout and assigning binding locations. This design choice is in contrast to most existing GPU shader compilers, which aggressively eliminate unused textures, and other parameters of opaque types, and then only perform layout and reflect the parameters that remain. -### The Easy Way: Using Binding Ranges +#### The Easy Way: Using Binding Ranges The concept of binding ranges in the Slang is closely linked to the way that descriptor sets are organized for Vulkan/D3D12/etc. Using the Slang reflection API for binding ranges makes it possible to write a non-recursive version of the above logic: @@ -1018,7 +1018,7 @@ Note that making use of binding ranges not only eliminates the need for recursio Note also that even when using binding ranges to simplify this code, an application still needs to apply logic as shown in `addBindingsForParameterBlock()` to account for a constant-buffer binding, in the case where a type includes ordinary data. As in the earlier example, we are keeping the code simple by computing binding indices directly in the host code (`m_bindingIndex++`), in a way that will match Slang’s automatic layout rules; this simple logic might not work for a codebase that employs manual binding annotations. -## Cache and Re-Use Shader Object Layouts +### Cache and Re-Use Shader Object Layouts Given the way that, e.g., the Vulkan API distinguishes between `VkDescriptorSets` and `VkDescriptorSetLayouts`, we advocate that the RHI for an applicaiton/engine should make a distinction between `ShaderObject`s and `ShaderObjectLayout`s. The relevant parts of the RHI might look something like: @@ -1036,11 +1036,11 @@ class RHI We encourage applications to cache and re-use `ShaderObjectLayout`s, to avoid redundantly creating multiple RHI objects corresponding to the same Slang reflection information. -## Writing to a Top-Level Shader Parameter +### Writing to a Top-Level Shader Parameter When application shader code has been written so that top-level shader parameters are all `ParameterBlock<>`s, it can be very easy to kick off parameter passing. This section will show the logic an application can use to construct and fill in a fresh parameter block for a top-level shader parameter, using the RHI design already shown. -### When Using Globals +#### When Using Globals In the case where the shader parameter in question is declared as a global: @@ -1065,7 +1065,7 @@ void appCodeToRenderSomething( In this example, the application code has determined the `descriptorSetIndex` that the `gSkyLightParameter` maps to for Vulkan (or, equivalently, the register space that the parameter maps to for D3D12). -### When Using Entry-Point Parameters +#### When Using Entry-Point Parameters In the case where the shader parameter in question is declared as a parameter of a compute entry point: @@ -1097,7 +1097,7 @@ Just as in the case for a global parameter, we have extracted the `skyLightParam Note that this code assumes that *all* of the shader parameters are being declared as uniform entry-point parameters. Cases that mix global and entry-point shader parameters need some additional logic that is beyond what that document has space to cover. -### Creating a Shader Object and Initial Cursor +#### Creating a Shader Object and Initial Cursor Regardless of how the reflection-data for the top-level parameter is queried, the next major task is to create a shader object based on the type of the type of the data in the parameter block, and to start filling it in: @@ -1124,7 +1124,7 @@ Next the code allocates a shader object to match the type layout for `SkyLight`, Once the shader object is allocated, the code invokes the `SkyLight::writeInto()` operation to write the state of the CPU-side `SkyLight `object into the GPU-side `SkyLight` structure, and then sets the shader object as a descriptor set using the application/engine’s RHI. -### Creating a Shader Cursor from a ShaderObject +#### Creating a Shader Cursor from a ShaderObject The key missing piece in the preceding code example is the operation to create a shader cursor from a shader object, but this turns out to be a simple matter. The desired operation in the `ShaderCursor` API is: @@ -1146,7 +1146,7 @@ ShaderCursor::ShaderCursor(ShaderObject* object) {} ``` -# Conclusion +## Conclusion Our goal with this document has been to illustrate an idiomatic approach that an application/engine can use to achieve GPU shader code and host-side logic for shader parameter-passing that is both practical and scalable to large codebases. diff --git a/docs/understanding-generics.md b/docs/understanding-generics.md index 496b8f06..3576d29b 100644 --- a/docs/understanding-generics.md +++ b/docs/understanding-generics.md @@ -7,9 +7,9 @@ intro_image_absolute: true intro_image_hide_on_mobile: false --- -# Understanding Slang Generics +## Understanding Slang Generics -## Introduction +### Introduction When writing shader code, we often need to write similar logic that works with different types. For example, we might have different kinds of lights in our @@ -23,7 +23,7 @@ be verified at the definition site rather than when the code is used. This leads to clearer error messages and faster compilation times, as the compiler doesn't need to repeatedly verify the same code for each use case. -## Generics, Traits, and Type Classes +### Generics, Traits, and Type Classes Slang's generics system shares similarities with several modern programming language features: Swift's protocols, Rust's traits, and Haskell's type @@ -37,7 +37,7 @@ capabilities." This distinction leads to more flexible and composable code, as types can implement multiple interfaces without the complexity of multiple inheritance. -## A Motivating Example +### A Motivating Example Let's start with a common scenario in graphics programming - implementing different types of lights: @@ -132,7 +132,7 @@ In short, overloaded functions can only be used when the types are fully known at the call site. Overloading can also obscure the programmer's intentions, giving poorer error messages and code understandability. -### Here's how we can improve this with generics and interfaces: +#### Here's how we can improve this with generics and interfaces: We can define an interface, to which lights must conform. @@ -244,7 +244,7 @@ This generic solution provides several benefits: objects which aren't light will tell the programmer exactly that, rather than some error message within the implementation of `cullLights` -## Performance and Compilation +### Performance and Compilation Like Rust traits and C++ templates, Slang's generics are completely resolved at compile time through a process called monomorphization ([except when using existential types](https://github.com/shader-slang/slang/blob/master/docs/design/existential-types.md)). This means that for @@ -266,7 +266,7 @@ specialized for `PointLight` and one for `SpotLight`. This results in: - Full optimization opportunities for each specific type - Larger binary size when many specializations are needed -## From C++ Templates to Slang Generics +### From C++ Templates to Slang Generics While C++ templates and Slang generics can solve similar problems, their approaches differ significantly. The lighting example, for example: @@ -330,7 +330,7 @@ interface. float addValue(T v0, T v1) where T : IArithmetic { return v0 + v1; } ``` -## Generic programming over Scalars and Vectors +### Generic programming over Scalars and Vectors It is still possible to write functions which can generically operate over scalars and vectors, for example using the @@ -339,9 +339,9 @@ or [`IFloat`](https://shader-slang.com/stdlib-reference/interfaces/ifloat-01/index.html) interfaces. -## Advanced Generic Features +### Advanced Generic Features -### Associated Types +#### Associated Types Sometimes we need to work with types that are related to our generic parameter. For example, different lights might use different parameter types, which we @@ -372,7 +372,7 @@ struct SpotLight : ILight } ``` -### Generic Value Parameters +#### Generic Value Parameters Sometimes we need to parameterize by compile-time values, for example abstracting over a compile-time integer is shown here: @@ -392,7 +392,7 @@ struct LightArray where T : ILight } ``` -## Further Reading +### Further Reading - [Interfaces in Slang](https://github.com/shader-slang/slang/blob/master/docs/design/interfaces.md) - [Existential types in Slang](https://github.com/shader-slang/slang/blob/master/docs/design/existential-types.md)