Shaders
Dagor Shading Language acts as a preprocessor/compiler for pure HLSL shaders. In DSHL, we can bind resources
for HLSL shaders, configure fixed shader stages (culling, Z test, …) and more.
Pure HLSL code needs to be contained within hlsl{...}
blocks.
Defining and compiling shaders
Let’s look at a simple DSHL shader example:
shader simple_shader
{
// this is the description of vertex buffer expected for the vertex shader
channel float3 pos=pos; // position
channel float3 vcol=vcol; // vertex color
hlsl {
struct VsInput
{
float3 pos: POSITION0;
float3 color: COLOR0;
};
struct VsOutput
{
float4 pos : SV_POSITION;
float3 color : COLOR0;
};
VsOutput test_vertex(VsInput input)
{
VsOutput ret;
ret.pos = float4(input.pos, 1.0);
ret.color = input.color;
return ret;
}
float4 test_pixel(VsOutput input) : SV_Target0
{
return float4(input.color.rgb, 1.0);
}
}
compile("target_vs", "test_vertex");
compile("target_ps", "test_pixel");
}
Here, shader (name)
defines the actual name that the shader will have after its compilation into pure HLSL.
Channels pos
and vcol
describe the vertex buffer data that the vertex shaders expects to recieve.
DSHL preshader creates appropriate layout for the C++ code based on these channel
variables. See Channels for more info.
After defining the shader in the hlsl
block, you need to specify its entry point via compile("target_(stage)", "entry_function")
, where the
entry_function
should be the name of the respective shader function in the hlsl
block and stage
defines one of the following shader stages:
target_vs
(vertex shader)target_hs
(hull shader)target_ds
(domain shader)target_gs
(geometry shader)target_ps
(pixel shader)target_cs
(compute shader)target_ms
(mesh shader)target_as
(amplification shader)target_vs_for_gs
(if vertex shader is used together with geometry shader on PS4/PS5, vertex shader must be compiled differently)target_vs_for_tess
(if vertex shader is used together with tesselation shader on PS4/PS5, vertex shader must be compiled differently)target_vs_half
(vertex shader with half type)target_ps_half
(pixel shader with half type)
You can also specify to which specific shader stage will the code from hlsl
block go by specifying the shader stage in the parentheses, e.g. hlsl(stage) {...}
Available shaders are: ps
, vs
, cs
, ds
, hs
, gs
, ms
, as
. If you omit the specification, the code from hlsl{...}
block will be
sent to all of these shaders.
Preshader
In addition to declaring just the shader code itself, DSHL allows you to declare a pre-shader, which is a script that allows you to easily pipe data from C++ to the shader.
The most common use case for this piping are various bindings of textures and buffers:
instead of doing the classical dance of “pick the slot, set the texture to the slot, remember not to mess up and use the same slot twice”,
you can bind variables to a shader through a global string
to DSHL data type
map called shader variables.
This map is in a 1-to-1 correspondence with the global DSHL variables you define in .dshl files Global variables, and is RW.
So you can, for example, both read an int
defined inside a shader from C++, and set a texture to a global texture
variable defined inside a shader.
On the C++ side, you simply fill in this map using set_texture
, and on the shader side, you ask the preshader system to grab a certain shader
variable and set it to an HLSL variable. The syntax is as follows:
(shader_stage) {
hlsl_variable_name @type_suffix = variable|expression [hlsl {/*hlsl text*/}]
}
This code is then compiled by our shader compiler into a sequence of simple interpreted commands, which are stored in the shader dump and executed before running a shader.
Acceptable shader stages:
cs
– Compute Shaderps
– Pixel Shadervs
– Vertex Shaderms
– Mesh Shader
Acceptable types:
Postfix |
Type |
---|---|
@f1 |
float |
@f2 |
float2 |
@f3 |
float3 |
@f4 |
float4 |
@f44 |
float4x4 |
@i1 |
int |
@i2 |
int2 |
@i3 |
int3 |
@i4 |
int4 |
@u1 |
uint |
@u2 |
uint2 |
@u3 |
uint3 |
@u4 |
uint4 |
@tex |
Texture |
@tex2d |
Texture2D |
@tex3d |
Texture3D |
@texArray |
Texture2DArray |
@texCube |
TextureCube |
@texCubeArray |
TextureCubeArray |
@uav |
Unordered Access View flag |
@smp |
Texture with SamplerState |
@smp2d |
Texture2D with SamplerState |
@smp3d |
Texture3D with SamplerState |
@smpArray |
Texture2DArray with SamplerState |
@smpCube |
TextureCube with SamplerState |
@smpCubeArray |
TextureCubeArray with SamplerState |
@shd |
Texture2D with SamplerComparisonState |
@shdArray |
Texture2DArray with SamplerComparisonState |
@buf |
Buffer/StructuredBuffer |
@cbuf |
ConstantBuffer |
@static |
Material Texture2D with SamplerState |
@staticCube |
Material TextureCube with SamplerState |
@staticTexArray |
Material Texture2DArray with SamplerState |
@tlas |
Top-level acceleration structure (RT) |
Note
All variables declared in (vs)
stage are also visible for hlsl(<gs, hs, ds>){...}
blocks.
All variables declared in (ms)
stage are also visible for hlsl(as){...}
block.
Examples
Let’s create float4x4
matrix:
(ps) { globtm_psf@f44 = { globtm_psf_0, globtm_psf_1, globtm_psf_2, globtm_psf_3 }; }
Here, the HLSL variable of globtm_psf
will be initialized by the preshader with the values of globtm_psf_0..3
,
which are all float4
types, stored inside the global shader variable map.
It is the C++ code’s responsibility to call
set_color4(get_shader_variable_id("get_globtm_psf_X"), Point4(...));
for X=0..3
to fill the rows with adequate values. Yes, the color4
name is very unfortunate.
For (vs)
block there is a built-in globtm
shader variable available. You can declare HLSL globtm
directly from it:
(ps) { globtm@f44 = globtm; }
You can also operate on arrays
(ps) { my_arr@type[] = {element1, element2, ..., elementN}; }
Textures and samplers
Default float4
HLSL textures are defined via @tex2d, @tex3d, @texArray, @texCube, @texCubeArray
postfixes.
For example, this code
(ps) {
hlsl_texture@tex2d = some_texture;
hlsl_texarray@texArray = some_texarray;
}
will be compiled to
Texture2D hlsl_texture: register(t16);
Texture2D hlsl_texarray: register(t17);
// registers are automatically chosen by the compiler
Postfixes @smp2d, @smp3d, @smpArray, @smpCube, @smpCubeArray
ensure that a SamplerState
object gets defined with texture/textures,
assigned to the same register number.
For @shd, @shdArray
postfixes, a SamplerComparisonState
object also gets defined in addition to SamplerState
(shd
stands for shadow, as these textures are often used for shadows).
For example, this code
(ps) {
hlsl_texture@smp2d = some_texture;
hlsl_texarray@smpArray = some_texarray;
hlsl_shdtexture@shd = some_shdtexture;
}
will be compiled to
SamplerState hlsl_texture_samplerstate: register(s0);
SamplerState hlsl_texarray_samplerstate: register(s1);
SamplerState hlsl_shdtexture_samplerstate: register(s2);
SamplerComparisonState hlsl_shdtexture_cmpSampler:register(s2);
Texture2D hlsl_texture: register(t0);
Texture2DArray hlsl_texarray: register(t1);
Texture2D hlsl_shdtexture: register(t2);
Note that you can use <texture_name>_samplerstate
or <texture_name>_cmpSampler
, generated by the shader compiler, in hlsl{...}
blocks
(e.g. hlsl_shdtexture_cmpSampler
from the example).
Postfixes @tex
and @smp
define a texture of a specific type and must be followed by hlsl{...}
block
(which defines the texture type).
(ps) {
// textures without samplers
uint_texture@tex = uint_texture hlsl { Texture2D<uint> uint_texture@tex; }
float_texarray@tex = float_texarray hlsl { Texture2DArray<float> float_texarray@tex; }
// textures with samplers
uint_texture@smp = uint_texture hlsl { Texture2D<uint> uint_texture@smp; }
float_texarray@smp = float_texarray hlsl { Texture2DArray<float> float_texarray@smp; }
}
Material textures
Textures bound to a material (diffuse, normals, etc.) are called material textures.
In preshader, these textures must be treated differently than global or dynamic textures,
using @static, @staticCube, @staticTexArray
postfixes.
shader example_shader
{
texture diffuse_tex = material.texture.diffuse;
texture normal_tex = material.texture[1];
texture cube_tex = material.texture[2];
texture some_texarray = material.texture[3];
(ps) {
diffuse_tex@static = diffuse_tex;
normal_tex@static = normal_tex;
cube_tex@staticCube = cube_tex;
some_texarray@staticTexArray = some_texarray;
}
}
Material textures are automatically used as bindless textures if you are compiling for DX12;
bindless is also supported for Vulkan and PlayStation (with special -enableBindless:on
compiler flag).
Inside HLSL blocks, material textures should be referenced by their getters get_<texture_name>()
,
instead of their names:
hlsl(ps) {
float4 albedo = tex2DBindless(get_diffuse_tex(), input.diffuseTexCoord.uv);
}
Note
Even if bindless textures feature is disabled, the aformentioned syntax still applies.
In case when bindless textures are used, MaterialProperties
constant buffer will be filled with uint2
indices of such textures (first component indexes the texture, second component indexes the sampler).
These indices are then used to retrieve the corresponding texture and sampler from static_textures[]
and static_samplers[]
arrays.
This is what get_<texture_name>()
essentialy does for you.
Buffers
Buffer
and ConstantBuffer
declarations must be followed with hlsl{...}
block. For example
(ps) {
some@buf = my_buffer hlsl {
#include <myStruct.h>
StructuredBuffer<MyStruct> some@buf;
}
}
(ps) {
my_buf@cbuf = my_const_buffer hlsl {
#include <myStruct.h>
cbuffer my_buf@cbuf
{
MyStruct data;
};
}
}
Hardcoded registers
You can bind any resource to a hardcoded register, while all auto resources will not overlap with it.
Also, the always_referenced
keyword is not required, the integer variable will be saved in the dump and will be readable on the CPU side.
int reg_no = 3;
shader sh {
(ps) {
foo_vec@f4 : register(reg_no);
foo_tex@smp2d : register(reg_no);
foo_buf@buf : register(reg_no) hlsl { StructuredBuffer<uint> foo_buf@buf; };
foo_uav@uav : register(reg_no) hlsl { RWStructuredBuffer<uint> foo_uav@uav; };
}
}
Register number must be declared as a global int
variable.
Note
With this method of declaring a resource, no stcode will be generated.
Unordered Access View
Unordered Access View @uav
postfix provides a hint for the shader compiler that the resource should be bound to the appropriate u
register.
Note that such declaration must be followed with the hlsl{...}
block to define the actual type of the UAV resource.
buffer some_buffer;
texture some_texture;
shader some_shader {
(cs) {
hlsl_buffer@uav = some_buffer hlsl {
RWStructuredBuffer<uint> hlsl_buffer@uav;
}
hlsl_texture@uav = some_texture hlsl {
RWTexture2D<float4> hlsl_texture@uav;
}
}
// ...
}
Top-level Acceleration Structure
For raytracing purposes, you can also declare a TLAS (top-level acceleration structure) like this:
tlas some_tlas;
shader some_shader {
(cs) {
hlsl_tlas@tlas = some_tlas;
}
// ...
}
In HLSL terms, hlsl_tlas
will have the type RaytracingAccelerationStructure
.
Shader blocks
Shader Blocks are an extension of the Preshader idea and define variables/constants which are common for multiple shaders that support
them.
The intent is to optimize constant/texture switching.
For example:
float4 world_view_pos;
block(global_const|frame|scene|object) name_of_block
{
(ps) { world_view_pos@f3 = world_view_pos; }
(vs) { world_view_pos@f3 = world_view_pos; }
}
Note that a block
, just like a shader
, defines a preshader script.
This is basically the main gist of why blocks are useful:
they allow you to extract a part of a preshader common to multiple shaders and execute it once when setting the block, not every time a shader is executed.
In this example, world_view_pos
would be visible within pixel and vertex shader in each shader that supports this block.
Shader block layers
Specifier in block(...)
parentheses is called a layer. It indicates how often the values inside the block are supposed to change.
Available layers are:
global_const
(for global constants, supposed to change rarely)frame
(for shader variables that are supposed to change once per frame)scene
(for shader variables that are supposed to change when the rendering mode changes, within one frame)object
(for shader variables that are supposed to change for each object)
Warning
Per-object blocks are evil and should be avoided at all costs. They imply a draw-call-per-object model, which has historically proven itself antagonistic to performance.
Rendering modes mentioned in the frame
layer are defined by the user and can be specific for each shader.
For example, there are 4
scene blocks in rendinst_opaque_inc.dshl
shader, that are switched throughout the rendering of a single frame:
rendinst_scene
for color passrendinst_depth_scene
for depth passrendinst_grassify_scene
for grassify passrendinst_voxelize_scene
for voxelization pass
Using shader blocks in shaders
Syntax for using such blocks in shaders is as follows:
shader shader_name
{
supports some_block;
supports some_other_block;
hlsl(ps) {
// assuming world_view_pos was defined in one of these blocks
float3 multiplied_world_pos = 2.0 * world_view_pos;
...
}
}
With the support of multiple blocks you can use only variables from intersection of these blocks.