DreamShaderLang
DreamShaderLang 2.0

Custom Pass pipelines

.dsp — render passes of your own, written as source. Named buffers, fullscreen / compute / mesh / clear / copy passes at injection points of the renderer, and parameters activations can override, compiled to one UDreamPassPipeline. Needs UE 5.8.

Custom Pass pipelines — .dsp

DreamShader » DreamShaderLang 2.0 » Custom Pass pipelines

A .dsp file is one UDreamPassPipeline: named buffers, an ordered list of passes that each run at an injection point of the renderer of their choosing, and the parameters an activation can override. It is how you add a render pass to Unreal without touching the engine — a mask of selected objects, a blur, a screen-space effect, a compute shader that writes a data texture.

Producesone UDreamPassPipeline, named after the file, beside where a .dss of the same folder puts its product; plus one UTextureRenderTarget2D per exported buffer
Runs inthe DreamShaderPass runtime module — Unreal Engine 5.8 and later. On an older engine a .dsp still parses, formats and binds; compiling it is refused
Passesfullscreen (a Post Process material, or HLSL), compute (HLSL), mesh (selected primitives drawn again), clear, copy
Activated byProject Settings, ADreamPassVolume, UDreamPassComponent, Blueprint / C++ — see Custom Pass runtime
Since2.1.0
// DShader/Passes/CP_Highlight.dsp — selected objects get an orange outline, also behind walls
#pragma pipeline(Order = 100)

/// @group Look
uniform float4 OutlineColor = float4(1.0, 0.6, 0.0, 1.0);
/// @group Look   @slider 1 8
uniform float  OutlineWidth = 3.0;

buffer Mask    : R8(Clear = 0);
/// @desc The blurred mask; the ground material reads it too.
buffer Blurred : R8(Scale = 0.5, Export = true);

pass DrawMask : mesh
{
    Injection = AfterOpaque;
    Filter    = Layer(Highlight);
    Material  = "M_HighlightMask";
    Depth     = None;
    write Output0 = Mask;
}

pass Blur : compute
{
    Injection = AfterOpaque;
    Dispatch  = Blurred;
    read  Source = Mask;
    write Result = Blurred;
    param Radius = OutlineWidth;

    hlsl
    {
        // The statements of the entry: Id is this thread's texel, and the threads past the dispatch returned already.
        const float2 UV = (Id.xy + 0.5) * ResultSize.zw;
        const int R = (int)Radius;
        float Sum = 0;
        for (int y = -R; y <= R; ++y)
        {
            for (int x = -R; x <= R; ++x)
            {
                Sum += Source.SampleLevel(DP_LinearClamp, UV + float2(x, y) * SourceSize.zw, 0).r;
            }
        }
        Result[DP_ViewRect.xy + Id.xy] = Sum / ((2 * R + 1) * (2 * R + 1));
    }
}

pass Composite : fullscreen
{
    Injection = BeforePostProcess;
    Material  = "PP_OutlineComposite";
    read  Mask;
    read  Blurred;
    write SceneColor;
    param Color = OutlineColor;
}

The materials are ordinary .dss files: a #pragma material(Domain = PostProcess, ...) material reads the pass's buffers through UE.UserSceneTexture, and a mesh pass's material writes its outputs through UE.DreamPassOutput. See Custom Pass material nodes. An HLSL pass's code is written in the .dsp itself, as Blur's is, or in a .usf the pass names with Shader = — see HLSL passes. Complete examples, with their .dss, are in Custom Pass examples.

The file

DeclarationForm
#pragma pipeline(...)at most onePipeline keys
uniformuniform <type> <Name> [= <value>];a parameter activations can override: float–float4, int, bool, Texture2D (with /// @default <asset>). /// takes @group, @desc, @slider, @sort
static conststatic const <type> <Name> = <value>;a constant, folded where it is used
bufferbuffer <Name> : <Format>[(<Key> = <Value>, ...)];Buffers
hlslhlsl { <HLSL> }, at most oneHLSL every pass whose code is in the .dsp is compiled with — shared functions — and entries a pass picks with Entry = <function>;. See HLSL passes
passpass <Name> : <kind> { ... }Passes; they run in declaration order within an injection point

buffer, pass and hlsl are keywords in a .dsp only, and read, write, param and hlsl inside a pass only, so a .dss reads exactly as before. A .dsp has no DreamShaderLang functions and no #include of its own: HLSL functions, and the files they include, go in an hlsl block. The text of an hlsl block is HLSL for the shader compiler, kept as it is written — comments, blank lines, #if, #define and #include included; DreamShader's own preprocessor does not touch it. Comments survive dsc fmt and Adopt.

Pipeline keys

KeyValueDefault
Orderinteger — pipelines with passes at one injection point run in ascending Order, then by asset path0
Injectionthe injection point of a pass that names noneBeforePostProcess
ViewsGame, Editor, SceneCapture, PlanarReflection, ReflectionCapture, Thumbnail, joined with |Game | Editor
RequiresPostProcess, SceneResolve, CustomStencil — in a view that lacks one, the whole pipeline is off rather than half of itnone
Enableda bool uniform, or a constanttrue

Injection points

Every pass names its own; the pipeline only gives the default.

PointWhere in the frameThe scene textures there
BeginViewbefore visibility, before anything is drawnnone — no SceneColor, no SceneDepth, no View uniform buffer (see HLSL passes)
BeforeBasePassafter the depth prepassSceneDepth
AfterBasePassthe GBuffer is written, nothing is litGBuffer A–F, Velocity, SceneDepth; SceneColor holds emissive only; CustomDepth if r.CustomDepth.Order puts it first (the default with DBuffer decals)
AfterOpaquelighting, fog and sky done, translucency noteverything opaque, CustomDepth / CustomStencil
BeforePostProcesseverything but the post-process chaineverything; translucency rendered after DOF is still separate
PostProcess.BeforeDOF, PostProcess.AfterDOFinside the post-process chain, render resolutionthe chain's colour, all scene textures
PostProcess.TranslucencyAfterDOFthe after-DOF translucency, before it is compositedTranslucency is the chain here; SceneColor is read-only. The engine skips this point when there is no separate translucency
PostProcess.ReplaceTonemapperinstead of the tonemapper — one pipeline at most, and only one extension in the process gets itSceneColor, bloom; must produce the final colour
PostProcess.AfterMotionBlur, PostProcess.AfterTonemap, PostProcess.AfterFXAAafter the upscaler, output resolutionthe chain's colour
EndOfViewthe finished view, before the UIthe view family texture (LDR)

A point a view does not have — post processing turned off, a scene capture that does not resolve, path tracing for AfterBasePass — is simply not reached there; its passes are counted as skipped by DreamPass.Dump, and a buffer they would have written reads as its Clear value.

Buffers

buffer <Name> : <Format>[(<Key> = <Value>, ...)];

Formats: R8 RG8 RGBA8 R16F RG16F RGBA16F R32F RG32F RGBA32F Depth32 — Depth32 only as a mesh pass's own depth. R32U and RG32U are reserved: no pass reads or writes an integer buffer yet (DSH7321); an id fits R32F exactly up to 16777216.

KeyValueDefault
Scalerelative to Resolution1.0
Sizeint2(w, h), a fixed size instead of Scale—
ResolutionRender (before the upscaler) or Output (after it)the resolution of the first pass that writes it
Cleara scalar, float4(...), or None — with None, a read before every write is an error0
Mips1 only; other counts are rejected because passes cannot initialize or update a mip chain1
Historytrue: kept from one frame to the next, per view; <Name>.Previous reads last frame'sfalse
Exporttrue: copied, after the last pass that writes it, into a render target asset ordinary materials, Blueprints, UMG and Niagara read — see Exported buffersfalse

Built-in names need no declaration: SceneColor, SceneDepth, CustomDepth, CustomStencil, GBufferA…GBufferF, Velocity, Translucency. Where each can be read and written is in Custom Pass runtime.

Passes

/// @desc optional description
pass <Name> : <kind>
{
    <Key> = <Value>;
    read  [<name in the pass> =] <buffer>[.Previous];
    write [<name in the pass> =] <buffer>;
    param <name in the pass> = <expression>;
    hlsl { <HLSL> }
}

Keys start with a capital letter, bindings with read, write or param. read Mask; is read Mask = Mask;. hlsl { ... } — at most one, with no ; after it — is the code of an HLSL pass, written in the pass; see HLSL passes.

KindKeysread X = Bwrite Y = Bparam P = E
fullscreen with MaterialMaterialthe material's UE.UserSceneTexture named Xexactly one: write B;the material parameter P
fullscreen with HLSLShader, Entry, or an hlsl blockthe HLSL name Xup to four, SV_Target0..3 in orderthe HLSL name P
computeShader, Entry, Threads, Dispatch, or an hlsl blockthe HLSL name Xup to four UAVsthe HLSL name P
meshFilter, Material, Mode, Depth, Cull, Blend, Usage, Nanite, NaniteValue—Output0…Output3: the outputs of UE.DreamPassOutputthe override material's parameter P
clearValue—one—
copy—one, the sourceone, the target—

Every kind also takes Injection and Enabled (a bool uniform; the pass is off where it is false).

E in a param is a uniform, a static const, a literal, DreamPassWeight (the pipeline's weight in the view), or one of those times and plus constants — param Radius = OutlineWidth * 2 + 1;. Anything else is refused: the value is computed on the CPU once per view.

fullscreen

Material = "<name or object path>" runs a Post Process material through the engine's own post-process pass, so it sees exactly what a post-process volume's material would. Its buffers arrive as UE.UserSceneTexture inputs, placed as the engine places them: one slot per name, in the material's order, skipping the slots its SceneTexture nodes read (PostProcessInput0…4). A material that does not read PostProcessInput0 itself can bind five buffers, otherwise four. A material that writes a data buffer rather than the scene colour must set bDisablePreExposureScale = true in its #pragma material, or exposure scales what it reads and writes.

Without a Material, the pass runs HLSL in a pixel shader slot instead: an hlsl { ... } block of its own, Entry = <function> naming a function of the file's hlsl block, or Shader = "<file>.usf" with Entry. See HLSL passes.

compute

The code is an hlsl { ... } block of the pass's own, an Entry of the file's hlsl block, or a .usf (Shader and Entry). An entry is a function with [numthreads(x, y, z)] in front of it; Threads = uint3(...) is needed only when the group size is a macro — and for a block that holds only the statements of the entry, whose group size is Threads (uint3(8, 8, 1) when it is not written). Dispatch is <buffer> (one thread per texel), <buffer> * <scale> / <buffer> / <n>, or uint3(x, y, z). See HLSL passes.

mesh

Draws the selected primitives again into up to four buffers.

KeyValues
FilterStencil(v), Stencil(v, mask), Layer(A | B), List(Name), combined with | and & (& binds tighter)which primitives. Layers are the project's (Project Settings ▸ DreamShader Custom Pass), given to primitives by UDreamPassLayerComponent; lists are filled by UDreamPassSubsystem::AddToList
Materiala Surface material with UE.DreamPassOutputthe override material
ModeOverride (default with a material), Own (default without), OwnOrOverridewhose material draws: the override, the primitive's own (which needs UE.DreamPassOutput), or its own where it can
DepthTestScene (default), None, Own(<Depth32 buffer>)against the scene's depth, not at all, or among these primitives only
CullAuto (default), Back, Front, NoneAuto follows the primitive's own material and the primitive, also when an override draws
BlendReplace (default), Add, Max, Min, AlphaBlend
UsageStaticMesh, InstancedStaticMeshes, SkeletalMesh, Landscape, SplineMesh, GeometryCachewhat the override material needs usage flags for; checked when the pipeline compiles. Default: the first three
NaniteSkip (default for layers and lists), StencilMask (default for stencil filters), AssignStencil(<value>)see below
NaniteValuefloat4(...)what StencilMask writes

[!IMPORTANT] A Nanite primitive cannot be drawn again by a mesh pass: the engine gives Nanite geometry no mesh batches a pass could draw, and its own Nanite rasterizer is not public. Skip leaves such primitives out (one warning per pass). StencilMask writes NaniteValue wherever CustomStencil matches the pass's Stencil(...) filter — the engine's custom depth pass does rasterize Nanite, with r.CustomDepth=3 — so a mask still covers them, without their material's output. AssignStencil(v) makes that work for layers and lists too, by giving their Nanite members custom depth and stencil v; it collides with any stencil your project uses itself, which is why it is off by default.

When Mode = Override meets a primitive whose own material is masked or moves its vertices, the primitive's own material draws it if that material has UE.DreamPassOutput — otherwise the override draws it as if opaque, and a warning says so once. That is the rule the engine's custom depth pass uses.

clear and copy

clear clears its buffer to Value (a depth for Depth32). Buffers are cleared when first used anyway; clear is for clearing one again later in the frame. copy copies when format and size agree and draws scaled and converted otherwise — copy of SceneColor into a half-size buffer is a grab.

References

A Material is, as a .dsi's Parent is, either a bare name — the material of a .dss, or the instance of a .dsi, under the same source root, compiled first when it is missing or older than its source — or an object path to any material. A Shader is a path relative to the .dsp's folder — next to the .dsp is the usual place — or a virtual path such as /Project/Passes/Blur.usf; so is an #include in an hlsl block. The file needs no mapped shader directory: what the engine compiles is a snapshot of it (HLSL passes).

What the compiler checks

Everything that can be known before the frame is checked when the pipeline compiles: that every name is declared or built in; that a kind may run at its injection point and a built-in buffer may be read or written there; that no buffer is read before every write in frame order (Clear = None); that no pass reads and writes one buffer (B.Previous and B are two); that a fullscreen material reads the names its reads bind, and no more than it has slots for; that a mesh pass's material has UE.DreamPassOutput with the outputs it writes connected, and the usage flags it needs; that an HLSL file exists, its Entry does, and its bindings fit the slot (8 inputs, 4 outputs, 64 scalars of parameters); that HLSL written in the .dsp is in one place per pass, that its form and entry are what the pass runs, and that the file's hlsl block names nothing a pass's slot defines and calls no entry; that a pass writing the scene colour runs at the scene colour's resolution; that an exported buffer is not a depth or integer buffer. The codes are DSH2300–DSH2349, DSH3300–DSH3349, DSH4400–DSH4449, DSH7300–DSH7379, DSH8300–DSH8339 — see the diagnostics index.

Tools

  • dsc compile CP_X.dsp, dsc check, dsc check -Shaders (which also pre-checks every HLSL pass for the project's shader formats), dsc dump-ir, dsc index and dsc fmt take a .dsp.
  • dsc decompile /Game/.../CP_X writes Decompiled/Pipelines/Game/.../CP_X.dsp (the folder follows the asset's package path), then parses and binds what it wrote and compares it with the asset.
  • Adopt Into Source on a pipeline edited in the details panel rewrites the .dsp in place, value by value; comments and order survive.
  • dsc pass-registry lists the HLSL slots and which pass has each; -Gc frees the slots of passes that no longer exist.
  • The material browser lists .dsp files, and New Source offers three pipeline templates.

See also

On this page