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:
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_float4(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 @staticSmp/staticTex, @staticSmpCube/@staticTexCube ... postfixes.
Need embedded sampler? |
2D |
Cube |
Array |
3D |
Cube Array |
|---|---|---|---|---|---|
Yes |
@staticSmp |
@staticSmpCube |
@staticSmpArray |
@staticSmp3D |
@staticSmpCubeArray |
No |
@staticTex |
@staticTexCube |
@staticTexArray |
@staticTex3D |
@staticTexCubeArray |
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@staticSmp = diffuse_tex;
normal_tex@staticSmp = normal_tex;
cube_tex@staticSmpCube = cube_tex;
some_texarray@staticSmpArray = 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.
Explicit bindless resources
The @static* material textures above are an implicit bindless path: the
material system owns the resource and packs its uint2 {texture, sampler} slot
into the per-material cbuffer for you.
@bindless* postfixes are the explicit counterpart: you bind an arbitrary
resource into a bindless heap yourself and pass the shader its slot index through
an int shader variable. Texture and sampler are independent (one int slot
each), and the resource is whatever C++/daFrameGraph registered in the bindless heap
at that index, rather than a material texture slot.
Postfix |
HLSL element type |
|---|---|
@bindlessTex2D |
Texture2D |
@bindlessTex3D |
Texture3D |
@bindlessTexCube |
TextureCube |
@bindlessTexArray |
Texture2DArray |
@bindlessTexCubeArray |
TextureCubeArray |
@bindlessSampler |
SamplerState |
@bindlessByteBuffer |
ByteAddressBuffer |
The right-hand side of the declaration is an int shader variable holding the
bindless slot index; -1 means “no resource bound”.
For each @bindless* declaration the compiler generates two functions: get_<name>()
fetches the resource from its bindless heap by the slot index (returning the postfix’s
HLSL element type, so get_albedo_smp() is a SamplerState and
get_albedo_buffer() a ByteAddressBuffer), and has_<name>() returns whether
a resource is bound (slot >= 0). Optional resources must be guarded with
has_<name>() before calling get_<name>():
int albedo_bindless_index = -1;
int bindless_sampler_index = -1;
shader example_bindless
{
(ps) {
albedo_2d@bindlessTex2D = albedo_bindless_index;
albedo_smp@bindlessSampler = bindless_sampler_index;
}
hlsl(ps) {
float4 main_ps() : SV_Target0
{
if (!has_albedo_2d())
return float4(0, 0, 0, 1);
SamplerState smp = get_albedo_smp();
return get_albedo_2d().SampleLevel(smp, float2(0, 0), 0);
}
}
}
Note
Unlike @static* material textures, @bindless* has no non-bindless fallback:
it requires bindless support to be enabled for the target. Using it for a target
without bindless enabled is a compile error.
There is no sugar for StructuredBuffer<T>: it needs a per-element type, while the
shared heap is untyped. Declare such a heap by hand and pass the slot through a plain
@i1 int instead. Heap spaces 1-6 (the third argument to BINDLESS_*_ARRAY)
are reserved by the @bindless* heaps (textures 1-5, ByteAddressBuffer 6);
use 7+ for your own arrays.
int my_sbuffer_index = -1;
shader example_bindless_sbuffer
{
(ps) {
my_sbuffer_id@i1 = (my_sbuffer_index);
}
hlsl(ps) {
BINDLESS_BUF_ARRAY(StructuredBuffer<float4>, sbufArr, 7);
float4 main_ps() : SV_Target0
{
if (my_sbuffer_id < 0)
return float4(0, 0, 0, 1);
return sbufArr[my_sbuffer_id][0];
}
}
}
The int slot variable is filled on the CPU side. Manually, the slot index comes
from the bindless API in <drv/3d/dag_bindless.h> (see
Bindless Resources API):
d3d::allocate_bindless_resource_range() / d3d::update_bindless_resource() (or
the d3d::add_bindless_resource() shortcut). Pass the returned index to the shader
variable:
#include <drv/3d/dag_bindless.h>
// allocate a slot and point it at the resource
uint32_t idx = d3d::add_bindless_resource(D3DResourceType::TEX, tex);
ShaderGlobal::set_int(get_shader_variable_id("albedo_bindless_index", true), idx);
Alternatively, inside a frame graph node, daFrameGraph’s bindlessShaderVar() request
does the allocation and sets this variable for you. See
dafg::VirtualResourceRequest in Declaring Node.
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_scenefor color passrendinst_depth_scenefor depth passrendinst_grassify_scenefor grassify passrendinst_voxelize_scenefor 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.
Refined Blocks
Refined blocks are an evolution of shader blocks that guarantee identical cbuffer layout across all shaders declaring the same block. This makes it safe to pre-fill a single cbuffer once and bind it to multiple shaders without per-shader patching.
With traditional shader blocks, the compiler may assign different cbuffer offsets for the same variable in different shaders (due to varying sets of variables used per shader). Refined blocks solve this by merging all partial layouts at link time into a single canonical layout shared by every shader that participates in the block.
Motivation
In a typical rendering frame, many shaders share common parameters (view matrices, lighting data, etc.). With regular blocks, even though the values are the same, the cbuffer layout can differ per shader, requiring a flush-and-refill on each shader switch. Refined blocks eliminate this overhead: you fill the cbuffer once for all shaders that declare the same refined block.
Syntax
To include a preshader variable in a refined block, append the #rb tag to its type suffix:
shader my_shader
{
(cs)
{
my_float@f1#rb = some_global_float;
my_vector@f4#rb = some_global_float4;
my_int@i1#rb = some_global_int;
my_matrix@f44#rb = some_global_matrix;
my_tex@tex2d#rb = some_texture;
my_sampler@sampler#rb = some_sampler;
my_buf@buf#rb = some_buffer hlsl { StructuredBuffer<float> my_buf@buf; };
my_uav@uav#rb = some_uav hlsl { RWStructuredBuffer<float> my_uav@uav; };
my_cbuf@cbuf#rb = some_cbuf hlsl {
cbuffer my_cbuf@cbuf { float4 cbuf_data; };
};
}
}
Variables without #rb in the same preshader block are treated normally and are not part of the refined block.
Supported var types
The following type suffixes are supported with #rb:
Numeric:
@f1,@f2,@f3,@f4,@f44,@i1,@i2,@i3,@i4,@u1,@u2,@u3,@u4Textures:
@tex2d,@tex3d,@texArray,@texCube,@texCubeArraySamplers:
@samplerBuffers:
@buf,@uav,@cbuf
Computed expressions
Numeric refined block vars can use arbitrary arithmetic expressions on the right-hand side, not just simple variable references:
(cs)
{
computed_val@f1#rb = (some_float4.w * another_float4.z + yet_another.y - single_float);
computed_vec@f2#rb = (vec_a.x, vec_b.y);
computed_scaled@f4#rb = (some_float4 * 2);
}
The compiler generates stcode for these expressions that is executed at flush time.
How refined blocks work
Compile time: The shader compiler collects all
#rb-tagged variables across all shaders. For each unique variable name, an offset in the shared cbuffer is assigned. The merged layout is stored in the shader dump.Incremental builds: Each
.objcaches its partial refined block layout. Unchanged files skip recomputation. In multiproc mode, the preshader pass merges all partial layouts into a singlerblock.layout.obj; worker processes load this merged layout.Runtime flush: The application creates a block hierarchy, sets variable values, and calls
flush(). This executes the compiled stcode to pack all variable values into the cbuffer at their offsets. The resulting cbuffer can then be bound to any shader that uses refined block.Register allocation: Textures and buffers with explicit HLSL declarations (
@buf,@uav,@cbuf) get per-shader register allocations that are reserved so they don’t collide with other bindings. Textures without explicit HLSL type (@tex2d, etc.) use bindless resource IDs stored in the cbuffer when bindless is enabled.
Runtime C++ API
The runtime API is declared in <shaders/dag_refinedBlock.h>.
Block hierarchy
Refined blocks follow a three-layer hierarchy: GLOBAL → VIEW → PASS.
Child blocks inherit parent variable values and can shadow them locally.
#include <shaders/dag_refinedBlock.h>
auto globalBlock = refined_block::get_global();
auto viewBlock = globalBlock.refineBlock("my_view");
auto passBlock = viewBlock.refineBlock("my_pass");
Setting variables
Use VariableMap::getVariableId() to get variable IDs, then call set() on any layer:
int varF4 = VariableMap::getVariableId("my_float4_var");
globalBlock.set(varF4, Color4(1, 2, 3, 4));
int varTex = VariableMap::getVariableId("my_texture");
passBlock.set(varTex, myTexture.getBaseTex());
int varSampler = VariableMap::getVariableId("my_sampler");
passBlock.set(varSampler, samplerHandle);
int varBuf = VariableMap::getVariableId("my_buffer");
passBlock.set(varBuf, myBuffer.getBuf());
int varUav = VariableMap::getVariableId("my_uav");
passBlock.set(varUav, myUavBuffer.getBuf());
int varCbuf = VariableMap::getVariableId("my_cbuf");
passBlock.set(varCbuf, myCbufBuffer.getBuf());
Flushing and binding
Call refined_block::flush() to pack all blocks into their cbuffers, then setState()
on the pass block to bind resources before dispatch/draw:
refined_block::flush();
passBlock.setState();
myShader.dispatch(1, 1, 1);
Call refined_block::clear() to release all block data (e.g. on shutdown or full reset):
refined_block::clear();
Mixing refined and regular vars
You can freely mix #rb-tagged and regular preshader vars in the same block:
(cs)
{
shared_data@f4#rb = common_float4; // refined block var
local_data@f4 = shader_specific_float4; // regular per-shader var
}
Regular vars behave as before (per-shader cbuffer layout). Refined block vars share the layout across all participating shaders.