Regeneration
What a rebuild destroys and what survives, divergence and the three ways out, the ownership guard, and the build key that decides whether a rebuild happens at all.
A generated asset is source-derived output, not a document. When a source file is compiled again,
the generated graph is torn down and rebuilt from the .dsm or .dsf. Every hand edit inside the
asset is destroyed, with exactly one exception.
Internalize that sentence and most of this page follows from it.
| Aspect | Value |
|---|---|
| Applies to | every generated UMaterial, UDreamShaderMaterialInstance, UMaterialFunction, UMaterialFunctionMaterialLayer, UMaterialFunctionMaterialLayerBlend |
| Triggered by | any compile that is not skipped by the source hash |
| Effect | the generated graph is torn down and rebuilt from source |
A rebuild that would destroy a hand edit does not happen. since 1.8.0 An asset is fingerprinted at the end of every successful generation, and a rebuild that finds the fingerprint no longer matching stops and asks you to choose — see Divergence below.
The table that follows therefore describes what a rebuild does to an asset that is still untouched, and what the Revert to Source action does to one that is not.
What survives
| Edit | Survives regeneration |
|---|---|
a comment box whose text does not begin with DreamShader: | yes |
a comment box whose text begins with DreamShader: | no — deleted |
| nodes you added by hand | no — deleted |
| node property tweaks on generated nodes | no — the node is deleted and recreated |
| node positions | no, unless pinned by a Layout section |
| material settings changed in the editor | no — every property in Reset properties is restored to its default, then Settings is reapplied |
| parameter overrides on a generated ThinCustom instance | no — see the warning below |
FunctionInput / FunctionOutput pin identities on a material function | yes since 1.3.2 |
| named-reroute variable GUIDs | yes — regenerated only when invalid |
Sequence
- Refuse if the asset is open in an asset editor, then refuse if it has diverged — both before anything below runs, and before any of it is reversible. since 1.8.0
Modify()the target object.- Clear the generated comments — every
UMaterialExpressionCommentwhose text starts with the literalDreamShader:. - Null every material property input, from the first to the last material-property slot.
- Delete every expression in the graph.
- Reset the material to defaults — see Reset properties. Material functions skip this step.
- Apply
Settings. - Rebuild:
Propertiesnodes, theGraphbody or the whole-surfaceCustomnode, theOutputsbindings. - Lay out — runs in memory-only mode too since 1.6.0, see Project Settings.
- Recompile.
Step 4 now has one strategy: the graph leaves as a unit. The node-by-node path existed to break
inbound links, which is O(n^2) across a graph and pointless when the whole collection is detached
at once, so the former 1200-expression threshold is gone in 1.8.0 and every rebuild takes the fast
path. The material path also resets the material's editor parameter cache; the material-function path
does not.
A rebuild is atomic
since 1.8.0Steps 1–9 either all take effect or none of them do. The old graph is detached rather than destroyed, and destroyed only once the rebuild has fully succeeded; a failure at any point puts it back — nodes, connections, render state, material-function usage and pin GUIDs alike — before the compile returns. The asset a failed compile leaves behind is the asset it started with.
That matters because not every failure is caught up front. The whole-file parse, the Settings
validation and the Outputs validation are gates that run before the asset is touched at all,
but a Graph block is compiled one statement at a time by the graph builder, which runs at step 7 —
after the teardown. Until 1.8.0 such a failure left the asset emptied, which was bad for a
material and much worse for a material function, whose call sites read their pins from the live
asset: one bad .dsf took every material that called it down with it, with no undo (generated assets
are deliberately not RF_Transactional).
Two details make the rollback work. The snapshot is taken with delta serialization off — a
property equal to the class default is otherwise skipped, and the restore would leave whatever the
failed build had set it to — and it covers the asset's editor-only data object as well as the
asset, since UE 5.1 moved the expression collection and the material property inputs onto a separate
UMaterialEditorOnlyData / UMaterialFunctionEditorOnlyData UObject; snapshotting the material alone
captures nothing but a pointer to it.
The one thing a rollback does not restore is the DreamShader: comment boxes deleted at step 2 —
before the snapshot, necessarily, since a snapshot taken first would capture references to comments
that are already gone. They are regenerated decoration, and the next successful compile recreates
them. Comments you wrote are never prefixed and are never deleted in the first place.
The one hand edit that survives
A comment box whose text does not carry the DreamShader prefix.
| Aspect | Value |
|---|---|
| Prefix | DreamShader: — the word, a colon, and a single trailing space |
| Comparison | case-sensitive |
| Effect | a comment whose text starts with the prefix is deleted before the rebuild; every other comment is left untouched |
dreamshader: Notes, DREAMSHADER: Notes and DreamShader:Notes (no space) all fail the prefix
test and therefore survive. This is the supported way to annotate a generated material by hand.
The corollary: renaming a generated box from DreamShader: Sampling to Sampling makes it
permanent, and the next regeneration creates a second box named DreamShader: Sampling on top of
it. To keep DreamShader's own boxes in sync, leave their text alone and change the Comment(Name=…)
entry in the source Layout section instead.
Preserved identities
Two identities are deliberately preserved so that existing call sites do not break.
- Material function pins. Before the graph is cleared, the
IdGUID of everyUMaterialExpressionFunctionInputandUMaterialExpressionFunctionOutputis cached by name and restored onto the newly created pin with the same name. AMaterialFunctionCallnode elsewhere in the project keeps its wiring across a regeneration of the function. since 1.3.2 - Named reroutes. A declaration's variable GUID is regenerated only when the existing one is invalid.
Renaming an input or output in the source is therefore a breaking change for its call sites: the old name's GUID has nothing to restore onto.
Parameter overrides on a generated instance
Under the ThinCustom backend, regeneration calls ClearParameterValuesEditorOnly() on the emitted
UDreamShaderMaterialInstance. Every parameter override set by hand on a generated instance is
wiped on every regeneration — scalar, vector, texture, static switch and static component-mask
alike.
Since since 1.8.0 this is no longer silent: a parameter override is part of the output digest, so the next rebuild is stopped by the divergence gate and asks you to pick Revert / Adopt / Detach. Before that, no diagnostic was emitted — the values were simply gone the next time the source was compiled.
Workaround: never tune a generated instance directly. Either
- move the value into the source as a
Propertiesdefault, so the generated instance carries it, or - create a child
UMaterialInstanceConstantparented to the generated instance and override there. The child is a normal asset that regeneration never touches, and because the generated instance owns the static permutation, the child shares its shader map at no extra compile cost.
The Material Content Browser's instance-creation action produces exactly such a child, in
<parent directory>/<Instance Subfolder> — see
In-memory Materials.
Divergence
since 1.8.0The source hash fingerprints the source a compile reads. It says nothing about the asset that
compile produced. So an asset you edited by hand still looked untouched: the next time its .dsm
moved, regeneration tore the graph down and rebuilt it, and the edit was gone with no diagnostic and
no undo.
The output digest is the other half: a fingerprint of what the asset actually holds, written to
DreamShader.OutputDigest at the end of every successful generation. Comparing it against the asset
before a rebuild answers the question the source hash cannot — has somebody been working in
here?
DreamShader.SourceFile decides ownership; the digest decides whether the contents are still ours.
| State | Meaning | Effect on a rebuild |
|---|---|---|
| Foreign | no DreamShader.SourceFile — DreamShader never generated this | the ownership guard refuses; nothing is touched |
| Unstamped | ours, but carrying no comparable digest | rebuilt normally, and restamped |
| Generated | ours, and the contents match the stamp | rebuilt normally |
| Diverged | ours, and the contents no longer match | refused — see below |
Unstamped covers two cases, both benign: an asset generated before digests existed, and one whose
stamp carries a different schema tag. The tag is the digest format version plus the engine version
(DSD1-5.8), because the property set the digest walks is the engine's — without it, upgrading the
engine would re-fingerprint every asset in the project at once and report the whole library as
hand-edited.
What counts as a hand edit
The rule is the digest covers exactly what a rebuild would destroy. A property a rebuild never touches survives it untouched, so blocking on that property would refuse a rebuild in order to protect something that was never in danger.
| Change | Divergence |
|---|---|
| a node added, deleted, or rewired | yes |
| a property changed on a generated node (including a parameter's default value) | yes |
a material property input (an Outputs binding) rewired | yes |
| any property in Reset properties | yes |
a material function's Description, UserExposedCaption, ExposeToLibrary, LibraryCategories, usage | yes |
| a parameter override on a generated ThinCustom instance | yes |
| the instance's parent, or the hidden base material's graph | yes |
| a node dragged to a new position | no |
| a comment box added by hand (any text) | no |
a node's comment bubble, collapsed state, preview visibility, Desc | no |
| a named reroute's display colour | no |
| a material property outside the reset list — preview mesh, thumbnail, physical material | no |
pin Id GUIDs and named-reroute variable GUIDs | no — these are identity, deliberately carried across a rebuild |
A named reroute seeds its display colour from its own object path name, which changes every time the node is recreated. It is excluded for that reason as much as for being cosmetic: leaving it in made two rebuilds of one unchanged source disagree with each other.
What a refusal looks like
Generation fails with the asset exactly as you left it — not cleared, not half-built:
Asset '/Game/Materials/M_Emissive.M_Emissive' has been edited by hand since DreamShader generated it
from 'DShader/Materials/M_Emissive.dsm', so it was NOT rebuilt (rebuilding would destroy those
edits). Right-click the asset > DreamShader and choose one: 'Revert to Source' discards the edits and
rebuilds, 'Adopt Into Source' rewrites 'DShader/Materials/M_Emissive.dsm' from the edited asset,
'Detach From DreamShader' hands the asset over to you and stops managing it.The check runs before anything mutates the asset. For a material function that ordering is the whole point: the gate fires before the usage is restamped, not merely before the graph teardown.
This gate and the atomic rebuild above answer two different questions, and you want both. The gate stops a rebuild that would have succeeded from overwriting work you did by hand; atomicity stops a rebuild that fails from leaving the asset in pieces. Neither covers the other's case.
-Force does not get past this. bForce answers "is the source hash stale", and the editor
asserts it for every file in its own startup sweep — honouring it here would have left the gate dead
in the mode the editor spends all its time in. Only the Revert action overrides a divergence,
because only a person can make that call.
A rebuild is only ever attempted when the source moved. An unchanged source is skipped by the source hash long before the gate, so hand-editing an asset and leaving its source alone reports nothing at all — nothing is in danger.
The three ways out
All three live on the asset's right-click menu, under DreamShader, for materials, material functions, layers, layer blends, and generated ThinCustom instances alike.
| Action | What it does |
|---|---|
| Revert to Source | Discards the hand edits and rebuilds from the source, which is not modified. The "the source was right after all" answer. Reverting a saved asset rebuilds and saves it; reverting a memory-only one rebuilds it in memory — doing it the other way round would leave the edits on disk and report success, and the next session would read the same divergence straight back off the package. |
| Adopt Into Source | Rewrites the .dsm / .dsf from the asset's current contents, so the hand edits become the source of truth, then recompiles so the two agree again. The "the asset was right" answer. |
| Detach From DreamShader | Drops every DreamShader.* stamp. The asset keeps its contents and becomes an ordinary hand-authored asset that DreamShader will never rebuild. Afterwards it is Foreign, so compiling the source that used to own it fails with the ownership guard until you rename or move one of the two. Save the asset to keep the change — the detach only edits it in memory. |
Three things to know about Adopt Into Source: the previous source is copied to <source>.bak
before anything is written; it is refused when the source file declares more than one asset; and
the file's own form — hand-written comments, import directives, formatting — becomes the
decompiler's output.
That multi-asset refusal matters: the decompiler emits one block, not a translation unit, so adopting one asset out of a file that declares several would silently delete the others. Use Export DSM and merge by hand in that case.
The digest is stamped for memory-only assets too, along with the source path (but not the source
hash, which would switch the skip on). Without the path the asset reads as Foreign and the gate
never fires — which would have left it dead in the editor's default in-memory mode.
Open in an asset editor
since 1.8.0A rebuild is refused outright while the asset is open in an asset editor:
Asset '{ObjectPath}' is open in an asset editor, so it was NOT rebuilt. An open editor works on its
own copy of the asset and writes that copy back when you press Apply or Save, which would silently
undo this rebuild. Close the editor and compile again.The reason is that an asset editor does not edit the asset. FMaterialEditor duplicates it into
a transient UPreviewMaterial and copies that duplicate back over the original on Apply or Save
(UpdateOriginalMaterial); the material instance editor writes back through a
UMaterialEditorInstanceConstant wrapper. An editor that stayed open across a rebuild is therefore
holding a pre-rebuild copy of the asset, and the next Apply reverts everything the rebuild did —
which then shows up as a divergence on the compile after that, a long way from the
cause.
Refusing is the only safe answer available here. Whether that copy has unapplied edits in it is
FMaterialEditor::bMaterialDirty, which is private to the MaterialEditor module — so "close it if it
is clean, refuse if it is not" is not a question this plugin can ask. Closing it blindly is worse:
the engine's save prompt would appear in the middle of a compile-on-save, and would hang a headless
build.
The provenance actions are the exception. Revert to Source and Adopt Into Source close the editor themselves, do the work, and reopen it — because you just clicked them, quite possibly from that editor's own toolbar, and a menu item that is permanently dead where it is most useful is not a guard, it is a bug. The engine's save prompt may appear as part of that close; for Adopt it is load-bearing rather than noise, since unapplied editor changes are not part of "this asset's current contents" until the prompt is answered. Cancelling it cancels the whole action.
The ownership guard
DreamShader refuses to overwrite an asset it did not generate.
| Aspect | Value |
|---|---|
| Fires when | the target package exists on disk and the existing object carries no DreamShader.SourceFile metadata |
| Applies to | every backend and every block kind (the ThinCustom instance path since 1.8.0) |
| Result | generation fails; the existing asset is untouched |
| Message | Raised for |
|---|---|
Asset '{ObjectPath}' already exists and was not generated by DreamShader. Rename your shader or move/delete the existing asset before regenerating. | a material |
Asset '{ObjectPath}' already exists and was not generated by DreamShader. Rename your function or move/delete the existing asset before regenerating. | a material function |
The guard is inert for packages that are not on disk: a memory-only asset has no saved package to protect, so the check does not run. That gap is covered from the other side — divergence applies in memory too, because the source path is stamped there as well.
A wrong-class asset at the target path is refused by a class check instead, before the ownership question is asked at all:
Asset '{ObjectPath}' already exists and is not a DreamShader instance material. Delete it (or remove
Backend="Instance") before switching backends.Before 1.8.0 the ThinCustom instance path checked only that class, not provenance — so generating
onto a path holding a hand-authored UDreamShaderMaterialInstance adopted and rebuilt it, clearing
its parameter overrides. Since the default backend is ThinCustom, that was the widest of the three
paths and the only one without the check. It now runs the same guard as the others.
Reset properties
Before the graph is rebuilt, a material's render state is restored to these values, in this order.
Settings is applied afterwards, so any key you declare wins; anything you do not declare returns
to the value below, regardless of what the material editor last held.
| Property | Reset to |
|---|---|
BlendMode | BLEND_Opaque |
MaterialDomain | MD_Surface |
| shading model | MSM_DefaultLit |
TwoSided | false |
OpacityMaskClipValue | 0.3333 |
Wireframe | false |
DitheredLODTransition | false |
DitherOpacityMask | false |
bAllowNegativeEmissiveColor | false |
bCastDynamicShadowAsMasked | false |
bCastRayTracedShadows | true |
bEnableResponsiveAA | false |
bScreenSpaceReflections | false |
bContactShadows | false |
bDisableDepthTest | false |
bOutputTranslucentVelocity | false |
bWriteOnlyAlpha | false |
BlendableOutputAlpha | false |
TranslucencyLightingMode | TLM_VolumetricNonDirectional |
bTangentSpaceNormal | true |
bAlwaysEvaluateWorldPositionOffset | false |
bFullyRough | false |
bIsSky | false |
bIsThinSurface | false |
MaterialDecalResponse | MDR_ColorNormalRoughness |
bHasPixelAnimation since UE 5.4 | false |
NumCustomizedUVs | 0 |
bUsedWithVolumetricCloud since 1.8.0 | true when Domain="Volume", otherwise false |
bUsedWithVolumetricCloud is the one entry above that is not a constant: the domain decides it. A
Volume-domain material is a volumetric material, and the volumetric cloud renderer refuses one that
is not flagged for it — so you never have to write the flag by hand. Write it explicitly to override
the domain's choice; a Volume material that only ever feeds volumetric fog can turn the flag back
off with bUsedWithVolumetricCloud = "false"; and save the cloud shader permutations.
Material functions have no render state; their asset-level fields are reapplied instead:
| Source setting | Field | When absent |
|---|---|---|
Description | Description | cleared |
UserExposedCaption | UserExposedCaption | cleared |
ExposeToLibrary | bExposeToLibrary | set to false |
LibraryCategories | LibraryCategoriesText — comma-separated, entries trimmed, empties dropped | cleared |
The material-function usage is also re-stamped from the block kind on every regeneration.
The build key (source hash)
Regeneration is expensive, so an unchanged source skips it entirely.
| Aspect | Value |
|---|---|
| Hashed | the prepared source — the file with every import recursively inlined — plus the context that compiles it since 1.8.0 |
| Algorithm | FCrc::StrCrc32, formatted %08x — eight lowercase hex digits |
| Stored in | the generated asset's package metadata, keyed by the asset object |
| Bypassed by | the bForce flag on the generation entry points |
prepared text -> CRC32 -> "%08x" -> DreamShader.SourceHash e.g. "9f2c41ab"
source path -> project-relative, forward slashes -> DreamShader.SourceFileWhat is hashed
The hash covers the text the parser actually sees, after import inlining — not the bytes of the file on disk. That is the single most surprising property of the cache.
| Change | Changes the hash of |
|---|---|
edit M_Foo.dsm | M_Foo.dsm |
edit Common.dsh, imported by M_Foo.dsm and M_Bar.dsm | both M_Foo.dsm and M_Bar.dsm |
| move the project to another directory | nothing — the stored path is project-relative |
| rename the source file | the stored path no longer matches, so nothing is skipped |
| reformat whitespace or edit a comment | the hash — the text is compared byte for byte, not semantically |
| change Default Compiler Backend | every source since 1.8.0 |
| change a shading-model / blend-mode / domain mapping | every source since 1.8.0 |
| upgrade the plugin, or the engine | every source since 1.8.0 |
It is a build key, not just a source hash
since 1.8.0The stamp fingerprints the source in the context that compiles it. Anything that changes what a given source produces is folded in, because leaving it out makes the skip check answer "still current" about an asset that is not:
| Input | Why it is in the key |
|---|---|
| the prepared source text | the compile's actual input, imports already inlined — which is why a changed .dsh or a called .dsf needs nothing else here |
| Default Compiler Backend | decides whether a Shader block becomes a UMaterial or a thin instance |
| the mapping tables | decide what a Settings key resolves to |
| plugin version, plus a hand-bumped format tag | upgrading the generator invalidates what the old one wrote |
| engine version | what is generable moves with it (Substrate, for one) |
Before this, the key hashed the source text alone, so changing the backend left every already generated asset looking current — which is why that one setting had its own forced rebuild sweep bolted on. The sweep no longer forces: each affected asset now fails the skip check on its own, and — just as importantly — one the setting does not affect is still skipped instead of being needlessly rebuilt.
Changing the composition of the key invalidates every existing stamp. That costs one rebuild per asset, once, and is the intended effect.
A batch compiles in dependency order
since 1.8.0When a batch contains both a function and something that calls it, the batch is ordered so that
dependencies compile first. This is not a nicety: a .dsm that calls a ShaderFunction binds its
call node against the live UMaterialFunction asset — SetMaterialFunction reads the pins off the
object, not off the source — so compiling the caller first binds it against the previous version of
that function's interface. Rename a function input and save both files, and which one won used to
depend on the iteration order of the pending file map.
Both drain points — the watcher's pending-file batch and the whole-project sweep — topologically sort
by the import graph. Only edges inside the batch are honoured; a cycle is left for the import loader
to reject with DreamShader import cycle detected at '{File}'.
Editing a .dsh invalidates every dependent .dsm and .dsf, but a header never generates anything
by itself — saving it fails with DreamShader header '{File}' does not generate assets directly. Recompile dependent .dsm or .dsf files instead. The dependents are rebuilt only when they are
themselves compiled: on their own save, through Generate all in-memory materials, through the
Material Content Browser's Compile button, through the
commandlet, or at cook.
Where the metadata lives
Two keys are written into the generated asset's package metadata, keyed by the asset object. These are the only two keys DreamShader ever sets — nothing writes a generation timestamp.
| Key | Value |
|---|---|
DreamShader.SourceFile | the source path made relative to the project directory, with forward slashes. A source outside the project keeps its absolute path. |
DreamShader.SourceHash | the eight-hex-digit CRC32. Written only when non-empty. |
Storing the project-relative path is deliberate: a checkout on another machine, or a moved project
directory, still recognizes its own generated assets instead of regenerating everything.
DreamShader.SourceFile doubles as the ownership marker — its presence is what the guard above
tests, and what Clean Persisted Generated Assets filters on.
Which assets get stamped, and when:
| Asset | Stamped |
|---|---|
UDreamShaderMaterialInstance (ThinCustom) | always — memory-only and persisted alike |
the hidden MB_DreamThinBase_* base | persist mode only |
UMaterial (Graph backend) | persist mode only |
UMaterialFunction / layer / layer blend | persist mode only |
A ThinCustom instance additionally carries the source path and the hash as read-only UPROPERTYs —
SourceFilePath and SourceHash, category DreamShader — so they are visible in the details panel
without inspecting package metadata. SourceFilePath holds the full normalized source path, not
the project-relative form the metadata stores.
When regeneration is skipped
The short circuit fires only when all of the following hold:
| # | Condition |
|---|---|
| 1 | the generation call did not set bForce |
| 2 | the asset exists and the newly computed hash is non-empty |
| 3 | the stored DreamShader.SourceFile is present and non-empty |
| 4 | the stored source path equals the project-relative path of the source being compiled, ignoring case |
| 5 | the stored DreamShader.SourceHash equals the new hash, case-sensitively |
| Asset | Skip point | Extra condition | Message |
|---|---|---|---|
| ThinCustom material | after the instance is created or reused, before the hidden base is created | — | Skipped {AssetPath} from {File}; source hash is unchanged. |
Graph-backend material | after the material is created or reused | — | Skipped {AssetPath} from {File}; source hash is unchanged. |
| Material function | after the function asset is created or reused | the asset's material-function usage must already match the one the block requires | silent — the asset path is returned with no message |
Placing the ThinCustom check before the base is created is what makes a skip cheap: no base material,
no ownership check, no graph teardown. A material function whose usage does not match — a
ShaderLayer block whose asset is still marked Default, for instance — is regenerated even when
the hash matches, and the usage is corrected.
Forcing a rebuild
| Path | Force |
|---|---|
| Auto-compile on save | no — the hash short circuit is active |
| Generate all in-memory materials (backend-setting change) | yes |
| The editor's startup sweep | no since 1.8.0 — see In-memory Materials |
| Material Content Browser Compile / thumbnail refresh | yes |
| Live preview render | yes |
| Materialize, and child-instance creation | yes |
| Cook | yes |
Commandlet -run=DreamShader | only with -Force; otherwise it reports Skipped {AssetPath} from {SourceFile}; source hash is unchanged. |
There is no way to clear the stored hash from the source language. To force a rebuild without a force-capable entry point, either change the source text — any change, including whitespace — or delete the generated asset.
Notes
- Regeneration is not undoable, but it is atomic since 1.8.0: a failure puts the
asset back the way it was before the rebuild. Generated material instances are deliberately not
RF_Transactional, because undo/redo desynchronizes the shader map — which is why the rollback is a snapshot rather than a transaction. - The safest mental model: treat the
.dsm/.dsfas the asset. Anything you want to persist belongs in the source. - A regeneration that is skipped by the source hash does none of the above — the asset is not touched at all.
- Deleting the generated asset and recompiling is always equivalent to a forced regeneration, except that a material function's pin GUIDs are lost and its call sites break.
- The hash is a CRC32, not a cryptographic digest. It detects edits; it is not an integrity mechanism.
- The generated
.ushhelper include is not covered by this short circuit. It is rewritten on every compile of a unit that declaresFunctionblocks. - The decompiler is the way to capture hand edits: export the edited
material back to
.dsm/.dsf, then make that the source of truth. Since1.8.0, Adopt Into Source is exactly that as a right-click action, with the old source backed up to<source>.bakfirst.
Diagnostics
| Message | Cause | Fix |
|---|---|---|
| Asset '{ObjectPath}' already exists and was not generated by DreamShader. Rename your shader or move/delete the existing asset before regenerating. | The ownership guard, on a material. | Rename the Shader, or move or delete the hand-authored asset. |
| Asset '{ObjectPath}' already exists and was not generated by DreamShader. Rename your function or move/delete the existing asset before regenerating. | The ownership guard, on a material function. | |
| Asset '{ObjectPath}' already exists and is not a Material. | Graph backend, a non-UMaterial at the path. | Details |
| Asset '{ObjectPath}' already exists and is not a DreamShader instance material. Delete it (or remove Backend="Instance") before switching backends. | ThinCustom backend, wrong class at the path. | Details |
| Asset '{ObjectPath}' already exists and is not a MaterialFunction asset. | A function kind, wrong class at the path. | |
| Asset '{ObjectPath}' already exists as '{ActualClass}', but {Kind} generation requires '{ExpectedClass}'. Delete or move the existing asset and regenerate it. | A function kind, wrong material-function subclass. | |
| Generated DreamShader asset '{Path}' could not be saved. | The package save failed after a successful rebuild. | |
| Generated DreamShader asset packages could not be saved. | The paired instance and base save failed. | |
| In-memory material mode: '{PackageName}' already exists as a saved asset, which shadows in-memory regeneration. Delete the saved asset to make it fully in-memory. | A log warning: a saved asset shadows a memory-only rebuild. | Details |
| Asset '{ObjectPath}' has been edited by hand since DreamShader generated it from '{SourceFile}', so it was NOT rebuilt (rebuilding would destroy those edits). … | The output digest no longer matches — the asset was hand-edited. -Force does not override it. | Right-click the asset ▸ DreamShader and pick Revert to Source, Adopt Into Source, or Detach From DreamShader. |
| Asset '{ObjectPath}' is open in an asset editor, so it was NOT rebuilt. An open editor works on its own copy of the asset and writes that copy back when you press Apply or Save, which would silently undo this rebuild. Close the editor and compile again. | The target asset is open in an asset editor. | Close that editor and compile again. Revert / Adopt close it for you. |
| Skipped {AssetPath}; another editor owns this project's DreamShader bridge, and only that one writes generated assets to disk. | A second editor is open on the project and does not own the bridge. | Details |
| Skipped {AssetPath} from {File}; source hash is unchanged. | Not an error — the short circuit fired. | Pass -Force, or change the source text, to rebuild anyway. |
| DreamShader header '{File}' does not generate assets directly. Recompile dependent .dsm or .dsf files instead. | A .dsh was compiled directly. |
Worked example
Shader(Name="Docs/M_Regen")
{
Properties {
ScalarParameter Intensity = 2.0 [Group="Look"; SortPriority=10];
VectorParameter Tint = float4(1.0, 0.4, 0.1, 1.0) [Group="Look"];
}
Settings { Domain = "UI"; ShadingModel = "Unlit"; }
Outputs { vec3 Color; Base.EmissiveColor = Color; }
Graph { Color = Tint.rgb * Intensity; }
Layout { Node(Var="Color", X=-400, Y=0); }
}Hand-edit the generated asset, then save the .dsm again. Since 1.8.0 that is stopped by the
divergence gate first; the table below is what happens once you pick Revert to Source:
before regeneration after regeneration
----------------------------------------------- --------------------------------------------
comment "DreamShader: Output: EmissiveColor" recreated
comment "Reviewed 2026-07-30" KEPT — no DreamShader: prefix
extra Multiply node wired in by hand deleted
Two Sided ticked in the material editor reset to false (not declared in Settings)
Intensity override = 5.0 on the instance cleared, back to the source default 2.0
Color node dragged to (900, 400) back to (-400, 0), pinned by LayoutAnd the metadata on the generated instance:
DreamShader.SourceFile DShader/Docs/M_Regen.dsm
DreamShader.SourceHash 9f2c41ab