Shader Architecture
A permutation is a compiled variant of a shader for a combination of compile-time options (#defines): skinning on/off, normal map on/off, shadow quality, MSAA, platform.
40.1 Includes & shared code
Put shared structs and functions in .hlsli headers (common.hlsli, lighting.hlsli, brdf.hlsli).
Share structs with C++: write headers that compile as both HLSL and C++ using macros (#ifdef __cplusplus → using float4 = DirectX::XMFLOAT4;), or use code generation from reflection.
Implement a custom IDxcIncludeHandler for virtual file systems and dependency tracking (hot reload when any include changes).
40.2 Variants & permutations
A permutation is a compiled variant of a shader for a combination of compile-time options (#defines): skinning on/off, normal map on/off, shadow quality, MSAA, platform. They avoid runtime branching but multiply quickly: 10 boolean features = 1,024 variants.
Managing the explosion: Only compile used combinations gather from materials/levels at cook time
Prefer uniform branches for rarely-performance-critical features one variant, a cheap coherent branch
Split features into "static" (permutation) and "dynamic" (uniform)
Unity's shader_feature vs multi_compile, Unreal's permutation domains Specialization constants (Vulkan) one SPIR-V, specialized at pipeline creation Ubershaders + bindless fewer PSOs, branch on material data Pipeline/PSO caching
ID3D12PipelineLibrary, driver caches, precompiled PSO lists to avoid hitches Distributed compilation & shader caches engines cache by hash of source + defines + compiler version
// lit.hlsl — compiled with -D HAS_NORMAL_MAP=0/1 -D HAS_SKINNING=0/1 -D SHADOW_QUALITY=0/1/2
#ifndef HAS_NORMAL_MAP
#define HAS_NORMAL_MAP 0
#endif
float3 GetNormal(VSOut i)
{
#if HAS_NORMAL_MAP
return PerturbNormal(i);
#else
return normalize(i.normalW);
#endif
}40.3 The legacy Effects framework (.fx)
Before D3D12, the Effects framework (D3DX9 Effects, Effects 10/11 — now open source as *FX11* on GitHub) bundled shaders, state, and techniques in .fx files:
The host used D3DX11CreateEffectFromMemory, effect->GetVariableByName("gWVP")->AsMatrix()->SetMatrix(...), and pass->Apply(0, ctx). Effects are not supported by DXC or D3D12; you'll still see them in older samples, tools (e.g. some DCC tools and FX Composer-era assets), and the conceptual ancestors: Unity's ShaderLab passes and engine material systems.
// effect.fx (Effects 11, compiled with fxc /T fx_5_0 — the fx_5_0 profile is deprecated)
cbuffer cbPerObject { float4x4 gWVP; };
Texture2D gDiffuse;
SamplerState gSampler { Filter = MIN_MAG_MIP_LINEAR; AddressU = Wrap; AddressV = Wrap; };
struct VSOutFX { float4 pos : SV_Position; float2 uv : TEXCOORD0; };
VSOutFX VS(float3 p : POSITION, float2 uv : TEXCOORD0)
{
VSOutFX o; o.pos = mul(float4(p, 1), gWVP); o.uv = uv; return o;
}
float4 PS(VSOutFX i) : SV_Target { return gDiffuse.Sample(gSampler, i.uv); }
RasterizerState NoCull { CullMode = None; };
technique11 Textured
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, VS()));
SetGeometryShader(NULL);
SetPixelShader(CompileShader(ps_5_0, PS()));
SetRasterizerState(NoCull);
}
}🧪 Part III Exercises
Write a geometry shader that renders a wireframe overlay using SV_Barycentrics (SM 6.1) — then do the same with a GS that outputs edge distances, and compare.
Tessellate a terrain patch grid with distance-based edge factors and height-map displacement; cull patches outside the frustum by setting tessellation factors to 0.
Implement a compute-shader separable Gaussian blur using a groupshared tile; benchmark it against the pixel-shader version with GPU timestamps.
Build a GPU particle system with emit/simulate/compact compute passes, a counter buffer updated with InterlockedAdd, and ExecuteIndirect for drawing.
Implement Cook–Torrance PBR with IBL: generate irradiance and prefiltered cube maps and the BRDF LUT in compute shaders; render a grid of spheres varying roughness and metallic.
Replace the groupshared reduction of §27.3 with wave intrinsics; make it robust to wave sizes 32 and 64.
Convert a mesh to meshlets with meshoptimizer and render it with mesh + amplification shaders, including frustum and normal-cone culling; color by meshlet.
Write a DXR path: ray generation, closest hit, two miss shaders, and ray-traced shadows; then implement the same shadows with RayQuery in a pixel shader.
Convert your renderer to SM 6.6 bindless (ResourceDescriptorHeap) with vertex pulling, and compile the same HLSL to SPIR-V for Vulkan with [[vk::binding]] and push constants.
Capture a frame in PIX (or RenderDoc), find the most expensive pass, debug one pixel's shader at source level, and reduce its VGPR count using RGA or the vendor profiler.