DreamShaderLang
Plugin referenceDreamShaderLang

GraphFunction

GraphFunction

GraphFunction

DreamShader » DreamShaderLang » GraphFunction

A top-level block whose body is HLSL, but whose UE.* calls are lifted out of the text, built as real material nodes, and wired into the call's Custom node as auto-named input pins.

Declared in.dsm, .dsf, .dsh — and inside a Namespace body
Kindtop-level block
Generatesnothing by itself. Each call site generates one UMaterialExpressionCustom node plus the material expressions lifted out of its body.
Since1.3.1

The legacy front end reads a GraphFunction as a /// @custom function whose UE. calls are lifted (legacy rule L8); see the 2.0 language and dsc migrate.

Synopsis

GraphFunction [ ( <generate-attributes> ) ] [ <return-type> ] <name> ( [ <parameter-list> ] )
{
    <hlsl-with-UE-calls>
}
<parameter-list> ::= <parameter> [ , <parameter> ] …
<parameter>      ::= [ { in | out } ] <type-token> <parameter-name>

The keyword GraphFunction is matched case-sensitively. The parameter grammar, the in / out qualifiers, the "at least one output" rule, the return-type form, the type tokens, the body's identifier normalisation, the leading #includes, the shape of the node's code and the optional (Generate = ..., Library = ..., Description = ...) header (after 2.1.2) that asks a package to generate a material function from it are identical to Function — read that page for all of them. This page documents only what differs.

How a GraphFunction differs from a Function

AspectFunctionGraphFunction
Inline / SelfContainedacceptednot accepted — see below
UE.* calls in the bodyleft as literal HLSL text, which the shader compiler does not knowbuilt as material expressions and passed in as Custom-node inputs
Body placementthe calling node's code, and a wrapper member of any node whose body calls itthe calling node's code
Callable from another Function bodyyes — it is embeddedonly when the body lifts no UE.* call; otherwise DSH6327
RecursionDSH6260DSH6260, and DSH6330 through a lifted call
Substrate parameters and resultsnever (DSH6253)never (DSH6253)

(since 2.0.0) Both put their body in the node's code: there is no shared include for a Function to be emitted into, so that row of the 1.x comparison is gone.

[!WARNING] GraphFunction SelfContained Foo(…) and GraphFunction Inline Foo(…) are DSH6307 (since 2.0.0; 1.x skipped the modifier check and read the word as a return type). GraphFunction SelfContained(…) declares a function named SelfContained. There is no self-contained mode for GraphFunction.

UE.* hoisting

When the body is read, it is scanned and every UE.* call is recorded as a lifted call. When the node's code is built, the call's text is replaced by the name of a generated input pin; the value that pin carries is the material expression the call evaluates to.

A call is lifted only when all of these hold, in order:

StepCondition
1The character before the U is an identifier boundary — not [A-Za-z0-9_], or start of text
2The next three characters are U/u, E/e, . — the prefix is matched case-insensitively in a 1.x body, so UE., ue., Ue. and uE. all trigger
3The character after the . starts an identifier: [A-Za-z_], then [A-Za-z0-9_]
4After optional whitespace, the next character is (
5A matching ) exists (the search skips "…" strings)

If step 1–4 fails, the text is copied through verbatim and silently. If step 5 fails, the body is DSH6314.

The scan skips // comments, /* */ comments, "…" strings and '…' character literals, so a UE.Time() written inside a comment is not lifted.

The extracted text — the whole UE.Foo(…) including its parentheses — is parsed as a Graph expression, so any UE.* builtin, including the generic UE.Expression(…), is available. (since 2.0.0) It is bound in the function's own scope: its parameters, replaced at each call by that call's arguments, and the file's file-scope declarations. A name only the caller has is DSH6325 — 1.x evaluated lifted calls in the caller's Graph scope; pass the value as a parameter instead. A name nobody has, a variable the body itself declares included, is DSH6326.

[!NOTE] The lift consumes exactly UE.Name( … ) up to the matching ) — nothing more. A trailing swizzle or member access stays behind as HLSL text applied to the pin, so UE.CameraVector().xy becomes _ds_<Fn>_UE0 .xy and is evaluated by the shader compiler, not by the graph builder. The pin carries the node's default output at its full component count. The pin name replaces the call's text padded with spaces to the call's length (and keeping any line break inside the call), so every later column of the body still matches the source.

[!WARNING] Only UE. is lifted. A Substrate.* call is not lifted and reaches the shader compiler as text, where it is not valid HLSL; (since 2.0.0) the front end says so with a warning, DSH6316. Substrate values cannot cross a Custom node boundary at all. Build Substrate material graphs in a ShaderFunction or directly in the Shader's Graph.

Values the hoist refuses

A lifted call has to produce something a Custom node input carries: a number, a bool, or (since 2.0.0) a texture object. A MaterialAttributes value or a Substrate material is DSH6329. A node with several outputs feeds the pin its default output.

Generated input pins

Each lifted call adds one input to the Custom node. The pin name is derived, then deduplicated:

StepRule
1Base name is __ds_<FunctionName>_UE<N>, where N counts the lifted calls of the body from 0, in source order — the same names at every call site
2The whole base name is identifier-sanitized: non-[A-Za-z0-9_] → _, then runs of consecutive underscores are collapsed to one — which turns the leading __ into a single _
3While the name collides with a declared in parameter or an earlier lifted call (compared case-insensitively), _1, _2, … is appended

So a GraphFunction WindPulse whose body contains one UE.Time() call produces a pin literally named:

_ds_WindPulse_UE0

and a namespaced Common::Pulse produces _ds_Common_Pulse_UE0.

[!NOTE] The visible pin name has a single leading underscore. __ds_ appears in the pre-sanitisation base name only. These pin names are user-visible on the generated Custom node and in any shader-compiler error that mentions them.

The declared in parameters occupy the first pins, in declaration order, under their declared names. Lifted pins follow.

Generated Custom-node code

(since 2.0.0) The code has the shape every Function node has — see Function § Generated HLSL — with each lifted call replaced by its pin's name:

<Out0Type> <Out0> = (<Out0Type>)0;      // a function without a return type: its first `out`
// Begin DreamShader source: <file>
// DreamShader custom: <Name> line <N>
<body, with every lifted UE.* call replaced by its pin name>
// End DreamShader source: <file>
return <Out0>;                         // only when the body has no top-level return
ElementRule
First outputthe returned value, or a local for the first out, declared ahead of the body and returned after it
Other out parametersthe node's additional outputs, named after the declared parameters (since 2.0.0; 1.x named them after the caller's variables). The engine declares and zero-initialises them; the body assigns them directly, so there are no writeback lines.
Bodyverbatim between the source markers
Returnappended only when the body has no top-level return

Because the body is the node's code, a return expr; returns the node's first output at any brace depth, and the additional outputs keep what the body assigned before it. See Function § return lowering.

Plain Function calls in the body are embedded as they are in a Function node.

Restrictions

RestrictionConsequence
No Inline / SelfContainedDSH6307
No recursion, direct or indirectDSH6260 for bodies that call each other; DSH6330 when a lifted call's arguments reach the function again
Called only from a Graph block when it lifts callsanother body calling it is DSH6327
Outputs carry Float1…Float4 or MaterialAttributesa texture or sampler output is DSH6254
No Substrate inputs or resultsDSH6253
An empty bodyno diagnostic; the outputs come out as 0

Argument validation

The call forms and their argument rules are a Function's: see Function § Calling a Function. (since 2.0.0) Named arguments are accepted, a value call on a multi-output function reads its first output, and the 1.x synthetic value target __ds_<Name>_value<N> no longer exists.

Notes

  • A .dsm or .dsf that contains only GraphFunction blocks generates no assets.
  • The mangled spelling DreamShaderFn_WindPulse(…) no longer names the function in a Graph block (since 2.0.0): DSH4208.
  • A Function and a GraphFunction cannot share a name: DSH4210 (since 2.0.0; 1.x reported the call as ambiguous).
  • On UE 5.3 the generated Custom node displays its code in the material graph; from UE 5.4 onward ShowCode is set to false.
  • Each lifted call is lowered on its own. Two textually identical calls in one body produce two pins, though the underlying material expressions may still be shared by node reuse.

Diagnostics

The declaration, binding, code and call diagnostics of a Function apply unchanged — see Function § Diagnostics. These are the ones a GraphFunction adds. Each carries the line and column of the construct; the code's page has the message.

Parse time

CodeRaised when
DSH6307SelfContained or Inline after GraphFunction
DSH6314a UE.* call in the body is never closed
DSH6316a Substrate.* call in the body, which is not lifted (warning)

The other parse-time codes (DSH6300–DSH6308, DSH6319) are a Function's; see Function § Parse time.

Binding

CodeRaised when
DSH6325a lifted call reads a name only the caller has
DSH6326a lifted call reads a name that is neither a parameter nor declared at file scope
DSH6329a lifted call makes a MaterialAttributes or Substrate value
DSH6330the function reaches itself through a lifted call

Custom-node code

CodeRaised when
DSH6327another body calls a function that lifts calls
DSH6328a lifted call's recorded place is not in the body (internal error)

The complete cross-stage list lives in the diagnostics index.

Example

// DShader/Lib/Motion.dsh

GraphFunction WindPulse(in float2 uv, out float pulse)
{
    float t = UE.Time();
    pulse = sin(uv.x * 8.0 + t);
}

GraphFunction float Parallax(in float2 uv, in float depth)
{
    float2 view = UE.CameraVector().xy;
    return uv.x + view.x * depth;
}

Used from a Shader:

import "Lib/Motion.dsh"

Shader(Name="Materials/M_Wind")
{
    Properties = { float Depth = 0.25; }
    Outputs    = { vec3 Color; Base.EmissiveColor = Color; }
    Graph = {
        vec2  UV = UE.TexCoord(Index = 0);
        float Pulse;
        WindPulse(UV, Pulse);
        float Shift = Parallax(UV, Depth);
        Color = vec3(Pulse, Shift, 0.0);
    }
}

Generated material for the WindPulse call:

UMaterialExpressionTextureCoordinate       →  Custom pin "uv"
UMaterialExpressionTime                    →  Custom pin "_ds_WindPulse_UE0"
UMaterialExpressionCustom  Description="WindPulse"  OutputType=CMOT_Float1

with Code:

float pulse = (float)0;
// Begin DreamShader source: DShader/Lib/Motion.dsh
// DreamShader custom: WindPulse line 4

    float t = _ds_WindPulse_UE0;
    pulse = sin(uv.x * 8.0 + t);
// End DreamShader source: DShader/Lib/Motion.dsh
return pulse;

See also

On this page