VirtualFunction tools
VirtualFunction tools
VirtualFunction tools
DreamShader » Tools » VirtualFunction tools
The editor actions that generate, open and refresh a
VirtualFunction declaration for an existing UMaterialFunction
asset, plus the startup service that keeps those declarations in sync with the assets they name.
| Attaches to | UMaterialFunction, UMaterialFunctionMaterialLayer, UMaterialFunctionMaterialLayerBlend |
| Menu location | Content Browser context menu ▸ DreamShader ▸ VirtualFunction, and the Material Editor DreamShader toolbar combo |
| Writes | <SourceDirectory>/VirtualFunctions/<Name>.dsh, UTF-8 without BOM |
| Since | 1.2.0 (the declaration) · 1.2.1 (the menu and the .dsh writer) · 1.2.2 (reuse, OpenVirtualFunction, startup sync) |
Context menu
The VirtualFunction section is built fresh on every right-click. Before building it, the editor
searches every project source file for a VirtualFunction declaration whose resolved asset matches
the selected UMaterialFunction. The entries you get depend on the answer.
When a declaration already exists
| Entry name | Label | Tooltip | Icon | Effect |
|---|---|---|---|---|
DreamShader.OpenVirtualFunction | OpenVirtualFunction | "Open the existing DreamShader VirtualFunction definition in VSCode." | Icons.OpenInExternalEditor | Opens the file that holds the declaration, positioned at the declaration's line and column |
DreamShader.CopyVirtualFunctionReference | Copy Virtual Function Reference | "Copy a DreamShader Graph call that references this existing VirtualFunction." | GenericCommands.Copy | Copies a call built from the declared name, not from the asset name |
No other entry is offered — in particular there is no Create, because one already exists.
When no declaration exists
| Entry name | Label | Tooltip | Icon | Effect |
|---|---|---|---|---|
DreamShader.CopyVirtualFunction | CopyVirtualFunction | "Copy a complete DreamShader VirtualFunction declaration for this Material Function." | GenericCommands.Copy | Copies the whole declaration to the clipboard |
DreamShader.CreateVirtualFunction | CreateVirtualFunction | "Create a .dsh file containing the VirtualFunction declaration." | Icons.Save | Writes the declaration to a new .dsh and opens it |
DreamShader.CopyVirtualFunctionPrototype | Copy extern Prototype (2.0) (since 2.0.0) | "Copy the 2.0 extern prototype of this Material Function, with its /// @asset line, for a .dss or a .dsh." | GenericCommands.Copy | Copies the .dss spelling of the declaration — see Editor integration |
DreamShader.CreateVirtualFunctionPrototype | Create extern Prototype (2.0) (since 2.0.0) | "Create a .dsh file holding the 2.0 extern prototype of this Material Function, for .dss sources to #include." | Icons.Save | Writes that prototype to a new .dsh and opens it |
DreamShader.CopyVirtualFunctionCall | CopyVirtualFunctionCall | "Copy a DreamShader Graph call example for this VirtualFunction." | GenericCommands.Copy | Copies a call example to the clipboard |
The match is on the declaration's resolved asset object path, compared case-insensitively; the first
match wins. Two spellings count as a declaration: a 1.x VirtualFunction block in any source, and
(since 2.0.0) an extern prototype with a /// @asset line in a .dsh or .dss. Because the
lookup reads the files, a declaration written by hand a moment ago is found immediately — nothing is
cached from startup.
[!WARNING] The lookup re-enumerates and re-reads every source (
.dsm,.dsh,.dsf,.dss,.dsi,.dsp) under every source root, outside each root'sPackagesfolder, from disk, on every right-click of a Material Function asset; a.dshor.dssthat mentions bothexternand@assetis also parsed. The cost scales with the number of source files and is paid before the context menu appears. There is no cache and no way to disable it short of-NoDreamShaderEditorBridge, which removes the menu entirely.
Actions
Runtime substitutions are shown as {Placeholder} throughout this page.
| Action | Success toast | Failure toasts |
|---|---|---|
| CopyVirtualFunction | Copied VirtualFunction definition for {Asset}. — the full text is also logged at Display | DreamShader could not find the selected Material Function. · DreamShader failed to build VirtualFunction: {Error} |
| CreateVirtualFunction | Created VirtualFunction file: {File} | asset gone · build failure · DreamShader failed to create directory: {Directory} · DreamShader failed to write VirtualFunction file: {File} · Created VirtualFunction file but could not open it: {File} |
| OpenVirtualFunction | Opened VirtualFunction definition: {File} | asset gone · DreamShader could not find a VirtualFunction definition for {Asset}. · DreamShader could not open VirtualFunction file: {File} |
| Copy Virtual Function Reference | Copied VirtualFunction reference for {Name}. — the declared name, not the asset name | asset gone · not found · DreamShader failed to build VirtualFunction reference: {Error} |
| CopyVirtualFunctionCall | Copied VirtualFunction call for {Asset}. | asset gone · DreamShader failed to build VirtualFunction call: {Error} |
[!NOTE] CreateVirtualFunction on an asset that already has a declaration silently redirects to OpenVirtualFunction. It never writes a duplicate file. The toast you get is
Opened VirtualFunction definition: {File}, not a "created" message. Create extern Prototype does the same when either spelling exists.
Where the file is written
| Aspect | Value |
|---|---|
| Directory | <SourceDirectory>/VirtualFunctions/ — DShader/VirtualFunctions/ by default |
| Preferred file name | <sanitized MaterialFunction name>.dsh |
| On collision | <sanitized name>_<crc32 of the asset's object path>.dsh, the CRC printed as eight lowercase hex digits |
| Encoding | UTF-8 without BOM |
| After writing | opened through the preferred-editor chain — VSCode, then the OS default editor, then Notepad |
A .dsh written here is picked up by the source-directory watcher like any other header, so the
materials that import it recompile automatically.
The generated declaration
VirtualFunction(Name="<SanitizedName>")
{
Options = {
Asset = <asset-literal>;
Description = "Generated from <full object path>";
}
Inputs = {
[opt ]<type> <Name>[ = <default>][ [ Description="…"; SortPriority=<n>; ] ];
…
}
Outputs = {
<type> <Name>[ [ Description="…"; SortPriority=<n>; ] ];
…
}
}The body is tab-indented. A declaration is produced only when the asset exposes at least one output.
Asset literal
| Object path begins with | Emitted as |
|---|---|
/Game/ | Path(Game, "<rest>") |
/Engine/ | Path(Engine, "<rest>") |
| a content plugin's mount point (longest match over the enabled content plugins) | Path(Plugins.<PluginName>, "<rest>") |
| anything else | the raw object path, unwrapped |
See Path for the accepted root spellings.
Input types
The mapping from Unreal's function-input types to DreamShaderLang tokens is exhaustive:
EFunctionInputType | Token |
|---|---|
FunctionInput_Scalar | float |
FunctionInput_Vector2 | float2 |
FunctionInput_Vector3 | float3 |
FunctionInput_Vector4 | float4 |
FunctionInput_Texture2D | Texture2D |
FunctionInput_TextureCube | TextureCube |
FunctionInput_Texture2DArray | Texture2DArray |
FunctionInput_VolumeTexture | VolumeTexture |
FunctionInput_MaterialAttributes | MaterialAttributes |
FunctionInput_Substrate | Substrate |
FunctionInput_StaticBool | StaticBool — kept distinct so callers can pass a static bool |
FunctionInput_Bool | bool |
| (any other value) | float4 |
Optional inputs and defaults
An input is prefixed opt when the asset marks it as using its preview value as the default. The
default literal is rendered from that preview value:
| Input type | Default literal |
|---|---|
| Scalar | the float value |
| StaticBool, Bool | true / false |
| Vector2 | float2(x, y) |
| Vector3 | float3(x, y, z) |
| Vector4 | float4(x, y, z, w) |
| anything else | (empty — the = suffix is omitted entirely) |
[!WARNING] A texture or material-attributes input marked optional gets the
optprefix but no default value, because there is no literal syntax for one. Supply the argument at the call site, or edit the declaration.
Metadata suffix
| Key | Emitted when |
|---|---|
Description | the asset's description for that parameter is non-empty |
SortPriority | the asset's sort priority differs from the parameter's own index |
Identifier sanitization
| Rule | Detail |
|---|---|
| First character | must be a letter or _; anything else is replaced with _ |
| Later characters | must be alphanumeric or _; anything else is replaced with _ |
Runs of _ | consecutive underscores collapse to one |
| All-underscore result | becomes DreamShaderSymbol |
| Empty input | becomes DreamShaderSymbol |
A name that degraded to DreamShaderSymbol | falls back to Input<N>, Output<N> or VirtualFunction<N>, <N> being the 1-based index |
| Duplicate names in one declaration | uniquified with _2, _3, … |
Non-ASCII letters are preserved — the sanitizer is Unicode-aware, not ASCII-only.
Inside emitted string literals, \, ", carriage return, line feed and tab are escaped.
The call example
CopyVirtualFunctionCall and Copy Virtual Function Reference both produce:
<FunctionName>(<arg>, <arg>, …, OutputIndex=0)| Rule | Detail |
|---|---|
| Argument per input | the input's sanitized, uniquified name |
| Optional inputs | the literal token default |
| Trailing argument | OutputIndex=0, always appended, even for a single-output function |
| Error | Cause |
|---|---|
VirtualFunction name cannot be empty. | the declared name was empty |
VirtualFunction '{Name}' does not expose any outputs. | the function exposes no outputs |
No MaterialFunction asset was provided. | a null asset reached the builder |
MaterialFunction '{Name}' does not have a valid package path. | the asset has no package to build a literal from |
MaterialFunction '{Name}' does not expose any outputs. | the declaration builder found no outputs |
The copied text is a template for 1.x text. Replace the placeholder argument names with real Graph
values, and change OutputIndex when you want a different output. See Calls.
default as an argument is a legacy rule (L25): 1.x text only, and
dsc migrate rewrites it to named arguments.
Startup sync service
Once per editor session, at bridge startup, DreamShader re-reads every VirtualFunction declaration
in the project, rebuilds it from the live asset, and writes the file back when the text changed.
[!WARNING] There is no watcher for this. Editing a
UMaterialFunction's inputs or outputs does not refresh the declaration; nor does saving the.dsh. The declarations are re-synchronized only on the next editor start.
| Aspect | Behaviour |
|---|---|
| Scanned files | every source under the writable source roots — the project's own; plugin roots are skipped — excluding each root's Packages folder |
| Refused files | any source containing preprocessor directives — DSH9001 (since 1.9.0) |
| Keyword matching | the bare word VirtualFunction, matched case-sensitively, with identifier-boundary checks |
| Skipped regions | quoted strings with \ escapes, // line comments and /* */ block comments |
| Block extraction | a balanced ( … ) followed by a balanced { … }; an optional trailing ; is absorbed into the block's range |
| Validation | each extracted block is parsed on its own by the legacy front end (since 2.0.0; the 1.x parser before) and must yield exactly one VirtualFunction, whose Options.Asset must resolve |
| Not synced | a 2.0 extern prototype with /// @asset: the lookup finds it, sync never rewrites it (since 2.0.0) |
| Comparison | on normalized text — CRLF and CR become LF, then the whole text is trimmed — so line-ending and surrounding-whitespace differences never trigger a rewrite |
| Rewrite order | replacements are applied back to front, by descending start offset, so earlier offsets stay valid |
| Write-back | the whole file, UTF-8 without BOM |
Because the scanner is a hand-rolled lexer rather than the front end, it finds declarations
anywhere a file may legally hold one — including inside a .dsm alongside a Shader block.
Conditional sources are refused (since 1.9.0)
A source containing any preprocessor directive is not synced. It is
reported once, as DSH9001, and left exactly as written.
The reason is the write-back, not the read. Sync records StartIndex / EndIndex as byte
offsets and splices the rebuilt declaration over that range. The preprocessor guarantees line-count
conservation — which buys line alignment and nothing more, because a cut line is replaced by a
shorter empty one. Every byte offset past the first cut therefore points at the wrong place, and
there is no offset map in the contract to correct with. Were a preprocessed string to reach
SaveStringToFile, every dead branch in the file would be flattened permanently.
This is the same accident as Adopt, and worse in one respect: Adopt is a menu item a person chooses, while this service runs unattended at bridge startup across every writable source in the project.
[!NOTE] The gate is reached only after a
VirtualFunctionsubstring test, so a conditional material that holds no declaration — which sync would never have rewritten anyway — does not report an error at every editor start.The read-only users of the same scanner are unaffected and keep scanning the raw text: the context menu, Open Virtual Function, and the copied call example all see the union over every branch. That matches the rule the dependency graph follows, and it is safe for the same reason — reading every branch over-reports, and over-reporting is harmless.
Sync diagnostics
Every message below is recorded with stage virtualFunctionSync, code virtual-function-sync,
source DreamShader VirtualFunction, and severity error — the only severity the sync produces.
They reach diagnostics.json, the sharded diagnostics/ directory and bridge.db.
| Message | Cause |
|---|---|
DreamShader could not read VirtualFunction source file '{File}'. | the file could not be read; reported at line 1, column 1 |
VirtualFunction attributes are missing a closing ')'. | the attribute list is unbalanced |
VirtualFunction body is missing a closing '}'. | the body braces are unbalanced |
VirtualFunction declaration is invalid: {ParserError} | the extracted block did not parse — {ParserError} is the message of the legacy front end's first error, without its code — or did not contain exactly one VirtualFunction |
VirtualFunction '{Name}' asset reference is invalid: {Error} | Options.Asset could not be resolved; the raw literal is recorded as the asset path |
VirtualFunction '{Name}' references missing MaterialFunction '{Path}'. | the asset path resolved but the object failed to load |
VirtualFunction '{Name}' could not be refreshed from MaterialFunction '{Path}': {Error} | the declaration builder failed for the live asset |
DreamShader failed to update VirtualFunction source file '{File}'. | the rewritten file could not be written; reported at line 1, column 1 |
Expected exactly one VirtualFunction block. is the parser error text carried by the fourth row when
a block contains zero or several declarations.
The conditional-source refusal obeys the paragraph
above like every other row: its record still carries stage virtualFunctionSync, code
virtual-function-sync and severity error. Its DSH9001 travels in the message text, exactly
as DSH8149 does on the Adopt gate — the wire contract is unchanged.
It is nonetheless raised through the same FailWith helper the coded diagnostics use, and that is a
deliberate trade rather than an inconsistency: gen-diagnostics.ps1
discovers codes by scanning for exactly that call shape, so a refusal written any other way would
exist in the source and not in this manual. DSH9001 is the first entry in the DSH9xxx tools
range, which was empty before it.
Reporting
| Log | When |
|---|---|
DreamShader refreshed {Count} VirtualFunction definition(s) in '{File}'. | a file was rewritten |
DreamShader scanned {Scanned} VirtualFunction definition(s), refreshed {Refreshed}, reported {Issues} issue(s). | at the end, only when something happened |
Files that hold at least one declaration and produced no diagnostics have their diagnostics cleared; files that produced diagnostics have them set.
Example
Right-click /Game/Functions/MF_Weathering (a UMaterialFunction with one scalar and one texture
input, and one output) ▸ DreamShader ▸ CreateVirtualFunction. The editor writes
<Project>/DShader/VirtualFunctions/MF_Weathering.dsh and opens it:
VirtualFunction(Name="MF_Weathering")
{
Options = {
Asset = Path(Game, "Functions/MF_Weathering");
Description = "Generated from /Game/Functions/MF_Weathering.MF_Weathering";
}
Inputs = {
opt float Amount = 0.5 [
Description="Weathering strength";
];
Texture2D Mask;
}
Outputs = {
float3 Result;
}
}CopyVirtualFunctionCall on the same asset copies:
MF_Weathering(default, Mask, OutputIndex=0)Using it from a material:
import "VirtualFunctions/MF_Weathering.dsh";
Shader(Name="Materials/M_Rock")
{
Properties = {
Texture2D MaskTex = Path(Game, "Textures/T_Mask");
}
Outputs = {
float3 Color;
Base.BaseColor = Color;
}
Graph = {
Color = MF_Weathering(0.8, MaskTex, OutputIndex=0);
}
}See also
- VirtualFunction — the declaration's grammar and its
Optionskeys - Options — every key the
Optionssection accepts - Editor integration — where the menu attaches and what else lives there
- Calls — calling a
VirtualFunctionfrom aGraph - Inputs / Outputs / Results —
opt, defaults and metadata in a declaration - Path — the
Path(Root, "…")literal theAssetkey uses - Import — how a material picks up the generated
.dsh - Preprocessor — why a conditional source is refused with
DSH9001 - Divergence — Adopt, the other source writer, refused on the same grounds
- Decompiler — the same declaration builder, used for every called function
- Bridge — where the sync diagnostics are published
- Diagnostics index — every message, by stage