Slang Language Guide

Making it easier to work with shaders


Slang Language Guide

This document will try to describe the main characteristis of the Slang language that might make it different from other shading languages you have used.

The Basics

Slang is similar to HLSL, and it is expected that many HLSL programs can be used as Slang code with no modifications. Big-picture stuff that is supported:

  • A C-style preprocessor
  • Ordinary function, struct, typedef, etc. declarations
  • The standard vector/matrix types like float3 and float4x4
  • The less-used explicit vector<T,N> and matrix<T,R,C> types
  • cbuffer declarations for uniform parameters
  • Global-scope declarations of texture/sampler parameters, including with register annotations
  • Entry points with varying in/out parameters using semantics (including SV_* system-value semantics)
  • The built-in templated resource types like Texture2D<T> with their object-oriented syntax for sampling operations
  • Attributes like [unroll] are parsed, and passed along for HLSL/DXBC output, but dropped for other targets
  • struct types that contain textures/samplers as well as ordinary uniform data, both as function parameters and in constant buffers
  • The built-in functions up through Shader Model 6.0 (as documented on MSDN) are supported

New Features

Import Declarations

In order to support better software modularity, and also to deal with the issue of how to integrate shader libraries written in Slang into other langauges, Slang introduces an import declaration construct.

The basic idea is that if you write a file foo.slang like this:

// foo.slang

float4 someFunc(float4 x) { return x; }

you can then import this code into another file in Slang, HLSL, or GLSL:

// bar.slang

import foo;

float4 someOtherFunc(float4 y) { return someFunc(y); }

The simplest way to think of it is that the import foo declaration instructs the compiler to look for foo.slang (in the same search paths it uses for #include files), and give an error if it isn’t found. If foo.slang is found, then the compiler will go ahead and parse and type-check that file, and make any declarations there visible to the original file (bar.glsl in this example).

When it comes time to generate output code, Slang will output any declarations from imported files that were actually used (it skips those that are never referenced), and it will cross-compile them as needed for the chosen target.

A few other details worth knowing about import declarations:

  • The name you use on the import line gets translated into a file name with some very simple rules. An underscore (_) in the name turns into a dash (-) in the file name, and dot separators (.) turn into directory seprators (/). After these substitutions, .slang is added to the end of the name.

  • If there are multiple import declarations naming the same file, it will only be imported once. This is also true for nested imports.

  • Currently importing does not imply any kind of namespacing; all global declarations still occupy a single namespace, and collisions between different imported files (or between a file and the code it imports) are possible. This is a bug.

  • If file A.slang imports B.slang, and then some other file does import A;, then only the names from A.slang are brought into scope, not those from B.slang. This behavior can be controlled by having A.slang use __exported import B; to also re-export the declarations it imports from B.

  • An import is not like a #include, and so the file that does the import can’t see preprocessor macros defined in the imported file (and vice versa). Think of import foo; as closer to using namspace foo; in C++ (perhaps without the same baggage).

Explicit Parameter Blocks

One of the most important new features of modern APIs like Direct3D 12 and Vulkan is an interface for providing shader parameters using efficient parameter blocks that can be stored in GPU memory (these are implemented as descritpor tables/sets in D3D12/Vulkan, and “attribute buffers” in Metal). However, HLSL and GLSL don’t support explicit syntax for parmaeter blocks, and so shader programmers are left to manually pack parameters into blocks either using register/layout modifiers, or with API-based remapping (in the D3D12 case).

Slang supports a simple and explicit syntax for exploiting parameter blocks:

struct ViewParams
{
	float3 cameraPos;
	float4x4 viewProj;
	TextureCube envMap;
};

ParameterBlock<ViewParams> gViewParams;

In this example, the fields of gViewParams will be assigned to registers/bindings in a way that supports allocating them into a single parameter block. For example, when generating GLSL for Vulkan, the Slang compiler will generate a single uniform block (for cameraPos and viewProj) and a global textureCube for envMap, both decorated with the same layout(set = ...).

Interfaces

Slang supports declaring interfaces that user-defined struct types can implement. For example, here is a simple interface for light sources:

// light.slang

struct LightSample { float3 intensity; float3 direction; };

interface ILight
{
	LightSample sample(float3 position);
}

We can now define a simple user type that “conforms to” (implements) the ILight interface:

// point-light.slang

import light;

struct PointLight : ILight
{
	float3 position;
	float3 intensity;

	LightSample sample(float3 hitPos)
	{
		float3 delta = hitPos - position;
		float distance = length(delta);

		LightSample sample;
		sample.direction = delta / distance;
		sample.intensity = intensity * falloff(distance);
		return sample;
	}
}

Generics

Slang supports generic declarations, using the commong angle-brack (<>) syntax from languages like C#, Java, etc. For example, here is a generic function that works with any type of light:

// diffuse.slang
import light;

float4 computeDiffuse<L : ILight>( float4 albedo, float3 P, float3 N, L light )
{
	LightSample sample = light.sample(P);
	float nDotL = max(0, dot(N, sample.direction));
	return albedo * nDotL;
}

The computeDiffuse function works with any type L that implements the ILight interface. Unlike with C++ templates, the computeDiffuse function can be compiled and type-checked once (you won’t suddenly get unexpected error messages when plugging in a new type).

Global-Scope Generic Parameters

Putting generic parameter directly on functions is helpful, but in many cases existing HLSL shaders declare their parameters at global scope. For example, we might have a shader that uses a global declaration of material parameters:

Material gMaterial;

In order to allow such a shader to be converted to use a generic parameter for the material type (to allow for specialization), Slang supports declaring type parameters at the global scope:

type_param M : IMaterial;
M gMaterial;

Conceptually, you can think of this syntax as wrapping your entire shader program in a generic with parameter <M : IMaterial>. This isn’t beautiful syntax, but it may help when incrementally porting an existing HLSL codebase to use Slang’s features.

Associated Types

Sometimes it is difficult to define an interface because each type that implements it might need to make its own choice about some intermediate type. As a concrete example, suppose we want to define an interface IMaterial for material surface shaders, where each material might use its own BRDF. We want to support evaluating the pattern of the surface separate from the reflectance function.

// A reflectance function
interface IBRDF
{
	float3 eval(float3 wi, float3 wo);
}
struct DisneyBRDF : IBRDF { ... };
struct KajiyaKay : IBRDF { ... };

// a surface pattern
interface IMaterial
{
	??? evalPattern(float3 position, float2 uv);
}

What is the type ??? that evalPattern should return? We know that it needs to be a type that supports IBRDF, but which type? One material might want to use DisneyBRDF while another wants to use KajiyaKay.

The solution in Slang, as in modern languages like Swift and Rust, is to use associated types to express the depdence of the BRDF type on the material type:

interface IMaterial
{
	associatedtype B : IBRDF;
	B evalPattern(float3 position, float2 uv);
}

struct MyCoolMaterial : IMaterial
{
	typedef DisneyBRDF B;
	B evalPattern(float3 position, float2 uv)
	{ ... }
}

Associated types are an advanced concept, and we only recommend using them when they are needed to define a usable interface.

Future Extensions

Implicit Generics Syntax

The syntax for generics and interfaces in Slang is currently explicit, but verbose:

float4 computeDiffuse<L : ILight>( L light, ... )
{ ... }

As a future change, we would like to allow using an interface like ILight as an ordinary parameter type:

float4 computeDiffuse( ILight light, ... )
{ ... }

This simpler syntax would act like “syntactic sugar” for the existing explicit generics syntax, so it would retain all of the important performance properties.

Returning a Value of Interface Type

While the above dealt with using an interface as a parameter type, we would eventually like to support using an interface as the return type of a function:

ILight getALightSource(Scene scene) { ... }

Implementing this case efficiently is more challenging. In most cases, an associated type can be used instead when an interface return type would be desired.

Not Supported

Some features of the current HLSL language are not supported, but probably will be given enough time/resources:

  • Local variables of texture/sampler type (or that contain these)
  • Matrix swizzles
  • Explicit packoffset annotations on members of cbuffers

Some things from HLSL are not planned to be supported, unless there is significant outcry from users:

  • Pre-D3D10/11 syntax and operations
  • The “effect” system, and the related <> annotation syntax
  • Explicit register bindings on textures/samplers nested in cbuffers
  • Any further work towards making HLSL a subset of C++ (simply because implementing a full C++ compiler is way out of scope for the Slang project)