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 Shader

  • ps – Pixel Shader

  • vs – Vertex Shader

  • ms – 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_scene for color pass

  • rendinst_depth_scene for depth pass

  • rendinst_grassify_scene for grassify pass

  • rendinst_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.

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, @u4

  • Textures: @tex2d, @tex3d, @texArray, @texCube, @texCubeArray

  • Samplers: @sampler

  • Buffers: @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

  1. 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.

  2. Incremental builds: Each .obj caches its partial refined block layout. Unchanged files skip recomputation. In multiproc mode, the preshader pass merges all partial layouts into a single rblock.layout.obj; worker processes load this merged layout.

  3. 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.

  4. 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: GLOBALVIEWPASS. 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();

Sharing blocks across shaders

The key benefit of refined blocks is that multiple shaders declaring the same #rb variables share the same cbuffer layout. For example, if shader_a and shader_b both declare my_vec@f4#rb = some_var;, the variable will be at the same cbuffer offset in both shaders. After a single flush(), the same cbuffer can drive both shaders without rebinding.

Variables unique to a single shader (e.g., shader1_only@f1#rb) are still part of the merged layout but simply unused by shaders that don’t declare them.

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.