DreamShaderLang
Tooling

Packages

DShader/Packages — the install root the plugin implements, the manifest and lock file the extensions implement, and the enumeration gap between them.

A package is a reusable DreamShaderLang library: .dsh headers, .dsf function files, examples, a README. Packages are installed under DShader/Packages and reached by import.

AspectValue
Kinddirectory convention
Path<SourceDirectory>/Packages<Project>/DShader/Packages by default
Created bythe DreamShader runtime module at startup, unconditionally
Configurableonly indirectly, through the Source Directory project setting

The plugin implements exactly two things about packages: the import root, and the exclusion of package files from source enumeration. The manifest, the lock file, the store and the install commands are conventions the editor extensions implement — no plugin C++ reads either JSON file. Read Division of labour before you debug a package problem, because it decides which repository to look in.

Layout

M_Sample.dsm
dreamshader.lock.json
dreamshader.package.json
README.md
LICENSE
Noise.dsh
PathCompiled by the plugin?
DShader/Materials/M_Sample.dsmyes — an ordinary source
DShader/Packages/**/Library/*.dshnever compiled directly; importable, and inlined into whatever imports it
DShader/Packages/**/*.dsfsee the .dsf gap — interactively yes, headlessly no
DShader/Packages/**/Examples/*.dsmnever, by any path
DShader/dreamshader.lock.jsonnot read by the plugin at all

The directory name is hard-coded: the package root is always the literal subdirectory Packages of the configured source directory. Changing Source Directory moves it; there is no separate package directory setting.

Division of labour

Everything marked extension is defined and enforced by the editor extensions in their own repositories, and cannot be observed from the plugin at all.

AspectImplemented by
Creating <SourceDirectory>/Packages at startupplugin
<SourceDirectory>/Packages as the third import resolution rootplugin
Excluding package files from source enumerationplugin
dreamshader.package.json — existence, fields, validationextension
dreamshader.lock.json — existence, contents, updatesextension
Installing, updating, removing packagesextension
Package store, index sources, GitHub topic searchextension
Package scaffolding / "create package" flowsextension
Scoped (@scope/name) namingextension

A package is resolvable purely because its files exist under <SourceDirectory>/Packages. Deleting the manifest does not break import, and adding one does not change how the plugin behaves. The manifest and the lock file exist for the extensions' installer and store.

Import resolution

An import specifier is normalized, then tried against three candidate roots in order. The first candidate that both stays inside its own root and exists on disk wins. This resolver is shared by the compiler's import inliner and the editor's dependency scanner, so both agree.

Specifier normalization

StepRule
1leading and trailing whitespace trimmed
2every \ replaced with /
3all leading ./ sequences stripped, repeatedly — ././X becomes X
4if the result has no extension, .dsh is appended

Step 4 means import "@scope/pkg/Library/Noise" and import "@scope/pkg/Library/Noise.dsh" are the same import. An extensionless specifier can therefore never resolve to a .dsf.

Candidate roots

OrderCandidate pathRoot it must stay under
1<directory of the importing file>/<specifier>the importing file's own import root — whichever of <SourceDirectory> or <SourceDirectory>/Packages contains it (longest match), or the file's own directory if neither does
2<SourceDirectory>/<specifier><SourceDirectory>
3<SourceDirectory>/Packages/<specifier><SourceDirectory>/Packages

Because candidate 1 is rooted at Packages for a file that lives inside a package, a .dsh inside a package can reach its siblings with a relative specifier but cannot climb out of the package tree with ../: the escape guard rejects that candidate, and resolution falls through to roots 2 and 3.

A specifier that resolves under no root produces DreamShader import '{Specifier}' referenced from '{File}' could not be resolved. The full grammar, cycle handling and line-number mapping are on Imports and Namespaces.

Source-file enumeration

The plugin has two enumerators. They exclude different things, and that asymmetry decides which package files are compiled.

EnumeratorExtensions scannedExcludedSorted
Full source enumeration.dsm, .dsh, .dsfeverything under <SourceDirectory>/Packagesyes
Material source enumeration.dsm, .dsfonly .dsm files under <SourceDirectory>/Packagesno

The exclusion test is a case-insensitive path-prefix comparison against the package directory, and it matches the package directory itself as well as anything beneath it at any depth.

FeatureEnumeratorSees package .dsmSees package .dsf / .dsh
Startup in-memory generationfullnono
Commandlet compile -Allfullnono
Cook-time materializationfullnono
Material Content Browser, Gen page listfullnono
VirtualFunction declaration syncfullnono
Recompile DSM / full rescan queuematerialno.dsf yes
Dependency-graph rebuildmaterialno.dsf yes

The Packages exclusion is complete for .dsm but only partial for .dsf. The auto-compile paths test for "a .dsm under Packages", which no .dsf can satisfy — so a .dsf inside a package is queued by Recompile DSM, is recompiled when the file watcher sees it saved, and participates in the dependency graph, while remaining invisible to compile -All, to cook, to the Gen page and to VirtualFunction sync.

A package that ships .dsf function assets will therefore generate those assets in an interactive editor session and not generate them in a headless build. Ship library code as .dsh headers, or copy the .dsf out of Packages into the project's own source tree.

Examples/**/*.dsm inside a package is never compiled by any path, and never appears in the Material Content Browser's Gen page. To use an example, copy it out of DShader/Packages into DShader/, or any subdirectory of it that is not Packages.

Package .dsh headers are fully importable but are never scanned for VirtualFunction declarations, so a VirtualFunction block shipped inside a package is never validated or refreshed against its UMaterialFunction asset. See Editor Tools.

dreamshader.package.json

Placed at the root of a package. Not read by the plugin. The fields below are the convention the extension's installer and store use; the extension repository is authoritative.

{
  "name": "@typedreammoon/dream-noise",
  "version": "1.0.0",
  "displayName": "Dream Noise",
  "description": "Reusable noise functions for DreamShaderLang.",
  "author": "TypeDreamMoon",
  "repository": "https://github.com/TypeDreamMoon/dream-noise",
  "license": "MIT",
  "dreamshader": {
    "language": "DreamShaderLang",
    "version": ">=1.0.0",
    "entry": "Library/Noise.dsh"
  },
  "keywords": ["noise", "fbm", "voronoi"]
}
FieldTypePurpose
namestringpackage identity; plain name or scoped @scope/name
versionstringpackage version; SemVer recommended
displayNamestringhuman-readable name shown in the store
descriptionstringshort description shown in the store
authorstringauthor attribution
repositorystringGit URL; used to update an installed package
licensestringSPDX identifier
dreamshader.languagestringlanguage identity, DreamShaderLang
dreamshader.versionstringDreamShaderLang version range the package targets
dreamshader.entrystringrecommended entry header, for documentation and store display
keywordsstring[]store search terms

A package repository becomes discoverable in the store by carrying the GitHub topic dreamshader-package.

dreamshader.lock.json

AspectValue
Path<SourceDirectory>/dreamshader.lock.json
Written bythe extension, on install and update
Read bythe extension
Recordspackage name, version, repository, commit, install path

Neither read nor written by the plugin. Deleting it changes nothing about compilation; it exists so a team can see which package revisions are checked in. Commit it if your team installs packages.

Extension commands

Command-palette entries provided by the VSCode extension. Install and update require a working git on PATH — the plugin never downloads anything.

DreamShaderLang: Install Package from GitHub
DreamShaderLang: Browse Package Store
DreamShaderLang: Update Installed Packages
DreamShaderLang: Remove Installed Package
DreamShaderLang: Open Packages Folder
DreamShaderLang: Add Package Store Index Source
DreamShaderLang: Remove Package Store Index Source
DreamShaderLang: Create Package Step by Step

Install accepts either an owner/repo shorthand or a full https://github.com/owner/repo URL. The store reads one or more index JSON files, configured through dreamshader.packageStoreIndexUrls, plus an optional GitHub topic search — see VSCode and Rider.

Authoring a package

Create the directory

Under a scoped name, so it cannot collide with anyone else's: DShader/Packages/@scope/package-name/.

Add the manifest

dreamshader.package.json at the package root, with at least name and version. The plugin does not need it; the store and the installer do.

Put the public API under Library/

One .dsh per topic, wrapped in a Namespace so the helper names cannot collide with the consuming project's own. Point dreamshader.entry at the header users should import.

Add examples under Examples/

They will never be compiled from inside the package, which is exactly what you want — they document usage without generating assets in every project that installs the package.

Publish

Push to GitHub with the topic dreamshader-package, and add an entry to a package store index if the package should be discoverable there.

A library header:

Namespace(Name = "DreamNoise")
{
    Function float Remap01(float x)
    {
        return saturate(x * 0.5 + 0.5);
    }
}

And a project material consuming it:

import "@typedreammoon/dream-noise/Library/Noise.dsh";

Shader(Name="Materials/M_Noise")
{
    Properties = {
        float Scale = 4.0 [Slider(0.1, 32)];
    }

    Outputs = {
        vec3 Color;
        Base.EmissiveColor = Color;
    }

    Graph = {
        vec2  UV = UE.TexCoord(Index = 0);
        float N  = DreamNoise::Remap01(UV.x * Scale);
        Color = vec3(N, N, N);
    }
}

Versioning

ChangeSuggested bump
Add a helper without breaking existing callsminor
Fix an implementation while keeping the APIpatch
Rename or remove a public helpermajor
Change generated output behaviour significantlymajor, plus migration notes in the README

Design guidance

PracticeReason
Keep public helpers namespacedpackage names and project names cannot collide
Export stable entry headersmaterials should not depend on your internal file layout
Ship library code as .dsh, not .dsfa package .dsf is invisible to headless builds — see above
Put examples in Examples/they document usage and are never compiled
Avoid project-specific asset pathsa package should move between projects unchanged
Document the import line in the READMEit is the only thing every user needs

Notes

  • The package directory is created at module startup even when it is empty, and even in a commandlet process, alongside the source directory and the generated-shader directory.
  • Package files are ordinary files. Nothing prevents editing a package .dsh in place — but the extension's update command overwrites it.
  • Because the exclusion is a path test, a symbolic link or junction that points a non-package path at package content is treated as whatever its resolved path is.

Example

A package installed at DShader/Packages/@typedreammoon/dream-noise/, and how its specifier resolves:

specifier   @typedreammoon/dream-noise/Library/Noise.dsh
candidate 1 <Project>/DShader/Materials/@typedreammoon/dream-noise/Library/Noise.dsh   missing
candidate 2 <Project>/DShader/@typedreammoon/dream-noise/Library/Noise.dsh             missing
candidate 3 <Project>/DShader/Packages/@typedreammoon/dream-noise/Library/Noise.dsh    resolved

Dropping the extension resolves identically:

import "@typedreammoon/dream-noise/Library/Noise";

Where next

On this page