DreamShaderLang
Generation

The Pipeline

How one DreamShaderLang source file becomes Unreal assets — every stage, what triggers a build, and where each stage can fail.

Generation

DreamShader » Generation

The stage that turns a DreamShaderLang source file into Unreal assets.

Inputone .dsm or .dsf file, plus every .dsh header it imports — the same pipeline compiles .dss, .dsi and .dsp
Rejected input.dsh — a header is compiled only as part of a file that imports it (DSH8296)
ProducesUMaterial / UDreamShaderMaterialInstance, UMaterialFunction, UMaterialFunctionMaterialLayer, UMaterialFunctionMaterialLayerBlend. No .ush include (since 2.0.0) — see Generated HLSL
Runs inthe editor process only (the emitter uses editor-only material APIs)

Pipeline

(since 2.0.0) A .dsm / .dsf is read by the legacy front end into the same tree the .dss parser builds, and from there takes the one pipeline every source kind takes (Source/DreamShaderCompiler/Private/Pipeline/DreamShaderCompilePipeline.cpp). A stage reports every error it finds; a stage that reported one stops the run before the next stage. Every diagnostic has a stable code and is reported as <file>(<line>,<col>): DSHnnnn: <message> — see the diagnostics index and the code ranges. The codes below are the ones a stage raises for a 1.x source.

#StageWhat it doesFails with
1Normalize pathfull path, normalized—
2File-kind gatebuilds .dss, .dsi, .dsp, .dsm and .dsf; a .dsh never on its ownDSH8296
3Readthe file's textDSH8290
4Preprocess#if / #define against the project's define table, in the 1.x dialect for .dsm / .dsf; directive lines become empty lines, so positions do not moveDSH8291, carrying the preprocessor's own DSH1030–DSH1042
5Parsethe legacy front end reads the 1.x blocks, sections and Graph statementsDSH2200–DSH2258, DSH3250–DSH3278, DSH5250–DSH5265, DSH6300–DSH6319, and the lexer's DSH2101–DSH2106; then a texture default whose class shell names the wrong class, DSH1043 / DSH1044
6Bindnames, types, calls, settings; each imported .dsh is resolved, preprocessed and parsed on its own, and its declarations are declared into the fileDSH4200–DSH4264, DSH5200–DSH5231, the legacy rules DSH5275–DSH5292, DSH6200–DSH6211, DSH7200–DSH7233; an import that cannot be resolved, read or parsed, DSH8292–DSH8295
7Build keyhash of every imported header and of the file, plus the settings and versions that decide the output— see Caching
8Lowerthe bound tree into the graph IR, Custom-node code includedDSH4350–DSH4383, DSH6220–DSH6223, DSH6250–DSH6264, DSH6327, DSH6328
9Passes, validateconstant folding, de-duplication, dead-node pruning; the IR validatorDSH4390; DSH4300–DSH4335
10Emitone asset per product, in dependency order — see belowDSH8200–DSH8237; two exported functions that call each other, DSH8299
11Compose resultone line per product, plus a Warnings: block — see Success messages—

A .dsi resolves its parent after the parse (DSH8260–DSH8265) and a .dsp the materials and shader files its passes name; see .dsi and .dsp.

A compile that fails returns the first error as its first line, then every other diagnostic of the run: errors, then warnings, then infos. The result's code is the first error's.

The compile material entry point runs the same compile as compile assets (since 2.0.0): a source is one compile unit, and its material may call a function the same file declares, which is emitted first.

Inside the emit

One product — a Shader, ShaderFunction, ShaderLayer or ShaderLayerBlend block — in this order. The diagnostics of the asset layer reach the result inside the emitter's: the emitter's message quotes the asset layer's code and text.

#Sub-stageFails with
1Resolve the destination from Name= / Root=DSH8200 — see Asset paths
2Create or reuse the asset: the class check and the ownership guardDSH8201 (Graph material), DSH8203 (ThinCustom instance), DSH8202 (function, layer, blend)
3Another editor owns writing this project's generated assetsDSH8209, an info: skipped, not failed
4Source-hash short circuitDSH8237, an info: skipped — see Caching
5Open in an asset editorDSH8206 — see Regeneration
6DivergenceDSH8207 — see Divergence
7ThinCustom: capture the instance's parameter overrides, then find or create the hidden baseDSH8230
8Build the graph, apply settings, lay out, recompile — atomicallysee below and Regeneration
9Stamp the provenance metadata and the output digest—
10Save, unless the product is an Ephemeral ThinCustom one or writing is refusedDSH8229

The checks the 1.x generator made here — a top-level Shader present, a non-empty Outputs, a known Backend — are the front end's now: a Shader without Outputs is the warning DSH2256 and still builds, a Shader with no Graph whose Outputs compute nothing either is DSH2255, and a Backend that is no backend is DSH7202 (an empty one DSH7201; Instance is read as ThinCustom with DSH7204).

Inside the graph build

Shared by both backends. For the ThinCustom backend it runs against the hidden base material, not the emitted instance.

  1. Clear the generated DreamShader: comment boxes, then detach the existing graph — kept, so a failure puts it back (DSH8208 when it cannot be snapshotted).
  2. Reset every material property to its engine default.
  3. Apply Settings (DSH8215 for a refused one).
  4. Emit the IR graph node by node — parameters, expressions, one Custom node per Function call, a MaterialFunctionCall per ShaderFunction call — and connect each Outputs binding through a DS_<output> named-reroute pair. A node the engine could not build is DSH8210–DSH8228.
  5. Lay the graph out, in the project's Graph Layout Style.
  6. Commit — the detached graph is destroyed only now — and recompile the material.

What triggers a compile

TriggerForcedThinCustom resultScope
Auto-compile on save (file watcher + debounce)noEphemeralthe saved file; a saved .dsh queues every source that imports it
The startup sweep, and the sweep after a change to a setting the build key coversnoEphemeralevery project source
Recompile DSM, Clean Generated ShadersyesEphemeralevery project source
Material Content Browser Compile / thumbnail-refresh buttonsyesEphemeralone source
Live preview rendererthrough the bridge, yes; through the preview WebSocket, only when the request sets forceEphemeralone source
Materialize, and creating a child instance of an Ephemeral materialyesMaterializedone source
A .dsi whose parent material is missing or older than its source (DSH8264)noMaterializedthe parent's source, first
Commandlet -run=DreamShaderwith -ForceMaterializedas asked
Cook, on the cook director process onlyyesMaterializedevery project source

Auto-compile is governed by two project settings: Auto Compile On Save (default on) and Save Debounce Seconds (default 0.25, clamped to [0.05, 10.0]). See Project settings.

[!NOTE] Only a ThinCustom product can live in memory. The interactive editor keeps it there, Ephemeral, until a cook, the commandlet or an explicit Materialize puts it on disk. A Graph-backend material and every material function, layer and blend are saved by every build that is allowed to write (since 2.0.0). See Ephemeral materials.

Outcomes that are not assets

A source file can compile successfully and produce nothing to place in the Content Browser.

SourceOutcome
a .dsm / .dsf that declares only Function, GraphFunction, Namespace or VirtualFunction blockssuccess: Compiled {File}; it declares no material and no exported function, so no asset was written.
a .dsm / .dsf with no block at allfailure, DSH2254

Success messages

One line per product, in the order the products were emitted. The lines are a wire format: .skill/dsc.ps1 reads Generated <Kind> <ObjectPath> from <Source>. to list what a run wrote.

MessageEmitted for
Generated {Kind} {ObjectPath} from {File}.each product built. {Kind} is Material (either backend), MaterialFunction, MaterialLayer, MaterialLayerBlend, MaterialInstance or PassPipeline; {ObjectPath} is the object path, /Game/M.M
Generated RenderTarget {ObjectPath} from {File}.the render target of each exported buffer of a .dsp (since 2.1.0)
Skipped {ObjectPath} from {File}; source hash is unchanged (build key {BuildKey}).a product the hash short circuit skipped, of any kind — see Caching
Skipped {ObjectPath}; another editor owns this project's DreamShader bridge, and only that one writes generated assets to disk.a product another editor owns the writing of (DSH8209)
Skipped {ObjectPath} from {File}; the source marks it NoGeneration, and only a compile that names the source builds it.(after 2.1.2) a NoGeneration product a compile that did not name the file left alone (DSH8277)
Skipped {ObjectPath} from {File}; the source marks it NoGeneration, and it has never been built.(after 2.1.2) the same, with no asset on disk (DSH8278)
Compiled {File}; it declares no material and no exported function, so no asset was written.a source with no product

{File} is the full, normalized source path. Under them, a Warnings: header lists every warning of the run in wire form, then the generation warnings DSH8155, DSH9011 and DSH9012.

[!NOTE] (since 2.0.0) The 1.x lines Generated {AssetPath} from {File}., Generated DreamShader thin-custom material …, the (virtual) suffix and the helper-include line are gone: every product says Generated with its kind, and a function skipped by the hash says Skipped instead of nothing.

Runtime substitutions are rendered as {Placeholder} on this page; the compiler emits the substituted text.

Progress reporting

Generation reports through Unreal's slow-task system. The dialog is delayed so a fast compile never flashes one, carries a Cancel button, and is not shown under IsRunningCommandlet(). Resolving a source's products for the Material Content Browser runs the front half with no dialog at all.

ScopeTitleFramesDialog delay
one compileCompiling DreamShader source '{File}'...6: Reading '{File}'..., Parsing '{File}'..., Resolving names in '{File}'..., Lowering '{File}' to IR..., Validating the IR of '{File}'..., Building the graph for '{File}'...0.35 s
Classic layout (nested)Laying out DreamShader material graph...one per nodeinherited

{File} is the file name without its folder here.

Where the time goes

For a .dsm / .dsf the first five frames are the front end, and quick. The last, Building the graph for '{File}'..., holds the emit — and the emit ends in the shader compile (RecompileMaterial, a ThinCustom instance's UpdateStaticPermutation, a function's UpdateMaterialFunction), which is unbounded. (since 2.0.0) The 1.9 Step 1 of 2 / Step 2 of 2 prefixes are gone with the 1.x generator.

[!NOTE] A bar that stops moving on Building the graph for … is normal. Shader compilation happens outside the plugin, and a single heavy permutation can hold it for minutes.

Cancelling (since 1.9.0)

The cancel is checked before every stage, before every product, and inside the emit once the old graph has been detached. Cancelling takes the same path a failed compile takes:

  • The asset is restored to exactly what it held before the compile started — the atomic-rebuild rollback is what does it, so there is never a half-built graph or an emptied material function.
  • Nothing is stamped and nothing is saved.
  • The compile reports DSH8298 as an error (since 2.0.0; through 1.x DSH9010).

The one thing a cancel does not put back is the generated DreamShader: comment boxes on a material or function, for the same reason a failed rebuild does not: they are destroyed before the snapshot is taken. The next successful compile recreates them, and comments you wrote yourself are never touched.

[!WARNING] Cancelling does not stop shaders that Unreal has already queued. If you cancel during the shader compile, ShaderCompileWorker.exe keeps working through whatever was already dispatched.

When a shader compile takes too long (since 1.9.0)

If the shader compile of one asset runs past 30 seconds, generation emits DSH9011 once, as a warning in the log and in the result's Warnings: block. It never blocks or fails the compile — it exists to say what that wait usually means:

  • A Custom node whose loop bound is one of its inputs — a for or while whose limit is not a literal or a #defined constant.
  • Combined with implicit-mip texture sampling — Texture2DSample, Texture3DSample or .Sample — inside divergent control flow.

The compiler derives the mip level from screen-space derivatives, which are undefined inside divergent flow, so it fully unrolls a loop whose iteration count it cannot know. To confirm the compile is still working rather than hung, check whether ShaderCompileWorker.exe is busy in Task Manager. To fix it, bound the loop with a literal or a #define, or call SampleLevel / SampleGrad, which take the mip level as an argument.

Generation also looks for that pair while it emits a Custom node's code and warns up front, as DSH9012, before the compile is even started. Both are advisory; neither fails a build.

Pages

PageCovers
Asset pathsName= + Root= → package path → on-disk .uasset
Ephemeral materialsthe ThinCustom result, the hidden base, visibility, materializing, cook
Cachingthe build key, the metadata keys, when regeneration is skipped
Graph layouthow generated nodes are positioned
Regenerationwhat a rebuild destroys and what survives
Divergencethe output digest, and what happens when an asset was edited by hand
Generated HLSLthe HLSL a Function call puts into its Custom node
Source controlwhich generated assets have files, the two recipes for versioning them, dsc list-generated

Notes

  • The parse unit is the file. (since 2.0.0) import inlines nothing: an imported .dsh is preprocessed and parsed on its own, and its declarations are declared into the importing file. A name declared twice is DSH4210, an import cycle DSH4211. A header cannot hold an asset block (DSH2249), so "one Shader per file" (DSH2250) means the file. The build key still covers every imported byte. See import.
  • Every diagnostic carries the line and column of the construct it is about, in the file that construct is in: one raised inside an imported header names the header. Only a few file-level ones, such as an import that cannot be resolved, report line 1.
  • Products are emitted in dependency order: a function that another product of the same file calls is built first, so a Shader can call a ShaderFunction declared beside it. Otherwise the order is the order of declaration.
  • Warnings never fail a compile. They follow the result lines under a Warnings: header.
  • Generation is editor-only. There is no runtime code path that builds a material from DreamShaderLang.
  • To write the same material in the 2.0 language, see DreamShaderLang 2.0 and dsc migrate.

Example

// DShader/Materials/M_Emissive.dsm
import "Common.dsh";

ShaderFunction(Name="Functions/F_Tint")
{
    Inputs  { vec3 InColor; vec3 InTint; }
    Outputs { vec3 OutColor; }
    Graph   { OutColor = InColor * InTint; }
}

Shader(Name="Materials/M_Emissive")
{
    Properties { vec3 Tint = vec3(1.0, 0.4, 0.1); }
    Settings   { ShadingModel = "Unlit"; }
    Outputs    { vec3 Color; Base.EmissiveColor = Color; }
    Graph      { Color = F_Tint(vec3(1.0, 1.0, 1.0), Tint); }
}

One compile of that file produces:

/Game/Functions/F_Tint            UMaterialFunction, saved
/Game/Materials/M_Emissive        UDreamShaderMaterialInstance + hidden UMaterial base, Ephemeral in the editor

Result message:

Generated MaterialFunction /Game/Functions/F_Tint.F_Tint from I:/.../M_Emissive.dsm.
Generated Material /Game/Materials/M_Emissive.M_Emissive from I:/.../M_Emissive.dsm.

See also

On this page