Shaders are regular HLSL 2021 files that live alongside C++ source code in the solution.

They are compiled to platform bytecode by EsotericaReflector.exe (invoked via the Esoterica.Scripts.Reflect project or RunReflection.bat) — DXIL for D3D12, SPIR-V for Vulkan, and so on.

The engine creates root signatures and pipelines from the bytecode at initialization.

Your First Shader

We start with the simplest possible surface shader — Downsample.esf — to show the complete example before diving into each piece.

#include "Base/Render/RHI.esh"
#include "Engine/Render/Shaders/CommonSamplers.esh"
#include "Engine/Render/Shaders/FullscreenTriangle.esh"

#ifndef __cplusplus
ESF_RESOURCE_TABLE struct DownsampleResourceTable
{
    Texture2D                   m_inputTexture;
    SamplerState                m_inputSampler;
};
#endif

#include "Engine/_Module/_AutoGenerated/ShaderReflection/Downsample.esh"

#ifndef __cplusplus

ESF_PARAMETER string ShaderType = "Surface";

#define DownsampleRootSignature    EE_BEGIN_ROOT_SIGNATURE \
                                   DownsampleResourceTableRootConstant( "b0", "space=0" )

EE_DECLARE_ROOT_CONSTANTS( DownsampleResourceTableData, b0, space0 );

[RootSignature( DownsampleRootSignature )]
float4 PS_main( FullscreenTriangleVertexOutput input ) : SV_Target
{
    DownsampleResourceTable resourceTable = CreateDownsampleResourceTable( RootConstants );

    return resourceTable.m_inputTexture.SampleLevel( resourceTable.m_inputSampler, input.m_uv, 0 );
}

#endif

Every .esf follows this structure:

  1. IncludesRHI.esh (shared CPU/GPU types), helper headers, and the auto-generated reflection header.
  2. Resource table — declared with ESF_RESOURCE_TABLE, defines the inputs the shader consumes. Wrapped in #ifndef __cplusplus because it's HLSL-only.
  3. ESF_PARAMETER — the only mandatory one is ShaderType. Tells the reflector which pipeline to build.
  4. Root signature — wires the resource table's packed data into a root constant slot.
  5. Entry pointPS_main here; MaterialShaderMain, VS_main, or CS_main for other shader types. Calls Create<Name>ResourceTable once at the top to decode the packed data.

This is the pattern for every shader in the engine. The rest of this document explains each piece in detail.

Shader Compilation — How and When

Shader compilation is separate from C++ compilation. After changing a .esf or.esh:

When EsotericaReflector.exe runs, it performs these steps for each .esf (as part of its broader C++ reflection pass):

  1. Parse — scans the file for ESF_PARAMETER, ESF_RESOURCE_TABLE, and ESF_CUSTOM_PARAMETERS declarations.
  2. Generate reflection headers — writes a .esh file under Code/Engine/_Module/_AutoGenerated/ShaderReflection/<Name>.esh containing the packed data struct, Create<Name>ResourceTable, and Load<Name>ResourceTable functions.