DreamShaderLang
Diagnostics

Limitations

What DreamShaderLang 1.5.0 does not do yet — the Graph language's boundaries, the silent failures, the decompiler's round-trip gaps, and the function-level ceiling on material layers.

DreamShaderLang covers the material-authoring patterns that are worth keeping as reviewable text. It is not a replacement for Unreal's material editor, and it is not a shader compiler. This page is the honest list of where the edges are at 1.5.0, so you can find out here rather than three hours into a shader.

Each section links to the page that covers the topic properly. Nothing below is restated in detail.

Graph is not a general-purpose language

A Graph block builds a node graph. It has ten statement forms, four arithmetic operators, and no control flow other than if / else. There is no keyword table, so unsupported syntax is not rejected as unsupported syntax — it is misclassified.

AbsentAlternative
for, while, do, switchunroll by hand, or move the loop into a Function whose body is real HLSL
return inside Graphassign the declared output variable
Ternary ? :if / else, lerp, or a StaticSwitchParameter call
&&, ||, bitwise and shift operatorsnested if statements, or a Function
%fmod(a, b) or mod(a, b)
Compound assignment and ++ / --a = a + b
Matrix types and arraysUE.TransformVector / UE.TransformPosition, swizzles, or matrix locals inside a Function
Integer arithmeticint, uint, bool and half collapse to float widths; the integer marker exists only to reject int(a) / int(b)
C-style casts, hex/octal/binary literals, the comma operator, nested brace initializers, preprocessor directivesconstructors, decimal literals, separate statements, import

That is the design, not a backlog. Anything genuinely imperative belongs in a Function or GraphFunction, where the body is HLSL compiled into a Custom node.

The expensive part is not what is missing — it is that some of it fails silently. The Graph tokenizer maps every character it does not know to an end-of-expression token, and the parser accepts the expression that precedes it. a % b compiles as a. if (x > 0 && y > 0) compiles as if (x > 0). v[0] compiles as v. No message is produced on any surface.

What Graph Is Not is the full treatment, including the two positions that do report the problem and the parenthesis trick that forces a diagnostic.

The other silent classes

Truncation is the big one, but not the only one. These all compile clean:

WrittenWhat happens
a+=b; — no spacesa plain assignment creating a new variable literally named a+; a never changes
++a / --arepeated unary operators; ++a emits nothing, --a emits two Multiply nodes and is numerically a
If (x) { }, Else { }if and else are the only case-sensitive Graph keywords; a mis-cased spelling becomes a declaration
Shader(Name="A", Name="B")a duplicate header attribute — the last value wins
A duplicate Settings keythe last value wins. Duplicate metadata, UE builtin, Expression and Layout arguments are all hard errors
An unknown key in a material-function Settings blockignored; only Description, UserExposedCaption, ExposeToLibrary and LibraryCategories are read
An unknown or positional argument to a registered UE.* builtinignored. The generic UE.Expression path rejects both
[Texture=Path(Game,"Typo")] on a texture-sample nodethe slot is set to null and the write reports success; the material compiles with an unbound sampler
A default value on a material function's Outputs / Results entryparsed and never used
float Strength = 1.0f; or 1abc; in Propertiesboth parse as 1.0
float3(1,0,0), vec3(1,0,0), (1,0,0), Nonsense(1,0,0) as a property defaultall identical — the text before ( is ignored
A second Layout = { … } sectionreplaces the first. Properties, Inputs and Outputs append instead
An unterminated /* … */ commentconsumed to end of file and accepted

The shadowing rule is worth reading twice: the 19 math builtins and every vector constructor name are reserved. A Function, GraphFunction or property named lerp, clamp, dot or float3 still compiles and still generates its asset, but is unreachable from a Graph block, with no diagnostic.

Diagnostics have one severity, and imperfect positions

Every stored diagnostic is an error. Parse warnings — the two layer deprecations, the missing Outputs warning — never enter the store and appear only in the Output Log, so an editor extension cannot distinguish them. And line and column numbers for parse errors raised inside a section body are computed against the wrong offset base: the file is right, the position is not.

See Diagnostics for both, and for the surfaces a message reaches.

The bridge also does not run inside a commandlet or a cook, so headless runs produce no diagnostics.json, no shards and no bridge.db rows — only log lines. See Commandlet.

The decompiler is a migration aid, not a round-trip

Exporting an existing UMaterial or UMaterialFunction gives you a working starting point, not a guaranteed reproduction. The exporter has a curated case for roughly forty expression classes and a generic UE.Expression fallback for everything else, and it writes a // Warning: comment for what it could not express.

The gaps worth checking by hand before you delete an original asset:

GapEffect
Struct-, array-, map- and set-valued node properties are not reflecteda fallback UE.Expression node keeps the class default for that state, with no per-property warning
Material-function settings are never emittedDescription, ExposeToLibrary, LibraryCategories and UserExposedCaption are lost from an exported .dsf
Only a blessed UMaterial property set is emittedOpacityMaskClipValue, NumCustomizedUVs, translucency lighting mode, displacement scaling, Nanite overrides and the rest keep the class default
Node comment text and node SortPriority are droppedcomment bubbles and pin ordering are not reproduced
Material instances are rejected outrightexport the parent UMaterial, then re-create the instance
A MaterialFunctionCall on a layer or layer blend falls back to UE.Expressionthe call is not expressed as a VirtualFunction
A MaterialFunctionCall with no assigned function becomes 0.0the branch is silently constant-folded
Graph cycles emit a default literalthe cyclic branch evaluates to a constant
An append wider than four components is masked downcomponents are dropped; check the emitted swizzle
The generated Name= points into Decompiled/…recompiling creates a second asset rather than replacing the original

Treat an export as a first draft: compile it, diff the two materials in the editor, then take over the original path by editing Name= and Root=. Full detail on Decompiler.

Material layers are function-level only

ShaderLayer and ShaderLayerBlend generate the two layer function assets — UMaterialFunctionMaterialLayer and UMaterialFunctionMaterialLayerBlend. That is where the support stops.

BoundaryDetail
The layer stack is not authored from sourceassigning layers and blends to a material or material instance stays an editor operation
Layer interfaces are fixeda layer takes at most one MaterialAttributes input; a blend takes exactly two. Both produce exactly one MaterialAttributes output
Scalars, vectors and textures cannot be layer inputsexpose them through Properties; they become parameter nodes and appear on the layer stack's parameter panel
Base.MaterialAttributes and Base.FrontMaterial are mutually exclusive on one Shaderpick one
MaterialLayer / MaterialLayerBlend are deprecatedsince 1.3.0; both still generate identical assets and warn once

See Top-level Blocks for the arity rules and the generated asset shape.

Node coverage is reflection-shaped

Twenty-seven UE.* names are registered as sugar with hand-written argument handling. Everything else in the engine is reachable only through the generic UE.Expression path, which resolves a class by reflection and writes properties as literals. Three consequences:

  • Struct and array properties are awkward. They go through Unreal's own text import, so they need Unreal's literal syntax — (R=1,G=0,B=0,A=1) — and anything that syntax cannot express fails with Property '{Property}' on '{Class}' is not a supported literal type yet.
  • Class names have no U prefix. Resolution compares the reflected class name, so Sine, MaterialExpressionSine and /Script/Engine.MaterialExpressionSine resolve, while UMaterialExpressionSine never does.
  • Only the generic path dedupes. Generic UE.Expression calls, Substrate.* wrappers and the math builtins are common-subexpression cached. The 27 registered sugar builtins create a fresh node per call.

Two builtins the parser accepts have no generator implementation at all — UE.VertexNormalWS and UE.VertexTangentWS — and must be written through the generic form with an explicit OutputType.

Engine-version boundaries

Supported engines are UE 5.3 – 5.8. Features gated above the floor fail with an explicit message rather than silently:

FeatureRequires
Substrate.* calls, Substrate values, Base.FrontMaterial, ShadingModel="Substrate"since UE 5.4
UE.TransformPosition / UE.TransformVector PeriodicWorld basissince UE 5.5
UE.TransformPosition FirstPerson basis and FirstPersonInterpolationAlphasince UE 5.6
Plugin-content mount checks on Path( … ) and Root=since UE 5.6
UE.CollectionParam Group / SortPrioritysince UE 5.7 — validated then dropped below it
ShaderLayerBlend BlendInputRelevancesince UE 5.7 — not written below it

Substrate also requires Substrate to be enabled in the project, independently of the engine version.

Backend boundaries

ThinCustom is the default backend, and it lowers a Shader to an HLSL Custom node on a hidden base material. That path cannot produce Substrate values:

{File}: Material output '{Name}' expects a Substrate value and cannot be driven by a material
Custom node. Use a Graph block and Substrate.* nodes.

Set Backend = "Graph" for Substrate work. The other consequence of the default is that interactive compiles are memory-only — the generated material does not appear in the Content Browser until you Materialize it. That is deliberate, not a failure; see In-memory Materials and Backend.

Imports and packages

BoundaryDetail
Import cycles are rejecteddiamond imports are fine; a cycle fails the parse
One Shader per parse unitenforced across the whole transitive import closure, not per file
.dsh and .dsf file-kind checks are substring scansShader( inside a comment in a .dsh still rejects the file
Sources under DShader/Packages are never auto-compiledand never appear in the Gen page — see Packages
Resolution is root-relativemachine-absolute paths do not travel; keep imports project-root or package relative

What is deliberately not a goal

DreamShaderLang exists so that behaviour which must be reviewed, regenerated and shared lives in text. It is not trying to absorb the material editor. Exploratory, highly visual, one-off graphs are faster to build in Unreal — build them there, then decompile the parts that stabilized and keep those in source.

Where to next

On this page