DreamShaderLang
DreamShaderLang 2.0

Migrating from 1.x

dsc migrate rewrites .dsm, .dsf and .dsh sources as .dss — and proves, before it writes anything, that no comment is lost and the same graph comes out.

dsc migrate — 1.x sources to .dss

DreamShader » Tools » Migrate

dsc migrate rewrites .dsm, .dsf and .dsh files as DreamShaderLang 2.0. Nothing forces a migration: the legacy front end keeps building 1.x sources through the same compiler. Migrate when you want what only a .dss has — real expressions and control flow, #include, material instances beside the material, comments that survive tooling.

./dsc.ps1 migrate DShader/Materials/M_Foo.dsm -Check    # prove the rewrite, write nothing
./dsc.ps1 migrate DShader/Materials/M_Foo.dsm           # write M_Foo.dss, move M_Foo.dsm away
./dsc.ps1 migrate -All -Check                           # every 1.x source of the writable roots
./dsc.ps1 migrate -Root MoonToon -Check                 # one source root, by its or its plugin's name
Switch
<file>one 1.x source. A header it imports is migrated with it only when nothing else includes that header.
-Allevery 1.x source under the writable source roots (the project's). A plugin's root is left out.
-Root <name>every 1.x source of one root, named by the root's display name or its plugin's name.
-Checkdo everything except write. The exit code says whether every file would migrate.
-DryRunreport what would be written; no proofs are skipped.
-Out <dir>write the .dss files under this directory, mirroring the source tree, and leave the 1.x files alone.
-NoBackupdelete the 1.x file instead of moving it to Saved/DreamShader/Migrated/.

In a set, headers go first, and a header is migrated only when every file that includes it is migrated in the same run: a .dsh may hold both dialects, so a half-migrated project keeps building.

What is proved before anything is written

  1. No comment is lost. The comments of the source are counted against the comments of the result (DSH9092).
  2. The text builds as 2.0. It is parsed, bound and lowered by the 2.0 front end, and every call in its @custom bodies is matched against the HLSL its callee becomes — what the shader compiler would do, and what nothing else does for a header, which lowers to nothing on its own (DSH9097). A rejected text is kept under Saved/DreamShader/Migrated/Rejected/, and the diagnostics point into it.
  3. It builds the same graph. The IR of the 1.x file and the IR of the new text are compared from their roots (DSH9098, a warning with the first differing node). What is one asset written two ways does not count: a literal on a pin against its Const* twin, an identity swizzle, the spacing of Custom node code and where its source markers stand, the name the IR gives a Custom node's first output, \r\n against \n in a description, a number at float precision.
  4. The asset stays where it is. If the new file's place would name another asset path, the export gets the /// @name that keeps it (DSH9098 says so when it cannot).

A source with #if lines is refused (DSH9090): only the branch taken today would survive.

Measured on the four source roots of the development project (71 files): all 71 build as 2.0, 68 compare equal, and the three warnings are true differences — two 1.x decompiler artefacts that declared a node narrower than it is, and one explicit .x into a float pin.

What the rewrite does

The legacy front end already reads every 1.x construct into the 2.0 tree. What is left to write out is what only the legacy rules make work, because a .dss is bound without them. Each rule is also a diagnostic on the 1.x source, so dsc check on a .dsm shows what a migration will change.

1.x.dssRule
import "x.dsh";#include "x.dsh"
Shader(Name = "...") { Properties / Settings / Outputs / Graph }#pragma material(...), uniforms with /// directives, one export void M(inout material Base)
ShaderFunction inputs and outputsparameters; a first output named Result is the return value, the others are out
Function / GraphFunction/// @custom, body verbatim; UE. calls in a GraphFunction body are respelled in placeL8
Namespace(Name = "N") { Function F }N_F, with /// @name N::F so the Custom node keeps its title; N::F( calls follow
Function F(in a, out r, out o), which returns nothingT F(a, out o): the body starts T r = (T)0; and ends return r;L9
F(a, r, o) passing that first out too, in a body or a graphr = F(a, o)L9
a Function of a Namespace blockkeeps its out parametersL9
x = N::F(a, o) in a body of the same filex = N_F_r(a, o), through a @custom function added beside N_F that returns its first outL9
VirtualFunction/// @asset + extern prototype
mix, fract, mod, vec3lerp, frac, fmod, float3L2 (DSH5277)
F(a).Out, F(a, Output = "Out"), [k] on a functionone call statement with a local per output, shared by equal callsL3b
x = UE.Node(), or UE.Node() straight into a pin, with several outputsUE.Node().FirstOutputL3c (DSH5287)
F(a, b, R, O) with R the return valueR = F(a, b, O); an undeclared receiver is declaredL5 (DSH5283)
opt float S;float S = 0.0L7
SAMPLERTYPE_Normal, PPI_SceneColorNormal, SceneColorL12 (DSH5278)
a name in another casethe declaration's spellingL19 (DSH5275, DSH5276)
a float4 into a float3 place.rgbL22 (DSH5289)
a block named like a function it can callthe block's function is X_Asset, /// @name keeps the assetL23 (DSH5290)
F(a, default, c)F(a, C = c)L25
x = value; with x declared nowhereT x = value;L26 (DSH5292)
7 / 2, a / between integers 1.x divided as floatsfloat(7) / 2L27
UE.TransformPosition(P) with no DestinationUE.TransformPosition(P, Destination = World), the destination 1.x wrote
Description = "a\r\nb"the doc block's free text, one /// line per line
a property without SortPriority/// @sort 32, the priority 1.x left it at
Backend = "Instance" / ""ThinCustom / Graph

Comments stay with the statement or declaration they stood by; the comment above a 1.x block goes above the first declaration that block became.

Functions that return nothing (L9)

1.x wrote the HLSL of every Function and GraphFunction that returns nothing as returning its first out, zeroed first. That is what lets a 1.x body call one as a value — fac = Checker(co); — or pass that out as well, Voronoi(co, d, color);, which 1.x read as d = Voronoi(co, color);. A .dss function is emitted exactly as it is declared, so the migrated function is declared that way:

/// @custom
float BLChecker_fac(float3 p)
{
    float result = (float)0;
    ...
    return result;
}

and every call that passed the first out assigns it instead, in bodies and in graphs. The node a graph makes of it is the node 1.x made.

A function of a Namespace block is a library's public entry point: its out parameters are its Custom node's pins and the outputs of the material function made from it, so it keeps them. 1.x could not call one from a body (it did not rewrite N::F there); where a body of the same file reads one's value anyway, the call goes to an adapter migrate adds beside it, N_F_<first out>.

The shape of a function is decided in the file that declares it, so migrate a header before the files that include it, in place — -All and a set do. A call of a 1.x function of a header that is still 1.x is written for that function as it is now, and DSH9094 says which calls will need the new form once the header is migrated: a graph's call of a function that returns nothing, or a body that reads a Namespace function's value.

What can stop a file

DSH9090the source uses #if
DSH9091an import names a source root in front of its path
DSH9097the migrated text does not build: a construct the migrator does not handle, or a 1.x leniency with no 2.0 spelling. Fix the 1.x source, migrate again.
DSH9099the .dss already exists, or a file could not be written or moved. The 1.x file is never deleted unless the .dss was written.

See also

On this page